함수를 호출하는데는 몇 가지 지켜야 할 약속이 있습니다.
변수를 어떻게 넘겨줄 것인가, 리턴값의 반환,
인수로 전달한 것을 어떻게 정리할 것인가 등이 약속되어 있어야 합니다.
호출하는 함수(caller)와 호출 당하는 함수(callee) 사이에는
이 약속이 지켜져야 하는 것은 당연합니다.
만약 이 약속이 안 맞춰지면 거의 100% 프로그램이 죽는다고 보면 됩니다.
프로그램이 실행이 되면 프로세스라 칭해지며, 메모리가 할당이 됩니다.
이렇게 메모리가 할당되면 힙 영역과 스택 영역이 존재하게 되는데,
힙은 낮은 번지에서 높은 번지로 자라나고,
스택은 높은 번지에서 낮은 번지로 자라난다고 합니다.
힙과 스택의 사이에는 자유 영역이 존재하고
이 자유영역이 겹치게 되면 메모리 부족이 발생합니다.
함수를 호출하면 전달되는 인자나 내부의 변수를 저장하기 위해서 스택 영역이 잡힙니다.
recursive하게 함수를 구현했을 때,
Base를 제대로 정의하지 않아서 무한으로 돌게 되었을 때
Stack Overflow를 내뱉는 경우가 있을 것인데
이것은 스택 영역을 계속 잡아나가기 때문에 발생하는 오류입니다.
이 스택의 현재 위치를 가리키고 포인터가 esp라는 것입니다.
앞에 붙은 e는 확장(Extended)의 뜻이고 sp는 Stack Pointer 입니다.
스택이 가지는 동작은 Push와 Pop입니다.
Push는 말그대로 스택에 밀어넣는것이고, Pop은 빼내는 것입니다.
Push와 Pop은 esp의 값을 변경해서 현재 스택의 위치값을 변경시켜 줍니다.
함수가 호출되면 전달 인수, 함수 종료 후에 돌아갈 복귀 번지, 지역 변수 등을 보관해야 합니다.
스택에 이런 정보가 저장되는데 이것을 스택 프레임이라고 합니다.
대략 이런 구조라고 생각하고 함수 호출 규약을 보도록 하겠습니다.
함수 호출 규약의 필요성은 이미 위에 말했고,
함수 호출 규약은 다음과 같은 것이 존재합니다.
__cdecl, __stdcall, __fastcall, thiscall, naked 입니다.
호출 규약 | 인수 전달 | 스택 정리 | 이름 규칙 |
__cdecl | 오른쪽부터 | 호출원 | _함수명 |
__ stdcall | 오른쪽부터 | 함수 | _함수명@인수크기 |
__fastcall | ECX, EDX에 처음 2개 전달, 나머지는 오른쪽 먼저 | 함수 | @함수명@인수크기 |
thiscall | 오른쪽부터, this를 ECX에 저장 | 함수 | C++ 이름 규칙을 따름 |
naked | 오른쪽부터 | 함수 | 없음 |
위의 표는 Winapi 사이트에 존재하는 것을 가져왔습니다.
일반적으로 사용하는 것은 __cdecl과 __stdcall입니다.
간단하게 설명하면 __cdecl은 함수에서 사용하는 변수들을 호출원이 정리한다는 것입니다.
즉, 함수 자체에서 정리하는 것이 아니라 호출한 쪽에서 정리를 한다는 것이죠.
__cdecl이 사용되는 경우는 가변 인수가 전달될 때 입니다.
printf 같은 함수가 가변 인수가 전달되는 경우인데,
printf는 몇 개의 인수가 전달되는지 정해져 있지 않는 함수입니다.
몇 개가 전달되는 지는 알 수가 없기 때문에 호출하는 쪽에서 판단하는 편이 유용합니다.
즉, 호출한 쪽에서 관리하는 편이 스택을 정리할 때 더 유용하기 때문입니다.
스택의 돌아갈 기점을 호출하는 쪽이 관리해서 돌아갈 때 호출하는 쪽이 처리하는 것입니다.
그리고 __stdcall은 정리를 호출되는 쪽(caleee)에서 합니다.
이것은 가장 일반적인 함수 호출 규약(default)으로 되어 있습니다.
정리하자면 __cdecl은 가변 인수를 이용할 때 사용되고, __stdcall은 범용으로 사용됩니다.
그리고 fastcall은 MSDN에서 다음과 같이 정의되어 있습니다.
This calling convention is not supported.
fastcall은 지원이 안된다는 의미입니다.
그리고 thiscall은 class에 적용되는 것입니다.
일반적으로 멤버 변수는 객체간에 공유가 안되지만, 함수는 공유가 됩니다.
변수는 객체마다 값이 다르지만 함수의 동작은 모두 같기 때문입니다.
어떤 객체가 함수를 호출했는지를 호출당한 함수는 알 수가 없습니다.
물론 프로그램을 만들때는 class명 A, B; 이런식으로 만들어서
A.함수명() 형식이기 때문에 알 수 있습니다.
하지만 컴파일러는 이것을 알지 못합니다.
그래서 호출한 객체의 정보를 ECX 레지스터에 넣습니다.
어떤 객체가 호출했는지 알 수 있게 해주기 위해서입니다.
객체가 메소드를 호출할 때는 알아서 컴파일러가 이런 호출 규약을 사용합니다.
그래서 우리가 일반적으로 사용할 수 있는 것은 __cdecl과 __stdcall 밖에 없는 것입니다.
그리고 naked는 일반적인 용도로는 사용되지 않습니다.
MSDN에는 호출 규약이 다음과 같이 정리되어 있습니다.
Member name | Description |
Cdecl | The caller cleans the stack. This enables calling functions with varargs, which makes it appropriate to use for methods that accept a variable number of parameters, such as printf. |
FastCall | This calling convention is not supported. |
StdCall | The callee cleans the stack. This is the default convention for calling unmanaged functions with platform invoke. |
ThisCall | The first parameter is the this pointer and is stored in register ECX. Other parameters are pushed on the stack. This calling convention is used to call methods on classes exported from an unmanaged DLL. |
Winapi | This member is not actually a calling convention, but instead uses the default platform calling convention. For example, on Windows the default is StdCall and on Windows CE .NET it is Cdecl. |
사실 일반적인 프로그래밍에는 이런 호출 규약을 정확히 알 필요는 없을지도 모릅니다.
하지만 좀 더 저수준의 디버깅을 하거나 하면 유용할 수 있습니다.
'Programming > C&CPP' 카테고리의 다른 글
함수 오버로딩(overloading)과 오버라이딩(overriding) (0) | 2014.12.04 |
---|---|
다형성과 가상함수 (0) | 2014.12.03 |
한글 윈도우즈, Visual C++ 한글 처리 방식 (0) | 2014.12.03 |
C++ 클래스 상속 (0) | 2014.12.01 |
C++ 클래스 생성자에서 초기화 리스트 사용해야하는 경우 (0) | 2014.11.24 |