티스토리 뷰
/* 본 포스팅은 최호성 저자의 '독하게 시작하는 C프로그래밍' 교재를 참조로 작성되었음을 먼저 알려드립니다. */
함수 호출 규칙(calling convention)은 호출자 함수가 피호출자 함수를 호출하는 과정에서 매개변수를 전달하는 순서 및 매개변수가 사용한 메모리 관리방법 등에 관한 규칙이다. 대표적으로 __cdecl, __stdcall, __fastcall 등 세 가지 정도가 있는데 이 세 가지가 C 언어의 표준에서 정의하는 것은 아니다. 모두 약간씩 차이가 있는데 C/C++ 컴파일러의 기본 함수 호출 규칙은 __cdecl이다.
우리가 자동변수를 선언할 때 auto를 생략해도 되는 것처럼 __cdecl도 생략할 수 있다. 따라서 아무것도 기술하지 않으면 함수 호출 규칙은 __cdecl이다. 제대로 표시하자면 함수의 반환 자료형과 이름 사이에 아래와 같은 호출 규칙이 명시되어야 한다.
#include <stdio.h> int __cdecl main(void) { printf("Hello world!\n"); return 0; }
세 함수 호출 규약의 특징을 요약하면 아래의 표와 같다.
호출 규칙 |
매개변수 스택 정리 |
매개변수 메모리 |
__cdecl |
Caller |
Stack |
__stdcall |
Callee |
Stack |
__fastcall |
Callee |
Stack + Register |
# __cdecl
프로젝트의 속성에서 '[구성 속성] -> [C/C++] -> [고급]'의 '호출 규칙' 항목을 보면 기본 설정이 __cdecl로 되어 있다. 따라서 별도로 명시하지 않았을 때 모든 함수 호출 규칙은 __cdecl인 것이다.
__cdecl 호출 규칙은 매개변수를 오른쪽부터 스택에 Push한다. 그리고 매개변수로 인해 증가한 스택을 호출자 함수가 본래 크기로 줄인다. 자동변수는 모두 스택 영역 메모리를 사용한다. 메모리가 자동으로 관리된다는 말은 결국 사용되는 스택의 영역이 늘거나 줄어드는 것에 불과하다.
힙 영역 메모리처럼 운영체제에 반환되는 형태로 관리되는 것이 아니다. 아래 코드의 gMax( )함수는 int형 매개변수 세 개를 받아 그 중 최대값을 반환해주는 함수다. 따라서 매개변수로 인해 증가하는 스택의 크기는 12바이트이다. // 4바이트 int 변수 * 3 = 12바이트
#include <stdio.h> int __cdecl gMax(int a, int b, int c) { int nMax = a; if (b > nMax) nMax = b; if (c > nMax) nMax = c; return nMax; } int __cdecl main(void) { int nResult = 0; nResult = gMax(1, 2, 3); return 0; }
위의 코드를 작성하여 디스어셈블리 화면을 확인하면 아래와 같은 내용을 확인할 수 있는데, 잘 보면 gMax( )함수를 호출하는 부분에서 오른쪽 인수 3부터 스택에 Push하는 것을 확인할 수 있다.
int nResult = 0; 0003171E mov dword ptr [nResult],0 nResult = gMax(1, 2, 3); 00031725 push 3 00031727 push 2 00031729 push 1 0003172B call _gMax (03126Ch) 00031730 add esp,0Ch 00031733 mov dword ptr [nResult],eax return 0; 00031736 xor eax,eax
그리고 call 연산으로 gMax( )함수를 호출한 후 이 함수가 반환하면 main( )함수 내부에서는 add esp, 0Ch라는 연산을 수행한다. 여기서 '0Ch'는 0x0C 즉, 10진수 12를 의미하며, esp(extended stack pointer)는 스택 메모리에 대한 '포인터'이다. 포인터에 대해 ADD 연산을 수행하므로 주소값이 증가한다. // 스택의 주소가 증가하는 건 스택이 감소한다는 걸 뜻한다. 상식적으로 생각할 때 이해가 안될 수 있지만 실제로 스택은 쌓일 때마다 주소가 감소한다. 먼저 한계 선을 긋고 그 안에서 왔다갔다 하는 개념이기 때문에 이렇게 설계가 되었다고 생각하면 된다.
위의 코드에서는 12바이트(4바이트 int형 변수 3개)만큼 증가한다. 주소가 증가했다는 건 스택의 감소를 의미한다. 즉, main( )함수에 들어가는 이 한줄의 코드로 스택은 gMax( )함수호출 전 상태로 복원되는 것이다. 이러한 점 때문에 자동변수 메모리는 자동으로 관리된다는 말을 할 수 있는 것이다.
# __stdcall
__stdcall 호출 규칙 또한 __cdecl 호출 규칙처럼 매개변수를 오른쪽부터 스택에 Push 한다. 그러나 매개변수로 인해 증가한 스택을 호출자 함수가 정리하는 것이 아니라 피호출자 함수가 정리한다. // 피호출자 함수를 어렵게 생각할 것 없이 불려지는 함수라고 생각하면 된다. A called by B 라는 문장이 있다면 A가 피호출자 함수가 된다.
이 역시도 어셈블리를 살펴봄으로써 확인할 수 있는데, 아래의 코드의 어셈블리는 좀 복잡한 관계로 호출자 함수의 어셈블리에서 스택을 정리하는 코드가 사라졌다는 정도만 확인하려 한다.
#include <stdio.h&g; int __stdcall gMax(int a, int b, int c) { int nMax = a; if (b > nMax) nMax = b; if (c > nMax) nMax = c; return nMax; } int __cdecl main(void) { int nResult = 0; nResult = gMax(1, 2, 3); return 0; }
main( ) 함수는 __cdecl 호출 규칙을 사용하지만, gMax( ) 함수는 __stdcall 호출 규칙을 사용하도록 명시했다. 따라서 main( )함수가 gMax( )를 호출하면서 증가한 12바이트의 스택 메모리는 피호출자 함수인 gMax( )에 의해 정리될 것이다. 그러므로 앞서 살펴봤던 add esp, 0Ch 는 main( ) 함수의 어셈블리에 존재하지 않을 것이다.
int nResult = 0; 00F7171E mov dword ptr [nResult],0 nResult = gMax(1, 2, 3); 00F71725 push 3 00F71727 push 2 00F71729 push 1 00F7172B call _gMax@12 (0F71073h) 00F71730 mov dword ptr [nResult],eax return 0; 00F71733 xor eax,eax }
gMax( ) 함수를 호출한 이후로 mov dword ptr [nResult], eax 라는 연산이 수행되었는데 이는 gMax( ) 함수가 반환한 값을 nResult에 대입하는 C 코드에 대한 어셈블리 코드다. call 연산과 mov 연산 사이에 스택을 정리하는 코드가 더는 보이지 않는다. gMax( ) 함수 내부에서 스택을 정리하기 때문이다.
# __fastcall
__fastcall은 __stdcall 처럼 매개변수는 오른쪽부터 스택에 Push하고 피호출자 함수가 스택을 정리한다. 단, 매개변수가 여러 개면 가장 나중에 Push 되어야 할 왼쪽 첫 번째, 두 번째 매개변수는 스택 대신 CPU의 레지스터(EDX, ECX)를 이용해 전달한다. 따라서 매개변수가 메모리에 복사되는 횟수를 줄이고, 이 효과로 연산속도가 일부 향상될 수 있다. // 레지스터에 대해선 OS 2일차 게시물을 살펴보도록 하자.
#includeint __fastcall gMax(int a, int b, int c) { int nMax = a; if (b > nMax) nMax = b; if (c > nMax) nMax = c; return nMax; } int __cdecl main(void) { int nResult = 0; nResult = gMax(1, 2, 3); return 0; }
int nResult = 0; 00B6171E mov dword ptr [nResult],0 nResult = gMax(1, 2, 3); 00B61725 push 3 00B61727 mov edx,2 00B6172C mov ecx,1 00B61731 call @gMax@12 (0B6134Dh) 00B61736 mov dword ptr [nResult],eax return 0; 00B61739 xor eax,eax }
위 예제의 디스어셈블리 (아래 코드)를 보면 왼쪽 첫 번째, 두 번째 인수인 1과 2는 각각 ECX, EDX 레지스터로 전달되고 오른쪽 첫 번째인 3은 스택에 Push 되었다. 그리고 __stdcall과 마찬가지로 호출자 함수에서 스택을 정리하는 코드도 보이지 않는다.
/* C++에는 이 세 가지 외에 '__thiscall'이란 함수 호출 규칙이 하나 더 있다. 이 호출 규칙은 객체의 멤버함수(method)를 호출하는 것에 관련된 호출 규칙이다. 존재한다는 사실만 일단 알아두자. */
/* 또한 우리가 이렇게 일일이 호출 규칙을 정해줄 일은 거의 없다고 봐도 된다. 컴파일러가 알아서 최적화를 시켜주기 때문이다. */
'Develop Story > C' 카테고리의 다른 글
<함수 포인터와 역호출 구조> #qsort (0) | 2017.08.25 |
---|---|
<함수에 대한 고급 이론> __inline 인라인 함수 (0) | 2017.08.18 |
<변수와 상수 고급 이론> 형한정어 Volatile (0) | 2017.08.18 |
<변수와 상수 고급 이론> 형한정어 Const (0) | 2017.08.18 |
<피보나치 수열>: #순환 버전 #반복 버전 (0) | 2017.07.25 |
- Total
- Today
- Yesterday