Calling Convention - 호출 규약

뒬량·2025년 1월 20일

📖 학습 목표

🤙cdecl, SYSV 호출 규약이란?

함수 호출 규약이란?

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

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

왜 배워야 하는가?

함수 호출 규약을 적용하는 것은 일반적으로 컴파일러의 몫이지만,
시스템 해킹을 하는 사람들의 입장에서는 컴파일러를 쓰지 않을 경우도 꽤 존재하기 때문에 (컴파일러의 도움 없이 어셈블리 코드를 작성하려 하거나, 어셈블리로 작성된 코드를 읽고자 할 때) 필수적인 기술이다.

🤙x86 cdecl 호출 규약

  • cdecl 호출 규약은 c/c++ 함수에서 기본적으로 사용되는 호출 규약
  • x86 아키텍처는 레지스터의 수가 적어 스택을 통해 인자 전달
  • 인자를 전달하기 위해 사용한 스택을 호출자가 정리하는 것이 cdecl 특징
  • 스택을 통해 인자 전달 시, 마지막 인자부터 스택에 PUSH

cdecl 호출 규약 실습

  • x86 바이너리에서는 역순으로 push
    위에서 (3,2)로 되어있는 코드가
  1. push 2
  2. push 3
    역순으로 push 되어있는 것을 확인할 수 있다.
  • add esp, 8
    esp와 push는 다른 방향으로 스택이 쌓이고 callee가 종료되면 caller는 call과정에서 사용한 스택을 정리(0x8만큼)

cdecl 호출 규약 예제

💻아래 코드를 컴파일 했을 때, 컴파일된 어셈블리 코드 중 각각 (a), (b), (c), (d)에 들어갈 것을 쓰시오

// Name: callconv_quiz.c
// Compile: gcc -o callconv_quiz callconv_quiz.c -m32
int __attribute__((cdecl)) sum(int a1, int a2, int a3){
	return a1 + a2 + a3;
}
void main(){
	int total = 0;
	total = sum(1, 2, 3);
}

main:
0x080483ed <+0>: push ebp
0x080483ee <+1>: mov ebp,esp
0x080483f0 <+3>: sub esp,0x10
0x080483f3 <+6>: mov DWORD PTR [ebp-0x4],0x0
0x080483fa <+13>: (a)
0x080483fc <+15>: (b)
0x080483fe <+17>: (c)
0x08048400 <+19>: call 0x80483db
0x08048405 <+24>: (d)
0x08048408 <+27>: mov DWORD PTR [ebp-0x4],eax
0x0804840b <+30>: nop
0x0804840c <+31>: leave
0x0804840d <+32>: ret

  • (a) push 0x3
  • (b) push 0x2
  • (c) push 0x1

x86 아키텍쳐이므로, 함수의 인자들을 스택에 거꾸로 push 함
따라서 각각 push 0x3, push 0x2, push 0x1

  • (d) add esp, 0xc

sum 함수의 반환값이 eax에 저장되고 push 되어있던 세 인수를 정리해줘야 하므로 12byte( int 인수 3개 )만큼이 줄어들어야 한다.

cf) x86 아키텍처에서는 push 될 때 0x4만큼의 공간이 할당됨

esp와 push는 다른 방향으로 스택이 쌓이므로, 
push로 12byte를 넣어주었다면 정리할 때는 esp에 12byte를 넣어주면 된다. 
(add esp, 0xc)

🤙x86-64 SYSV 호출 규약

  • 64비트 리눅스에서 사용하는 함수 호출 규약
  • SYSV에서 정의한 함수 호출 규약
    1. 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9에 순서대로 저장하여 전달, 더 많은 인자는 스택을 추가로 이용
    2. 호출자가 인자 전달에 사용된 스택을 정리
    3. 함수의 반환 값은 RAX로 전달

    SYSV 호출 규약 실습

  • 순서대로 저장되어 있음
  1. 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9에 순서대로 저장하여 전달

SYSV 호출 규약 예제

💻SYSV를 적용하여 아래 코드를 컴파일 했을 때, 컴파일된 어셈블리 코드 중 (a), (b), (c) 에 들어갈 것을 쓰시오.

// Name: callconv_quiz2
// Compile: gcc -o callconv_quiz2 callconv_quiz2.c
int sum(int a1, int a2, int a3){
	return a1 + a2 + a3;
}
void main(){
	int total = 0;
	total = sum(1, 2, 3);
}

main:
0x00000000004004f2 <+0>: push rbp
0x00000000004004f3 <+1>: mov rbp,rsp
0x00000000004004f6 <+4>: sub rsp,0x10
0x00000000004004fa <+8>: mov DWORD PTR [rbp-0x4],0x0
0x0000000000400501 <+15>: (a)
0x0000000000400506 <+20>: (b)
0x000000000040050b <+25>: (c)
0x0000000000400510 <+30>: call 0x4004d6
0x0000000000400515 <+35>: mov DWORD PTR [rbp-0x4],eax
0x0000000000400518 <+38>: nop
0x0000000000400519 <+39>: leave
0x000000000040051a <+40>: ret

  • (a) mov edx, 0x3
  • (b) mov esi, 0x2
  • (c) mov edi, 0x1

여기서는 순서 바뀌어도 상관 없음, 실습할 때도 항상 순서가 일정하지 않았기 때문


정리

cdecl에서 정의한 함수 호출 규약

  1. cdecl 호출 규약은 c/c++ 함수에서 기본적으로 사용되는 호출 규약

  2. x86 아키텍처는 레지스터의 수가 적어 스택을 통해 인자 전달

  3. 인자를 전달하기 위해 사용한 스택을 호출자가 정리하는 것이 cdecl 특징

  4. 스택을 통해 인자 전달 시, 마지막 인자부터 스택에 PUSH

    SYSV에서 정의한 함수 호출 규약

  5. 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9에 순서대로 저장하여 전달, 더 많은 인자는 스택을 추가로 이용

  6. 호출자가 인자 전달에 사용된 스택을 정리

  7. 함수의 반환 값은 RAX로 전달(EAX도 가능)


  • 참고

rax나 eax, rdx나 edx의 차이점은 64비트에서는 r-레지스터, 32비트에서는
e-레지스터가 사용된다.

단, e는 64비트에서 사용 가능하지만, r은 32비트에서 사용 불가능
중요한 점은 32비트 레지스터를 64비트에서 사용할 경우 상위 32비트는 자동으로 0으로 설정

x86 (32비트):

오래된 운영체제와 프로그램에서는 32비트 코드가 기본이다. 
여전히 많은 레거시 시스템에서 32비트 코드가 사용된다.

x86-64 (64비트):

최신 운영체제 및 소프트웨어는 64비트 아키텍처를 기본으로 지원한다. 
Windows, Linux, macOS 등 대부분의 최신 운영체제는 64비트를 지원하며, 
많은 현대적인 애플리케이션들이 64비트로 컴파일된다.

참고 URL
[x86, x64(amd64), Arm64] 뜻, 구분방법, CPU 역사, 차이점 알아보기
https://m.blog.naver.com/cjs0308cjs/223242935705
Calling Convention (함수 호출 규약)
https://gkgktkdwl.tistory.com/19

0개의 댓글