C++에서 함수 호출 규약(Calling Convention)은 함수가 호출될 때 인수를 어떻게 전달하고, 반환값을 어떻게 처리하며, 스택을 누가 정리하는지를 정의하는 방식입니다. 주요 호출 규약은 다음과 같습니다.
cdecl (C Declaration)
- 기본적인 C/C++ 함수 호출 규약
- 인수 전달: 스택 (오른쪽에서 왼쪽)
- 반환값: EAX (32비트), RAX (64비트)
- 스택 정리: 호출자(caller)
- 가변 인자 지원: 가능 (printf 같은 함수에서 사용)
- MSVC에서 _cdecl, GCC에서 __attribute__((cdecl))로 지정
int __cdecl add(int a, int b) {
return a + b;
}
stdcall (Standard Call)
- Windows API에서 많이 사용
- 인수 전달: 스택 (오른쪽에서 왼쪽)
- 반환값: EAX / RAX
- 스택 정리: 피호출자(callee)
- 가변 인자 지원: 불가능
- MSVC에서 _stdcall, GCC에서 __attribute__((stdcall))로 지정
int __stdcall multiply(int a, int b) {
return a * b;
}
fastcall
- 일부 인수를 레지스터를 사용하여 전달 (MSVC: ECX, EDX)
- 나머지는 스택을 사용 (오른쪽에서 왼쪽)
- 반환값: EAX / RAX
- 스택 정리: 피호출자(callee)
- 가변 인자 지원: 불가능
- MSVC에서 _fastcall, GCC에서 __attribute__((fastcall))로 지정
int __fastcall subtract(int a, int b) {
return a - b;
}
thiscall
- C++ 멤버 함수에서 사용되는 기본 호출 규약
- this 포인터를 ECX 레지스터(32비트)로 전달
- 나머지 인수는 스택을 사용 (오른쪽에서 왼쪽)
- 반환값: EAX / RAX
- 스택 정리: 피호출자(callee)
- 직접 지정 불가 (컴파일러가 자동 적용)
class MyClass {
public:
int add(int a, int b); // thiscall 사용 (컴파일러에 의해 자동 적용)
};
vectorcall (MSVC 전용)
- 벡터 타입(예: __m128)을 레지스터로 전달하는 호출 규약
- MSVC에서 _vectorcall로 지정
- 가변 인자 지원: 불가능
int __vectorcall processVector(float x, float y, float z) {
return x + y + z;
}
SysV ABI (System V AMD64 ABI)
- 리눅스 및 macOS 64비트 시스템에서 사용되는 기본 호출 규약
- 인수 전달:
- 정수형: RDI, RSI, RDX, RCX, R8, R9
- 부동소수점: XMM0 ~ XMM7
- 나머지는 스택을 사용 (오른쪽에서 왼쪽)
- 반환값: RAX / XMM0
- 스택 정리: 호출자(caller)
명시적 호출 규약 적용
함수 포인터에 호출 규약을 지정할 수도 있습니다.
typedef int (__stdcall *FuncPtr)(int, int);
이렇게 하면, 해당 함수 포인터가 stdcall 규약을 따르는 함수만 가리킬 수 있습니다.
요약
호출 규약 | 인수 전달 방식 | 스택 정리 | 가변 인자 지원 | 주 사용처 |
cdecl | 스택 (R → L) | 호출자 | O | 기본 C/C++ |
stdcall | 스택 (R → L) | 피호출자 | X | Windows API |
fastcall | 레지스터(ECX, EDX) + 스택 | 피호출자 | X | 최적화 |
thiscall | this는 ECX + 스택 | 피호출자 | X | C++ 멤버 함수 |
vectorcall | 벡터 레지스터 사용 | 피호출자 | X | SIMD 최적화 |
SysV ABI | 레지스터 (RDI, RSI, ...) + 스택 | 호출자 | O | 리눅스 64비트 |
Q&A
Q: 함수 호출 규약을 선택하는 기준은 무엇인가요?
A: 일반적으로 cdecl이 기본이며, Windows API에서는 stdcall을 사용합니다. 최적화가 필요하면 fastcall이나 vectorcall을 고려할 수 있습니다.
Q: 가변 인자를 지원하는 호출 규약은 무엇인가요?
A: cdecl과 SysV ABI는 가변 인자를 지원하지만, stdcall, fastcall, thiscall, vectorcall은 지원하지 않습니다.
Q: 서로 다른 호출 규약을 혼용하면 어떤 문제가 발생하나요?
A: 함수 호출 규약이 다르면 스택 정리 방식이 다르므로 크래시 또는 스택 오염이 발생할 수 있습니다. 따라서 함수 호출 규약을 일관되게 사용해야 합니다.
Q: 64비트 환경에서는 어떤 호출 규약을 사용하나요?
A: Windows의 64비트에서는 fastcall 기반의 Microsoft x64 calling convention을 사용하며, Linux/macOS에서는 SysV ABI가 기본입니다.
결론
C++에서 호출 규약을 잘 이해하면, 함수 호출 최적화나 다른 언어와의 인터페이스(예: Windows API, DLL 호출)에서 유용하게 활용할 수 있습니다. 특히, 플랫폼에 따라 기본 호출 규약이 다를 수 있으므로 주의해야 합니다. 🚀