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 기본 설명

Trackback Address :: http://www.reversecore.com/trackback/13 관련글 쓰기

  1. kernys 2009/08/09 16:19 댓글주소 | 수정 | 삭제 | 댓글

    좋은 글 감사합니다 ^^

  2. Ezbeat 2009/10/25 09:14 댓글주소 | 수정 | 삭제 | 댓글

    좋은 블로그네요 ^^ 내용정리도 잘 되어있구요~ 시간날때마다 쭉 읽어봐야겠네요~!

  3. 베리굿 2009/11/08 19:54 댓글주소 | 수정 | 삭제 | 댓글

    음... 대강 읽어보고 나중에 다시한번봐야겠네요 지금으로선 이해가 잘 가지않네요

  4. letov 2009/11/10 14:15 댓글주소 | 수정 | 삭제 | 댓글

    쉽게 잘 정리되어 있네요. 감사합니다.

  5. rapperdo 2010/01/27 22:25 댓글주소 | 수정 | 삭제 | 댓글

    우연히 들르게 되었는데 좋은글이 정말 많이 있네요!^^ 잘 보겠습니다~~ 감사해요~^^

  6. anonymax 2010/03/03 00:19 댓글주소 | 수정 | 삭제 | 댓글

    이렇게 정리하는게 쉽지 않을텐데.. 정말 대단하십니다. 처음부터 끝까지 정독해야겠네요^^ 좋은 글 감사합니다.

  7. 시간의흔적 2010/03/16 16:39 댓글주소 | 수정 | 삭제 | 댓글

    좋은 글 잘 보고 갑니다~~~

  8. 롤키 2010/03/28 12:06 댓글주소 | 수정 | 삭제 | 댓글

    이 글을 먼저 봤으면 밤 안샜겠네요...;;
    SendMessage를 후킹하는데 ESP가 꼬여버리는 오류가 발생했는데
    계속 고민하다가 호출규약을 떠올려보니.. 해결됐습니다.
    그리고 정보를 더 찾아보다 이 글을보니..
    허무하네요 -_-....아....ㅠㅠㅠㅠ

    • reversecore 2010/03/29 20:16 댓글주소 | 수정 | 삭제

      롤키님, 안녕하세요.

      예전에 저도 같은 문제로 고생한 경험이 있답니다.

      제 경우는 고생한만큼 기억에 잘 남았답니다. ^^

      감사합니다.

  9. 이누 2010/07/28 15:11 댓글주소 | 수정 | 삭제 | 댓글

    전에 어셈블리 책 볼때 나왔던 내용인데

    머리 속 어딘가 뭉실뭉실 떠다니던게

    이 글을 보니 확실히 단단한 덩어리가 되었다는 느낌입니다!

    좋은 글 감사합니다 ^-^

  10. 질문 2010/08/09 19:05 댓글주소 | 수정 | 삭제 | 댓글

    앞의 두 파라미터라면 arg1,arg2를 말씀하시는 건가요?
    test(arg1, 2, 3, 4)일 경우를 예로 들어서요 ㅎㅎ

    그리구 fastcall은 _fastcall을 붙여주면 되는지요 ㅎㅎ
    그리구 ecx와 edx만 사용하나요? arg1은 항상 ecx에 들어가나요?

    • reversecore 2010/08/10 18:31 댓글주소 | 수정 | 삭제

      네, 제가 예전에 확인했던 기억으로는 그렇습니다.

      혹시 말씀하신 내용과 다른 경우가 발생하면 저에게도 좀 알려주시면 감사하겠습니다. ^^

  11. mjxaone 2011/01/21 13:10 댓글주소 | 수정 | 삭제 | 댓글

    이럴수가.. 너무 감사해요.!!!!
    이렇게 확실하게 저를 가르쳐주다니요!!!!

    감사합니다.!

  12. 궁금이 2011/01/27 22:13 댓글주소 | 수정 | 삭제 | 댓글

    stdcall에서는 SUB ESP,8과 같이 스택프래임을 만드는 과정(?)이 왜 안 보이는 건가요?

    • reversecore 2011/01/31 11:46 댓글주소 | 수정 | 삭제

      안녕하세요.

      위 코드에 대해서 말씀하시는 것이지요?

      문의하신 SUB ESP, 8 명령은 4 byte 로컬 변수 2 개를 위한 스택 공간을 확보하는 명령이라고 보시면 됩니다.

      401000 주소의 add() 함수는 로컬 변수를 사용하지 않습니다.
      그래서 문의하신 코드가 보이지 않는 것입니다.

      하지만 MOV EBP, ESP 명령에 의해서 스택프레임은 생성되었습니다.

      감사합니다.

  13. 리버싱입문자 2011/05/23 15:59 댓글주소 | 수정 | 삭제 | 댓글

    OS관련책이나 기타 관련책에서 항상 나오는 함수호출규약을 보고
    솔직히 이해하기 힘들었는데, 이 블로그를 통해서 3가지의 방식에 대해
    '이해'를 하게 되었습니다.

    항상 감사드리며 책 출간도 기다리고 있겠습니다~

  14. 초보자 2011/05/28 16:42 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요^^ 얼마전에 이 블로그를 알게되어 열심히 공부하고 있습니다.

    한가지 궁금한건... 제가 직접 코드를 짜보고 직접 디버깅 해보고 싶은데..
    예제로 올려주신 파일들은 어떤 컴파일러를 사용하신건지 궁금하네요^^

    아 그리고 책 출간 예정일은 언제쯤인가요??? ㅎㅎ 항상 기다리고 있습니다

    • reversecore 2011/06/03 22:23 댓글주소 | 수정 | 삭제

      안녕하세요.

      제 예제는 모두 Visual C++ Express 8.0/10.0 으로 제작하였습니다.

      책 출간이 다가오면 블로그에 가장 먼저 공지할께요~ ^^

      감사합니다.

  15. 열정 2011/06/05 11:35 댓글주소 | 수정 | 삭제 | 댓글

    잘 보고 갑니다.


◀ PREV : [1] : ... [76] : [77] : [78] : [79] : [80] : [81] : [82] : [83] : [84] : ... [91] : NEXT ▶