kj0on

[Definition] 32비트 함수 호출 규약 (32bit Calling Convention) 본문

Reversing/Definition

[Definition] 32비트 함수 호출 규약 (32bit Calling Convention)

kj0on 2025. 7. 14. 17:43
목차 접기

0. 32비트 스택 프레임 (32bit Stack Frame)


32비트 스택프레임에 대한 자세한 설명은 https://kj0on.tistory.com/41 참고


1. 정의


함수와 호출자 간에 인수를 전달하고 값을 반환하기 위한 규칙
 
프로시저(함수) 호출 시 인자를 어디에 어떤 순서로 전달하고, 누가 스택을 정리하며, 레지스터를 보존할지, 어느 레지스터로 값을 반환할지 등을 규정한 저수준 인터페이스 계약이다. 컴파일러, 언어, OS, CPU가 서로 다른 오브젝트 코드를 같은 ABI 안에서 링크 및 호출할 수 있게 해준다.
 
https://learn.microsoft.com/ko-kr/cpp/cpp/calling-conventions?view=msvc-170


2. Caller(호출자)와 Callee(피호출자)

[이미지1] caller callee

Caller는 함수를 호출하는 쪽이고, Callee는 호출된 함수이다. Caller는 인자를 준비하고 제어를 Callee에게 넘기며, Callee는 이를 받아 작업을 수행한 뒤 결과를 반환한다. 이 과정에서 책임이 명확하게 나뉘며, 어떤 쪽이 어떤 책임을 지는지는 사용된 호출 규약에 따라 사전에 정해진 방식으로 결정된다.


3. 호출 규약 비교 요약 (C/C++)

키워드 스택 정리 매개 변수 전달
__cdecl 호출자 매개 변수를 스택에 역순으로(오른쪽에서 왼쪽으로) 푸시합니다.
__clrcall 해당 없음 CLR 식 스택에 매개 변수를 순서대로(왼쪽에서 오른쪽으로) 로드합니다.
__stdcall 호출 수신자 매개 변수를 스택에 역순으로(오른쪽에서 왼쪽으로) 푸시합니다.
__fastcall 호출 수신자 레지스터에 저장된 다음 스택에 푸시됩니다.
__thiscall 호출 수신자 스택에 푸시됨; this ECX에 저장된 포인터
__vectorcall 호출 수신자 레지스터에 저장된 다음 스택에 역순으로(오른쪽에서 왼쪽으로) 푸시됩니다.

https://learn.microsoft.com/ko-kr/cpp/cpp/argument-passing-and-naming-conventions?view=msvc-170


4. 함수 호출 규약 분류

Win16 및 초기 컴파일러 환경에서는 각 프로그래밍 언어마다 고유한 호출 방식이 존재했고, 운영체제 내부용 전용 호출 규약도 함께 사용되었다. 그러나 이러한 다양성은 호환성 문제와 확장성의 한계를 드러냈고, 결국 Win32 시대에 들어 실무적으로 표준화된 호출 규약들이 등장하면서 언어나 환경을 넘어 보다 효율적이고 일관된 방식으로 정립되기 시작했다. 이후 Win64 환경에서는 아예 단일 호출 규약이 강제되어 모든 언어와 컴파일러가 통일된 방식으로 함수를 호출하게 되었으며, 사실상 호출 규약 간의 차이가 사라졌다.
 

[이미지3] Calling Convention

이 글에서는 다양한 호출 규약 중에서도 현재 32비트 환경에서 가장 빈번히 사용되는 __cdecl, __stdcall, __fastcall, __vectorcall 만 집중적으로 다룬다.


5. Windows x32 함수 호출 규약 비교


함수 호출 규약 각각의 인수 전달 방식, 인수 전달 매체, 스택 유지 관리 책임 ,함수 이름 데코레이션을 설명한다. 또한 이해를 돕기 위해 실제 예제 코드와 함께 호출 규약에 따른 함수 호출 과정의 차이를 비교한다.


5-1. 예제 코드

#include <stdio.h>

int int_add_fun(int a, int b, int c, int d) {
    int i1 = a;
    int i2 = b;
    int i3 = c;
    int i4 = d;
    int result = 0;
    result = i1 + i2 + i3 + i4;
    return result;
}

float float_add_fun(float a, float b, float c, float d) {
    float f1 = a;
    float f2 = b;
    float f3 = c;
    float f4 = d;
    float result = 0;
    result = f1 + f2 + f3 + f4;
    return result;
}

int main() {

    int int_result = int_add_fun(1, 2, 3, 4);
    printf("%d + %d + %d + %d = %d\n", 1, 2, 3, 4, int_result);

    float float_result = float_add_fun(0.1f, 0.2f, 0.3f, 0.4f);
    printf("%.1f + %.1f + %.1f + %.1f = %.1f\n", 0.1f, 0.2f, 0.3f, 0.4f, float_result);

    return 0;
}

이 코드는 네 정수를 더하는 함수 int_add_fun과 네 소수를 더하는 함수 float_add_fun을 정의하고, 이를 main 함수에서 호출하여 결과를 출력하는 간단한 예제이다.
 

[이미지4] 실행 결과


5-2. __stdcall

[이미지5] __stdcall (/Od)

 

항목 설명
인수 전달 순서 오른쪽 → 왼쪽 (Right to Left)
인수 전달 매체 스택 (All on stack)
스택 유지 관리 책임 피호출자 (Callee)
함수 이름 데코레이션 _<function name>@<bytes>

 

5-2-1. 인수 전달 순서

[이미지5] __stdcall (/Od) Parameter Passing Order

모든 인수(정수, 포인터, 부동소수점, 구조체 등)를 오른쪽에서 왼쪽 순으로 스택에 push한다.


5-2-2. 인수 전달 매체

[이미지6] __stdcall (/Od) Parameter Passing Medium

모든 인수(정수, 포인터, 부동소수점, 구조체 등)를 스택으로 전달한다.


5-2-3. 스택 유지 관리 책임

[이미지7] __stdcall (/Od) Stack Cleanup Responsibility

Callee(피호출자)가 ret <n>으로 스택을 정리한다.


5-2-4. 함수 이름 데코레이션

[이미지8] __stdcall (/Od) Name Decoration Scheme

언더바(_)가 함수 이름 앞에 붙는다. 함수 이름 뒤에는 앳기호(@)가 오고 그 뒤에 인수 목록의 바이트 수(10진수)가 접미사로 지정된다.


5-3. __cdecl

[이미지9] __cdecl (/Od)

 

항목 설명
인수 전달 순서 오른쪽 → 왼쪽 (Right to Left)
인수 전달 매체 스택 (All on stack)
스택 유지 관리 책임 호출자 (Caller)
함수 이름 데코레이션 _<function name>


5-3-1. 인수 전달 순서

[이미지10] __cdecl (/Od) Parameter Passing Order

모든 인수(정수, 포인터, 부동소수점, 구조체 등)를 오른쪽에서 왼쪽 순으로 스택에 push한다.


5-3-2. 인수 전달 매체

[이미지11] __cdecl (/Od) Parameter Passing Medium

모든 인수(정수, 포인터, 부동소수점, 구조체 등)를 스택으로 전달한다.


5-3-3. 스택 유지 관리 책임

[이미지12] __cdecl (/Od) Stack Cleanup Responsibility

Caller(호출자)가 add esp, <n>으로 스택을 정리한다.


5-3-4. 함수 이름 데코레이션

[이미지13] __cdecl (/Od) Name Decoration Scheme

C 링크를 사용하는 __cdecl 함수를 내보낼 경우를 제외하고 언더바(_)는 함수 이름 앞에 접두사로 지정된다.


5-4. __fastcall

[이미지14] __fastcall (/Od)

 

항목 설명
  레지스터 (Register) 스택 (Stack)
인수 전달 순서 왼쪽 → 오른쪽 (Left to right) 오른쪽 → 왼쪽 (Right to left)
인수 전달 매체 ECX, EDX Stack
스택 유지 관리 책임 X 피호출자 (Callee)
함수 이름 데코레이션 @<function name>@<bytes>

 

 

5-4-1. 인수 전달 순서

[이미지15] __fastcall (/Od) Parameter Passing Order

정수, 포인터 인수의 경우 첫 번째와 두 번째 인수를 각각 ECX, EDX 레지스터에 로드하고, 세 번째 이후 인수는 오른쪽에서 왼쪽 순으로 스택에 push한다. 부동소수점 및 레지스터 크기를 벗어나는 인수(64비트 자료형, 구조체 등)는 모두 오른쪽에서 왼쪽 순으로 스택에 push하며, ECX, EDX를 사용하지 않는다.


5-4-2. 인수 전달 매체

[이미지16] __fastcall (/Od) Parameter Passing Medium

정수 및 포인터 두 인수는 레지스터(ECX, EDX)로, 나머지 인수는 스택으로 전달한다.


5-4-3. 스택 유지 관리 책임

[이미지17] __fastcall (/Od) Stack Cleanup Responsibility

Callee(피호출자)가 ret <n>으로 스택을 정리한다.


5-4-4. 함수 이름 데코레이션

[이미지18] __fastcall (/Od) Name Decoration Scheme

함수 이름 앞, 뒤에 앳기호(@)가 붙는다. 매개 변수 목록의 바이트 수(10진수)가 접미사로 지정된다.


5-5. __vectorcall

[이미지19] __vectorcall (/Od)

 

항목 설명
  레지스터 (Register) 스택 (Stack)
인수 전달 순서 왼쪽 → 오른쪽 (Left to right) 오른쪽 → 왼쪽 (Right to left)
인수 전달 매체 ECX, EDX, XMM0 ~ XMM5 Stack
스택 유지 관리 책임 X 피호출자 (Callee)
함수 이름 데코레이션 <function name>@@<bytes>

 

5-5-1. 인수 전달 순서

[이미지20] __vectorcall (/Od) Parameter Passing Order

정수, 포인터 인수의 경우 첫 번째와 두 번째 인수를 각각 ECX, EDX 레지스터에 로드하고, 세 번째 이후 인수는 오른쪽에서 왼쪽 순으로 스택에 push한다. 레지스터 크기를 벗어나는 인수(64비트 자료형, 구조체 등)는 모두 오른쪽에서 왼쪽 순으로 스택에 push하며, ECX, EDX를 사용하지 않는다. 부동소수점 및 벡터 인수는 왼쪽에서 오른쪽 순으로 XMM0부터 최대 XMM5까지 채운다. 레지스터에 실리지 못한 인수는 오른쪽에서 왼쪽 순으로 스택에 push한다.


5-5-2. 인수 전달 매체

[이미지21] __vectorcall (/Od) Parameter Passing Medium

부동소수점 및 벡터 인수는 XMM0~XMM5 레지스터, 정수 및 포인터 인수는 ECX, EDX 레지스터로, 나머지 인수는 스택으로 전달한다.


5-5-3. 스택 유지 관리 책임

[이미지22] __vectorcall (/Od) Stack Cleanup Responsibility

Callee(피호출자)가 ret <n>으로 스택을 정리한다.


5-5-4. 함수 이름 데코레이션

[이미지23] __vectorcall (/Od) Name Decoration Scheme

함수 이름 뒤에 두 개의 앳기호(@@)가 붙고 그 뒤에 매개 변수 목록의 바이트 수(10진수) 접미사로 지정된다.


[이미지24] 함수 호출 규약 (Calling Convention)


6. 함수 호출 규약의 분기 원인과 설계적 배경

 

6-1.  __stdcall : Win32의 기본 호출 규약


__stdcall은 16비트 Windows 시절의 __pascal 호출 규약을 기반으로 만들어진 Win32의 표준 호출 규약이다. __pascal 규약처럼 피호출자(callee)가 스택을 정리한다는 특징은 유지하면서도, 인자 전달 순서는 C 언어의 관례대로 오른쪽에서 왼쪽 순으로 바꾸었다. 덕분에 호출할 때마다 스택 정리 코드를 작성할 필요가 없어 코드 크기를 줄이고 프로그래머의 실수를 줄일 수 있어 Windows API나 COM 인터페이스의 기본 호출 방식으로 자리 잡았다. standard call이라는 명칭에서도 알 수 있듯이 Windows API의 대부분 함수들이 이 규약을 따르고 있다.


6-2. __cdecl : 가변 인자를 위한 호출 규약

#include <stdio.h>
#include <stdarg.h>

int main(void)
{
    int total = var_add_fun(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    printf("total = %d\n", total);
    return 0;
}

int var_add_fun(int count, ...)
{
    int result = 0;
    va_list args;
    va_start(args, count);
    for (int i = 0; i < count; ++i) {
        result += va_arg(args, int);
    }
    va_end(args);
    return result;
}

위 코드는 가변 인자 n개를 모두 더한 뒤 그 합계를  printf로 출력하고 종료하는 프로그램이다.
 

[이미지25] C2373 Error

__stdcall로 해당 코드를 빌드할 수 없다. 그 이유는 헤더 선언이 없으면 컴파일러는 int var_add_fun(int , ...); 함수를 묵시적으로 __cdecl로 가정해 첫 선언을 만든다. __stdcall로 빌드를 진행하면 실제로 작성된 함수의 정의는 int __stdcall var_add_fun(int , ... )로 설정되기 때문에 동일 식별자에 대해 두 번째 선언의 호출 규약이 다르다고 판단해 C2373 컴파일러 오류가 발생한다. __cdecl로 빌드 시 정상적으로 동작한다.
 

[이미지26] C2373 (https://learn.microsoft.com/ko-kr/cpp/error-messages/compiler-errors-1/compiler-error-c2373?view=msvc-170)

MSVC 에러 C2373는 같은 함수 이름이 이전 선언과 다른 호출 규약, 수식어로 다시 선언 및 정의될 때 발생한다.
 

[이미지27] __stdcall 선언

만약 var_add_fun 함수를 __stdcall로 선언하고, 실제로 해당 호출 규약을 __stdcall로 적용해 빌드한다면 어떤 결과가 나타날까?
 

[이미지28] main

디버깅 후 어셈블리 코드를 분석해 보면, 함수 이름 데코레이션과 호출자(Caller)가 스택을 정리한다는 점을 토대로 var_fun_add 함수가 __cdecl로 강제 변환되었음을 확인할 수 있다. 이를 통해 함수에 호출 규약을 명시하지 않은 경우, 그리고 함수 선언에 __stdcall이나 다른 규약을 명시하더라도 가변 인자 함수인 경우에는 무조건 __cdecl로 강제된다는 것을 알 수 있다(https://learn.microsoft.com/ko-kr/cpp/cpp/stdcall?view=msvc-170).
 

[이미지29] Caller Callee

__cdecl로 강제되는 이유는 __stdcall과 같이 Callee(피호출자)에서 스택을 정리하는 경우는 Callee(피호출자)에서 인자 개수를 알 수 없기 때문에 해당 호출 규약을 가변 함수에 적용할 수 없기 때문이다. 또한 호출 규약을 명시하지 않은 경우에도 인자 개수, 타입 정보를 알 수 없는 상태이기 때문에 가장 안전하고 보편적인 호출을 보장하려면 __cdecl이 기본값일 수 밖에 없다. 위 이미지를 보면 Caller(호출자)는 함수를 호출하기 전, 인자를 직접 push(또는 ESP에 mov)하기 때문에 인자의 크기를 알 수 있다. 하지만 Callee(피호출자)는 스택에 존재하는 인자값을 가져와서 함수 내부코드만 수행하기 때문에 인자의 크기를 알 수가 없는 것이다. 이 경우는 가변인자를 사용했을 때 나타나며, 인자의 개수가 고정적일 때에는 Callee(피호출자) 또한 인자의 크기를 파악할 수 있다.
 

[이미지30] Caller Callee

Callee에서 가변 인자의 개수를 절대로 알 수 없는 것은 아니다. 가변 인자 모두를 사용하도록 코드를 작성 한다면 Callee에서도 인자의 크기를 알 수 있다. 그렇다면 왜 "Callee(피호출자)에서 인자의 크기를 알 수가 없다."라고 정의하는 것일까. 이 질문을 조금 바꿔서 "위와 같이 Caller에서 인자의 개수를 바꿔가면서 함수를 호출했다면 컴파일 시점에서 Callee의 ret <n>에 <n> 값을 쓸 수 있는가?"에 대한 답을 하면 된다. 함수를 호출한다는 것은 그 함수로 jmp 한다는 것을 의미한다(push eip와 더불어). Caller에서 호출을 여러번 했지만 이는 var_add_fun 함수가 여러 개 인것을 뜻하지는 않는다. 호출에서 모두 동일한 var_add_fun에 접근하고 있다. 이 때 ret <n>에 값을 쓸 수 있을까? 컴파일 시점에서 <n>의 값을 다르게 설정하기 위해서는 함수를 호출할 때 마다 var_add_fun 전체를 복사해서 ret <n> 값을 바꿔줘야 할 것이다. 이는 코드 중복을 유발하고 실행 파일의 크기를 불필요하게 증가시키며, 함수 재사용이라는 개념을 무력화시킨다는 점에서 매우 비효율적이다.
 

[이미지31] Caller Callee

Caller에서 가변 인자의 크기를 계산할 수 있다면 그 값을 Callee에 넘겨줘서 <n>의 값을 바꾸면 되지 않을까? 이론적으로는 가능하지만 현실적으로는 매우 비효율적인 방식이다. ret <n>은 컴파일 시점에 상수로 결정되는 기계어 명령어이기 때문에, 실행 중에 가변적으로 값을 바꿀 수 없다. 만약 동적으로 바꾸려면 ret 명령 자체를 가변 인자 크기를 받아오는 형태로 모두 교체해야 하는데, 이는 오히려 함수 구조를 깨뜨리고 오버헤드를 유발한다.
 

[이미지32] printf

이처럼 가변 인자를 처리하는 구조적 한계로 인해, 호출 규약이 다르더라도 가변 인자 함수를 호출할 때에는 반드시 __cdecl 호출 규약을 따라야 한다. 실제로 __stdcall, __fastcall, __vectorcall 등 다른 호출 규약을 사용하는 환경에서도, printf와 같은 대표적인 가변 인자 함수는 항상 __cdecl로 호출되는 것을 확인할 수 있다. 이는 호출 규약의 선언과 무관하게, 가변 인자 함수는 Caller(호출자)가 스택을 정리해야만 올바르게 동작할 수 있기 때문이다.
 

[이미지33] nop patch

만약 가변 인자 함수를 사용하는 코드에서 __stdcall처럼 Callee(피호출자)가 스택을 정리하는 방식으로 동작하게 되면 어떤 문제가 발생할까? 앞서 설명했듯이, 이러한 상황은 컴파일러가 호출 규약과 가변 인자 구조의 불일치를 감지하여 빌드 자체를 허용하지 않기 때문에 정상적으로 생성할 수 없다. 따라서 해당 코드를 __cdecl로 빌드한 뒤, Caller(호출자)에서 수행하는 스택 정리 명령어 add esp, <n>을 nop 패치하여 강제로 호출 규약을 변경한다.
 

[이미지34] 실행 결과

실행 결과, 프로그램은 정상적으로 실행된다. 표면적으로는 문제가 없는 것처럼 보이지만 함수 호출 전후의 ESP 값을 비교해 보면 호출 이후 ESP가 원래 위치로 되돌아오지 않았다는 점을 통해 스택이 정리되지 않았음을 확인할 수 있다. 이는 add esp, <n> 명령어가 제거되었기 때문에, 가변 인자로 전달된 인자들이 스택에 그대로 남아 있는 상태가 된다.
 

#include <stdio.h>
#include <stdarg.h>

int __cdecl var_add_fun(int count, ...);

int main(void)
{
    for (int i = 0; i < 1000000; i++) {
        int total = var_add_fun(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        printf("total = %d\n", total);
    }
    return 0;
}

int var_add_fun(int count, ...)
{
    int result = 0;
    va_list args;
    va_start(args, count);
    for (int i = 0; i < count; ++i) {
        result += va_arg(args, int);
    }
    va_end(args);
    return result;
}

이 함수가 프로그램 내에서 여러 번 호출되는 상황을 가정하고, 반복문을 이용해 해당 함수를 100만 회 호출한 뒤, 그 결과가 어떻게 나타나는지 확인한다.
 

[이미지35] nop patch

 
호출자에 의해 스택이 정리되지 않은 상태가 매 호출마다 누적되면서 스택 영역이 점차 소진되고 결국 가드 페이지를 침범하기 때문에 EXCEPTION_STACK_OVERFLOW 예외가 발생한다. 이러한 구조적 제약으로 인해, __stdcall 호출 규약에서는 가변 인자를 수용할 수 없고 가변 인자 함수는 반드시 Caller(호출자)가 스택을 정리하는 __cdecl 호출 규약을 따르도록 강제되는 것이다. __cdecl은 Caller(호출자)가 스택을 정리하기 때문에, __stdcall에 비해 add esp, 명령어 한 줄이 추가되어 코드 크기가 다소 커질 수 있다. 하지만 이 한 줄로 가변 인자를 포함한 다양한 호출 상황을 유연하게 처리할 수 있다는 점에서, __cdecl은 단순한 대체가 불가능한, 매우 효율적인 호출 규약이라 할 수 있다.


6-3. __fastcall : 호출 성능 향상을 위한 레지스터 최적화


__fastcall은 함수 호출 시 전달되는 인자 중 일부를 레지스터로 전달함으로써 호출 성능을 개선하기 위해 도입된 호출 규약이다. 기존의 __stdcall과 __cdecl에서는 모든 인자를 스택에 push하여 전달했기 때문에, 호출 시마다 메모리 접근이 필요했고 이로 인한 오버헤드가 존재했다. __fastcall은 이러한 호출 비용을 줄이기 위해, 첫 번째와 두 번째 정수형 또는 포인터 인자를 각각 ECX, EDX 레지스터에 저장하고, 그 이후의 인자들만 스택을 통해 전달한다. 이로 인해 스택 접근을 최소화할 수 있으며, 특히 짧고 빈번하게 호출되는 함수에서 유의미한 성능 향상을 기대할 수 있다. 다만 __fastcall은 부동소수점, 구조체 등은 모두 스택을 통해 전달된다는 제약을 가진다. 또한 전달할 인자가 많을 경우, 결국 대부분의 인자가 스택으로 넘어가게 되어 성능 개선 효과가 제한적이다. 컴파일러에 따라 구현 방식이 상이하며, 표준이 아닌 비표준 확장 규약 이기 때문에, 외부 라이브러리나 플랫폼 간 호환성이 떨어질 수 있다는 문제점 또한 존재한다. 그럼에도 불구하고 __fastcall은 호출 오버헤드를 줄이려는 설계 목적에 부합하며 이는 이후 등장하는 __vectorcall 규약의 기반이 된다. 호출 규약 설계가 점점 스택 → 레지스터 중심으로 최적화되는 흐름을 보여준다.


6-4. __vectorcall : 부동소수점과 SIMD 연산을 위한 호출 최적화


__fastcall은 두 개의 정수, 포인터 인수를 ECX, EDX로 전달해 스택 접근을 줄였지만, 부동소수점, SIMD 데이터에는 전혀 최적화가 없다는 한계가 있었다. x86 SSE, XMM 레지스터를 활용하지 못해 스택을 사용했고, 3D 벡터나 행렬처럼 128bit 이상 자료형은 매 호출마다 메모리를 오가야 했다. 이 병목을 해소하려고 MSVC 2013부터 도입된 규약이 __vectorcall이다. __vectorcall은 XMM, YMM 등 레지스터를 적극 활용하여 부동소수점 및 벡터 인자들을 레지스터에 우선적으로 전달하고, 레지스터 슬롯이 부족한 경우에만 나머지 인자들을 스택에 배치한다. 이 구조 덕분에 특히 수치 계산, 그래픽 처리, 물리 시뮬레이션, 신호처리 등 고성능 연산이 많은 영역에서 매우 큰 성능 개선을 기대할 수 있었다. 이 규약은 __fastcall의 단점을 보완하면서도, SIMD 중심의 현대 CPU 구조에 맞춘 고성능 연산 최적화 모델로 이해할 수 있다.


[이미지36] Common Calling Convention


__stdcall, __cdecl, __fastcal, __vectorcall 성능 비교는 https://kj0on.tistory.com/52 참고


7. Caller-saved & Callee-saved

용어 의미
Caller-saved Register Caller(호출자)가 함수 호출 전 값을 보존해야 하는 레지스터. Callee(피호출자)는 해당 레지스터를 자유롭게 덮어써도 된다.
Callee-saved Register Callee(피호출자)가 함수 종료 시 값을 원래대로 복원해야 하는 레지스터. Caller(호출자)는 해당 레지스터를 자유롭게 덮어써도 된다.

 

[이미지74] Caller Saved

Caller-saved Register는 호출 규약에서 호출자 보존으로 지정된 범주로, 호출자가 함수 호출 전 해당 레지스터의 값을 스택이나 메모리 등에 저장하고 호출 후 복원할 의무를 진다. 따라서 피호출자는 이 레지스터들을 자유롭게 사용, 변경할 수 있으며, 호출자는 필요 데이터가 손상되지 않도록 사전에 보존 작업을 수행한다.

 

[이미지75] Callee Saved

Callee-saved 레지스터는 피호출자 보존으로 지정된 범주로, 피호출 함수가 진입 시 레지스터 값을 저장하고 반환 직전에 복원해야 할 책임을 진다. 이 덕분에 호출자는 해당 레지스터들의 값이 함수 호출 전후로 유지된다는 가정하에 코드를 작성할 수 있으며, 깊은 호출 체계에서도 상위 컨텍스트가 안정적으로 유지된다.


8. 참고 문헌


[1] x86 calling conventions, https://en.wikipedia.org/wiki/X86_calling_conventions
[2] Tutorial 1: The Basics, https://www.plantation-productions.com/Webster/Win32Asm/IczelionTuts/tut1.html
[3] Operating systems C function call conventions and stack, https://piazza.com/class_profile/get_resource/il71xfllx3l16f/ilc7248vusx5h7

[4] What registers am I to push and pop when calling a function?, https://stackoverflow.com/questions/22715001/what-registers-am-i-to-push-and-pop-when-calling-a-function