호출 규약

정의
호출 규약(Calling Convention)은 프로그램 내부 혹은 서로 다른 프로그램 간에 함수·프로시저를 호출할 때, 인자 전달 방식, 반환값 처리, 스택 관리, 레지스터 사용, 이름 장식(name mangling) 등과 같은 상세 규칙을 정의한 약속이다. 이러한 규약은 컴파일러와 링커, 운영체제, 그리고 하드웨어 아키텍처가 일관된 방식으로 함수 호출을 수행하도록 보장하여, 서로 다른 컴파일러로 만든 객체 파일 간의 호환성을 가능하게 한다.

주요 요소

요소 내용 예시
인자 전달 방식 함수 인자를 스택에 푸시하거나, 레지스터에 복사한다. cdecl – 오른쪽에서 왼쪽 순으로 스택에 푸시
stdcall – 오른쪽에서 왼쪽 순이지만 호출자(caller)가 아니라 callee가 스택을 정리
스택 정리 책임 호출 후 스택 포인터를 원래 위치로 되돌리는 책임이 호출자에게 있느냐(caller‑clean) 혹은 피호출자에게 있느냐(callee‑clean) 를 결정한다. cdecl – caller clean
stdcall – callee clean
반환값 처리 반환값을 레지스터(예: eax/rax)에 저장하거나, 메모리 주소를 인자로 전달한다. 정수 반환 → eax/rax
구조체 반환 → 메모리 주소 인자
레지스터 보존 규약 함수 호출 전후에 유지되어야 할 레지스터 집합을 정의한다. (callee‑saved vs. caller‑saved) stdcallebx, esi, edi는 callee‑saved
이름 장식 동일한 함수명이 다른 언어·옵션에 의해 충돌하지 않도록 함수 이름에 추가 정보를 부여한다. C++ → ?Func@@YAXH@Z (MSVC)
스택 정렬 호출 시 스택 포인터를 특정 바이트 경계(예: 16‑byte)로 맞춘다. x86‑64 System V ABI – 16‑byte 정렬 요구

주요 호출 규약 종류

규약 사용 플랫폼·언어 특징
cdecl x86, 대부분의 C 컴파일러 인자 오른쪽‑왼쪽 순 스택 푸시, 호출자 스택 정리, 가변 인자 지원
stdcall Windows API, Win32 인자 오른쪽‑왼쪽 순, 피호출자 스택 정리, 가변 인자 미지원
fastcall Microsoft, 일부 GCC 첫 2~3개의 인자를 레지스터(ecx, edx)에 전달, 나머지는 스택
thiscall C++ 멤버 함수 (MSVC) this 포인터를 ecx 레지스터에 전달, 나머지는 stdcall 와 유사
System V AMD64 ABI Linux, macOS, *nix x86‑64 첫 6개의 정수·포인터 인자를 레지스터(rdi, rsi, rdx, rcx, r8, r9)에 전달, 스택은 16‑byte 정렬
Microsoft x64 calling convention Windows x86‑64 첫 4개의 정수·포인터 인자를 레지스터(rcx, rdx, r8, r9)에 전달, 스택 정렬 16‑byte, caller‑clean 방식
ARM AAPCS (Procedure Call Standard for the ARM Architecture) ARM, ARM64 인자 전달 규칙이 레지스터(r0‑r3)와 스택으로 나뉨, 스택 8‑byte 정렬, 부동소수점은 s0‑s15(32‑bit) 또는 v0‑v7(64‑bit) 사용
WebAssembly ABI 웹 어셈블리 모든 정수·포인터 인자를 스택에 푸시, 레지스터는 가상 머신 내부에서 최적화

플랫폼·언어별 적용 사례

  1. Windows (x86) API
    • 대부분의 Win32 함수는 stdcall을 사용한다. 예를 들어, CreateWindowExA는 피호출자가 스택을 정리한다.
  2. Linux (x86‑64) 시스템 콜
    • 커널 진입점은 System V AMD64 ABI와 동일한 레지스터 전달 방식을 따른다.
  3. C++ 멤버 함수
    • MSVC에서는 thiscall을 사용해 this 포인터를 ecx에 전달하고, 나머지 인자는 stdcall 규칙을 따른다.
  4. JNI (Java Native Interface)
    • Java 가상 머신이 네이티브 메서드를 호출할 때는 플랫폼 기본 호출 규약을 그대로 사용한다.
  5. 다양한 언어 간 인터옵
    • Python의 ctypes, Ruby의 FFI 등은 사용자가 호출 규약(cdecl, stdcall 등)을 지정할 수 있게 하여, 외부 공유 라이브러리와 안전하게 연동한다.

역사와 표준화

  • 1970‑80년대: 초기 C 컴파일러는 cdecl을 기본으로 채택했으며, 이는 어셈블리 수준에서 바로 구현 가능했다.
  • 1990년대: 마이크로소프트는 Windows API를 효율적으로 만들기 위해 stdcall을 도입했고, 동시에 fastcall을 제안했다.
  • 2000년대: 64‑bit 전환으로 레지스터 기반 전달이 일반화되면서 System V AMD64 ABIMicrosoft x64 calling convention이 정립되었다.
  • 현재: 다양한 CPU 아키텍처와 운영체제마다 독자적인 ABI(응용 바이너리 인터페이스) 표준이 존재하고, 각각의 ABI 문서에 호출 규약이 상세히 기술되어 있다. 예를 들어, AMD64 System V ABI(PDF), Microsoft x64 Calling Convention(MSDN), ARM AAPCS(ARM Architecture Reference Manual) 등이다.

호출 규약과 ABI의 관계

  • ABI는 프로그램이 운영체제와 하드웨어와 상호작용하는 전체 규약을 의미한다. 호출 규약은 ABI의 핵심 하위 요소 중 하나이며, 함수 호출· 반환, 스택 레이아웃, 레지스터 보존, 시스템 콜 인터페이스 등을 포함한다.
  • 따라서 동일한 ABI를 따르는 서로 다른 컴파일러가 만든 바이너리도 서로 호환될 수 있다. 반대로, ABI가 다르면 호출 규약이 달라지므로 직접적인 바이너리 호환이 불가능하다.

실제 코드 예시 (x86, cdecl)

/* foo.c – cdecl 규약을 사용한 함수 */
int foo(int a, int b) {
    return a + b;
}

/* main.c – 동일 컴파일러로 호출 */
extern int foo(int, int);   // 선언만 하면 cdecl 가정

int main(void) {
    int r = foo(3, 4);      // 컴파일러가 인자를 스택에 푸시하고 호출자
    return r;               // 호출 후 스택 정리
}

위 예시에서 컴파일러는 다음과 같은 어셈블리 시퀀스를 생성한다(간략화):

push    ebp
mov     ebp, esp
push    4          ; b
push    3          ; a
call    _foo
add     esp, 8    ; caller가 스택 정리

요약
호출 규약은 함수·프로시저 호출 시 반드시 지켜야 할 저수준 인터페이스 규칙으로, 인자 전달, 반환값, 스택/레지스터 관리, 이름 장식 등을 포괄한다. 플랫폼·언어·컴파일러마다 다양한 규약이 존재하지만, 모두 ABI의 일부분으로 정의되어 서로 다른 모듈 간의 바이너리 호환성을 보장한다.


출처 : AMD64 System V ABI, Microsoft Docs – x64 Calling Convention, ARM Architecture Reference Manual, 다양한 컴파일러 문서(Visual C++, GCC, Clang) 등.

둘러보기

더 찾아볼 만한 주제