웬디의 기묘한 이야기

글 작성자: WENDYS
반응형

Calling Convention

X86에선 호출 규약이 __cdecl, __stacll, __fastcall 등으로 나누어있지만 x64에선 __fastcall 하나의 호출 규약만을 사용하도록 정의 되어있습니다. (__fastcall은 x86 전용이라고 합니다. x64는 추후 다시 정리하겠습니다!)

오늘은 x86 호출 규약에 대해서만 정리를 해보겠습니다.


함수 호출규약을 아무것도 입력하지 않으면 기본값은 __cdecl 입니다.

해당 테스트는 Visual Studio 2015 Community 에서 이루어졌습니다.


__cdecl (c declaration)

이 규약에서 호출자는 스택에서 인수를 정리하며, printf() 와 같은 가변 인자 함수를 지원합니다.

함수의 호출시 함수 호출 전과 함수가 끝난 후의 ESP의 위치는 같아야 합니다. 그렇기때문에 함수를 사용하기 위해 사용한 스택을 누군가는 정리를 해주어야 한다는 것 입니다. 누군가는 caller 혹은 callee 가 되겠네요. (직접 하는일은 아니고 내부적으로 처리됩니다.)


__cdecl sample code


int cdecl_test1(int a, int b, int c) {
    int result = a + b + c;
    return result;
}

int __cdecl cdecl_test2(int a, int b, int c) {
    int result = a + b + c;
    return result;
}

int main()
{
    cdecl_test1(1, 2, 3);
    cdecl_test2(1, 2, 3);
    return 0;
}


__cdecl sample asm code


cdecl_test1(1, 2, 3);
00E53C3E  push        3  
00E53C40  push        2  
00E53C42  push        1  
00E53C44  call        cdecl_test1 (0E5134Dh)  
00E53C49  add         esp,0Ch  

     cdecl_test2(1, 2, 3);
00E53C4C  push        3  
00E53C4E  push        2  
00E53C50  push        1  
00E53C52  call        cdecl_test2 (0E51352h)  
00E53C57  add         esp,0Ch

2개의 asm 코드가 일치합니다.

위의 코드는 호출 규약을 입력하지 않은 코드이고

아래 코드는 __cdecl을 입력한 코드 입니다.

위에서 설명드린 대로 아무것도 입력하지 않으면 __cdecl이네요


asm code를 보게되면 push 3, push 2, push 1 을 하고있습니다.

즉, 오른쪽부터 왼쪽으로 인자를 스택에 넣고 있는 것 입니다.

그리고나서 call cdecl_test 함수를 호출하네요

그런데 다음줄에 add esp, 0Ch는 뭘까요??

함수 내부에서가 아닌 함수 외부에서 stack이 정리되고 있습니다. Caller에서 정리를 한다는 이야기가 되는것이죠


정리

- 전달 인자는 오른쪽에서 왼쪽으로 전달 된다.

- cdecl 방식은 caller 에서 전달 인자에 대한 Stack을 정리한다. (함수 외부)

- 호출 규약을 입력하지 않으면 기본값은 cdecl 이다


__stdcall

Win32 API 등의 표준 규약입니다.

Cdecl 방식과는 거의 비슷하게 동작하며, 어떤부분이 다르게 동작하는지 한번 봐보겠습니다.


__stdcall sample code


int __stdcall stdcall_test(int a, int b, int c) {
    int result = a + b + c;
    return result;
}

int main()
{
    stdcall_test(1, 2, 3);
    return 0;
}


__stdcall sample asm code


stdcall_test(1, 2, 3);
00E53C5A  push        3  
00E53C5C  push        2  
00E53C5E  push        1  
00E53C60  call        stdcall_test (0E51357h)

같은듯 다른 무언가가 있네요

스택에 전달인자를 넣는건 똑같고 함수 call 하는것까진 똑같네요

하지만 call 이후에 뭔가가 없습니다.

add esp,0Ch 의 부분이 여기엔 없네요

그것이 __cdecl과 __stdcall의 차이점 입니다.

바로 __cdecl은 caller가 stack을 정리하고 __stdcall은 callee가 직접 정리를 한다는것이죠


정리

- 전달 인자는 오른쪽에서 왼쪽으로 전달 된다.

- stdcall 방식은 callee 에서 전달 인자에 대한 Stack을 정리한다. (함수 내부)


__fastcall

이름부터 속도가 빠를 것 같은 호출 규약 입니다.

해당 규약은 표준화된 규약은 아니기때문에 컴파일러마다 다르게 처리됩니다.

일반적인 fastcall 호출 규약은 레지스터 내 하나 이상의 인수르 통과시키며 호출에 필요한 메모리 접근의 수를 줄입니다.

VisualStudio, GCC의 fastcall 은 처음 두 개의 인수(왼쪽에서 오른쪽)를 통과시켜 ECX, EDX에 맞추고 나머지 인수들은 오른쪽에서 왼쪽 순으로 스택으로 PUSH 하게 됩니다.


__fastcall sample code


int __fastcall fastcall_test(int a, int b, int c) {
    int result = a + b + c;
    return result;
}

int __fastcall fastcall_test2(int a, int b, int c, int d, int e) {
    int result = a + b + c + d + e;
    return result;
}

int main()
{
    fastcall_test(1, 2, 3);
    fastcall_test2(1, 2, 3, 4, 5);
    return 0;
}


__fastcall sample asm code


fastcall_test(1, 2, 3);
00E53C65  push        3  
00E53C67  mov         edx,2  
00E53C6C  mov         ecx,1  
00E53C71  call        fastcall_test (0E5135Ch)

    fastcall_test2(1, 2, 3, 4, 5);
00E53C76  push        5  
00E53C78  push        4  
00E53C7A  push        3  
00E53C7C  mov         edx,2  
00E53C81  mov         ecx,1  
00E53C86  call        fastcall_test2 (0E51361h)

여기서 fastcall이 왜 빠른지 나옵니다.

모든 전달 인자를 스택에 쌓는게 아니라 레지스터에 직접 넣어버림으로 인해서 속도를 향상시킨 방식입니다.


실제로 전달인자를 우측에서 좌측으로 입력하고,

처음 두개의 인자는 ECX, EDX 레지스터에 입력하는 모습을 볼 수 있습니다.

더불어 call 이후엔 스택을 정리하지 않는것으로 보아 이건 callee 즉 함수 내부에서 스택이 정리된다는것을 확인할 수 있습니다.


정리

- 전달 인자는 처음 두개는 ECX, EDX 레지스터에 직접 넣고 나머지에 대해선 오른쪽에서 왼쪽으로 전달 된다.

- fastcall은 레지스터를 직접 이용하기때문에 속도가 빠르다.

- fastcall 방식은 callee 에서 전달 인자에 대한 Stack을 정리한다. (함수 내부)

Disassembly는 지난번 소개해드렸던 방법으로 확인 및 테스트가 가능합니다.

2015/12/31 - [Development/Debugging] - visual studio 디버깅시 디스어셈블리 확인하기


반응형