ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 호출규약
    프로그래밍/프로그래밍 관련 2005. 6. 21. 19:27
    %C8%A3%C3%E2%B1%D4%BE%E0HWP.zip

    호출 규약


    가.스택
    이 절에서는 호출 규약(Calling Convention)에 대해 알아 본다. 호출 규약이란 함수를 호출하는 방식에 대한 일종의 약속인데 인수는 어떻게 전달하며 리턴값은 어떻게 반환하고 인수 전달을 위해 사용한 메모리는 누가 정리할 것인지 등을 규정한다. 당연히 호출하는 쪽과 호출되는 쪽의 약속이 맞아야 하며 어느 한쪽이 약속을 어길 경우 함수도 제대로 동작하지 않을 뿐더러 메모리가 엉망이 되기 때문에 프로그램은 실행을 계속할 수 없다.

    호출 규약은 컴파일러 내부에서 일어나는 일이기 때문에 이해하기 쉽지 않다. 사실 함수를 만들 수 있고 호출하는 방법을 알고 있고 내부 동작에 관심이 없다면 굳이 호출 규약을 알아야 하는 것은 아니다. 그러나 컴파일러의 내부 동작과 함수의 호출 과정을 알게 되면 재귀 호출이나 가변 인수 등의 고급 기법들을 이해함으로써 좀 더 자유롭게 구사할 수 있다. 또한 저수준 디버깅에도 활용할 수 있으며 C/C++이 아닌 다른 언어로 만든 함수도 호출하는 방법도 알게 된다.

    호출 규약을 이해하기 위해서는 스택에 대해 알아야 하며 스택은 기계어 수준에서 동작하기 때문에 어셈블리 언어에 대한 개념도 필요하다. 이 절에서는 스택 프레임을 설명하기 위해 어셈블리 코드 리스트를 보이는데 잘 모른다 하더라도 당장 내용을 이해하는데는 큰 지장이 없다. eax, ebp 등의 레지스터는 CPU가 사용하는 변수라고 생각하면 되고 대입할 때는 mov 명령을 사용한다는 것 정도만 알아도 된다. 물론 어셈블리 언어를 안다면 더할 나위없이 좋겠지만 말이다.

    스택(Stack)은 시스템이 사용하는 메모리 공간이다. CPU가 임시적인 정보를 저장할 필요가 있을 때 이 영역을 사용한다. 프로그램이 실행중일 때의 메모리 모양은 운영체제에 따라 조금씩 달라질 수 있지만 일반적으로 다음과 같다.


    앞 부분에 프로그램의 코드가 있고 이어서 데이터 영역이 있으며 데이터 영역 아래쪽에는 자유 영역인 힙이 있다. 힙은 실행중에 동적으로 할당되는 메모리 영역이며 할당이 발생하면 뒤쪽으로 이동하면서 자유 영역을 사용한다. 스택은 메모리의 가장 뒷부분(높은 번지)에 위치하는데 값을 넣으면 위쪽으로 이동한다. 힙과 스택 사이에는 자유 영역이 있어 두 영역 사이의 완충 역할을 한다. 만약 힙과 스택이 만나게 되면 메모리가 부족한 상태가 된다.

    스택에 값을 저장하는 동작을 push라고 하며 저장된 값을 빼내는 동작을 pop이라고 한다. 스택의 현재 위치는 esp레지스터에 기억되며 push하면 esp가 감소하면서 값이 스택으로 들어가고 pop하면 esp가 증가하면서 저장된 값을 빼내온다. 스택에 저장된 값들은 LIFO(Last In First Out)의 원칙에 따라 가장 최후에 들어간 값이 가장 먼저 나온다. 예를 들어 A, B, C 순으로 값을 push했다면 pop할 때는 C, B, A 순으로 빼내야 한다.

    CPU의 범용 레지스터는 개수가 많지 않기 때문에 필요한 레지스터가 이미 값을 가지고 있을 때는 스택에 레지스터값을 잠시 대피해 놓고 사용한다. 예를 들어 eax, ecx를 잠시 다른 용도로 사용하고 싶다고 하자. 이때 CPU는 두 레지스터의 값을 스택에 push하여 저장해 놓고 이 레지스터를 사용하며 다 사용하고 난 후 pop해서 복구한다.

     

    push ecx                    // ecx 저장

    push eax                    // eax 저장

    eax, ecx 레지스터 사용

    pop eax                     // eax 복구

    pop ecx                     // ecx 복구

     

    저장된 값을 복구할 때는 푸시한 반대 순서대로 복구해야 한다. esp는 푸시될 때 감소하며 팝할 때 다시 증가하여 항상 다음 액세스할 위치를 가리킨다.


    그림에서 보다시피 두 번 푸시한 후 두 번 팝하면 스택은 원래 상태로 돌아가는 특성이 있다. 그래서 푸시, 팝 회수만 정확하게 맞추어 주면 얼마든지 많은 값들을 저장할 수 있으며 넣은 순서의 역순으로 꺼내기만 하면 된다. 또한 푸시한 대상과 팝하는 대상이 반드시 일치하지 않아도 되므로 스택을 경유하여 레지스터끼리 값을 대입할 수도 있고 교환하는 것도 가능하다.

     

    push ecx

    pop eax
     push ecx

    push eax

    pop eax

    pop ecx
     
    eax에 ecx 대입
    eax와 ecx의 교환
     

     

    스택에 대해 더 상세한 내용을 알고 싶다면 어셈블러 개론서를 참조하기 바란다.

    나.스택 프레임
    함수가 호출될 때 스택에는 함수로 전달되는 인수, 실행을 마치고 돌아올 복귀 번지, 지역 변수 등의 정보들이 저장된다. 스택에 저장되는 함수의 호출 정보를 스택 프레임(Stack Frame)이라고 한다. 또한 함수 실행중에도 필요할 경우 임시적인 정보 저장을 위해 스택을 사용하되 이때 푸시 회수와 팝 회수는 일치하므로 함수가 리턴하면 정확하게 호출전의 상태로 돌아간다.

    다음 예제를 통해 함수가 호출될 때 스택 프레임이 어떻게 생성되고 어떤 정보들이 저장되는지 살펴 보도록 하자. 호출원에서 인수를 전달하는 방식과 함수가 값을 리턴하는 방식, 지역 변수가 생성되는 영역을 살펴볼 것이다.

     

    예 제 : StackFrame
     

    #include <Turboc.h>

     

    int Add(int a, int b)

    {

       int c,d,e;

       c=a+b;

       return c;

    }

     

    void main()

    {

       int result;

       result=Add(1,2);

       printf("result = %d\n",result);

    }

     

    Add 함수는 두 개의 인수를 전달받아 인수의 합을 구한 후 그 결과를 리턴해 준다. Add 함수 내에는 세 개의 지역 변수가 선언되어 있는데 당장 사용되지는 않는 변수들이지만 함수 동작에 꼭 필요한 변수라고 가정하자. 이 함수의 동작은 아주 간단하지만 인수, 지역 변수, 리턴값을 모두 가지고 있어 스택 프레임을 살펴 보기에는 적절하다. Add 함수를 호출하는 부분에 중단점을 설정해 놓고 실행한 후 View/Debug Window/Disassembly를 선택하면 어셈블리 코드를 볼 수 있다. 다음은 main 함수에서 result=Add(1,2);를 호출하는 코드이다.

     

    push 2

    push 1

    call Add

    add esp,8

    result=eax

     

    호출원에서는 함수로 인수를 전달하기 위해 인수들을 스택에 푸시한다. 뒤쪽 인수부터 순서대로 스택에 푸시한 후 call 명령으로 Add 함수를 호출했다. call 명령은 함수가 복귀할 번지를 스택에 푸시한 후 함수의 번지로 점프하는 것이다. 이 상태에서 스택에는 두 인수 2, 1과 복귀 번지가 저장되어 있을 것이다. 다음은 Add 함수의 코드를 보자.

     

    push ebp

    mov ebp,esp

    sub esp,0ch

    mov eax,[ebp+8]

    add eax,[ebp+0ch]

    mov [ebp-4],eax

    mov exa,[ebp-4]

    mov esp,ebp

    pop ebp

    ret

     

    Add 함수는 호출원에서 사용하던 ebp를 스택에 먼저 푸시한 후 ebp에 스택의 현재 위치를 대입한다. ebp는 함수 실행중에 인수와 지역 변수를 액세스하기 위한 기준 번지(Base Pointer)로 사용되며 함수 본체에서는 값이 변하지 않고 계속 유지된다. Add를 호출한 함수(이 경우 main)도 ebp를 기준 번지로 사용하므로 Add는 자신의 ebp를 설정하기 전에 호출원의 ebp를 저장해야 할 필요가 있다.

    다음으로 esp를 0ch(십진수 12)만큼 감소시켜 지역 변수가 들어갈 공간을 만든다. Add 함수는 총 3개의 정수형 변수를 사용하므로 지역 변수 영역에 필요한 공간은 12바이트이며 esp를 이만큼 위로 올림으로써 스택상에 빈 공간을 만드는 것이다. 지역 변수 영역은 함수에 선언된 지역 변수의 총 크기만큼 할당되며 별도의 초기화는 하지 않는다. 여기까지의 코드를 접두(prolog)라고 하며 함수가 실행을 준비하는 과정이다. sub esp,0ch까지 실행되었을 때의 스택 모양은 다음과 같다. 이 그림에서 각 격자는 4바이트이다.


    ebp는 자신이 저장된 스택 위치를 가리키고 있으며 esp는 지역 변수 영역의 상단을 가리키고 있다. esp는 함수 실행중에 필요에 따라 오르락 내리락거리겠지만 ebp는 계속 기준 번지를 가리킨다. 이 상태에서 ebp를 기준으로 스택의 아래쪽에는 함수로 전달된 인수가 있고 위쪽에는 함수의 지역 변수가 있으며 인수와 지역 변수들은 ebp에 대한 상대적인 오프셋으로 구할 수 있다.

    ebp가 가리키는 곳 바로 아래(ebp+4)에는 복귀 번지가 저장되어 있으며 복귀 번지의 아래(ebp+8)에는 첫 번째 인수 a가 있다. 함수의 본체에서는 ebp+8을 액세스함으로써 첫 번째 인수를 읽거나 쓸 수 있으며 두 번째 이후의 인수도 오프셋만 바꿔서 액세스할 수 있다. ebp 바로 위(ebp-4)에는 첫 번째 지역 변수가 있는데 이 경우 c이다. 일반적으로 n번째 지역 변수는 ebp-n*4의 위치에 있다고 할 수 있다.

    실행 준비가 완료되면 함수의 본체 코드가 실행된다. 이 함수의 본체인 c=a+b 코드는 다음 세 줄로 컴파일된다. 기계어는 메모리간의 덧셈을 직접 지원하지 않기 때문에 레지스터로 값을 읽은 후 레지스터와 연산해야 한다.

     

    mov eax,[ebp+8]          // eax=a

    add eax,[ebp+0ch]           // eax=eax+b

    mov [ebp-4],eax          // c=eax

    mov eax,[ebp-4]          // return c

     

    먼저 eax에 ebp+8의 내용을 읽어 오는데 스택 그림에서 보다시피 ebp+8은 첫 번째 인수인 a이다. eax가 a값을 대입받은 후 ebp+0ch에 있는 인수 b의 값을 더하며 이 결과를 ebp-4에 있는 지역 변수 c에 대입한다. 연산을 마친 후 그 결과를 eax 레지스터에 다시 대입하는데 eax에 대입되는 값은 리턴값이다. 여기까지 함수의 본체는 실행을 마쳤으며 남은 정리 작업을 수행한다. 정리 작업을 하는 코드를 접미(epilog)라고 하는데 이 함수의 경우 다음 세 줄이다.

     

    mov esp,ebp

    pop ebp

    ret

     

    esp에 ebp를 대입하는데 이는 지역 변수를 위해 할당했던 스택 영역을 회수한다는 뜻이다. 지역 변수의 생명은 이 시점에서 끝이 난다. esp를 복구한 후 ebp를 팝하면 호출원에서 사용하던 ebp가 복구된다. 마지막으로 ret 명령으로 복귀하는데 ret는 스택에 저장된 복귀 번지를 꺼내 그 번지로 리턴한다. main 함수에서 call 명령으로 이 함수를 호출할 때 복귀 번지를 저장해 두었는데 이 번지는 call Add 바로 다음 위치이다.

     

    add esp,8

    result=eax

     

    esp에 8을 더하는 이유는 함수 호출전에 인수 전달을 위해 푸시한 값을 삭제하기 위해서이다. push 2, push 1에 의해 esp는 8만큼 감소했으므로 원래대로 돌려 놓으려면 8만큼 증가시켜야 한다. 이 동작은 팝을 두 번 하는 것과 동일하되 인수의 수가 많을 때는 일일이 팝을 할 수 없으므로 esp를 직접 증가시켜 여러 번의 팝을 대신하고 있다.

    인수 전달 영역까지 해제하면 스택은 함수가 호출되기 전의 상태로 정확하게 복구된다. esp, ebp 등의 주요 레지스터들이 호출 전과 완전히 똑같이 복구되는 것이다. 마지막으로 eax에 저장되어 있는 Add 함수의 리턴값을 result에 대입한다. 이 결과값은 printf 호출로 화면으로 출력될 것이며 프로그램은 종료된다.

    Add 함수가 하나가 호출될 때의 스택 프레임이 어떻게 작성되는지 보았는데 내부 동작이라 다분히 복잡하다. 이번에는 함수가 연속적으로 두 번 호출될 때 어떤 일이 벌어지는지 연구해 보자. 다음과 같이 Add를 호출하는 함수가 있다고 하자.

     

    void Dog(int v)

    {

       int n,m;

       n=Add(1,2);

       ....

    }

     

    Dog 함수는 정수형 인수 하나를 취하고 두 개의 정수형 지역 변수를 사용한다. main에서 이 함수를 호출할 때 스택 프레임은 다음과 같이 작성된다.


    Dog가 호출되었을 때 Dog의 스택 프레임이 생성된다. 이 함수는 앞에서 설명한 방식대로 ebp를 기준으로 자신의 인수와 지역 변수를 액세스할 수 있다. 이 상태에서 Add가 호출되면 Dog의 스택 프레임 위에 Add의 스택 프레임이 생성된다. Dog는 Add로 전달할 인수 2, 1과 복귀 번지를 차례로 스택에 푸시하며 Add는 Dog의 ebp를 스택에 저장한 후 자신의 ebp를 새로 대입하고 esp를 감소시켜 자신의 지역 변수 영역을 만들 것이다. Add도 자신의 ebp를 기준으로 인수와 지역 변수를 액세스한다.

    Add가 실행을 마치고 돌아오기 직전에 지역 변수 영역을 해제하며 Dog의 ebp를 복구해 준다. Dog는 Add가 리턴되면 Add로 전달한 인수 영역을 해제하여 Add가 호출되기 전의 상태로 스택을 돌려 놓는다. 그리고 자신의 ebp로 계속 인수와 지역 변수들을 액세스할 수 있다.

    함수가 호출될 때 인수와 복귀 번지, 지역 변수 영역 등의 스택 프레임이 생성되고 리턴된 후 원래대로 정확하게 복구되도록 되어 있다. 호출하는 쪽과 호출되는 쪽이 서로 긴밀히 협조하여 스택이 항상성을 유지하도록 하는 것이다. 그래서 스택 공간이 남아 있는 한 회수에 제한없이 함수를 계속 호출할 수 있고 임의의 함수끼리 서로 호출이 가능하다.

     

    참고

    위에서 보인 어셈블리 코드는 실제 컴파일러가 생성하는 코드와는 조금 다른데 스택 프레임을 이해하는데 굳이 필요치 않다고 판단되는 부분은 생략했다. 스택 프레임의 실제 모양은 컴파일러마다 조금씩 다르며 같은 컴파일러라도 빌드 모드나 최적화 옵션 설정에 따라 달라진다. 비주얼 C++의 경우 지역 변수의 총 크기보다 0x40 바이트만큼 더 여유분을 할당하며 초기화되지 않는 지역 변수 영역은 모두 0xcc로 초기화한다.

    다.호출 규약
    앞 항에서 Add 함수의 어셈블리 코드를 통해 스택 프레임의 실제 모양을 확인해 보았다. 인수는 뒤쪽부터 순서대로 전달하며 인수 전달에 사용한 스택은 호출원이 정리했는데 이는 C/C++ 언어의 기본 호출 규약인 __cdecl의 스택 프레임 모양일 뿐이다. 호출 규약이 바뀌면 스택 프레임의 모양도 달라질 수 있다.

    호출 규약은 호출원과 함수간의 약속이므로 양쪽이 다른 형태로 약속을 할 수도 있는 것이다. 그렇다면 __cdecl이 아닌 다른 호출 규약은 어떻게 스택 프레임을 작성하는지 차이점을 분석해 보자. 호출 규약에 따라 인수를 전달하는 방법과 스택의 정리 책임, 함수의 이름을 작성하는 방법이 달라진다.

     

    호출 규약
     인수 전달
     스택 정리
     이름 규칙
     
    __cdecl
     오른쪽 먼저
     호출원
     _함수명
     
    __stdcall
     오른쪽 먼저
     함수
     _함수명@인수크기
     
    __fastcall
     ECX, EDX에 우선 전달. 나머지는 오른쪽 먼저
     함수
     @함수명@인수크기
     
    thiscall
     오른쪽 먼저, this 포인터는 ecx 레지스터로 전달된다.
     함수
     C++ 이름 규칙을 따름.
     
    naked
     오른쪽 먼저
     함수
     없음
     

     

    리턴값을 돌려 주는 방식도 호출 규약에 따라 달라질 수 있는데 다행히 현존하는 모든 호출 규약의 리턴 방식은 동일하다. 4바이트의 값을 돌려줄 때는 eax 레지스터를 사용하며 8바이트의 값을 리턴할 때는 edx:eax 레지스터 쌍을 사용한다. 8바이트를 초과하는 큰 리턴값, 예를 들어 구조체 등은 임시 영역에 리턴할 값을 넣어 두고 그 포인터를 eax에 리턴한다.

    __stdcall
    C/C++ 언어의 디폴트 호출 규약은 __cdecl인데 Add 함수의 호출 규약을 __stdcall로 바꿔 보자. __stdcall은 윈도우즈 API 함수들의 기본 호출 규약이며 비주얼 베이직도 이 호출 규약을 사용한다. __cdecl과 인수를 전달하는 방법은 동일하되 인수 전달에 사용된 스택을 정리하는 주체가 호출원이 아니라 함수라는 점이 다르다. Add 함수의 호출 규약을 바꾸기 위해 다음과 같이 수정해 보자.

     

    int __stdcall Add(int a, int b)

    {

       int c,d,e;

       c=a+b;

       return c;

    }

     

    함수 이름앞에 __stdcall 키워드만 삽입해 주면 이 함수는 __stdcall 호출 규약을 사용한다. main에서 함수를 호출하는 부분이 다음과 같이 변경된다.

     

    push 2

    push 1

    call Add

    result=eax

     

    인수를 스택에 밀어 넣는다는 것과 인수를 푸시하는 순서는 동일하다. 단, 함수가 리턴된 후에 인수 전달에 사용한 스택을 복구하지 않는다는 점이 __cdecl과 다르다. 인수 전달에 사용한 영역은 이제 Add 함수가 직접 정리한다. 이 함수의 접두, 본체는 __cdecl과 동일하며 접미 부분이 다음과 같이 변경된다.

     

    push ebp

    ....

    ret 8

     

    복귀 코드가 ret에서 ret 8로 바뀌었으며 복귀하면서 esp를 8만큼 증가시킨다. 이 코드에 의해 함수는 실행을 마치고 복귀함과 동시에 인수 영역을 해제한다. Add 함수 자신이 복귀하면서 스택을 정리하므로 호출원에서는 스택을 정리할 필요가 없다. 호출원은 인수를 순서대로 스택에 푸시한 후 함수만 불러주면 된다.

    __cdecl과의 차이점
    __cdecl과 __stdcall의 가장 큰 차이점은 스택 정리 주체가 누구인가하는 점인데 사실 이 차이점이 컴파일된 결과 코드에 미치는 영향은 별로 없다. 스택 정리 주체와는 상관없이 스택은 항상 호출 전의 상태로 복구되며 프로그램의 동작도 완전히 동일하다. 실행 속도는 거의 차이가 없으며 프로그램의 크기는 __stdcall이 조금 더 작다. 왜냐하면 함수를 여러 번 호출하더라도 스택을 정리하는 코드는 함수 끝의 접미에 딱 한번만 작성되기 때문이다. 반면 __cdecl은 호출원이 스택을 정리하므로 호출할 때마다 정리 코드가 반복되어 프로그램 크기가 조금 더 커진다.


    또 다른 중요한 차이점은 가변 인수 함수를 만들 수 있는가 아닌가 하는 점이다. __stdcall은 함수가 직접 스택을 정리하기 때문에 가변 인수 함수를 지원하지 않는다. 함수 접미에 스택 정리 코드를 작성하려면 인수의 총 크기를 미리 알아야 하는데 가변 인수 함수는 전달되는 인수 개수가 가변이므로 이 크기가 고정적이지 않아 접미에서 스택을 직접 정리할 수 없다.

    이에 비해 __cdecl은 함수가 스택을 정리할 책임이 없으며 호출원이 함수를 부를 때마다 스택을 정리한다. 함수를 호출하는 쪽에서는 인수를 몇개나 전달했는지 알 수 있으므로 실제 전달한 인수 크기만큼 스택을 정리할 수 있다. 그래서 printf나 scanf같은 가변 인수를 지원하는 함수는 모두 __cdecl 호출 규약을 사용한다. 또한 윈도우즈 API 함수의 기본 호출 규약은 __stdcall이지만 wsprintf는 예외적으로 __cdecl로 작성되어 있다.

    호출 규약 중 호출원이 스택을 정리하는 것은 __cdecl밖에 없으며 그래서 가변 인수를 지원할 수 있는 호출 규약도 __cdecl이 유일하다. 가변 인수 함수를 만드려면 반드시 __cdel 호출 규약을 사용해야 한다.

    __fastcall
    다음은 __fastcall 호출 규약을 테스트해 보자. 함수 정의부를 int __fastcall Add(int a, int b)로 수정하기만 하면 된다. 호출부의 코드는 다음과 같다.

     

    mov edx,2

    mov ecx,1

    call Add

    result=eax

     

    __fastcall은 인수 전달을 위해 edx, ecx 레지스터를 사용하는데 두 개의 인수를 차례대로 edx, ecx에 대입했다. 만약 인수가 둘 이상이면 세 번째 이후의 인수는 __cdecl과 마찬가지로 스택에 밀어넣을 것이다. 인수 전달을 위해 스택을 쓰지 않고 레지스터를 우선적으로 사용하므로 인수 전달 속도가 빨라진다는 이점이 있다. 함수의 코드는 다음처럼 작성된다.

     

    push ebp

    mov ebp,esp

    sub esp,14h

    mov [ebp-8],edx       // 첫번째 인수를 지역 변수로

    mov [ebp-4],ecx       // 두번째 인수를 지역 변수로

    mov eax,[ebp-4]

    add eax,[ebp-8]

    mov [ebp-0ch],eax        // c는 세번째 지역 변수가 된다.

    mov exa,[ebp-0ch]

    mov esp,ebp

    pop ebp

    ret

     

    edx, ecx 레지스터를 통해 전달받은 인수 둘을 순서대로 지역 변수 영역에 복사한 후 사용하는데 어차피 인수도 지역 변수의 일종이므로 이렇게 해도 별 상관이 없다. 스택 정리는 함수가 하는데 Add 함수의 경우 인수가 두 개 뿐이므로 인수 전달을 위해 스택을 사용하지 않았으며 그래서 정리할 내용이 없다. 만약 인수가 세 개라면 제일 끝의 ret는 ret 4가 될 것이다.

    레지스터는 스택보다 훨씬 더 빠르게 동작하기 때문에 __fastcall은 이름값대로 호출 속도가 빠르다. 대신 이식성에 불리하다는 단점이 있다. 이 호출 규약은 ecx, edx 레지스터를 사용하도록 되어 있는데 이 두 레지스터가 모든 CPU에 공통적으로 존재하는 것이 아니기 때문이다. 그래서 윈도우즈는 이 호출 규약을 지원하기는 하지만 사용하지는 않는다. 볼랜드의 델파이가 __fastcall을 사용한다.

    __thiscall
    thiscall은 클래스의 멤버 함수에 대해서만 적용되는데 ecx로 객체의 포인터(this)가 전달된다는 것이 특징이며 나머지 규칙은 __stdcall과 동일하다. 예외적으로 가변 인수를 사용하는 멤버 함수는 __cdecl로 작성되며 이때 this는 스택의 제일 마지막에(그러므로 첫 번째 인수로) 전달된다.

    이 호출 규약은 컴파일러가 멤버 함수에 대해서만 특별히 적용하는 것이므로 일반 함수에는 이 호출 규약을 적용할 수 없다. thiscall은 이 호출 규약의 이름일 뿐 키워드가 아니기 때문에 함수 원형에 thiscall이라고 쓸 수도 없다.

    __naked
    __naked 호출 규약은 컴파일러가 접두, 접미를 작성해 주지 않는 호출 규약이다. 스택 프레임의 상태 보존을 위해 컴파일러가 어떤 코드도 작성하지 않으므로 접두, 접미는 사용자가 직접 작성해야 한다. 스택은 어셈블리 수준에서만 다룰 수 있으므로 인라인 어셈블리를 사용해야 하며 제약점도 많기 때문에 일반적인 목적으로는 사용되지 않는다.

    이 호출 규약이 반드시 필요한 경우는 C/C++이 아닌 언어에서 호출하는 함수를 작성할 때이다. 예를 들어 어셈블리에서는 인수 전달에 스택을 쓰지 않고 범용 레지스터만으로도 인수를 전달할 수 있다. 이런 경우는 C컴파일러가 만들어주는 접두, 접미가 불필요하다. 또한 속도가 지극히 중요한 디바이스 드라이버를 작성할 때도 이 호출 규약을 사용한다. __naked 호출 규약을 사용하려면 함수의 정의부에 __declspec(naked)를 적어주면 된다.

    여기서 알아본 호출 규약 외에도 __pascal, __fortran, __syscall 이라는 호출 규약이 있었으나 지금은 지원되지 않는다. 비주얼 C++은 과거와의 호환성을 위해 이 단어를 키워드로 인정하기는 하지만 실제로 사용할 경우 에러로 처리한다.

    이상으로 다섯 가지의 호출 규약에 대해 정리했는데 실제로 사용되고 사용자가 지정할 수 있는 호출 규약은 현실적으로 __cdecl, __stdcall 두 가지밖에 없는 셈이다.

    라.호출 규약 불일치
    컴파일러는 함수의 호출 규약에 맞게 스택 프레임을 작성하고 관리한다. __cdecl을 쓰면 이 규약에 맞게 호출원이 스택을 정리하도록 컴파일할 것이며 __stdcall을 쓰면 함수가 스택을 정리하도록 컴파일할 것이다. 호출 규약에 따라 내부 코드가 약간 달라지고 무시해도 좋을 정도의 미세한 속도, 크기 차이가 발생하지만 실행 결과는 동일하다.

    그래서 함수의 호출 규약을 어떤 것으로 쓸 것인가는 고민할만한 문제가 아니다. 컴파일러가 지정한 호출 규약대로 함수를 컴파일하고 이 함수를 불러주는 코드도 만들어 주기 때문이다. 그러나 아주 특수한 경우 호출원과 함수의 호출 규약이 달라질 수도 있다. 어떤 때 이런 불일치가 발생할 수 있는가 하면 함수를 작성한 언어와 호출하는 언어가 다를때, 분리된 DLL에 있는 함수를 호출할 때이다.

    함수를 C/C++로 작성했고 부르는 쪽도 C/C++이며 같은 모듈 내에서 호출한다면 이런 불일치가 발생할 리가 없다. 함수의 호출 규약과 다르게 함수를 호출하면 어떻게 되는지 실험을 해 보자. 한 예제에서 이런 상황을 만들어 보려면 함수 포인터로 호출 규약을 억지로 다르게 만들어 놓고 호출하면 된다.

     

    예 제 : WrongCall
     

    #include <Turboc.h>

     

    int __cdecl Add(int a, int b)

    {

       int c,d,e;

       c=a+b;

       return c;

    }

     

    void main()

    {

       int result;

       int (__stdcall *pf)(int,int);

       pf=(int (__stdcall *)(int,int))Add;

       result=pf(1,2);

       printf("result = %d\n",result);

    }

     

    Add 함수는 __cdecl로 선언되었으며 이 호출 규약은 호출원이 스택을 정리하도록 되어 있으므로 자신이 스택을 정리하지 않을 것이다. main에서는 Add를 바로 호출하지 않고 __stdcall 호출 규약을 사용하는 함수 포인터 pf로 강제 대입한 후 호출했다. 단순히 pf=Add라고 대입하면 함수와 함수 포인터의 호출 규약이 맞지 않아 대입되지 않으므로 강제로 캐스팅해서 대입해야 한다.

    pf는 __stdcall 호출 규약을 쓰는 함수에 대한 함수 포인터이므로 이 포인터로 함수를 호출하면 main은 스택을 정리하지 않는다. 왜냐하면 pf가 가리키는 함수는 스스로 스택을 정리한다고 생각하기 때문이다. 이렇게 되면 인수가 전달되기는 하지만 아무도 스택을 정리하지 않기 때문에 Add가 리턴된 직후 스택은 원래 상태로 복구되지 않는다.

    스택에 저장되는 정보들은 보통 아주 중요한 정보들이며 이 정보가 손상되면 프로그램의 동작은 예측할 수 없다. 스택 포인터를 잃어버린 프로그램은 엉뚱한 값을 레지스터에 대입하거나 복귀 번지를 잘못 찾아 임의의 메모리 번지로 점프하여 폭주해 버린다. 결국 이 프로그램은 Add가 리턴되는 즉시 다운될 확률이 99.9629%나 된다.

    이번에는 반대의 경우를 생각해 보자. Add 함수를 __stdcall 호출 규약으로 작성하고 main에서 pf를 __cdecl 호출 규약을 쓰는 함수 포인터로 선언한 후 pf로 Add를 호출하면 어떻게 될까? 이 경우 스택은 이중으로 정리될 것이고 스택이 깨지기 때문에 마찬가지로 이 프로그램은 다운된다.

    댓글