어셈블리: 함수 호출 규약(Calling Convention)
[어셈블리 스터디]
호출자와 피호출자간의 약속!
함수 호출 규약(Calling Convention)
함수 호출 규약(Calling Convention)이란 호출자(Caller)와 피호출자(Callee) 간에 '함수를 호출할 때 전달되는 인자의 순서나 사용이 끝나고 나서의 스택 정리 등'에 대한 약속이라고 할 수 있습니다. 크게 3가지가 있으며, 이 3가지는 각각 __cdecl, __stdcall, __fastcall 방식입니다. 오늘은 이 세가지 방식에 대해 알아보려고 합니다.
__cdecl 방식
__cdecl 방식은 C/C++ 함수에서 기본적으로 사용되는 호출 규약이며, 호출자가 스택을 정리합니다. 그리고 인자는 오른쪽에서 왼쪽으로 전달되며 호출자가 피호출자를 호출 시에 전달되는 인자의 개수를 알고있기 때문에 가변 인수 함수를 만들 수 있다는 장점을 지니고 있습니다. 우선 아래의 코드를 빌드하고 난 뒤의 바이너리를 디버거를 통해 살펴보도록 하겠습니다.
int sum(int a, int b) { return a + b; } int main(int argc, char* argv[]) { sum(5, 4); return 0; }
위 코드에서는 5와 4를 sum 함수에 전달한 후에, sum 함수에서 전달된 값을 가지고 서로 더한 값을 반환하게 됩니다. main 함수의 어셈블리를 잠시 확인해보도록 합시다.
00401068 |. 6A 04 PUSH 4
0040106A |. 6A 05 PUSH 5
0040106C |. E8 94FFFFFF CALL Consolas.00401005
00401071 |. 83C4 08 ADD ESP,8
위는 main 함수의 일부이며, 코드를 보시면 오른쪽에서 왼쪽으로 인자가 차례대로 스택에 올라가는 것을 볼 수 있으며, sum 함수(401005)를 호출하고 나서 아래를 보시면 ADD ESP, 8로 호출자(main 함수)에서 스택을 정리하는 것을 확인할 수 있습니다. 그리고 추가적으로 ADD ESP, 8과 PUSH문이 두번 쓰인것으로 봐서 4바이트를 차지하는 2개의 인자가 전달되었음을 확인할 수 있습니다. 이처럼, cdecl 방식은 호출자가 피호출자의 스택 프레임을 정리한다는 것을 알 수 있습니다.
__stdcall 방식
__stdcall 방식은 Win32 API에서 사용되며, 피호출자가 스택을 정리합니다. 그리고 인자는 __cdecl 방식과 마찬가지로 오른쪽에서 왼쪽으로 전달되며 Win32 API에서는 가변 인수 함수가 없기 때문에, 매개변수의 개수가 고정적입니다. 이는 호출자에서 스택을 정리하는 것보다, 피호출자가 스택을 정리하는게 더욱 효율적입니다. 우선 아래의 코드를 빌드하고 난 뒤의 바이너리를 디버거를 통해 살펴보도록 하겠습니다.
int __stdcall sum(int a, int b) { return a + b; } int main(int argc, char* argv[]) { sum(5, 4); return 0; }
위 예제는 제일 처음에 사용되었던 예제와 기능이 동일합니다. 단지 이번에는 __stdcall가 함수명 앞에 붙었을 뿐입니다. 한번 main 함수의 어셈블리를 확인해보도록 합시다.
00401068 |. 6A 04 PUSH 4 0040106A |. 6A 05 PUSH 5 0040106C |. E8 9EFFFFFF CALL Consolas.0040100F
위는 main 함수의 일부이며, 코드를 보시면 오른쪽에서 왼쪽으로 인자가 차례대로 스택에 올라가는 것을 볼 수 있습니다. 그런데 이번에는 호출자가 아닌 피호출자에서 스택을 정리하는 것이기 때문에 호출자에 따로 스택을 정리하는 코드가 존재하지 않습니다. 피호출자를 살펴보아야 하죠.
00401020 >/> 55 PUSH EBP 00401021 |. 8BEC MOV EBP,ESP .. 00401038 |. 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8] 0040103B |. 0345 0C ADD EAX,DWORD PTR SS:[EBP+C] .. 00401041 |. 8BE5 MOV ESP,EBP 00401043 |. 5D POP EBP 00401044 \. C2 0800 RETN 8
위에서 눈여겨보아야 할 부분이 함수의 마지막 부분인 401044입니다. 401044를 보시면 RETN 8 명령으로 sum 함수 내에서 스택을 정리함을 알 수 있습니다. 이렇게 반환이 끝나고 나서 ESP(스택 포인터)를 8만큼 증가시킵니다. 이처럼 __stdcall 방식은 __cdecl 방식과는 다르게 호출자에서 스택을 정리하지 않고, 피호출자에서 스택을 정리한다는 사실을 기억해두세요.
__fastcall 방식
__fastcall 방식은 이름부터가 정말 빨라보이지 않나요? 이 __fastcall 방식에선 스택이 아닌 가까운 레지스터를 사용함으로써 호출 속도가 빠르며 피호출자가 스택을 정리하나 스택을 사용하지 않고 레지스터를 이용하므로 정리할 내용이 없어 따로 정리를 하지 않습니다. 우선 아래의 코드를 빌드하고 난 뒤의 바이너리를 디버거를 통해 살펴보도록 하겠습니다.
int __fastcall sum(int a, int b) { return a + b; } int main(int argc, char* argv[]) { sum(5, 4); return 0; }
위 예제는 제일 처음에 사용되었던 예제와 기능이 동일합니다. 단지 이번에는 __fastcall가 함수명 앞에 붙었을 뿐입니다. 한번 main 함수의 어셈블리를 확인해보도록 합시다.
00401068 |. BA 04000000 MOV EDX,4 0040106D |. B9 05000000 MOV ECX,5 00401072 |. E8 98FFFFFF CALL Consolas.0040100F
위는 main 함수의 일부이며, 코드를 보시면 EDX 레지스터와 ECX 레지스터를 사용하여 전달하고 있음을 알 수 있습니다. 이 __fastcall 방식을 사용하게 되면 앞에 두개의 매개변수는 ECX와 EDX 레지스터를 이용하지만, 나머지 매개변수는 위에서 설명드린 __cdecl 방식과 __stdcall 방식과 동일하게 오른쪽부터 왼쪽으로 인자가 스택에 올라갑니다. 우선 피호출자를 확인하여 어떻게 덧셈이 이루어지는지 살펴보도록 합시다.
00401020 >|> 55 PUSH EBP 00401021 |. 8BEC MOV EBP,ESP .. 0040103A |. 8955 F8 MOV DWORD PTR SS:[EBP-8],EDX 0040103D |. 894D FC MOV DWORD PTR SS:[EBP-4],ECX 00401040 |. 8B45 FC MOV EAX,DWORD PTR SS:[EBP-4] 00401043 |. 0345 F8 ADD EAX,DWORD PTR SS:[EBP-8] .. 00401049 |. 8BE5 MOV ESP,EBP 0040104B |. 5D POP EBP 0040104C \. C3 RETN
위의 코드에서는, EDX 레지스터의 값을 EBP-8에 넣고 ECX 레지스터의 값을 EBP-4에 넣습니다. 그리고 EBP-4의 값을 EAX 레지스터에 넣고, EAX 레지스터의 값과 EBP-8의 값을 서로 더한값이 EAX 레지스터에 들어갑니다. 이처럼 __fastcall 방식은 앞에 두개의 매개변수는 ECX와 EDX 레지스터를 이용하여 호출 속도가 빠른 방식입니다.
'정리 > Assembly' 카테고리의 다른 글
어셈블리: 데이터 타입, 피연산자 타입, 명령어 정리 (5) | 2012.11.25 |
---|---|
범용 레지스터: EAX, ECX, EDX, ESI, EDI, ESP, EBP (7) | 2012.11.24 |