這是我的《Advanced .Net Debugging》這個系列的第九篇文章。這篇文章的內容是原書的第二部分的【除錯實戰】的第七章【互用性】。互用性包含兩個方面,第一個方面就是託管程式碼呼叫 COM,此情況叫做 COM 互用性(也叫做 COM Interop);第二個方面就是託管程式碼呼叫從 DLL 中匯出的函式,這種情況稱為平臺呼叫服務(Platform Invocation Services,P/Invoke)。本章將介紹 COM 互用性和平臺呼叫服務內部工作機制,以及當託管程式碼和非託管程式碼之間發生不正確互動時出現的一些問題,並解決它。這樣看來,如果想成為一位稱職的除錯人員,掌握的東西還是挺多的。當然,高階除錯會涉及很多方面的內容,你對 .NET 基礎知識掌握越全面、細節越底層,除錯成功的機率越大,當我們遇到各種奇葩問題的時候才不會手足無措。
如果在沒有說明的情況下,所有程式碼的測試環境都是 Net 8.0,如果有變動,我會在專案章節裡進行說明。好了,廢話不多說,開始我們今天的除錯工作。
除錯環境我需要進行說明,以防大家不清楚,具體情況我已經羅列出來。
作業系統:Windows Professional 10
除錯工具:Windbg Preview(Debugger Client:1.2306.1401.0,Debugger engine:10.0.25877.1004)和 NTSD(10.0.22621.2428 AMD64)
下載地址:可以去Microsoft Store 去下載
開發工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
Net 版本:.Net 8.0
CoreCLR原始碼:原始碼下載
在此說明:我使用了兩種除錯工具,第一種:Windbg Preivew,圖形介面,使用方便,操作順手,不用擔心干擾;第二種是:NTSD,是命令列形式的偵錯程式,在命令使用上和 Windbg 沒有任何區別,之所以增加了它的除錯過程,不過是我的個人愛好,想多瞭解一些,看看他們有什麼區別,為了學習而使用的。如果在工作中,我推薦使用 Windbg Preview,更好用,更方便,也不會出現奇怪問題(我在使用 NTSD 除錯斷點的時候,不能斷住,提示記憶體不可讀,Windbg preview 就沒有任何問題)。
如果大家想了解除錯過程,二選一即可,當然,我推薦檢視【Windbg Preview 除錯】。
二、目錄結構
為了讓大家看的更清楚,也為了自己方便查詢,我做了一個目錄結構,可以直觀的檢視文章的佈局、內容,可以有針對性檢視。
2.1、平臺呼叫
A、基礎知識
B、眼見為實
1)、NTSD 除錯
2)、Windbg Preview 除錯
2.2、COM
2.3、P/Invoke 呼叫的除錯
2.3.1、呼叫協定
A、基礎知識
B、眼見為實
1)、NTSD 除錯
2)、Windbg Preview 除錯
2.3.2、委託
A、基礎知識
B、眼見為實
1)、Windbg Preview 除錯
2.4、互操作中的記憶體洩漏問題的除錯
A、基礎知識
B、眼見為實
1)、NTSD 除錯
2)、Windbg Preview 除錯
2.5、COM 互用性中終結操作的除錯
三、除錯原始碼
廢話不多說,本節是除錯的原始碼部分,沒有程式碼,當然就談不上測試了,除錯必須有載體。
3.1、ExampleCore_7_01
1 using System.Runtime.InteropServices; 2 3 namespace ExampleCore_7_01 4 { 5 internal class Program 6 { 7 [DllImport("Kernel32.dll", SetLastError = true)] 8 private static extern bool Beep(uint freq, uint dur); 9 10 static void Main(string[] args) 11 { 12 Beep(1000, 1000); 13 14 Console.ReadLine(); 15 } 16 } 17 }
1 using System.Runtime.InteropServices; 2 3 namespace ExampleCore_7_02 4 { 5 internal class Program 6 { 7 [DllImport("ExampleCore_7_022.dll", CallingConvention = CallingConvention.FastCall, CharSet = CharSet.Unicode)] 8 public static extern void Alloc(string str); 9 static void Main(string[] args) 10 { 11 var str = "hello world"; 12 13 Alloc(str); 14 15 Console.ReadLine(); 16 } 17 } 18 }
1 extern "C" 2 { 3 __declspec(dllexport) void Alloc(wchar_t* c); 4 } 5 6 #include "iostream" 7 #include <Windows.h> 8 9 using namespace std; 10 11 void Alloc(wchar_t* c) 12 { 13 wprintf(L"%s \n", c); 14 }
1 using System.Runtime.InteropServices; 2 3 namespace ExampleCore_7_03 4 { 5 internal class Program 6 { 7 //static GCHandle handle; 1、 8 9 public delegate void Callback(int i); 10 11 static void Main(string[] args) 12 { 13 TestCallback(); 14 GC.Collect(); //(在 Net Framework 環境下,不註釋,就出問題,註釋掉就沒問題;在 .NET 8.0 環境,註釋與否都不出錯)。 15 Console.WriteLine("Press any key to exit"); 16 Console.ReadLine(); 17 } 18 19 private static void TestCallback() 20 { 21 Callback? callback = MyCallback; 22 //handle=GCHandle.Alloc(callback, GCHandleType.Normal); 2、 23 24 AsyncProcess(callback); 25 26 callback = null; 27 } 28 29 private static void MyCallback(int result) 30 { 31 Console.WriteLine($"回撥的結果:Result={result}"); 32 } 33 34 [DllImport("ExampleCore_7_033", CallingConvention = CallingConvention.StdCall)] 35 private static extern void AsyncProcess(Callback callback); 36 } 37 }
1 #include <iostream> 2 #include <Windows.h> 3 using namespace std; 4 5 typedef void(__stdcall* PCallback)(UINT result); 6 7 extern "C" 8 { 9 _declspec(dllexport) void __stdcall AsyncProcess(PCallback ptr); 10 } 11 12 DWORD WINAPI ThreadWorkItem(LPVOID lpParameter) 13 { 14 printf("C++ 的工作執行緒,tid=%d \n", GetCurrentThreadId()); 15 16 Sleep(2000); 17 18 PCallback callback = (PCallback)lpParameter; 19 20 callback(5); 21 22 return 0; 23 } 24 25 void __stdcall AsyncProcess(PCallback ptr) 26 { 27 HANDLE hThread = CreateThread(NULL, 0, ThreadWorkItem, ptr, 0, NULL); 28 }
3.6、ExampleCore_7_04
1 using System.Runtime.InteropServices; 2 using System.Text; 3 4 namespace ExampleCore_7_04 5 { 6 internal class Program 7 { 8 static void Main(string[] args) 9 { 10 Console.WriteLine("請輸入迭代的次數。"); 11 if (int.TryParse(Console.ReadLine(), out int it)) 12 { 13 StringBuilder stringBuilder = new StringBuilder(200); 14 for (int i = 0; i < it; i++) 15 { 16 GetDate(stringBuilder); 17 } 18 19 GC.Collect(); 20 } 21 22 Console.WriteLine("Press any key to exit!"); 23 Console.ReadLine(); 24 } 25 26 [DllImport("ExampleCore_7_044.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode)] 27 private static extern void GetDate(StringBuilder date); 28 } 29 }
3.7、ExampleCore_7_044(動態連結庫,C++)
1 #include <iostream> 2 #include <Windows.h> 3 using namespace std; 4 5 extern "C" 6 { 7 _declspec(dllexport) void __stdcall GetDate(WCHAR* pszDate); 8 } 9 10 void __stdcall GetDate(WCHAR* pszDate) 11 { 12 SYSTEMTIME time; 13 WCHAR* pszTmpDate = new WCHAR[200]; 14 15 GetSystemTime(&time); 16 17 wsprintf(pszTmpDate, L"%d-%d-%d", time.wMonth, time.wDay, time.wYear); 18 19 wcscpy(pszDate, pszTmpDate); 20 }
四、基礎知識
在這一段內容中,有的小節可能會包含兩個部分,分別是 A 和 B,也有可能只包含 A,如果只包含 A 部分,A 字母會省略。A 是【基礎知識】,講解必要的知識點,B 是【眼見為實】,透過除錯證明講解的知識點。
4.1、平臺呼叫
A、基礎知識
平臺呼叫服務 P/Invoke 是 CLR 的一部分,負責確保託管程式碼可以呼叫從非託管程式集中匯出的各種函式,原因很簡單,託管型別引數和非託管型別引數是不一致的,比如:託管的引用型別是帶有附加資訊的,而非託管型別是不可能有的。
如果需要呼叫非託管的函式,可以使用 P/Invoke 來實現。透過 P/Invoke 來呼叫函式的基本過程如下:
I、定義託管函式與非託管函式對應。
II、用 DllImport 特性來修飾這個託管函式,表示它代表一個非託管函式。
III、呼叫託管程式碼函式,從而使 CLR 載入 Dll 並在呼叫階段切換到非託管函式。
DllImport 特性用來表示這個函式對應於一個 P/Invoke 定義,SetLastError 屬性表示這個函式退出時設定最近的錯誤。
可以使用 ln 命令來幫助確定指標指向的內容。 檢視損壞的堆疊以確定呼叫哪個過程時,此命令也很有用。
具體解釋:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debuggercmds/ln--list-nearest-symbols-?source=recommendations
當發生託管程式碼呼叫非託管程式碼的時候,就會發生【切換棧幀】。切換棧幀 需要根據被呼叫的非託管函式的複雜性來處理各種不同的模式。在這些模式中,最重要的就是在切換過程中發生的列集(marshling)操作。列集是指在不同的資料表示形式之間的轉換,這是因為託管環境和非託管環境是不一樣的。對於簡單型別列集操作可以自動完成,對於複雜的型別就需要做特殊處理了。
在 P/Invoke 層中使用瞭如下演算法:
I)、將指定的模組(DLL)載入到程序的地址空間中。
II)、找到所需函式的地址。
III)、對資料進行列集封裝。
IIII)、呼叫函式。
B、眼見為實
除錯原始碼:ExampleCore_7_01
除錯任務:觀察 CLR 是如何透過 P/Invoke 實現呼叫非託管函式的。
因為【Beep】是Windows 提供的蜂鳴函式,可以直接用【bp】命令下斷點。當斷電觸發時,觀察棧回溯,分析 CLR 是如何呼叫非託管程式碼的。過了這麼多年,這個函式的名稱也有了變化,現在是【KERNEL32!BeepImplementation】。
1)、NTSD 除錯
編譯我們的專案,開啟【Visual Studio 2022 Developer Command Prompt v17.9.6】命令列工具,輸入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_7_01\bin\Debug\net8.0\ExampleCore_7_01.exe】,開啟【NTSD】偵錯程式。
當我們進入 Windbg 偵錯程式介面後,我們使用【x kernel32!*beep*】命令,查詢一下【Beep】這個函式。
1 0:000> x kernel32!*beep* 2 00007ffd`e45b6980 KERNEL32!BeepImplementation (BeepImplementation) 3 00007ffd`e4602418 KERNEL32!_imp_Beep = <no type information>
有了地址,我們就可以針對這個地址下斷點,執行命令【bp 00007ffd`e45b6980】,或者【bp KERNEL32!BeepImplementation】這兩種形式都是可以的。我們可以使用【bl】命令檢視斷點列表。
1 0:000> bp 00007ffd`e45b6980 2 0:000> bl 3 0 e 00007ffd`e45b6980 0001 (0001) 0:**** KERNEL32!BeepImplementation 4 0:000>
斷點設定成功後,【g】直接執行偵錯程式,它會在斷點出暫停。
1 0:000> g 2 ModLoad: 00007ffd`e6150000 00007ffd`e6182000 C:\Windows\System32\IMM32.DLL 3 ModLoad: 00007ffd`c77e0000 00007ffd`c7839000 C:\Program Files\dotnet\host\fxr\8.0.4\hostfxr.dll 4 ModLoad: 00007ffd`28960000 00007ffd`289c4000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\hostpolicy.dll 5 ModLoad: 00007ffd`0cca0000 00007ffd`0d186000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\coreclr.dll 6 ModLoad: 00007ffd`e4cf0000 00007ffd`e4e1b000 C:\Windows\System32\ole32.dll 7 ModLoad: 00007ffd`e4990000 00007ffd`e4ce3000 C:\Windows\System32\combase.dll 8 ModLoad: 00007ffd`e4820000 00007ffd`e48ed000 C:\Windows\System32\OLEAUT32.dll 9 ModLoad: 00007ffd`e3f80000 00007ffd`e4002000 C:\Windows\System32\bcryptPrimitives.dll 10 (48c4.479c): Unknown exception - code 04242420 (first chance) 11 ModLoad: 00007ffd`0c010000 00007ffd`0cc9c000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll 12 ModLoad: 00007ffd`0ee90000 00007ffd`0f049000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\clrjit.dll 13 ModLoad: 00007ffd`e3b90000 00007ffd`e3ba2000 C:\Windows\System32\kernel.appcore.dll 14 ModLoad: 00000289`b4cf0000 00000289`b4cf8000 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_7_01\bin\Debug\net8.0\ExampleCore_7_01.dll 15 ModLoad: 00000289`b4d00000 00000289`b4d0e000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Runtime.dll 16 ModLoad: 00007ffd`c37c0000 00007ffd`c37e8000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Console.dll 17 Breakpoint 0 hit 18 KERNEL32!BeepImplementation: 19 00007ffd`e45b6980 48895c2418 mov qword ptr [rsp+18h],rbx ss:000000b3`e0f9e6b0=000000b3e0f9e9a0
我們此時使用【!clrstack】命令看看棧回溯。
1 0:000> !clrstack 2 OS Thread Id: 0x479c (0) 3 Child SP IP Call Site 4 000000B3E0F9E6C8 00007ffde45b6980 [InlinedCallFrame: 000000b3e0f9e6c8] ExampleCore_7_01.Program.Beep(UInt32, UInt32) 5 000000B3E0F9E6C8 00007ffcad271a63 [InlinedCallFrame: 000000b3e0f9e6c8] ExampleCore_7_01.Program.Beep(UInt32, UInt32) 6 000000B3E0F9E6A0 00007FFCAD271A63 ILStubClass.IL_STUB_PInvoke(UInt32, UInt32) 7 000000B3E0F9E790 00007FFCAD271963 ExampleCore_7_01.Program.Main(System.String[])
我們看到了 ExampleCore_7_01.Program.Main 方法呼叫了 ExampleCore_7_01.Program.Beep 方法。ExampleCore_7_01.Program.Beep 方法對應的棧幀有如下一個字首:[InlinedCallFrame: 000000b3e0f9e6c8]
000000b3e0f9e6c8 這個地址我們使用【dp 000000b3e0f9e6c8】命令檢視一下它的內容。1 0:000> dp 000000b3e0f9e6c8 2 000000b3`e0f9e6c8 00007ffd`0d09a548 ffffffff`ffffffff 3 000000b3`e0f9e6d8 00007ffc`ad3200c0 00007ffc`ad3200c0 4 000000b3`e0f9e6e8 000000b3`e0f9e6a0 00007ffc`ad271a63 5 000000b3`e0f9e6f8 000000b3`e0f9e780 00000000`b7808e98 6 000000b3`e0f9e708 00007ffc`ad3200c0 00000289`b3427b10 7 000000b3`e0f9e718 00000000`00000000 00007ffd`e45b6980 8 000000b3`e0f9e728 00000000`00000000 00000000`000003e8 9 000000b3`e0f9e738 00000000`000003e8 00000000`00000001
00007ffd`0d09a548 針對這個地址,我們使用【ln 00007ffd`0d09a548】命令,是什麼東西。
1 0:000> ln 00007ffd`0d09a548 2 (00007ffd`0d09a548) coreclr!InlinedCallFrame::`vftable' | (00007ffd`0d09a5d8) coreclr!vtable_DebuggerSecurityCodeMarkFrame 3 Exact matches: 4 coreclr!vtable_InlinedCallFrame = 0x00007ffd`0cdf9d80 5 coreclr!InlinedCallFrame::`vftable' = <function> *[18]
1 0:000> dp 00007ffd`0d09a548 2 00007ffd`0d09a548 00007ffd`0cdf9d80 00007ffd`0cdf9d90 3 00007ffd`0d09a558 00007ffd`0cdf9d80 00007ffd`0cdf8ac0 4 00007ffd`0d09a568 00007ffd`0cdf9de0 00007ffd`0cd5cbf0 5 00007ffd`0d09a578 00007ffd`0cea42b0 00007ffd`0cdf9d90 6 00007ffd`0d09a588 00007ffd`0cd5c4d0 00007ffd`0cdc1750 7 00007ffd`0d09a598 00007ffd`0cdf9d90 00007ffd`0cd5c4f0 8 00007ffd`0d09a5a8 00007ffd`0cdf9d90 00007ffd`0cdfa060 9 00007ffd`0d09a5b8 00007ffd`0cdf9d90 00007ffd`0cea4320
00007ffd`0cdf9d80 針對這個地址,我們繼續使用【ln 00007ffd`0cdf9d80】命令看看它的內容。
1 0:000> ln 00007ffd`0cdf9d80 2 (00007ffd`0cdf9d80) coreclr!LoaderAllocator::CleanupDependentHandlesToNativeObjects | (00007ffd`0cdf9d90) coreclr!BaseDomain::IsAppDomain 3 Exact matches: 4 coreclr!DebuggerController::TriggerFuncEvalExit (class Thread *) 5 coreclr!standalone::GCToEEInterface::WalkAsyncPinnedForPromotion (class Object *, struct ScanContext *, <function> *) 6 coreclr!DispatchStubState::SetLastError (int) 7 coreclr!BINDER_SPACE::AssemblyVersion::~AssemblyVersion (void) 8 coreclr!ThreadDebugBlockingInfo::~ThreadDebugBlockingInfo (void) 9 coreclr!StubCacheBase::AddStub (unsigned char *, class Stub *) 10 coreclr!DispatchStubState::MarshalLCID (int) 11 coreclr!JIT_DebugLogLoopCloning (void) 12 coreclr!SVR::GCHeap::Shutdown (void) 13 coreclr!LoaderAllocator::UnregisterDependentHandleToNativeObjectFromCleanup (class LADependentHandleToNativeObject *) 14 coreclr!DispatchStubState::MarshalReturn (class MarshalInfo *, int) 15 coreclr!StgPoolSeg::~StgPoolSeg (void) 16 coreclr!noncopyable::~noncopyable (void) 17 coreclr!standalone::GCToEEInterface::WalkAsyncPinned (class Object *, void *, <function> *) 18 coreclr!HashMap::Iterator::~Iterator (void) 19 coreclr!standalone::GCToEEInterface::SyncBlockCachePromotionsGranted (int) 20 coreclr!EEClass::~EEClass (void) 21 coreclr!ILMarshaler::EmitCreateMngdMarshaler (class ILCodeStream *) 22 coreclr!ILMarshaler::EmitClearCLR (class ILCodeStream *) 23 coreclr!CEEInfo::methodMustBeLoadedBeforeCodeIsRun (struct CORINFO_METHOD_STRUCT_ *) 24 coreclr!LADependentNativeObject::~LADependentNativeObject (void) 25 coreclr!CEEJitInfo::recordCallSite (unsigned int, struct CORINFO_SIG_INFO *, struct CORINFO_METHOD_STRUCT_ *) 26 coreclr!Frame::ExceptionUnwind (void) 27 coreclr!EEDbgInterfaceImpl::ClearThreadException (class Thread *) 28 coreclr!MethodTable::MethodDataInterfaceImpl::UpdateImplMethodDesc (class MethodDesc *, unsigned int) 29 coreclr!DebuggerController::TriggerMethodEnter (class Thread *, class DebuggerJitInfo *, unsigned char *, class FramePointer) 30 coreclr!DebuggerController::TriggerUnwind (class Thread *, class MethodDesc *, class DebuggerJitInfo *, unsigned int64, class FramePointer, CorDebugStepReason) 31 coreclr!DebuggerController::DebuggerDetachClean (void) 32 coreclr!LoaderAllocator::RegisterDependentHandleToNativeObjectForCleanup (class LADependentHandleToNativeObject *) 33 coreclr!ComPrestubMethodFrame::ExceptionUnwind (void) 34 coreclr!EEDbgInterfaceImpl::DebuggerModifyingLogSwitch (int, wchar_t *) 35 coreclr!CEEInfo::updateEntryPointForTailCall (struct CORINFO_CONST_LOOKUP *) 36 coreclr!ILMarshaler::EmitConvertSpaceCLRToNative (class ILCodeStream *) 37 coreclr!EETypeHashTable::Iterator::~Iterator (void) 38 coreclr!InstMethodHashTable::Iterator::~Iterator (void) 39 coreclr!LoaderAllocator::ReleaseManagedAssemblyLoadContext (void) 40 coreclr!ILMarshaler::EmitConvertContentsCLRToNative (class ILCodeStream *) 41 coreclr!DebuggerController::TriggerFuncEvalEnter (class Thread *) 42 coreclr!CrossLoaderAllocatorHash<MethodDescBackpatchInfoTracker::BackpatchInfoTrackerHashTraits>::KeyValueStoreOrLAHashKeyToTrackers::~KeyValueStoreOrLAHashKeyToTrackers (void) 43 coreclr!PtrHashMap::PtrIterator::~PtrIterator (void) 44 coreclr!ILMarshaler::EmitSetupArgumentForMarshalling (class ILCodeStream *) 45 coreclr!ILMarshaler::EmitClearNative (class ILCodeStream *) 46 coreclr!OleVariant::MarshalCBoolVariantOleRefToCom (struct tagVARIANT *, struct VariantData *) 47 coreclr!ILMarshaler::EmitMarshalViaPinning (class ILCodeStream *) 48 coreclr!Frame::UpdateRegDisplay (struct REGDISPLAY *) 49 coreclr!LoaderAllocator::CleanupDependentHandlesToNativeObjects (void) 50 coreclr!MethodTable::MethodDataInterface::InvalidateCachedVirtualSlot (unsigned int) 51 coreclr!FrameBase::GcScanRoots (<function> *, struct ScanContext *) 52 coreclr!OleVariant::MarshalCBoolVariantComToOle (struct VariantData *, struct tagVARIANT *) 53 coreclr!EmptyApcCallback (unsigned int64) 54 coreclr!MDInternalRO::EnumMethodImplClose (struct HENUMInternal *, struct HENUMInternal *) 55 coreclr!OleVariant::MarshalWinBoolVariantOleRefToCom (struct tagVARIANT *, struct VariantData *) 56 coreclr!EEJitManager::EnumMemoryRegionsForMethodUnwindInfo (CLRDataEnumMemoryFlags, class EECodeInfo *) 57 coreclr!LoaderAllocator::CleanupHandles (void) 58 coreclr!OleVariant::MarshalWinBoolVariantComToOle (struct VariantData *, struct tagVARIANT *) 59 coreclr!block_serialize_header_func (void *, struct _FastSerializer *) 60 coreclr!OleVariant::MarshalAnsiCharVariantComToOle (struct VariantData *, struct tagVARIANT *) 61 coreclr!CrossLoaderAllocatorHash<InliningInfoTrackerHashTraits>::KeyValueStoreOrLAHashKeyToTrackers::~KeyValueStoreOrLAHashKeyToTrackers (void) 62 coreclr!JIT_LogMethodEnter (struct CORINFO_METHOD_STRUCT_ *) 63 coreclr!JIT_StressGC (void) 64 coreclr!listen_port_reset (void *, <function> *) 65 coreclr!OleVariant::MarshalAnsiCharVariantOleRefToCom (struct tagVARIANT *, struct VariantData *) 66 coreclr!_guard_check_icall_nop (unsigned int64) 67 coreclr!standalone::GCToEEInterface::UpdateGCEventStatus (int, int, int, int) 68 coreclr!OleVariant::MarshalWinBoolVariantOleToCom (struct tagVARIANT *, struct VariantData *) 69 coreclr!OleVariant::MarshalAnsiCharVariantOleToCom (struct tagVARIANT *, struct VariantData *) 70 coreclr!WKS::GCHeap::Shutdown (void) 71 coreclr!CEEInfo::classMustBeLoadedBeforeCodeIsRun (struct CORINFO_CLASS_STRUCT_ *) 72 coreclr!LCGMethodResolver::FreeCompileTimeState (void) 73 coreclr!DebuggerController::TriggerTraceCall (class Thread *, unsigned char *) 74 coreclr!Debugger::CleanupTransportSocket (void) 75 coreclr!ILMarshaler::EmitClearNativeContents (class ILCodeStream *) 76 coreclr!LoaderAllocator::RegisterHandleForCleanup (struct OBJECTHANDLE__ *) 77 coreclr!LoaderAllocator::UnregisterHandleFromCleanup (struct OBJECTHANDLE__ *) 78 coreclr!UnmanagedToManagedFrame::ExceptionUnwind (void) 79 coreclr!EEDbgInterfaceImpl::ClearAllDebugInterfaceReferences (void) 80 coreclr!ILMarshaler::EmitClearCLRContents (class ILCodeStream *) 81 coreclr!OleVariant::MarshalCBoolVariantOleToCom (struct tagVARIANT *, struct VariantData *) 82 coreclr!MethodTable::MethodDataInterface::UpdateImplMethodDesc (class MethodDesc *, unsigned int) 83 coreclr!CEEInfo::beginInlining (struct CORINFO_METHOD_STRUCT_ *, struct CORINFO_METHOD_STRUCT_ *) 84 coreclr!ILMarshaler::EmitConvertSpaceNativeToCLR (class ILCodeStream *) 85 coreclr!DispParamMarshaler::CleanUpManaged (class Object **) 86 coreclr!ILMarshaler::EmitConvertContentsNativeToCLR (class ILCodeStream *) 87 0:000>
00007ffd`0d09a548 這個地址就是 coreclr!InlinedCallFrame 的虛擬函式表(vftable),我們可以使用【!u 00007ffd`0d09a548】命令檢視它的彙編原始碼。
1 0:000> !u 00007ffd`0d09a548 2 Unmanaged code 3 00007ffd`0d09a548 809ddf0cfd7f00 sbb byte ptr [rbp+7FFD0CDFh],0 4 00007ffd`0d09a54f 00909ddf0cfd add byte ptr [rax-2F32063h],dl 5 00007ffd`0d09a555 7f00 jg coreclr!InlinedCallFrame::`vftable'+0xf (00007ffd`0d09a557) 6 00007ffd`0d09a557 00809ddf0cfd add byte ptr [rax-2F32063h],al 7 00007ffd`0d09a55d 7f00 jg coreclr!InlinedCallFrame::`vftable'+0x17 (00007ffd`0d09a55f) 8 00007ffd`0d09a55f 00c0 add al,al 9 00007ffd`0d09a561 8adf mov bl,bh 10 00007ffd`0d09a563 0cfd or al,0FDh 11 00007ffd`0d09a565 7f00 jg coreclr!InlinedCallFrame::`vftable'+0x1f (00007ffd`0d09a567) 12 00007ffd`0d09a567 00e0 add al,ah
2)、Windbg Preview 除錯
我們編譯專案,開啟【Windbg Preview】使用者態偵錯程式,依次點選【檔案】----》【Launch executable】載入我們可執行程式 ExampleCore_7_01.exe,開啟偵錯程式的介面,程式已經處於中斷狀態。
當我們進入 Windbg 偵錯程式介面後,我們使用【x kernel32!*beep*】命令,查詢一下【Beep】這個函式。
1 0:000> x kernel32!*beep* 2 00007ffd`e45b6980 KERNEL32!BeepImplementation (BeepImplementation) 3 00007ffd`e4602418 KERNEL32!_imp_Beep = <no type information>
然後我們在這個方法上下斷點,透過【bp KERNEL32!BeepImplementation 】命令下斷點。
1 0:000> bp KERNEL32!BeepImplementation
斷點設定成功後,然後繼續【g】執行偵錯程式,會在我們設定的斷點出中斷執行。
1 0:000> g 2 ModLoad: 00007ffd`e6150000 00007ffd`e6182000 C:\Windows\System32\IMM32.DLL 3 ModLoad: 00007ffd`1f370000 00007ffd`1f3c9000 C:\Program Files\dotnet\host\fxr\8.0.4\hostfxr.dll 4 ModLoad: 00007ffd`0f1a0000 00007ffd`0f204000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\hostpolicy.dll 5 ModLoad: 00007ffd`0cde0000 00007ffd`0d2c6000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\coreclr.dll 6 ModLoad: 00007ffd`e4cf0000 00007ffd`e4e1b000 C:\Windows\System32\ole32.dll 7 ModLoad: 00007ffd`e4990000 00007ffd`e4ce3000 C:\Windows\System32\combase.dll 8 ModLoad: 00007ffd`e4820000 00007ffd`e48ed000 C:\Windows\System32\OLEAUT32.dll 9 ModLoad: 00007ffd`e3f80000 00007ffd`e4002000 C:\Windows\System32\bcryptPrimitives.dll 10 (2f24.4f6c): Unknown exception - code 04242420 (first chance) 11 ModLoad: 00007ffd`0bb40000 00007ffd`0c7cc000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll 12 ModLoad: 00007ffd`03aa0000 00007ffd`03c59000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\clrjit.dll 13 ModLoad: 00007ffd`e3b90000 00007ffd`e3ba2000 C:\Windows\System32\kernel.appcore.dll 14 ModLoad: 00000180`c62a0000 00000180`c62a8000 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_7_01\bin\Debug\net8.0\ExampleCore_7_01.dll 15 ModLoad: 00000180`c62b0000 00000180`c62be000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Runtime.dll 16 ModLoad: 00007ffd`294b0000 00007ffd`294d8000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Console.dll 17 Breakpoint 0 hit 18 KERNEL32!BeepImplementation: 19 00007ffd`e45b6980 48895c2418 mov qword ptr [rsp+18h],rbx ss:00000025`eb17e610=00000025eb17e900
紅色標註的就是我們想要斷住的方法。到了這裡,我們看看當前的呼叫棧,使用【!clrstack】命令。
1 0:000> !clrstack 2 OS Thread Id: 0x4f6c (0) 3 Child SP IP Call Site 4 00000025EB17E628 00007ffde45b6980 [InlinedCallFrame: 00000025eb17e628] ExampleCore_7_01.Program.Beep(UInt32, UInt32) 5 00000025EB17E628 00007ffcad3b1a63 [InlinedCallFrame: 00000025eb17e628] ExampleCore_7_01.Program.Beep(UInt32, UInt32) 6 00000025EB17E600 00007ffcad3b1a63 ILStubClass.IL_STUB_PInvoke(UInt32, UInt32) 7 00000025EB17E6F0 00007ffcad3b1963 ExampleCore_7_01.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_7_01\Program.cs @ 13]
從【!clrstack】命令的輸出可以看到,ExampleCore_7_01.Program.Main 方法正在呼叫 ExampleCore_7_01.Program.Beep 方法。與 ExampleCore_7_01.Program.Beep 方法對應的棧幀有著如下的字首:[InlinedCallFrame: 00000025eb17e628]。
[InlinedCallFrame: 00000025eb17e628] 這樣有一個地址,就是一個棧針,這個棧針就是 CLR 裡面的部分,這個棧針地址就會呼叫 LoadLibrary方法,載入【Kernel32.dll】,如果載入了這個dll,就不需要在載入了,如果沒有載入才載入。載入了 dll 找到 Beep 方法的方發表,呼叫執行就可以了。
我們可以使用【dp 00000025eb17e628】命令檢視一下這個指標。
1 0:000> dp 00000025eb17e628 2 00000025`eb17e628 00007ffd`0d1da548 ffffffff`ffffffff 3 00000025`eb17e638 00007ffc`ad4600c0 00007ffc`ad4600c0 4 00000025`eb17e648 00000025`eb17e600 00007ffc`ad3b1a63 5 00000025`eb17e658 00000025`eb17e6e0 00000000`c9008e98 6 00000025`eb17e668 00007ffc`ad4600c0 00000180`c48fe920 7 00000025`eb17e678 00000000`00000000 00007ffd`e45b6980 8 00000025`eb17e688 00000000`00000000 00000000`000003e8 9 00000025`eb17e698 00000000`000003e8 00000000`00000001
00007ffd`0d1da548 這個地址像一個程式碼地址,因此,我們使用【ln 00007ffd`0d1da548】命令檢視該地址能否被解析成程式碼。
1 0:000> ln 00007ffd`0d1da548 2 Browse module 3 Set bu breakpoint 4 5 (00007ffd`0d1da548) coreclr!InlinedCallFrame::`vftable' | (00007ffd`0d1da5d8) coreclr!vtable_DebuggerSecurityCodeMarkFrame 6 Exact matches: 7 coreclr!vtable_InlinedCallFrame = 0x00007ffd`0cf39d80 8 coreclr!InlinedCallFrame::`vftable' = <function> *[18]
當然,我們也可以使用【!u 00007ffd`0d1da548】命令,檢視 coreclr!InlinedCallFrame 方法的原始碼,這個原始碼是彙編原始碼。
1 0:000> !u 00007ffd`0d1da548 2 Unmanaged code 3 00007ffd`0d1da548 809df30cfd7f00 sbb byte ptr [rbp+7FFD0CF3h],0 4 00007ffd`0d1da54f 00909df30cfd add byte ptr [rax-2F30C63h],dl 5 00007ffd`0d1da555 7f00 jg coreclr!InlinedCallFrame::`vftable'+0xf (00007ffd`0d1da557) 6 00007ffd`0d1da557 00809df30cfd add byte ptr [rax-2F30C63h],al 7 00007ffd`0d1da55d 7f00 jg coreclr!InlinedCallFrame::`vftable'+0x17 (00007ffd`0d1da55f) 8 00007ffd`0d1da55f 00c0 add al,al 9 00007ffd`0d1da561 8af3 mov dh,bl 10 00007ffd`0d1da563 0cfd or al,0FDh 11 00007ffd`0d1da565 7f00 jg coreclr!InlinedCallFrame::`vftable'+0x1f (00007ffd`0d1da567) 12 00007ffd`0d1da567 00e0 add al,ah
輸出資訊表明這個地址對應於物件 InlinedCallFrame 的虛擬函式表。我們可以進一步將虛擬函式錶轉儲出來,執行命令【dp 00007ffd`0d1da548 l4】。
0:000> dp 00007ffd`0d1da548 l4 00007ffd`0d1da548 00007ffd`0cf39d80 00007ffd`0cf39d90 00007ffd`0d1da558 00007ffd`0cf39d80 00007ffd`0cf38ac0
00007ffd`0cf39d80 並在這個函式地址上使用【ln 00007ffd`0cf39d80】命令來觀察它所包含的內容。
1 0:000> ln 00007ffd`0cf39d80 2 Browse module 3 Set bu breakpoint 4 5 [D:\a\_work\1\s\src\coreclr\inc\utilcode.h @ 417] SrcSrv Command: https://raw.githubusercontent.com/dotnet/runtime/2d7eea252964e69be94cb9c847b371b23e4dd470/src/coreclr/inc/utilcode.h 6 (00007ffd`0cf39d80) coreclr!LoaderAllocator::CleanupDependentHandlesToNativeObjects | (00007ffd`0cf39d90) coreclr!BaseDomain::IsAppDomain 7 Exact matches: 8 coreclr!DebuggerController::TriggerFuncEvalExit (class Thread *) 9 coreclr!standalone::GCToEEInterface::WalkAsyncPinnedForPromotion (class Object *, struct ScanContext *, <function> *) 10 coreclr!DispatchStubState::SetLastError (int) 11 coreclr!BINDER_SPACE::AssemblyVersion::~AssemblyVersion (void) 12 coreclr!ThreadDebugBlockingInfo::~ThreadDebugBlockingInfo (void) 13 coreclr!StubCacheBase::AddStub (unsigned char *, class Stub *) 14 coreclr!DispatchStubState::MarshalLCID (int) 15 coreclr!JIT_DebugLogLoopCloning (void) 16 coreclr!SVR::GCHeap::Shutdown (void) 17 coreclr!LoaderAllocator::UnregisterDependentHandleToNativeObjectFromCleanup (class LADependentHandleToNativeObject *) 18 coreclr!DispatchStubState::MarshalReturn (class MarshalInfo *, int) 19 coreclr!StgPoolSeg::~StgPoolSeg (void) 20 coreclr!noncopyable::~noncopyable (void) 21 coreclr!standalone::GCToEEInterface::WalkAsyncPinned (class Object *, void *, <function> *) 22 coreclr!HashMap::Iterator::~Iterator (void) 23 coreclr!standalone::GCToEEInterface::SyncBlockCachePromotionsGranted (int) 24 coreclr!EEClass::~EEClass (void) 25 coreclr!ILMarshaler::EmitCreateMngdMarshaler (class ILCodeStream *) 26 coreclr!ILMarshaler::EmitClearCLR (class ILCodeStream *) 27 coreclr!CEEInfo::methodMustBeLoadedBeforeCodeIsRun (struct CORINFO_METHOD_STRUCT_ *) 28 coreclr!LADependentNativeObject::~LADependentNativeObject (void) 29 coreclr!CEEJitInfo::recordCallSite (unsigned int, struct CORINFO_SIG_INFO *, struct CORINFO_METHOD_STRUCT_ *) 30 coreclr!Frame::ExceptionUnwind (void) 31 coreclr!EEDbgInterfaceImpl::ClearThreadException (class Thread *) 32 coreclr!MethodTable::MethodDataInterfaceImpl::UpdateImplMethodDesc (class MethodDesc *, unsigned int) 33 coreclr!DebuggerController::TriggerMethodEnter (class Thread *, class DebuggerJitInfo *, unsigned char *, class FramePointer) 34 coreclr!DebuggerController::TriggerUnwind (class Thread *, class MethodDesc *, class DebuggerJitInfo *, unsigned int64, class FramePointer, CorDebugStepReason) 35 coreclr!DebuggerController::DebuggerDetachClean (void) 36 coreclr!LoaderAllocator::RegisterDependentHandleToNativeObjectForCleanup (class LADependentHandleToNativeObject *) 37 coreclr!ComPrestubMethodFrame::ExceptionUnwind (void) 38 coreclr!EEDbgInterfaceImpl::DebuggerModifyingLogSwitch (int, wchar_t *) 39 coreclr!CEEInfo::updateEntryPointForTailCall (struct CORINFO_CONST_LOOKUP *) 40 coreclr!ILMarshaler::EmitConvertSpaceCLRToNative (class ILCodeStream *) 41 coreclr!EETypeHashTable::Iterator::~Iterator (void) 42 coreclr!InstMethodHashTable::Iterator::~Iterator (void) 43 coreclr!LoaderAllocator::ReleaseManagedAssemblyLoadContext (void) 44 coreclr!ILMarshaler::EmitConvertContentsCLRToNative (class ILCodeStream *) 45 coreclr!DebuggerController::TriggerFuncEvalEnter (class Thread *) 46 coreclr!CrossLoaderAllocatorHash<MethodDescBackpatchInfoTracker::BackpatchInfoTrackerHashTraits>::KeyValueStoreOrLAHashKeyToTrackers::~KeyValueStoreOrLAHashKeyToTrackers (void) 47 coreclr!PtrHashMap::PtrIterator::~PtrIterator (void) 48 coreclr!ILMarshaler::EmitSetupArgumentForMarshalling (class ILCodeStream *) 49 coreclr!ILMarshaler::EmitClearNative (class ILCodeStream *) 50 coreclr!OleVariant::MarshalCBoolVariantOleRefToCom (struct tagVARIANT *, struct VariantData *) 51 coreclr!ILMarshaler::EmitMarshalViaPinning (class ILCodeStream *) 52 coreclr!Frame::UpdateRegDisplay (struct REGDISPLAY *) 53 coreclr!LoaderAllocator::CleanupDependentHandlesToNativeObjects (void) 54 coreclr!MethodTable::MethodDataInterface::InvalidateCachedVirtualSlot (unsigned int) 55 coreclr!FrameBase::GcScanRoots (<function> *, struct ScanContext *) 56 coreclr!OleVariant::MarshalCBoolVariantComToOle (struct VariantData *, struct tagVARIANT *) 57 coreclr!EmptyApcCallback (unsigned int64) 58 coreclr!MDInternalRO::EnumMethodImplClose (struct HENUMInternal *, struct HENUMInternal *) 59 coreclr!OleVariant::MarshalWinBoolVariantOleRefToCom (struct tagVARIANT *, struct VariantData *) 60 coreclr!EEJitManager::EnumMemoryRegionsForMethodUnwindInfo (CLRDataEnumMemoryFlags, class EECodeInfo *) 61 coreclr!LoaderAllocator::CleanupHandles (void) 62 coreclr!OleVariant::MarshalWinBoolVariantComToOle (struct VariantData *, struct tagVARIANT *) 63 coreclr!block_serialize_header_func (void *, struct _FastSerializer *) 64 coreclr!OleVariant::MarshalAnsiCharVariantComToOle (struct VariantData *, struct tagVARIANT *) 65 coreclr!CrossLoaderAllocatorHash<InliningInfoTrackerHashTraits>::KeyValueStoreOrLAHashKeyToTrackers::~KeyValueStoreOrLAHashKeyToTrackers (void) 66 coreclr!JIT_LogMethodEnter (struct CORINFO_METHOD_STRUCT_ *) 67 coreclr!JIT_StressGC (void) 68 coreclr!listen_port_reset (void *, <function> *) 69 coreclr!OleVariant::MarshalAnsiCharVariantOleRefToCom (struct tagVARIANT *, struct VariantData *) 70 coreclr!_guard_check_icall_nop (unsigned int64) 71 coreclr!standalone::GCToEEInterface::UpdateGCEventStatus (int, int, int, int) 72 coreclr!OleVariant::MarshalWinBoolVariantOleToCom (struct tagVARIANT *, struct VariantData *) 73 coreclr!OleVariant::MarshalAnsiCharVariantOleToCom (struct tagVARIANT *, struct VariantData *) 74 coreclr!WKS::GCHeap::Shutdown (void) 75 coreclr!CEEInfo::classMustBeLoadedBeforeCodeIsRun (struct CORINFO_CLASS_STRUCT_ *) 76 coreclr!LCGMethodResolver::FreeCompileTimeState (void) 77 coreclr!DebuggerController::TriggerTraceCall (class Thread *, unsigned char *) 78 coreclr!Debugger::CleanupTransportSocket (void) 79 coreclr!ILMarshaler::EmitClearNativeContents (class ILCodeStream *) 80 coreclr!LoaderAllocator::RegisterHandleForCleanup (struct OBJECTHANDLE__ *) 81 coreclr!LoaderAllocator::UnregisterHandleFromCleanup (struct OBJECTHANDLE__ *) 82 coreclr!UnmanagedToManagedFrame::ExceptionUnwind (void) 83 coreclr!EEDbgInterfaceImpl::ClearAllDebugInterfaceReferences (void) 84 coreclr!ILMarshaler::EmitClearCLRContents (class ILCodeStream *) 85 coreclr!OleVariant::MarshalCBoolVariantOleToCom (struct tagVARIANT *, struct VariantData *) 86 coreclr!MethodTable::MethodDataInterface::UpdateImplMethodDesc (class MethodDesc *, unsigned int) 87 coreclr!CEEInfo::beginInlining (struct CORINFO_METHOD_STRUCT_ *, struct CORINFO_METHOD_STRUCT_ *) 88 coreclr!ILMarshaler::EmitConvertSpaceNativeToCLR (class ILCodeStream *) 89 coreclr!DispParamMarshaler::CleanUpManaged (class Object **) 90 coreclr!ILMarshaler::EmitConvertContentsNativeToCLR (class ILCodeStream *)
不是很難,就不多做解釋了。
4.2、COM
元件物件模型(Component Object Model,COM)是微軟在 1993 年引入的一種二進位制介面。它提供了一種通用的方式來定義與語言無關的元件,並且 COM 元件可以跨越機器的邊界來建立和使用。COM 是作為一種標準引入的,透過 COM 互用性實現了託管程式碼與現有 COM 物件的互動,也可以說是實現了與非託管程式碼的一種互動方式。這種互動可以是雙向的,因為託管程式碼可以呼叫現有的非託管程式碼 COM 物件,而非託管程式碼也可以呼叫以 COM 形式出現的託管物件。
元件物件模型 (COM) 允許物件向其他元件公開其功能並在 Windows 平臺上託管應用程式。 為了實現與其現有程式碼庫的互操作,.NET Framework 始終為與 COM 庫進行互操作提供強大支援。 在 .NET Core 3.0 中,此支援中的很大一部分已新增到 Windows 上的 .NET Core。
COM 互操作功能可以透過 .NET 執行時中的內建系統或透過實現 ComWrappers API(在 .NET 6 中引入)來實現。 從 .NET 8 開始,可以使用 COM 源生成器 自動實現基於-IUnknown 介面的 ComWrappers API。
如果大家想了解更多的內容,可以去微軟官網檢視。官網地址:https://learn.microsoft.com/zh-cn/dotnet/standard/native-interop/cominterop
在 COM 互用性中包含三個實體:COM 二進位制檔案、託管客戶端、PIA(Primary Interop Assembly,PIA,主互呼叫程式集)。Tlbimp.exe 可以利用 COM 二進位制檔案生成一個 PIA,這個 Tlbimp.exe 是 .NET SDK 的一部分。除了這個三個實體,還包含第四個實體,執行時刻呼叫封裝(RCW),這個實體是在執行時被建立的。我們直接來一張圖,看看這四個實體之間的關係,如圖:
如圖所示,首先是託管客戶端呼叫 COM 物件中定義的方法,該物件是在 PIA 中定義的。CLR 透過來自 PIA 的資訊建立 RCW 的例項。然後,RCW 截獲對這個方法的呼叫,將引數轉換為非託管型別,切換環境,並且呼叫非託管程式碼中的方法。
RCW 另外一個功能負責處理底層 COM 物件的生命週期。COM 物件的生命週期是透過一種引用計數模式來管理的,這就意味著每當獲取物件的一個介面時,引用計數就會增加。相反,當不在需要一個介面時,引用計數就會遞減。當引用計數為 0 時,就可以銷燬物件了。RCW 能跟蹤引用的數量,並確保相應的遞增/遞減引用計數。當託管客戶端使用完 RCW 並且不存在未釋放的引用後, RCW 會被回收,並且相關的 COM 物件都會被釋放。
RCW 有兩種釋放的方式:第一種,當不存在對 RCW 的引用後,RCW 會遞減並且清除對底層 COM 物件的任何引用,因而 COM 物件直到垃圾收集器清除時才會被清除。第二種,我們可以使用 Marshal.ReleaseComObject 方法強制釋放 COM 物件。
有一些 SOS 命令可以獲取 COM 互用性相關的資訊。
【!t】或者【!threads】命令獲取所有託管執行緒的資訊,其中就包含【套間】型別的資訊。【套間】是一種邏輯結構,與 COM 執行緒模型緊密相關。如果某個 COM 元件的編寫不考慮併發呼叫的情況,就可以使用單執行緒套間(STA),這種套間會使 COM 子系統對所有這個元件的呼叫序列化。相反,如果能夠處理併發呼叫的元件就可以使用多執行緒套間(MTA)模型,在這種情況下,針對元件的訪問就不需要序列化。
當任何一個執行緒使用 COM 元件時,它必須選擇合適的套間模型。在預設的情況下,所有的 .NET 執行緒都在 MTA 模型中。
我們來一張圖直觀的感受一下【!t】命令的結果,如圖:
【!syncblk】命令也可以輸出與 COM 互用性相關的資訊,如圖:
在該命令的輸出中給出了 CLR 已經例項化的並且當前處於活躍狀態的 RCW 的數量。當想快速瞭解當前 RCW 的使用情況時,這個命令很有用。
【!COMState】命令能夠對程序中的每個執行緒輸出 COM 的詳細資訊。效果如圖:
4.3、P/Invoke 呼叫的除錯
4.3.1、呼叫約定
A、基礎知識
呼叫約定(calling conventions):是在主呼叫函式和被呼叫函式之間的契約。呼叫約定包含了在實現正確的呼叫時呼叫方和被呼叫方都認可的一組規則。
呼叫約定如下:
StdCall:引數傳遞=棧(從右到左),負責清理的函式=被調函式,Dllmport的CallingConvention 域=CallingConvention.StdCall。
Cdecl:引數傳遞=棧(從右到左),負責清理的函式=主調函式,Dllmport的CallingConvention 域=CallingConvention.Cdecl。
FastCall:引數傳遞=暫存器/棧(從右到左),負責清理的函式=被調函式,Dllmport的CallingConvention 域=CallingConvention.FastCall。
ThisCall:引數傳遞=暫存器/棧(從右到左),負責清理的函式=被調函式,Dllmport的CallingConvention 域=CallingConvention.ThisCall。
截圖看的更清楚一點,如圖:
當使用 P/Invoke 呼叫非託管函式時,一定要使用正確的呼叫約定,如果呼叫協定不一致容易造成程式的崩潰,這種問題時難以發現的。在預設情況下,P/Invoke 使用 Winapi 呼叫約定,從嚴格意義上來說,這不是一種呼叫約定,而是告訴執行時使用預設的平臺呼叫約定。例如:在 Windows 平臺上,預設的平臺呼叫約定是 StdCall,而在 Windows CE 上則是 Cdecl。此外,還可以透過 DllImportAttribute 特性的 CallingConvention 域來指定一種不同的呼叫約定。
B、眼見為實
除錯原始碼:ExampleCore_7_02 和 ExampleCore_7_022(C++,動態連結庫)
除錯任務:呼叫約定造成的系統崩潰。
在我的測試中,這些呼叫協定【CallingConvention.StdCall、CallingConvention.ThisCall、CallingConvention.Cdecl、CallingConvention.Winapi】都是正常執行的,只有【CallingConvention.FastCall】這個協定出錯,所以錯誤協定就使用【CallingConvention.FastCall】來進行演示。
其實,我們可以直接執行系統,系統顯示的更直接,不用什麼偵錯程式都可以看得懂。效果如圖:
1)、NTSD 除錯
編譯專案,開啟【Visual Studio 2022 Developer Command Prompt v17.9.6】命令列工具,輸入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_7_02\bin\Debug\net8.0\ExampleCore_7_02.exe】,開啟偵錯程式,進入到偵錯程式。
我們可以【g】直接執行偵錯程式,偵錯程式會丟擲異常,中斷執行。
1 0:000> g 2 ModLoad: 00007ff9`c1140000 00007ff9`c1172000 C:\Windows\System32\IMM32.DLL 3 ModLoad: 00007ff9`36e00000 00007ff9`36e59000 C:\Program Files\dotnet\host\fxr\8.0.4\hostfxr.dll 4 ModLoad: 00007ff9`0a5f0000 00007ff9`0a654000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\hostpolicy.dll 5 ModLoad: 00007ff8`f5c40000 00007ff8`f6126000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\coreclr.dll 6 ModLoad: 00007ff9`c2630000 00007ff9`c275b000 C:\Windows\System32\ole32.dll 7 ModLoad: 00007ff9`c13e0000 00007ff9`c1733000 C:\Windows\System32\combase.dll 8 ModLoad: 00007ff9`c1740000 00007ff9`c180d000 C:\Windows\System32\OLEAUT32.dll 9 ModLoad: 00007ff9`c0730000 00007ff9`c07b2000 C:\Windows\System32\bcryptPrimitives.dll 10 (fa0.1cf0): Unknown exception - code 04242420 (first chance) 11 ModLoad: 00007ff8`f4d70000 00007ff8`f59fc000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll 12 ModLoad: 00007ff8`f4bb0000 00007ff8`f4d69000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\clrjit.dll 13 ModLoad: 00007ff9`c0f10000 00007ff9`c0f22000 C:\Windows\System32\kernel.appcore.dll 14 ModLoad: 00000110`f1e80000 00000110`f1e88000 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_7_02\bin\Debug\net8.0\ExampleCore_7_02.dll 15 ModLoad: 00000110`f1e90000 00000110`f1e9e000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Runtime.dll 16 ModLoad: 00007ff9`9bfa0000 00007ff9`9bfc8000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Console.dll 17 (fa0.1cf0): C++ EH exception - code e06d7363 (first chance) 18 ModLoad: 00007ff9`8c330000 00007ff9`8c55e000 C:\Windows\SYSTEM32\icu.dll 19 (fa0.1cf0): CLR exception - code e0434352 (first chance) 20 (fa0.1cf0): CLR exception - code e0434352 (!!! second chance !!!) 21 KERNELBASE!RaiseException+0x69: 22 00007ff9`c07ecf19 0f1f440000 nop dword ptr [rax+rax]
原著中說的是丟擲“訪問違例”的異常,我這裡是沒有看到,只是看到了核心態丟擲了異常。
我按著原書的步驟來,如果我們想獲取是哪行原始碼出問題了,可以使用【!lines】命令
1 0:000> !lines 2 Line number information will be loaded
我們再使用【!clrstack】命令,檢視一下託管執行緒呼叫棧,原始碼的行號就會顯示出來。
1 0:000> !clrstack 2 OS Thread Id: 0x1cf0 (0) 3 Child SP IP Call Site 4 000000031FB7E548 00007ff9c07ecf19 [PrestubMethodFrame: 000000031fb7e548] ExampleCore_7_02.Program.Alloc(System.String) 5 000000031FB7E720 00007FF89624196E ExampleCore_7_02.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_7_02\Program.cs @ 13]
ExampleCore_7_02.Program.Main 棧幀的內容最後有一個數字 13,這個就是原始碼的行號,出錯的行號。
效果如圖:
最開始的時候,我們的偵錯程式丟擲異常,有一個地址 00007ff9`c07ecf19 ,這個地址就是錯誤程式碼,可以使用【!u 00007ff9`c07ecf19】命令檢視程式碼的內容。
1 0:000> !u 00007ff9`c07ecf19 2 Unmanaged code 3 00007ff9`c07ecf19 0f1f440000 nop dword ptr [rax+rax] 4 00007ff9`c07ecf1e 488b8c24c0000000 mov rcx,qword ptr [rsp+0C0h] 5 00007ff9`c07ecf26 4833cc xor rcx,rsp 6 00007ff9`c07ecf29 e8f2880600 call KERNELBASE!_security_check_cookie (00007ff9`c0855820) 7 00007ff9`c07ecf2e 4881c4d8000000 add rsp,0D8h 8 00007ff9`c07ecf35 c3 ret 9 00007ff9`c07ecf36 cc int 3 10 00007ff9`c07ecf37 8364243800 and dword ptr [rsp+38h],0 11 00007ff9`c07ecf3c ebcf jmp KERNELBASE!RaiseException+0x5d (00007ff9`c07ecf0d) 12 00007ff9`c07ecf3e cc int 3
就是丟擲異常的程式碼。
2)、Windbg Preview 除錯
編譯專案,開啟【Windbg Preview】,依次點選【檔案】---【Launch executable】,載入我們的可執行程式 ExampleCore_7_02.exe,進入偵錯程式。
我們直接【g】執行偵錯程式,看到偵錯程式丟擲異常。
1 0:000> g 2 ModLoad: 00007fff`50700000 00007fff`50732000 C:\Windows\System32\IMM32.DLL 3 ModLoad: 00007ffe`567c0000 00007ffe`56819000 C:\Program Files\dotnet\host\fxr\8.0.4\hostfxr.dll 4 ModLoad: 00007ffe`56750000 00007ffe`567b4000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\hostpolicy.dll 5 ModLoad: 00007ffe`56260000 00007ffe`56746000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\coreclr.dll 6 ModLoad: 00007fff`52120000 00007fff`5224b000 C:\Windows\System32\ole32.dll 7 ModLoad: 00007fff`52380000 00007fff`526d3000 C:\Windows\System32\combase.dll 8 ModLoad: 00007fff`50e10000 00007fff`50edd000 C:\Windows\System32\OLEAUT32.dll 9 ModLoad: 00007fff`50350000 00007fff`503d2000 C:\Windows\System32\bcryptPrimitives.dll 10 (25fc.9f0): Unknown exception - code 04242420 (first chance) 11 ModLoad: 00007ffe`550d0000 00007ffe`55d5c000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll 12 ModLoad: 00007ffe`54f10000 00007ffe`550c9000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\clrjit.dll 13 ModLoad: 00007fff`506e0000 00007fff`506f2000 C:\Windows\System32\kernel.appcore.dll 14 ModLoad: 000001f6`83a80000 000001f6`83a88000 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_7_02\bin\Debug\net8.0\ExampleCore_7_02.dll 15 ModLoad: 000001f6`83a90000 000001f6`83a9e000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Runtime.dll 16 ModLoad: 00007ffe`54ee0000 00007ffe`54f08000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Console.dll 17 (25fc.9f0): C++ EH exception - code e06d7363 (first chance) 18 ModLoad: 00007fff`1a810000 00007fff`1aa3e000 C:\Windows\SYSTEM32\icu.dll 19 (25fc.9f0): CLR exception - code e0434352 (first chance) 20 (25fc.9f0): CLR exception - code e0434352 (!!! second chance !!!) 21 KERNELBASE!RaiseException+0x69: 22 00007fff`4fbfcf19 0f1f440000 nop dword ptr [rax+rax]
原書上說的是出現了異常,原因是訪問違例,這裡不是的,這麼多年了,變化也不小。
我按著原書的步驟來,如果我們想獲取是哪行原始碼出問題了,可以使用【!lines】命令。
1 0:000> !lines 2 Line number information will not be loaded
其實,在【Windbg Preview】裡面不需要使用這個命令。原書的內容是使用了【k】命令,其實作用不大,我們其實可以直接使用【!clrstack】命令,看得更直接。
1 0:000> !clrstack 2 OS Thread Id: 0x9f0 (0) 3 Child SP IP Call Site 4 000000338C57E528 00007fff4fbfcf19 [PrestubMethodFrame: 000000338c57e528] ExampleCore_7_02.Program.Alloc(System.String) 5 000000338C57E700 00007ffdf687196e ExampleCore_7_02.Program.Main(System.String[])
託管執行緒的呼叫棧很清楚,就不多說了。我們可以使用【!u 00007fff4fbfcf19】檢視一下這個地址的程式碼是什麼。
1 0:000> !u 00007fff4fbfcf19 2 Unmanaged code 3 00007fff`4fbfcf19 0f1f440000 nop dword ptr [rax+rax] 4 00007fff`4fbfcf1e 488b8c24c0000000 mov rcx,qword ptr [rsp+0C0h] 5 00007fff`4fbfcf26 4833cc xor rcx,rsp 6 00007fff`4fbfcf29 e8f2880600 call KERNELBASE!_security_check_cookie (00007fff`4fc65820) 7 00007fff`4fbfcf2e 4881c4d8000000 add rsp,0D8h 8 00007fff`4fbfcf35 c3 ret 9 00007fff`4fbfcf36 cc int 3 10 00007fff`4fbfcf37 8364243800 and dword ptr [rsp+38h],0 11 00007fff`4fbfcf3c ebcf jmp KERNELBASE!RaiseException+0x5d (00007fff`4fbfcf0d) 12 00007fff`4fbfcf3e cc int 3
KERNELBASE!RaiseException 核心丟擲的異常。
4.3.2、委託
A、基礎知識
【託管程式碼】到【非託管程式碼】的切換過程中,物件的固定是有 P/Invoke 層全權負責的,但是這個固定的範圍這個同步的 Request-Response 週期,如果超過請求相應週期,那就容易出現各種問題,比如:ExampleCore_7_03。
P/Invoke 層可以獲取一個託管程式碼委託,並將它轉換為一個函式指標,然後由非託管函式來使用。
當我們想知道原始碼的除錯行號的時候,可以使用【!lines】命令,如果發生了錯誤,有錯誤碼的話,可以使用【!error】命令檢視具體錯誤資訊。
從託管程式碼切換到非託管程式碼整個過程中,要確保所使用的物件都被固定住。雖然 P/Invoke 層在執行 P/Invoke 呼叫時能自動固定物件,但這些物件在函式呼叫完成後會被自動接觸固定並切換回託管程式碼。
當非託管程式碼使用一個已被收集的委託時,表現出的問題很難琢磨,摸不清頭腦,往往也需要一些時間才會暴露出來。如果我們想當能儘快暴露出問題,可以使用 MDA,callbackOnCollectedDelegate 這個 MDA 每當呼叫一個已被收集的委託時,程式就會立刻報告一個錯誤。這個 MDA 是在 Net Framework 環境下使用的。
B、眼見為實
除錯原始碼:ExampleCore_7_03 和 ExampleCore_7_033(C++,動態連結庫)
除錯任務:本來想測試由委託非同步引發的崩潰,但是我這個版本測試可以正常執行。
編譯我們的兩個專案(C# 專案和 C++ 專案),直接【ctrl+f5】執行 ExampleCore_7_03.exe 專案,無論我們是否註釋掉【GC.Collect();】這行程式碼,程式都不會報錯。在 .NET Framework 環境下,如果執行垃圾回收,我們的 callback 就會被回收,後面執行就會丟擲“空引用異常”。我在 .NET 8.0 環境里正常執行,沒有出現問題。執行結果如圖:
1)、Windbg Preview 除錯
編譯專案,開啟【Windbg Preview】,依次點選【檔案】----》【Launch executable】附加程式 ExampleCore_7_03.exe,進入偵錯程式,偵錯程式當前處於中斷狀態。我本來想使用【bp ExampleCore_7_03!AsyncProcess】命令給【AsyncProcess】方法下斷點,但是執行不成功。
1 0:000> bp ExampleCore_7_03!AsyncProcess 2 Bp expression 'ExampleCore_7_03!AsyncProcess' could not be resolved, adding deferred bp
那我們就透過原始碼的方式直接給 C++ AsyncProcess 方法下斷點。我們點選 Windbg 選單欄,依次選擇【Source】--->【Open Source File】,開啟選擇我們的 C++ 專案中的 ExampleCore_7_033.cpp 檔案。效果如圖:
斷點設定完成後,我們直接執行【g】命令,繼續執行偵錯程式。效果如圖:
【Windbg Preview】命令視窗展示如圖:
執行效果如下:
1 0:000> bl 2 0 e Disable Clear u 0001 (0001) (@@masm(`E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_7_033\ExampleCore_7_033.cpp:28+`)) 3 4 0:000> g 5 ModLoad: 0000026d`613d0000 0000026d`613de000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Runtime.dll 6 ModLoad: 00007ff8`0d190000 00007ff8`0d1b8000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Console.dll 7 *** WARNING: Unable to verify checksum for E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_7_03\bin\Debug\net8.0\ExampleCore_7_033.DLL 8 ModLoad: 00007ff8`0c490000 00007ff8`0c4b6000 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_7_03\bin\Debug\net8.0\ExampleCore_7_033.DLL 9 ModLoad: 00007ff8`499a0000 00007ff8`499ce000 C:\Windows\SYSTEM32\VCRUNTIME140D.dll 10 ModLoad: 00007fff`b9560000 00007fff`b9781000 C:\Windows\SYSTEM32\ucrtbased.dll 11 Breakpoint 0 hit 12 ExampleCore_7_033!AsyncProcess+0x1f:(斷點處) 13 00007ff8`0c4a17ff 48c744242800000000 mov qword ptr [rsp+28h],0 ss:000000d7`5a97e328={coreclr!HelperMethodFrame_1OBJ::`vftable' (00007fff`9dfc9d08)}
我們繼續執行【dv】命令,可以看到有一個 ptr,那就是我們從託管程式碼中傳遞到非託管程式碼中的委託,就是一個指標。
1 0:000> dv 2 ptr = 0x00007fff`3e063024 3 hThread = 0x00007fff`9dfc9af8
【ptr】這個欄位在【Windbg Preview】裡是可以點選的,如果是命令列除錯就不可以了,比如:NTSD 等。如圖:
我們可以使用【u 0x00007fff`3e063024】命令,檢視一下這個 ptr 是什麼。
1 0:000> u 7fff3e063024 2 00007fff`3e063024 49ba0030063eff7f0000 mov r10,7FFF3E063000h 3 00007fff`3e06302e 48b8d0d0d29dff7f0000 mov rax,offset coreclr!TheUMEntryPrestub (00007fff`9dd2d0d0) 4 00007fff`3e063038 48ffe0 jmp rax 5 00007fff`3e06303b 0000 add byte ptr [rax],al 6 00007fff`3e06303d 0000 add byte ptr [rax],al 7 00007fff`3e06303f 0000 add byte ptr [rax],al 8 00007fff`3e063041 0000 add byte ptr [rax],al 9 00007fff`3e063043 0000 add byte ptr [rax],al
我們在【PCallback callback = (PCallback)lpParameter;】這行程式碼在下一個斷點,也就是2秒後會執行這個回撥。效果如圖:
斷點設定成功後,我們繼續執行偵錯程式,使用【g】命令。
1 0:000> g 2 ModLoad: 00007ff8`6fd70000 00007ff8`6fd82000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Threading.dll 3 ModLoad: 0000026d`61400000 0000026d`61408000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Text.Encoding.Extensions.dll 4 ModLoad: 00007ff8`6fd50000 00007ff8`6fd65000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Runtime.InteropServices.dll 5 Breakpoint 1 hit 6 ExampleCore_7_033!ThreadWorkItem+0x3e:(成功斷住) 7 00007ff8`0c4a172e 488b8500010000 mov rax,qword ptr [rbp+100h] ss:000000d7`5b6ffec0=00007fff3e063024
如圖:
到了這裡,我們在使用【u 0x00007fff`3e063024】命令,檢視一下這個 ptr 是什麼東西。
1 0:009> u 0x00007fff`3e063024 2 00007fff`3e063024 49ba0030063eff7f0000 mov r10,7FFF3E063000h 3 00007fff`3e06302e 48b8d0d0d29dff7f0000 mov rax,offset coreclr!TheUMEntryPrestub (00007fff`9dd2d0d0) 4 00007fff`3e063038 48ffe0 jmp rax 5 00007fff`3e06303b 0000 add byte ptr [rax],al 6 00007fff`3e06303d 0000 add byte ptr [rax],al 7 00007fff`3e06303f 0000 add byte ptr [rax],al 8 00007fff`3e063041 0000 add byte ptr [rax],al 9 00007fff`3e063043 0000 add byte ptr [rax],al
我在 .NET Framework 版本中,此時【ptr】已經是壞的資料了。效果如圖:
都是亂碼了,都是 ??? 問號了,就是說 ptr 不存在了,說明已經被我們 GC 回收了。但是在 .NET 8.0 資料還是存在的,也許是改進了,具體原因我還沒有搞清楚。
如果遇到這樣的情況,我們怎麼解決呢?其實很簡單,在我們的 C# 程式碼中,宣告一個靜態的 handle 就可以了,如:static GCHandle handle;在我的程式碼中,註釋的部分就是解決辦法。
4.4、互操作中的記憶體洩漏問題的除錯
A、基礎知識
在理想情況下,託管程式碼永遠不用(至少不直接)和非託管程式碼進行互操作。或者,對於現有的每個非託管程式碼元件都有一個經過完備測試的可靠的 .NET 封裝。然而,這種情況是不存在的。在這樣的情況下,必須使用互操作。
當互操作的時候,我們如何快速的找出問題的出處,有具體的解決思路,我認為這是更重要的。
當我們在分析記憶體耗盡或者高記憶體使用量等問題時,必須非常小心。通常,簡單的分析託管堆並不足以找出記憶體使用過高的原因。有時候,雖然託管堆看上去很正確,但是我們還需要在託管堆之外的其他地方進行分析,並判斷在程序的整體記憶體使用量上是否存在異常。
接下來,我透過一個測試用例來說明遇到由於互操作引起的記憶體暴漲的問題時,如何分析和解決的。
這個程式可以看成是對 P/Invoke 呼叫的模擬壓力測試。在執行程式之前,先要開啟【工作管理員】,開啟方法可以透過滑鼠右鍵選單,也可以透過快捷鍵【ctrl+shift+Esc】。接下來,執行這個程式,輸入不同的迭代次數,檢視記憶體的使用情況。
我們分別執行 4 次,每次的迭代次數分別是:1000、10000、100000、1000000,分別記錄每次迭代所使用的記憶體情況。
第一次:1000,使用記憶體 3.6 MB,執行效果如圖:
我們可以看到,隨著迭代次數的增加,所使用的記憶體也是遞增的。在最後一次迭代(一百萬次)中,記憶體盡然使用了 477.2 MB,說明程式在使用記憶體出現了問題,這就是問題的表現,接下來我們嘗試解決一下。
B、眼見為實
除錯原始碼:ExampleCore_7_04 和 ExampleCore_7_044(C++,動態連結庫)
除錯任務:除錯互操作中的記憶體洩露的問題。
1)、NTSD 除錯
編譯專案,開啟【Visual Studio 2022 Developer Command Prompt v17.9.6】命令列工具,輸入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_7_04\bin\Debug\net8.0\ExampleCore_7_04.exe】,開啟偵錯程式。
進入偵錯程式後,我們使用【g】命令,直接執行,直到偵錯程式輸出“請輸入迭代的次數。”,效果如圖:
然後,我們輸入 1000000,回車繼續執行,直到偵錯程式輸出“Press any key to exit!”字樣,偵錯程式暫停。如圖:
此時,我們開啟【工作管理員】,檢視一下我們的專案執行佔用多少記憶體,如圖:
回到偵錯程式,點選組合鍵【ctrl+c】進入中斷模式,開始我們的除錯吧。
我們先使用【!eeheap -loader】命令看看載入堆上是否存在異常。
1 0:002> !eeheap -loader 2 Loader Heap: 3 -------------------------------------- 4 System Domain: 00007ff9c23140c0 5 LowFrequencyHeap: 00007FF9625C0000(10000:e000) 00007FF962550000(10000:10000) 00007FF962520000(10000:10000) 00007FF962450000(10000:10000) 00007FF9623F0000(10000:10000) 00007FF962350000(70000:70000) 00007FF962330000(3000:1000) Size: 0xbf000 (782336) bytes total, 0x2000 (8192) bytes wasted. 6 HighFrequencyHeap: 00007FF962620000(10000:2000) 00007FF9625F0000(10000:10000) 00007FF9625D0000(10000:10000) 00007FF962590000(10000:10000) 00007FF962570000(10000:10000) 00007FF962540000(10000:10000) 00007FF962510000(10000:10000) 00007FF962470000(10000:10000) 00007FF962440000(10000:10000) 00007FF962420000(10000:10000) 00007FF962400000(10000:10000) 00007FF9623C0000(10000:10000) 00007FF962334000(9000:6000) Size: 0xb8000 (753664) bytes total, 0x3000 (12288) bytes wasted. 7 StubHeap: Size: 0x0 (0) bytes. 8 Virtual Call Stub Heap: 9 IndcellHeap: 00007FF962340000(6000:1000) Size: 0x1000 (4096) bytes. 10 LookupHeap: Size: 0x0 (0) bytes. 11 ResolveHeap: Size: 0x0 (0) bytes. 12 DispatchHeap: Size: 0x0 (0) bytes. 13 CacheEntryHeap: Size: 0x0 (0) bytes. 14 Total size: Size: 0x178000 (1540096) bytes total, 0x5000 (20480) bytes wasted. 15 -------------------------------------- 16 Domain 1: 00000201b777ee80 17 LowFrequencyHeap: 00007FF9625C0000(10000:e000) 00007FF962550000(10000:10000) 00007FF962520000(10000:10000) 00007FF962450000(10000:10000) 00007FF9623F0000(10000:10000) 00007FF962350000(70000:70000) 00007FF962330000(3000:1000) Size: 0xbf000 (782336) bytes total, 0x2000 (8192) bytes wasted. 18 HighFrequencyHeap: 00007FF962620000(10000:2000) 00007FF9625F0000(10000:10000) 00007FF9625D0000(10000:10000) 00007FF962590000(10000:10000) 00007FF962570000(10000:10000) 00007FF962540000(10000:10000) 00007FF962510000(10000:10000) 00007FF962470000(10000:10000) 00007FF962440000(10000:10000) 00007FF962420000(10000:10000) 00007FF962400000(10000:10000) 00007FF9623C0000(10000:10000) 00007FF962334000(9000:6000) Size: 0xb8000 (753664) bytes total, 0x3000 (12288) bytes wasted. 19 StubHeap: Size: 0x0 (0) bytes. 20 Virtual Call Stub Heap: 21 IndcellHeap: 00007FF962340000(6000:1000) Size: 0x1000 (4096) bytes. 22 LookupHeap: Size: 0x0 (0) bytes. 23 ResolveHeap: Size: 0x0 (0) bytes. 24 DispatchHeap: Size: 0x0 (0) bytes. 25 CacheEntryHeap: Size: 0x0 (0) bytes. 26 Total size: Size: 0x178000 (1540096) bytes total, 0x5000 (20480) bytes wasted. 27 -------------------------------------- 28 Jit code heap: 29 LoaderCodeHeap: 0000000000000000(0:0) Size: 0x0 (0) bytes. 30 Total size: Size: 0x0 (0) bytes. 31 -------------------------------------- 32 Module Thunk heaps: 33 Module 00007ff962334000: Size: 0x0 (0) bytes. 34 Module 00007ff96251e0a0: Size: 0x0 (0) bytes. 35 Module 00007ff96251fbc8: Size: 0x0 (0) bytes. 36 Module 00007ff96254a268: Size: 0x0 (0) bytes. 37 Module 00007ff96254c020: Size: 0x0 (0) bytes. 38 Module 00007ff962572108: Size: 0x0 (0) bytes. 39 Module 00007ff9625745e0: Size: 0x0 (0) bytes. 40 Total size: Size: 0x0 (0) bytes. 41 -------------------------------------- 42 Module Lookup Table heaps: 43 Module 00007ff962334000: 0000000000000008(0:0) 0000000000000000(0:0) Size: 0x0 (0) bytes. 44 Module 00007ff96251e0a0: 0000000000000008(0:0) 0000000000000000(0:0) Size: 0x0 (0) bytes. 45 Module 00007ff96251fbc8: 0000000000000008(0:0) 0000000000000000(0:0) Size: 0x0 (0) bytes. 46 Module 00007ff96254a268: 0000000000000008(0:0) 0000000000000000(0:0) Size: 0x0 (0) bytes. 47 Module 00007ff96254c020: 0000000000000008(0:0) 0000000000000000(0:0) Size: 0x0 (0) bytes. 48 Module 00007ff962572108: 0000000000000008(0:0) 0000000000000000(0:0) Size: 0x0 (0) bytes. 49 Module 00007ff9625745e0: 0000000000000008(0:0) 0000000000000000(0:0) Size: 0x0 (0) bytes. 50 Total size: Size: 0x0 (0) bytes. 51 -------------------------------------- 52 Total LoaderHeap size: Size: 0x2f0000 (3080192) bytes total, 0xa000 (40960) bytes wasted. 53 ======================================= 54 0:002>
無論是系統域還是私有域,記憶體使用量都是 1.5 MB ,和 492.8 MB 差的太遠了,說明載入堆沒異常。
我們繼續使用【!eeheap -gc】命令檢視一下 GC 堆是否有問題。
1 0:002> !eeheap -gc 2 Number of GC Heaps: 1 3 generation 0 starts at 0x00000201BB400028 4 generation 1 starts at 0x00000201BBC00028 5 generation 2 starts at 0x000002424DED0008 6 ephemeral segment allocation context: none 7 segment begin allocated committed allocated size committed size 8 generation 0: 9 00000241CD3CF1C0 00000201BB400028 00000201BB400028 00000201BB421000 0x0(0) 0x20fd8(135128) 10 generation 1: 11 00000241CD3CF320 00000201BBC00028 00000201BBC141E8 00000201BBC21000 0x141c0(82368) 0x20fd8(135128) 12 generation 2: 13 00000201B7761B20 000002424DED0008 000002424DED1BE8 000002424DEE0000 0x1be0(7136) 0xfff8(65528) 14 00000241CD3CF950 00000201BE000028 00000201BE000028 00000201BE001000 0x0(0) 0xfd8(4056) 15 Large object heap starts at 0x0000000000000000 16 segment begin allocated committed allocated size committed size 17 00000241CD3CF3D0 00000201BC000028 00000201BC000028 00000201BC001000 0x0(0) 0xfd8(4056) 18 Pinned object heap starts at 0x0000000000000000 19 00000241CD3CEC40 00000201B9400028 00000201B9404018 00000201B9411000 0x3ff0(16368) 0x10fd8(69592) 20 Total Allocated Size: Size: 0x19d90 (105872) bytes. 21 Total Committed Size: Size: 0x53f58 (343896) bytes. 22 ------------------------------ 23 GC Allocated Heap Size: Size: 0x19d90 (105872) bytes. 24 GC Committed Heap Size: Size: 0x53f58 (343896) bytes. 25 0:002>
GC 堆的內容也不是很大,所以就不是託管堆的問題。
我們使用【!address -summary】命令檢視一下程序整體的記憶體使用情況。
1 0:002> !address -summary 2 3 4 Mapping file section regions... 5 Mapping module regions... 6 Mapping PEB regions... 7 Mapping TEB and stack regions... 8 Mapping heap regions... 9 Mapping page heap regions... 10 Mapping other regions... 11 Mapping stack trace database regions... 12 Mapping activation context regions... 13 14 --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal 15 Free 66 7dbe`41e6b000 ( 125.743 TB) 98.24% 16 MappedFile 161 200`0351a000 ( 2.000 TB) 88.62% 1.56% 17 <unknown> 82 41`97314000 ( 262.362 GB) 11.35% 0.20% 18 Heap 44 0`1fa49000 ( 506.285 MB) 0.02% 0.00% 19 Image 240 0`03422000 ( 52.133 MB) 0.00% 0.00% 20 Stack 18 0`00900000 ( 9.000 MB) 0.00% 0.00% 21 Other 8 0`001dd000 ( 1.863 MB) 0.00% 0.00% 22 TEB 6 0`0000e000 ( 56.000 kB) 0.00% 0.00% 23 PEB 1 0`00001000 ( 4.000 kB) 0.00% 0.00% 24 25 --- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal 26 MEM_MAPPED 168 200`03705000 ( 2.000 TB) 88.62% 1.56% 27 MEM_PRIVATE 152 41`b765e000 ( 262.866 GB) 11.37% 0.20% 28 MEM_IMAGE 240 0`03422000 ( 52.133 MB) 0.00% 0.00% 29 30 --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal 31 MEM_FREE 66 7dbe`41e6b000 ( 125.743 TB) 98.24% 32 MEM_RESERVE 103 241`95f22000 ( 2.256 TB) 99.97% 1.76% 33 MEM_COMMIT 457 0`28263000 ( 642.387 MB) 0.03% 0.00% 34 35 --- Protect Summary (for commit) - RgnCount ----------- Total Size -------- %ofBusy %ofTotal 36 PAGE_READWRITE 174 0`1ee61000 ( 494.379 MB) 0.02% 0.00% 37 PAGE_NOACCESS 23 0`0410c000 ( 65.047 MB) 0.00% 0.00% 38 PAGE_READONLY 158 0`02b64000 ( 43.391 MB) 0.00% 0.00% 39 PAGE_EXECUTE_READ 63 0`026ea000 ( 38.914 MB) 0.00% 0.00% 40 PAGE_WRITECOPY 30 0`0007f000 ( 508.000 kB) 0.00% 0.00% 41 PAGE_READWRITE | PAGE_GUARD 7 0`00017000 ( 92.000 kB) 0.00% 0.00% 42 PAGE_EXECUTE_WRITECOPY 1 0`00010000 ( 64.000 kB) 0.00% 0.00% 43 PAGE_EXECUTE_READWRITE 1 0`00002000 ( 8.000 kB) 0.00% 0.00% 44 45 --- Largest Region by Usage ----------- Base Address -------- Region Size ---------- 46 Free 242`6f510000 7bb2`723d0000 ( 123.697 TB) 47 MappedFile 7dfe`ece65000 1f6`d66c9000 ( 1.964 TB) 48 <unknown> 201`be001000 3f`fb39f000 ( 255.925 GB) 49 Heap 242`50ae0000 0`00fcf000 ( 15.809 MB) 50 Image 7ff9`c0fb1000 0`00bfe000 ( 11.992 MB) 51 Stack 93`e7500000 0`0017c000 ( 1.484 MB) 52 Other 201`b7ac0000 0`00181000 ( 1.504 MB) 53 TEB 93`e71c9000 0`00004000 ( 16.000 kB) 54 PEB 93`e71b2000 0`00001000 ( 4.000 kB) 55 56 0:002>
紅色標註的和我們 492.8 MB 的記憶體使用差不多,為什麼會在非託管堆上呢,由此,我們可以聯想到是和 P/Invoke 呼叫相關的,於是,我們在檢查程式碼,找出問題。
2)、Windbg Preview 除錯
編譯專案,開啟【Windbg Preview】偵錯程式,依次點選【檔案】---【Launch Executable】,載入我們的專案可執行程式 ExampleCore_7_04.exe,進入到偵錯程式中。
我們【g】直接執行偵錯程式,然後再控制檯程式中輸入迭代次數 1000000,回車繼續執行,直到我們的控制檯程式輸出“Press any key to exit!”時,回到偵錯程式,點選【Break】按鈕,中斷偵錯程式的執行,開始我們的除錯。
我們先透過【!eeheap -loader】命令檢視一下載入堆有沒有異常。
1 0:001> !eeheap -loader 2 Loader Heap: 3 ---------------------------------------- 4 System Domain: 7ff9951d40c0 5 LoaderAllocator: 7ff9951d40c0 6 LowFrequencyHeap: 7ff935460000(10000:e000) 7ff9353f0000(10000:10000) 7ff9353c0000(10000:10000) 7ff9352f0000(10000:10000) 7ff935290000(10000:10000) 7ff9351f0000(70000:70000) 7ff9351d0000(3000:1000) Size: 0xbf000 (782336) bytes total, 0x2000 (8192) bytes wasted. 7 HighFrequencyHeap: 7ff9354c0000(10000:2000) 7ff935490000(10000:10000) 7ff935470000(10000:10000) 7ff935430000(10000:10000) 7ff935410000(10000:10000) 7ff9353e0000(10000:10000) 7ff9353b0000(10000:10000) 7ff935310000(10000:10000) 7ff9352e0000(10000:10000) 7ff9352c0000(10000:10000) 7ff9352a0000(10000:10000) 7ff935260000(10000:10000) 7ff9351d4000(9000:6000) Size: 0xb8000 (753664) bytes total, 0x3000 (12288) bytes wasted. 8 FixupPrecodeHeap: 7ff9354b0000(10000:10000) 7ff9354a0000(10000:10000) 7ff935480000(10000:10000) 7ff935440000(10000:10000) 7ff935420000(10000:10000) 7ff935400000(10000:10000) 7ff9353d0000(10000:10000) 7ff935320000(10000:10000) 7ff935300000(10000:10000) 7ff9352d0000(10000:10000) 7ff9352b0000(10000:10000) 7ff935270000(10000:10000) Size: 0xc0000 (786432) bytes total. 9 NewStubPrecodeHeap: 7ff935280000(10000:10000) Size: 0x10000 (65536) bytes total. 10 IndirectionCellHeap: 7ff9351e0000(6000:1000) Size: 0x1000 (4096) bytes total. 11 Total size: Size: 0x248000 (2392064) bytes total, 0x5000 (20480) bytes wasted.(記憶體使用不大,沒異常) 12 ---------------------------------------- 13 Domain 1: 029001582830 14 LoaderAllocator: 029001582830 15 No unique loader heaps found. 16 ---------------------------------------- 17 JIT Manager: 029001585bf0 18 LoaderCodeHeap: 7ff935330000(80000:4000) Size: 0x4000 (16384) bytes total. 19 Total size: Size: 0x4000 (16384) bytes total. 20 ----------------------------------------
我們再使用【!eeheap -gc】命令檢視一下 GC 堆有沒有異常情況。
1 0:001> !eeheap -gc 2 3 ======================================== 4 Number of GC Heaps: 1 5 ---------------------------------------- 6 Small object heap 7 segment begin allocated committed allocated size committed size 8 generation 0: 9 02d016fdf1c0 029005000028 029005000028 029005021000 0x21000 (135168) 10 generation 1: 11 02d016fdf320 029005800028 0290058141e8 029005821000 0x141c0 (82368) 0x21000 (135168) 12 generation 2: 13 02d016fdf950 029007c00028 029007c00028 029007c01000 0x1000 (4096) 14 NonGC heap 15 segment begin allocated committed allocated size committed size 16 029001636e20 02d097be0008 02d097be1be8 02d097bf0000 0x1be0 (7136) 0x10000 (65536) 17 Large object heap 18 segment begin allocated committed allocated size committed size 19 02d016fdf3d0 029005c00028 029005c00028 029005c01000 0x1000 (4096) 20 Pinned object heap 21 segment begin allocated committed allocated size committed size 22 02d016fdec40 029003000028 029003004018 029003011000 0x3ff0 (16368) 0x11000 (69632) 23 ------------------------------ 24 GC Allocated Heap Size: Size: 0x19d90 (105872) bytes.(GC 堆才 105 KB,也沒問題) 25 GC Committed Heap Size: Size: 0x65000 (413696) bytes.
GC 堆也沒出現記憶體暴漲的情況,沒什麼問題。
由於大部分記憶體消耗並不在託管堆上,因此,我們需要使用【!address -summary】來了解程序中記憶體使用的情況。
1 0:001> !address -summary 2 3 --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal 4 Free 65 7dbe`41feb000 ( 125.743 TB) 98.24% 5 MappedFile 147 200`0351a000 ( 2.000 TB) 88.62% 1.56% 6 <unknown> 82 41`97316000 ( 262.362 GB) 11.35% 0.20% 7 Heap 44 0`1fa49000 ( 506.285 MB) 0.02% 0.00%(這個是比較類似的) 8 Image 240 0`03422000 ( 52.133 MB) 0.00% 0.00% 9 Stack 15 0`00780000 ( 7.500 MB) 0.00% 0.00% 10 Other 8 0`001dd000 ( 1.863 MB) 0.00% 0.00% 11 TEB 5 0`0000c000 ( 48.000 kB) 0.00% 0.00% 12 PEB 1 0`00001000 ( 4.000 kB) 0.00% 0.00% 13 14 --- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal 15 MEM_MAPPED 154 200`03705000 ( 2.000 TB) 88.62% 1.56% 16 MEM_PRIVATE 148 41`b74de000 ( 262.864 GB) 11.37% 0.20% 17 MEM_IMAGE 240 0`03422000 ( 52.133 MB) 0.00% 0.00% 18 19 --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal 20 MEM_FREE 65 7dbe`41feb000 ( 125.743 TB) 98.24% 21 MEM_RESERVE 100 241`95ef7000 ( 2.256 TB) 99.97% 1.76% 22 MEM_COMMIT 442 0`2810e000 ( 641.055 MB) 0.03% 0.00% 23 24 --- Protect Summary (for commit) - RgnCount ----------- Total Size -------- %ofBusy %ofTotal 25 PAGE_READWRITE 172 0`1ee4e000 ( 494.305 MB) 0.02% 0.00% 26 PAGE_NOACCESS 18 0`03fe6000 ( 63.898 MB) 0.00% 0.00% 27 PAGE_READONLY 151 0`02b4b000 ( 43.293 MB) 0.00% 0.00% 28 PAGE_EXECUTE_READ 63 0`026ea000 ( 38.914 MB) 0.00% 0.00% 29 PAGE_WRITECOPY 30 0`0007f000 ( 508.000 kB) 0.00% 0.00% 30 PAGE_READWRITE | PAGE_GUARD 6 0`00014000 ( 80.000 kB) 0.00% 0.00% 31 PAGE_EXECUTE_WRITECOPY 1 0`00010000 ( 64.000 kB) 0.00% 0.00% 32 PAGE_EXECUTE_READWRITE 1 0`00002000 ( 8.000 kB) 0.00% 0.00% 33 34 --- Largest Region by Usage ----------- Base Address -------- Region Size ---------- 35 Free 2d0`b9220000 7b23`496c0000 ( 123.138 TB) 36 MappedFile 7dff`44b1f000 1f5`9fa0f000 ( 1.959 TB) 37 <unknown> 290`07c01000 3f`fb3af000 ( 255.925 GB) 38 Heap 2d0`9a7f0000 0`00fcf000 ( 15.809 MB) 39 Image 7ff9`93be1000 0`00bfe000 ( 11.992 MB) 40 Stack f6`14000000 0`0017b000 ( 1.480 MB) 41 Other 290`01910000 0`00181000 ( 1.504 MB) 42 TEB f6`139fb000 0`00004000 ( 16.000 kB) 43 PEB f6`139e0000 0`00001000 ( 4.000 kB)
既然是在非託管堆上分配的,我們很容易就會聯想到和 P/Invoke 有關聯。此時,我們在看看原始碼,問題也就可以找到了。
4.5、COM 互用性中終結操作的除錯
當我們在編寫帶有 Finalize 方法的型別時必須小心,要始終確保 Finalize 方法能夠返回以避免終結佇列中的物件累計,否則就會導致記憶體耗盡的情況發生。終結執行緒會以序列的方式選擇物件執行終結操作。
在這一節,我沒有找到很好的例子,所以說測試就不做了。
分析 COM 互用性問題的步驟,到是可以總結一下:
1)、我們先使用【!eeheap -loader】命令,檢視一下載入堆是否存在異常。
2)、繼續使用【!eeheap -gc】命令觀察一下託管堆是否存在異常。
3)、如果載入堆和 GC 堆都沒有問題,很有可能就是非託管堆出現了問題。
4)、我們繼續使用【!address -summary】命令檢視一下程序記憶體使用的情況。
5)、如果涉及到終結器操作的,我們還可以使用【!FinalizeQueue】命令,檢視終結佇列的情況,檢視哪些物件還沒有執行 Finalize 方法。
6)、我們可以使用【!t】或者【!threads】命令檢視一下終結執行緒是否活躍。
7)、繼續使用【k】命令檢視終結執行緒的呼叫棧,就能確定它是否活躍。
8)、結合我們的原始碼找出問題。
五、總結
這篇文章的終於寫完了,這篇文章的內容相對來說,不是很多。寫完一篇,就說明進步了一點點。Net 高階除錯這條路,也剛剛起步,還有很多要學的地方。皇天不負有心人,努力,不辜負自己,我相信付出就有回報,再者說,學習的過程,有時候,雖然很痛苦,但是,學有所成,學有所懂,這個開心的感覺還是不可言喻的。不忘初心,繼續努力。做自己喜歡做的,開心就好。