함수 호출 규약에는 stdcall, cdecl, fastcall, thiscall, vectorcall, system v 등 다양하게 있지만 본 글에서는 리눅스 gcc 가 사용하는 함수 호출 규약에 대해서만 다루도록 하겠다.
Intro
Caller 와 Callee
Caller 는 호출자로 함수를 호출하는 함수이다. Callee 는 반대로 호출 당하는 피호출자이다.
이걸 먼저 설명하는 이유는 함수 호출규약에 따라서 인자를 어떻게 정리하는지, 어떤 함수가 정리하는지가 다르기 때문이다.
실습 코드는 다음과 같이 작성했다.
# include <stdio.h> void A(int arg1, int arg2){ B(arg1, arg2); } void B(int arg3, int arg4){ printf("%d, %d", arg3, arg4); } void main(){ int a = 1; int b = 2; A(a, b); }
x86 Calling Conention
cdecl
cdecl은 C언어에서 사용하는 표준 C 호출규약이다.
스택 프레임은 이렇게 생성된다.
A 에서 B 에 인자 전달하는 부분에서 멈춰서 메모리를 확인해봤다.
2, 1 순으로 전달됨을 알 수 있다.
A 안의 B함수 인자는 B(arg1, arg2);
다음과 같고 arg1 에 1 arg2 에 2 가 전달 되므로 x86 cdecl 인자 전달 순서는 오른쪽에서 왼쪽으로 전달됨을 알 수 있다.
또한 레지스터를 사용하지 않고 스택을 사용하고 있다.
다음은 B 함수 호출이 종료된 후
인자를 정리하는 부분이다.
이때의 스택 상황은 이렇다.
ni 로 add
명령어를 한 이후에
인자가 정리된 걸 알 수 있고 인자가 정리 되었더라도 스택에 값은 남아 있음을 알 수 있다.
Caller가 인자를 정리하는 방식으로 인자의 개수를 미리 정해두지 않고 사용할 수 있다.
따라서 가변인자를 사용할 수 있다.
printf 는 대표적인 가변인자 함수인데 서식문자의 개수에 따라서 인자의 개수를 정한다.
B에서 printf 호출 시의 argument 인데
%d 를 두개 넣어줬기 때문에 출력은 1, 2 가 된다.
x86_64 Calling Convention
리눅스의 경우 x64 Calling Convention 은 System V 를 사용한다.
System V
x86과의 가장 큰 차이점은 스택 대신 레지스터를 사용한다는 것이다.
레지스터는 RDI, RSI, RDX, RCX, R8, R9 순으로 사용하고 이 이상부터는 스택을 사용한다. (이 외에 XMM 같은 레지스터도 사용하는데 부동소수점인 경우 사용한다.)
또한 함수의 반환은 RAX로 한다.
x86과는 달리 레지스터를 이용해서 인자를 전달하는 것을 볼 수 있다.
또한 B 함수 안에서는 레지스터를 이용해 넘어온 인자를 stack 에 저장하는 것을 볼 수 있다.
해당 위치에 저장 되었다.
또한 printf 에서도 레지스터를 이용해서 인자를 전달한다.
이 글은 옵시디언을 이용해서 작성되었습니다.
'TOOR' 카테고리의 다른 글
[TOOR] 5.2 NXbit (0) | 2023.09.19 |
---|---|
[TOOR] 5.1. BOF (내 안의 버퍼가 넘친다!) (0) | 2023.09.19 |
[TOOR] 4.Handray (0) | 2023.09.19 |
[TOOR] 3.Stack Frame (0) | 2023.09.19 |
[TOOR] 1.Memory Structure (0) | 2023.09.19 |