minhui study

[WEEK1]호출규약(32bit/64bit) 정리 본문

Hacking/Reversing

[WEEK1]호출규약(32bit/64bit) 정리

minhui 2020. 9. 11. 23:36

함수 호출 규약

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

→ 함수 호출 후에 ESP(스택 포인터)를 어떻게 정리하는지에 대한 약속이 바로 함수 호출 규약이다.

 

●  cdecl

 

●  stdcall

 

●  fastcall

 


1. cdecl

 

주로 C언어에서 사용되는 방식이며 호출자(함수를 호출한 곳)에서 스택을 정리한다.

#include "stdio.h"

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

int main(int argc, char* argv[])
{
	return add(1,2);
}
=======main()문=========
PUSH EBP
MOV EBP, ESP
PUSH 2   ;파라미터 2
PUSH 1   ;파라미터 1 
CALL 00401000 ;ADD함수 호출
ADD ESP, 8     ;ADD함수 호출한 후, 스택 정리
POP EBP
RETN

매개변수는 스택에서 push하여 함수 호출 시 전달하고, 어떤 add() 함수의 caller인 main 함수에서 스택을 정리(ADD ESP, 8)한다. 즉, 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* argv[])
{
	return add(1,2);
}
==========add함수============
PUSH EBP
MOV EBP, ESP
MOV EAX, DWORD PTR SS:[EBP+8]
MOV EAX, DWORD PTR SS:[EBP+C]
POP EBP  
RETN 8   ;리턴 후 지정된 크기만큼 ESP를 증가시킨다. 

...

=========main()문===========
PUSH EBP
MOV EBP, ESP
PUSH 2   ;파라미터 2
PUSH 1   ;파라미터 1 
CALL 00401000 ;add함수 호출
POP EBP
RETN

cdecl방식과는 달리 main()함수에서 add()함수를 호출 후에 스택 정리 코드(ADD ESP, 8)가 생략된다. stdcall방식에서는 스택의 정리는 add()함수 마지막(40100A)의 RETN 8 명령에서 수행된다. RETN 8 명령어의 의미는 RETN + POP 8바이트이다. 즉 리턴 후에 지정된 크기만큼 ESP를 증가시키는 것이다.

정리하자면 Callee(피호출자)인 add()함수 내부에서 스택을 정리하는 방식 stdcall방식인 것이다.

 

stdcall방식의 장점은 호출되는 함수 내부에 스택 정리 코드가 존재하므로 함수를 호출할 때마다 ADD ESP, XXX 명령을 써줘야 하는 cdecl 방식에 비해서 코드 크기가 작아진다는 것이다. 

 

 

 

3. fastcall

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

 

cdecl, stdcall은 함수를 호출하기 전에 인자 값을 이미 스택에 저장을 한 뒤에 스택을 호출하지만 fastcall은 레지스터로 그 인자 값을 가져왔으며 가져온 인자 값을 새로운 스택 프레임 포인터 EBP의 위에 호출한다. 즉, 함수 내에서 가져온 인자를 새로운 EBP의 스택에 데이터를 저장한다.

https://m.blog.naver.com/PostView.nhn?blogId=stop2y&logNo=221052331132&proxyReferer=https:%2F%2Fwww.google.com%2F

즉, fastcall은 3번째 인자부터는 함수가 실행되기 전에 인자 값을 스택에 미리 저장한다. 그리고 최초의 인자 2개는 레지스터로 그 값을 저장하여 함수가 실행되고 나서 새로운 EBP가 나타났을 때 스택에 값을 저장한다.

https://m.blog.naver.com/PostView.nhn?blogId=stop2y&logNo=221052331132&proxyReferer=https:%2F%2Fwww.google.com%2F

함수 내 과정을 보면 끝부분에 MOV ESP, EBP가 있는데 이는 EBP값을 ESP로 복사하다는 뜻으로 현재 EBP의 위치에서 ESP까지의 스택을 모두 정리하다는 것이다. 

이제 스택 값은 MOV ESP, EBP의 명령어를 통해 정리가 되었지만(2개의 인자) 세 번째 인자는 Caller에서 인자 값을 스택에 저장한 것이기 때문에 POP EBP를 해주어 EBP가 Caller의 EBP로 바뀌게 해준 다음 세 번째 인자 크기 4byte를 RET 4로 정리해주면 ESP+4를 해줌으로서 스택에 있는 인자까지 정리할 수 있다. 그리고 RET명령어를 통해 스택을 정리하였으므로 Callee방식임을 알 수 있다.

https://m.blog.naver.com/PostView.nhn?blogId=stop2y

fastcall의 장점은 이름 그대로 좀 더 빠른 함수 호출이 가능하다. 왜냐하면 CPU입장에서 멀리 있는 메모리보다 CPU와 같이 붙어있는 레지스터에 접근하는 것이 훨씬 빠르기 때문이다. 하지만 만약 함수 호출 전에 ECX, EDX에 중요한 값이 저장되어 있다면 백업해 놓아야 할 것이다. 또한 함수 내용이 복잡하다면 ECX,EDX 레지스터를 다른 용도로 사용할 필요가 있을 때 역시 이들이 가지고 있는 파라미터 값을 어딘가에 따로 저장할 필요가 생긴다.

 

 



<참고 자료>

리버싱 핵심원리 (이승원 지음)

m.blog.naver.com/PostView.nhn?blogId=stop2y&logNo=221052331132&proxyReferer=https:%2F%2Fwww.google.com%2F

 

[리버싱] 함수 호출 규약 cdecl / stdcall / fastcall

함수 호출 규약스택 프레임을 사용하여 데이터를 read ,write 하는 함수들의 대화 방식을 함수 호출 규약이...

blog.naver.com

 

'Hacking > Reversing' 카테고리의 다른 글

[WEEK1] 어셈블리어 C언어로 변환하기  (0) 2020.09.13
Wargame 'Dreamhack' rev-0  (0) 2020.09.13
Crackme #1  (0) 2020.09.13
Stack구조 및 Stack Frame  (0) 2020.09.03
32bit Register (범용, 세그먼트, EFLAGS...)  (0) 2020.09.02
Comments