Background : Calling Convention

곽무경·2022년 6월 30일
0

System Hacking

목록 보기
9/27

함수 호출 규약

함수의 호출반환에 대한 약속

  • 한 함수에서 다른 함수를 호출하면, 프로그램의 실행 흐름은 다른 함수로 이동
  • 호출한 함수가 반환하면, 다시 원래의 함수로 돌아와서 기존의 실행 흐름 지속
  • 함수를 호출할 때에는
    호출자(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 호출 규약 : cdecl

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
...생략

x86-64 호출 규약 : SYSV

  1. 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9 에 순서대로 저장하여 전달
    더 많은 인자를 사용해야 할 경우 스택을 이용
  2. 호출자에서 인자 전달에 사용된 스택을 정리
  3. 함수의 반환 값은 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, rsprbprsp 로 옮김으로써 새로운 스택 프레임 할당
rsp 의 값을 빼주면 공간을 할당하는 것이지만,
callee 함수는 지역변수를 사용하지 않으므로 공간할당은 하지 않음
callee 함수의 ret 변수는 반환값을 저장하는 용도로만 사용됨
gcc는 이러한 변수를 스택에 할당하지 않고 rax 를 직접 사용함

반환값 전달


callee 함수의 종결부에서 반환값을 rax 로 옮김

반환


스택에 저장해뒀던 callee 호출 다음 명령어 주소 0x5555555551b9를 스택에서 꺼내와서
ret 명령으로 호출자로 복귀

0개의 댓글