Programming/Win32 API2008. 12. 1. 15:40
Win32 Global API Hook - 제3강 Win32 어셈블리 프로그래밍 (2)
===========================================================*/

Win32 어셈블리 두번째 시간입니다. 그동안 좀 바쁜일이 많아서, 오랜만에 강좌를 올리게되었습니다. 오늘
은 지난시간에 말씀드린대로 호출규약에 따른 함수들의 형태가 어셈블리로 어떻게 쓰여지는지와 함께 흐름
제어의 기본인 조건분기와 반복구문이 어떻게 구현되는지에 대해 알아보겠습니다. 먼저 강좌에 많은 관심
과 응원을 보내주신 여러분들께 감사하다는 말씀 드리면서 강좌들어갑니다.

(강좌의 진행상 존칭을 생략하겠습니다.)

1. 프로시저 호출규약 (Procedure Calling Convention)

지난시간에 프로시저(함수)가 어떤 형태로 매개변수와 반환값을 다루고, 지역변수를 관리하는가에 대한 일
반적인 메카니즘을 배웠다. 그러면 조금 더 구체적인 내용으로 들어가서 호출규약에 관한 얘기를 해보자.
호출규약(calling convention)이란 매개변수를 함수에 전달하고 함수가 사용한 스택을 어떻게 정리하는지
에 대한 구체적인 규칙에 따른 분류이다. 호출규약에는 다음과 같은 것들이 있다.

(1) __cdecl        : C 호출규약
(2) __stdcall     : 표준 호출규약
(3) __fastcall    : 빠른 호출규약
(4) __declspec(naked)    : 벗은(?) 호출규약
(5) this        : this 호출규약

참고로, 이것외에 __pascal 호출규약이 있는데 Win32로 넘어오면서 더이상 지원하지 않는다. 자, 그러면 하
나씩 알아보자. 일단 코드를 보면서 눈으로 직접 확인해보자. (진실은 언제나 디버거가 말해주듯이...)

(1) __cdecl    (C 호출규약)

70:       ret = cdecl_Call(3, 4);
0040D588   push        4
0040D58A   push        3
0040D58C   call        @ILT+0(cdecl_Call) (00401005)
0040D591   add         esp,8
0040D594   mov         dword ptr [ebp-4],eax

우리는 이미 지난시간에 많은것(?)을 배웠기에 이정도 어셈블리 코드는 그야말로 껌인것이다. 함수이름은
따로 설명할 필요도 없이 __cdecl로 선언된 C 호출규약 함수이다. 먼저 매개변수가 스택에 푸쉬되는 순서
를 보자. 오른쪽 매개변수부터 거꾸로 푸쉬되는 것을 볼 수 있다. 그리고 함수가 호출되고 난 다음, 호출한
쪽에서 명시적으로 스택을 정리하는것 또한 확실히 알 수 있겠다.

그럼, 실제 함수의 구현을 보자.

19:   // C 호출규약
20:   int __cdecl cdecl_Call(int a, int b)
21:   {
00401040   push        ebp
00401041   mov         ebp,esp
00401043   sub         esp,40h
00401046   push        ebx
00401047   push        esi
00401048   push        edi
00401049   lea         edi,[ebp-40h]
0040104C   mov         ecx,10h
00401051   mov         eax,0CCCCCCCCh
00401056   rep stos    dword ptr [edi]
22:       return a+b;
00401058   mov         eax,dword ptr [ebp+8]
0040105B   add         eax,dword ptr [ebp+0Ch]
23:   }
0040105E   pop         edi
0040105F   pop         esi
00401060   pop         ebx
00401061   mov         esp,ebp
00401063   pop         ebp
00401064   ret

함수가 열리면서("{") 함수시작코드와 함께 에러처리를 위한 0xCC(int 3)이 지역변수공간에 쫙 깔리는걸 보
니 디버그모드인걸 알수 있다. 어쨋든 중요한것은 맨 마지막 ret 문이다. 그냥 반환되는걸 알 수 있다. 왜
냐면 아까 함수를 호출한쪽에서 스택을 정리해주기 때문에 호출되는 함수에서는 그냥 리턴만 하면된다.

(2) __stdcall (표준 호출규약)

72:       ret = stdcall_Call(3, 4);
0040D597   push        4
0040D599   push        3
0040D59B   call        @ILT+5(stdcall_Call) (0040100a)
0040D5A0   mov         dword ptr [ebp-4],eax

보아하니 __stdcall 호출규약 또한 매개변수를 오른쪽에서 왼쪽으로 스택에 푸쉬한다. 그런데? 함수호출이
수행되고 난 다음 아까보았던 스택 정리코드(add esp, 8)가 빠져있는걸 볼 수 있다. 이걸로 보아 호출되는
함수내부에서 스택을 정리한다고 미루어 짐작할 수 있다.

25:   // 표준 호출규약
26:   int __stdcall stdcall_Call(int a, int b)
27:   {
00401070   push        ebp
00401071   mov         ebp,esp
00401073   sub         esp,40h
00401076   push        ebx
00401077   push        esi
00401078   push        edi
00401079   lea         edi,[ebp-40h]
0040107C   mov         ecx,10h
00401081   mov         eax,0CCCCCCCCh
00401086   rep stos    dword ptr [edi]
28:       return a+b;
00401088   mov         eax,dword ptr [ebp+8]
0040108B   add         eax,dword ptr [ebp+0Ch]
29:   }
0040108E   pop         edi
0040108F   pop         esi
00401090   pop         ebx
00401091   mov         esp,ebp
00401093   pop         ebp
00401094   ret         8

실제로 함수구현을 보니까 정말로 마지막 반환코드(ret 8) 스택포인터을 8바이트 올리고 리턴하는걸 볼 수
있다. "ret n" 은 n바이트 만큼 esp를 증가한 후 에 리턴하는 어셈블리 명령이다. 그럼, 여기서 문제 ret 8 
이 수행되고 나면 esp(스택포인터)는 얼마만큼 증가할까? 8바이트라고? 그렇게 간단하면 왜 물어보겠는가.
정답은 12바이트(0xC)이다. 왜냐하면 ret 명령이 스택에서 리턴주소를 꺼내온뒤 그곳으로 점프(jmp)하기 때
문이다. (지난시간에 call 명령과 함께 비교하며 설명했다. 기억해두자, call과 ret명령은 내부적으로 스택
에 리턴주소를 위한 push, pop을 수행한다.)

(3) __fastcall (빠른 호출규약)

__fastcall 호출규약은 이름 그대로 빠른 호출을 위해서 사용되어진다. 메모리보다 상대적으로 빠른 레지스
터를 이용한다고 해서 레지스터 호출규약(Register calling convention)이라고도 한다. 그러나 Win32 사용
자모드에서는 원칙적으로 레지스터 호출규약을 사용하지 않는다. 다음강좌인 VxD 강좌때 보게 되겠지만,
VxD 시스템 함수들은 레지스터 호출규약을 사용하는 것들이 상당수 존재한다. 그러나 Windows NT/2000 커널
모드 드라이버에서는 더이상 레지스터 호출규약을 사용하지 않는데 그 이유는 레지스터는 CPU에 종속적인
저장매체이기때문에 CPU간 호환성을 보장할 수 없기때문이다.

74:       ret = fastcall_Call(3, 4, 5, 6);
0040D5A3   push        6
0040D5A5   push        5
0040D5A7   mov         edx,4
0040D5AC   mov         ecx,3
0040D5B1   call        @ILT+35(fastcall_Call) (00401028)
0040D5B6   mov         dword ptr [ebp-4],eax

호출하는 쪽을 보게 되면, 첫번째와 두번째 매개변수를 ecx, edx 레지스터에 담아서 호출하는것을 볼 수 있
다. 그러나 레지스터의 수는 한정되어 있으므로 모든 매개변수를 레지스터에 담을수는 없다. 따라서 처음
두개의 매개변수만 ecx, edx에 담고 나머지는 스택을 통하는데 이때에도 마찬가지로 오른쪽에서 왼쪽으로
스택에 푸쉬하고, __stdcall과 마찬가지로 호출되는 함수쪽에서 스택을 정리한다.

31:   // 빠른 호출규약
32:   int __fastcall fastcall_Call(int a, int b, int c, int d)
33:   {
004010A0   push        ebp
004010A1   mov         ebp,esp
004010A3   sub         esp,48h
004010A6   push        ebx
004010A7   push        esi
004010A8   push        edi
004010A9   push        ecx
004010AA   lea         edi,[ebp-48h]
004010AD   mov         ecx,12h
004010B2   mov         eax,0CCCCCCCCh
004010B7   rep stos    dword ptr [edi]
004010B9   pop         ecx
004010BA   mov         dword ptr [ebp-8],edx
004010BD   mov         dword ptr [ebp-4],ecx
34:       return a+b+c+d;
004010C0   mov         eax,dword ptr [ebp-4]
004010C3   add         eax,dword ptr [ebp-8]
004010C6   add         eax,dword ptr [ebp+8]
004010C9   add         eax,dword ptr [ebp+0Ch]
35:   }
004010CC   pop         edi
004010CD   pop         esi
004010CE   pop         ebx
004010CF   mov         esp,ebp
004010D1   pop         ebp
004010D2   ret         8

함수의 구현부이다. 그런데 뭔가 좀 이상한것이 있다. 빠른 호출을 위해서 레지스터를 통해서 매개변수를
넘겨준다고 했는데, 매개변수로 넘어온 레지스터를 다시 지역변수 공간(ebp-알파)에 담아서 사용하는것을
볼 수 있다. 이건 예제가 디버그모드로 빌드되었기 때문인데 릴리즈로 빌드된 어셈블리 코드는 다음과 같
다.

00401020   lea         eax,[ecx+edx]
00401023   mov         edx,dword ptr [esp+4]
00401027   mov         ecx,dword ptr [esp+8]
0040102B   add         eax,edx
0040102D   add         eax,ecx
0040102F   ret         8

lea 명령이 처음나온것 같은데, lea 명령은 아주 자주 쓰이는 명령이니까 잘 알아두자.
lea (Load Effective Address)
: 오른쪽 피연산자의 주소(메모리)를 왼쪽 피연산자(레지스터)로 전송한다. 보통 C언어 포인터변수를 설정
하는 문장에서 사용된다.

처음 두개의 매개변수는 eax와 ecx 레지스터로 취급하고 나머지 레지스터는 스택을 이용하는것을 볼 수 있
다.

(3) __declspec(naked) (벗은(?) 호출규약)

해석이 조금 이상한데 의미상으로는 스택프레임을 설정하는 기존의 함수구조를 위한 함수시작코드와 함수종
료코드를 제공하지 않는 다는 의미이다. 그러므로 이것을 __cdecl 형태로 구현해서 사용하던, __stdcall 형
태로 사용하던 그것은 순전히 사용자 맘이다. naked 호출규약은 엄밀한 의미로는 호출규약이라고 할수 없는
데, 왜냐면 이것은 함수의 구현부에서 함수가 구현되는 방법의 문제이지 앞에서의 호출규약처럼 함수의 선
언의 문제가 아니기 때문이다. 어쨋든 코드를 보자.

76:       ret = naked_Call(3, 4);
00401159   push        4
0040115B   push        3
0040115D   call        @ILT+25(naked_Call) (0040101e)
00401162   add         esp,8
00401165   mov         dword ptr [ebp-4],eax

매개변수는 위의것과 동일하게 오른쪽에서 왼쪽으로 푸쉬되며, __cdecl 과 마찬가지로 호출한 쪽에서 스택
을 복원해줌을 알 수 있다. 이것은 naked 호출규약이라서 그런게 아니고 디폴트 호출이 일어난것이다. 아
까 말했듯이 naked 호출규약은 함수구현의 문제이다.

// 벗은(?) 호출규약
__declspec(naked) int naked_Call(int a, int b)
{
   //return a+b;
   __asm
   {
       push     ebp             // 함수 시작코드
       mov        ebp, esp

       mov        eax, a
       add        eax, b

       mov        esp, ebp    // 함수 종료코드
       pop        ebp
       ret
   }
}

디스어셈블된 코드가 아니고 그냥 C 코드를 보였다. 왜냐면 인라인어셈블리를 사용했기 때문이다. 보다시
피 함수시작코드와 함수종료코드를 사용자 스스로 구현해주어야 한다는 것을 알 수 있다.

(5) this : this 호출규약

마땅히 붙일 이름이 애매한데 C++에서 this 포인터가 어떻게 처리되는지를 정의해놓은 규약이라고 보면된
다. 역시 코드를 보면,

78:       This_Call thiscall;
79:       ret = thiscall.Call(3, 4);
00401168   push        4
0040116A   push        3
0040116C   lea         ecx,[ebp-8]
0040116F   call        @ILT+10(This_Call::Call) (0040100f)
00401174   mov         dword ptr [ebp-4],eax

다른것들은 별로 눈여겨볼것이 없는데, 함수를 call 하기 직전에 무언가(지역변수로 추정되는)를 ecx에
lea 명령으로 로드한뒤 call해주는 것을 볼 수 있다. 여기서 ecx 레지스터가 바로 this 포인터이다. 클래스
의 멤버함수를 부를때 C++ 컴파일러는 ecx 레지스터를 클래스 인스턴스 자신을 가리키는 포인터를 담는 용
도로 사용한다는 것이다. 따라서 멤버함수는 자기가 언제 어디에서 호출되었다 하더라도 나를 부른 녀석이
어떤 인스턴스인지를 정확히 알 수 있다. (물론 이  ecx 레지스터를 이용해서 재미있는 장난을 해볼수도 있
을텐데, 시간이 남는 사람은 한번 이것 저것 해보기 바란다.)


56:   // this 호출규약
57:   class This_Call
58:   {
59:   public:
60:       int Call(int a, int b)
61:       {
004011C0   push        ebp
004011C1   mov         ebp,esp
004011C3   sub         esp,44h
004011C6   push        ebx
004011C7   push        esi
004011C8   push        edi
004011C9   push        ecx
004011CA   lea         edi,[ebp-44h]
004011CD   mov         ecx,11h
004011D2   mov         eax,0CCCCCCCCh
004011D7   rep stos    dword ptr [edi]
004011D9   pop         ecx
004011DA   mov         dword ptr [ebp-4],ecx
62:           return a+b;
004011DD   mov         eax,dword ptr [ebp+8]
004011E0   add         eax,dword ptr [ebp+0Ch]
63:       }
004011E3   pop         edi
004011E4   pop         esi
004011E5   pop         ebx
004011E6   mov         esp,ebp
004011E8   pop         ebp
004011E9   ret         8

구현부는 별로 볼게 없다. 단지 this 포인터라고 알려진 ecx 레지스터를 지역변수 공간으로 로드하는것을
볼수 있는데 이것 또한 디버그 모드이기때문에 생기는 코드일것이라고 생각된다. (궁금하면 직접 확인해보
시길...)

자, 모두 5개의 호출규약에 관해서 각각의 호출규약이 실제로 어떤 형태로 구현되는지를 완벽하게 이해했을
거라고 믿는다. 몇가지 덧붙이자면 실제로 대다수의 Win32 API 는 __stdcall 로 지정되어 있다. 우리가 항
상 보는 WinMain() 함수 또한 __stdcall 이다.
마이크로소프트는 추후 확장성을 고려해서 재정의된 데이터타입(WORD, DWORD 등)을 사용하는것을 권장하는
데 같은 맥락으로 함수 호출규약또한 재정의된 형태로 사용하는것을 권장한다. 그럼 Win32 SDK 에서 재정의
된 호출규약이 어떤것인지 잠깐 알아보자.

CDECL        __cdecl
WINAPI        __stdcall
APIENTRY    __stdcall
CALLBACK    __stdcall
PASCAL        __stdcall
WSAAPI        __stdcall
FASTCALL    __fastcall

CDECL을 제외한 우리가 많이 보던 CALLBACK, WINAPI니 하는 것들은 모두 __stdcall 로 처리되는것을 알  수
있다. (사족을 달자면, 꼭 저렇지만은 않다. 어떤 환경이냐에 따라서 약간씩 다르게 전처리 되기는 하지
만, 일반적인 Win32환경에서 특별히 신경쓰지 않는한 저 규칙을 벗어나지 않을 것이다.) 아참, 잊을 뻔 했
는데 VC++에서는 특별히 컴파일옵션을 만지지 않는한 디폴트로 __cdecl 호출규약을 사용한다.

자, 그러면 호출규약에 대해서는 직접 확인해보았으니 더이상 의문이 없을것 같지만, 왜 이렇게 여러가지
호출규약들이 존재해야 하는지는 궁금해할 사람들이 있을지 모르겠다. 일단 크게 __cdecl 과 __stdcall 호
출규약의 커다란 차이점은 스택을 어디에서 정리해줄 것이냐의 차이점이다. 전자는 호출하는 쪽에서 후자
는 호출되는 함수내부에서 스택을 정리해준다. 상식적으로 생각해서 호출받는 쪽에서 스택을 정리하는
__stdcall 쪽이 더 합리적이라고 생각될지 모르지만, __cdecl 이 필요한 이유는 따로 있다.

xprintf 계열의 가변인자 함수에서 그 이유를 찾을 수 있다. __stdcall 함수의 제작자들은 함수가 매개변수
의 개수(정확히 말하면 매개변수를 받는 스택의 크기이다.)를 알고 있다는 가정하에서 함수를 구현하기를
원했다. 함수의 재사용성과 모듈화를 극대화하기 위한 조치였다고 볼 수 있는데, 이런 형태의 구조에서는
가변인자를 구현할 수 없다는 단점이 있었다. 일단 가변인자를 구현하려면 호출받는쪽에서 몇개의 매개변수
가 들어올지 모르기 때문에 호출받는쪽에서 스택을 정리해줄 수가 없었고, 오로지 호출하는쪽에서만이 매개
변수의 수를 알고 있을뿐이었다. 그러므로 당연히 가변인자를 지원하는 함수들은 스택을 호출하는쪽에서 정
리해주어야만 했고 따라서 __cdecl 호출규약이 필요하게 된것이다. 사실, 이러한 호출규약에 따른 분류는
다분히 C언어의 관점에서 보았을때 그렇다는 말이다. 실제 CPU의 관점에서는 우리가 스택을 이용하던 레지
스터를 이용하던 함수를 구현하던 그냥 쭉 흐르는듯이 프로그램이 진행되던 별 상관없다. 그냥 시키는대로
만 할 뿐이다. 단지 운영체제의 많은 부분이 C언어로 구현 되어 있으며, 우리가 후킹하려는 API또한 대부분
C언어로 구현되어 있으므로 C언어가 함수를 어떻게 구성하는지가 중요한것이다.

아, 갑자기 생각났는데 실제 함수의 이름은 내부적으로는(정확하게 말하면 Link시에) 다른이름으로 변환되
어져서 사용되는데 __cdecl 함수는 앞부분에 밑줄이 붙고 __stdcall 함수들은 앞부분에 밑줄과 더불어서 뒤
에 @와 매개변수의 크기(스택의 크기)가 붙는다. 이것만 보더라도 __stdcall 함수들은 함수스스로가 매개변
수의 크기를 지정해두었다는 것을 알 수가 있다. (확인을 위해서는 extern "C" 문장으로 C 타입으로 함수
를 선언해주어야만 한다. C++은 다중정의(오버로드)를 구현하기 위해 함수 이름을 더 복잡하게 만들어버리
기 때문에 사람이 확인하기가 쉽지 않다.)

2. 흐름제어

흐름제어의 가장 중요한 요소인 조건문과 반복문에 대해서 알아보자. 이쯤되면 척하면 척일텐데 벌써 조건
문과 반복문 예제를 만들어서 디스어셈블하고 있는 저 친구를 보라 !! (참, 영특하지 않은가? ^^)

(1) 조건문

67:       if(a > 3)
0040115F   cmp         dword ptr [ebp-4],3
00401163   jle         main+34h (00401174)
68:       {
69:           printf("3 보다 크다. !!\n");
00401165   push        offset string "3 \xba\xb8\xb4\xd9 \xc5\xa9\xb4\xd9. !!\n" (00422f84)
0040116A   call        printf (0040d780)
0040116F   add         esp,4
70:       }
71:       else
00401172   jmp         main+41h (00401181)
72:       {
73:           printf("3 보다 작거나 같다. !!\n");
00401174   push        offset string "3 \xba\xb8\xb4\xd9 \xc0\xdb\xb0\xc5\xb3\xaa \xb0\xb0\xb4
\xd9. !!\n" (0042
00401179   call        printf (0040d780)
0040117E   add         esp,4
74:       }

이제는 이정도만 봐도 알 수 있을것이다.

조건문에서 사용되는 어셈블리 명령어는 매우 여러가지이지만 그 방식은 거의 동일하다. 일단 cmp 명령을
만났다. 딱 보니까 지역변수 a와 정수 3을 비교하는것 같 다. 그 아래 jle 명령을 보자. 이름에서 느껴지는
뉘앙스가 웬지, 작거나 같으면 점프(Jump if less or equal)을 연상시키지 않는가? 우리가 생각한 그대로
다. 정확하게 두번째 블럭안으로 점프하게 된다. 그럼 그렇지 않다면 그 다음 문장, 바로 3보다 큰경우를
수행하는 첫번째 블럭을 수행하고 두번째 블럭을 건너뛰고 진행하게 된다.

자, 전체적인 흐름은 이해가 될것인데 좀더 구체적으로 살펴보자. 단순히 cmp 하나의 명령으로 어떤놈이 작
고, 큰지, 또는 같은지를 알 수 있을까? 이것은 플래그 레지스터라는 놈을 이해하면 된다. 플래그 레지스터
가 각각의 상태를 표현하기 위해 특정한 값들을 갖게된다. 예를 들면 cmp 명령으로 비교한 두개의 값이 같
다면 ZR 플래그(또는 ZF)가 1로 세트된다. 이렇게 cmp나 test등의 비교명령은 플래그 레지스터를 이용해서
그 결과를 나타내는데 우리는 이러한 플래그들을 가지고 원하는 실행위치를 점프하게되는데, 이럴때 사용하
는 것이 바로 jxx 계열의 조건점프명령이다. 우리는 지금까지 jmp 명령만을 보았는데 이것은 무조건 점프하
는 명령이라면 아래에 리스트된 조건점프 명령들은 플래그들의 조합으로 비교의 결과를 인식하고 조건에 맞
는 경우에만 점프하게 된다.

je (jump if equal)    : 같다면 점프한다. ( == )
jne (jump if not equal)    : 같지 않다면 점프한다. ( != )
jl (jump if less)    : 작다면 점프한다. ( < )
jg (jump if greater): 크다면 점프한다. ( > )
jge (jump if greater or equal)    : 크거나 같다면 점프한다. ( >= )
jle (jump if less or equal)        : 작거나 같다면 점프한다. ( <= )

뭐, 별로 어려울건 없을것이다. 실제로 매크로어셈블러 문서를 살펴보면 알게되겠지만 조건점프명령은 저
것 말고도 여러가지가 더 있다. 명령어에서 e가 들어간 명령은 z로 바뀐 다른명령어로도 사용되는데 둘의
의미는 동일하다. 예를 들면 je와 jz는 동일하게 동작한다. 의미는 jz은 zero flag가 설정된 경우에 점프한
다라는 의미이지만 비교결과가 같다면 zero flag가 설정되기 때문에 결과적으로 같은 동작을 수행한다. (실
제 바이트코드를 보면 정말 그런지 알수 있을것이다. 이런것들은 초보자들에게 약간의 혼란을 가져오는데
실제로 Intel CPU 매뉴얼과 매크로어셈블러 매뉴얼이 표기하는 명령어 형식이나 플래그 레지스터의 이름등
에서 약간의 차이가 있다. 사실 같은 의미인데 이름을 조금씩 다르게 지정해놓은것이 몇가지 존재한다.)

그런데, 어셈블리 명령과는 별개로 위의 디스어셈블 코드를 살펴보면 재미있는것을 발견할 수 있을텐데, 그
것은 바로 우리가 제시했던 조건과는 반대되는 조건으로 검사하고 분기한다는 점이다. 우리는 3보다 크다면
을 검사했는데 실제 디스어셈블된 코드를 보면 3보다 작거나 같다면으로 검사한다는 것이다. 이것은 컴파일
러가 어셈블리코드를 생성할때 가급적 jmp명령을 사용하지 않기위해서 최적화를 수행하는 것이다. 실제로
반대로 구현해보면 jmp 명령을 두번 사용해야 한다는것을 알 수 있을것이다. 이것은 실제로 어셈블리로 프
로그래밍할때에는 기본적인 사항인데 우리가 원하는 조건의 반대조건으로 검사하는 것이 프로그램을 더 작
고 빠르게 만든다. jmp명령은 실행위치를 변경하는 상대적으로 비싼대가를 치뤄야 하는 명령이기 때문이다.

(2) 반복문

76:       for(int i=0; i<5; i++)
0040D7F1 C7 45 F8 00 00 00 00 mov         dword ptr [ebp-8],0
0040D7F8 EB 09                jmp         main+53h (0040d803)
0040D7FA 8B 45 F8             mov         eax,dword ptr [ebp-8]
0040D7FD 83 C0 01             add         eax,1
0040D800 89 45 F8             mov         dword ptr [ebp-8],eax
0040D803 83 7D F8 05          cmp         dword ptr [ebp-8],5
0040D807 7D 0B                jge         main+64h (0040d814)
77:       {
78:           a++;
0040D809 8B 4D FC             mov         ecx,dword ptr [ebp-4]
0040D80C 83 C1 01             add         ecx,1
0040D80F 89 4D FC             mov         dword ptr [ebp-4],ecx
79:       }
0040D812 EB E6                jmp         main+4Ah (0040d7fa)

C언어에서 가장 많이 사용되는 for()문이다. 일단 지역변수 i의 값을 0으로 세트하고 바로 점프하는 명령
이 있다. 어디로? 바로 조건식으로 점프한다. 조건식은 정수 5와 i를 비교한후, 크거나 같다면(역시 반대
의 조건으로 검사하는것을 볼 수 있다.) 반복문을 탈출한다. 반복문 내에서는 지역변수 a를 1 증가하고, 지
역변수 i를 1 증가하는 증감식으로 점프한다. 그런다음 다시 조건문으을 실행한다. 뭐 별로 어려울것 없
다. 그럼 다음은 while()문을 보자.

76:       int i = 5;
0040D7F1 C7 45 F8 05 00 00 00 mov         dword ptr [ebp-8],5
77:       while(i--)
0040D7F8 8B 45 F8             mov         eax,dword ptr [ebp-8]
0040D7FB 8B 4D F8             mov         ecx,dword ptr [ebp-8]
0040D7FE 83 E9 01             sub         ecx,1
0040D801 89 4D F8             mov         dword ptr [ebp-8],ecx
0040D804 85 C0                test        eax,eax
0040D806 74 0B                je          main+63h (0040d813)
78:       {
79:           a++;
0040D808 8B 55 FC             mov         edx,dword ptr [ebp-4]
0040D80B 83 C2 01             add         edx,1
0040D80E 89 55 FC             mov         dword ptr [ebp-4],edx
80:       }
0040D811 EB E5                jmp         main+48h (0040d7f8)

위의 for()문과 똑같은 일을 수행하는 while()문이다. 역시 별로 어려울것 없으니 스스로 따라가보자.

여기서 또하나 재미난것은, 실제 어셈블리 명령어중에 loop라는 명령어가 존재한다. loop 명령은 ecx 레지
스터에 지정된 수만큼 반복작업을 수행하는데 실제로 마이크로소프트 C컴파일러는 loop 명령을 사용하지 않
는 경우가 대부분이다. 위의 예처럼 조건분기 명령을 사용해서 반복문을 구현한다. 혹시 디버그모드라서?
그렇다면 릴리즈로 빌드한뒤 디스어셈블된 코드를 살펴보자. 그러나 릴리즈모드에서도 별로 다르지 않다.
(음... 쓰고나서 보니까 위의 예제를 릴리즈로 빌드하면 아마도 반복문이 사라져버릴것이다. 왜냐면 반복문
에서 하는 유일한 작업인 a++; 문장은 프로그램 어디에도 영향을 주지않는 있으나마나한 코드이기 때문에
컴파일러가 릴리즈 최적화를 수행하면서 무시될것이다. 어쨋든 유효한 반복구문을 사용해서 확인해보자.)

또 참고로 얘기하면 일반적으로 while()문이 for()문보다 빠르다고 알려져있는데 위의 코드를 보면 왜 그런
지 확실히 알 수 있을것이다. (코드길이가 작아서? 음... 것도 틀린건 아니지만 보다 중요한것은 for()문보
다 while()문이 jmp 명령을 더 적게 사용하는것을 볼수 있다. 아까도 말했듯이 jmp 명령은 상대적으로 비
싼 명령이다.)

3. 끝으로

이상으로 Win32 어셈블리 프로그래밍에 대한 강좌를 마칠까 한다. 사실 2회의 강좌로 어셈블리를 모두 이해
할수 있을거라고는 생각하지 않지만, 적어도 C언어로 작성한 프로그램이 어떠한 형태로 구현되는지에 대한
감은 잡았을거라고 생각한다. 원래 지난시간에 약속한대로 Win32의 강력한 에러처리 메카니즘인 구조화 예
외처리(SEH)에 대해서도 다루려고 했지만 시간상 미처 다루지 못했다. 실제로 API 후킹을 구현하게 되면 반
드시 예외처리를 해주어야 하는데 그 이유는 시스템 DLL을 건드리는 작업은 매우 조심스러운 작업이기때문
에 문제가 생기면 시스템을 재부팅해야 하는 경우가 비일비재하기 때문이다. 따라서 문제가 발생하면 즉시
에러를 복구하고 정상적으로 종료함으로써 다른 프로그램들에게 영향을 주지 않아야 한다. 구조화 예외처리
에 관해서는 다음번 Hooking Code를 작성하는 부분에서 다시 얘기할 기회가 있으니, 그때 다루기로 하자.

필자가 소개한 어셈블리에 대한 내용은 극히 일부분이거나 기초가 되는 수준이라는것을 잊지말자. Intel
x86 계열 CPU는 오늘 소개한 명령이외에도 문자열처리나 멀티미디어 데이터처리 등에 관한 무수히 많은 명
령어들을 제공하며, 어셈블러 또한 무수히 많은 의사명령과 예약어등을 제공한다. 이 모든것을 모두 알아
야 할 필요는 없다고 해도, 분명 자바나 베이식만을 다루는 사람과 어셈블리까지 아는 사람과는 프로그래밍
을 바라보는 시각에서부터 엄청난 차이가 난다고 믿어 의심치 않는다.

4. 다음강좌에서는

이미 예고한대로 다음강좌에서는 Windows9x에서 지원하는 디바이스드라이버인 VxD에 대해서 얘기하도록 하
겠다. 내용의 방대함으로 모든것을 다룰수 없음에 우리가 필요한 부분만을 구현하는 쪽으로 강좌의 촛점을
맞춰나가겠다. 더불어서 보호모드와 운영체제 실행권한인 Ring0에 관한 설명과 디바이스드라이버를 통하지
않고 Ring0를 얻어내는 Windows의 뒷문(?)에 대해서도 얘기해보자. 디바이스드라이버에 대한 경험이 전혀
없는 독자라면 다음 강좌부터는 실습을 따라가는것조차 버거울텐데 그 이유는 아직도 디바이스드라이버 개
발환경이라는것이 원시적이기 짝이 없기때문이다. 일단 VC++같은 멋진 통합환경과 디버거를 기대한다면 일
찌감치 다른일을 찾아보는것이 빠를것이다. 내용이 내용이니만큼 준비해야할 것들과 다음강좌의 진행내용
을 전체적으로 알아보자.

(1) Window 98 DDK(DeviceDriver Development Kit)
: 마이크로소프트 DDK 홈페이지(http://www.microsoft.com/ddk/)에 가면 구할수 있다. 각 운영체제별로 DDK
를 따로 제공하는데, 우리는 일단 98에서 작업할것이므로 98용 DDK를 다운로드받길 바란다.

(2) Numega SoftIce
: 지난강좌에서 언급했던 John Robbinson이나 Matt Pietrek이 시스템엔지니어로 근무하는 Numega Software
에서 제공하는 시스템 디버거이다. 커널모드와 사용자모드를 둘다 지원하는 거의 유일한 디버거이며, 크래
커들의 필수품으로 애용될만큼 디버거보다는 크래킹 도구로 널리 알려져 있다. 문제는 이것은 상용프로그램
이란점인데(가격또한 만만치 않다.) 회사오너를 졸라서 구입하든 또 다른 경로(?)를 통해서 구하든 각자 알
아서 구해오도록 하자. (필자에게 보내달라고 하지는 말기 바란다. 필자는 아직 딸린식구는 없지만, 앞길
이 구만리같은 젊은이기에... ^^)

(3) 그밖에 익숙한 텍스트편집기
: 정 쓸만한게 없다면 VC++를 사용해도 된다. 그리고 취향에 따라 다르겠지만 노트패드나 DOS용 에디터를
사용해도 무관하다. 필자의 경우는 울트라에디터를 사용하는데 프로그래머를 위한 지원이 잘되어 있는 편집
기라고 생각한다.

<잡설>
서두에도 언급했듯이 많은분들이 관심을 보여주셨는데요. 그 결과 무수히 많은(약간 과장해서... ^^) 질문
메일이 오곤 했습니다. 일일이 답변해드리지 못한점 사과드리고요. 질문들에 대한 답변이나 그밖에 잡다한
것들을 모아서 다음 강좌이전에 가벼운 내용의 쉬어가기 형식으로 한번 올리겠습니다. 그럼 다들 감기조심
하시고 언제나 즐기는 프로그래머가 되시길...
Posted by skensita