4.1) 뼈대 프로그램(stub program)
그럼 이제 본격적으로 NASM을 이용하여 어셈블리 프로그래밍을 해보자. 포스트에 적힌 방법대로 프로젝트를 생성하고 파일을 처음으로 만들면 다음과 같이 당황스러운 코드를 만나게 된다.
HelloWorld.asm |
%include 'handy/handy.inc'
segment .data sHelloWorld db 'Hello, world!', 10, 0
segment .text global _main
_main: push ebp mov ebp, esp
push sHelloWorld call print_string add esp, 4
mov eax, 0 mov esp, ebp pop ebp ret |
실행 결과 |
Hello, world! |
그런데 이게 정말 당황스러운 코드일까? 사실 우리는 이와 비슷한 코드를 이미 본 적이 있다. 이전 문서의 마지막 예제를 다시 가져오겠다.
ProcNaked.c |
#include "CIL.h" STRING sHelloWorld = "Hello, world!\n";
PROC(main) // naked_proc 프로시저를 호출합니다. CALL(naked_proc) ENDP
// NAKED 프로시저를 정의합니다. PROC_NAKED(naked_proc) PUSH(bp) // 이전 스택 시작 주소를 푸시하여 보관합니다. MOVL(bp, sp) // 스택 시작 주소를 현재 스택 포인터로 맞춥니다.
PUSH(sHelloWorld) INVOKE(print_str) ADD(sp, 4)
MOVL(sp, bp) // 현재 스택 포인터를 스택 시작 주소로 맞춥니다. POP(bp) // 보관했던 이전 스택 시작 주소를 불러옵니다. RET() // 복귀 지점으로 돌아갑니다. ENDP_NAKED |
naked_proc의 정의와 _main 레이블 이하의 코드를 유심히 쳐다보면, 두 코드가 거의 비슷하다는 사실을 알 수 있다! 주석을 제거하고 둘을 하나로 합쳐서 보자.
코드 비교 표 |
|
%include 'handy/handy.inc'
segment .data sHelloWorld db 'Hello, world!', 10, 0
segment .text global _main
_main: push ebp mov ebp, esp
push sHelloWorld call print_string add esp, 4
mov eax, 0 mov esp, ebp pop ebp ret |
#include "CIL.h"
STRING sHelloWorld = "Hello, world!\n";
PROC_NAKED(naked_proc) PUSH(bp) MOVL(bp, sp)
PUSH(sHelloWorld) INVOKE(print_str) ADD(sp, 4)
MOVL(sp, bp) POP(bp) RET() ENDP_NAKED |
코드가 각각의 줄에 거의 대응하도록 변경하고 살펴보니, 몇몇 차이를 제외하면 두 코드가 아주 비슷함을 알 수 있다. 우리는 CIL을 배웠고, CIL은 어셈블리를 보다 쉽게 익힐 수 있도록 고안된 언어다. 즉, CIL을 이해하고 있는 우리는 어셈블리도 어렵지 않게 익힐 수 있다. 그럼 이 뼈대 프로그램을 먼저 분석하는 것으로 어셈블리 언어의 문법에 대한 감을 잡아보자.
- %include 'handy/handy.inc'
첫 줄은 handy 폴더에 있는 handy.inc 파일을 포함하는 전처리기 지시어(preprocessor directive)다. 지시어(directive)란 소스 코드 중 실제 기계어로 변환 가능한 명령어가 아니라 소스 코드를 변환하는 프로그램에 전달하는 메시지를 말하며, 이 경우 handy.inc 파일을 포함하는 작업을 전처리기가 수행하기 때문에 %include는 전처리기에 대한 지시어가 된다.
- segment .data
3.1절에서 프로세스의 메모리를 크게 네 단계로 나눌 수 있다고 했다. segment는 소스 코드에 영역 별로 메모리를 정의하고 싶을 때 사용하는 어셈블러 지시어이고, segment 다음에 영역을 넘겨서 해당 영역에 메모리를 정의할 수 있도록 한다. 따라서 이는 이 지시어 다음에 나오는 모든 소스는 데이터 세그먼트를 정의하는 데 사용됨을 나타낸다.
- sHelloWorld db 'Hello, world!', 10, 0
문자열을 정의한다. db는 byte 형식의 데이터를 의미한다(후에 자세히 다룬다). 문자열 뒤에 정수 10이 들어가 있는 이유가 궁금할 텐데, 바로 10은 개행 문자의 ASCII 코드 값이기 때문이다. 이 문장은 파고들면 설명할 것이 아주 많이 나오지만, 지금 설명하면 독자가 혼란스럽게 느낄 수 있는 만큼 후에 자세히 다루겠다. 일단은sHelloWorld는 byte의 배열로 정의된 레이블(label)이고, 개행 문자의 ASCII 코드 값이 10이기 때문에 개행 문자를 표시하기 위해 10을 넣었다는 점, 널 문자(\0)로 문자열을 끝내기 위해 0을 마지막에 넣었다는 점만 기억하고 있으면 된다.
- segment .text
segment에 대해서는 설명했다. ‘.text’는 해당 지시어 다음에 등장하는 모든 소스가 코드 세그먼트에 대한 것이라고 어셈블러에게 전달하는 어셈블러 지시어다.
- global _main
_main 레이블이 전역에 선언된 레이블임을 의미하는 어셈블러 지시어다. 기본적으로 어셈블리 언어의 레이블은 모두 내부 선언되어있다(이는 C 언어에서 함수의 원형을 선언하면 기본적으로 전역에 선언된다는 점과 대조되는 중요한 특성이다). 따라서 이 지시어가 없이 레이블을 정의만 한 상태라면 다른 파일에서 이 레이블에 접근하는 것이 불가능하다. 즉 global은 다른 파일에서 레이블에 접근할 수 있도록 만들어준다.
- _main:
_main 프로시저를 정의한다. 어셈블리에서는 프로시저의 정의와 레이블의 정의가 서로 같은데 이에 대해서는 후에 다루겠다.
이와 같이 두 코드는 내부 구조가 완전히 동일하다. 이 프로그램을 이용하여 자신이 원하는 다른 문장을 화면에 출력하는 코드를 작성할 수 있다면 뼈대 프로그램을 사용하는 방법은 완전히 이해한 것이나 다름없다.