함수 호출 규약, Function Calling Convention (1/2)
함수의 호출을 구현하기 위해서 필요한 것에는 어떤 것들이 있을까? 플랫폼이나 언어에 따라서 다를 수 있겠지만, 아마도 기본적으로는 실행할 함수의 코드 위치를 갖고 있는 포인터나 그 값을 가져올 수 있는 함수 이름, 스레드가 현재 처리하고 있는 함수의 정보를 저장할 공간, 함수의 실행이 끝난 뒤 리턴 값을 돌려 받을 공간, 함수의 실행에 필요한 인자를 넘겨줄 공간 등이 필요할 것이다.

이 때 함수에서 다른 함수로 실행이 넘어간 뒤에 다시 원래의 함수Caller로 돌아오려면 호출자가 사용하던 데이터를 복구할 필요가 있는데, 함수를 연달아서 호출한 경우 나중에 호출된 함수Callee의 데이터는 항상 호출한 함수의 데이터보다 먼저 제거된다. 함수 호출에 관련된 데이터의 이런 특성이 스택이 갖는 후입선출LIFO, Last-In First Out 속성과 들어맞기 때문에 함수 호출 과정에서 현재 함수에서 사용하는 로컬 변수나 호출될 함수에게 전달할 인자를 저장하기 위한 데이터 구조에는 스택Stack을 주로 사용하게 된다.

함수 호출 과정에 있어서 기계어로 번역된 코드가 호출될 함수에 인자를 전달하고 리턴값을 받아오는 방식은 CPU 아키텍처Architecture에 따라서 매우 다를 것이고, 심지어 같은 CPU 아키텍처 상의 같은 언어라도 다른 컴파일러를 사용한다면 다를 수도 있을 것이다. 하지만 인자를 스택에 넣을 것인가 메모리에 전달할 것인가, 뒤에 오는 인자를 먼저 넣을 것인가, 나중에 넣을 것인가, 하나의 레지스터에 들어가지 않는 크기의 데이터를 반환하는 경우에 스택에 넣어서 반환할 것인가 등, 이런 여러 가지 함수 호출에 필요한 조건들이 컴파일러 벤더에 따라서 달라진다면 다른 컴파일러가 생성한 바이너리Executable Binary 간의 호환성을 보장할 수가 없게 된다. 예를 들어 A사의 컴파일러가 생성한 라이브러리를 B사의 링커로 링크해서 라이브러리 내의 함수를 호출한다는 것을 상상하기 어려울 것이다.

사실 함수를 호출하는 방법에 어떤 규칙이 없다면 바이너리 단위의 호환성을 논의하기 이전에 운영체제OS, Operating System가 어떤 바이너리도 정상적으로 실행되는 것을 보장할 수 없을 것이다. 예를 들어 OS는 C++로 작성된 코드의 엔트리 함수 void main(int, char* [])을 호출하면서 int 인자를 스택에 먼저 넣고, char* [] 인자를 나중에 넣었는데, 호출된 main() 함수 쪽에서는 int 인자를 먼저 꺼내고 char* [] 인자를 나중에 꺼냈다면 무슨 일이 생길 것인가?

따라서 함수를 호출하는 데 있어서 어떤 규칙을 만드는 것은 서로 다른 컴파일러, 더 나아가 서로 다른 언어 간에 상호 작용성을 높이고, 빌드Build된 바이너리의 재사용성을 높이는 데에 있어서 필수적이라고 할 수 있는 것이다. 이렇게 함수 호출을 위해서 밟는 절차를 정해둔 것을 함수 호출 규약Function Calling Convention이라고 한다. 여기에서는 게임 제작 환경으로 보편적이라고 간주할 수 있는 인텔 아키텍처IA-32와 마이크로소프트 윈도우Microsoft Windows 상의 함수 호출 규약을 중점적으로 살펴보자.

함수 호출 과정과 스택 프레임Stack Frame
함수 호출 규약을 이해하기 위해서는 먼저 스택 프레임에 대한 이해가 필요하다. 스택 프레임은 호출된 함수가 실행되는 동안에 필요한 로컬 변수 등의 정보나 호출한 함수의 실행에 필요한 정보Context가 손실되지 않도록 스택에 저장할 때 취하는 구조Data Structure를 의미한다. 이렇게 저장해야 하는 정보에는 함수의 실행이 종료된 뒤에 리턴할 주소나, 로컬 변수, 자신을 호출한 함수의 스택 프레임 위치, 레지스터 같은 기계 상태, 예외 처리 리스트 등이 포함된다. 스택 프레임은 기본적으로 함수가 호출될 때마다 새로 설정되며, 프레임 포인터Frame Pointer 혹은 베이스 포인터Base Pointer를 통해서 참조할 수 있다. 프레임 포인터는 현재 실행되는 함수의 스택 프레임이 스택 상의 어느 주소 위치하고 있는지 가리키는 값으로, 인텔 아키텍처에서는 ebp 레지스터에 저장된다.

실제로 함수가 호출되는 과정을 통해서 스택 프레임의 구성에 대해서 자세히 살펴보도록 하자.

1. 인자를 스택에 집어 넣는다
호출된 함수가 인자를 받을 수 있도록 스택에 집어 넣는다. cdecl 호출 규약에서는 오른쪽 인자부터 스택에 집어 넣도록 되어 있다. 호출 규약에 따라서 인자를 평가하고 스택에 집어넣는 순서가 다를 수 있기 때문에 코드를 작성할 때 인자가 평가되는 순서에 의존하는 코드를 작성해서는 안 된다.

// int main(int argc, char* argv[])
    x = function(a, b, c);
0041117E mov    eax,dword ptr [c]
00411181 push   eax    // 인자 c를 스택에 집어넣는다
00411182 mov    ecx,dword ptr [b]
00411185 push   ecx    // 인자 b를 스택에 집어넣는다
00411186 mov    edx,dword ptr [a]
00411189 push   edx    // 인자 a를 스택에 집어넣는다
표 1) 인자를 스택에 집어 넣는 어셈블리 코드와 스택의 상태


2. 함수를 호출한다
call 명령이 실행되면 CPU는 인스트럭션 포인터Instruction Pointer인 eip 레지스터, 즉 함수 호출이 종료된 뒤 돌아와야 할 코드의 주소를 스택에 집어 넣은 뒤, call 명령 뒤에 이어지는 인자를 eip 레지스터에 로드한다. 스택에 집어넣은 eip 레지스터 값은 이후 함수가 종료된 뒤에 돌아올 코드 위치가 된다.

0041118A call   function (4110A5h)
표 2) 함수를 호출하는 어셈블리 코드와 스택의 상태


3. 프레임 포인터를 설정한다
호출된 함수는 호출한 함수의 프레임 포인터를 스택에 보관하고, 그 시점의 스택 포인터 esp를 프레임 포인터로 설정한다. 설정된 프레임 포인터는 함수 호출 인자나 로컬 변수 등 스택 프레임 정보를 참조하는 기준값으로 사용하게 된다.

즉 이 시점에서 함수 인자 a, b, c는 각각 [ebp+8], [ebp+12], [ebp+16]으로 참조할 수 있게 되며, 마찬가지로 [ebp+0]은 이전 함수의 프레임 포인터, [ebp+4]는 리턴 주소가 된다. 이전 함수에서도 같은 방식으로 자신의 스택 프레임을 참조하기 때문에 이전 함수의 프레임 포인터를 보관해 주어야만 함수에서 돌아간 뒤에 원래 함수의 실행을 계속할 수 있다.

// int function(int a, int b, int c)
int function(int a, int b, int c)
{
00411140 push   ebp    // 호출한 함수의 프레임 포인터를 저장한다
00411141 mov    ebp,esp  // 현재 스택 위치를 프레임 포인터로 설정한다

표 3) 프레임 포인터를 재설정하는 어셈블리 코드와 스택의 상태


4. 로컬 변수를 위한 공간을 할당한다
호출된 함수에서 사용할 로컬 변수를 저장할 공간을 준비한다. 이 작업은 메모리의 얼라인먼트 단위(인텔 아키텍처에서는 4바이트)에 맞춰 일어나고, 단순히 스택 포인터를 이동시키는 것으로 완료된다. 즉 이 함수에서 로컬 변수는 ebp와 esp 사이의 공간을 사용하게 되는 것으로, 프레임 포인터를 기준으로 첫 번째 로컬 변수는 [ebp-4], 두 번째 로컬 변수는 [ebp-8]과 같은 식으로 참조하게 된다.

00411143 sub    esp,40h   // 로컬 변수 공간으로 64 바이트를 할당한다
표 4) 로컬 변수 공간을 할당하는 어셈블리 코드와 스택의 상태


5. 호출한 함수의 실행 상태를 보존한다
호출된 함수에서 범용 레지스터를 사용하는 경우, 이전 함수에서 범용 레지스터를 저장해 둔 값이 지워져서 원래 함수로 돌아간 뒤에 정상적으로 실행을 재개할 수 없게 된다. 안전하게 이전 함수의 실행 내역을 보존하기 위해서 범용 레지스터의 내용을 스택에 보존한다.

00411146 push   ebx    // 호출한 함수의 실행 상태를 보존한다
00411147 push   esi
00411148 push   edi

표 5) 실행 상태를 보존하는 어셈블리 코드와 스택의 상태


6. 함수를 실행한다
함수의 실행에 필요한 준비가 완료되었으므로 실제 함수의 연산을 수행한다.

    return a+b+c;
00411149 mov    eax,dword ptr [ebp+8]
0041114C add    eax,dword ptr [ebp+12]
0041114F add    eax,dword ptr [ebp+16]

표 6) 인자로 주어진 값을 더한 뒤 리턴하는 어셈블리 코드


7. 호출한 함수의 실행 상태를 복구한다
5번 과정에서 저장한 레지스터를 복구한다. 스택을 사용하고 있기 때문에 넣은 순서와 반대의 순서로 꺼내고 있다는 점에 주목할 필요가 있다.

00411152 pop    edi    // 호출한 함수의 실행 상태를 복구한다
00411153 pop    esi
00411154 pop    ebx

표 7) 실행 상태를 복구하는 어셈블리 코드와 스택의 상태


8. 스택을 정리하고, 프레임 포인터를 복구한다
로컬 변수 할당 등으로 사용한 원래대로 복구한다. 이 과정은 로컬 변수 할당 과정과 마찬가지로, 단순히 함수에 진입할 시점의 스택 포인터를 복구하는 것으로 완료된다. 그 후 현재 프레임 포인터 위치에 있는 이전 함수의 프레임 포인터를 복구한다. 이것으로 원래 함수로 돌아가기 위한 준비는 모두 완료된다.

00411155 mov    esp,ebp    // 사용한 스택을 정리한다
00411157 pop    ebp      // 호출한 함수의 프레임 포인터를 복구한다

표 8) 프레임 포인터를 복구하는 어셈블리 코드와 스택의 상태


9. 함수로부터 돌아간다
ret 명령을 수행하면 스택에 저장해 둔 리턴 주소를 꺼내서 eip에 로드하게 된다. 이로서 호출된 함수에서 완전히 빠져 나와 원래 함수로 돌아온 상태가 된다.

00411158 ret
표 9) 함수에서 리턴하는 어셈블리 코드와 스택의 상태


10. 스택에 집어넣은 인자를 정리한다
cdecl 호출 규약에서는 호출자가 인자를 정리해야 하므로, 스택 포인터에 (인자당 4 바이트) * (3 개의 인자) = (12 바이트) 만큼 스택 포인터를 이동시켜서 인자를 꺼내는 처리를 수행한다. 이것으로 함수 호출 과정이 끝난다. 리턴 값을 갖는 함수를 호출했다면 eax 레지스터에는 함수 호출의 결과값이 들어있게 된다.

0041118F add    esp,0Ch         // 스택에 넣은 인자를 꺼낸다
00411192 mov    dword ptr [x],eax    // 리턴 값을 x에 저장한다

표 10) 인자를 정리하는 어셈블리 코드와 스택의 상태
함수 호출 규약이나 예외 처리를 사용했는가의 여부에 따라서 스택 프레임의 구조는 약간씩 달라질 수 있다. 위의 과정 중, 3~5번에 해당하는 함수 실행 준비 과정을 프롤로그Prolog라고 부르고, 7~9번의 함수 실행을 마무리하는 프롤로그의 반대 과정을 에필로그Epilog라고 부른다.
by 매운맛나리 | 2006/01/15 17:02 | 사전 | 트랙백(6) | 핑백(1) | 덧글(0)
트랙백 주소 : http://beforu.egloos.com/tb/2117375
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Tracked from Confluence: .. at 2014/04/25 15:39

제목 : 어셈블리어 뽀개기
어셈블리어 익히기 어셈블리어를 활용해 간단한 프로그래밍을 직접해봐야 어셈블리어에 대한 감을...more

Tracked from Confluence: .. at 2014/04/28 10:45

제목 : 어셈블리어 뽀개기
어셈블리어 익히기 어셈블리어를 활용해 간단한 프로그래밍을 직접해봐야 어셈블리어에 대한 감을...more

Tracked from Confluence: .. at 2014/04/28 10:51

제목 : 어셈블리어 뽀개기
어셈블리어 익히기 어셈블리어를 활용해 간단한 프로그래밍을 직접해봐야 어셈블리어에 대한 감을...more

Tracked from Confluence: .. at 2014/04/29 10:47

제목 : 어셈블리어 뽀개기
어셈블리어 익히기 어셈블리어를 활용해 간단한 프로그래밍을 직접해봐야 어셈블리어에 대한 감을...more

Tracked from Confluence: .. at 2014/04/29 10:47

제목 : 어셈블리어 뽀개기
어셈블리어 익히기 어셈블리어를 활용해 간단한 프로그래밍을 직접해봐야 어셈블리어에 대한 감을...more

Tracked from Confluence: .. at 2014/05/12 10:48

제목 : OS 1.1 어셈블리어 뽀개기
어셈블리어 익히기 어셈블리어를 활용해 간단한 프로그래밍을 직접해봐야 어셈블리어에 대한 감을...more

Linked at Purewell.BIZ : 임.. at 2007/09/21 15:42

... ary buffer만이 assign이 일어나면 다른 주소를 할당해서 사용하였기 때문에 Heap1 variable과 비슷한 영역을 가진다. 덧글: 괜찮은 링크 발견!! 함수 호출 규약 ... more

:         :

:

비공개 덧글



< 이전페이지 다음페이지 >