Calling Convention
April 29, 2019
C 언어에서 함수를 호출하는 규약에 대해 설명
종류
함수 호출규약에는 다음의 것들이 있다.
Calling Convention | Argument Passing | Stack Maintenance | Name Decoration (C only) | Notes |
---|---|---|---|---|
__cdecl | Right to left. | Calling function pops arguments from the stack. | Underscore prefixed to function names. Ex: _Foo. | This is the default calling convention for C/C++ |
__stdcall | Right to left. | Called function pops its own arguments from the stack. | Underscore prefixed to function name, @ appended followed by the number of decimal bytes in the argument list. Ex: _Foo@10. | This is the almost system calling convention. |
__fastcall | First two DWORD arguments are passed in ECX and EDX, the rest are passed right to left. | Called function pops its own arguments from the stack. | A @ is prefixed to the name, @ appended followed by the number of decimal bytes in the argument list. Ex: @Foo@10. | Only applies to Intel CPUs. This is the default calling convention for Borland Delphi compilers. |
thiscall | this pointer put in ECX, arguments passed right to left. | Called function pops its own arguments from the stack. | None. | Used automatically by C++ code. Used by Com |
naked | Right to left. | Calling function pops arguments from the stack. | None. | Used by VxDs. Used by Custom Prolog and Epilog |
가장 보편적인 방식은 stdcall 과 cdecl 이다. 이 두 호출방식의 가장 큰 차이점은 스택을 누가 정리하느냐이다.
stdcall 의 경우 callee 에서 스택을 정리하므로 caller 와 callee 모두 파라메터의 개수를 알고 있어야 한다.
반면 cdecl 의 경우 caller 에서 스택을 정리하므로 callee 는 파라메터의 개수를 정확히 몰라도 된다. 바로 이 점에 C 언어의 가변인자(Variable Argument) 를 가능케 하는 것이다. printf 같은 함수를 생각해보라.
프롤로그(prolog) 및 에필로그(epilog)
함수 호출은 크게 다음과 같이 나뉘어 진다.
- 함수가 사용할 파라메터를 스택에 넣고 함수 시작지점으로 점프(함수 호출)한다.
- 함수 내에서 사용할 스택프레임을 설정한다.
- 함수의 내용을 수행한다.
- 수행을 마치고 처음 호출한 지점으로 돌아가기 위해 스택을 복원한다.
- 호출한 지점의 다음 라인으로 점프한다.
이때 2번 과정을 프롤로그(prolog) 라고 부르며, 4번 과정을 에필로그(epilog) 라고 부른다. 보면 알겠지만 스택프레임의 설정과 복원과 관계가 있다.
__cdecl
특징
- 스택에 파라메터 삽입 순서 : right → left
- 스택의 정리를 호출한 함수(caller)에서 수행한다. 따라서 가변인자를 사용할 수 있다.
- Name Mangling : 함수 이름 앞에 _ 추가
ex) _Foo - C / C++ 언어의 기본 함수 호출규약
아래 간단한 C 프로그램이 있다.
int foo( int a, int b )
{
int c = a + b;
return c;
}
int main()
{
foo( 1, 2 );
return 0;
}
그리고 cdecl 호출 규약으로 살펴본 어셈블리 코드는 아래와 같다. 1)
int __cdecl foo( int a, int b )
{
/* (2) caller 의 ebp 를 저장하고 callee (foo) 의 ebp 를 확보
push ebp
mov ebp, esp
push ecx ; local 변수 c 의 자리 확보
*/
int c = a + b;
/* (3) 계산
mov eax, [ebp + 8] ; a 를 eax 에 넣고
add eax, [ebp + 12] ; b 를 더한 후
mov [ebp - 4], eax ; c 에 저장
*/
return c;
/* (4) 리턴값 저장
mov eax, [ebp - 4]
*/
}
/* (5) foo 함수 종료 후 caller 의 ebp, esp 복구
mov esp, ebp
pop ebp
ret
*/
int main()
{
foo( 1, 2 );
/* (1) foo 함수 호출
push 2
push 1
call foo (128102Dh)
add esp, 8 ; (6) <<스택을 정리하는 부분>>
*/
return 0;
}
위 코드를 보면 역순으로 파라메터를 push 하고 함수호출을 한 후, 이어지는 다음 라인에서 스택을 정리함을 알 수 있다. 즉 caller 가 직접 스택을 정리한다.
- 파라메터를 역순으로 push 하고 함수를 호출한다. 이때 리턴주소 (call 호출 다음라인) 를 push 하고 함수주소로 jump 한다.
- caller 의 ebp 를 저장하고 callee 의 ebp 를 새로 확보한다. 이 새로운 ebp 를 이용하여 foo 함수 내에서 파라메터 및 local 변수로의 접근을 시도할 것이다. 그리고 local 변수에 사용할 스택을 할당하는데 여기서는 4바이트 변수 하나이므로 단순히 ecx 를 push 함으로써 이를 수행한다. 하지만 local 변수들이 8바이트 (혹은 그 이상) 일 경우 “sub esp, 8” 과 같이 스택을 할당하게 된다.
- 계산을 수행하고 결과값을 local 변수에 저장한다. 이때 ebp 를 이용하여 파라메터 및 local 변수로 접근하는 것을 알 수 있다.
- 리턴할 값을 eax 에 저장한다.
- 이제 foo 수행은 모두 끝났다. caller 의 esp, ebp 를 복구하고 스택에 저장되어 있는 다음 주소 (main 함수에서 foo 호출한 부분의 다음주소) 를 꺼내어 jump 한다.
- cdecl 호출규약은 caller 가 스택을 정리한다.
__stdcall
특징
- 스택에 파라메터 삽입 순서 : right → left
- 스택의 정리를 호출된 함수(callee)에서 직접 수행한다.
- Name Mangling : 함수 이름 앞에 _ 추가. 함수 이름 뒤어 @ 를 붙이고 매개변수의 전체 바이트수를 10진수로 표기
ex) _Foo@12 - 거의 모든 시스템 함수(WinAPI) 에서 사용. 파스칼 및 베이직 언어에서 사용하는 방식
어셈블리 코드는 cdecl 과 기본적으로 동일하고 (6)스택을 정리하는 부분만 다르다.
int __stdcall foo( int a, int b )
{
/* (2) caller 의 ebp 를 저장하고 callee (foo) 의 ebp 를 확보
push ebp
mov ebp, esp
push ecx ; local 변수 c 의 자리 확보
*/
int c = a + b;
/* (3) 계산
mov eax, [ebp + 8] ; a 를 eax 에 넣고
add eax, [ebp + 12] ; b 를 더한 후
mov [ebp - 4], eax ; c 에 저장
*/
return c;
/* (4) 리턴값 저장
mov eax, [ebp - 4]
*/
}
/* (5) foo 함수 종료 후 caller 의 ebp, esp 복구
mov esp, ebp
pop ebp
ret 8 ; (6) <<스택을 정리하는 부분>>
*/
int main()
{
foo( 1, 2 );
/* (1) foo 함수 호출
push 2
push 1
call foo (128102Dh)
*/
return 0;
}
__fastcall
특징
- 스택에 파라메터 삽입 순서 : 처음 두개의 파라메터를 각각 ecx, edx 에 저장. 그 외 나머지는 right → left 순서로 스택에…
- 스택의 정리를 호출된 함수(callee)에서 직접 수행한다.
- Name Mangling : 함수 이름 앞과 끝에 @ 이 붙고 뒤에 매개변수의 전체 바이트수를 10진수로 표기
ex) @Foo@12
어셈블리 코드는 기본적으로 stdcall 과 같지만 (1)caller 의 파라메터 삽입, (2)callee 의 파라메터 획득 과정이 다르다. 그리고 아래 코드의 경우 (3)계산 과정도 살짝 달라지게 된다.
// 예를 들기 위해 사용하지 않는 파라메터 ex1, ex2 추가
int foo( int a, int b, int ex1, int ex2 )
{
/* (2) caller 의 ebp 를 저장하고 callee (foo) 의 ebp 를 확보
push ebp
mov ebp, esp
sub esp, 12 ; local 변수 c 와 ecx, edx 로 넘어온 변수 저장을 위해 12 바이트 local 스택을 할당
mov [ebp - 12], edx ; edx (변수 b) 를 local 스택에 저장
mov [ebp - 8], ecx ; ecx (변수 a) 를 local 스택에 저장
*/
int c = a + b;
/* (3) 계산
mov eax, [ebp - 8] ; a 를 eax 에 넣고
add eax, [ebp - 12] ; b 를 더한 후
mov [ebp - 4], eax ; c 에 저장
*/
return c;
/* (4) 리턴값 저장
mov eax, [ebp - 4]
*/
}
/* (5) foo 함수 종료 후 caller 의 ebp, esp 복구
mov esp, ebp
pop ebp
ret 8 ; (6) <<스택을 정리하는 부분>>
*/
int main()
{
foo( 1, 2, 3, 4 );
/* (1) foo 함수 호출. 처음 두개의 파라메터를 각각 ecx, edx 에 넣고 나머지는 스택에 넣는다.
push 4
push 3
mov edx, 2
mov ecx, 1
call foo (128102Dh)
*/
return 0;
}
thiscall
thiscall 은 클래스에서 동작하는 함수 호출규약이다.
특징
- 스택에 파라메터 삽입 순서 : right → left, this포인터는 ecx 에 저장
- 스택의 정리를 호출된 함수(callee)에서 직접 수행한다.
- 모든 파라메터는 스택으로 전달되고 this 포인터만 ecx 레지스터를 통해 전달된다. thiscall 호출규약은 명시적으로 지정할 수 없으며, 가변인자를 사용하지 않는 클래스 멤버함수가 기본적으로 사용하는 호출규약이다. 클래스 멤버함수가 가변인자를 사용할 경우 컴파일 시점에 호출규약이 __cdecl 로 변경된다.
어셈블리 코드
class CThiscall
{
public:
int foo( int a, int b )
{
/* (2) caller 의 ebp 를 저장하고 callee (foo) 의 ebp 를 확보
push ebp
mov ebp, esp
sub esp, 8 ; local 변수 c 와 this 포인터 저장을 위해 8 바이트 local 스택을 할당
mov [ebp - 8], ecx ; this 포인터를 local 스택에 저장
*/
int c = a + b;
/* (3) 계산
mov eax, [ebp + 8] ; a 를 eax 에 넣고
add eax, [ebp + 12] ; b 를 더한 후
mov [ebp - 4], eax ; c 에 저장
*/
return c;
/* (4) 리턴값 저장
mov eax, [ebp - 4]
*/
}
/* (5) foo 함수 종료 후 caller 의 ebp, esp 복구
mov esp, ebp
pop ebp
ret 8 ; (6) <<스택을 정리하는 부분>>
*/
};
int main()
{
CThiscall tmp;
tmp.foo( 1, 2 );
/* (1) foo 함수 호출
push 2
push 1
lea ecx, [tmp] ; this 포인터를 ecx 에 저장
call CThiscall::foo (128102Dh)
*/
return 0;
}
naked
특징
- 스택에 파라메터 삽입 순서 : right → left
- 스택의 정리를 호출한 함수(caller)에서 수행한다.
- 컴파일러가 프롤로그(prolog) 와 에필로그(epilog) 코드를 생성하지 않으며 사용자가 직접 inline assembly 를 사용하여 자신만의 프롤로그 / 에필로그 코드를 작성해야 한다. 따라서 일반적인 C / C++ 과는 다르게 파라메터가 있는 위치를 직접 지정하거나, 레지스터 사용방식을 마음대로 정할 수 있다. 이 방식은 virtual device driver (VxDs) 개발자에게 특히 유용하다.
샘플 코드
int __declspec(naked) foo( int a, int b )
{
// prolog
__asm {
push ebp
mov ebp, esp
push ecx
}
int c;
c = a + b;
// epilog
__asm {
mov eax, c // 리턴 값을 eax 에 저장
mov esp, ebp
pop ebp
ret // 스택 정리는 caller 가 하므로 return 만 한다.
}
}
int main()
{
foo( 1, 2 );
return 0;
}