C++/CLI についてちょっと動作をみてみたので、その内容をまとめます。

そもそも、この言語の正しい呼び方は C++/CLI でいいんでしょうかね。個人的に Managed C++ と呼んでいた時期もありましたが、間違いだったのだろうか。MSDN によると、あくまでも C++ の拡張という位置づけのようです。いつの間にか C++/CX なんてのもあるみたいですが、そんなのは知らない。

Component Extensions for Runtime Platforms
http://msdn.microsoft.com/en-us/library/xey702bw.aspx

その名の通り、CLI で動く C++ であり、C++ のコードはそのままに、.NET Framework も使えてしまう素晴らしい環境です。そのわりにマイナーですが。そもそも存在すら知らない人が多そう。私自身は、SAP をやっていた時代に SAP MDM のタプル検索のパフォーマンスを測定するため、.NET API 経由で MDM を操作するテスト プログラムをなぜか言語として C++/CLI を選んで書いた記憶があります。確か別の製品のほうが速くて、結局 MDM 採用には至らなかったんですが。MDM .NET API というのは ↓ です。たぶん Java API のほうが広く使われています。当時は、そもそも SAP MDM なんて・・・という話もありましたが、今はどうなんでしょうかね。

MDM Java and .NET API Guide
http://help.sap.com/saphelp_nwmdm71/helpdata/en/loio30bf76947bb64c48a2e835fda42c5183_30bf76947bb64c48a2e835fda42c5183/13/041975d8ce4d4287d5205816ea955a/frameset.htm

それはさておき C++/CLI ですが、デバッグして見てみたかったのは以下の関数呼び出しのパターンです。気になりますよね。

  • C++/CLI –> Win32 Native DLL
  • C++/CLI –> C# Library
  • C# –> C++/CLI Library

というわけで、以下 5 つのプロジェクトからなるソリューションを Visual Studio で作ります。カッコ内は使ったテンプレートです。

  • cppclrapp (Visual C++ > CLR > CLR Console Application)
  • cppclrlib (Visual C++ > CLR > Class Library)
  • cslib (Visual C# > Windows > Class Library)
  • sublib(Visual C++ > CLR > Class Library)
  • NativeDll (Visual C++ > Win32 > Win32 Project)

ソリューション エクスプローラーはこんな感じ。

開発環境は以下の通りです。

  • OS: Windows 8.1 x64
  • IDE: Visual Studio 2013 Update 1
  • .NET Framework: 4.5
  • Windows Debugger: SDK 8.1 に入っているもの

以下のコードを書きました。デバッグしやすいように DebugBreak() を入れてあるのと、インライン展開されてシンボルが見えなくなる部分があったので、関数呼び出しを二段階にした部分があります。

cppclrapp - cppclrapp.cpp

// cppclrapp.cpp : main project file.

#include <windows.h>

#ifdef _WIN64 
#ifdef _DEBUG 
#using "..\x64\debug\cppclrlib.dll" 
#else 
#using "..\x64\release\cppclrlib.dll" 
#endif 
#else 
#ifdef _DEBUG 
#using "..\debug\cppclrlib.dll" 
#else 
#using "..\release\cppclrlib.dll" 
#endif 
#endif

int main(array<System::String ^> ^args) 
{ 
    System::Console::WriteLine("Program Start with {0} parameters", args->Length); 
    DebugBreak();

    cppclrlib::Class1 ^Class = gcnew cppclrlib::Class1(); 
    Class->Run(); 
    return 42; 
} 

cppclrlib – cppclrlib.cpp

// This is the main DLL file.

#include <Windows.h>

#ifdef _DEBUG 
#using "..\debug\cslib.dll" 
#else 
#using "..\release\cslib.dll" 
#endif

using namespace System;

typedef DWORD(WINAPI *NATIVEDLLFUNCTION)(int, LPWSTR);

bool DoNative(String^% Param) { 
    bool Ret = false; 
    HMODULE NativeDll = NULL; 
    NATIVEDLLFUNCTION NativeDllFunction = NULL; 
    const int BufferLength = 20; 
    WCHAR Buffer[BufferLength];

    NativeDll = LoadLibrary(L"NativeDll.dll"); 
    if (!NativeDll) { 
        Console::WriteLine("LoadLibrary failed - 0x{0:x8}\n", GetLastError()); 
        goto cleanup; 
    }

    NativeDllFunction = (NATIVEDLLFUNCTION)GetProcAddress(NativeDll, "NativeDllFunction"); 
    if (!NativeDllFunction) { 
        Console::WriteLine("GetProcAddress failed - 0x{0:x8}\n", GetLastError()); 
        goto cleanup; 
    }

    Console::WriteLine("NativeDll!NativeDllFunction: {0}", NativeDllFunction(BufferLength, Buffer));

    Param = gcnew String(Buffer); 
    Ret = true;

cleanup: 
    if (NativeDll) 
        FreeLibrary(NativeDll);

    return Ret; 
}

namespace cppclrlib { 
    public ref class Class1 
    { 
    private: 
        // RunInternal is jit-compiled. 
        void RunInternal() { 
            DebugBreak(); 
            String^ Param = "Hello."; 
            Console::WriteLine("cppclrlib.Class1.Run: {0}", DoNative(Param)); 
            Console::WriteLine("cslib.Class1.Run: {0}", cslib::Class1::Run(Param)); 
            Console::WriteLine("Final parameter: {0}", Param); 
        } 
    public: 
        // Run is never jit-compiled. Inline? 
        void Run() { 
            RunInternal(); 
        } 
    }; 
}

cslib – Class1.cs

using System;

namespace cslib 
{ 
    public class Class1 
    { 
        [System.Runtime.InteropServices.DllImport("kernel32.dll")] static extern void DebugBreak();

        public static int Run(ref string OutParam) { 
            DebugBreak();

            var sub = new sublib.Class1(); 
            Console.WriteLine("sublib.Class1: {0}", sub.Run(ref OutParam));

            OutParam += " +cslib.Class1.Run"; 
            return 42; 
        } 
    } 
} 

nativedll – dllmain.cpp

// dllmain.cpp : Defines the entry point for the DLL application. 
#include <windows.h>

BOOL APIENTRY DllMain( HMODULE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved 
                     ) 
{ 
    switch (ul_reason_for_call) 
    { 
    case DLL_PROCESS_ATTACH: 
    case DLL_THREAD_ATTACH: 
    case DLL_THREAD_DETACH: 
    case DLL_PROCESS_DETACH: 
        break; 
    } 
    return TRUE; 
}

nativedll – nativedll.cpp

// nativedll.cpp : Defines the exported functions for the DLL application. 
//

#include <windows.h>

DWORD WINAPI NativeDllFunction(int BufferLength, LPWSTR Buffer) { 
    DebugBreak(); 
    if (Buffer && BufferLength > 0) { 
        for (int i = 0; i < BufferLength - 1; ++i) { 
            Buffer[i] = L'A' + i; 
        } 
        Buffer[BufferLength-1] = 0; 
    } 
    return 42; 
} 

nativedll – nativedll.def

EXPORTS 
    NativeDllFunction @100 

sublib – sublib.cpp

// This is the main DLL file.

using namespace System;

namespace sublib { 
    public ref class Class1 
    { 
    private: 
        String ^Name;

    public: 
        Class1() { 
            Name = " +sublib.Class1.Run"; 
        }

        int Run(String^% Param) { 
            DebugBreak(); 
            Param += Name; 
            return 42; 
        } 
    }; 
} 

Debug ビルドは無視して、Release ビルドを使います。プラットフォームは、x86 と x64 を両方で。C# の cslib.dll については、どちらも Any CPU が使えます。Configuration Manager はこんな感じです。既定だと、C# のビルドの出力は bin フォルダーにできるので、そのへんは C++ の出力フォルダーに合わせるか、ビルド後にファイルをコピーする感じで対応します。

 

1. とりあえず実行してみる

デバッガーを使って一通り実行します。SOS のコマンドは以下のページをご参照ください。

SOS.dll (SOS Debugging Extension)
http://msdn.microsoft.com/en-us/library/bb190764(v=vs.110).aspx

D:\VSDev\Projects\cppclr>cdb x64\Release\cppclrapp

Microsoft (R) Windows Debugger Version 6.3.9600.16384 AMD64 
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: x64\Release\cppclrapp

************* Symbol Path validation summary ************** 
Response                         Time (ms)     Location 
Deferred                                       cache*D:\symbols.pub*http://msdl.microsoft.com/download/symbols 
Symbol search path is: cache*D:\symbols.pub*http://msdl.microsoft.com/download/symbols 
Executable search path is: 
ModLoad: 00007ff7`ea0b0000 00007ff7`ea0c6000   cppclrapp.exe 
(省略) 
ModLoad: 00007ff8`a0720000 00007ff8`a080f000   C:\Windows\SYSTEM32\MSVCR120.dll 
(1ed8.af4): Break instruction exception - code 80000003 (first chance) 
ntdll!LdrpDoDebuggerBreak+0x30: 
00007ff8`c7797710 cc              int     3 
0:000> g 
ModLoad: 00007ff8`c6ac0000 00007ff8`c6b65000   C:\Windows\system32\ADVAPI32.dll 
(省略) 
ModLoad: 00007ff8`bbd60000 00007ff8`bc6f8000   C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll 
ModLoad: 00007ff8`bbc80000 00007ff8`bbd56000   C:\Windows\SYSTEM32\MSVCR120_CLR0400.dll 
(1ed8.af4): Unknown exception - code 04242420 (first chance) 
ModLoad: 00007ff8`ba550000 00007ff8`bbad5000   C:\Windows\assembly\NativeImages_v4.0.30319_64\mscorlib\1c4f23e80bd4b68fb3f56bdb16dbb647\mscorlib.ni.dll 
ModLoad: 00007ff8`b97b0000 00007ff8`b98df000   C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clrjit.dll 
ModLoad: 00007ff8`beeb0000 00007ff8`beec5000   cppclrlib.dll 
ModLoad: 000000f1`8e1c0000 000000f1`8e1d5000   cppclrlib.dll 
ModLoad: 00007ff8`beeb0000 00007ff8`beec5000   D:\VSDev\Projects\cppclr\x64\Release\cppclrlib.dll 
ModLoad: 000000f1`8c7e0000 000000f1`8c7e8000   cslib.dll 
ModLoad: 000000f1`8e220000 000000f1`8e228000   cslib.dll 
ModLoad: 000000f1`8c7e0000 000000f1`8c7e8000   D:\VSDev\Projects\cppclr\x64\Release\cslib.dll 
ModLoad: 00007ff8`bed90000 00007ff8`beda5000   sublib.dll 
ModLoad: 000000f1`8e190000 000000f1`8e1a5000   sublib.dll 
ModLoad: 00007ff8`bed90000 00007ff8`beda5000   D:\VSDev\Projects\cppclr\x64\Release\sublib.dll 
Program Start with 0 parameters 
(1ed8.af4): Break instruction exception - code 80000003 (first chance) 
KERNELBASE!DebugBreak+0x2: 
00007ff8`c4ca8886 cc              int     3 
0:000> lm 
start             end                 module name 
00007ff7`ea0b0000 00007ff7`ea0c6000   cppclrapp   (deferred) 
00007ff8`beeb0000 00007ff8`beec5000   cppclrlib   (deferred) 
000000f1`8e1c0000 000000f1`8e1d5000   cppclrlib_f18e1c0000   (deferred) 
000000f1`8c7e0000 000000f1`8c7e8000   cslib      (deferred) 
000000f1`8e220000 000000f1`8e228000   cslib_f18e220000   (deferred) 
00007ff8`bed90000 00007ff8`beda5000   sublib     (deferred) 
000000f1`8e190000 000000f1`8e1a5000   sublib_f18e190000   (deferred) 
00007ff8`b97b0000 00007ff8`b98df000   clrjit     (deferred) 
00007ff8`ba550000 00007ff8`bbad5000   mscorlib_ni   (deferred) 
00007ff8`a0720000 00007ff8`a080f000   MSVCR120   (deferred) 
00007ff8`bbc80000 00007ff8`bbd56000   MSVCR120_CLR0400   (deferred) 
00007ff8`bbd60000 00007ff8`bc6f8000   clr        (deferred) 
00007ff8`bc790000 00007ff8`bc82c000   mscoreei   (deferred) 
(省略) 
00007ff8`c4bd0000 00007ff8`c4cde000   KERNELBASE   (pdb symbols)          d:\symbols.pub\kernelbase.pdb\9CF6BE36C1A844C5BDADA48367CC148D2\kernelbase.pdb 
00007ff8`c76d0000 00007ff8`c7879000   ntdll      (pdb symbols)          d:\symbols.pub\ntdll.pdb\6332539D05E347DDA41DCBA242578BC31\ntdll.pdb 
0:000> .loadby sos clr 
0:000> !clrstack 
c0000005 Exception in C:\Windows\Microsoft.NET\Framework64\v4.0.30319\sos.clrstack debugger extension. 
      PC: 00007ff8`9f22c7e3  VA: 00000000`00000000  R/W: 0  Parameter: 00000000`00000000 
0:000> !clrstack 
OS Thread Id: 0xaf4 (0) 
        Child SP               IP Call Site 
000000f18c5ae7b8 00007ff8c4ca8886 [InlinedCallFrame: 000000f18c5ae7b8] <Module>.DebugBreak() 
000000f18c5ae7b8 00007ff85c7556ea [InlinedCallFrame: 000000f18c5ae7b8] <Module>.DebugBreak() 
000000f18c5ae790 00007ff85c7556ea DomainBoundILStubClass.IL_STUB_PInvoke() 
000000f18c5ae850 00007ff85c75564e <Module>.main(System.String[]) 
000000f18c5ae8a0 00007ff85c753a1e <Module>.mainCRTStartupStrArray(System.String[]) 
000000f18c5aec00 00007ff8bbd6d173 [GCFrame: 000000f18c5aec00] 
0:000> g 
(1ed8.af4): Break instruction exception - code 80000003 (first chance) 
KERNELBASE!DebugBreak+0x2: 
00007ff8`c4ca8886 cc              int     3 
0:000> !clrstack 
OS Thread Id: 0xaf4 (0) 
        Child SP               IP Call Site 
000000f18c5ae768 00007ff8c4ca8886 [InlinedCallFrame: 000000f18c5ae768] <Module>.DebugBreak() 
000000f18c5ae768 00007ff85c7556ea [InlinedCallFrame: 000000f18c5ae768] <Module>.DebugBreak() 
000000f18c5ae740 00007ff85c7556ea DomainBoundILStubClass.IL_STUB_PInvoke() 
000000f18c5ae800 00007ff85c75577b cppclrlib.Class1.RunInternal() 
000000f18c5ae850 00007ff85c755662 <Module>.main(System.String[]) 
000000f18c5ae8a0 00007ff85c753a1e <Module>.mainCRTStartupStrArray(System.String[]) 
000000f18c5aec00 00007ff8bbd6d173 [GCFrame: 000000f18c5aec00] 
0:000> g 
ModLoad: 00007ff8`c15a0000 00007ff8`c15a7000   D:\VSDev\Projects\cppclr\x64\Release\NativeDll.dll 
(1ed8.af4): Break instruction exception - code 80000003 (first chance) 
*** WARNING: Unable to verify checksum for D:\VSDev\Projects\cppclr\x64\Release\NativeDll.dll 
KERNELBASE!DebugBreak+0x2: 
00007ff8`c4ca8886 cc              int     3 
0:000> !clrstack 
OS Thread Id: 0xaf4 (0) 
        Child SP               IP Call Site 
000000f18c5ae720 00007ff8c4ca8886 [InlinedCallFrame: 000000f18c5ae720] 
000000f18c5ae720 00007ff85c75598e [InlinedCallFrame: 000000f18c5ae720] 
000000f18c5ae6d0 00007ff85c75598e <Module>.DoNative(System.String ByRef) 
000000f18c5ae800 00007ff85c7557a4 cppclrlib.Class1.RunInternal() 
000000f18c5ae850 00007ff85c755662 <Module>.main(System.String[]) 
000000f18c5ae8a0 00007ff85c753a1e <Module>.mainCRTStartupStrArray(System.String[]) 
000000f18c5aec00 00007ff8bbd6d173 [GCFrame: 000000f18c5aec00] 
0:000> k7 
Child-SP          RetAddr           Call Site 
000000f1`8c5ae698 00007ff8`c15a1026 KERNELBASE!DebugBreak+0x2 
000000f1`8c5ae6a0 00007ff8`5c75598e NativeDll!NativeDllFunction+0x16 
000000f1`8c5ae6d0 00007ff8`5c7557a4 0x00007ff8`5c75598e 
000000f1`8c5ae800 00007ff8`5c755662 0x00007ff8`5c7557a4 
000000f1`8c5ae850 00007ff8`5c753a1e 0x00007ff8`5c755662 
000000f1`8c5ae8a0 00007ff8`bbd6d173 0x00007ff8`5c753a1e 
000000f1`8c5ae920 00007ff8`bbd6d02e clr!CallDescrWorkerInternal+0x83 
0:000> g 
NativeDll!NativeDllFunction: 42 
cppclrlib.Class1.Run: True 
(1ed8.af4): Break instruction exception - code 80000003 (first chance) 
KERNELBASE!DebugBreak+0x2: 
00007ff8`c4ca8886 cc              int     3 
0:000> !clrstack 
OS Thread Id: 0xaf4 (0) 
        Child SP               IP Call Site 
000000f18c5ae708 00007ff8c4ca8886 [InlinedCallFrame: 000000f18c5ae708] <Module>.DebugBreak() 
000000f18c5ae708 00007ff85c7556ea [InlinedCallFrame: 000000f18c5ae708] <Module>.DebugBreak() 
000000f18c5ae6e0 00007ff85c7556ea DomainBoundILStubClass.IL_STUB_PInvoke() 
000000f18c5ae7a0 00007ff85c755e06 cslib.Class1.Run(System.String ByRef) 
000000f18c5ae800 00007ff85c755815 cppclrlib.Class1.RunInternal() 
000000f18c5ae850 00007ff85c755662 <Module>.main(System.String[]) 
000000f18c5ae8a0 00007ff85c753a1e <Module>.mainCRTStartupStrArray(System.String[]) 
000000f18c5aec00 00007ff8bbd6d173 [GCFrame: 000000f18c5aec00] 
0:000> g 
sublib.Class1: 42 
cslib.Class1.Run: 42 
Final parameter: ABCDEFGHIJKLMNOPQRS +sublib.Class1.Run +cslib.Class1.Run 
ntdll!NtTerminateProcess+0xa: 
00007ff8`c776683a c3              ret 
0:000> q 
quit:

D:\VSDev\Projects\cppclr>

最初に幾つかのモジュールが読み込まれますが、.NET の特徴として、イメージが複数箇所に分かれてメモリにマップされるようです。cppclrlib.dll は、00007ff8beeb0000 と 000000f18e1c0000 に分かれました。スタック トレースにおける SP (Stack Pointer) と IP (Instruction Pointer) のアドレスを見る限り、それぞれコード セグメントとデータ セグメントのようなので、単純に推測すると、データ領域とコード領域が明確に分かれているようですね。詳しくは知りませんが、まあそういうものなのでしょう。

初回ブレークはそのまま実行して、最初の DebugBreak() 呼び出しのところで SOS をロードします。そのあと !clrstack を実行しましたが、なぜか初回は必ず c0000005 例外でコマンドが失敗します。再実行するとうまくいきます。謎の仕様ですが、これも深追いはしません。

初回の DebugBreak は、cppclrapp の main 関数です。!clrstack は、もちろんスタックを見るために実行しているわけですが、k コマンドだと main 関数のシンボル名は表示されません。一方 !clrstack の出力では “<Module>.main(System.String[])” という名前が表示されています。つまり、C++/CLI のモジュールも基本的には .NET アセンブリと考えられそうです。しかし、普通に考えれば cppclrapp!main となりそうなところが、<Module> という意味不明なモジュール名で置き換えられています。ブレークから再開すると、cppclrlib のコードが実行されます。このときの !clrstack では cppclrlib.Class1.RunInternal と表示されていて、<Module> とは表示されてません。このことから、どうやら名前空間が存在しないシンボルは、<Module>. という接頭辞がついてしまうようです。

次の DebugBreak は、NativeDll のコードです。ここでは !clrstack と k を両方実行していますが、それぞれマネージドとネイティブのシンボルしか表示できていません。仕様でしょうが、まあ不便ですね。さらに、この 2 つのコマンドでは表示フォーマットが微妙に異なっていて、!clrstack の 2 列目は、リターンアドレスではなく IP です。このため、Call Site として表示されるシンボル名は 1 行ずれます。見間違いやすいので注意です。

さて、ここでのスタック トレースがまさに C++/CLI がネイティブ コードを呼び出しているところですが、他の DebugBreak() でのブレーク時と異なり、DomainBoundILStubClass.IL_STUB_PInvoke というフレームがありません。NativeDll!NativeDllFunction と KERNELBASE!DebugBreak の間にないのは当然ですが、NativeDll!NativeDllFunction と <Module>.DoNative の間にもありません。コードが短くなるだけでなく、内部的に PInvoke は使っていないのでしょう。ただしパフォーマンスが速いのかどうかは実際に測定してみないと判断できません。

最後の DebugBreak は C# のコードである cslib です。上述のように、C++/CLI のモジュールはマネージドの世界に住んでいると考えてよさそうなので、この部分は当然直接的な呼び出しになってます。もちろん !clrstack できれいに表示されます。

今回のデバッグを始める前は、C++/CLI ってのはネイティブとマネージドの混合になっていて、例えば cppclrapp であれば、cppclrapp!main というネイティブの関数が、マネージドの部分とは独立して存在するのではないかと推測していましたが、違っているようです。

2. <Module>. とは何なのか


名前空間に所属しないシンボルは、デバッガー内では <Module>. という接頭辞がついてしまうことが推測されるわけですが、ではそういった関数にブレーク ポイントを貼るにはどうすればいいのでしょうか。普通のマネージドの関数であれば !name2ee を使って MD (Method Descriptor) を取得してから !bpmd なんかを実行します。では main 関数でブレークしているところで !name2ee を使ってみましょう。

0:000> !clrstack 
OS Thread Id: 0xe30 (0) 
        Child SP               IP Call Site 
000000be0c5eee18 00007ff8c4ca8886 [InlinedCallFrame: 000000be0c5eee18] <Module>.DebugBreak() 
000000be0c5eee18 00007ff85c7456ea [InlinedCallFrame: 000000be0c5eee18] <Module>.DebugBreak() 
000000be0c5eedf0 00007ff85c7456ea DomainBoundILStubClass.IL_STUB_PInvoke() 
000000be0c5eeeb0 00007ff85c74564e <Module>.main(System.String[]) 
000000be0c5eef00 00007ff85c743a1e <Module>.mainCRTStartupStrArray(System.String[]) 
000000be0c5ef260 00007ff8bbd6d173 [GCFrame: 000000be0c5ef260] 
0:000> !name2ee cppclrapp!<Module>.main 
Module:      00007ff85c602fb8 
Assembly:    cppclrapp.exe 
Token:       0000000006000001 
MethodDesc:  00007ff85c604e70 
Name:        <Module>.main(System.String[]) 
JITTED Code Address: 00007ff85c745600

なんと、<Module>. を含めて立派にシンボル名として機能するようです。少し気持ち悪いですが、必須テクニックでしょう。

次に、!dumpdomain から網羅的に展開してみます。今度は cppclrlib をターゲットにします。

0:000> !dumpdomain 
(省略) 
Assembly:           000000be0c78b910 [D:\VSDev\Projects\cppclr\x64\Release\cppclrlib.dll] 
ClassLoader:        000000be0c78ba50 
SecurityDescriptor: 000000be0c78ce60 
  Module Name 
00007ff85c6073d8            D:\VSDev\Projects\cppclr\x64\Release\cppclrlib.dll 
(省略) 
0:000> !dumpmodule -mt 00007ff85c6073d8 
Name:       D:\VSDev\Projects\cppclr\x64\Release\cppclrlib.dll 
Attributes: PEFile SupportsUpdateableMethods 
Assembly:   000000be0c78b910 
LoaderHeap:              0000000000000000 
TypeDefToMethodTableMap: 00007ff85c718800 
TypeRefToMethodTableMap: 00007ff85c719220 
MethodDefToDescMap:      00007ff85c719468 
FieldDefToDescMap:       00007ff85c7197f0 
MemberRefToDescMap:      0000000000000000 
FileReferencesMap:       00007ff85c71a588 
AssemblyReferencesMap:   00007ff85c71a590 
MetaData start address:  00007ff8c0384340 (41712 bytes)

Types defined in this module

              MT          TypeDef Name 
------------------------------------------------------------------------------ 
00007ff85c7801e8 0x02000001 <Module> 
00007ff85c780280 0x02000002 cppclrlib.Class1 
00007ff85c609ae0 0x020000e6 <CppImplementationDetails>.$ArrayType$$$BY0O@$$CB_W 
00007ff85c609b58 0x020000e7 <CppImplementationDetails>.$ArrayType$$$BY0BC@$$CBD 
00007ff85c780470 0x02000100 <CrtImplementationDetails>.ModuleLoadException 
00007ff85c780620 0x02000101 <CrtImplementationDetails>.ModuleLoadExceptionHandlerException 
00007ff85c7807a8 0x02000102 <CrtImplementationDetails>.ModuleUninitializer 
00007ff85c780308 0x0200012a <CrtImplementationDetails>.LanguageSupport 
00007ff85c780390 0x0200012b gcroot<System::String ^> 
00007ff85c609be0 0x0200012c __s_GUID 
00007ff85c609c58 0x0200012d <CppImplementationDetails>.$ArrayType$$$BY00Q6MPEBXXZ 
00007ff85c609ce0 0x02000131 <CrtImplementationDetails>.Progress+State 
00007ff85c609f70 0x02000132 <CppImplementationDetails>.$ArrayType$$$BY0A@P6AXXZ 
00007ff85c780020 0x0200013a <CppImplementationDetails>.$ArrayType$$$BY0A@P6AHXZ 
00007ff85c7800a8 0x0200013b __enative_startup_state 
00007ff85c609e30 0x0200013e <CrtImplementationDetails>.TriBool+State 
00007ff85c7808b8 0x02000142 <CrtImplementationDetails>.ThisModule

おお!<Module> とかいう名前のメソッド テーブルが出てきましたね!ということは !dumpmt で一網打尽にできそうです。

EEClass:         00007ff85c71f398 
Module:          00007ff85c6073d8 
Name:            <Module> 
mdToken:         0000000002000001 
File:            D:\VSDev\Projects\cppclr\x64\Release\cppclrlib.dll 
BaseSize:        0x18 
ComponentSize:   0x0 
Slots in VTable: 91 
Number of IFaces in IFaceMap: 0 
-------------------------------------- 
MethodDesc Table 
           Entry       MethodDesc    JIT Name 
00007ff85c742060 00007ff85c609410    JIT <Module>..cctor() 
00007ff85c60c9f8 00007ff85c609170   NONE <Module>.DoNative(System.String ByRef) 
00007ff85c60ca00 00007ff85c609180   NONE <Module>.<CrtImplementationDetails>.NativeDll.IsInDllMain() 
00007ff85c7438d0 00007ff85c6093c0    JIT <Module>.<CrtImplementationDetails>.LanguageSupport.DomainUnload(System.Object, System.EventArgs) 
00007ff85c742230 00007ff85c6093d0    JIT <Module>.<CrtImplementationDetails>.LanguageSupport.Cleanup(<CrtImplementationDetails>.LanguageSupport*, System.Exception) 
00007ff85c7420f0 00007ff85c6093e0    JIT <Module>.<CrtImplementationDetails>.LanguageSupport.{ctor}(<CrtImplementationDetails>.LanguageSupport*) 
00007ff85c743930 00007ff85c6093f0    JIT <Module>.<CrtImplementationDetails>.LanguageSupport.{dtor}(<CrtImplementationDetails>.LanguageSupport*) 
00007ff85c742320 00007ff85c609400    JIT <Module>.<CrtImplementationDetails>.LanguageSupport.Initialize(<CrtImplementationDetails>.LanguageSupport*) 
00007ff85c60cb50 00007ff85c609418   NONE <Module>.gcroot<System::String ^>.{ctor}(gcroot<System::String ^>*) 
00007ff85c743950 00007ff85c609428    JIT <Module>.gcroot<System::String ^>.{dtor}(gcroot<System::String ^>*) 
00007ff85c742650 00007ff85c609438    JIT <Module>.gcroot<System::String ^>.=(gcroot<System::String ^>*, System.String) 
00007ff85c60cb68 00007ff85c609448   NONE <Module>.gcroot<System::String ^>..PE$AAVString@System@@(gcroot<System::String ^>*) 
00007ff85c743010 00007ff85c609458    JIT <Module>.<CrtImplementationDetails>.AtExitLock._handle() 
(いろいろ省略) 
00007ff85c60cc00 00007ff85c609578   NONE <Module>._atexit_m(Void ()) 
00007ff85c60cc08 00007ff85c609588   NONE <Module>._initatexit_app_domain() 
00007ff85c60cc10 00007ff85c609598   NONE <Module>._app_exit_callback() 
00007ff85c60cc18 00007ff85c6095a8   NONE <Module>._onexit_m_appdomain() _onexit_m_appdomain(Int32 ()) 
00007ff85c60cc20 00007ff85c6095b8   NONE <Module>._atexit_m_appdomain(Void ()) 
00007ff85c60cc28 00007ff85c6095c8   NONE <Module>.DecodePointer(Void*) 
00007ff85c60d0f0 00007ff85c609610   NONE <Module>.EncodePointer(Void*) 
00007ff85c60cc38 00007ff85c609658   NONE <Module>._initterm_e(Int32 ()*, Int32 ()*) 
00007ff85c60cc40 00007ff85c609668   NONE <Module>._initterm(Void ()*, Void ()*) 
00007ff85c742ae0 00007ff85c609688    JIT <Module>._initterm_m(Void* ()*, Void* ()*) 
00007ff85c60cc78 00007ff85c6096d8   NONE <Module>.DebugBreak() 
00007ff85c60cc80 00007ff85c609720   NONE <Module>.GetProcAddress() GetProcAddress(HINSTANCE__*, SByte*) 
00007ff85c60cc88 00007ff85c609768   NONE <Module>.GetLastError() 
00007ff85c60cc90 00007ff85c6097b0   NONE <Module>.LoadLibraryW(Char*) 
00007ff85c60cc98 00007ff85c6097f8   NONE <Module>.FreeLibrary(HINSTANCE__*) 
00007ff85c60d0b0 00007ff85c609840   NONE <Module>._getFiberPtrId() 
00007ff85c60d0e0 00007ff85c609888   NONE <Module>._amsg_exit(Int32) 
00007ff85c60d0d0 00007ff85c6098d0   NONE <Module>.__security_init_cookie() 
00007ff85c60d0c0 00007ff85c609918   NONE <Module>.Sleep(UInt32) 
00007ff85c60c9d8 00007ff85c609a30   NONE <Module>._cexit() 
00007ff85c60c9e8 00007ff85c609a78   NONE <Module>.__FrameUnwindFilter(_EXCEPTION_POINTERS*) 

DoNative 関数が出てきました。表示は抜粋していますが、内部で使われている関数の一覧が全部出てくるので興味深いですね。驚きなのは、 <Module>..cctor() というコンストラクターが存在することです。どうやら、<Module> というのはデバッガーが表示の便宜上適当に処理しているのではなく、本当にクラスとして存在しているようです。

3. call 命令はどうなっているか

C++/CLI といえど結局は全部マネージド コードとして扱えばよく、名前空間に所属しないシンボルも、<Modules> クラスの static メソッドとして扱えるのであれば、C# などと同じようにデバッグできそうです。最後に、もう少しマニアックに call 命令がどうなっているのかを見てみます。実用的な意味はないです。

0:000> k5 
Child-SP          RetAddr           Call Site 
000000be`0c5eecf8 00007ff8`c23d1026 KERNELBASE!DebugBreak+0x2 
000000be`0c5eed00 00007ff8`5c74598e NativeDll!NativeDllFunction+0x16 
000000be`0c5eed30 00007ff8`5c7457a4 0x00007ff8`5c74598e 
000000be`0c5eee60 00007ff8`5c745662 0x00007ff8`5c7457a4 
000000be`0c5eeeb0 00007ff8`5c743a1e 0x00007ff8`5c745662 
0:000> !name2ee cppclrlib!<Module>.DoNative 
Module:      00007ff85c6073d8 
Assembly:    cppclrlib.dll 
Token:       0000000006000001 
MethodDesc:  00007ff85c609170 
Name:        <Module>.DoNative(System.String ByRef) 
JITTED Code Address: 00007ff85c7458a0

0:000> !U 00007ff85c7458a0 
Normal JIT generated code 
<Module>.DoNative(System.String ByRef) 
Begin 00007ff85c7458a0, size 231 
>>> 00007ff8`5c7458a0 55              push    rbp 
00007ff8`5c7458a1 53              push    rbx 
00007ff8`5c7458a2 56              push    rsi 
00007ff8`5c7458a3 57              push    rdi 
00007ff8`5c7458a4 4154            push    r12 
00007ff8`5c7458a6 4155            push    r13 
00007ff8`5c7458a8 4156            push    r14 
00007ff8`5c7458aa 4157            push    r15 
00007ff8`5c7458ac 4881ece8000000  sub     rsp,0E8h 
00007ff8`5c7458b3 488d6c2420      lea     rbp,[rsp+20h] 
00007ff8`5c7458b8 488bc1          mov     rax,rcx 
(省略) 
00007ff8`5c7458da e81980ecff      call    00007ff8`5c60d8f8 (<Module>.LoadLibraryW(Char*), mdToken: 0000000006000054) 
00007ff8`5c7458df 48894500        mov     qword ptr [rbp],rax 
00007ff8`5c7458e3 4885c0          test    rax,rax 
00007ff8`5c7458e6 0f8454010000    je      00007ff8`5c745a40 
00007ff8`5c7458ec 488d154de9c363  lea     rdx,[cppclrlib!`string' (00007ff8`c0384240)] 
00007ff8`5c7458f3 488bc8          mov     rcx,rax 
00007ff8`5c7458f6 e81d80ecff      call    00007ff8`5c60d918 (<Module>.GetProcAddress() GetProcAddress(HINSTANCE__*, SByte*), mdToken: 0000000006000052) 
00007ff8`5c7458fb 48894508        mov     qword ptr [rbp+8],rax 
00007ff8`5c7458ff 4885c0          test    rax,rax 
00007ff8`5c745902 7535            jne     00007ff8`5c745939 
(省略) 
00007ff8`5c745939 488d4d30        lea     rcx,[rbp+30h] 
00007ff8`5c74593d e8a6f1615f      call    clr!JIT_InitPInvokeFrame (00007ff8`bbd64ae8) 
00007ff8`5c745942 48894570        mov     qword ptr [rbp+70h],rax 
00007ff8`5c745946 48896550        mov     qword ptr [rbp+50h],rsp 
00007ff8`5c74594a 48896d60        mov     qword ptr [rbp+60h],rbp 
00007ff8`5c74594e 48b97033261ebe000000 mov rcx,0BE1E263370h 
00007ff8`5c745958 488b09          mov     rcx,qword ptr [rcx] 
00007ff8`5c74595b 48894d10        mov     qword ptr [rbp+10h],rcx 
00007ff8`5c74595f 488d4d30        lea     rcx,[rbp+30h] 
00007ff8`5c745963 48894810        mov     qword ptr [rax+10h],rcx 
00007ff8`5c745967 488d0d20000000  lea     rcx,[00007ff8`5c74598e] 
00007ff8`5c74596e 48894d58        mov     qword ptr [rbp+58h],rcx 
00007ff8`5c745972 c7400c00000000  mov     dword ptr [rax+0Ch],0 
00007ff8`5c745979 488d9580000000  lea     rdx,[rbp+80h] 
00007ff8`5c745980 b914000000      mov     ecx,14h 
00007ff8`5c745985 4533db          xor     r11d,r11d 
00007ff8`5c745988 488b4508        mov     rax,qword ptr [rbp+8] 
00007ff8`5c74598c ffd0            call    rax 
00007ff8`5c74598e 894578          mov     dword ptr [rbp+78h],eax 
00007ff8`5c745991 488b4570        mov     rax,qword ptr [rbp+70h] 
00007ff8`5c745995 c7400c01000000  mov     dword ptr [rax+0Ch],1

0:000> x kernelbase!getprocaddress 
00007ff8`c4bd52ac KERNELBASE!GetProcAddress (<no parameter info>)

0:000> !name2ee cppclrlib!<Module>.GetProcAddress 
Module:      00007ff85c6073d8 
Assembly:    cppclrlib.dll 
Token:       0000000006000052 
MethodDesc:  00007ff85c609720 
Name:        <Module>.GetProcAddress() GetProcAddress(HINSTANCE__*, SByte*) 
Not JITTED yet. Use !bpmd -md 00007ff85c609720 to break on run.

NativeDll のブレークでいろいろ実行します。まずは DoNative のアセンブリ言語を見ます。uf だとうまく表示できないので、!U を使います。全部だと長いので、GetProcAddress で NativeDllFunction へのアドレスを取得して、それを実行するところを中心に抜粋しました。

マネージド関数であろうとネイティブであろうと、call は普通に near ジャンプです。GetProcAddress の部分は <Module>.GetProcAddress という関数へ飛ぶようなので、ネイティブと同じように、ジャンプ テーブル的なものを作って、ワンクッション置いてから目的のコードに辿り着くようです。

最後のコマンドで、<Module>.GetProcAddress はまだ JIT されていないと表示されました。既に NativeDllFunction まで辿り着いているのに、おかしな話です。これは cppclrlib.Class1.Run でも起きた現象ですが、インライン展開のようなことが起きているようです。

追いかけだすとキリがないので、今回はこの辺までで。