Calling Convention
인수 전달 및 명명 규칙
Microsoft C++ 컴파일러 인수를 전달 하기 위한 규칙을 지정 하 고 함수와 호출자 간에 값을 반환할 수 있습니다. 지원되는 모든 플랫폼에서 모든 규칙을 사용할 수 있는 것은 아니며, 일부 규칙은 플랫폼별 구현을 사용합니다. 대부분의 경우 특정 플랫폼에서 지원되지 않는 규칙을 지정하는 키워드 또는 컴파일러 스위치는 무시되며 플랫폼 기본 규칙이 사용됩니다.
x86 플랫폼에서 모든 인수는 전달될 때 32비트로 확장됩니다. 반환 값도 32비트로 확장되며 EDX:EAX 레지스터 쌍에서 반환되는 8바이트 구조체를 제외하고 EAX 레지스터에서 반환됩니다. 더 큰 구조체는 숨겨진 반환 구조체에 대한 포인터로 EAX 레지스터에서 반환됩니다. 매개 변수는 오른쪽에서 왼쪽으로 스택에 푸시됩니다. POD가 아닌 구조체는 레지스터에서 반환되지 않습니다.
컴파일러는 ESI, EDI, EBX 및 EBP 레지스터가 함수에서 사용되는 경우 이러한 레지스터를 저장하고 복원하는 프롤로그 및 에필로그 코드를 생성합니다.
참고
구조체, 공용 구조체 또는 클래스가 값으로 함수에서 반환되는 경우 형식의 모든 정의가 동일해야 하며, 그렇지 않으면 프로그램이 런타임에 실패할 수 있습니다.
사용자 고유의 함수 프롤로그 및 에필로그 코드를 정의 하는 방법에 대 한 자세한 내용은 Naked 함수 호출합니다.
대상을 x64 플랫폼에서 참조 하는 코드의 호출 규칙 기본값에 대 한 자세한 x64 호출 규칙합니다. ARM 플랫폼을 대상으로 하는 코드의 호출 규칙 문제에 대 한 자세한 내용은 일반적인 Visual C++ ARM 마이그레이션 문제니다.
함수를 쓰기 위해선 먼저 함수에 정해진 규칙, 말 그대로 정해진 규칙을 따르며 제작을 해야할 도리(?) 정도로 생각하는 게 맞겠다. 하지만 이 규칙들은 C++이나 실행파일에서 육안으로 판별하기 어렵다. 이 부분은 어셈블리어(Assembly)까지 내려가야 육안으로 확실히 구분이 된다.
C++ 에서 어셈블리어 확인해 본다.
지원하는 규약을 표로 정리해 보았다.
키워드 | 스택 정리 | 매개 변수 전달 |
__cdecl | 호출자 Caller | 매개 변수를 스택에 역순으로(오른쪽에서 왼쪽으로) 푸시합니다. |
__clrcall | N/A | CLR 식 스택에 매개 변수를 순서대로(왼쪽에서 오른쪽으로) 로드합니다. |
__stdcall | 호출 수신자 Callee | 매개 변수를 스택에 역순으로(오른쪽에서 왼쪽으로) 푸시합니다. |
__fastcall | 호출 수신자 Callee | 레지스터에 저장된 다음 스택에 푸시합니다. |
__thiscall | 호출 수신자 Callee | 스텍에 푸시합니다. this 포인터를 ECX에 저장합니다. |
__vectorcall | 호출 수신자 Callee | 레지스터에 저장된 다음 스택에 역순으로(오른쪽에서 왼쪽으로) 푸시합니다. |
The __pascal, __fortran, and __syscall calling conventions are no longer supported. You can emulate their functionality by using one of the supported calling conventions and appropriate linker options.
<windows.h> now supports the WINAPI macro, which translates to the appropriate calling convention for the target. Use WINAPI where you previously used PASCAL or __far __pascal.
더해서 __pascal, __pfortran, __syscall은 더 이상 지원하지 않는다. window.h는 WINAPI 매크로(__stdcall)를 지원하며, 이전에 지원하지 않는 규약에는 __stdcall 을 쓰라고 한다.
하지만 이 모든 규약은 x86에서 적용이 된다. x64에서는 __fastcall이 적용된다.
x64에서의 함수 호출 규칙
x64 calling convention
This section describes the standard processes and conventions that one function (the caller) uses to make calls into another function (the callee) in x64 code.
Calling convention defaults
The x64 Application Binary Interface (ABI) uses a four-register fast-call calling convention by default. Space is allocated on the call stack as a shadow store for callees to save those registers. There's a strict one-to-one correspondence between the arguments to a function call and the registers used for those arguments. Any argument that doesn’t fit in 8 bytes, or isn't 1, 2, 4, or 8 bytes, must be passed by reference. A single argument is never spread across multiple registers. The x87 register stack is unused, and may be used by the callee, but must be considered volatile across function calls. All floating point operations are done using the 16 XMM registers. Integer arguments are passed in registers RCX, RDX, R8, and R9. Floating point arguments are passed in XMM0L, XMM1L, XMM2L, and XMM3L. 16-byte arguments are passed by reference. Parameter passing is described in detail in Parameter Passing. In addition to these registers, RAX, R10, R11, XMM4, and XMM5 are considered volatile. All other registers are non-volatile. Register usage is documented in detail in Register Usage and Caller/Callee Saved Registers.
For prototyped functions, all arguments are converted to the expected callee types before passing. The caller is responsible for allocating space for parameters to the callee, and must always allocate sufficient space to store four register parameters, even if the callee doesn’t take that many parameters. This convention simplifies support for unprototyped C-language functions and vararg C/C++ functions. For vararg or unprototyped functions, any floating point values must be duplicated in the corresponding general-purpose register.Any parameters beyond the first four must be stored on the stack after the shadow store prior to the call. Vararg function details can be found in Varargs. Unprototyped function information is detailed in Unprototyped Functions.
Alignment
Most structures are aligned to their natural alignment. The primary exceptions are the stack pointer and malloc or alloca memory, which are aligned to 16 bytes in order to aid performance. Alignment above 16 bytes must be done manually, but since 16 bytes is a common alignment size for XMM operations, this value should work for most code. For more information about structure layout and alignment, see Types and Storage. For information about the stack layout, see x64 stack usage.
Unwindability
Leaf functions are functions that don't change any non-volatile registers. A non-leaf function may change non-volatile RSP, for example, by calling a function or allocating additional stack space for local variables. In order to recover non-volatile registers when an exception is handled, non-leaf functions must be annotated with static data that describes how to properly unwind the function at an arbitrary instruction. This data is stored as pdata, or procedure data, which in turn refers to xdata, the exception handling data. The xdata contains the unwinding information, and can point to additional pdata or an exception handler function. Prologs and epilogs are highly restricted so that they can be properly described in xdata. The stack pointer must be aligned to 16 bytes in any region of code that isn’t part of an epilog or prolog, except within leaf functions. Leaf functions can be unwound simply by simulating a return, so pdata and xdata are not required. For details about the proper structure of function prologs and epilogs, see x64 prolog and epilog. For more information about exception handling, and the exception handling and unwinding of pdata and xdata, see x64 exception handling.
Parameter passing
The first four integer arguments are passed in registers. Integer values are passed in left-to-right order in RCX, RDX, R8, and R9, respectively. Arguments five and higher are passed on the stack. All arguments are right-justified in registers, so the callee can ignore the upper bits of the register and access only the portion of the register necessary.
Any floating-point and double-precision arguments in the first four parameters are passed in XMM0 - XMM3, depending on position. The integer registers RCX, RDX, R8, and R9 that would normally be used for those positions are ignored, except in the case of varargs arguments. For details, see Varargs. Similarly, the XMM0 - XMM3 registers are ignored when the corresponding argument is an integer or pointer type.
__m128 types, arrays, and strings are never passed by immediate value. Instead, a pointer is passed to memory allocated by the caller. Structs and unions of size 8, 16, 32, or 64 bits, and __m64 types, are passed as if they were integers of the same size. Structs or unions of other sizes are passed as a pointer to memory allocated by the caller. For these aggregate types passed as a pointer, including __m128, the caller-allocated temporary memory must be 16-byte aligned.
Intrinsic functions that don't allocate stack space, and don't call other functions, sometimes use other volatile registers to pass additional register arguments. This optimization is made possible by the tight binding between the compiler and the intrinsic function implementation.
The callee is responsible for dumping the register parameters into their shadow space if needed.
The following table summarizes how parameters are passed:
매개 변수 유형 | 전달 하는 방법 |
부동 소수점 | 처음 4 개의 매개 변수는 XMM0 ~ XMM3에 전달됩니다. 다른 매개 변수는 스택에 전달됩니다. |
정수 |
처음 4 개의 매개 변수는 RCX, RDX, R8, R9에 전달됩니다. |
사용자용 자료형 |
처음 4 개의 매개 변수는 RCX, RDX, R8, R9에 전달됩니다. 다른 매개 변수는 스택에 전달됩니다. |
사용자용 자료형 |
포인터로 전달됩니다. 처음 4개의 매개 변수는 RCX, RDX, R8, R9 내 포인터로 전달됩니다. By pointer. First 4 parameters passed as pointers in RCX, RDX, R8, and R9 |
__m128 |
포인터로 전달됩니다. 처음 4개의 매개 변수는 RCX, RDX, R8, R9 내 포인터로 전달됩니다. By pointer. First 4 parameters passed as pointers in RCX, RDX, R8, and R9 |
Example of argument passing 1 - all integers
모든 정수를 전달하는 인수의 예제
func1(int a, int b, int c, int d, int e); // a in RCX, b in RDX, c in R8, d in R9, e pushed on stack
Example of argument passing 2 - all floats
모든 부동 소수점 수를 전달하는 인수의 예제
func2(float a, double b, float c, double d, float e); // a in XMM0, b in XMM1, c in XMM2, d in XMM3, e pushed on stack
Example of argument passing 3 - mixed ints and floats
혼합된 정수 및 부동 소수점을 전달하는 인수의 예제
func3(int a, double b, int c, float d); // a in RCX, b in XMM1, c in R8, d in XMM3
Example of argument passing 4 -__m64, __m128, and aggregates
혼합된 자료형 및 __m64, __m128을 전달하는 인수의 예제
func4(__m64 a, _m128 b, struct c, float d); // a in RCX, ptr to b in RDX, ptr to c in R8, d in XMM3
Varargs
If parameters are passed via varargs (for example, ellipsis arguments), then the normal register parameter passing convention applies, including spilling the fifth and subsequent arguments to the stack. It's the callee's responsibility to dump arguments that have their address taken. For floating-point values only, both the integer register and the floating-point register must contain the value, in case the callee expects the value in the integer registers.
Unprototyped functions
For functions not fully prototyped, the caller passes integer values as integers and floating-point values as double precision. For floating-point values only, both the integer register and the floating-point register contain the float value in case the callee expects the value in the integer registers.
C++복사
func1(); func2() { // RCX = 2, RDX = XMM1 = 1.0, and R8 = 7 func1(2, 1.0, 7); }
Return values
A scalar return value that can fit into 64 bits is returned through RAX; this includes __m64 types. Non-scalar types including floats, doubles, and vector types such as __m128, __m128i, __m128d are returned in XMM0. The state of unused bits in the value returned in RAX or XMM0 is undefined.
User-defined types can be returned by value from global functions and static member functions. To return a user-defined type by value in RAX, it must have a length of 1, 2, 4, 8, 16, 32, or 64 bits. It must also have no user-defined constructor, destructor, or copy assignment operator; no private or protected non-static data members; no non-static data members of reference type; no base classes; no virtual functions; and no data members that do not also meet these requirements. (This is essentially the definition of a C++03 POD type. Because the definition has changed in the C++11 standard, we don't recommend using std::is_pod for this test.) Otherwise, the caller assumes the responsibility of allocating memory and passing a pointer for the return value as the first argument.Subsequent arguments are then shifted one argument to the right. The same pointer must be returned by the callee in RAX.
These examples show how parameters and return values are passed for functions with the specified declarations:
Example of return value 1 - 64-bit result
Output복사
__int64 func1(int a, float b, int c, int d, int e); // Caller passes a in RCX, b in XMM1, c in R8, d in R9, e pushed on stack, // callee returns __int64 result in RAX.
Example of return value 2 - 128-bit result
Output복사
__m128 func2(float a, double b, int c, __m64 d); // Caller passes a in XMM0, b in XMM1, c in R8, d in R9, // callee returns __m128 result in XMM0.
Example of return value 3 - user type result by pointer
Output복사
struct Struct1 { int j, k, l; // Struct1 exceeds 64 bits. }; Struct1 func3(int a, double b, int c, float d); // Caller allocates memory for Struct1 returned and passes pointer in RCX, // a in RDX, b in XMM2, c in R9, d pushed on the stack; // callee returns pointer to Struct1 result in RAX.
Example of return value 4 - user type result by value
Output복사
struct Struct2 { int j, k; // Struct2 fits in 64 bits, and meets requirements for return by value. }; Struct2 func4(int a, double b, int c, float d); // Caller passes a in RCX, b in XMM1, c in R8, and d in XMM3; // callee returns Struct2 result by value in RAX.
Caller/Callee saved registers
The registers RAX, RCX, RDX, R8, R9, R10, R11 are considered volatile and must be considered destroyed on function calls (unless otherwise safety-provable by analysis such as whole program optimization).
The registers RBX, RBP, RDI, RSI, RSP, R12, R13, R14, and R15 are considered nonvolatile and must be saved and restored by a function that uses them.
Function pointers
Function pointers are simply pointers to the label of the respective function. There are no table of contents (TOC) requirements for function pointers.
Floating-point support for older code
The MMX and floating-point stack registers (MM0-MM7/ST0-ST7) are preserved across context switches. There is no explicit calling convention for these registers. The use of these registers is strictly prohibited in kernel mode code.
FpCsr
The register state also includes the x87 FPU control word. The calling convention dictates this register to be nonvolatile.
The x87 FPU control word register is set to the following standard values at the start of program execution:
Register[bits]Setting
FPCSR[0:6] | Exception masks all 1's (all exceptions masked) |
FPCSR[7] | Reserved - 0 |
FPCSR[8:9] | Precision Control - 10B (double precision) |
FPCSR[10:11] | Rounding control - 0 (round to nearest) |
FPCSR[12] | Infinity control - 0 (not used) |
A callee that modifies any of the fields within FPCSR must restore them before returning to its caller. Furthermore, a caller that has modified any of these fields must restore them to their standard values before invoking a callee unless by agreement the callee expects the modified values.
There are two exceptions to the rules about the non-volatility of the control flags:
-
In functions where the documented purpose of the given function is to modify the nonvolatile FpCsr flags.
-
When it's provably correct that the violation of these rules results in a program that behaves the same as a program where these rules aren't violated, for example, through whole-program analysis.
MxCsr
The register state also includes MxCsr. The calling convention divides this register into a volatile portion and a nonvolatile portion. The volatile portion consists of the six status flags, in MXCSR[0:5], while the rest of the register, MXCSR[6:15], is considered nonvolatile.
The nonvolatile portion is set to the following standard values at the start of program execution:
Register[bits]Setting
MXCSR[6] | Denormals are zeros - 0 |
MXCSR[7:12] | Exception masks all 1's (all exceptions masked) |
MXCSR[13:14] | Rounding control - 0 (round to nearest) |
MXCSR[15] | Flush to zero for masked underflow - 0 (off) |
A callee that modifies any of the nonvolatile fields within MXCSR must restore them before returning to its caller. Furthermore, a caller that has modified any of these fields must restore them to their standard values before invoking a callee unless by agreement the callee expects the modified values.
There are two exceptions to the rules about the non-volatility of the control flags:
-
In functions where the documented purpose of the given function is to modify the nonvolatile MxCsr flags.
-
When it's provably correct that the violation of these rules results in a program that behaves the same as a program where these rules aren't violated, for example, through whole-program analysis.
No assumptions can be made about the state of the volatile portion of MXCSR across a function boundary, unless specifically described in a function's documentation.
setjmp/longjmp
When you include setjmpex.h or setjmp.h, all calls to setjmp or longjmp result in an unwind that invokes destructors and __finally calls. This differs from x86, where including setjmp.h results in __finally clauses and destructors not being invoked.
A call to setjmp preserves the current stack pointer, non-volatile registers, and MxCsr registers. Calls to longjmp return to the most recent setjmp call site and resets the stack pointer, non-volatile registers, and MxCsr registers, back to the state as preserved by the most recent setjmp call.
See also
거두절미하고 위 내용을 쉽게 설명하자면 x64비트의 함수호출규약은 처음 4개의 인자까지는 __fastcall을 디폴트로 본다. 더해 __stdcall, __cdecl 등을 하든 간에 __fastcall로 실행되는 것을 볼 수 있었다. 본인은 이 차이를(x64 체제에선 호출규약이 다르게 적용된다.) 눈으로 직접 보기 위하여 앞서 언급한 6가지의 함수 호출 중 4가지를 시험적으로 뽑아보았다.
원본 코드_
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
int __cdecl Func1(int _x, int _y, int _z)
{
return _x + _y + _z;
}
//int __clrcall Func2(int _x, int _y, int _z)
//{
// return _x + _y + _z;
//}
int __stdcall Func3(int _x, int _y, int _z)
{
return _x + _y + _z;
}
int __fastcall Func4(int _x, int _y, int _z)
{
return _x + _y + _z;
}
//int __thiscall Func5(int _x, int _y, int _z)
//{
// return _x + _y + _z;
//}
int __vectorcall Func6(int _x, int _y, int _z)
{
return _x + _y + _z;
}
int main()
{
Func1(1, 2, 3);
//Func2(1, 2, 3);
Func3(1, 2, 3);
Func4(1, 2, 3);
//Func5(1, 2, 3);
Func6(1, 2, 3);
return 0;
}
|
함수 2번과 5번은 나중에라도 편하게 실험하기 위해 만든 것이니 신경 ㄴㄴ
X86 어셈블러에서 각종 함수호출 차이
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
; Line 77
push 3
push 2
push 1
call ?Func1@@YAHHHH@Z ; //__cdecl [ Func1 ] - 이름은 편의상 수정
add esp, 12 ; 0000000cH
; Line 79
push 3
push 2
push 1
call ?Func3@@YGHHHH@Z ; //__stdcall
; Line 80
push 3
mov edx, 2
mov ecx, 1
call ?Func4@@YIHHHH@Z ; //__fastcall
; Line 82
push 3
mov edx, 2
mov ecx, 1
call ?Func6@@YQHHHH@Z ; //__vectorcall
|
X64 어셈블러에서 각종 함수호출 차이
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
; Line 77
mov r8d, 3
mov edx, 2
mov ecx, 1
call ?Func1@@YAHHHH@Z ; //__cdecl [ Func1 ] - 이름은 편의상 수정
; Line 79
mov r8d, 3
mov edx, 2
mov ecx, 1
call ?Func3@@YAHHHH@Z ; //__stdcall
; Line 80
mov r8d, 3
mov edx, 2
mov ecx, 1
call ?Func4@@YAHHHH@Z ; //__fastcall
; Line 82
mov r8d, 3
mov edx, 2
mov ecx, 1
call ?Func6@@YQHHHH@Z ; //__vectorcall
|
이렇듯 확실하게 x86과 x64가 다른 점을 알 수 있다. 문제는 함수 인자가 4개 이상일 때다. 이 부분은 다음 글에서 __fastcall과의 비교에서 실험하도록 하겠다.
x64에서 float와 __m128의 차이
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
; Line 154
movss xmm2, DWORD PTR __real@40400000
movss xmm1, DWORD PTR __real@40000000
movss xmm0, DWORD PTR __real@3f800000
call ?Func7@@YAHMMM@Z ; Func7 // float __cdecl
; Line 155
movss xmm0, DWORD PTR __real@40c00000
movss DWORD PTR [rsp+40], xmm0
movss xmm0, DWORD PTR __real@40a00000
movss DWORD PTR [rsp+32], xmm0
movss xmm3, DWORD PTR __real@40800000
movss xmm2, DWORD PTR __real@40400000
movss xmm1, DWORD PTR __real@40000000
movss xmm0, DWORD PTR __real@3f800000
call ?Func7x@@YAHMMMMMM@Z ; Func7x // float default convention + 4 < [ parameter count ]
; Line 157
movss xmm2, DWORD PTR __real@40400000
movss xmm1, DWORD PTR __real@40000000
movss xmm0, DWORD PTR __real@3f800000
call ?Func9@@YAHMMM@Z ; Func9 // float __stdcall
; Line 158
movss xmm2, DWORD PTR __real@40400000
movss xmm1, DWORD PTR __real@40000000
movss xmm0, DWORD PTR __real@3f800000
call ?Func10@@YAHMMM@Z ; Func10 // float __fastcall
; Line 160
movss xmm2, DWORD PTR __real@40400000
movss xmm1, DWORD PTR __real@40000000
movss xmm0, DWORD PTR __real@3f800000
call ?Func12@@YQHMMM@Z ; Func12 // float __vectorcall
; Line 167
movaps xmm0, XMMWORD PTR Z$[rbp]
movaps XMMWORD PTR $T3[rbp], xmm0
movaps xmm0, XMMWORD PTR Y$[rbp]
movaps XMMWORD PTR $T2[rbp], xmm0
movaps xmm0, XMMWORD PTR X$[rbp]
movaps XMMWORD PTR $T1[rbp], xmm0
lea r8, QWORD PTR $T3[rbp]
lea rdx, QWORD PTR $T2[rbp]
lea rcx, QWORD PTR $T1[rbp]
call ?Func13@@YAHT__m128@@00@Z ; Func13 // __m128 __cdecl
; Line 168
movaps xmm0, XMMWORD PTR Z$[rbp]
movaps XMMWORD PTR $T9[rbp], xmm0
movaps xmm0, XMMWORD PTR Y$[rbp]
movaps XMMWORD PTR $T8[rbp], xmm0
movaps xmm0, XMMWORD PTR X$[rbp]
movaps XMMWORD PTR $T7[rbp], xmm0
movaps xmm0, XMMWORD PTR Z$[rbp]
movaps XMMWORD PTR $T6[rbp], xmm0
movaps xmm0, XMMWORD PTR Y$[rbp]
movaps XMMWORD PTR $T5[rbp], xmm0
movaps xmm0, XMMWORD PTR X$[rbp]
movaps XMMWORD PTR $T4[rbp], xmm0
lea rax, QWORD PTR $T9[rbp]
mov QWORD PTR [rsp+40], rax
lea rax, QWORD PTR $T8[rbp]
mov QWORD PTR [rsp+32], rax
lea r9, QWORD PTR $T7[rbp]
lea r8, QWORD PTR $T6[rbp]
lea rdx, QWORD PTR $T5[rbp]
lea rcx, QWORD PTR $T4[rbp]
call ?Func13x@@YAHT__m128@@00000@Z ; Func13x // __m128 default convention + 4 < [ parameter count ]
; Line 170
movaps xmm0, XMMWORD PTR Z$[rbp]
movaps XMMWORD PTR $T12[rbp], xmm0
movaps xmm0, XMMWORD PTR Y$[rbp]
movaps XMMWORD PTR $T11[rbp], xmm0
movaps xmm0, XMMWORD PTR X$[rbp]
movaps XMMWORD PTR $T10[rbp], xmm0
lea r8, QWORD PTR $T12[rbp]
lea rdx, QWORD PTR $T11[rbp]
lea rcx, QWORD PTR $T10[rbp]
call ?Func15@@YAHT__m128@@00@Z ; Func15 // __m128 __stdcall
; Line 171
movaps xmm0, XMMWORD PTR Z$[rbp]
movaps XMMWORD PTR $T15[rbp], xmm0
movaps xmm0, XMMWORD PTR Y$[rbp]
movaps XMMWORD PTR $T14[rbp], xmm0
movaps xmm0, XMMWORD PTR X$[rbp]
movaps XMMWORD PTR $T13[rbp], xmm0
lea r8, QWORD PTR $T15[rbp]
lea rdx, QWORD PTR $T14[rbp]
lea rcx, QWORD PTR $T13[rbp]
call ?Func16@@YAHT__m128@@00@Z ; Func16 // __m128 __fastcall
; Line 173
movaps xmm2, XMMWORD PTR Z$[rbp]
movaps xmm1, XMMWORD PTR Y$[rbp]
movaps xmm0, XMMWORD PTR X$[rbp]
call ?Func18@@YQHT__m128@@00@Z ; Func18 // __m128 __vectorcall
|
여기서 __m128이란 여러 자료형(int, float 등)을 배열형태로 담은 공용체(union)을 뜻한다. 크기 연산(sizeof) 시 16이 나온다. - 사용방법 -> 포탈
뭐 중요한 건 그게 중요한 것이 아니고, 과연 이 큰 크기의 인자를 어떻게 함수에서 어떻게 받아드리는 가를 이해하는 것이 핵심 과제다.
float형_ ; Line 154을 보면, 값이 바로 xmm에 저장되는 것을 볼 수 있다. 그리고 역시 바로 아래 함수 ; Line 155에서는, float를 6개 받는 함수에선 앞에 4개를 제외한 2개의 인자를 스택에 저장하는 것을 볼 수 있다.
__m128형_ ; Line 167을 보면, xmm0에 XMMWORD 크기의 포인터부터 마련하는 것을 볼 수 있다. 그리고 레지스터에 값을 넣고 그 값을 만들어진 포인터 소스에 더한다(lea연산). 바로 아래 함수 ; Line 168에서는, 포인터 6개를 마련하는 것은 같고 그 6개 중 4개만을 레지스터에 소스값을 더해 받아주고, 나머지는 스택에 저장한다.
이제 아래에선 x86내에 지켜지는 함수호출규약은 각각 무엇이며, x64호출규약 설명이 저렇게 긴 이유를 하나씩 보며 아래쪽에서 따로 어떻게 적용되는지 직접 실험하는 시간을 가져보겠다.
__cdecl
__cdecl is the default calling convention for C and C++ programs. Because the stack is cleaned up by the caller, it can do vararg functions. The __cdecl calling convention creates larger executables than __stdcall, because it requires each function call to include stack cleanup code. The following list shows the implementation of this calling convention.
요소 | 구현 |
인수 전달 순서 | 오른쪽에서 왼쪽 |
스택 유지 관리 책임 | 호출하는 함수가 스택에서 인수를 꺼냅니다. |
이름 데코레이션 규칙 |
밑줄 문자 (_)는 경우를 제외 하 고 이름에 접두사가 Underscore character () is prefixed to names, except when __cdecl functions that use C linkage are exported. |
대/소문자 변환 규칙 | 대/소문자 변환은 수행되지 않습니다. |
참고
For related information, see Decorated Names.
Place the __cdecl modifier before a variable or a function name. Because the C naming and calling conventions are the default, the only time you must use __cdecl in x86 code is when you have specified the /Gv (vectorcall), /Gz (stdcall), or /Gr (fastcall) compiler option. The /Gd compiler option forces the __cdecl calling convention.
On ARM and x64 processors, __cdecl is accepted but typically ignored by the compiler. By convention on ARM and x64, arguments are passed in registers when possible, and subsequent arguments are passed on the stack. In x64 code, use __cdecl to override the /Gvcompiler option and use the default x64 calling convention.
For non-static class functions, if the function is defined out-of-line, the calling convention modifier does not have to be specified on the out-of-line definition. That is, for class non-static member methods, the calling convention specified during declaration is assumed at the point of definition. Given this class definition:
C++
struct CMyClass { void __cdecl mymethod(); };
C++
void CMyClass::mymethod() { return; }
이 코드는
C+
void __cdecl CMyClass::mymethod() { return; }
이 코드와 일치합니다.
For compatibility with previous versions, cdecl and _cdecl are a synonym for __cdecl unless compiler option /Za (Disable language extensions) is specified.
Example
In the following example, the compiler is instructed to use C naming and calling conventions for the system function.
C++
// Example of the __cdecl keyword on function
int __cdecl system(const char *);
// Example of the __cdecl keyword on function pointer
typedef BOOL (__cdecl *funcname_ptr)(void * arg1, const char * arg2, DWORD flags, ...);
See also
__cdecl은 호출자에 의해 가변인자로 받은 스텍 메모리를 해제하는 식이며, 현재 함수호출규약의 기본형(디폴트)로 돼 있다. 물론, 이 기본형을 [프로젝트] -> [속성] -> [C/C++] -> [고급] -> [호출 규칙] 에서 수정할 수는 있다. 여기서! 호출자(Caller)라는 꽤나 말이 어색할 수 있는데 호출자는 함수를 부르는 함수이다. 예를 들면, main()함수에서 A()라는 함수를 실행했다면, A()의 호출자는 main()함수가 되는 것이다. 더불어 이 상황에서 A()의 함수는 main()으로부터 호출 수신자(Callee)가 되는 것이다.
스택 메모리를 호출자가 직접 해제를 하다보니 실행파일이 커진다는 설명이 있는데, 이는 해제하는 코드를 가변 인자 코드를 가진 함수 하나하나 마다 메모리를 해제하는 코드를 넣어야하기 때문이라 설명하고 있다.
__cdecl is the default calling convention for C and C++ programs. Because the stack is cleaned up by the caller, it can do vararg functions. The __cdecl calling convention creates larger executables than __stdcall, because it requires each function call to include stack cleanup code. The following list shows the implementation of this calling convention.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
; Line 88
push 4
push 3
push 2
push 1
call ?Func1@@YAHHHHZZ ; Func1
add esp, 16 ; 00000010H
호출자 부분 - Caller
.........
호출 수신자 부분 - Callee
~~
; Line 9
pop edi
pop esi
pop ebx
add esp, 192 ; 000000c0H
cmp ebp, esp
call __RTC_CheckEsp
mov esp, ebp
pop ebp
ret 0
?Func1@@YAHHHHZZ ENDP ; Func1
|
cdecl의 어셈블리에서 가장 중요하게 보아야할 부분은 7번 줄과 24번줄이다.
7번줄 - [add esp, 16 ; 00000010H] 은 *esp를 16바이트크기만큼의 앞으로 당겨오라는 뜻이다. 16번째 줄부터 함수는 종료시점이 된 것이며, 내부에서 계산 및 데이터를 담아두기 위해 사용한 여러가지 포인터들을 pop과 add 과정을 통해, 다시금 함수 시작지점으로 돌아가고 있는 것이다.
허나, 24번 째 줄을 보면 마지막 인자값만큼 돌아가는 양이 0으로 되어있다. 보통 함수가 인자들을 가질 경우 그 사용한 인자들의 크기만큼 다시 돌아가야 원상태로 복귀하는 것이다. 그 크기와 위치는 이미 7번줄에서 확인할 수 있었다. 이렇게 호출자에서 인자 크기만큼 되돌려 주게 된다면, 호출 수신자는 함수를 실행하고 돌려놓기 위한 스택을 정리하는 과정에서 인자의 크기를 전혀 알 필요가 없게 된 것이다.
이와 같은 이유 때문에, __cdecl은 가변인자를 가질 수 있게 된다. 물론, 용량 일부분을 포기하고... 그 용량차이는... 워낙 미량이라 잘 보이지는 않는다. 하지만 분명 코드 한 줄씩 더쓰이는 만큼이겠다.
__stdcall
The __stdcall calling convention is used to call Win32 API functions. The callee cleans the stack, so the compiler makes vararg functions __cdecl. Functions that use this calling convention require a function prototype.
Syntax
return-type __stdcall function-name[( argument-list )]
Remarks
The following list shows the implementation of this calling convention.
ElementImplementation
요소 | 구현 |
인수 전달 순서 | 오른쪽에서 왼쪽 |
인수 전달 규칙 | 포인터 또는 참조 형식이 전달되지 않는 경우 값으로 전달 |
스택 유지 관리 책임 | 호출된 함수가 스택에서 자신의 인수를 꺼냅니다. |
이름 규칙 |
밑줄()이 이름 앞에 붙습니다. 이름 뒤에는 기호(@)가 오고 그 위에 인수 목록의 바이트 수(10진수)가 옵니다. 따라서 int func( int a, double b )로 선언된 함수는 _func@12로 꾸며집니다. An underscore () is prefixed to the name. The name is followed by the at sign (@) followed by the number of bytes (in decimal) in the argument list. Therefore, the function declared as int func( int a, double b ) is decorated as follows: _func@12 |
대/소문자 변환 규칙 | 없음 |
The /Gz compiler option specifies __stdcall for all functions not explicitly declared with a different calling convention.
For compatibility with previous versions, _stdcall is a synonym for __stdcall unless compiler option /Za (Disable language extensions) is specified.
Functions declared using the __stdcall modifier return values the same way as functions declared using __cdecl.
On ARM and x64 processors, __stdcall is accepted and ignored by the compiler; on ARM and x64 architectures, by convention, arguments are passed in registers when possible, and subsequent arguments are passed on the stack.
For non-static class functions, if the function is defined out-of-line, the calling convention modifier does not have to be specified on the out-of-line definition. That is, for class non-static member methods, the calling convention specified during declaration is assumed at the point of definition. Given this class definition,
C++
struct CMyClass { void __stdcall mymethod(); };
C++
void CMyClass::mymethod() { return; }
이 코드는
C++
void __stdcall CMyClass::mymethod() { return; }
이 코드와 동일합니다.
Example
In the following example, use of __stdcall results in all WINAPI function types being handled as a standard call:
C++복사
// Example of the __stdcall keyword
#define WINAPI __stdcall
// Example of the __stdcall keyword on function pointer
typedef BOOL (__stdcall *funcname_ptr)(void * arg1, const char * arg2, DWORD flags, ...);
See also
WINAPI로 define까지 해놓은 코드이며, 그 특징은 호출자에서 인자 스텍을 넣는 행위만 할뿐 삭제해주지 않는다는 것이다. 따라서 해당 스텍은 함수 내부에서 직접 삭제를 해야하며, 만약 가변인자로 인자가 들어갈 경우 정확한 인자의 크기를 함수 내에서 알 수 없어 함수 실행 전으로 회귀하지 못한다.
즉, __stdcall은 가변인자를 사용할 수 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// 호출자
; Line 90
push 3
push 2
push 1
call ?Func3@@YGHHHH@Z ; Func3
...................
// 호출 수신자
; Line 17
pop edi
pop esi
pop ebx
add esp, 192 ; 000000c0H
cmp ebp, esp
call __RTC_CheckEsp
mov esp, ebp
pop ebp
ret 12 ; 0000000cH
|
호출자 쪽에서는 그저 함수 호출만 하고 종료가 된다. 즉, 스텍을 지우는 코드가 없다. 그 코드는 27줄 ret에서 확인할 수 있는데 12칸 이동... 즉, 12인 이유는 인자 3개를 받았기 때문에 3 * 4가 된다.(크기만큼 이동)
그렇다면 만약 __stdcall 에 가변인자를 사용하면 터질까?
원문_
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
int __cdecl Func1(int _x, int _y, int _z, ...)
{
return _x + _y + _z;
}
int __stdcall Func3(int _x, int _y, int _z,...)
{
return _x + _y + _z;
}
int main()
{
Func1(1, 2, 3, 4);
Func3(1, 2, 3, 4);
return 0;
}
|
어셈블리어_
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
; Line 86
push 4
push 3
push 2
push 1
call ?Func1@@YAHHHHZZ ; Func1
add esp, 16 ; 00000010H
; Line 88
push 4
push 3
push 2
push 1
call ?Func3@@YAHHHHZZ ; Func3
add esp, 16 ; 00000010H
|
7줄과 14줄... 인자의 스텍을 삭제한다.
다음글
'C++' 카테고리의 다른 글
함수 호출 규약 - __thiscall, __clrcall (0) | 2019.12.23 |
---|---|
함수 호출 규약 - __fastcall, __vectorcall (0) | 2019.12.23 |
THREAD를 생성하는 5가지 방법 (0) | 2019.12.23 |
메모리 (0) | 2019.12.23 |
C++이란... (0) | 2019.12.23 |