Tutorial 2: MessageBox
이번 장에서는,"Win32 assembly is great! "라는 메시지박스를 화면에 표시하는 윈도우즈 프로그램을 제작한다.
소스 | 실행 결과 |
|
윈도우즈에는 프로그램을 제작하기 위한 풍부한 자원이 미리 준비되어 있다. 그 중 핵심을 이루는 것이, 윈도우즈 API(Application Programming Interface)다. 윈도우즈API는 윈도우즈 자신을 위해 존재하는 매우 편리한 함수들의 모임으로, 어떤 윈도우 프로그램에서도 이런 API를 사용할 수 있다. 이 함수들의 실체는 kernel32.dll, user32.dll 과 gdi32.dll라는 DLL안에 있다. Kernel32.dll은 메모리나 프로세스를 관리하는 함수들로 되어있고 User32.dll는 유저 인터페이스를 주관하는 함수들로 되어있다. Gdi32.dll는 출력처리를 담당하고 있다. 이런 DLL(이들을,"Big 3" 혹은 "main 3"라 말한다) 외에도 물론 사용 가능한 API 함수에 대해서도 추후에 자세하게 설명하겠다.
윈도우즈의 프로그램은 이런 DLL을 실행시에 링크 하게 되어 있다. 즉, 실행파일에 API 함수의 코드는 포함되지 않는다. 그렇기 때문에 프로그램은, 실행시에 요구한 API 함수가 어디에 있는지를 찾기 위해 필요한 정보를 가지고 있어야만 한다. 그런 정보는 임포트 라이브러리 라는 부분에 존재하며, 프로그램을 작성할 때는, 정확한 임포트 라이브러리를 링크 시키지 않으면 안 된다. 그렇지 않으면, API 함수의 위치를 모르게 되는 것이다.
윈도우즈 프로그램은 메모리에 로드될 때, 윈도우즈는 프로그램에 들어있는 정보를 읽는다. 정보란 것은, 해당 프로그램이 사용하는 함수명과 그 함수가 들어있는 DLL의 이름이다. 윈도우즈가 이러한 정보를 찾게되면, 해당 DLL을 로드하고, 필요한 함수의 주소를 설정하게 되어 함수를 정상적으로 호출할 수 있게 된다.
API 함수에는 2개의 카테고리가 있다. 그중 하나는 ANSI 이며, 다른 하나는 Unicode이다. ANSI용의 API 함수명에는 끝부분에"A"가 추가된다. 예를 들면, MessageBoxA 와 같다. Unicode용의 API 함수명의 함수명의 끝에는 "W"가 붙게된다(이는 Wide Char 의"W"일 것이다). Windows9x는 ANSI가 디폴트이며, WindowsNT는 Unicode가 기본값으로 되어 있다.
대부분의 경우, NULL문자('\0')로 끝나는 ANSI 문자배열을 사용한다. 참고로, ANSI문자의 크기는 1바이트이다. ANSI 문자 코드는 영어권의 언어는 잘 처리하지만 아시아계열의 언어체계에는 맞지 않다. 그렇기 때문에, UNICODE가 고안되었다. UNICODE의 사이즈는 2바이트로, 65,536문자를 표현할 수 있게 된다.
그러나, 플랫폼에 의해서 어떤 API 함수를 사용하는지를 선택할 수 없을때는 접미어가 없는("W"도"A"도 붙이지 않는다) API 함수를 사용하면 좋다.
|
그럼, 표준 뼈대코드로 이루어진 프로그램을 보자. 이 프로그램은 실행하자 말자 종료하는 프로그램이다.
.386 .model flat, stdcall .data .code start: end start프로그램의 실행은, end 의 뒤에 있는 label명으로 부터 실행이 시작된다. 예제에서는, start 라는 label로 부터 프로그램이 시작된다. 그리고, 제어명령을 만날 때까지 계속 실행된다.추가로, 제어명령이란, jmp, jne, je, ret...등의 명령어들이다. 이런 제어명령은, 문자 그대로 프로그램의 흐름을 변경시키는 것이다. 또한 윈도우즈 프로그램은 종료할 때에, ExitProcess 라는 API 함수를 호출해야 한다.
ExitProcess proto uExitCode:DWORD
아래의 설명은 함수의 프로토타입이다. 함수프로토타입은, 어셈블러나 링커에게 그 함수의 성격을 지시하는 역할을 한다. 함수프로토타입은 아래와 같다.
FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...간단하게 설명하면, PROTO 키워드에 앞에 함수명이 오고, 함수가 필요로 하는 인수의 리스트를 쉼표(콤마)로 구분해서 적어주면 된다. 앞의 예제ExitProcess는, DWORD 형의 인수를 하나만 받는 함수로서 정의되어 있다. 이런 함수 프로토타입은, 하이레벨 함수 호출 구문을 사용할 때에 매우 편리하다(Invoke). 일반적인 타입체크형태의 함수를 호출하는(Call 구문 사용) 경우를 생각해 보자. 예를 들면,
이렇게 ExitProcess를 호출했을 경우에는, dword 를 스택에 미리 푸쉬 하지 않으면 어떻게 될까? 어셈블러나 링커는 에러를 검출할 수 없기 때문에, 프로그램이 다운된 후에라야, 이런 실수를 알아차리게 될 것이다. 그러나,
call ExitProcess
invoke ExitProcess와 같이 했을 경우는, 링커가 스택에 푸쉬를 하지 않고 호출했다고 가르쳐주기 때문에, 에러를 피할 수 있을 것이다. 그래서, call 구문 보다는 invoke 구문을 사용하도록 강력히 추천한다.
invoke 구문의 사용법은 아래와 같다
INVOK expression [, arguments]expression 에는, 함수의 이름이나 함수의 포인터를 기술할 수 있다. 함수의 인수는 쉼표로 구분되어야 한다.
대부분의 API 함수의 프로토타입은 인클루드파일에(INC) 저장할 수 있다. 만약 hutch 의 MASM32를 사용하고 있다면 , MASM32/include 폴더에 인클루드 파일들이 있다. 인클루드 파일은 .inc 라는 확장자(extension)로 되어있고, 어느 함수가 어느DLL에 들어 있는지와 , 그 DLL의 이름과 함수프로토타입도 .inc파일에 포함되어 있다. 예를 들어, ExitProcess 함수는 kernel32.lib 로 export 되어 있으므로, ExitProcess 함수의 프로토타입은 kernel32.inc 에 들어 있게 된다. (.lib파일과 .inc파일은 쌍으로 움직인다)
또한, 자신만의 함수프로토타입 또한 만들 수도 있다. 본 강좌에서는, hutch의 windows.inc(http://win32asm.cjb.net 에서 다운로드할 수 있다)를 사용하고 있다.ExitProcess 함수로 돌아와서, uExitCode 인수는 윈도우즈에게 반환하는 값이다. 이제 ExitProcess 함수는 다음과 같이 CALL 할 수 있다.
invoke ExitProcess, 0이 명령문을 뼈대 프로그램의 start label의 바로 뒤에 입력해 보자. 이로써, 실행하자 마자 곧바로 종료하는 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 startcasemap 옵션은, 명령문의 대소문자를 구별 하도록 하는 명령으로서, ExitProcess 와 exitprocess 는 전혀 다른 함수가 된다. API가 C언어에 기반했기 때문에 반드시 대소문자를 구별해야한다. 하지만 MASM자체는 대소문자를 구별하지 않으므로 저렇게 옵션을 준것이다. 새로운 지시어인, include 에 대해 설명한다. 이것은, include 뒤에 기입되는 파일명을, 그 include 지시어 부분에 삽입하는 것이다. 위의 예에서는,include \masm32\include\windows.inc 이다. windows.inc 파일을 열고, windows.inc 에 있는 내용을 입력하게된다. hutch의 windows.inc 에는, win32 환경에 필요한 상수나 구조체가 정의되어 있지만, 함수프로토타입은 없다. windows.inc 파일은 절대 이해하기 쉬운것이 아니다. hutch 와 나는 최대한 상수나 구조체를 windows.inc 에 기록했지만, 아직도 누락된 부분이 많이 있을 수 있기 때문에, 계속 업데이트 해 나갈 예정이므로, hutch 의 홈 페이지를 체크해서 항상 확인 하기 바란다.
windows.inc 에서, 프로그램은 상수나 구조체를 얻을 수 있었으므로, 이번에는 함수 프로토타입을, include 해야한다. 그 파일은 \masm32\include 폴더에 있다.위의 예에서는, kernel32.dll 로 export 된 함수를 호출하므로, kernel32.dll 의 함수 프로토타입을 가지는 파일(kernel32.inc)을 인클루드 해야한다. 텍스트 편집기로 kernel32.inc 를 열면, kernel32.dll 로 사용할수 있는 모든함수의 프로토타입을 확인할 수 있을 것이다. 만약 kernel32.inc 를 인클루드 하지 않으면, ExitProcess 함수를 호출할 수는 있지만, 단순 구문인 call 명령어로만 호출 할 수 있다.즉, invoke 명령으로 함수를 호출 할 수 없는 것이다. 주의 해야하는 것은, invoke 명령으로 함수 호출을 하려면 ,반드시 소스파일의 어디엔가 호출하고 싶은 함수의 프로토타입을 기술해야만 한다는 것이다. 예제에서는, kernel32.inc 를 인클루드 하지 않았다면, ExitProcess 함수를 invoke 명령으로 호출하기 전에, ExitProcess 함수의 프로토타입을 어디엔가 선언해야 사용할 수 있다는 의미이다. 즉, Invoke구문은 PROTO문과 쌍으로 움직인다는 소리다.
이번에는 includelib에 대해 설명한다.include 와 비슷하지만 기능이 다르다. 임포트 라이브러리를 어셈블러에 지시하는 지시어이다. 어셈블러가 이 지시어를 발견하면, 오브젝트 파일에 링크용 명령을 입력해서, 링커가 어느 파일을 링크 하는지 알수 있도록 한다. includelib 지시어를 사용하지 않아도, 명령행에서 링커를 실행 할 때에 임포트 하는 파일명을 지정해 주어도 되지만, 명령행에서는, 128글자의 제한도 있고, 여러가지로 불편해서, includelib 지시어를 사용하는 편이 더 편하다.현재까지의 예를, msgbox.asm 라는 파일명으로 저장한다. 그리고, ml.exe 에 경로가 설정된 곳에서 부터 msgbox.asm을 어셈블(assemble) 한다.
ml /c /coff /Cp msgbox.asm
- /c 는 어셈블(assemble)만 수행하고 링크작업은 하지 않는 옵션이다. link.exe 를 실행하기 전에 다른 작업을 할 경우, 자동으로 link.exe 를 실행하지 않도록 지정한다.
- /coff 는COFF 포맷의 obj 파일을 생성하는 옵션이다. COFF(Common Object File Format)는 Unix 환경하에서도 사용되는 표준 실행 파일 포맷이다.
- /Cp 는 프로그램의 label명등에 사용된 이름의 대소문자를 구별 하도록 하는 옵션이다. 만약,MASM32를 사용하고 있다면, 코드중에 option casemap:none 을 기술하면 같은 효과를 얻을 수 있다. 위의 예에서는, .model 지시어 바로 뒷부분에 입력했으므로 대소문자가 구별되게 된다.
msgbox.asm 의 어셈블(assemble)이 성공하면, msgbox.obj 파일이 생성된다. msgbox.obj 파일은 오브젝트 파일로서, 실행 파일의 한 단계 이전의 상태이다. 오브젝트 파일은 명령어와 데이터를 바이너리 형식으로 가지고 있고 부족한 정보는 링커에 의해 도움을 받아서 주소가 지정 되게끔해서 실행되게 만든다.
다음으로 링크를 실행한다 (/c 옵션을 사용했을 경우)
link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib msgbox.obj
- /SUBSYSTEM:WINDOWS 실행 파일의 운영환경의 종류를 지정한다
- /LIBPATH:<path to import library> 임포트 라이브러리가 있는 디렉토리를 지정한다. MASM32를 사용하고 있다면, MASM32\lib 디렉토리에 있다.(예제에서는 소스파일에 지정했다)
링커는 오브젝트 파일을 읽어서, 임포트 라이브러리에서 구한 주소를 얻는다. 이 작업으로, 실행이 가능한 msgbox.exe 가 만들어진다.
이제 msgbox.exe 를 실행해 보자. 아마 아무일도 일어나지 않을 것이다. 그렇지만 프로그램은 정상적인 윈도우즈의 프로그램인 것이다. 그리고, 실행파일의 크기를 보면, 겨우 1,536바이트 밖에 되지 않는다..
그럼 이제 본론으로 돌아와서 메세지박스를 표시 해 보자. 메세지박스를 표시하는 함수는 다음과 같다.
MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
- hwnd 는 부모윈도우의 핸들이다. 「핸들」이라는 것은, 윈도우를 구별하는 정수값이라고 생각하면 된다. 값자체에 의미는 없기 때문에, 단지 윈도우를 구별하는 용도라고 생각하면 된다. 만약, 윈도우에 대해서 어떤 조작을 하고 싶다면, 이 「핸들」을 기초로 해서 윈도우를 구별해서 여러가지 작업을 수행 할 수 있다.
- lpText 는 메세지박스로 출력하고 싶은 문자열의 포인터다. 「포인터」라는 것은 실제로는 「주소」로서,"문자열의 포인터" = "문자열의 주소" 라고 생각하면 된다.
- lpCaption 은 메세지박스의 캡션으로서 표시하고 싶은 문자열의 포인터이다
- 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이것을 어셈블(assemble)한 후 실행해 보자. "Win32 Assemblyis Great! " 라는 문자열을 가진 메세지박스가 표시 될것이다.
다시 한번 소스 코드를 살펴보자
소스에는, .data 섹션에 NULL문자로 끝나는 2개의 문자열이 정의되어 있다. ANSI 로 취급되는 모든 문자열은 NULL문자로 종료해야 하는 것을 기억해 두자.
그리고, 2개의 상수인 NULL 과 MB_OK 를 사용하고 있다. 이런 상수는 windows.inc 에 정의되어 있고, 실제로는 정수값이 된다. 이것은 프로그램의 가독성을 향상시켜준다.
인수부분에 있는, addr연산자는, 변수의 주소를 넘겨준다. 이 연산자는, invoke 지시어에서만 유효하기 때문에, addr 연산자로 얻은 주소를, 레지스터나 변수에 대입할 수 없다. 예를 들어, 위의 예에서는 addr 대신에 offset 을 사용할 수 있지만, addr 과 offset 사이에는 다소 다른 부분이 있다.
- invoke 의 명령문에서 사용하는 변수명이, 그 invoke 의 뒤에 정의되어 있는 경우, offset 은 정상 작동하지만, 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 을 사용하면, MASM 는 적절히 어셈블(assemble) 해 준다.
- addr은 로컬 변수에서도 작동하지만, offset 은 로컬변수에는 사용할 수 없다. 로컬 변수는, 스택의 어떤부분(미사용영역)에 확보되므로, 실행중 일때만 그 주소를 알 수 있다. offset 은 어셈블(assemble)시(정적)에 동작하는 것이므로, 로컬 변수에 사용할 수 없는 것은, 당연하다. 이에 반해서, addr은 로컬 변수에서도 사용할 수가 있다. 가능한 이유는 , 어셈블(assemble)시에 addr 에 의해 참조되는 변수가 글로벌영역에 확보된 것인지, 로컬영역에 확보된 것인지를 판단해서, 작동을 적절히 변경하기 때문이다. 만약, 글로벌영역에 확보된 것이라면, 오브젝트 파일에는, 그 변수의 주소를 기록한다. 이 경우는,offset 과 같은 방식으로 동작한다. 반면에, 로컬 변수일 경우에는, 함수를 호출하기 전에, 아래와 같은 변수의 주소를 계산하는 코드가 기록 된 후, 그 주소를 획득해서, 함수를 호출한다.
lea eax, LocalVar push eaxlea 명령은 실행시에 변수의 주소 관련 문제를 해결하므로, 정상적으로 동작하게 되는 것이다.