함수의 호출 및 반환에 대한 약속
- 한 함수에서 다른 함수를 호출하면, 프로그램의 실행 흐름은 다른 함수로 이동
- 호출한 함수가 반환하면, 다시 원래의 함수로 돌아와서 기존의 실행 흐름 지속
- 함수를 호출할 때에는
호출자(Caller)의 상태(Stack Frame), 반환 주소(Return Address)를 저장해야 함- 호출자는 피호출자(Callee)가 요구하는 인자를 전달해야 하며
피호출자의 실행이 종료될 때는 반환 값을 전달받아야 함
- x86-64에서는 레지스터를 이용해 인자를 전달,
x86에서는 레지스터의 수가 적으므로 스택을 이용해 인자를 전달- C언어를 컴파일 할 때
윈도우에서는 MSVC를, 리눅스에서는 gcc를 주로 사용- x86 → cdecl, stdcall, fastcall, thiscall
x86-64 → System V AMD64 ABI의 Calling Convention, MS ABI의 Calling Convention
x86에서는 레지스터의 수가 적으므로, 스택을 이용해 인자를 전달
인자 전달을 위해 사용한 스택을 호출자가 정리하는 특징이 있음
마지막 인자부터 첫 번째 인자까지 거꾸로 스택에 push
예제 코드
// Name: cdecl.c // Compile: gcc -fno-asynchronous-unwind-tables -nostdlib -masm=intel \ // -fomit-frame-pointer -S cdecl.c -w -m32 -fno-pic -O0 void __attribute__((cdecl)) callee(int a1, int a2){ // cdecl로 호출 } void caller(){ callee(1, 2); }
어셈블리어로 컴파일
$gcc -fno-asynchronous-unwind-tables -nostdlib -masm=intel -fomit-frame-pointer -S cdecl.c -w -m32 -fno-pic -O0
결과
.file "cdecl.c" .intel_syntax noprefix .text .globl callee .type callee, @function callee: ; 피호출자 endbr32 nop ; nop : 아무일도 하지 않음(1바이트의 크기만 차지) ret ; 스택을 정리하지 않고 리턴 .size callee, .-callee .globl caller .type caller, @function caller: ; 호출자(호출자가 스택을 정리) endbr32 push 2 ; 2를 스택에 저장 push 1 ; 1을 스택에 저장(역순) call callee add esp, 8 ; 스택을 정리(push를 2번 했으므로 8byte만큼 esp가 증가) nop → 32bit이므로 1번에 4byte씩, 총 8byte ret .size caller, .-caller .ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0" .section .note.GNU-stack,"",@progbits ...생략
- 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9 에 순서대로 저장하여 전달
더 많은 인자를 사용해야 할 경우 스택을 이용- 호출자에서 인자 전달에 사용된 스택을 정리
- 함수의 반환 값은 RAX 로 전달
예제 코드
// Name: sysv.c // Compile: gcc -fno-asynchronous-unwind-tables -masm=intel \ // -fno-omit-frame-pointer -S sysv.c -fno-pic -O0 #define ull unsigned long long ull callee(ull a1, int a2, int a3, int a4, int a5, int a6, int a7) { ull ret = a1 + a2 + a3 + a4 + a5 + a6 + a7; return ret; } void caller() { callee(123456789123456789, 2, 3, 4, 5, 6, 7); } int main() { caller(); }
caller 함수 진입
7번째 인자인 7은 스택으로 push
나머지 6개의 인자들은 각각 rdi, rsi, rdx, rcx, r8, r9 레지스터에 대입 후 callee 함수 호출callee 함수 호출 직전 레지스터들의 상태 (각각 대입된 모습)
callee 함수 진입
스택에 저장되어있는 0x5555555551b9 → 반환 주소
caller 함수의 callee함수 호출(call) 다음 명령어의 주소
callee에서 반환되면 이 주소를 꺼내어 원래의 흐름으로 돌아갈 수 있다.
스택 프레임 저장 및 할당
push rbp → 기존의 rbp (스택 프레임의 가장 낮은 주소, Stack Frame Pointer SFP) 저장
mov rbp, rsp → rbp 를 rsp 로 옮김으로써 새로운 스택 프레임 할당
rsp 의 값을 빼주면 공간을 할당하는 것이지만,
callee 함수는 지역변수를 사용하지 않으므로 공간할당은 하지 않음
callee 함수의 ret 변수는 반환값을 저장하는 용도로만 사용됨
gcc는 이러한 변수를 스택에 할당하지 않고 rax 를 직접 사용함
반환값 전달
callee 함수의 종결부에서 반환값을 rax 로 옮김
반환
스택에 저장해뒀던 callee 호출 다음 명령어 주소 0x5555555551b9를 스택에서 꺼내와서
ret 명령으로 호출자로 복귀