본문 바로가기
Reversing/리버싱 핵심 원리

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

by Y06 2020. 11. 7.

10장, 함수 호출 규약

함수 호출 규약?

Calling Convention은 우리 말로 '함수 호출 규약'이라고 한다. 이것은 '함수를 호출할 때 파라미터를 어떤 식으로 전달하는가?'에 대한 일동의 약속이다.

함수 호출 전에 파라미터를 스택을 통해서 전달한다. 스택이란 프로세스에서 정의된 메모리 공간이며 아래 방향(주소가 줄어드는 방향)으로 자란다. 또한 PE 헤더에 그 크기가 명시되어 있다. 즉 프로세스가 실행될 때 스택 메모리의 크기가 결정된다(malloc/new 같은 동적 메모리 할당과는 다르다).

 

Q, 그렇다면 함수가 실행완료되었을 때 스택에 들어있던 파라미터는 어떻게 해야 될까요?

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

 

함수 호출 규약은 ESP(스택 포인터)와 연관이 있는데 함수 호출 후에 ESP를 어떻게 정리하는지에 대한 약속이 '함수 호출 규약)이다.

 

주요 함수 호출 규약

 

  • cdecl
  • stdcall
  • fastcall

이 있다.

Caller와 Callee
Caller(호출자) - 함수를 호출한 쪽
Callee(피호출자) - 호출을 당한 함수
ex) main()에서 printf()를 호출 했다면 Caller는 main(), Callee는 printf()

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

}

401013~40101C 주소 영역의 코드를 보면, add() 함수의 파라미터 1,2를 역순으로 스택에 입력하고, add() 함수(401000)를 호출한 후 ADD ESP, 8 명령으로 스택을 정리하고 있다. 이와 같이 Caller인 main() 함수가 자신이 스택에 입력한 함수 파라미터를 직접 정리하는 방식이 cdecl이다.

 

[참고]

cdecl 방식의 장점: C언어의 printf() 함수와 같이 가변 길이 파라미터를 전달할 수 있다. 이러한 가변 길이 파라미터는 다른 Calling Convention에서는 구현이 어렵다

stdcall

Callee(피호출자)인 add()함수 내부에서 스택을 정리하는 방식이 stdcall 방식이다. Win32 API는 C언어로 된 라이브러리이지만 stdcall 방식을 이용한다. 이는 C 이외의 다른 언어(Delphi(Pascal), Visual Basic 등)에서 API를 직접 호출할 때 호환성을 좋게 하기 위한 것이다.

#include <stdio.h>

int add (int a, int b) {

     return (a+b);

}

int main (int argc, char* argv[ ]) {

     return add(1,2);

}

코드에서 보면 main() 함수에서 add() 함수 호출 후에 스택 정리 코드(ADD ESP, 8)가 생략되어 있다.

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

 

[장점]

  • 호출되는 함수(Callee) 내부에 스택 정리 코드가 존재 (코드 크기가 작아짐)
  • API를 직접 호출할 때 호환성이 좋게 함

Fastcall

stdcall 방식과 비슷하다. 

함수에 정달하는 파라미터 일부(2개까지) 스택 메모리가 아닌 레지스터를 이용하여 전달한다. 즉, 4개의 파라미터를 받았을시 먼저 ECX, EDX 파라미터를 통해 먼저 전달 받고 그 다음에 스택 메모리를 이용해 전달한다.

 

[장점]

  • 빠른 함수 호출 가능 (레지스터에 접근하기 때문임)

[단점]

  • 레지스터를 관리하는 추가적인 오버헤드가 필요한 경우