#cpp
비주얼 C/C++ 컴파일러는 여러가지 방법으로 함수를 호출한다. 호출 방법이 여러가지이기때문에 호출 방법에 대한 규정에 대하여 알아두고, 이해한다면 프로그램을 디버그하는 것과 코드를 어셈블리어와 링크하는 것에 도움이 될 것이다.
아래의 그림을 보면 main()함수와 Sum()함수가 있다. 여기에서 main()함수는 Sum()함수를 호출하기 때문에 호출자가 되고, Sum()함수는 호출되기 때문에 피호출자가 된다.
__stdcall
과 __cdecl
호출 규정을 따르는 함수는 호출시 두개 모두 오른쪽에서
왼쪽방향으로 매개인자를 스택프레임에 삽입한다. 예를 들면 위의 그림에서 main()
함수가 Sum(a, b) 함수를 호출할 때 스택프레임에는 변수 b가 먼저 삽입된 후 그
다음에 변수 a가 삽입된다.
아래의 표는 __stdcall
호출규정과 __cdecl
호출규정을 비교한 것이다.
__stdcall
은 인자의 개수가 고정적인 Win32 API 함수를 호출하는데 사용하는
호출 규정이고, __cdecl
은 인자의 개수가 가변적인 함수(printf() 등)를 호출하는데
사용되는 호출 규정이다. 이 두가지 호출 규정에 적용되는 함수가 다르기 때문에
이 함수들이 호출되는 과정도 다르다.
__stdcall
__stdcall
호출 규정의 경우 함수 호출시 넘어가는 매개인자의 개수가 고정적이기
때문에 스택 프레임에 삽입, 삭제되는 인자들이 항상 일정하다. 그래서 함수 호출시
피호출자는 스스로 스택을 정리한 후 함수를 실행하고, 실행을 마친 후 자신이 삽입했던
인자들을 제거하면서 스택을 정리하고 함수를 빠져 나온다.
__cdecl
반면 __cdecl
호출 규정의 경우 함수 호출시 넘어가는 매개인자의 개수가 일정하지
않기때문에 피호출자는 스택을 스스로 정리하기 어렵다. 그래서 함수 호출시 호출자가
스택을 정리한 후 함수를 실행하고, 실행을 마친 후에 다시 호출자가 자신이 삽입했던
인자들을 제거하면서 스택을 정리한다.
__fastcall
이 방식은 스택이 아닌 가까운 레지스터를 사용함으로써 호출 속도가 빠르며 피호출자가 스택을 정리하나 스택을 사용하지 않고 레지스터를 이용한다. 이 호출 규칙은 x86 아키텍처에만 적용된다.
그러나 왼쪽에서 오른쪽으로 인수 목록에서 발견된 처음 두 개의 DWORD 이하 인수만 ECX 및 EDX 레지스터로 전달되고, 다른 모든 인수는 오른쪽에서 왼쪽으로 스택에 전달된다.
이름 그대로 가장 빠른 호출 속도를 가지고 있으나, 초고속 그래픽 등 극한의 스피드를 요구하는 프로그래밍 등 외에는 __fastcall로 얻을 수 있는 속도 이득은 극히 미미하다. 하지만 빈번히 호출되는 함수라면 속도 이득이 없다고 할수 없으며 잘 사용하면 최적의 속도를 내는 프로그래밍에 유리하다.
이 두가지 호출 규저에 따른 호출 방법은 결국 실행되는 코드수와 실행속도는 같지만 어셈블리어로 변환된 실행코드(혹은 실행파일)의 크기는 다르다.
__stdcall
호출 규정을 따르는 함수가 여러번 호출될 경우, 호출자에는 "call Sum"이
중복하여 발생하고, 스택에 인자를 삽입 삭제하는 부분은 피호출자에서 공통적으로
수행한다.
이에 반해 __cdecl
호출 규정을 따르는 함수가 여러번 호출될 경우, 호출자는 스택에
인자를 삽입/삭제하는 부분이 함수를 호출할 때마다 발생하기때문에 __stdcall
호출
규정보다 어셈블리어로 변환된 실행 코드의 크기가 커진다.
__stdcall
호출 규정을 따르는 함수의 선언부는 컴파일러에 의해 다음과 같은 일정한
패턴에 맞추어 바뀐다. 먼저 함수의 이름 앞에는 underscore(_)가 붙고, 함수의 이름
뒤에는 "@(앳 마크)"가 붙습니다. 그리고 "@" 뒤에는 10진 정수로 매개인자의 데이터
크기의 합을 표시해준다. 간단히 예를들면 아래와 같다.
// int a의 데이터 크기가 4 이고, double b의 데이터 크기가 8 이므로 두 데이터 크기의 합은 12이다.
void __stdcall func(int a, double b) -> _func@12
반면 __cdecl
호출 규정의 경우 함수 이름 앞에 underscore(_)를 붙이지만 적용되는
함수가 가변인자 함수이기 때문에 매개인자의 데이터 크기의 합을 알기 힘드므로 "@"와
10진수의 데이터 크기부분을 표시하지 않는다. 위의 예를 __cdecl 에 적용하면
아래와 같다.
void __cdecl func(int a, double b) -> _func
__cdecl
의 경우 위의 코드와 같이 function 포인터의 이름이나 함수 이름 앞에
__cdecl
키워드를 위치시킨다.
__cdecl
은 C naming conventions(C 명명 규칙)와 C 호출 규정의 기본이기 때문에
__cdecl
이라고 명시하지 않으면 __cdecl
이 적용된다. 기본적으로 적용되는 호출
규정을 변경하려면 다음의 세가지 컴파일러 옵션 중 하나를 설정하면 된다.
__stdcall
을 적용한다.__fastcall
을 적용한다.__cdecl
호출
규정을 적용한다.#define
과 호출 규정LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
종종 위와 같이 정의된 함수를 본다. 여기에서 LRESULT
는 반환타입이고, WndProc
은
함수의 이름이다. 그렇다면 CALLBACK
은 뭘까?
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
#define
으로 선언된 CALLBACK
은 컴파일시 "__stdcall"로 대체되면서 위에 정의한 함수는
LRESULT __stdcall WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
로 대체되게 된다. 즉 WndProc
함수는 __stdcall
호출 규정을 따르는 함수임을
나타내는 것이다. CALLBACK 뿐만 아니라 위에 선언된 WINAPI 역시 함수 선언에서
사용하면 함수의 호출 규정을 __stdcall
로 적용하고, WINAPIV
는 함수의 선언에서
사용하면 함수를 __cdecl
호출 규정으로 적용한다. 그 외의 키워드도 함수의 호출
규정을 적용할 때 사용된다.
규약 | 내용 |
---|---|
__cdecl | x86 구조에서 주로 사용(C/C++ 컴파일러에서 기본적으로 사용함), 함수 호출 시 오른쪽 인자부터 스택에 전달함, 호출자가 스택을 정리 함(add ESP, n) |
__stdcall | MS Win32API 표준 규약, 함수 호출 시 오른쪽인자부터 스택에 전달함, 호출당한 함수가 사용한 스택을 정리함(ret n) |
__fastcall | 매개변수의 일부를 레지스터(ECX, EDX)로 전달함, 함수 호출 시 다른 규약에 비해 빠름, 호출당한 함수가 사용한 스택을 정리함(ret n) |