함수 호출 규약(Calling Convention)
- 함수를 호출하는 방식에 대한 약속.
1) 인자 전달 방법
2) 인자 전달 순서
3) 스택 프레임 정리 방법
스택 프레임(Stack Frame)
- 함수를 호출할때 상위에서 진행되던 함수를 저장하고, 인자를 전달하기 위해 스택 프레임의 구조를 사용한다.
main 함수가 실행되는 과정에서 A 함수가 호출된다면, A함수에 대한 인자를 약속된 순서대로 스택 프레임에 저장한다.
그 다음 A 함수가 끝나고 되돌아 올 주소(RET)와 A 함수가 실행되기 전의 EBP의 값(SFP)을 스택에 push한다.
이후 A 함수가 끝나면, A 함수의 스택 프레임은 정리되고 RET와 SFP 값을 통해 main 함수의 스택 프레임으로 돌아오게 된다.
x86 아키텍처에 사용되는 calling convention
- 여러가지 호출 규약들이 있지만, 대표적으로 cdecl, stdcall, fastcall에 대해서 알아본다. 각각의 호출 규약에 따라 인자 전달 매체나 스택 프레임 정리 방법이 다르다.
1) cdecl
호출자가 스택을 정리하고, 가변인자를 사용하여 함수를 호출한다.
예제 코드는 main 함수에서 func1를 수행한 뒤, func2를 수행한다.
func1 함수가 수행되는 부분을 어셈블리어로 확인해보면 인자로 사용된 1과 2를 뒤에서부터 스택에 push해주는 것을 확인 할 수 있다.
이후 func1 함수를 호출하고, 함수가 끝난 후 main 함수로 돌아와서 add를 통해 func1의 스택 프레임을 정리한다. 이 부분에서 func1 함수(callee)를 호출한 main 함수(caller)가 스택 프레임을 정리하는 것을 알 수 있다.
* func1과 func2의 선언 부분을 보면 func2에만 __cdecl을 붙여서 cdecl을 사용하겠다고 선언했다. 하지만 어셈블리어로 확인했을 때 두 스택 프레임의 흐름은 같다. calling convention을 지정하지 않아도 자동으로 cdecl를 사용해서 스택 프레임을 관리하고 있음을 알 수 있다.
2) stdcall
호출 당한 callee가 스택 프레임을 정리하고, 가변 인자를 사용할 수 없다.
앞서 설명한 cdecl과 같은 코드지만 func1의 선언 부분에 __stdcall 이라고 선언 되어있다.
이를 어셈블리어로 확인해보면 인자를 뒤에서부터 push 한 후, func1 함수를 호출한다.
func1의 함수의 마지막에서 ret 8 을 통해 인자를 정리해주며 main 함수로 돌아가게 된다.
3) fastcall
호출 당한 함수가 스택 프레임을 정리하고, 다른 호출 규약과는 다르게 레지스터를 사용해서 인자를 전달하기 때문에 속도가 빠르다. ECX와 EDX를 인자전달에 먼저 사용하고 인자가 3개 이상이 될 경우에 스택을 활용한다.
이 예제 코드에서는 ECX, EDX, 그리고 스택을 활용하는 것을 확인하기 위해 func1의 함수의 인자는 4개를 받는다.
인자의 전달 순서는 동일하게 오른쪽에서 왼쪽 순서로 인자를 전달한다. 가장 먼저 전달 될 인자는 4이며, 이는 스택에 push되는 것을 확인할 수 있다. 인자 전달은 오른쪽에서 왼쪽으로 하지만, ECX와 EDX 레지스터를 사용하게 되는 순서는 왼쪽부터인 것을 알 수 있다. 4와 3을 스택에 push한 후, 2는 EDX에, 1은 ECX에 값을 복사한다.
그리고 앞서 stdcall과 같이 호출당한 함수(callee)에서 인자를 정리하는 것을 확인 할 수 있다. => ret 8
x64 아키텍처에 사용되는 calling convention
- x64 아키텍처에서는 fastcall을 사용하며, 더 많은 레지스터를 사용해서 인자를 전달한다. windows와 linux의 사용되는 레지스터와 개수가 다르다.
1) linux (fastcall)
x86의 fastcall과 다를 것 없이 전달 순서와 스택 프레임 정리 방법은 같다. 하지만 인자 전달에 있어서 정수와 실수에 따라 사용되는 레지스터가 다르다. 리눅스 경우에는 정수가 7개 이상인 경우 스택을 이용하고, 실수는 9개 이상인 경우에 스택을 이용한다.
정수의 인자 전달의 순서는 오른쪽부터이고, 모든 값을 전달 했을 때 레지스터의 값은 RDI(1), RSI(2), RDX(3), RCX(4), R8, R9이 된다.
2) windows (fastcall)
windows인 경우에는 인자가 정수이던 실수이던 5개 이상인 경우에는 스택을 이용한다.
linux인 경우에는 RDI와 RSI 레지스터를 사용했지만, windows에서는 사용하지 않는다.
예제 코드의 func1의 함수가 실행될 때 인자의 값은 RCX(1), RDX(2), R8(3), R9(4)가 된다.