본문 바로가기
게임해킹/리버싱 핵심원리(나뭇잎책)

10장 함수 호출 규약

by HHack 2024. 12. 4.
반응형

10.1 함수 호출 규약

함수 호출 규약(Calling Convention) : "함수를 호출할 때 파라미터를 어떤 식으로 전달하는가?"에 대한 규약

스택이란 프로세스에서 정의된 메모리 공간이며 PE헤더에 그 크기가 명시되어 있습니다. 즉, 프로세스가 실행될 때 스택 메모리의 크기가 결정됩니다.(malloc, new 같은 동적 메모리 할당과 다릅니다.)

 

Q1 : 함수가 실행 완료되었을 때 스택에 들어있떤 파라미터는 어떻게 해야 될까요?

A1 : 그대로 놔둡니다.

   스택에 저장된 값은 임시로 사용하는 값이기 때문에 더이상 사용하지 않는다고 하더라도 값을 지우거나 하면 불필요하게 CPU 자원을 소모합니다. 어차피 다음에 스택에 다른 값을 입력할 때 저절로 덮어 쓰는데다가 스택 메모리는 이미 고정되어 있기 때문에 메모리를 해제 할 수도 없고 할 필요도 없습니다.

 

Q2 : 함수가 실행 완료되었을 때 ESP(스택 포인터)는 어떻게 되나요?

A2 : ESP 값은 함수 호출 전으로 복원되어야 합니다. 그래야 참조 가능한 스택의 크기가 줄어들지 않습니다.

  스택  메모리는 고정되어 있고 ESP로 스택의 현재 위치를 카리키는데, 만약 ESP가 스택의 끝을 가리킨다면 더이상 스택을 사용할 수 없습니다. 함수 호출 후에 ESP를 어떻게 정리하는지에 대한 약속이 바로 함수 호출 규약입니다.

 

※ 용어 설명

Caller(호출자) : 함수를 호출한 쪽

Callee(피호출자) : 호출을 당한 함수

EX) main() 함수에서 printf() 함수를 호출했다면 Caller는 main()이고, Callee는 printf()입니다.

10.1.1 cdecl (C Declaration)

cdecl 방식은 주로 C언어에서 사용되는 방식이며, Caller에서 스택을 정리하는 특징을 가지고 있습니다.

#include "stdio.h"

int add(int a, int b) {
	return (a + b);
}

int main() {
	return add(1, 2);
}

그림 10.1 cdecl.exe 예제 파일

  1. 401013 ~ 40101C : add() 함수의 파라미터 1, 2를 역순으로 스택에 입력하고 add() 함수(401000)을 호출 한 후 ADD ESP, 8 명령으로 스택을 정리하고 있습니다.

이와같이 Caller인 main() 함수가 자신이 스택에 입력한 함수 파라미터를 직접 정리하는 방식이 cdecl 입니다.

cdecl 방식의 장점은 C언어의 printf() 함수과 같이 가변 길이 파라미터를 전달할 수 있다는 것입니다. 또한 호출자가 스택을 정리하기 때문에 함수가 매개변수의 개수를 알 필요가 없습니다.

 

단점은 호출자가 스택을 정리를 담당하기 때문에 호출마다 오버헤드가 발생합니다.

10.1.2 stdcall (Standard Call)

stdcall은 Windows API에서 기본적으로 사용되는 호출 규약입니다. Callee에서 스택을 정리하는 것이 특징입니다. stdcall 방식으로 컴파일 하고 싶을 때는 '_stdcall' 키워드를 붙여주면 됩니다.

#include "stdio.h"

int _stdcall add(int a, int b) {
	return a + b;
}

int main() {
	return add(1, 2);
}

그림 10.2 stdcall.exe 예제 파일

스택의 정리는 add() 함수 마지막(40100A)의 RET 8 명령에서 수행됩니다. 이 의미는 리턴 후 지정된 크기만큼 ESP를 증가시키는 것입니다.

이와 같이 Callee인 add() 함수 내부에서 스택을 정리하는 방식이 stdcall 방식입니다.

 

  1. 장점:
    • 호출자가 스택을 정리할 필요가 없으므로 호출 오버헤드가 줄어듭니다.
    • cdecl 방식에 비해서 코드가 간결하고 스택 정리에 대한 실수를 줄입니다.
  2. 단점:
    • 가변 인수 함수는 지원되지 않습니다. 호출된 함수는 스택 정리 시 매개변수 개수를 정확히 알아야 하기 때문입니다.

 

10.1.3 fastcall

fastcall 방식은 기본적으로 stdcall 방식과 같지만 함수에 전달하는 파라미터 일부(2개까지)를 스택 메모리가 아닌 레지스터를 이용하여 전달합니다. 만약 어떤 함수의 파라미터가 4개라면 앞의 두 개의 파라미터는 각각 ECX, EDX 파라미터를 이용하여 전달합니다.

 

  1. 특징:
    • 첫 번째와 두 번째 매개변수는 레지스터를 통해 전달됩니다.
      • x86 아키텍처에서는 ECX, EDX를 사용.
      • x64 아키텍처에서는 RCX, RDX, R8, R9를 사용.
    • 나머지 매개변수는 스택에 푸시됩니다.
    • 호출된 함수(callee)가 스택을 정리합니다.
  2. 장점:
    • 매개변수가 많지 않은 경우 성능이 뛰어납니다. 레지스터 접근 속도가 스택보다 빠르기 때문입니다.
  3. 단점:
    • 매개변수가 많아 레지스터를 초과할 경우, 스택을 사용하게 되어 효율이 감소합니다.
    • ECX, EDX 레지스터를 관리하는 추가적인 오버헤드가 필요한 경우가 있습니다.
    • 함수 내용이 복잡할 경우 ECX, EDX 레지스터를 다른 용도로 사용할 필요가 있을 때 가지고 있는 파라미터 값들을 어딘가에 따로 저장할 필요가 생깁니다.

결론 : 어디에서 사용되나요?

  1. cdecl
    • 기본 호출 규약 (C/C++ 프로그램에서 기본 사용)
    • 가변 인수가 필요한 함수 (예: printf, scanf)
  2. stdcall
    • Windows API 함수 대부분에서 사용
  3. fastcall
    • 성능이 중요한 곳에서 사용 (컴파일러가 기본으로 설정하거나 커스텀)

 

 

반응형