Calling Conventions in Win32 System
Posted 2007. 6. 19. 14:25, Filed under: Study/Computer ScienceTitle : Calling Conventions in Win32 System
Author : proXima
Contact : proxima ]at[ postech.ac.kr
Date : 2006/10/27
================================================================================
calling convention에 대해 잘 알아두면,
프로그래밍할 때 도움이 될 수 있다.
그래서 간단하게 정리해 보았다.
읽다가 잘 이해가 안 되는 부분이 있으면, 끝까지 다 읽은 다음에 다시 한 번 보면 좋다.
왜냐면 이게 내용 비교가 서로 얽히는 부분이 있기 때문인데,
처음 읽고 개략적으로 어떤 것인지에 대해 알고 나면 한결 이해하기가 쉬울 것이라고 생각한다.
아래에는 __cdecl, __stdcall, __fastcall, __thiscall, naked 의 다섯 가지를 설명해 두었다.
1. __cdecl
int __cdecl functionA(int a, int b);
와 같은 형식으로 선언한다.
뭐라고 읽어야 할 지는 잘 모르겠지만, C declare 인 것 같다.
VC에서는 함수를 선언할 때, calling convention을 따로 지정해 주지 않으면 기본적으로 __cdecl로 선언된다. 물론 컴파일 설정에서 바꿀 수도 있다. (VS.NET이라면 구성 속성->C/C++->고급 에서 '호출 규칙'을 변경하면 된다.)
만약 main 함수에서 functionA를 호출할 경우, main 함수에서 functionA를 호출하는 부분은 다음과 같다.
push 2
push 1
call functionA
add esp, 8
그리고 functionA 함수가 끝나는 부분은 다음과 같다.
:
retn
functionA 함수를 호출한 주체(여기서는 main)가 스택을 정리해 준다.
(add esp, 8이 스택을 정리한다.)
때문에 함수 호출이 많은 경우 스택을 정리하는 코드가 여러 번 들어가야 하기 때문에 코드의 용량이 커질 수 있다. 하지만 그렇다고 무조건 쓰지 말라는 건 아니다. 이것도 필요할 때가 있다. 아래의 __stdcall에서 설명하도록 하겠다.
2. __stdcall
int __stdcall functionB(int a, int b);
와 같은 형식으로 선언한다.
standard call 이라는 뜻이며, __cdecl과는 스택을 정리하는 주체가 다르다는 차이점이 있다.
만약 위에서와 같이 main 함수에서 functionB를 호출한다고 하면, main은 __cdecl과 같다. 그러니 이번에는 functionB의 끝나는 부분만 보자.
:
retn 8
__stdcall 의 경우에는 functionB가 실행을 완료한 뒤, 자신이 필요로 했던 parameter들의 크기만큼 스택을 정리하며 종료한다. retn 8 에서 숫자 8이 의미하는 것은 8바이트를 정리하고 돌아가겠다는 의미이다.
이런 경우는 코드의 용량은 적게 할 수 있을지 모르지만, 제약이 있다. 함수가 받아들이는 parameter의 크기가 항상 static해야 한다는 점이다.
위의 __cdecl의 설명 중에, __cdecl이 필요할 때가 있다고 했는데, 바로 ... 이라는 parameter를 가지는 함수들을 만들 때이다. 예를 들면 printf가 있는데, printf의 경우는 그 안에 들어오는 format이 어떻게 되느냐에 따라 스택을 얼마나 탐색할 것인가를 런타임에 결정한다. 이렇게 함수에 넘기는 parameter의 크기가 정해지지 않는 경우는 함수의 내부에서 사용하는 스택의 크기가 static하지 않기 때문에 이런 함수들은 __stdcall 로 만들 수 없다.
Windows에서 제공하는 API들은 대부분 __stdcall로 선언되어 있다.
API의 선언부에 WINAPI나 CALLBACK 같은 것들이 실제로는 __stdcall을 define한 것이다.
3. __fastcall
int __fastcall functionC(int a, int b, int c, int d);
와 같은 형식으로 선언한다.
말 그대로 fast call 이며, __stdcall과 거의 비슷하지만, 함수의 실행이 조금 빠르다.
왜 조금 빠를 수 있냐면, parameter를 레지스터로 넘기기 때문이다. 하지만 모든 parameter를 레지스터로 넘기는 것은 아니다. 첫번째 parameter와 두번째 parameter를 각각 ecx, edx를 통해 넘기게 되고, 세번째 parameter부터는 스택에 쌓도록 되어 있다.
main 함수에서 functionC를 호출한다고 하면, main 함수는 다음과 같이 된다.
push 4
push 3
mov edx, 2
mov ecx, 1
call functionC
그리고 functionC의 마지막 부분은 다음과 같이 된다.
:
retn 8
이번에는 parameter를 네 개나 넘겼는데도 실제로 functionC가 정리하고 나오는 스택의 크기는 8바이트밖에 되지 않는다. 그 이유는 앞에서도 설명했듯이 parameter 두 개는 레지스터를 통해 전달되어 스택에는 두 개의 parameter밖에 들어가지 않았기 때문이다.
사족(蛇足) : 사실 __fastcall을 사용해 보지 않았기 때문에 어떤 문제가 생길 수 있는지에 대해서는 잘 모르겠지만, 아마 연산에 사용할 수 있는 레지스터가 두 개 줄어들기 때문에, 복잡한 연산을 해야 하는 경우는 오히려 속도의 저하가 있을 가능성도 있지 않을까 하는 생각이 든다.
4. __thiscall
__thiscall은 따로 선언하는 방법이 없다. 단, class의 method의 경우 자동으로 __thiscall로 선언이 된다. __thiscall의 특징은, C++에서 사용하는 this 포인터가 ecx에 들어간다는 것이다. ecx에 있는 주소값을 이용하여 그 클래스의 member variable이나 method 들을 사용할 수 있게 된다.
그 외 parameter를 넘기는 방식이나 스택을 정리하는 주체는 __stdcall과 같다.
5. naked
int __cdecl functionD(int a, int b, int c, int d);
:
:
int __declspec(naked) __cdecl functionD(int a, int b, int c, int d)
{
// contents
}
와 같은 형태로 선언한다.
위에 선언하는 형태가 조금 이상하다고 느꼈을지도 모르겠다. 그렇다. 실제로 naked는 __cdecl, __stdcall, __fastcall 등과 조금 스타일이 다르다. 실제로 다른 calling convention들과 함께 선언할 수도 있다. 단, 각각의 calling convention에 따라 retn이나 스택 및 레지스터의 상태를 고려하여 내부를 작성해야 한다. 뭐 그래도 다행스럽게 넘어오는 parameter들은 그 변수명을 그대로 사용할 수 있다. 실제로 함수 내부에서 a, b가 필요하다면, 굳이 스택에서 어느 정도 거리에 있는지를 계산할 필요까지는 없다는 것이다. 그냥 a, b라고 쓰면 된다.
그리고 naked는 함수 prototype으로는 선언할 수 없도록 되어 있다. 무슨 말이냐 하면, 함수 prototype을 선언할 때에는 __declspec(naked)를 붙일 수 없다는 것이다. 함수를 작성하는 부분에서만 사용할 수 있다. 만약 prototype에 선언하게 되면 에러를 만나게 될 것이다.
naked는 사전적으로 벌거벗었다는 뜻이다. 그러면 대체 왜 함수가 벌거벗었는가... 다음과 같다.
naked로 함수를 선언하게 되면, 함수를 작성할 때 기본적으로 작성되는 다음의 코드들이 전혀 존재하지 않는다.
함수 시작시
push ebp
mov ebp, esp
함수 종료시
retn
필요하게 되면, 이것들을 직접 inline assembly로 입력해 주어야 한다.
그리고 중요한 점 하나는, 반드시 함수를 종료하는 부분은 선언한 다른 convention에 맞춰주어야 한다는 것이다.
__cdecl로 선언하고서 retn 8 과 같이 스스로 스택을 정리하고 나오면 안 된다.
왜냐하면 naked로 선언하면 이것은 __cdecl에서와 같이, 호출하는 쪽에서 스택을 정리해 주는 형태로 컴파일이 되기 때문에, 만약 함수 내부에서 스택을 정리하고 나오도록 작성했다면, 호출이 종료된 뒤에 스택에서 local variable이나 return address 등이 날아가는 상황을 볼 수 있다.
이런 것들이 프로그래밍을 할 때 무슨 도움이 될 수 있겠냐고 질문할 수도 있겠지만, 뭐 그래도 알아두면 언제 써먹을 기회가 생길 지도 모르지 않을까 하고 생각하고 있다. 가끔 reverse engineering을 할 때, 어셈 코드에서 리턴부분을 보면서 '아 이건 __cdecl로 선언한 거로군' 뭐 이러고 혼자 생각할 때가 있다. 하지만 바람직하진 않은 것 같다.
-----------
웹에서 이것저것 찾아보다가 발견한 글.
내가 지난 번에 올렸던 파스칼 방식, C 방식의 함수 호출 방식 이외에도 여러가지 방식들을 잘 설명해 놓았다.
자료의 출처는 PLUS at POSTECH 인듯. (학부생)
역시 대단한 사람들이 많구나.
나랑 비슷한 나이또래 ( 동갑 아니면 한살 많거나.) 같은데 정말 대단한 듯.
능력의 차이라기보다는 경험과 환경의 차이라고 믿고 싶다.
그런만큼 나도 열심히 하련다.