시스템 해킹/DreamHack으로 해커를 꿈꾸며

[DreamHack] 함수 호출 규약 (Caller, Callee, cdecl, SYSV)

0x6b6569 2023. 2. 18. 21:01

 

함수의 호출 규약은 함수의 호출 및 반환에 대한 약속이다.

한 함수에서 다른 함수를 호출할 때, 프로그램의 실행 흐름은 다른 함수로 이동한다.

그리고 호출한 함수가 반환하면, 다시 원래의 함수로 돌아와서 기존의 흐름을 이어나갑니다.

 

여기서 질문! 어떻게 다시 원래의 함수로 돌아올까요?? 오늘은 이거에 대한 해답을 정리해볼까 합니다.

 

함수를 호출할 때는 반환된 이후를 위해 호출자(Caller)의 상태(Stack frame) 및 반환 주소(Return Address)를 저장해야 합니다. 또한 호출자는 피호출자(Callee)가 요구하는 인자를 전달해 줘야하며 실행이 종료될 때는 반환 값을 전달 받아야 합니다. 

 

함수의 호출 규약을 적용하는 것은 일반적으로 컴파일러의 몫입니다.

프로그래머가 고수준 언어로 코드를 작성한다면 컴파일러가 호출 규약에 맞게 코드를 컴파일 합니다. 호출 규약은 여러가지가 있는데 프로그래머가 호출 규약을 명시하지 않는다면 컴파일러는 지원하는 호출 규약중에서 CPU의 아키텍쳐에 적합한 것을 선택합니다. (출처 드림핵)

 

x86

  • cdecl
  • stdcall
  • fastcall
  • thiscall

x86-64

  • System V AMD64 ABI의 Calling Convention
  • MS ABI의 Calling Convention

컴파일러는 지원하는 호툴 규약 중 CPU 아키텍처에 적합한 것을 선택합니다. 예를 들어 x86(32bit) 아키텍쳐는 레지스터를 통해 피호출자의 인자를 전달하기에는 레지스터의 수가 적으므로, 스택으로 인자를 전달하는 규약을 사용합니다. 반대로 x86-64 아키텍쳐에서는 레지스터가 많으므로 적은 수의 인자는 레지스터만 사용해서 인자를 전달하고, 인자가 너무 많을 때만 스택을 사용합니다! 

 

그리고 CPU의 아키텍쳐가 같아도, 컴파일러가 다르면 적용하는 호출 규약이 다를 수 있어요. C언를 window 에서는 MSVC를 리눅스에서는 GCC를 많이 사용합니다. 이 둘은 아키텍처에 대해서도 다른 호출 규약을 적용합니다. 

 

cdecl

// 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);
}

 c 파일을 만들어서 확인을 해보자 

 

cdecl.s 

caller가  인자로 1,2 를 스택에 push (32bit라서 레지스터가 부족 ㅠㅠ) 하고 callee를 호출합니다.

callee 는 nop 하고 바로 ret을 합니다 (스택에 넣어준 인자를 정리를 안함!! 매우 중요)

 

그리고 caller 에서 add   esp, 8 을 보면 caller가 스택을 정리하는 것을 확인할 수 있습니다.

 

정리, cdecl은 caller가 스택을 정리한다.

 

 

SYSV

// 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(); }