오늘은 함수 호출 규약에 대해서 알아보도록 하자, 우리가 알고 있는 함수 호츌 규약은 아래와 같이 4가지를 볼 수 있다.


1. __cdecl

2. __stdcall

3. __fastcall

4. __thiscall


Visual Studio 2010으로 확인을 한다면 아래와 같은 그림을 볼 수 있다.




이제 하나씩 서로의 차이를 알아보자.




1.__cdecl


C언어의 표균 함수 호출 규약이다. 파라미터는 오른쪽에서 왼쪽으로 스택을 통해 전달된다.

스택은 호출한 곳에서 스택을 정리한다. 

스택을 호출한 곳에서 정리함에 따라 함수 호출의 인자의 갯수가 무의미 한 장점이 있다.

즉 가변인자를 지원한다. 


void printf(char * dest, ...); 이런식의 함수를 많이 사용 했을 것이다. 

이런 것들과 비슷한 가변 인자를 지원할 수 있는 것이다. 



extern "c" int __cdecl CdeclFunc(int a, int b , int c)

{

printf("%d%d%d\n",a,b,c);

}


해당 함수를 리버싱 하여 스택의 순서를 보면 아래와 같이 보인다.


push 3
push 2
push 1
call CdeclFunc
add esp, 12



함수의 인자가 3개이다. 고로 오른쪽으로 부터 스택에 전달되니 push 3, push 2, push 1 순으로 스택에 들어간다. 


그런 다음에 함수를 호출하는 call FunA를 하게 된다. 


그 다음에 __cdecl의 특징인 함수를 호출한 자가 스택을 정리하는 코드가 나타난다.


add exp, 12 이다. 4바이트 인자 3개의 스택을 정리하는 코드이다. 




2.__stdcall


윈도우 API에서 사용하는 함수 호출 표준 규약이다. 파라미터는 오른쪽에서 왼쪽으로 전달 되며, 스택을 정리하는 곳은 

함수를 호출 당한곳에서 이루어 진다. 즉 함수가 종료 되면서 스택을 정리하는 것이다. 


extern "c" int __stdcall StdcallFunc(int a, int b , int c)

{

printf("%d%d%d\n",a,b,c);

}


위와 같은 함수를 리버싱한다면 아래와 같은 코드가 나온다.


push 3
push 2
push 1
call _StdcallFunc@12


함수의 인자가 3개이다. 고로 오른쪽으로 부터 스택에 전달되니 push 3, push 2, push 1 순으로 스택에 들어간다.

__cdecl 처럼 함수가 호출이 끝나고 존재하는 스택 정리 코드가 없다. 

함수 호출을 당한 자가 스택을 정리하기 떄문에 가변인자를 지원하지 못한다.


이것이 __stdcall의 특장점이다.

여기서 보면 코드 한줄 없어 지는것이 얼마나 이득이냐고 반문 할 수 있지만 함수가 많어지고 호출이 많이 지면 

실행 파일의 용량과 수행속도에서 차이가 나기 시작하는 것이다. 




3.__fastcall


이름을 보면 빠른 함수 호출이다. 말 그대로 빠른 함수 호출을 하기 위해서 고안된 방식이다. Windows X64 프로그래밍의 fastcall과는 다른 성격이지만 비슷한 면도 많다. 


x86 레지스터를 이용하여 함수 인자 값을 전달 하는 계념이다. ecx,edx를 이용하여 함수 인자 2개를 저장하고 

남어지 부분들은 스택에 저장하는 방식이다. 


CPU가 일처리하기 위해서 주 기억장치에서 그 값을 불러다 쓸경우 보다는 레지스터에서 바로 땡겨서 쓰는 편히 훨 씬 빠르기 떄문에 함수 인자가 2개일 경우 보다 빠르게 동작 할 수 있는 장점이 있다. 


스택 저장은 오른쪽에서 왼쪽으로 동일하다.


extern "c" int __fastcall FastcallFunc(int a, int b , int c)

{

printf("%d%d%d\n",a,b,c);

}


인자 값 3개인 함수를 리버싱 해보겠다. 아래와 같은 코드가 나타난다.


push 3
mov edx, 2
mov ecx, 1
call @FastcallFunc@12


함수의 인자가 3개이다. 고로 오른쪽으로 부터 스택에 전달되니 push 3 이 스택에 들어간다.

그리고 남어지 2개의 인자는 앞서 설명 한 것과 같이 각각 edx, ecx에 들어가게 된다.

그리고 나서 함수를 호출 하며 __stdcall과 같이 스택은 함수호출을 당한자가 진행하게 된다.


함수 호출을 당한 자가 스택을 정리하기 떄문에 가변인자를 지원하지 못한다.



4. __thiscall

그리고 C++로 넘어 오면서 생긴 특수한 함수 호출 규약이 있다. 바로 __thiscall이다. 

흔히 C++의 Class를 지원하면서 자신의 클래스의 접근을 하고자 한다면 this-> 이런 형식을 남발 하면서 사용하고 있을 것이다. 


해당 방식의 스택은 역시 오른쪽에서 왼쪽으로 전달하게 되어 있으며 스택 정리 또한 호출 당한 쪽에서 진행하게 된다. 

그리고 This를 하기 위해서는 자신의 클래스 포인터를 전달 해야되는데 바로 ecx를 통해서 자신의 클래스 포인터를 전달 하게 된다. 


class CCallConv

{

public :

int thisCall(int a, int b, int c);

};


CCallConv conv; 

int CallConv::thisCall(int a,int b int c)

{

return printf("%d%d%d\n",a,b,c);

}


위와 같은 클래스를 리버싱 하면 아래와 같은 코드가 나온다.

push 3
push 2
push 1
lea ecx, conv
call CCallConv::ThisCall


함수의 인자가 3개이다. 고로 오른쪽으로 부터 스택에 전달되니 push 3 이 스택에 들어간다.

그리고 자신의 클래스 포인터인 conv 를 ecx에 저장하여 전달 하게 된다 .



스택을 정리하는 방식에 따른 차이가 왜 있을까?

함수 호출 규약의 주된 차이점은 스택 정리 방식이다. 호출한 쪽에서 정리하는 방식과 호출을 당한 곳에서 정리하는 방법이 있다. 호출한 쪽에서 정리하는 방법의 장점은 자신이 파라미터로 전달한 인자의 개수를 정확히 알 수 있기 때문에 가변 인자를 지원할 수 있다는 것이다. 그렇다면 __cdecl을 제외한 나머지 방식은 왜 스택을 호출을 당한 곳에서 정리하는 것일까?

그 이유는 속도에 있다. x86 CPU의 경우 ret 어셈블리 명령어를 두 가지 형태로 지원한다. ret를 하면 단순히 스택에 저장된 복귀 주소로 리턴 한다. 하지만 ret imm16을 사용하면 imm16만큼 스택에서 팝 한 다음 꺼내진 복귀 주소로 리턴 한다. 위에서 살펴 보았듯이 __cdecl은 스택 정리를 위해서 add 명령어가 추가된다. 반면에 호출을 당한 곳에서 정리하는 방식은 리턴할 때에 ret imm16을 사용함으로써 add 명령어가 추가되지 않아도 된다. 또한 486을 기준으로 했을 때 ret와 ret imm16은 5클럭 사이클을 소모하는 동일한 명령어로 처리된다. 따라서 __cdecl보다는 다른 호출 규약이 근소하게 빠를 수 있다.


마지막으로 정리를 하자면 아래와 같은 표로 정리 할 수 있다.


호출 규약파라미터스택특징
__cdecl오른쪽 -> 왼쪽호출한 곳가변 인자 지원
__fastcall오른쪽 -> 왼쪽호출 당한 곳파라미터 두 개를 ecx, edx를 통해서 전달하기 때문에 두 개 이하의 인자를 가진 함수에 대해서 빠름
__stdcall오른쪽 -> 왼쪽호출 당한 곳Windows 표준 호출 규약
__thiscall오른쪽 -> 왼쪽호출 당한 곳ecx를 통해서 this 포인터를 전달 함


Posted by 최우림 -=HaeJuK=-

티스토리 툴바