언젠가 정리해야지 하면서 이제야 정리하게 되었다. C나 C++를 하면서 함수 호출 규약은 적어도 한번은 들어보았을 것이다. 사실 이부분은 그냥 넘어가기 쉬운 부분이다.
그러나 컴파일러간, C와 C, C++와 C++, C와 C++간에 반드시 확인해서 넘어가야할 부분이다. 이부분은 언제 어디서 문제가 발생하지 모르기 때문이다. 그리고 API 정의할때도 반드시 확인해야하는 부분이기도 하다.
이 글에서는 X86환경에서 VC를 사용하여 확인하였다. 다른 컴파일러나 환경에서는 다를 수 있으니 주의하기 바란다.
함수 호출 규약 종류
여기서는 주로 VC에서 사용하는 함수 호출 규약을 살펴보기로 하겠다. 나머지 호출 규약은 추후에 계속 갱신할 계획이다.
VC에서 제공하는 함수 호출 규약은
- __cdecl
- __fastcall
- __stdcall
- __thiscall
이들을 구분하는 기준은 아래 3가지에 의해서 구분된다.
- 함수 인자 넘겨지는 방법(스택에 쌓이는 순서)
- 스택 정리하는 위치
- this객체 넘기는 방법(C++ only)
함 수 호출할 때에 함수에 넘겨지는 인자가 스택에 어떻게 쌓이는지 나타내는 것이다. 대부분 오른쪽에 왼쪽으로 스택에 저장되기 때문에 해당 호출 규약을 구분하기에는 모호한 부분이 있다. 그래서 공통된 이부분을 먼저 다루고 어셈블리에서 어떻게 사용되는를 살펴보겠다. 어셈블리에 대한 자세한 설명도 같이 하겠다.
하나씩 들어가기 전에 먼저 3가지 구분에 따라서 함수 호출 규약들을 정리해보았다.
구분 |
인자 넘기는 순서 | 스택 정리하는 위치 | this 넘기는 방법 (C++ only) |
Name Mangling | 비고 |
---|---|---|---|---|---|
cdecl | <--- | Caller | 스택 맨 위에 저장 | _someFunc | |
fastcall | 첫번째, 두번재 인자를 각각 ECX, EDX에 저장 그 외는 <--- |
Callee | ECX에 저장 첫번째 인자만 EDX에 저장됨 |
_someFunc@12 | |
stdcall | <--- | Callee | 스택 맨 위에 저장 | @someFunc@12 | |
thiscall | <--- | Callee | ECX에 저장 | 없음 | C++에서만 사용 가능 |
호출 규약들 간에 공통적으로 인자는 왼쪽에서 오른쪽 방향으로 스택에 저장이 된다. 단 fastcall만 몇개 인자가 레지스트리를 통해서 넘겨지는 것만 틀리다. 여기서는 다루지 않았지만 pascal이라는 함수 호출 규약이 있다. 이 함수 호출 규약이 인자는 오른쪽에서 왼쪽으로 스택에 저장되며, 스택 정리는 callee에서 수행되는 규약이다.
Caller와 Callee는 main함수에서 someFunc함수를 호출한다고 하면, someFunc함수 입장에서 보면 main함수는 caller가 되고 someFunc함수는 callee가 된다.
호출 규약 중에서 thiscall은 c 함수에서는 사용할 수 없다. 그렇기 때문에 C++에서만 확인했다.
그외 몇가지 함수 호출 규약이 있지만, 주로 사용하는 것만 다루었다. 그리고 지금은 거의 사용하지 않는 것은 다루지 않았다.
스택은 caller가 callee를 호출하기 전과 callee가 caller로 반환하기 전으로 2가지 그림을 보여줄 것이다.
cdecl
cdecl는 많은 x86 C 환경에서 사용되는 함수 호출 규약이다. 리눅스에서 de-facto 표준이라고 할 수 있다. cdecl이 가변인자를 처리할 수 있는 규약이다. 즉 모든 인자가 스택에 저장된다. VC에서는 C 함수에 별다른 함수 호출 규약을 지정하지 않으면 cdecl를 기본으로 지정된다.
cdecl호출규약의 특징은,
- 인자는 오른쪽에서 왼쪽으로 스택에 저장되서 넘겨짐
- 최종 스택 정리는 호출자(Caller)에서 수행함
- this는 인자가 저장된후 스택 맨 위에 저장됨
C언어
someFunc함수 호출 전에 main함수에서 인자를 스택에 저장한다. 그리고 someFunc함수를 호출한다.
int ret1 = someFunc (1, 2, 3); // (1) 일반함수 호출
002A356E push 3
002A3570 push 2
002A3572 push 1
002A3574 call 002A10E6
someFunc함수에서 연산 작업을 수행하고, 스택 포인터(ESP)도 함수 호출 전으로 복귀한다음에 반환한다.
int TestAPI someFunc(int p1, int p2, int p3)
{
00821390 push ebp
00821391 mov ebp,esp
...(중략)
}
...(중략)
008213BA mov esp,ebp
008213BC pop ebp
008213BD ret
다시 main 함수로 돌아와서 나머지 스택에 남아있는 인자도 정리한다. 즉, caller에서 스택을 정리한다.
002A3579 add esp,0Ch // 스택 정리
002A357C mov dword ptr [ebp-8],eax
C++언어
c 언어와 마찬가지로 인자를 스택에 저장한다. 그리고 this객체를 스택에 저장한다.
그런 후에 someMemFunc 함수를 호출한다.
int ret2 = someClass.someMemFunc (1, 2, 3); // (2) 멤버함수 호출
002A3587 push 3
002A3589 push 2
002A358B push 1
002A358D lea eax,[ebp-14h] // someClass
002A3590 push eax // this push
002A3591 call 002A11D6
someMemFunc 함수에서 this객체는 ebp+8를 참조해서 처리하게 된다.
int TestAPI someMemFunc(int p1, int p2, int p3)
{
002A13C0 push ebp
002A13C1 mov ebp,esp
...(중략)
return this->val;
002A13EC mov eax,dword ptr [ebp+8] // this를 ebp+8에서 참조
002A13EF mov eax,dword ptr [eax]
}
someMemFunc 함수 연산이 종료되고 함수가 리턴 될때 스택 포인터(ESP)가 처음 호출할 때의 위치로 이동하게 된다.
...(중략)
002A13F4 mov esp,ebp
002A13F6 pop ebp
002A13F7 ret
다시 main 함수로 돌아와 나머지 스택에 있는 인자를 정리한다. caller에서 스택 정리.
002A3596 add esp,10h // 스택 정리
002A3599 mov dword ptr [ebp-20h],eax
fastcall
fastcall은 함수 호출을 좀 더 빨리처리하기 위한 호출 규약이다. 별다른 방법이 있는 것이 아니라 인자를 스택에 넣는 것이 아니라 래지스트리에 넣는다. 당연히 레지스트리로 처리하기 때문에 빠를 수 밖에 없다. 이는 표준이 없기때문에 주의를 요한다.
단, 인자가 몇 개 이내 일경우에 유리하며, 그 이상이면 크게 차이는 없다.
fastcall의 특징을 살펴보면;
- 인자는 오른쪽에서 왼쪽으로 스택에 저장되서 넘겨지지만, C에서 첫번째 인자와 두번째 인자는 ECX, EDX에 저장되며, C++인 경우 첫번재 인자만 EDX에 저장됨
- 최종 스택 정리는 호출자(Callee)에서 수행함
- this는 인자가 ECX에 저장됨
C언어
someFunc 함수 호출 전에 첫번째 인자와 두번째 인자를 각각 ECX와 EDX에 저정한다. 그 이상이 되는 인자를 스택에 저장하고 someFunc 함수를 호출한다.
int ret1 = someFunc (1, 2, 3); // (1) 일반함수 호출
0116356E push 3
01163570 mov edx,2
01163575 mov ecx,1
0116357A call 011611DB
someFunc 함수가 실행되고, 연산이 끝나면 함수 포인터(ESP)가 함수 시작할 때 위치로 변경된다.
int TestAPI someFunc(int p1, int p2, int p3)
{
01161400 push ebp
01161401 mov ebp,esp
...(중략)
}
...(중략)
01161432 mov esp,ebp
01161434 pop ebp
마지막으로 someFunc함수에서 리턴될때에 스택 포인터를 조작한다. 즉, 스택 포인터에 있는 인자를 모두 제거하게 된다. 이는 callee에서 스택을 정리한 모습니다.
01161435 ret 4
main함수로 돌아와서는 결과를 저장한다.
0116357F mov dword ptr [ebp-8],eax
C++언어
someMemFunc함수를 호출하기 전에 인자를 레지스트리와 스택에 저장한다. C와 틀린 것은 첫번재 인자만 EDX 레지스트리에 저장하고 나머지 인자를 오른쪽에서 왼쪽으로 스택에 저장해서 넘겨지게 된다. ECX레지스트리에는 this 가 담겨서 넘겨지게 된다.
int ret2 = someClass.someMemFunc (1, 2, 3); // (2) 멤버함수 호출
0116358A push 3
0116358C push 2
0116358E mov edx,1
01163593 lea ecx,[ebp-14h] // someClass(this)를 ecx에 저장
01163596 call 011611E0
someMemFunc 함수가 호출되서 실행된 후에 스택 위치를 함수 처음 실행할 때 위치로 복원한다. C++이므로 this는 ecx를 참조하게 된다. 중간에 ecx를 [ebp-14h]에 저장했기 때문에 this에 대한 참조는 [ebp-14h]를 참고하면 된다.
int TestAPI someMemFunc(int p1, int p2, int p3)
{
00171500 push ebp
00171501 mov ebp,esp
...(중략)
00171523 mov dword ptr [ebp-14h],ecx
...(중략)
return this->val;
00171534 mov eax,dword ptr [ebp-14h]
00171537 mov eax,dword ptr [eax]
}
...(중략)
0017153C mov esp,ebp
0017153E pop ebp
C함수 호출과 마찬가지로 스택 정리를 callee(someMemFunc함수)에서 실행하고 리턴한다.
0017153F ret 8
main함수로 돌아과 결과를 저장한다.
0116359B mov dword ptr [ebp-20h],eax
stdcall
가장 표준적인 호출 규약(?)이다. parscal이라는 함수 호출규약의 변형으로 MS의 Win32 API와 Open Watcom C++에서 사용되는 기본 호출 규약이라고 보면 된다.
stdcall의 특징을 살펴보면;
- 인자는 오른쪽에서 왼쪽으로 스택에 저장되서 넘겨짐
- 최종 스택 정리는 호출자(Callee)에서 수행함
- this는 인자가 인자가 저장된 후 스택 맨위에 저장됨
C언어
someFunc함수를 호출하기 전에 인자를 오른쪽에서 왼쪽 방향으로 스택에 저장한다.
int ret1 = someFunc (1, 2, 3); // (1) 일반함수 호출
001F356E push 3
001F3570 push 2
001F3572 push 1
001F3574 call 001F11F9
someFunc함수가 호출되서 연산이 실행된 후에 스택에서 사용했던 모든 데이터를 지우고 함수 처음 스택 위치로 이동한다.
int TestAPI someFunc(int p1, int p2, int p3)
{
001F1400 push ebp
001F1401 mov ebp,esp
...(중략)
}
...(중략)
001F142A mov esp,ebp
001F142C pop ebp
someFunc함수가 리턴될 때에 스택에 남아있는 모든 인자를 지우고 스택을 정리한다. callee에서 스택을 정리.
001F142D ret 0Ch
main함수로 돌아와서 결과를 저장한다.
001F3579 mov dword ptr [ebp-8],eax
C++언어
someMemFunc함수를 호출하기 전에 인자를 오른쪽에서 왼쪽으로 스택에 저장한다. 그리고 this가 스택 맨 위에 저장된다.
int ret2 = someClass.someMemFunc (1, 2, 3); // (2) 멤버함수 호출
001F3584 push 3
001F3586 push 2
001F3588 push 1
001F358A lea eax,[ebp-14h]
001F358D push eax
001F358E call 001F11FE
someMemFunc함수가 실행된다. this에 대한 참조는 스택에 있는 [ebp+8]를 참고하면 된다. 이는 처음 스택에 저장된 this 위치를 가리킨다.
int TestAPI someMemFunc(int p1, int p2, int p3)
{
001F1AD0 push ebp
001F1AD1 mov ebp,esp
...(중략)
return this->val;
001F1AFC mov eax,dword ptr [ebp+8]
001F1AFF mov eax,dword ptr [eax]
}
someMemFunc함수가 종료될 때에 스택에 저장된 데이터를 지우고 함수가 처음 실행될 때의 스택 위치로 이동한다.
...(중략)
001F1B04 mov esp,ebp
001F1B06 pop ebp
someMemFunc함수가 리턴될 때에 스택에 남아 있는 모든 인자를 제거하고 반환된다. callee에서 스택을 정리.
001F1B07 ret 10h
main함수로 돌아와서 결과를 저장한다.
001F3593 mov dword ptr [ebp-20h],eax
thiscall
VC에서 C++에서 정적함수가 아닌 멤버함수에만 사용가능한 함수 호출 규약이다. 이 규약은 컴파일러에 따라서, 가변 인자 사용 여부에 따라서 두가지 버전이 존재한다.
GCC인 경우 thiscall을 거의 cdecl로 인지한다. VC인 경우는 stdcall고 비슷하지만 this 처리만 다를 뿐이다. VC에서 C++ 멤버함수에 별다른 함수 호출 규약을 지정하지 않으면 thiscall를 사용한다.
thiscall의 특징을 보면;
- 인자는 오른쪽에서 왼쪽으로 스택에 저장되서 넘겨짐
- 최종 스택 정리는 호출자(Callee)에서 수행함
- this는 ECX 레지스트리 저장되서 넘겨짐
C++언어
main함수에서 someMemFunc함수를 호출하기 전에 인자를 오른쪽에서 왼쪽으로 스택에 저장된다. 그리고 this도 ecx 레지스트리에 저장한다.
int ret2 = someClass.someMemFunc (1, 2, 3); // (2) 멤버함수 호출
00CC1FA6 push 3
00CC1FA8 push 2
00CC1FAA push 1
00CC1FAC lea ecx,[ebp-8]
00CC1FAF call 00CC1203
someMemFunc함수가 실행된다. this는 ecx에서 [ebp-8]에 저장되고 this는 [ebp-8]을 참조하면 된다. 그리고, 함수 연산이 완료되면 스택에 있는 모든 데이터가 제거되고 함수 호출할 때의 스택 위치로 이동한다.
int TestAPI someMemFunc(int p1, int p2, int p3)
{
00CC1B10 push ebp
00CC1B11 mov ebp,esp
...(중략)
00CC1B30 mov dword ptr [ebp-8],ecx
...(중략)
return this->val;
00CC1B41 mov eax,dword ptr [ebp-8]
00CC1B44 mov eax,dword ptr [eax]
}
...(중략)
00CC1B49 mov esp,ebp
00CC1B4B pop ebp
someMemFunc함수가 리턴될 때에 스택에 남아 있는 인자를 모두 제거한다. callee에서 스택을 정리.
00CC1B4C ret 0Ch
main함수로 돌아와서 결과를 저장한다.
00CC1FB4 mov dword ptr [ebp-14h],eax
결론
여기서 다룬 내용을 맹신하지 말라. 여러가지 변수에 의해서 실제 어셈블리 코드가 달라질 수 있고, 운영체제 별로 달라질 수 있는 부분이다. 특이 fastcall인 경우 VC이 Boland와 차이가 있다. 예를 들어 VC는 2개 인자가 레지스트리로 넘겨지지만 Boland는 3개 인자가 레지스트리로 넘겨진다. 이 것은 분명히 다르다. 당연히 GCC도 차이가 발생할 것이다.
이런 부분은 본인이 직접 확인해볼 수 밖에 없다. 모든 컴파일러와 환경에 대해서 다루기는 너무 힘들기 때문이다. 이 글에서는 다른 함수 호출 규약과는 다르게 this 객체를 더 추가해서 다루었다. C++를 주로 하다보니 이런 부분이 궁금했었기 때문이다.
이를 이용하면 C++에서 C함수를 호출하면서 C++의 this를 C함수의 인자로 넘겨줄 수 도 있다. 물론 이런 별로 좋은 방법은 아니다. 그냥 그렇 수 있다는 것일 뿐이다.
이상으로 이 글을 마친다. ospace.
참조
[1] 신영진, 2006, "함수 호출 규약", http://www.jiniya.net/lecture/techbox/callconv.html
[2] ?, "함수 호출 규약, Function Calling Convention (1/2)", http://beforu.egloos.com/2117375
[3] Winapi, "16-1-다.호출 규약", http://winapi.co.kr/clec/cpp2/16-1-3.htm
[4] Declaring Attributes of Functions, http://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html
[5] Calling convention, http://en.wikipedia.org/wiki/Calling_convention
[6] x86 calling conventions, http://en.wikipedia.org/wiki/X86_calling_conventions
[7] Name mangling, http://d.mumbi.net/cpp:name-mangling
'3.구현 > C or C++' 카테고리의 다른 글
[C++] 파일 읽기 성능 비교 (0) | 2011.12.28 |
---|---|
C++ Delegate 구현원리 (2) | 2011.01.17 |
Thunk와 용법3 - 인터페이스 프록시 (0) | 2011.01.12 |
Thunk와 용법2 - 콜백함수를 멤버함수로 사용하기 (0) | 2011.01.12 |
Thunk와 용법1 - 다중 상속에서 가상함수 (2) | 2011.01.12 |