본문 바로가기

리버싱 핵심원리

[리버싱 핵심원리] 10장 - 함수 호출 규약

10.1 함수 호출 규약

Calling Convention은 '함수 호출 규약'이라고 한다. 이것은 '함수를 호출할 때 파라미터를 어떤 식으로 전달하는가'에 대한 일종의 약속이다. 함수 호출 전에 파라미터를 스택을 통해 전달한다는 것을 알고 있다. 스택이란 프로세스에서 정의된 메모리 공간이며 아래 방향(주소가 줄어드는 방향)으로 자란다. 또한 PE 헤더에 그 크기가 명시되어 있다. 즉 프로세스가 실행될 때 스택 메모리의 크기가 결정된다.(동적 메모리 할당과는 다르다.)

 

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

 

함수 호출 규약은 함수 호출 후에 ESP(스택 포인터)를 어떻게 정리하는지에 대한 약속이다. 주요한 함수 호출 규약은 아래와 같다.

1. cdecl
2. stdcall
3. fastcall

위 방식 중 어떤 방식이든지 파라미터를 스택을 통해 전달한다는 기본 개념은 동일하다.

 

+) 용어 설명

   1. Caller(호출자) - 함수를 호출한 쪽

   2. Calee(피호출자) - 호출을 당한 쪽

 

1. cdecl

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

#include "stdio.h"

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

int main(int argc, char* argv[])
{
	return add(1,2);
}

해당 코드를 VC++로 최적화 옵션을 끄고 빌드한 후 OllyDbg로 디버깅한다.

위 코드를 보면 401010 주소가 main 함수의 시작 지점이고 401000이 add 함수의 시작 지점이다. 401013과 401015주소에서 add 함수의 파라미터를 역순으로 스택에 입력하고 있다. 이후 add 함수를 CALL 하고 40101C에서 사용한 스택을 정리하는 모습(main 함수에서!) 을 볼 수 있다. 이처럼 Caller인 main() 함수가 자신이 스택에 입력한 함수 파라미터를 직접 정리하는 방식이 cdecl이다

 

장점 : C언어의 printf() 함수와 같이 가변 길이 파라미터를 전달할 수 있다는 것이다. 이러한 가변 길이 파리미터는 다른 함수 호출 규약에서는 구현이 어렵다.

 

2. stdcall

stdcall 방식은 Win32 API에서 사용되며, Callee에서 스택을 정리하는 것이 특징이다. C언어는 기본적으로 cdecl 방식을 사용하지만 stdcall 방식으로 컴파일을 원할 땐 '_stdcall' 키워드를 붙여주면 된다.

#include "stdio.h"

int _stdcall add(int a, int b)
{
	return (a+b);
}
int main(int argc, char* argc[])
{
	return add(1,2);
}

해당 코드를 VC++로 최적화 옵션을 끄고 빌드한 후 OllyDbg로 디버깅한다.

add 함수의 스택 정리는 cdecl 방식과는 다르게 add 함수 내부의 RETN 8에 의해 수행된다. 여기서 RETN 8의 의미는 RETN + POP 8(byte)이다. 즉 리턴 후 지정된 크기(8 byte)만큼 ESP의 크기를 증가시켜 스택을 정리하는 것이다.

Callee인 add 함수 내부에서 스택을 정리하는 방식이 stdcall이다.

 

장점 : Callee 내부에 스택 정리 코드가 존재하므로 함수를 호출할 때마다 [ADD ESP, XXX] 명령을 써줘야하는 cdecl 방식에 비해 코드의 크기가 작아진다. Win32 API는 C언어로 된 라이브러리이지만 C 이외의 다른 언어에서 API를 직접 호출할 때 호환성을 좋게 하기 위해 stdcall 방식을 사용하는 것이다.

 

3. fastcall

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

 

장점 : 좀 더 빠른 함수 호출이 가능하다.(CPU와 가까이 있는 레지스터를 이용하기 때문이다.) 

 

단점 : ECX, EDX 레지스터를 관리하는 추가적인 오버헤드가 필요한 경우가 있다. (해당 레지스터에 중요한 값이 저장된 경우와 함수의 내용이 복잡하다면 레지스터를 다른 용도로 사용할 필요가 있을 때 레지스터의 값을 백업해 놓아야 하기 때문이다.)