본문 바로가기
프로그래밍 언어/C, C++

[C/C++] 함수 호출 규약 __stdcall과 __cdecl에 관하여

by 니키티스 2023. 8. 3.

함수 호출 규약?

함수 호출 규약을 공부하게 된 계기

함수의 선언형은

  • 반환형 + 함수명 + 매개변수 리스트

로 구성된다.

함수의 반환형은 단 하나이다.

그래서 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은 함수 호출 규약이다.

내가 알기로는 함수 호출 규약은 함수를 호출하는 규칙을 말한다고 들었는데... 정확하게 이 두 개가 뭘 하는 걸까? 왜 일반 함수를 선언할 땐 함수 호출 규약을 작성하지 않아도 되는 것일까?

기본 개념

호출자와 피호출자의 개념을 알고 시작해야 한다.

사진 출처: tipssoft(참고 자료에 링크 있음)

  • 호출자는 함수를 호출하는 쪽, 피호출자는 호출되는 쪽

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)가 체크되어 있어 생략할 수 있다.

Visual Studio 2019에서 고급란 최상단에 __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)

댓글