Calling Convention

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)

함수 호출은 크게 다음과 같이 나뉘어 진다.

  1. 함수가 사용할 파라메터를 스택에 넣고 함수 시작지점으로 점프(함수 호출)한다.
  2. 함수 내에서 사용할 스택프레임을 설정한다.
  3. 함수의 내용을 수행한다.
  4. 수행을 마치고 처음 호출한 지점으로 돌아가기 위해 스택을 복원한다.
  5. 호출한 지점의 다음 라인으로 점프한다.

이때 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 가 직접 스택을 정리한다.

아래는 스택의 변화를 순서대로 표시한 것이다. 위 코드의 주석 번호와 일치하는 곳에서의 스택 상태를 나타낸다.

  1. 파라메터를 역순으로 push 하고 함수를 호출한다. 이때 리턴주소 (call 호출 다음라인) 를 push 하고 함수주소로 jump 한다.
  2. caller 의 ebp 를 저장하고 callee 의 ebp 를 새로 확보한다. 이 새로운 ebp 를 이용하여 foo 함수 내에서 파라메터 및 local 변수로의 접근을 시도할 것이다. 그리고 local 변수에 사용할 스택을 할당하는데 여기서는 4바이트 변수 하나이므로 단순히 ecx 를 push 함으로써 이를 수행한다. 하지만 local 변수들이 8바이트 (혹은 그 이상) 일 경우 “sub esp, 8” 과 같이 스택을 할당하게 된다.
  3. 계산을 수행하고 결과값을 local 변수에 저장한다. 이때 ebp 를 이용하여 파라메터 및 local 변수로 접근하는 것을 알 수 있다.
  4. 리턴할 값을 eax 에 저장한다.
  5. 이제 foo 수행은 모두 끝났다. caller 의 esp, ebp 를 복구하고 스택에 저장되어 있는 다음 주소 (main 함수에서 foo 호출한 부분의 다음주소) 를 꺼내어 jump 한다.
  6. 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;
}

참조

1) VS2010 에서 샘플코드에 나와 있는 간략화된 어셈블리 코드를 보려면 프로젝트를 Release 모드로 바꾸고, Property 에서 C/C++ → Optimization → Optimization 을 Disable (/Od) 시키면 된다. 그리고 디버그 모드로 실행한 후 Debug → Windows → Disassembly 를 실행한다.