abstract
Calling Convention (함수 호출 규약) 에 대해서 알아 보겠습니다.
Calling Convention
우리 말로 '함수 호출 규약' 이라고 합니다.
"함수를 호출할 때 파라미터를 어떤 식으로 전달하고, 스택을 어떻게 정리하는가" 에 대한 일종의 약속입니다.
우린 이미 함수 호출전에 파라미터를 스택을 통해서 전달한다는 것을 알았습니다.
스택이란 프로세스에서 정의된 메모리 공간이며 아래 방향(주소가 줄어드는 방향)으로 자랍니다.
또한 PE header 에 그 크기가 명시되어 있습니다.
즉, 프로세스가 실행될 때 스택 메모리의 크기가 결정됩니다.
(malloc/new 와 같은 메모리 동적할당과는 틀립니다.)
질문) 그렇다면 함수가 실행 완료 되었을 때 스택에 들어있던 파라미터는 어떻게 해야 될까요?
그대로 놔둡니다.
스택에 저장된 값은 임시로 사용하는 값이기 때문에
더이상 사용하지 않는다고 하더라도 값을 지우거나 하면 쓸데없는 CPU 자원을 소모하게 됩니다.
어차피 다음번에 스택에 값을 입력할 때 저절로 덮어써집니다.
또한 스택 메모리는 이미 고정되어 있기 때문에 메모리 해제를 할 수 없고 할 필요도 없습니다.
질문) ESP(스택 포인터)는 어떻게 되어야 할까요?
ESP 값은 함수 호출전으로 복원되어야 합니다.
그래야 참조 가능한 스택의 크기가 줄어들지 않게 됩니다.
스택 메모리는 고정되어 있고 ESP 로 스택의 현재 위치를 가리키는데,
만약 ESP 가 스택의 끝을 가리킨다면 더 이상 스택을 사용할 수 없습니다.
함수 호출 후에 ESP (스택 포인터) 를 어떻게 정리하는지에 대한 약속이 바로 Calling Convention 입니다.
주요한 Calling Convention 은 아래와 같습니다.
- cdecl
- stdcall
- fastcall
Windows 프로그램의 디버깅에서는 cdecl 와 stdcall 의 차이점을 확실히 아셔야 합니다.
어떤 방식이던지 파라미터를 스택을 통해 전달한다는 것은 동일합니다.
* 설명에 앞서 간단한 용어 소개를 하겠습니다.
Caller(호출자) - 함수를 호출한 쪽
Callee(피호출자) - 호출 당한 함수
예를 들어 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);
}
위와 같은 테스트용 C 코드를 (최적화 옵션을 끄고) 빌드하면 disasm 코드는 아래와 같습니다.
00401000 PUSH EBP ; add() 시작
00401001 MOV EBP,ESP
00401003 MOV EAX,DWORD PTR SS:[EBP+8]
00401006 ADD EAX,DWORD PTR SS:[EBP+C]
00401009 POP EBP
0040100A RETN
...
00401010 PUSH EBP ; main() 시작
00401011 MOV EBP,ESP
00401013 PUSH 2
00401015 PUSH 1
00401017 CALL callconv.00401000 ; => add() 호출
0040101C ADD ESP,8 ; => stack 정리
0040101F POP EBP
00401020 RETN
진하게 표시된 코드를 보시면 add() 함수의 파라미터 1, 2 를 역순으로 스택에 입력하고,
add() 함수를 호출한 다음 ADD ESP, 8 명령으로 스택을 정리하고 있습니다.
아래의 그림을 보시면 이해하기 쉽습니다.
<Fig .1>
401017 주소의 CALL 명령을 실행하기 직전의 스택 상태입니다.
파라미터로 1, 2 가 전달되었습니다.
add() 함수를 실행하고 40101C 주소로 리턴되어도 스택 상태는 역시 <Fig. 1> 과 같습니다.
<Fig. 2>
40101C 주소의 ADD ESP, 8 명령이 실행되면 비로소 add() 함수의 파라미터를 위해 사용되었던
스택 크기(8 byte) 만큼 ESP 를 보정해 줍니다.
cdecl 방식의 장점은 printf() 함수와 같이 가변길이 파라미터를 전달 할 수 있다는 것입니다.
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);
}
위와 같은 테스트용 C 코드를 (최적화 옵션을 끄고) 빌드하면 disasm 코드는 아래와 같습니다.
00401000 PUSH EBP ; add() 시작
00401001 MOV EBP,ESP
00401003 MOV EAX,DWORD PTR SS:[EBP+8]
00401006 ADD EAX,DWORD PTR SS:[EBP+C]
00401009 POP EBP
0040100A RETN 8
...
00401010 PUSH EBP ; main() 시작
00401011 MOV EBP,ESP
00401013 PUSH 2
00401015 PUSH 1
00401017 CALL callconv.00401000
0040101C POP EBP
0040101D RETN
위 코드를 보시면 main() 함수에서 add() 함수 호출 후에 스택 정리 코드(ADD ESP, 8) 가 생략되어 있습니다.
스택의 정리는 add() 함수 마지막 코드인 RETN 8 명령에서 수행됩니다.
RETN 8 명령의 의미는 RETN + POP 입니다. 즉, 리턴 후 지정된 크기만큼 ESP 를 증가시키는 것입니다.
stdcall 방식의 장점은 호출되는 함수(Callee) 내부에 스택 정리 코드가 존재하므로
함수 호출할 때마다 ADD ESP, XXX 명령을 써줘야 하는 cdecl 방식에 비해서 코드 크기가 작아집니다.
Win32 API 는 분명 C 언어로 된 라이브러리 이지만 기본 cdecl 방식이 아닌 stdcall 방식을 사용합니다.
이는 C 이외의 다른 언어 (Delphi(Pascal), Visual Basic, etc) 에서 API 를 직접 사용할 때
호환성을 좋게 하기 위한 것으로 생각됩니다.
fastcall
기본적으로 stdcall 과 같습니다만, 함수에 전달 하는 파라미터 일부(2개 까지)를
스택 메모리가 아닌 레지스터를 이용하여 전달한다는 것이 특징입니다.
어떤 함수의 파라미터가 4 개라면, 앞의 2 파라미터는 각각 ECX, EDX 파라미터를 이용하여 전달하는 것입니다.
fastcall 의 장점은 이름 그대로 좀 더 빠른 함수 호출이 가능합니다.
(CPU 입장에서는 멀리 있는 메모리 보다 CPU 와 같이 붙어있는 레지스터에 접근하는 것이 더 빠릅니다.)
단점은 호출 자체는 빠르지만 ECX, EDX 레지스터를 관리하는 추가적인 오버헤드가 필요한 경우가 있습니다.
가령 함수 호출전에 ECX, EDX 에 중요한 값이 저장되어 있다면 백업해 놓아야 할 것입니다.
또한 함수 내용이 복잡하다면 ECX, EDX 레지스터를 다른 용도로 사용할 필요가 있을 때
역시 이들이 가지고 있는 파라미터 값을 어딘가에 따로 저장할 필요가 생기게 됩니다.
따라서 fastcall 은 함수 호출을 빠르게 하려는 용도보다는
레지스터를 사용하여 더 편하게 파라미터를 전달 할 때 사용된다고 보시면 되겠습니다.
Epilogue
지금까지 Calling Convention 에 대해서 알아보았습니다.
스택과 레지스터에 대해서 더 알아보고 싶으신 분은 제 블로그에 있는 관련 포스트를 참고하시기 바랍니다.
Stack Frame
IA-32 Register 기본 설명
'study' 카테고리의 다른 글
| PE(Portable Executable) File Format (8) - PE Header (12) | 2009/04/22 |
|---|---|
| PE(Portable Executable) File Format (7) - PE Header (37) | 2009/04/18 |
| PE(Portable Executable) File Format (6) - PE Header (57) | 2009/04/06 |
| PE(Portable Executable) File Format (5) - PE Header (24) | 2009/04/03 |
| PE(Portable Executable) File Format (4) - PE Header (21) | 2009/03/31 |
| PE(Portable Executable) File Format (3) - PE Header (16) | 2009/03/29 |
| PE(Portable Executable) File Format (2) - PE Header (29) | 2009/03/28 |
| PE(Portable Executable) File Format (1) - PE Header (51) | 2009/03/23 |
| Calling Convention - 함수 호출 규약 (28) | 2009/03/08 |
| Stack Frame (40) | 2009/03/03 |
| IA-32 Register 기본 설명 (45) | 2009/02/28 |
cdecl.exe
