DBG] Calling Convention MS] Windows

Calling Convention 에 대해서 정리해 보았습니다.
Calling Convention 은 간단히 function 의 parameter 를 어떻게 전달하느냐를 정의하는 것입니다.
예를 들면, Function(a, b) 를 컴파일 한다고 했을 때 파라미터 a 와 b 에 대하여, 스택에 넣는 순서, 레지스터 또는 스택을 사용하는 지 여부, 그리고 마지막으로 파라미터 해제에 대한 룰이 정해져야 합니다. [Calling Convention 을 이해하려면, Thread Stack의 Mechanism 이해하고 있어야 합니다.]
어떤 새로운 프로젝트를 시작할 때 ‘Naming Convention’ 을 정하고 시작하는 경우를 경험했을 듯 합니다. (Windows API 에서도 Naming Convention 이 있습니다.)
컴파일러 관점에서 본다면, 규칙이 있어야 한다는 것입니다.
어떤 언어(Pascal 같은 Language)는 스택에 저장할 때 왼쪽 변수부터 저장(왼쪽에서 오른쪽 순)을 하고, 어떤 언어(C 같은 Language)는 스택에 저장할 때 오른쪽 변수부터 저장(오른쪽에서 왼쪽 순)을 한다면 컴파일러는 이러한 규칙을 알고 있어야 합니다.
C와 Pascal API 을 혼용해서 쓴다면, 문제가 심각해지겠죠? (물론 가정입니다.)
사실 모두 똑같다면 문제가 없겠습니다만… C 언어와 C++ 언어의 차이가 존재하므로 Calling Convention 은 존재할 수 밖에 없습니다. (Windows API 를 사용할 때는 __stdcall 을 쓸 수 밖에 없는 이유가 있습니다. 이는 다음에 정리하도록 하지요.)
Calling Convention 의 핵심은 다음 세가지로 나뉘어 집니다.

  1. 파라미터의 전달 순서
  2. 파라미터의 전달 방법
  3. 파라미터를 해제 하는 곳

__cdecl (C language) :
  1) 오른쪽에서 왼쪽으로
  2) 스택을 사용
  3) Caller 에서 해제

__stdcall (WIN32 API)
  1) 오른쪽에서 왼쪽으로
  2) 스택을 사용
  3) Callee 에서 해제

__fastcall
  1) 오른쪽에서 왼쪽으로
  2) 두 개 미만의 파라미터인 경우 ecx, edx register 를 사용, 그 이상인 경우 스택을 사용.
  3) Callee 에서 해제

__thiscall (Native C++ Language)
  1) 오른쪽에서 왼쪽으로
  2) 스택을 사용, “this” pointer 는 ecx 를 사용
  3) Callee 에서 해제

직접 눈으로 보면서 확인하죠. (extern “C” 를 사용한 이유는 Function Name 을 쉽게 확인하기 위해서 사용했습니다.)

#include "stdafx.h"

#include <Windows.h>

 

extern "C" void __cdecl                            Func_CDECL(int a, int b);

extern "C" void __stdcall                 Func_STDCALL(int a, int b);

extern "C" void __fastcall        Func_FASTCALL1(int a);

extern "C" void __fastcall        Func_FASTCALL2(int a, int b);

extern "C" void __fastcall        Func_FASTCALL3(int a, int b, int c);

 

void __cdecl            Func_CDECL(int a, int b)

{

                  int c = a+b;

}

 

void __stdcall           Func_STDCALL(int a, int b)

{

                  int c = a+b;

}

 

void __fastcall          Func_FASTCALL1(int a)

{

                  int x = a;

}

 

void __fastcall          Func_FASTCALL2(int a, int b)

{

                  int x = a+b;

}

 

void __fastcall          Func_FASTCALL3(int a, int b, int c)

{

                  int x = a+b+c;

}

 

class CBase

{

public:

                  void __thiscall FUNC_THISCALL(int a, int b);

};

 

void __thiscall CBase::FUNC_THISCALL(int a, int b)

{

                  int c = a+b;

}

 

 

int _tmain(int argc, _TCHAR* argv[])

{

                  Func_CDECL(16, 32);

                  Func_STDCALL(16, 32);

                  Func_FASTCALL1(16);

                  Func_FASTCALL2(16, 32);

                  Func_FASTCALL3(16, 32, 48);

 

                  CBase cObj;

                  cObj.FUNC_THISCALL(16, 32);

 

                  return 0;

}

wmain 의 내용입니다. Function 을 호출(call) 하는 부분은 Highlight 했습니다.

0:000> uf wmain

CallingConvention!wmain

   52 00411380 55              push    ebp

   52 00411381 8bec            mov     ebp,esp

   52 00411383 83ec44          sub     esp,44h

   52 00411386 53              push    ebx

   52 00411387 56              push    esi

   52 00411388 57              push    edi

   53 00411389 6a20            push    20h

   53 0041138b 6a10            push    10h

   53 0041138d e886fdffff      call    CallingConvention!ILT+275(_Func_CDECL) (00411118)

   53 00411392 83c408          add     esp,8

   54 00411395 6a20            push    20h

   54 00411397 6a10            push    10h

   54 00411399 e8cbfcffff      call    CallingConvention!ILT+100(_Func_STDCALL (00411069)

   55 0041139e b910000000      mov     ecx,10h

   55 004113a3 e8dafcffff      call    CallingConvention!ILT+125(Func_FASTCALL1 (00411082)

   56 004113a8 ba20000000      mov     edx,20h

   56 004113ad b910000000      mov     ecx,10h

   56 004113b2 e8d5fcffff      call    CallingConvention!ILT+135(Func_FASTCALL2 (0041108c)

   57 004113b7 6a30            push    30h

   57 004113b9 ba20000000      mov     edx,20h

   57 004113be b910000000      mov     ecx,10h

   57 004113c3 e864fdffff      call    CallingConvention!ILT+295(Func_FASTCALL3 (0041112c)

   60 004113c8 6a20            push    20h

   60 004113ca 6a10            push    10h

   60 004113cc 8d4dff          lea     ecx,[ebp-1]

   60 004113cf e853fdffff      call    CallingConvention!ILT+290(?FUNC_THISCALLCBaseQAEXHHZ) (00411127)

   62 004113d4 33c0            xor     eax,eax

   63 004113d6 5f              pop     edi

   63 004113d7 5e              pop     esi

   63 004113d8 5b              pop     ebx

   63 004113d9 8be5            mov     esp,ebp

   63 004113db 5d              pop     ebp

   63 004113dc c3              ret


첫 번째 CDECL 인 경우, 두 번째 파라미터(20h/32)가 먼저 push 되고, 첫 번 째 파라미터(10h/16) 이 push 됩니다. (오른쪽에서 왼쪽으로 push 됩니다.) 그리고 push 된 파라미터 정리를 Caller(wmain) 에서 정리 하는 것을 볼 수 있습니다. [add esp,8] esp 를 8 byte [두 개의 DWORD 를 push 했으므로] 를 add 하는 것을 알 수 있습니다.

53 00411389 6a20            push    20h

53 0041138b 6a10            push    10h

53 0041138d e886fdffff        call    CallingConvention!ILT+275(_Func_CDECL) (00411118)

53 00411392 83c408          add     esp,8


Func_CDECL 의 내용입니다. Thread Stack 을 정리하는 것[Entry Procedure 및 Exit Procedure] 은 이미 살펴 보았기 때문에, Callee 에서 파라미터와 관련된 아무 일도 안 한다는 것만 확인 하면 됩니다.

0:000> uf 00411118

CallingConvention!Func_CDECL

   14 00411280 55              push    ebp

   14 00411281 8bec            mov     ebp,esp

   14 00411283 83ec44          sub     esp,44h

   14 00411286 53              push    ebx

   14 00411287 56              push    esi

   14 00411288 57              push    edi

   15 00411289 8b4508          mov     eax,dword ptr [ebp+8]

   15 0041128c 03450c          add     eax,dword ptr [ebp+0Ch]

   15 0041128f 8945fc          mov     dword ptr [ebp-4],eax

   16 00411292 5f              pop     edi

   16 00411293 5e              pop     esi

   16 00411294 5b              pop     ebx

   16 00411295 8be5            mov     esp,ebp

   16 00411297 5d              pop     ebp

   16 00411298 c3              ret


Windows API 에서 사용하는 STDCALL(__stdcall) 입니다.
CDECL 과 다르게 파라미터를 정리하는 operation 을 볼 수 없습니다. 다른 것은 CDECL 과 동일합니다.

54 00411395 6a20            push    20h

54 00411397 6a10            push    10h

54 00411399 e8cbfcffff        call    CallingConvention!ILT+100(_Func_STDCALL (00411069)


Func_STDCALL() 의 내용을 보면, Func_CDECL 과 다른 점을 확인할 수 있습니다. 그것은 ret 8 입니다. 앞서 설명한 것처럼 CDECL 과 차이점은 Callee 에서 파라미터를 해제 한다고 했었습니다.
ret 8 은 INTEL CPU 계열에서 STACK 을 정리하기 위한 Operation 으로 보시면 됩니다. 8 byte 만큼 esp 를 더 해줍니다. CDECL 보다 하나의 Operation 이 줄어 든 것을 확인할 수 있습니다.
사실 가장 중요한 것은 STDCALL 은 Callee 에서 파라미터를 해결해준다는 것입니다. 따라서 Caller 에서는 이와 관련된 어떠한 작업을 할 필요 없는 것입니다. 

0:000> uf 00411069

CallingConvention!Func_STDCALL    

19 004112a0 55              push    ebp

   19 004112a1 8bec            mov     ebp,esp

   19 004112a3 83ec44          sub     esp,44h

   19 004112a6 53              push    ebx

   19 004112a7 56              push    esi

   19 004112a8 57              push    edi

   20 004112a9 8b4508          mov     eax,dword ptr [ebp+8]

   20 004112ac 03450c          add     eax,dword ptr [ebp+0Ch]

   20 004112af 8945fc          mov     dword ptr [ebp-4],eax

   21 004112b2 5f              pop     edi

   21 004112b3 5e              pop     esi

   21 004112b4 5b              pop     ebx

   21 004112b5 8be5            mov     esp,ebp

   21 004112b7 5d              pop     ebp

   21 004112b8 c20800          ret     8


FASTCALL 은 이름이 의미하는 것처럼 다른 Calling Convention 보다 빠르게 사용될 수 있습니다. 왜냐하면 파라미터를 레지스터를 통해서 전달하기 때문입니다. 우선 하나의 파라미터를 사용했을 때는 ecx 를 사용합니다. 파라미터 해제는 Callee 에서 해제 합니다만, 하나의 파라미터만 사용했기 때문에(레지스터 ecx 를 사용) 해제할 필요도 없습니다.

55 0041139e b910000000      mov     ecx,10h

55 004113a3 e8dafcffff      call    CallingConvention!ILT+125(Func_FASTCALL1 (00411082)


0:000> uf 00411082

CallingConvention!Func_FASTCALL1

   24 004112d0 55              push    ebp

   24 004112d1 8bec            mov     ebp,esp

   24 004112d3 83ec48          sub     esp,48h

   24 004112d6 53              push    ebx

   24 004112d7 56              push    esi

   24 004112d8 57              push    edi

   24 004112d9 894dfc          mov     dword ptr [ebp-4],ecx

   25 004112dc 8b45fc          mov     eax,dword ptr [ebp-4]

   25 004112df 8945f8          mov     dword ptr [ebp-8],eax

   26 004112e2 5f              pop     edi

   26 004112e3 5e              pop     esi

   26 004112e4 5b              pop     ebx

   26 004112e5 8be5            mov     esp,ebp

   26 004112e7 5d              pop     ebp

   26 004112e8 c3              ret


2개의 파라미터를 사용했을 때는 레지스터 edx, ecx 를 사용합니다. 오른쪽에서 왼쪽으로 호출된 것과 파라미터를 해제할 필요가 없다는 것을 확인하시기 바랍니다.

56 004113a8 ba20000000      mov     edx,20h

56 004113ad b910000000      mov     ecx,10h

56 004113b2 e8d5fcffff      call    CallingConvention!ILT+135(Func_FASTCALL2 (0041108c)


0:000> uf 0041108c

CallingConvention!Func_FASTCALL2

   29 004112f0 55              push    ebp

   29 004112f1 8bec            mov     ebp,esp

   29 004112f3 83ec4c          sub     esp,4Ch

   29 004112f6 53              push    ebx

   29 004112f7 56              push    esi

   29 004112f8 57              push    edi

   29 004112f9 8955f8          mov     dword ptr [ebp-8],edx

   29 004112fc 894dfc          mov     dword ptr [ebp-4],ecx

   30 004112ff 8b45fc          mov     eax,dword ptr [ebp-4]

   30 00411302 0345f8          add     eax,dword ptr [ebp-8]

   30 00411305 8945f4          mov     dword ptr [ebp-0Ch],eax

   31 00411308 5f              pop     edi

   31 00411309 5e              pop     esi

   31 0041130a 5b              pop     ebx

   31 0041130b 8be5            mov     esp,ebp

   31 0041130d 5d              pop     ebp

   31 0041130e c3              ret


이제 3개 이상일 경우 FASTCALL 은 어떻게 호출 될까요? 레지스터 ecx, edx 만 사용하기 때문에 3번째 파라미터 부터는 스택을 사용해야 합니다. 당연히 파라미터 해제는 스택을 사용한 3번째 파라미터에 대한 해제 작업을 하면 됩니다. [ret 4]
이제 FASTCALL 의 장점을 아실 수 있을 듯 합니다. 파라미터 2개와 3개 이상의 차이점을 확인하실 수 있겠죠?

57 004113b7 6a30            push    30h

57 004113b9 ba20000000      mov     edx,20h

57 004113be b910000000      mov     ecx,10h

57 004113c3 e864fdffff      call    CallingConvention!ILT+295(Func_FASTCALL3 (0041112c)


0:000> uf 0041112c

CallingConvention!Func_FASTCALL3

   34 00411320 55              push    ebp

   34 00411321 8bec            mov     ebp,esp

   34 00411323 83ec4c          sub     esp,4Ch

   34 00411326 53              push    ebx

   34 00411327 56              push    esi

   34 00411328 57              push    edi

   34 00411329 8955f8          mov     dword ptr [ebp-8],edx

   34 0041132c 894dfc          mov     dword ptr [ebp-4],ecx

   35 0041132f 8b45fc          mov     eax,dword ptr [ebp-4]

   35 00411332 0345f8          add     eax,dword ptr [ebp-8]

   35 00411335 034508          add     eax,dword ptr [ebp+8]

   35 00411338 8945f4          mov     dword ptr [ebp-0Ch],eax

   36 0041133b 5f              pop     edi

   36 0041133c 5e              pop     esi

   36 0041133d 5b              pop     ebx

   36 0041133e 8be5            mov     esp,ebp

   36 00411340 5d              pop     ebp

   36 00411341 c20400          ret     4


THISCALL 은 this pointer 를 위해서 레지스터 ecx 를 사용한다는 정도로 정리하도록 하지요.

60 004113c8 6a20            push    20h

   60 004113ca 6a10            push    10h

   60 004113cc 8d4dff          lea     ecx,[ebp-1]


덧글

댓글 입력 영역