함수 호출 규약?
함수 호출 규약을 공부하게 된 계기
함수의 선언형은
- 반환형 + 함수명 + 매개변수 리스트
로 구성된다.
함수의 반환형은 단 하나이다.
그래서 int, double과 같은 기본형과 MyClass 같은 사용자 정의형이 반환형에 들어간다.
그런데 아래 함수들을 보니 반환형과 함수 이름 사이에 뭔가 인자가 하나 더 있다.
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR pScmdline, int iCmdshow);
LRESULT CALLBACK SystemClass::MessageHandler(HWND hwnd, UINT umsg,
WPARAM wparam, LPARAM lparam);
WINAPI, CALLBACK 등이 바로 그것이다.
이게 뭔가 해서 매크로 선언을 보니 minwindef.h에 이렇게 되어 있다.
...
**#define CALLBACK __stdcall**
**#define WINAPI __stdcall**
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
...
__stdcall과 __cdecl은 함수 호출 규약이다.
내가 알기로는 함수 호출 규약은 함수를 호출하는 규칙을 말한다고 들었는데... 정확하게 이 두 개가 뭘 하는 걸까? 왜 일반 함수를 선언할 땐 함수 호출 규약을 작성하지 않아도 되는 것일까?
기본 개념
호출자와 피호출자의 개념을 알고 시작해야 한다.
- 호출자는 함수를 호출하는 쪽, 피호출자는 호출되는 쪽
MSDN 설명
- cdecl은 C 및 C++ 프로그램의 기본 호출 규칙입니다. 스택은 호출자(caller)에 의해 정리되기 때문에 vararg 함수를 수행할 수 있습니다. __cdecl 호출 규칙은 각 함수 호출에 스택 정리 코드를 포함시키기 때문에 [stdcall](https://msdn.microsoft.com/ko-kr/library/zxk0tw93.aspx)보다 큰 실행 가능 명령문을 생성합니다.
- __stdcall 호출 규칙은 Win32 API 함수를 호출하는 데 사용됩니다. 호출 수신자(callee)가 스택을 정리하므로 컴파일러는 vararg 함수를 __cdecl로 만듭니다. 이 호출 규칙을 사용하는 함수에는 함수 프로토타입이 필요합니다.
간단 정리
__cdecl은 C 및 C++ 프로그램의 기본 호출 규칙이다.
- 인자의 개수가 가변적인 함수를 호출하는 데에 사용되는 호출 규정
__stdcall은 Win32 API 함수를 호출할 때 사용한다.
- 인자의 개수가 고정적인 Win32 API 함수를 호출하는 데에 사용되는 호출 규정
두 호출 규약의 특징은 다음과 같다.
1. 인자의 개수
- __cdecl은 매개변수의 개수가 가변적이다. vararg 함수를 수행할 수 있다는 것이 바로 그 뜻이다.
- printf, scanf와 같은 함수는 매개변수의 개수가 고정되어 있지 않다.
- __stdcall은 매개변수의 개수가 고정되어 있다.
2. 인자 전달 순서
- __cdecl과 __stdcall 모두 오른쪽에서 왼쪽으로 인자를 전달한다.
- int Sum(int a, int b); 와 같다면...
- 오른쪽 int b를 전달한 다음 왼쪽 int a를 전달한다.
3. ***누가 스택을 정리하는가?
- 스택 프레임에 삽입, 삭제되는 인자의 개수가 변하냐 아니냐에 따라 이 스택을 누가 정리할지가 정해진다.
- __stdcall은 인자의 개수가 고정적이므로 피호출자(Callee)가 스스로 스택을 정리한다.
- __cdecl은 인자의 개수가 일정하지 않으므로 피호출자가 스스로 스택을 정리하기 어렵다. 따라서 호출자(Caller)가 스택을 정리한다.
- 함수 호출 시 호출자가 스택을 정리한 후
- 함수를 실행하고
- 실행을 마친 후 다시 호출자가 자신이 삽입했던 인자를 제거하며 스택을 정리한다.
4. 실행 파일 크기
- 호출 규약에 따라 실행 파일 크기가 달라진다.
- 함수 명이 Sum이라 할 때, 만약 call Sum을 여러 번 작성한다고 치자.
- __stdcall은 실행 코드(실행 파일) 크기가 상대적으로 작다.
- 피호출자가 스택을 정리하므로, 호출자에 “call Sum이 중복해서 발생하고 스택에 인자를 삽입 삭제하는 부분은 피호출자가 공통적으로 수행
- __cdecl은 실행 코드(실행 파일) 크기가 상대적으로 크다.
- 호출자는 스택에 인자를 삽입/삭제하는 부분이 함수를 호출할 때마다(call Sum) 발생하기 때문에 __stdcall 호출 규정보다 어셈블리어로 변환된 실행 코드의 크기가 커진다.
5. 이름 변환 규정
- 호출 규정에 따라 함수의 선언부가 일정한 패턴에 맞추어 변환된다.
- __stdcall은 인자의 수가 고정적이므로 “_함수명+@+인자목록의 바이트 수(10진 정수)”로 쓴다.
- void __stdcall func(int a, double b) → _func@12
- __cdecl은 인자의 수가 가변적이므로 가변 인자 크기를 표시하지 않는다.
- void __cdecl func(int a, double b) → _func
그런데 우리가 실제로 함수를 선언할 때 함수 호출 규약을 따로 붙이지 않는다. 그럼 함수 호출 규약은 누가 정하는 것일까?
- 기본적으로 함수 호출 규약은 __cdecl이므로, 생략하면 __cdecl로 하게 된다.
- VS 옵션에서 C/C++ 고급란을 보면 __cdecl(/Gd)가 체크되어 있어 생략할 수 있다.
- 컴파일러 옵션에 따라 기본 호출 규정이 달라질 수 있다.
/Gz 컴파일러 옵션
함수가 명시적으로 호출 규정을 선언하지 않았을 경우 기본으로 __stdcall을 적용합니다.
/Gr 컴파일러 옵션
함수가 명시적으로 호출 규정을 선언하지 않았을 경우 기본으로 __fastcall을 적용합니다.
/Gd 컴파일러
옵션 해당 함수의 호출 규정이 어떤 것이든 상관없이 강제적으로 __cdecl 호출 규정을 적용시킵니다.
이외에도 호출 규약에 __fastcall, __thiscall도 있다.
- __fastcall은 __stdcall과 유사하게 매개변수를 오른쪽에서부터 스택에 Push 하고 피호출자 함수가 스택을 정리한다.
- 그러나 매개변수가 여러 개면 가장 나중에 Push 되어야 할 왼쪽 첫 번째, 두 번째 매개변수를 스택이 아니라 CPU의 레지스터(EDX, ECX)를 이용해 전달한다.
- __thiscall은 객체의 멤버함수(method)를 호출하는 것에 관련된 호출 규칙이다.
참고 자료
발견한 글: __stdcall 과 __cdecl 이 무엇인가? : 네이버 블로그 (naver.com)
기초 이론: 팁스소프트 > MFC/API 가이드 > [교육자료] __stdcall 과 __cdecl 에 대하여... (tipssoft.com)
어셈블리어로 보고 싶다면... <함수에 대한 고급 이론> 함수 호출 규칙 - waca's field (tistory.com)
'프로그래밍 언어 > C, C++' 카테고리의 다른 글
[C++] 레퍼런스, const 레퍼런스 반환 / 레퍼런스와 임시 개체의 수명 (1) | 2024.02.05 |
---|---|
[C/C++] 메모리 초기화하기: ZeroMemory 매크로 (0) | 2023.08.06 |
[C++] 스마트 포인터 - unique_ptr과 안전하게 쓰는 방법 (0) | 2023.07.20 |
[C++] 왜 string은 반환이 될까? 객체는 반환할 때 복사된다(+구조체) (0) | 2022.01.20 |
C언어에서의 16진수, signed int와 unsigned int (0) | 2021.05.03 |
댓글