반응형

Calling Convention

 

인수 전달 및 명명 규칙

인수 전달 및 명명 규칙Argument Passing and Naming Conventions 이 문서의 내용 --> Microsoft 전용Microsoft Specific Microsoft C++ 컴파일러 인수를 전달 하기 위한 규칙을 지정 하 고 함수와 호출자 간에 값을 반환할 수 있습니다.The Microsoft C++ compilers allow you to specify conventions for passing arguments and retu

docs.microsoft.com

더보기

인수 전달 및 명명 규칙

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 레지스터에 저장된 다음 스택에 역순으로(오른쪽에서 왼쪽으로) 푸시합니다.

 

 

사용되지 않는 호출 규칙

사용되지 않는 호출 규칙Obsolete Calling Conventions 이 문서의 내용 --> Microsoft 전용Microsoft Specific __Pascal, __fortran및 __syscall 호출 규칙은 더 이상 지원 되지 않습니다.The __pascal, __fortran, and __syscall calling conventions are no longer supported. 지원되는 호출 규칙 및 적절한 링커 옵션 중 하나를 사용하

docs.microsoft.com

더보기

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 호출 규칙

ABI 기본 x64 호출 규칙의 세부 정보입니다.

docs.microsoft.com

더보기

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에 전달됩니다.
다른 매개 변수는 스택에 전달됩니다.

사용자용 자료형
(8, 16, 32, 64 비트 내)
그리고 __m64

처음 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:

  1. In functions where the documented purpose of the given function is to modify the nonvolatile FpCsr flags.

  2. 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 software conventions

거두절미하고 위 내용을 쉽게 설명하자면 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(123);
    //Func2(1, 2, 3);
    Func3(123);
    Func4(123);
    //Func5(1, 2, 3);
    Func6(123);
 
 
    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

__cdecl__cdecl 이 문서의 내용 --> __cdecl 는 C 및 C++ 프로그램에 대 한 기본 호출 규칙입니다.__cdecl is the default calling convention for C and C++ programs. 스택은 호출자에 의해 정리 되기 때문에 vararg 함수를 수행할 수 있습니다.Because the stack is cleaned up by the caller, it can do vararg functions. __C

docs.microsoft.com

더보기

__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.

요소 구현
인수 전달 순서 오른쪽에서 왼쪽
스택 유지 관리 책임 호출하는 함수가 스택에서 인수를 꺼냅니다.
이름 데코레이션 규칙

밑줄 문자 (_)는 경우를 제외 하 고 이름에 접두사가
_C 링크를 사용 하는 cdecl 함수 내보내집니다.

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

Argument Passing and Naming Conventions
Keywords

__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은 가변인자를 가질 수 있게 된다. 물론, 용량 일부분을 포기하고... 그 용량차이는... 워낙 미량이라 잘 보이지는 않는다. 하지만 분명 코드 한 줄씩 더쓰이는 만큼이겠다.

 

 

esp 포탈~

 

 

__stdcall

 

__stdcall

__stdcall__stdcall 이 문서의 내용 --> __Stdcall 호출 규칙은 Win32 API 함수를 호출 하는 데 사용 됩니다.The __stdcall calling convention is used to call Win32 API functions. 호출 수신자가 스택을 정리 하므로 컴파일러는 __cdecl함수를 vararg 합니다.The callee cleans the stack, so the compiler makes vararg fun

docs.microsoft.com

더보기

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

Argument Passing and Naming Conventions
Keywords

 

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(1234);
    Func3(1234);
 
    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줄... 인자의 스텍을 삭제한다.

 

 

다음글

https://mtding00.tistory.com/8

반응형

'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
Posted by Lotus1031
,