튜토리얼 2: MessageBox

이번 튜토리얼에서는 "Win32 assembly is great!" 을 출력하는 메세지 박스를 보여주는 완전한 Windows 프로그램을 만들 것입니다.
예제파일은 여기에서 받으실 수 있습니다.

이론:

Windows는 윈도우 프로그램을 위해 많은 리소스를 제공합니다. 그 중심에 Windows API(Application Programming Interface)가 있습니다. Windows API 는 매우 유용한 함수들을 한데 모아놓은 것이며, Windows 에 내포되어 있기 때문에 Windows 프로그램에서 바로 사용할 수 있습니다. 이러한 함수들은 kernel32.dll, user32.dll, gdi32.dll 과 같은 동적링크 라이브러리(DLLs)들에 저장되어 있습니다. kernel32.dll 에는 메모리와 프로세스를 관리하는 함수들이 있습니다. user32.dll 은 프로그램 사용자의 인터페이스(user interface) 외관을 관리합니다. gdi32.dll 은 그래픽 연산을 책임집니다. 프로그램은 이 세개의 주요 DLL 이외에도 다른 DLL을 사용 할 수 있습니다. 그리고 그것들은 API 함수들에 대해 충분한 정보를 제공합니다.

Windows 프로그램들은 이들 DLL과 동적으로 연결됩니다. 다시 말해 API 함수들의 코드는 Windows 실행 프로그램에 포함되지 않습니다. 프로그램이 실행시 API 함수를 어디서 찾아야 하는지 알려주기 위해서, 실행파일 안에 정보를 포함시켜야만 합니다. 그 정보는 임포트(import)한 라이브러리 안에 있습니다. 반드시 프로그램에 올바른 import 라이브러리를 링크시키야 하며, 그렇지 않으면 API 함수를 사용 할 수 없습니다.

Windows 프로그램이 메모리에 로딩될 때, Windows 는 프로그램 안에 기록된 정보를 읽습니다. 그 정보는 프로그램이 사용하는 함수들의 이름들과 그 함수가 있는  DLL에 대한 것입니다. Windows 은 프로그램 안에서 이런 정보들을 발견하면, 해당 DLL 을 로드하고 프로그램 안에 그 함수의 주소를 입력합니다. 이런 방식으로 함수 호출시 올바른 함수를 찾아가게 되는 것입니다.

API 함수에는 두 개의 카테고리가 있습니다. 하나는 ANSI 를 위한 것이고, 다른 하나는 unicode 를 위한 것입니다. ANSI 를 위한 API 함수의 이름에는 "A"라는 접미사가 붙습니다. 예를들면 MessageBoxA 가 있습니다. unicode를 위한 함수에는 "W"(제 생각에는 Wide Char 에서 비롯되었다고 생각합니다)라는 접미사가 붙습니다. Windows 95 는 본디 ANSI 를 지원하고, Windows NT는 unicode 를 지원합니다.

일반적으로 우리는 NULL 로 끝나는 문자열인 ANSI 문자열에 친숙합니다. ANSI 문자는 한 문자당 1byte 를 차지합니다. ANSI 코드가 유럽문자를 표현하기 위해서는 적합하지만, 수 천 가지가 넘는 다양한 문자를 사용하는 동양의 언어를 표현하기는 불가능합니다. 이 때문에 UNICODE 가 나온 것입니다. UNICODE 문자 하나는 2 byte 를 차지하며, 65536 개의 문자를 표현 할 수 있습니다.

하지만 대부분의 경우, 플랫폼에 맞는 API 함수들을 결정하고 선택적으로 포함파일(include file)을 사용할 것입니다. API 함수 이름을 조회할 때, 접두사는 빼야합니다.

예:

아래는 완전한 프로그램의 뼈대입니다. 살을 붙여나가 봅시다.

.386
.model flat, stdcall 
.data 
.code 
start: 
end start

실행은 end 지시자 바로 다음에 나오는 라벨의 명령부터 시작됩니다. 위의 뼈대에서는 start 다음 명령부터 실행이 시작 됩니다. 실행은 jmp, jne, je, ret 등 처럼 흐름제어(flow-control) 명령어을 만나기전까지는 명령에서 다음 명령으로 순차적으로 수행될 것입니다. 이런 흐름제어(flow-control) 명령들은 실행의 흐름을 다른 명령이 있는 곳으로 바꾸어 버립니다. 프로그램을 종료하여 Windows 로 빠져 나오려면, ExpitProcess 라는 API 함수를 호출해야 합니다.

ExitProcess proto uExitCode:DWORD

이 행(line)을 함수 프로토타입(prototype)이라고 합니다. 함수 프로토타입은 어셈블러/링커에게 함수의 속성들을 정의하여 타입 체크가 가능하도록 만듭니다. 함수 프로토타입의 형식은 다음과 같습니다.

FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...

간단히 말하자면, 함수 이름 다음에 PROTO 키워드가 나오고, 그 다음 파라미터의 데이타 타입들이 콤마로 구분되어 나오게 됩니다. 위의 ExitProcess 예제에서는 ExitProcess 함수가 DWORD 타입의 파라미터 하나만 갖는다고 정의되었습니다. 함수 프로토타입은 invoke 라는 고급 호출(high-level call)을 사용할 때, 매우 유용합니다. invoke 를 타입검사(type-checking)이 되는 호출정도로 생각하시면 됩니다. 예를 들어, 다음과 같이 했다고 가정합시다.

call ExitProcess

dword 크기의 값을 스택에 넣지(push) 않았지만, 어셈블러/링커는 이 오류를 잡아내지 못 합니다. 하지만 나중에 프로그램 충(crash)돌이 발생하면, 잘못 되었다는 것을 알게 되겠지요. 만약 다음처럼 했다고 해 봅시다.

invoke ExitProcess

링커(linker)는 스택에 dword 크기의 값을 넣지(push) 않은 것을 잡아 낼 것이고, 에러를 피할 수 있게 됩니다. 저는 여러분이 단순한 call 보다는 invoke 를 사용하길 권장합니다. invoke 의 문법은 다음과 같습니다.

INVOKE  expression [,arguments]

expression 에는 함수 이름 또는 함수 포인터가 올 수 있습니다. 함수 파라미터들은 콤마로 구분합니다.

대부분의 API 함수 프로토타입은 include 파일들 안에 들어 있습니다.  만약 여러분이 hutch 의 MASM32 를 사용한다면, MASM32/include 폴더 안에 있을 것입니다. include 파일들은 .inc 확장자를 가지고 있으며, DLL 파일의 함수 프로토타입은 해당 DLL과 동일 이름의 .inc 파일 안에 있습니다. 예를 들어, ExitProcess 는 kernel32.lib 안에 있습니다. 그러므로 함수의 프로토타입은 kernel32.inc 안에 있습니다.
 
또한 여러분이 만든 함수의 프로토타입도 만들 수 있습니다.
저는 예제 전반에 걸쳐, http://win32asm.cjb.net 에서 받을 수 있는 hutch의 windows.inc 를 사용할 것입니다.

자, 그럼 ExitProcess 로 다시 돌아와 봅시다, uExitCode 파라미터는 프로그램이 종료 될 때, Windows 에게 반환(return)할 값입니다.

invoke ExitProcess, 0

start 라벨 바로 다음 줄에 위 코드(line)를 넣어봅시다. 아마도 실행하자마자 종료되어 Windows로 돌아오는 Win32 프로그램이 만들어 질 것입니다. 그렇다고 해도 이것 역시 온전한 프로그램입니다.

.386 
.model flat, stdcall 
option casemap:none 
include \masm32\include\windows.inc 
include \masm32\include\kernel32.inc 
includelib \masm32\lib\kernel32.lib 
.data 
.code 
start: 
        invoke ExitProcess,0 
end start


option casemap:none 은 MASM 에게 대소문자를 구분하여 ExitProcess 와 exitprocess 를 다르게 인식하라는 것 입니다.  include 에 주목하세요. 이 지시자 바로 뒤에는 그  위치에 삽입하고 싶은 파일의 이름이 나옵니다. 위의 예제에서는, MASM 이 include \masm32\include\windows.inc행을 실행할 때, 마치 여러분이 windows.inc 파일 안에 내용을 붙여 넣은 것 처럼, \MASM\include 폴더에 있는 windows.inc 를 열어 실행할 것입니다. hutch 의 windows.inc 안에는 win32 프로그래밍에 필요한 상수와 구조체들이 정의되어 있습니다. 함수의 프로토타입은 없는게 없습니다. 그렇기에 windows.inc 는 말할 필요도 없이 방대합니다. hutch 와 저는 가능하면 많은 상수들과 구조체를 넣으려고 하고 있지만, 아직도 포함해야 할 것들이 많이 남아 있습니다. 계속해서 업데이트 할 것입니다. hutch 와 저의 홈페이지에서 업데이트를 확인하세요.
windows.inc 를 통해 여러분의 프로그램은 상수와 구조체 정의를 얻을 것입니다. 앞으로 함수 프로토타입을 위해서는,  include 파일들을 포함시키면 됩니다.. 이것들은 모두 \masm32\include 폴더 안에 있습니다.

위에 예제에서는 kernel32.dll 안에 있는 함수를 사용했기 때문에, kernel32.dll 에  있는 함수 프로토타입들을 포함시켜야만 했습니다. 그 파일은 kernel32.dll 입니다. 이 파일을 문서 편집기로 열어보면, kernel32.dll 을 위한 함수 프로토타입들로 가득한 것을 확인하실 수 있습니다. kernel32.inc 를 포함시키지 않더라도, 단순히 call 문법을 사용해서 ExitProcess 함수를 호출 할 수는 있습니다. 하지만 invoke 로 함수를 호출 할 수는 없습니다. 요점은 이렇습니다. invoke 로 함수를 호출하려면, 그 함수의 프로토타입을 소스 코드 어디엔가 넣어야 합니다.  위 예제에서, kernel32.inc 를 include 하지 않더라도, invoke 를 사용하기 전에 소스코드 어디든 ExitProcess 의 함수 프로토타입만 넣어주면 이상없이 작동됩니다. include 파일들은 프로토타입을 일일이 타이핑하지 않도록 해 주며, 필요할 때 사용하시면 됩니다.
이제 다음으로 includelib 지시자를 살펴 봅시다. includelib 는 include 와 같지는 않습니다. 이것은 어셈블러에게 프로그램에서 사용하고 있는 포함 라이브러리(import library)가 무엇인지 알려주는 방법입니다. 어셈블러가 includelib 지시자를 만나면, 링커 명령(linker command)를 object 파일에 넣어, 링커가 링크시 프로그램이 필요로 하는 포함 라이브러리가 무엇인지 알게 해 줍니다. 그렇다고 해서 includelib 를 반드시 사용해야만 하는것은 아닙니다. 링커의 커맨드 라인(command line)에 포함시켜야 하는 라이브러리의 이름을 적어 주어도 됩니다. 하지만 그렇게 하는 것은 매우 귀찮은 일일 뿐더러, 커맨드 라인은 128 문자를 초과할 수 없습니다.

이제 msgbox.asm 이라는 이름으로 예제를 저장해 봅시다. ml.exe 가 있는 경로여야 합니다. msgbox.asm 을 다음과 같이 어셈블합니다.

ml  /c  /coff  /Cp msgbox.asm
  • /c 는 MASM 이 어셈블만 하도록 합니다. link.exe 는 호출하지 말라는 것이죠. 대부분의 경우, 대부분의 경우 link.exe 를 호출하기 전에 몇 가지 작업들을 해야 하기 때문에, link.exe 가 자동으로 호출되는 것을 기피할 것입니다.

    /coff 는 MASM 이 .obj 파일을 COFF 포맷으로 만도록 합니다. MASM은 object 와 실행파일 포맷(format)으로 유닉스에서 사용되는 COFF(Common Object File Format)의 변형을 사용합니다.
     
    /Cp 는 MASM 이 식별자의 대소문자를 구분하도록 합니다. 만약 여러분이 hutch 의 MASM32 를 사용한다면, 동일한 효과를 얻기 위해서 아마도 소스코드 상단 .model 바로 아래에 "option casemap:none" 이라고 했을 것입니다.
msgbox.asm 을 성공적으로 어셈블리 했다면, msgbox.obj 가 생겼을 것입니다. msgbox.obj 는 오브젝트 파일입니다. 오브젝트 파일은 실행파일을 만들기 위한 첫 단계입니다. 이 안에는 명령어/데이타들이 바이너리 형태로 들어 있습니다. 이제 linker 가 주소를 수정할 일이 남아 있습니다.

그럼 이제 링크(link) 를 해 봅시다.

link /SUBSYSTEM:WINDOWS  /LIBPATH:c:\masm32\lib  msgbox.obj

/SUBSYSTEM:WINDOWS 는 Link 에게 이 프로그램의 실행 순서를 알려 줍니다.
/LIBPATH:<path to import library> 는 Link 가 포함시켜야 할 라이브러리들이 어디 있는지 알려 줍니다. 만약  MASM32를 사용한다면, MASM32\lib 폴더에 있을 것입니다.

Link 는 object 파일을 읽고, 포함된 라이브러리로부터 주소를 수정합니다. 이 과정이 끝나고 나면 msgbox.exe 파일이 생성됩니다. 
이제 msgbox.exe 를 얻었습니다.
 
실행 해 보세요. 아무 일도 하지 않을 것 입니다. 아직 여기에 어떤 재미있는 것도 넣지 않았습니다. 하지만 그래도 이것은 Windows 프로그램입니다. 파일 사이즈를 한번 보죠!, 제 PC 에서는 1,536 bytes 입니다.

다음으로 메세지 박스를 넣어 볼 것입니다. 이 함수의 프로토타입은 다음과 같습니다.

MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD

hwnd 는 부모창의 핸들(handle)입니다. handle 은 참조하고 있는 윈도우를 나타내는 숫자라고 생각하면 됩니다. 이 값은 여러분에게 중요하지 않습니다. 단지 윈도우를 나타내는 숫자라고만 기억하면 됩니다. 여러분이 윈도우를 가지고 어떤 것을 하고 싶을 때, 이 핸들을 통해서 참조해야만 할 것입니다.
lpText 는 메세지 박스의 클라이언트 영역(client area)에 나타낼 문자를 가리키는 포인터(pointer)입니다. 포인터는(pointer) 어떤 것들의 실제 주소값 입니다. 문자열의 포인터 == string 의 주소
lpCaption 은 메세지 박스의 캡션(caption) 을 가리키는 포인터입니다.
uType 은 아이콘과 숫자와 메세지 박스의 버튼 타입을 명시합니다.

msgbox.asm 에 메세지 박스를 넣어 봅시다. 
 
.386 
.model flat,stdcall 
option casemap:none 
include \masm32\include\windows.inc 
include \masm32\include\kernel32.inc 
includelib \masm32\lib\kernel32.lib 
include \masm32\include\user32.inc 
includelib \masm32\lib\user32.lib
.data 
MsgBoxCaption  db "Iczelion Tutorial No.2",0 
MsgBoxText       db "Win32 Assembly is Great!",0
.code 
start: 
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK 
invoke ExitProcess, NULL 
end start

어셈블리하고 실행 해 봅시다. "Win32 Assembly is Great!" 를 출력하는 메세지 박스를 볼 수 있을 것 입니다.

소스코드를 다시 살펴 봅시다.
.data 섹션 안에 zero-terminated(역주:0으로 문자열 끝을 알리는) 문자열을 선언했습니다. Windows 에서 모든 ANSI 문자열은 NULL (16진수 0)로 끝난다는 것을 잊지 마세요.
NULL 과 MB_OK 이렇게 두 개의 상수를 사용하였습니다. 이 상수들은 windows.inc 에 기록되어 있습니다. 그렇기 때문에 값 대신 이름을 사용 할 수 있습니다. 이것은 소스코드의 가독성을 높여 줍니다.
addr 연산자는 라벨의 함수 주소를 전달하기 위해서 사용 되었습니다. 이것은 invoke 지시자 문맥에서만 사용할 수 있습니다. 예를 들어, 레지스터(register)나 변수에게 라벨의 주소를 저장하기 위해서는 사용할 수 없습니다. 이 경우엔 addr 대신 offset 을 사용할 수 있습니다. 이렇듯 둘 사이에는 조금 차이점이 있습니다.

1. addroffset와 달리, 앞에서 참조를 할 수는 없습니다. 예를 들어, 라벨이 소스코드에서 invoke 보다 아래쪽에 선언되어 있다면, addr 을 사용할 수 없습니다..
invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK 
...... 
MsgBoxCaption  db "Iczelion Tutorial No.2",0 
MsgBoxText       db "Win32 Assembly is Great!",0

MASM 은 에러를 발견할 것입니다. 만약 위의 코드에서 addr 대신 offset 을 사용한다면, 성공적으로 어셈블리 될 것 입니다. 

2. addroffset 과 달리 지역변수를 처리(handle)할 수 있습니다. 지역변수는 단지 스택에 예약된 공간입니다. 이 주소는 실행시간(runtime)에만 알 수 있습니다. offset 은 어셈블러에 의해서 어셈블리 시간에 계산 됩니다. 그렇기 때문에 offset 을 지역변수에 사용 할 수 없는 것은 당연합니다. addr 가 지역변수를 처리(handle)할 수 있는 이유는 어셈블러가 addr 이 참조하는 변수가 전역인지 지역인지를 가장 먼저 확인하기 때문입니다. 만약 전역변수라면, 오브젝트 파일 안에 변수의 주소를 넣습니다. 이것은 offset 과 비슷하게 동작합니다. 만약 이것이 지역변수라면 실제로 함수를 호출 하기 전에 다음과 같은 코드를 생성합니다.

lea eax, LocalVar 
push eax

lea 는 실행시간에 라벨의 주소를 알 수 있기 때문에, 성공적으로 실행됩니다.