반응형

이전글

https://mtding00.tistory.com/8

 

__thiscall

 

__thiscall

__thiscall__thiscall 이 문서의 내용 --> Microsoft 전용Microsoft Specific 합니다 __thiscall 멤버 함수에 사용 되 고 호출 규칙에서 사용 하는 기본 호출 규칙 C++ 변수 인수를 사용 하지 않는 멤버 함수입니다.The __thiscall calling convention is used on member functions and is the default calling convention used by C+

docs.microsoft.com

더보기

Microsoft Specific

The__thiscallcalling convention is used on member functions and is the default calling convention used by C++ member functions that do not use variable arguments.Under__thiscall, the callee cleans the stack, which is impossible forvarargfunctions.Arguments are pushed on the stack from right to left, with thethispointer being passed via register ECX, and not on the stack, on the x86 architecture.

One reason to use__thiscallis in classes whose member functions use__clrcallby default.In that case, you can use__thiscallto make individual member functions callable from native code.

When compiling with/clr:pure, all functions and function pointers are__clrcallunless specified otherwise.The/clr:pureand/clr:safecompiler options are deprecated in Visual Studio 2015 and unsupported in Visual Studio 2017.

In releases before Visual Studio 2005, the__thiscallcalling convention could not be explicitly specified in a program, because__thiscallwas not a keyword.

varargmember functions use the__cdeclcalling convention.All function arguments are pushed on the stack, with thethispointer placed on the stack last

Because this calling convention applies only to C++, there is no C name decoration scheme.

On ARM and x64 machines,__thiscallis accepted and ignored by the compiler.

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.

END Microsoft Specific

__thiscall은 x86에서만 작동되어 클래스 멤버함수에서 정의할 수 있는 규약이며, 매개변수들을 스택으로 저장하지만 그것을 this 포인터로 연결하는 형식이다. 그리고 그 this 포인터는 레지스터에 저장된다.

 

무조건 포인터만 레지스터로 저장되는 형태이며, __thiscall을 명시하지 않았을 경우 매개변수들은 __cdecl로 들어간다. __stdcall은 무시하며, __fastcall 혹은 __vectorcall 시 x86에서 2개만 레지스터에 저장하듯 레지스터에 저장되는 포인터를 포함하여 하나의 매개변수를 추가로 저장한 뒤 나머지 인자들은 모두 스택에 저장한다.


__thiscall 예제 코드

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
class A
{
public:
    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 __fastcall Func4x(int _x, int _y, int _z, int _w, int _k, int _l)
    {
        return _x + _y + _z + _w + _k + _l;
    }
    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;
    }
 
public:
    A() {};
    ~A() {};
};
 
int main()
{
    A a;
 
    a.Func1(1234);
    a.Func3(1234);
    a.Func4(123);
    a.Func4x(123456);
    a.Func5(123);
    a.Func6(123);
}
 
 

 

__thiscall x86 어셈블리

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
; Line 44
    push    4
    push    3
    push    2
    push    1
    lea    eax, DWORD PTR _a$[ebp]
    push    eax
    call    ?Func1@A@@QAAHHHHZZ            ; A::Func1
    add    esp, 20                    ; 00000014H // __cdecl
; Line 45
    push    4
    push    3
    push    2
    push    1
    lea    eax, DWORD PTR _a$[ebp]
    push    eax
    call    ?Func3@A@@QAAHHHHZZ            ; A::Func3
    add    esp, 20                    ; 00000014H // __stdcall
; Line 46
    push    3
    push    2
    mov    edx, 1
    lea    ecx, DWORD PTR _a$[ebp]
    call    ?Func4@A@@QAIHHHH@Z            ; A::Func4 // __fastcall
; Line 47
    push    6
    push    5
    push    4
    push    3
    push    2
    mov    edx, 1
    lea    ecx, DWORD PTR _a$[ebp]
    call    ?Func4x@A@@QAIHHHHHHH@Z            ; A::Func4x // __fastcall
; Line 48
    push    3
    push    2
    push    1
    lea    ecx, DWORD PTR _a$[ebp]
    call    ?Func5@A@@QAEHHHH@Z            ; A::Func5 // __thiscall
; Line 49
    push    3
    push    2
    mov    edx, 1
    lea    ecx, DWORD PTR _a$[ebp]
    call    ?Func6@A@@QAQHHHH@Z            ; A::Func6 // __vectorcall
 

가장 핵심이 되는 부분은 [ 34 - ; Line 48 ] 부분이다. 해당 함수는 __thiscall이고 this 포인터로 연결된 스택이기 때문에 함수 호출 후 __cdecl처럼 스택을 지우는 연산을 하지 않는다.



1
2
3
4
5
6
7
8
9
10
11
; Line 29
    pop    edi
    pop    esi
    pop    ebx
    add    esp, 204                ; 000000ccH
    cmp    ebp, esp
    call    __RTC_CheckEsp
    mov    esp, ebp
    pop    ebp
    ret    12                    ; 0000000cH
?Func5@A@@QAEHHHH@Z ENDP                ; A::Func5
 

해당 스택은 마치 __stdcall처럼 함수 구현부에서 포인터를 다시 돌리는 방식으로 처리하였다.

__thiscall x64 어셈블리

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
; Line 44
    mov    DWORD PTR [rsp+32], 4
    mov    r9d, 3
    mov    r8d, 2
    mov    edx, 1
    lea    rcx, QWORD PTR a$[rbp]
    call    ?Func1@A@@QEAAHHHHZZ            ; A::Func1 // __cdecl
; Line 45
    mov    DWORD PTR [rsp+32], 4
    mov    r9d, 3
    mov    r8d, 2
    mov    edx, 1
    lea    rcx, QWORD PTR a$[rbp]
    call    ?Func3@A@@QEAAHHHHZZ            ; A::Func3 // __stdcall
; Line 46
    mov    r9d, 3
    mov    r8d, 2
    mov    edx, 1
    lea    rcx, QWORD PTR a$[rbp]
    call    ?Func4@A@@QEAAHHHH@Z            ; A::Func4 // __fastcall
; Line 47
    mov    DWORD PTR [rsp+48], 6
    mov    DWORD PTR [rsp+40], 5
    mov    DWORD PTR [rsp+32], 4
    mov    r9d, 3
    mov    r8d, 2
    mov    edx, 1
    lea    rcx, QWORD PTR a$[rbp]
    call    ?Func4x@A@@QEAAHHHHHHH@Z        ; A::Func4x // __fastcall
; Line 48
    mov    r9d, 3
    mov    r8d, 2
    mov    edx, 1
    lea    rcx, QWORD PTR a$[rbp]
    call    ?Func5@A@@QEAAHHHH@Z            ; A::Func5 // __thiscall
; Line 49
    mov    r9d, 3
    mov    r8d, 2
    mov    edx, 1
    lea    rcx, QWORD PTR a$[rbp]
    call    ?Func6@A@@QEAQHHHH@Z            ; A::Func6 // __vectorcall
    npad    1
 

기존 x64규약과 같다. 다만 말단에 this 포인터를 레지스터로 넘기는 인자 하나가 더 추가되었을 뿐이다. 정말 단지 그 뿐이며, 4개의 개수가 넘어가면 스택으로 넘어가는 것까지 기존의 것과 완전 동일한 형태다.

 

 

 

 

 

__clrcall

 

__clrcall

__clrcall__clrcall 이 문서의 내용 --> 관리 코드에서만 함수를 호출할 수 있도록 지정합니다.Specifies that a function can only be called from managed code. 관리 코드 에서만 호출 되는 모든 가상 함수에 __clrcall 를 사용 합니다.Use __clrcall for all virtual functions that will only be called from managed code. 그러

docs.microsoft.com

더보기

icrosoft Specific

Specifies that a function can only be called from managed code. Use __clrcall for all virtual functions that will only be called from managed code. However this calling convention cannot be used for functions that will be called from native code.

Use __clrcall to improve performance when calling from a managed function to a virtual managed function or from managed function to managed function through pointer.

Entry points are separate, compiler-generated functions. If a function has both native and managed entry points, one of them will be the actual function with the function implementation. The other function will be a separate function (a thunk) that calls into the actual function and lets the common language runtime perform PInvoke. When marking a function as __clrcall, you indicate the function implementation must be MSIL and that the native entry point function will not be generated.

When taking the address of a native function if __clrcall is not specified, the compiler uses the native entry point. __clrcall indicates that the function is managed and there is no need to go through the transition from managed to native. In that case the compiler uses the managed entry point.

When /clr (not /clr:pure or /clr:safe) is used and __clrcall is not used, taking the address of a function always returns the address of the native entry point function. When __clrcall is used, the native entry point function is not created, so you get the address of the managed function, not an entry point thunk function. For more information, see Double Thunking. The /clr:pure and /clr:safe compiler options are deprecated in Visual Studio 2015 and unsupported in Visual Studio 2017.

/clr (Common Language Runtime Compilation) implies that all functions and function pointers are __clrcall and the compiler will not permit a function inside the compiland to be marked anything other than __clrcall. When /clr:pure is used, __clrcall can only be specified on function pointers and external declarations.

You can directly call __clrcall functions from existing C++ code that was compiled by using /clr as long as that function has an MSIL implementation. __clrcall functions cannot be called directly from functions that have inline asm and call CPU-specific intrinisics, for example, even if those functions are compiled with /clr.

__clrcall function pointers are only meant to be used in the application domain in which they were created. Instead of passing __clrcall function pointers across application domains, use CrossAppDomainDelegate. For more information, see Application Domains and Visual C++.

Example

Note that when a function is declared with __clrcall, code will be generated when needed; for example, when function is called.

C++

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
// compile with: /clr
using namespace System;
int __clrcall Func1() {
   Console::WriteLine("in Func1");
   return 0;
}
 
// Func1 hasn't been used at this point (code has not been generated),
// so runtime returns the adddress of a stub to the function
int (__clrcall *pf)() = &Func1;
 
// code calls the function, code generated at difference address
int i = pf();   // comment this line and comparison will pass
 
int main() {
   if (&Func1 == pf)
      Console::WriteLine("&Func1 == pf, comparison succeeds");
   else
      Console::WriteLine("&Func1 != pf, comparison fails");
 
   // even though comparison fails, stub and function call are correct
   pf();
   Func1();
}
 

Output

1
2
3
4
in Func1
&Func1 != pf, comparison fails
in Func1
in Func1
 

Example

The following sample shows that you can define a function pointer, such that, you declare that the function pointer will only be invoked from managed code. This allows the compiler to directly call the managed function and avoid the native entry point (double thunk issue).

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
// compile with: /clr
void Test() {
   System::Console::WriteLine("in Test");
}
 
int main() {
   void (*pTest)() = &Test;
   (*pTest)();
 
   void (__clrcall *pTest2)() = &Test;
   (*pTest2)();
}
 

See also

Argument Passing and Naming Conventions
Keywords

 

우선 __clrcall을 실행하기 위해서는 여지껏 보았던 규약과는 다른 환경에서 프로젝트를 실행해야한다.

 

using namespace System; in Visual Studio 2013

I am trying to use the Console::SetCursorPosition(int, int) method. When I add the line using namespace System;, as shown in the C++ example from the preceding MSDN documentation, I get the error ...

stackoverflow.com

VS2019 CLR 프로젝트 생성

 

 

본인은 이 방법으로 해결하였다. 그렇다 __clrcall을 실행하기 위해서는 CLR프로젝트에서 함수가 실행되어야하는 것이다.

접은글 내에서도 설명이 되어있듯이 /clr 키워드를 사용한 컴파일은 2015에서는 고려, 2017이후에서는 아예 지원을 하지 않는다. 그러므로 본인이 실행하려 한 환경은 2019이기 때문에, 반드시 새로운 프로젝트 내에서 실행이 되어야 했었다. 이거 알아내려고 하루를ㅠㅠ... 그럼 이쯤되면 그 CLR이 무엇인지 궁금해진다.




 

공통 언어 런타임 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전.

ko.wikipedia.org

핵심 사진

Manage Code는 실행되기 위해 각각의 컴파일러를 거친 뒤 MSIL에서 CLR로 거쳐 Native로 된다. 그럼 Native Code와 Manage Code는 무엇일까? Garbage Collection(쓰레기 수집)이라고 들어봤을 것이다. 바로 동적으로 할당한 메모리를 자동으로 관리해주는 코드들. 대표적으로 C#과 Java 등이 있겠다. 반대로 Native Code는 그냥 쓴 그대로 실행되는 코드들이다. 메모리가 남든 모자르든 터지든 일단 실행하고 보는 코드인 것이다. 굉장히 위험한 코드일 수 있지만 메모리 접근을 할 수 있다는 강력함 때문에 결국 많이 쓰는 코드이다. 그리고 현재 포스팅하는 이 글도 Native Code이다. 맞다. C와 C++ 등을 Native Code라 칭한다.

 

 

 

관리 코드란?

관리 코드가 CLR(공용 언어 런타임) 런타임에서 해당 실행이 관리되는 코드임을 알아봅니다.

docs.microsoft.com

더보기

.NET Framework로 작업하는 경우 “관리 코드”라는 용어를 자주 발견하게 됩니다. 이 문서에서는 이 용어의 의미 및 관련된 추가 정보를 설명합니다.

간단히 말해서, 관리 코드란 런타임에서 실행이 관리되는 코드입니다.이 경우 해당 런타임을 구현(Mono, .NET Framework 또는 .NET Core)에 관계없이공용 언어 런타임또는 CLR이라고 합니다.CLR은 관리 코드를 가져와서 기계어 코드로 컴파일한 다음 실행합니다.이 외에도 런타임에서는 자동 메모리 관리, 보안 경계, 형식 안전성 등 몇 가지 중요한 서비스를 제공합니다.

“비관리 코드”라고도 하는 C/C++ 프로그램 실행 방법과 이를 대조해 보세요.관리되지 않는 환경에서는 프로그래머가 거의 모든 작업을 수행합니다.실제 프로그램은 기본적으로 OS(운영 체제)에서 메모리에 로드하고 시작하는 이진 파일입니다.메모리 관리에서 보안 고려 사항에 이르기까지 다른 모든 작업은 프로그래머의 몫입니다.

관리 코드는 C#, Visual Basic, F# 등 .NET에서 실행할 수 있는 고급 언어 중 하나로 작성됩니다.이러한 언어로 작성된 코드를 해당 컴파일러로 컴파일할 때는 기계어 코드가 생성되지 않습니다.중간 언어코드가 생성되며, 런타임에서 이 코드를 컴파일하고 실행합니다.단, C++는 이 규칙의 유일한 예외이며 Windows에서 실행되는 관리되지 않는 네이티브 이진 파일을 생성할 수도 있습니다.

중간 언어 및 실행

“중간 언어”(또는 줄여서 IL)란?고급 .NET 언어로 작성된 코드를 컴파일하여 생성된 결과입니다.이러한 언어 중 하나로 작성된 코드를 컴파일하면 IL로 작성된 이진 파일을 얻게 됩니다.IL은 런타임에서 실행되는 모든 특정 언어에 독립적입니다. 필요한 경우 읽을 수 있는 별도의 사양도 있습니다.

고급 코드에서 IL을 생성한 후에는 대부분 실행하려고 할 것입니다.이때 CLR이 사용되며 IL 상태의 코드를Just-In-Time컴파일 또는JIT 처리하여 실제로 CPU에서 실행할 수 있는 기계어 코드로 변환하는 프로세스를 시작합니다.이러한 방식에서는 CLR이 코드에서 수행하는 작업을 정확히 알고 효과적으로 _관리_할 수 있습니다.

중간 언어를 CIL(공용 중간 언어) 또는 MSIL(Microsoft 중간 언어)이라고도 합니다.

비관리 코드와의 상호 운용성

물론, CLR에서 관리되는 환경과 관리되지 않는 환경 사이의 경계를 건널 수 있으며기본 클래스 라이브러리에도 이 작업을 수행하는 많은 코드가 있습니다.이를상호 운용성또는 줄여서interop라고 합니다.예를 들어 이러한 프로비전을 통해 관리되지 않는 라이브러리를 래핑하고 호출할 수 있습니다.그러나 이렇게 할 경우 코드가 런타임의 경계를 건널 때 실제 실행 관리가 다시 비관리 코드로 넘어가므로 동일한 제한이 적용됩니다.

이와 마찬가지로, C#은 CLR에서 실행이 관리되지 않는 코드 조각을 지정하는 안전하지 않은 컨텍스트를 활용하여 코드에서 직접 포인터 등의 관리되지 않는 구문을 사용할 수 있도록 하는 언어 중 하나입니다.

C++은 놀랍게도 그 CLR을 지원한다. 영문으로 되어있는 접은글에서 말하는 Manage란 상속과 같은 클래스적인 것으로 이해할 것이 아니라 코드로 접근해야한다. __clrcall은 다른 헤더파일에서 구현된 C++(Native Code)엔 호출할 수 없다. 뭐 본인이 집은 가장 __clrcall의 가장 중요요지는 이것이다.

 

The other function will be a separate function (a thunk) that calls into the actual function and lets the common language runtime perform PInvoke. When marking a function as __clrcall, you indicate the function implementation must be MSIL and that the native entry point function will not be generated.

 

__clrcall이란 것은 MSIL로 반드시 되어야 하는 함수를 가리키는 것이며, native 함수가 포함되거나 실행되지 않아야함 그 자체를 알려주는 것이다.

 

즉, 이것과 같다.

 

 

__clrcall 예제

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
int __cdecl Func13(__m128 _x, __m128 _y, __m128 _z)
{
    return 0;
}
int __cdecl Func13x(__m128 _x, __m128 _y, __m128 _z, __m128 _w, __m128 _k, __m128 _l)
{
    return 0;
}
int __clrcall Func14(__m128 _x, __m128 _y, __m128 _z)
{
    return 0;
}
int __stdcall Func15(__m128 _x, __m128 _y, __m128 _z)
{
    return 0;
}
int __fastcall Func16(__m128 _x, __m128 _y, __m128 _z)
{
    return 0;
}
//int __thiscall Func17(int _x, int _y, int _z)
//{
//    return _x + _y + _z;
//}
int __vectorcall Func18(__m128 _x, __m128 _y, __m128 _z)
{
    return 0;
}
 
 
 
int main() 
{    
    __m128 X = { 0000 };
    __m128 Y = { 0000 };
    __m128 Z = { 0000 };
 
    Func13(X, Y, Z);
    Func13x(X, Y, Z, X, Y, Z);
    Func14(X, Y, Z);
    Func15(X, Y, Z);
    Func16(X, Y, Z);
    //Func17(1, 2, 3);
    Func18(X, Y, Z);
}
 

이 코드는 실행이 되지 않는다. 바로 14번 함수가 범인이다. __clrcall은 네이티브 코드로 컴파일된 함수를 사용할 수 없게 만드는 기능이 있다. 이 예제를 CLR 환경에서 실행했을 때 뜨는 모든 경고와 그리고 터지게 한 오류를 정리하며 포스팅을 마무리하겠다.

 

1
2
3
4
5
6
7
8
9
심각도    코드    설명    프로젝트    파일    줄    비표시 오류(Suppression) 상태
경고    C4561     '__fastcall'이 '/clr' 옵션과 호환되지 않으므로 '__stdcall'로 변환됩니다.    kclrcall    D:\Class\6.0\TTmp\kclrcall\kclrcall.cpp    19    
경고    C4575     '__vectorcall'이 '/clr' 옵션과 호환되지 않습니다. '__stdcall'로 변환합니다.    kclrcall    D:\Class\6.0\TTmp\kclrcall\kclrcall.cpp    94    
경고    C4561     '__fastcall'이 '/clr' 옵션과 호환되지 않으므로 '__stdcall'로 변환됩니다.    kclrcall    D:\Class\6.0\TTmp\kclrcall\kclrcall.cpp    86    
오류    C3645     'Func14': 네이티브 코드로 컴파일된 함수에는 __clrcall을 사용할 수 없습니다.    kclrcall    D:\Class\6.0\TTmp\kclrcall\kclrcall.cpp    77    
경고    C4575     '__vectorcall'이 '/clr' 옵션과 호환되지 않습니다. '__stdcall'로 변환합니다.    kclrcall    D:\Class\6.0\TTmp\kclrcall\kclrcall.cpp    62    
경고    C4561     '__fastcall'이 '/clr' 옵션과 호환되지 않으므로 '__stdcall'로 변환됩니다.    kclrcall    D:\Class\6.0\TTmp\kclrcall\kclrcall.cpp    54    
경고    C4575     '__vectorcall'이 '/clr' 옵션과 호환되지 않습니다. '__stdcall'로 변환합니다.    kclrcall    D:\Class\6.0\TTmp\kclrcall\kclrcall.cpp    31    
경고    C4561     '__fastcall'이 '/clr' 옵션과 호환되지 않으므로 '__stdcall'로 변환됩니다.    kclrcall    D:\Class\6.0\TTmp\kclrcall\kclrcall.cpp    23
 

5번째 줄 - 오류 C3645 'Func14': 네이티브 코드로 컴파일된 함수에는 __clrcall을 사용할 수 없습니다.

반응형
Posted by Lotus1031
,