(최종 업데이트: 2014-03-15 11:59 - 아래 링크 이미지가 다 깨져서 구글 검색으로 찾아 새로 올림 ^O^)
Calling Convention, 우리 말로 "함수 호출 규약".
C/C++ 개발자라면 누구나 알아야 할 것 같은 이 개념들은,
사실 일반적인 C/C++ 프로그래밍에서는 다룰 일이 거의 없다.
대부분의 프로그래밍이 그렇듯, 사용자가 작성한 코드를 컴파일해서 실행하는 경우에는 이것이 무엇이고 어떤 경우에 알아야 하는지 알 필요가 없단 말이다. 그냥 컴파일해서 실행하면 될 뿐. C#, Java 등 기타 고급 언어인 경우에는 두 말할 필요도 없다.
이것이 뭔지 알 필요가 있는 경우는 딱 한 가지 뿐이다. 바로 남이 만든 DLL을 참조해서 호출해야 할 때.
여기서 남이란 Windows 자체 API일수도 있고, 제3사에서 만든 API일수도 있다.
그런데, COM으로 만들어진 DLL이라면 역시 알 필요가 없다.
표준 DLL로 만들어진 경우에만 Calling Convention에 대한 지식이 필요하다.
그만큼 일반적이지 않고 제한적이며 전문적인 지식이란 얘기다.
(또 Reverse Engineering에서 어셈블리를 파려면 반드시 알아야 하는 지식이기도 하다.)
서론이 길었는데,
간략 정리된 블로그 포스팅이 있어 Calling Convention 자체에 대한 상세한 내용은 직접 언급하지 않고 링크로 대체한다.(^^;;; 이건 뭐... 직접 쓰려고 했는데, 아래 링크들보다 더 잘 쓸 수는 없을 것 같아 포기.)
링크: [C/C++] Function Calling Conventions에 대한 정리 이거 하나로 끝낸다!! (http://recoverlee.tistory.com/25)
윈도우 프로그래밍에 대한 세미나를 준비하다가 말도 많고 탈도 많고 많은 프로그래머에게 골칫거리가 될 수 있는 함수 호출 규약(Function Calling Convention)에 대하여 정리해 보자.
일단 Microsoft의 Calling Convention의 종류는 다음과 같다.
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. | Calling function pops 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 |
1. __cdecl
- 인자 파싱 : Right -> Left
- 스택 관리 : Caller, 가변 인자 허용
- Name Mangling : 함수 이름 앞에 _추가
ex) _Foo - C와 C++ 함수의 기본 호출 규약
- 기본 호출 규약이므로 /Gz(stdcall) 또는 /Gr(fastcall) 옵션이 켜졌을 때, 필요한 변수나 함수 이름 앞에 __cdecl을 놓으면 된다.
/Gd 옵션은 강제로 _cdecl 규약으로 호출한다.
2. __stdcall
- 인자 파싱 : Right -> Left
- 스택 관리 : Callee.
- Name Mangling : 함수 이름 앞에 _추가, 함수 이름 뒤에 @추가 되고 @뒤에 다시 매개변수의 전체바이트에 해당하는 10진수가 추가된다.
ex) _Foo@12 - 거의 모든 시스템 함수에서 사용되는 호출 규칙이다.( WinAPI )
- /Gz 옵션은 C++멤버 함수와 __cdecl 또는 __fastcall이 표시된 함수를 제외한 모든 함수에 __stdcall 호출 규칙을 지정한다. 모든 __stdcall함수는 프로토타입을 가져야 한다. 가변 인수를 취하는 함수는 __cdecl로 표시해야 한다.
3. __fastcall
- 인자 파싱 : 처음 2개의 DWORD 또는 더 작은 인자들은 ecx와 edx에 전달, 나머지 인자들은 Right-> Left
- 스택 관리 : Callee.
- Name Mangling : 함수 이름 앞과 끝에 @가 추가되고 @뒤에 매개변수의 전체바이트에 해당하는 10진수가 추가된다.
ex) @Foo@12 - /Gr 옵션은 선언된 함수가 충돌하지 않고 이름이 main이 아니라면, 모듈 내 각 함수를 fastcall로 컴파일 한다.
4. thiscall
- 인자 파싱 : Right -> Left, this 매개 변수가 ecx레지스터에 전달.
- 스택 관리 : Caller.
- Name Mangling : 없음.
- 가변인자를 허용하지 않는 C++멤버함수의 기본 호출 규약으로 스택 끝에 this포인터를 넣으며 컴파일시 컴파일러에 의해 가변인자 함수는 __cdecl로 변경된다. thiscall은 키워드가 아니므로 thiscall 호출 규약은 명시적으로 사용할 수 없다. 모든 매개 변수들은 스택상에 놓여진다.
5.naked
- 인자 파싱 : Right -> Left
- 스택 관리 : Caller
- Name Mangling : 없음
- stack frame이 생략.
- 컴파일러가 기본적으로 만들어주는 Prolog와 Epilog를 변경할 때 사용된다. naekd를 사용하게 되면 아래와 같은 Prolog와 Epilog를 컴파일러가 생성하지 않는다. 사용자가 stack frame을 할당하여 사용해야 된다. 이는 CPU와의 이식성이 없기때문에 일반 응용프로그램에서는 거의 사용되지 않는다. 주로 디바이스 드라이버를 만들 때 사용한다.
Calling Convention에 따른 스택 프레임과 스택 포인터의 이동에 관해서는 아래의 블로그에 자세히 설명되어 있다.
링크1: 함수 호출 규약, Function Calling Convention (1/2) (http://beforu.egloos.com/2117375)
링크2: 함수 호출 규약, Function Calling Convention (2/2) (http://beforu.egloos.com/2117409)
이 때 함수에서 다른 함수로 실행이 넘어간 뒤에 다시 원래의 함수Caller로 돌아오려면 호출자가 사용하던 데이터를 복구할 필요가 있는데, 함수를 연달아서 호출한 경우 나중에 호출된 함수Callee의 데이터는 항상 호출한 함수의 데이터보다 먼저 제거된다. 함수 호출에 관련된 데이터의 이런 특성이 스택이 갖는 후입선출LIFO, Last-In First Out 속성과 들어맞기 때문에 함수 호출 과정에서 현재 함수에서 사용하는 로컬 변수나 호출될 함수에게 전달할 인자를 저장하기 위한 데이터 구조에는 스택Stack을 주로 사용하게 된다. 함수 호출 과정에 있어서 기계어로 번역된 코드가 호출될 함수에 인자를 전달하고 리턴값을 받아오는 방식은 CPU 아키텍처Architecture에 따라서 매우 다를 것이고, 심지어 같은 CPU 아키텍처 상의 같은 언어라도 다른 컴파일러를 사용한다면 다를 수도 있을 것이다. 하지만 인자를 스택에 넣을 것인가 메모리에 전달할 것인가, 뒤에 오는 인자를 먼저 넣을 것인가, 나중에 넣을 것인가, 하나의 레지스터에 들어가지 않는 크기의 데이터를 반환하는 경우에 스택에 넣어서 반환할 것인가 등, 이런 여러 가지 함수 호출에 필요한 조건들이 컴파일러 벤더에 따라서 달라진다면 다른 컴파일러가 생성한 바이너리Executable Binary 간의 호환성을 보장할 수가 없게 된다. 예를 들어 A사의 컴파일러가 생성한 라이브러리를 B사의 링커로 링크해서 라이브러리 내의 함수를 호출한다는 것을 상상하기 어려울 것이다. 사 실 함수를 호출하는 방법에 어떤 규칙이 없다면 바이너리 단위의 호환성을 논의하기 이전에 운영체제OS, Operating System가 어떤 바이너리도 정상적으로 실행되는 것을 보장할 수 없을 것이다. 예를 들어 OS는 C++로 작성된 코드의 엔트리 함수 void main(int, char* [])을 호출하면서 int 인자를 스택에 먼저 넣고, char* [] 인자를 나중에 넣었는데, 호출된 main() 함수 쪽에서는 int 인자를 먼저 꺼내고 char* [] 인자를 나중에 꺼냈다면 무슨 일이 생길 것인가? 따라서 함수를 호출하는 데 있어서 어떤 규칙을 만드는 것은 서로 다른 컴파일러, 더 나아가 서로 다른 언어 간에 상호 작용성을 높이고, 빌드Build된 바이너리의 재사용성을 높이는 데에 있어서 필수적이라고 할 수 있는 것이다. 이렇게 함수 호출을 위해서 밟는 절차를 정해둔 것을 함수 호출 규약Function Calling Convention이라고 한다. 여기에서는 게임 제작 환경으로 보편적이라고 간주할 수 있는 인텔 아키텍처IA-32와 마이크로소프트 윈도우Microsoft Windows 상의 함수 호출 규약을 중점적으로 살펴보자. 함수 호출 과정과 스택 프레임Stack Frame 함 수 호출 규약을 이해하기 위해서는 먼저 스택 프레임에 대한 이해가 필요하다. 스택 프레임은 호출된 함수가 실행되는 동안에 필요한 로컬 변수 등의 정보나 호출한 함수의 실행에 필요한 정보Context가 손실되지 않도록 스택에 저장할 때 취하는 구조Data Structure를 의미한다. 이렇게 저장해야 하는 정보에는 함수의 실행이 종료된 뒤에 리턴할 주소나, 로컬 변수, 자신을 호출한 함수의 스택 프레임 위치, 레지스터 같은 기계 상태, 예외 처리 리스트 등이 포함된다. 스택 프레임은 기본적으로 함수가 호출될 때마다 새로 설정되며, 프레임 포인터Frame Pointer 혹은 베이스 포인터Base Pointer를 통해서 참조할 수 있다. 프레임 포인터는 현재 실행되는 함수의 스택 프레임이 스택 상의 어느 주소 위치하고 있는지 가리키는 값으로, 인텔 아키텍처에서는 ebp 레지스터에 저장된다. 실제로 함수가 호출되는 과정을 통해서 스택 프레임의 구성에 대해서 자세히 살펴보도록 하자. 1. 인자를 스택에 집어 넣는다함수 호출 규약이나 예외 처리를 사용했는가의 여부에 따라서 스택 프레임의 구조는 약간씩 달라질 수 있다. 위의 과정 중, 3~5번에 해당하는 함수 실행 준비 과정을 프롤로그Prolog라고 부르고, 7~9번의 함수 실행을 마무리하는 프롤로그의 반대 과정을 에필로그Epilog라고 부른다. cdecl // int main(int argc, char* argv[])만 약 이 예제에서 function() 함수가 가변 인자를 받아들이는 함수이고 인자가 여러 개 주어졌다면 이어지는 인자는 프레임 포인터를 통해서 [ebp+20], [ebp+24], [ebp+28] 등으로 간단하게 계산해낼 수 있다. 주목할 부분은 call function 명령 이후의 add esp, 0ch 명령이다. cdecl 호출 규약에서는 함수 호출자가 스택에 집어넣었던 인자를 제거해야 하므로, (인자당 4 바이트) * (a, b, c 세 개의 인자) = (12 바이트) = (0ch 바이트)만큼 스택 포인터를 이동해서 인자를 꺼내는 처리를 해 주고 있는 것을 알 수 있다. stdcall stdcall 호출 규약은 윈도우 플랫폼에서 시스템 API를 호출할 때 사용하는 규약이다. stdcall 호출 규약에서 컴파일러는 멤버 함수의 오른쪽 인자부터 왼쪽 인자 순으로 스택에 집어 넣고, 함수 호출이 종료되면 호출된 함수가 인자를 스택에서 제거하며, 리턴 값이 있는 경우 레지스터 eax를 통해서 리턴 값을 돌려 받는다. 호출된 함수가 스택에서 제거해야 할 인자의 개수를 정확히 알아야 하기 때문에 cdecl 규약과 달리 stdcall 규약의 함수에는 가변 개수의 인자를 전달할 수 없다. stdcall 호출 규약을 사용하여 함수를 호출하는 예는 다음과 같다. // int main(int argc, char* argv[])cdecl 호출 규약을 사용한 예와 다른 점은 function() 함수의 선언부에 __stdcall이라는 한정자를 사용했다는 점과, 호출된 함수인 function()에서 리턴할 때 전달받은 인자를 스택에서 제거하기 위해서 ret 0ch 명령을 실행하고 있는 점이다. 이 명령을 수행하면 인자로 주어진 값만큼 스택을 정리하면서 함수에서 리턴하게 되므로 결과적으로 표 2)의 add esp, 0ch 명령과 같이 인자를 스택에서 제거하는 역할을 수행하게 된다. stdcall 호출 규약에서는 가변 개수의 인자를 사용할 수 없는 반면, 함수 내부의 한 곳에만 인자를 정리하는 코드가 존재하게 되므로 빌드된 바이너리의 크기가 작아지는 장점이 있다. thiscall thiscall 호출 규약은 C++에서 클래스 멤버 함수를 호출하기 위해서 사용하는 호출 규약이다. C++ 멤버 함수를 실행하기 위해서는 this 포인터를 알고 있어야 하기 때문에, thiscall 규약에서는 this 포인터를 ecx 레지스터에 넣어서 전달하게 된다. this 포인터를 ecx에 넣어서 전달한다는 점 외에는 stdcall 호출 규약과 동일하다. A test;fastcall fastcall 호출 규약은 인자를 전달하면서 스택이 아니라 레지스터를 사용하는 규약이다. 기본적인 동작 방식은 stdcall과 유사하지만, 가장 앞 쪽 두 개의 인자를 레지스터 ecx와 edx를 사용하여 전달한다는 점이 다르다. 스택을 사용하지 않고 레지스터에 인자를 전달하기 때문에 호출 비용이 조금 더 낮을 것으로 기대할 수 있다. fastcall 호출 규약을 사용하여 함수를 호출하는 예는 다음과 같다. // int main(int argc, char* argv[])naked naked 호출 규약은 컴파일러가 작성하는 프롤로그나 에필로그 코드를 사용하지 않고 자신이 프롤로그와 에필로그를 작성하고자 할 때 사용한다. 인자를 전달받는 방식은 cdecl과 마찬가지이기 때문에 인자를 정리할 필요는 없지만, 함수에서 돌아갈 때 스택을 원상복구 하지 않으면 프로그램이 정상 동작하지 않을 가능성이 높기 때문에 naked 호출 규약을 사용한 함수 안에서 스택을 다룰 때는 매우 주의해야 한다. 저수준Low-Level에서 특별한 작업을 해야 할 필요가 있는 경우가 아니라면 사용할 필요가 없는 호출 규약이다. int __declspec(naked) function(int a, int b, int c)프레임 포인터 생략Frame Pointer Omission 마 이크로소프트 비주얼 C++, Microsoft Visual C++ 컴파일러에는 속도 향상을 위해서 함수 내부에서 프레임 포인터를 사용하지 않는 옵션이 있다. 이 옵션이 적용되면 프롤로그와 에필로그가 약간씩 줄어들게 되고, ebp를 프레임 포인터로 사용하지 않고 범용 레지스터로 활용할 수 있게 되기 때문에 중간 결과를 저장하기 위한 공간이 늘어나서 코드 길이가 줄어들 수 있는 여지가 생긴다. 반면 프레임 포인터가 없기 때문에 실행 중 디버거를 연결해서 스택 프레임 상태를 알 수 없으므로 인자나 로컬 변수 상태, 이전 스택 프레임에 대한 정보를 찾을 수 없으므로 콜 스택Call Stack이나 함수 호출 인자를 볼 수 없기 때문에 디버깅이 어려워지게 된다. 프레임 포인터 생략이 적용되지 않은 경우와 적용된 경우에 생성된 프롤로그와 에필로그의 예는 다음과 같다. 왼쪽이 일반적인 경우, 오른쪽이 프레임 포인터 생략이 적용된 경우이다. 프롤로그참고 /Oy (Frame-Pointer Omission) MSDN Call CDECL DLL’s – Impossible? Paul Caton, Sep. 2003 Calling Convention Wikipedia fastcall 호진’s Weblog, Dec. 2005 How can a program survive a corrupted stack? The Old New Thing, Jan. 2004 Intel x86 Function-call Conventions – Assembly View Steve Friedl Mac OS X ABI Function Call Guide : IA-32 Function Calling Conventions Apple.com Playing with the stack CodeProject.com Reverse Engineering/Calling Conventions Wikipedia Stack Frame Procedure |
정리해서 말하자면, Calling Convention은 함수 호출이 끝난 후 스택을 어떻게 정리하느냐의 관점에 따라 나뉘어진 규약이다.
(스택을 제대로 정리하지 않으면 치명적인 메모리 참조 오류가 발생해서 프로그램이 죽는다.)
다행히 모든 호출 규약의 공통점은 함수 파라미터를 뒤에서부터 스택에 적재한다는 점이다. 앞의 몇 개를 레지스터에 넣느니 마느니 하는 사소한 차이점은 있지만 어쨌든 스택에 적재하는 순서는 맨 뒤의 파라미터부터라는 점...
스택 프레임을 만들고 해제할 때 사용되는 프롤로그, 에필로그는 어셈블리 및 리버스 엔지니어링에서 굉장히 중요한 개념이지만 C/C++ 프로그래밍의 범주에서는 크게 신경쓸 필요 없다. 그냥 그런 게 있다 정도만 알면 된다. C/C++ 개발자가 프롤로그, 에필로그를 알아야 하는 경우는 naked 방식의 함수를 만들 경우뿐인데... 내가 보기엔 디바이스 드라이버 관련 프로그래머나 리버스 엔지니어로 코드 인젝션을 할 때 정도 외엔 없다.
'Tech: > C·C++' 카테고리의 다른 글
#define 매크로 함수: 이상한 동작? (0) | 2014.03.24 |
---|---|
C++ 컴파일 시 LNK 2019, LNK 2001/2005 링커 오류 대처법 (0) | 2013.07.03 |
[펌] Win32 API FAQ (1) | 2008.06.26 |
VC++2005 배포 (0) | 2008.06.26 |
C++ 동기화 객체(Critical Section, Mutex, Semaphore, Event) (0) | 2008.06.26 |