호출 규약(Calling Convention)

호출 규약(Calling Convention)

=> 호출 규약은 함수를 호출하는 방식에 대한 약속이다. 인수는 어떻게 전달하고 리턴값은 어떻게 반환하며 함수 호출을 위해 사용한 메모리는 누가 정리할 것인가를 규정한다.

=> 사실 호출규약은 컴파일러 내부의 일이라 함수를 사용하기만 한다면 몰라도 큰 지장이 없다.

=> 하지만 이 과정을 이해하면 문법에 대한 자신감이 생기고 민감한 에러 상황에도 잘 대처할 수 있으며 언어간의 혼합 개발도 가능해진다.

1.스택

스택 동작 방식

스택의 현재 위치

스택의 사용 사례

    push ecx      ; ecx를 스택에 저장 
    push eax      ; eax를 스택에 저장
    eax, ecx 레지스터 사용 
    pop eax      ; eax 복구 (LIFO이므로 eax 먼저 pop한다.)
    pop ecx      ; ecx 복구

2.스택 프레임

<예제> ``` # include int Add(int a, int b){ int c,d,e; c = a+b; return c; } int main(){ int result; result = Add(1,2); printf("result = %d\n", result); return 0; } ``` - 위 코드의 Add 함수는 두 수의 합을 구해 리턴한다. Add()는 매개변수로 a,b , 지역변수로 c,d,e를 가지고 있고 c를 리턴한다. main 함수에서 Add(1,2);를 호출하는 어셈블리 코드는 아래와 같다. ``` push 2 push 1 call Add add esp,8 result = eax ``` - Add 함수로 전달할 인수를 스택에 푸시한다. 뒤쪽 인수부터 순서대로 푸시한 후 call 명령으로 Add 함수를 호출한다. - call 명령은 **복귀할 번지를 푸시한 후 함수의 번지로 점프하는 것**이다. (push main의 다음 번지(+4), jump Add) - 스택에는 인수 2,1와 복귀 번지가 저장되어 있다. Add 함수의 어셈블리 코드는 다음과 같다. **[전체 코드]** ``` push ebp mov ebp,esp sub esp,0ch mov eax,[ebp+8] add eax,[ebp+0ch] mov [ebp-4], eax mov eax, [ebp-4] mov esp, ebp pop ebp ret ``` 이 중에서 **접두(prolog) 코드** ``` push ebp mov ebp,esp sub esp,0ch ``` - Add 함수는 호출원에서 사용하던 ebp를 스택에 푸쉬한 후(push ebp), ebp에 스택의 현재 위치를 대입한다(mov ebp,esp). ebp는 인수와 지역변수를 엑세스하기 위한 기준 번지(Base Pointer)로 사용되며 함수 본체에서는 계속 유지된다. Add를 호출한 함수(위의 경우 main함수)도 ebp를 기준 번지로 사용하므로 Add는 자신의 ebp를 설정하기 전에 호출원(Caller)의 ebp를 저장해야 한다. - 다음으로 esp를 0ch(십진수로 12)만큼 감소시켜 정수형의 지역변수 3개를 저장할 공간을 만든다. (sub esp, 0ch) 별도의 초기화는 하지않는다. - 여기까지의 코드를 **접두(prolog)**라고 하며 **함수의 실행을 준비하는 과정**이다. ### **지금까지의 스택 모양** - ebp는 자신이 저장된 위치를 가리키고 esp는 지역변수 영역의 상단을 가리킨다. esp는 필요에 따라 오르락 내리락 거리지만 ebp는 계속 기준 번지를 가리킨다. **ebp를 기준으로 아래쪽에는 인수가 있고 위쪽에는 지역변수가 있어 ebp에 대한 상대적인 오프셋으로 참조한다.** - ebp가 가리키는 곳 바로 아래 (ebp+4)에 **복귀 번지**가 저장되어 있으며 그 아래(ebp+8)에 첫 번째 인수 a가 있다. 본체는 [ebp+8] 표현식으로 a를 액세스하고 두 번째 이후의 인수도 **오프셋**만 바꾸면 된다. ebp 바로 위(ebp-4)에는 첫 번째 지역변수인 c가 있고 **n번째 지역변수는 ebp-n*4**에 있다. 4바이트보다 더 큰 변수는 두 칸 이상을 차지한다. - 실행 준비가 완료되면 본체 코드가 실행된다. **본체 코드** ``` mov eax,[ebp+8] ;eax = a add eax,[ebp+0ch] ;eax = eax + b mov [ebp-4], eax ;c = eax mov eax, [ebp-4] ;return c ``` - 기계어는 메모리간의 덧셈을 직접 지원하지 않기 때문에 레지스터로 값을 읽은 후 레지스터와 연산한다. - 먼저 eax에 ebp+8에 있는 a를 읽는다. 여기에 ebp+0ch에 있는 b의 값을 더해 ebp-4에 있는 지역변수 c에 대입한다. 연산 결과는 리턴값을 저장하는 eax에 대입된다. - 실행을 마친 후 남은 정리 작업을 수행하는 접미 코드가 수행된다. **접미 코드(epilog)** ``` mov esp, ebp pop ebp ret ``` - esp에 ebp를 대입하여 지역변수에 할당한 스택을 회수한다. 지역변수의 생명은 이 시점에서 끝난다. ebp를 팝하면 호출원의 원래 ebp가 복구되며 ret 명령은 복귀 번지를 꺼내 리턴한다. - 복귀 번지는 main 함수의 Add() 호출문 바로 다음이다. (+4) 여기까지가 Add(2,1);의 스택 프레임이다. --- **남은 main 함수에서의 코드** ``` add esp, 8 result = eax ``` - esp에 8을 더해 2개의 인수 전달에 사용한 메모리를 회수한다. 인수 전달 영역까지 해제하면 스택은 함수가 호출되기 전의 상태로 깜쪽같이 복구되며 esp, ebp 등의 주요 레지스터도 복구된다. - 마지막으로 eax에 저장되어 있는 리턴값을 result에 대입한다. ## 3. C언어 ### 32bit - C언어의 기본(default) 호출 규약은 __cdecl이다. | 호출 규약 | 인수 전달 | 스택 정리 | |-----------|------------|-----------| | __cdecl | 오른쪽 먼저| Caller | | __stdcall | 오른쪽 먼저| Callee | | __fastcall| ECX,EDX에 우선 전달.나머지는 오른쪽 먼저 | Callee | ## 1. __cdecl 위의 스택 프레임의 Add 함수를 호출할때의 방식이 __cdecl이다. - 인자의 스택정리를 호출원(Caller)가 담당한다. 호출원(Caller) 의 어셈블리 코드 ``` call Add add esp,8 ``` - 함수(Add)를 호출한 이후로 보낸 인자(2와 1)에 대하여 호출원이 스택을 정리하는 코드가 있다.(add esp,8) - 이처럼 __cdecl의 방식은 호출원이 함수를 호출한 뒤에 인자스택을 정리해야 한다. 함수(Callee)의 어셈블리 코드 ``` ret ``` - 호출원이 인자 스택을 정리하므로 ret은 부가 기능없이 복귀번지를 꺼내 리턴해주기만 하면 된다. ## 2. __stdcall - 이 방식은 인자 스택을 함수(Callee)가 정리하는 방식이다. 호출원(Caller) 의 어셈블리 코드 ``` call Add ``` - 인자를 함수(Callee)가 정리하므로 호출원의 코드는 인자 스택에 관한 코드가 있을 필요가 없다. 따라서 함수를 호출하는 call Add 코드만 있다. 함수(Callee)의 어셈블리 코드 ``` ret 8 ``` - 복귀코드가 ret에서 ret 8로 변경되었다. - ret 8의 의미는 복귀번지를 꺼내 리턴하면서 동시에 esp를 8만큼 증가시킨다는 의미이다. esp를 8만큼 증가시킨다는 것은 인자 2와 1에 대해서 스택을 정리한다는 뜻이다. - 누가 정리하든 스택은 항상 호출전의 상태로 복귀되기 때문에 결과 코드에 큰 영향을 주지 않는다. 다만, 실행속도는 __cdecl과 __stdcall이 거의 같지만 프로그램 크기는 **__stdcall이 약간 더 작다.** **__cdecl은 함수를 호출할 때 마다 정리 코드가 작성되는 데 비해(add esp,8) __stdcall은 함수 자체에 정리 코드가 딱 한 번만 작성되기 때문이다. (ret → ret 8)** ## 3. __fastcall - __fastcall은 인수 전달을 위해 edx, ecx 레지스터를 사용한다. 인수가 더 많으면 세 번째 인수부터는 __cdecl과 마찬가지로 스택에 밀어넣는다. 레지스터를 우선적으로 사용하여 인수 전달 속도가 빠르다. 함수의 코드는 다음처럼 작성된다. ``` push ebp mov ebp,esp sub esp,14h mov [ebp-8],edx ; 첫 번째 인수를 지역변수로 mov [ebp-4],ecx ; 두 번째 인수를 지역변수로 mov eax,[ebp-4] add eax,[ebp-8] mov [ebp-0ch],eax ; c는 세 번째 지역변수가 된다. mov eax,[ebp-0ch] mov esp,ebp pop ebp ret ``` - edx, ecx 레지스터를 통해 전달받은 인수를 지역변수 영역에 복사하는데 어차피 인수도 지역변수의 일종이므로 이렇게 해도 별 상관이 없다. (비주얼 C++은 fastcall을 형식적으로만 지원할 뿐 fastcall의 장점을 취하지 못하는데 컴파일러 구현상 ecx, edx 레지스터가 꼭 필요하기 때문이다.) - 레지스터는 메모리의 스택보다 빨라 __fastcall은 호출 속도가 빠르다. 대신 모든 CPU에 여유 레지스터가 항상 존재하는 것이 아니어서 이식성에는 불리하다. (그래서 윈도우즈 API는 이 호출 규약을 지원하지만 사용하지는 않는다. 볼랜드의 델파이가__fastcall을 사용한다.) - 스택의 ebp - 4 에는 첫번째 인수 2가, ebp - 8에는 두번째 인수 1이, ebp - 0ch(12) 에는 c 가 그다음 ebp - 16 은 d, ebp - 20 에는 e 가 자리잡고 있다. 모두 지역변수이다. ### 64bit - 64bit는 레지스터의 개수가 많아 부담없이 레지스터를 사용할 수 있게 되었다. - 따라서 모든 인자의 전달을 레지스터를 이용해서 전달한다. ### 관련 링크 - 함수 호출 규약 [http://www.soen.kr/book/ccpp/annex/c2.htm](http://www.soen.kr/book/ccpp/annex/c2.htm) - 범용 레지스터(EAX,ECX,EDX,ESI,ESP,EBP) [https://exynoa.tistory.com/m/186](https://exynoa.tistory.com/m/186)