함수 호출 규약이란 함수를 호출할 때 규칙이다. 함수 호출 규약이 무엇인지 알기 전에 알아야하는 용어가 몇가지 있다.
먼저 prologue/epilogue이다. 아래 첫번째 그림에서 볼 수 있듯이 함수 호출전에 이전 프레임 포인터 레지스터(ebp)를 스택에 백업해두고, 프레임 베이스 포인터를 ebp에 저장해두는 것을 부분을 prologue라고 한다. 두번째 그림 처럼 "ebp + offset" 이런식으로 보통 함수에 넘겨준 인자나 return address에 쉽게 접근할 수 있다. 호출된 함수가 끝나면 해당 함수에 대한 스택은 소멸되어야 하므로 esp를 프레임 베이스 포인터로 복귀시키고, ebp는 이전 프레임의 베이스 포인터를 다시 받고 return address로 점프하는 동작을 합쳐서 epilogue라고 한다.
그리고 스택 프레임이란 용어도 알아야 한다. caller와 callee가 있으면 callee의 스택 프레임은 caller가 callee를 위해 stack에 push해주는 인자와 return address를 제외하고 callee함수 내부에서 생긴 지역 변수나, callee가 호출하는 또 다른 제 3의함수를 위한 인자들까지를 의미한다(사실 caller가 스택에 push해주는 callee를 위한 인자나 return address도 callee의 stack으로 포함한다는 글도 몇개 봤는데 두번째 사진은 학부생 수업시간에 쓰던 carnegie mellon university의 lecture자료이니 아마 저게 맞지 않을까 싶다...)
함수 호출 규약의 종류에 따라 인자를 오른쪽에서 왼쪽순으로 전달할건지, 왼쪽에서 오른쪽으로 전달할건지 달라지며, stack을 이용할것인지, stack®ister 둘다 이용할것인지도 달라지며, 함수가 종료되고 stack frame을 누가 정리할 것인지도 달라진다. 함수 호출 규약은 아키텍처마다(ex. x86, ARM 등) 다를 수 있으며, high-level language(ex. C, C++, JAVA 등)마다, 컴파일러마다 달라질 수 있다. 이 글에서 설명하는 함수 호출 규약은 x86 아키텍쳐에서의 함수 호출 규약이다.
쉽게 그림으로 표현하면 아래와 같다.
1. __stdcall
먼저 __stdcall방식부터 살펴보자 . __stdcall은 WIN32 API에서 주로 사용된다. WIN32 API에서는 가변 인자 함수가 없기 때문에 인자 개수가 고정이다. 인자를 스택으로 전달하고 인자를 오른쪽에서 왼쪽으로 전달한다. 그리고 함수 종료후 스택 정리는 callee가 한다고 한다. 어셈블리를 통해 직접 살펴보자(어셈블리 출처는 https://blog.hexabrain.net/254)
우선 인자를 왼쪽 인자인 4부터 스택을 이용해서 전달하고 있는 것을 main 함수 어셈블리 코드를 통해 확인할 수 있다. 그리고 sum 함수의 어셈블리 코드에 'RETN 8'라는 코드가 보인다. RETN 8의 의미는 스택에 저장된 return address로 return하면서 esp를 8만큼 더하라는 의미이다. 그 말은 callee가 리턴과 동시에 스택에 2개의 인자를 정리한다는 것이다.
2. __cdecl
__cdecl는 c declaration의 약자다. 위키피디아에 찾아보니 다음과 같은 의미가 있다.
The cdecl (which stands for C declaration) is a calling convention that originates from Microsoft's compiler for the C programming language and is used by many C compilers for the x86 architecture
해석하면 그냥 x86 아키텍쳐의 C 컴파일러에서 사용되는 calling convention이라는 의미이다. __stdcall과 마찬가지로 스택을 통해 인자를 전달하고 오른쪽에서 왼쪽으로 인자를 전달한다. 하지만 다른점은 caller가 스택을 정리해준다. 진짜 그런지 위의 소스 코드에서 __stdcall을 지우고 컴파일하고 확인해보자.
정말 main에서 ESP에 8을 더해주면서 caller가 스택을 정리해주는 것을 확인할 수 있다.
3. __fastcall
이름에서부터 알 수 있듯이, 함수 호출이 다른 호출 규약들보다 빠르다. 그 이유는 스택보다 접근이 빠른 레지스터를 이용하기 때문이다.
어셈블리 코드에서 볼 수 있듯이, edx, ecx 레지스터를 이용해서 오른쪽 인자부터 넘겨주고 있는 것을 확인할 수 있다. 인자가 3개이상인 경우부터는 2개만 edx, ecx 레지스터를 이용하고 나머지는 스택을 이용해서 전달하고 할당된 스택은 callee가 정리한다.
Reference
https://blog.hexabrain.net/254
'공부 > C || C++' 카테고리의 다른 글
C++ static variable, static member variable, static member function (0) | 2021.07.23 |
---|---|
C++ 연산자 오버로딩(Operator overloading) (0) | 2021.07.22 |
C++ iterator의 종류(input, output, forward, bidirectional, random access iterator) (0) | 2021.07.19 |
네임스페이스(namespace)란? std란? (0) | 2021.07.19 |
C++ 대입 연산자는 왜 void로 선언하면 안될까? 반환 타입에 왜 참조자를 붙여야 할까? (0) | 2021.07.18 |