출처: http://chungsm.tistory.com/27

Level of Difficulty     1  2   3

Calling Convention의 경우 대부분 함수호출 방식을 함께 정의한 Header를 제공함으로서 사용자가 API호출 규약을 따르도록 제공하고 있습니다.
프로그램적으로 강제할 수 있는 부분이 아니기 때문에 약속이 필요한 것입니다.

프로그램의 호환성등의 이유로 여러가지 호출방식이 사용되고 있는데 __stdcall, __cdecl, __fastcall, __thiscall등이 있으며 파라메터 전달과 처리 방법에 차이 가 있습니다.
(Win32 API는 WINAPI(__stdcall)형식으로 대부분 호출된다.)

위의 호출방식들이 stack에 파라메터를 저장하는 순서는 모두 마지막 파라메터(오른쪽) 부터 처음 파라메터(왼쪽)순으로 저장하게 됩니다. 파라메터의 접근은 모든 함수호출시에 스택포인터(ESP)의 기준(EBP)을 다시 설정하고 EBP를 기준으로 접근하게 됩니다.
stack address는 높은주소에서 낮은주소로 증가되므로 EBP + X값이 파라메터에 접근하는 값이 되는 것입니다.



__stdcall
(WINAPI)는 함수종료시 함수내부에 파라메터를 전달 받은 stack을 직접 정리(stack 포인터를 이전 상태로 올림)합니다.

184:      TestFunc(1, 2);
0040165D   push        2
0040165F   push        1

00401661   call        TestFunc (0040161b)


173:  DWORD __stdcall TestFunc(DWORD dw1, DWORD dw2)
174:  {

0040161B   push        ebp
0040161C   mov         ebp,esp

0040161E   push        ecx
0040161F   mov         dword ptr [ebp-4],0CCCCCCCCh
175:      DWORD dw = 0;
00401626   mov         dword ptr [dw],0
176:      dw = dw1 + dw2;
0040162D   mov         eax,dword ptr [dw1]
00401630   add         eax,dword ptr [dw2]
00401633   mov         dword ptr [dw],eax
177:      return dw;
00401636   mov         eax,dword ptr [dw]
178:  }
00401639   mov         esp,ebp
0040163B   pop         ebp

0040163C   ret         8

__stdcall로 함수를 호출했을 경우에 disassembly 코드를 보면 위와 같습니다.
가장 아래부분에 빨강색으로 강조된 "ret 8"을 보면 TestFunc()함수 자체적으로 종료시에 stack을 8(DWROD dw1 + DWORD dw2)만큼 정리해서 리턴하는 OPCODE를 호출하는 것을 확인할 수 있습니다.



 

__cdecl의 경우에는 함수를 호출하는 쪽에서 stack을 직접 정리하는 방식입니다.

184:      TestFunc(1, 2);
0040165B   push        2
0040165D   push        1
0040165F   call        TestFunc (0040161b)
00401664   add         esp,8


173:  DWORD __cdecl TestFunc(DWORD dw1, DWORD dw2)
174:  {

0040161B   push        ebp
0040161C   mov         ebp,esp

0040161E   push        ecx
0040161F   mov         dword ptr [ebp-4],0CCCCCCCCh
175:      DWORD dw = 0;
00401626   mov         dword ptr [dw],0
176:      dw = dw1 + dw2;
0040162D   mov         eax,dword ptr [dw1]
00401630   add         eax,dword ptr [dw2]
00401633   mov         dword ptr [dw],eax
177:      return dw;
00401636   mov         eax,dword ptr [dw]
178:  }
00401639   mov         esp,ebp
0040163B   pop         ebp

0040163C   ret

__cdecl의 경우 상위에 "0040165F   call   TestFunc (0040161b)"이후에 함수가 종료된후 해당함수를 호출한 쪽에서 "add   esp,8"를 직접 호출해서 stack을 정리하는 것을 알 수 있습니다.



__fastcall의 경우는 stack이 아닌 CPU register(ECX, EDX)를 이용해서 전달하는 방식으로 2개 이후부터는 stack을 사용합니다.
(64bit에서는 위의 모든 방식이 fastcall과 유사하게 register를 통해서 절달하는 방식으로 통일되었습니다. ^^)

184:      TestFunc(1, 2);
00401671   mov         edx,2
00401676   mov         ecx,1

0040167B   call        TestFunc (0040161b)


173:  DWORD __fastcall TestFunc(DWORD dw1, DWORD dw2)
174:  {

0040161B   push        ebp
0040161C   mov         ebp,esp

0040161E   sub         esp,0Ch
00401621   mov         dword ptr [ebp-0Ch],0CCCCCCCCh
00401628   mov         dword ptr [ebp-8],0CCCCCCCCh
0040162F   mov         dword ptr [ebp-4],0CCCCCCCCh
00401636   mov         dword ptr [ebp-0Ch],edx
00401639   mov         dword ptr [ebp-8],ecx
175:      DWORD dw = 0;
0040163C   mov         dword ptr [dw],0
176:      dw = dw1 + dw2;
00401643   mov         eax,dword ptr [dw1]
00401646   add         eax,dword ptr [dw2]
00401649   mov         dword ptr [dw],eax
177:      return dw;
0040164C   mov         eax,dword ptr [dw]
178:  }
0040164F   mov         esp,ebp
00401651   pop         ebp
00401652   ret


__fastcall의 경우 파라메터 전달을 위해서 CPU의 레지스터인 edx, ecx에 함수호출전 값을 대입하는 것을 알 수 있습니다.



__thiscall
의 경우는 c++의 class에서 사용되는 호출방식으로 별도로 함수에 직접 사용할 수 없으며 ECX레지스터에 클래스포인터(this)를 전달합니다. 파라메터 전달 방식과 처리는 __stdcall과 동일합니다.



이러한 호출규약이 틀릴경우에는 호출된 함수가 종료되는 시점에 전달받은 파라메터를 저장하고 있는 stack을 정리하면서 오류가 발생하게 됩니다.
예를 들면 WINAPI형식으로 구현된 함수를 CDECL형식으로 호출하면 함수내부에서 1차적으로 ESP(stack pointer)를 정리해서 리턴하도록 컴파일 되어 있는데 호출한 쪽에서도 stack을 정리하는 코드가 들어있어서 함수 종료 시 오류가 발생하게 되는 것입니다.
그 반대의 경우도 있겠죠.

"VOID WINAPI MyFunc()" 같이 정의된 헤더를 제공함으로서 WINAPI형식으로 호출 하도록 상호 약속을 하는 것입니다.
묵시적으로 외부에 제공되는 함수는 Win32API들 처럼 WINAPI(__stdcall)형식으로 제공하는게 대부분입니다.
즉 이경우 stack에 대한 정리는 API내부에서 처리하겠다는 의미로 생각 할 수 있겠습니다.

여기서 한가지 생각해 볼 것이 있는데 printf()같은 함수의 경우처럼 입력되는 파라메터가 가변적일 경우입니다.

printf("%s, %s, %s, %s, %s", szName, szAddress, szTemp, szTemp2, szTemp3);

위의 경우 파라메터로 5개의 문자열 포인터가 전달되었지만 이보다 더 많거나 혹은 하나만 전달될 경우도 있다는 것입니다.

이 경우 함수(printf())에서는 stack을 정리하는 코드가 미리 컴파일 되어서 생성 될 수 없겠죠.
stack을 통해서 파라메터가 얼마나 전달될지 모르는 경우이기 때문입니다.

이 경우에는 함수를 호출하는 쪽(파라메터를 입력한)에서 stack을 정리하도록 CDECL(__cdecl)형식으로 함수를 호출하고 사용해야 하는 것입니다.

Visual C++ 에서 기본적인(명시적인 정의가 없을때) 함수호출 방식이 __cdecl로 설정 되어 있습니다.
헤더에는 __stdcall로 선언하고 실제 구현부(.cpp)에는 아무런 선언을 안해놓으면 컴파일러는 __cdecl로 처리하게 되어서 아래와 같은 오류를 표시하게 됩니다.

error C2373: 'MyFunc' : redefinition; different type modifiers

함수명이 중복(호출방식은 다르므로)으로 재선언 되었다는 오류메세지입니다.
경력이 오래되시 개발자 중에서도 이 메세지에 당황하는 경우를 많이 보았습니다 -_-;
선언과 구현부는 되도록 동일하게 구현하는 습관을 가져야 겠죠..

AND