본문 바로가기

컴퓨터와 보안/리버싱

함수 호출 규약(Calling Convention)

함수 호출 규약이란?

함수를 호출할 때 '파라미터를 어떤 식으로 전달하는가?' 즉 함수를 호출하는 방식에 대한 일종의 약속이다.

함수 호출 규약은 인자 전달 방법, 인자 전달 순서, Stack Frame을 정리하는 방법 에 따라 그 종류를 구분한다.

 

 

함수 호출 전에 파라미터는 스택을 통해 전달된다.

스택이란 프로세스에서 정의된 메모리 공간이며 아래 방향(주소가 줄어드는 방향)으로 자란다.

 

  • PUSH 명령어는 현재 ESP 레지스터가 가지고 있는 스택 메모리 주소에 인자 값을 저장한다. ESP 포인터는 PUSH 명령 어에 의해 작아지게 되고, 스택이 아래로 길어진다. 

  • pop 명령어는 현재 ESP 레지스터가 가지고 있는 스택 메모리 주소에 있는 데이터를 인자로 복사하고, ESP 포인터의 값을 +4 한다. (이 때, 무조건 4바이트 단위로 복사하게 된다.)  ESP 포인터는 pop 메모리에 의해 커진다.

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

-> 그대로 놔둔다.

스택에 저장된 값은 임시로 사용하는 값이기 때문에 더 이상 사용하지 않는다고 하더라도 값을 지우거나 하면 불필요하게 CPU 자원을 소모하고 어차피 다음 번에 스택에 다른 값을 입력할 때 저절로 덮어쓰기 때문이다.

 

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

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

 

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

 

주요한 함수 호출 규약을 설명해 보겠다.

 

*참고*

Caller - 함수를 호출한 쪽

Callee - 호출을 당한 함수

 

 

cdecl

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

 

다음 코드를 빌드한 후 Ollydbg로 디버깅 해보자

 

#include <stdio.h>

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

 

그림 1

그림 1에서 401013~40101C 주소 영역의 코드를 보면

1. add()함수의 파라미터 1, 2를 역순으로 스택에 입력하고 

2. add() 함수를 호출한 후

3. ADD ESP,8 명령으로 스택을 정리하고 있다. 

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

 

stdcall

stdcall 방식은 Win32 API에서 사용되며, Callee에서 스택을 정리하는 것이 특징이다.

C언어에서 stdcall 방식으로 컴파일 하고 싶을 때는 '_stdcall' 키워드를 붙여주면 된다.

 

다음 코드를 빌드한 후 ollydbg로 디버깅 해보자

 

#include <stdio.h>

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

 

그림 2

cdecl 방식과 차이점이 보인다.

바로 main() 함수에서 add() 함수 호출 후에 스택 정리 코드(ADD ESP, 8)이 생략되어 있다는 것이다.

스택의 정리는 add() 함수 마지막 RETN 8 명령에서 수행된다.

RETN 명령의 의미는 RETN + POP 8 바이트 이다. 즉 리턴 후 지정된 크기만큼 ESP를 증가시키는 것이다.

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

 

 

fastcall

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

가령 push 파라미터가 4개면 이중 2개는 stack에 쌓아서 함수에 전달하고 2개는 레지스터를 이용해 전달하는 방식이다.

이렇게 되면 push와 pop을  2번씩만 할 수 있다.

fastcall도 stdcall과 마찬가지로 '_fastcall' 키워드를 붙여주면 된다.

 

fastcall의 장점은 이름 그대로 좀 더 빠른 함수 호출이 가능하다는 것이다.

(Memory 보다 Register에 접근하는 속도가 빠르기 때문)

하지만 레지스터를 관리하는데 추가적인 오버헤드가 발생하는 경우가 있다.

'컴퓨터와 보안 > 리버싱' 카테고리의 다른 글

Lena's Reversing for Newbies  (0) 2021.04.15
abex' crackme #2 분석  (0) 2021.04.12
abex' crackme #1 분석  (0) 2021.04.06
프로세스 메모리 구조와 스택 프레임 구조  (1) 2021.04.04
어셈블리어와 레지스터  (0) 2021.04.04