|
一、介绍
这是我的《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 }
复制代码 View Code 3.2、ExampleCore_7_02
- 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 }
复制代码 View Code 3.3、ExampleCore_7_022(动态链接库,C++)
- 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 }
复制代码 View Code 3.4、ExampleCore_7_03
- 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 }
复制代码 View Code 3.5、ExampleCore_7_033(动态链接库,C++)
- 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 }
复制代码 View Code 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 }
复制代码 View Code 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 }
复制代码 View Code
四、基础知识
在这一段内容中,有的小节可能会包含两个部分,分别是 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 <strong>00007ffd`e45b6980</strong> 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 <strong>Breakpoint 0 hit
- </strong>18 <strong>KERNEL32!BeepImplementation:
- </strong>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 <strong>[InlinedCallFrame: 000000b3e0f9e6c8] ExampleCore_7_01.Program.Beep</strong>(UInt32, UInt32)
- 5 000000B3E0F9E6C8 00007ffcad271a63 <strong>[InlinedCallFrame: 000000b3e0f9e6c8] ExampleCore_7_01.Program.Beep</strong>(UInt32, UInt32)
- 6 000000B3E0F9E6A0 00007FFCAD271A63 <strong>ILStubClass.IL_STUB_PInvoke</strong>(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 <strong>00007ffd`0d09a548</strong> 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 <strong>00007ffd`0d09a548
- </strong>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]
复制代码 00007ffd`0d09a548 这个地址就是 coreclr!InlinedCallFrame 的虚函数表(vftable)。我们继续使用【dp 00007ffd`0d09a548】命令查看它的内容。- 1 0:000> dp 00007ffd`0d09a548
- 2 00007ffd`0d09a548 <strong>00007ffd`0cdf9d80</strong> 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 <strong>coreclr!InlinedCallFrame::`vftable</strong>'+0xf (00007ffd`0d09a557)
- 6 00007ffd`0d09a557 00809ddf0cfd add byte ptr [rax-2F32063h],al
- 7 00007ffd`0d09a55d 7f00 jg <strong>coreclr!InlinedCallFrame::`vftable</strong>'+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 <strong>coreclr!InlinedCallFrame::`vftable</strong>'+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 <strong>KERNEL32!BeepImplementation (BeepImplementation)
- </strong>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 <strong>Breakpoint 0 hit
- </strong>18 <strong>KERNEL32!BeepImplementation:
- </strong>19 <strong>00007ffd`e45b6980 48895c2418 mov qword ptr [rsp+18h],rbx ss:00000025`eb17e610=00000025eb17e900</strong>
复制代码 红色标注的就是我们想要断住的方法。到了这里,我们看看当前的调用栈,使用【!clrstack】命令。- 1 0:000> !clrstack
- 2 OS Thread Id: 0x4f6c (0)
- 3 Child SP IP Call Site
- 4 00000025EB17E628 00007ffde45b6980 [<strong>InlinedCallFrame: 00000025eb17e628</strong>] ExampleCore_7_01.Program.Beep(UInt32, UInt32)
- 5 00000025EB17E628 00007ffcad3b1a63 [InlinedCallFrame: 00000025eb17e628] ExampleCore_7_01.Program.Beep(UInt32, UInt32)
- 6 00000025EB17E600 00007ffcad3b1a63 <strong>ILStubClass.IL_STUB_PInvoke</strong>(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 <strong>00007ffd`0d1da548</strong> 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 <strong>00007ffd`0d1da548
- </strong>2 Browse module
- 3 Set bu breakpoint
- 4
- 5 (<strong>00007ffd`0d1da548</strong>) <strong>coreclr!InlinedCallFrame::`vftable</strong>' | (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 <strong>coreclr!InlinedCallFrame::`vftable'</strong>+0xf (00007ffd`0d1da557)
- 6 00007ffd`0d1da557 00809df30cfd add byte ptr [rax-2F30C63h],al
- 7 00007ffd`0d1da55d 7f00 jg <strong>coreclr!InlinedCallFrame::`vftable</strong>'+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 <strong>coreclr!InlinedCallFrame::`vftable</strong>'+0x1f (00007ffd`0d1da567)
- 12 00007ffd`0d1da567 00e0 add al,ah
复制代码 输出信息表明这个地址对应于对象 InlinedCallFrame 的虚函数表。我们可以进一步将虚函数表转储出来,执行命令【dp 00007ffd`0d1da548 l4】。- 0:000> dp <strong>00007ffd`0d1da548</strong> l4
- 00007ffd`0d1da548 <strong>00007ffd`0cf39d80</strong> 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 <strong>(fa0.1cf0): CLR exception - code e0434352 (first chance)
- </strong>20 <strong>(fa0.1cf0): CLR exception - code e0434352 (!!! second chance !!!)
- </strong>21 KERNELBASE!RaiseException+0x69:
- 22 <strong>00007ff9`c07ecf19</strong> 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 <strong>ExampleCore_7_02.Program.Main</strong>(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_7_02\Program.cs @ <strong>13</strong>]
复制代码 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 <strong>(25fc.9f0): CLR exception - code e0434352 (first chance)
- </strong>20 <strong>(25fc.9f0): CLR exception - code e0434352 (!!! second chance !!!)
- </strong>21 <strong>KERNELBASE!RaiseException</strong>+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 <strong>[PrestubMethodFrame: 000000338c57e528] ExampleCore_7_02.Program.Alloc(System.String)
- </strong>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 <strong>KERNELBASE!RaiseException</strong>+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 <strong>ExampleCore_7_033!AsyncProcess+0x1f:(断点处)
- </strong>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 <strong>ptr</strong> = <strong>0x00007fff`3e063024
- </strong>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 <strong>ExampleCore_7_033!ThreadWorkItem+</strong>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,运行效果如图:
第二次:10000,使用内存 37.0 MB,运行效果如图:
第三次:100000,使用内存 77.0 MB,运行效果如图:
第四次:1000000,使用内存 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 <strong>Total size: Size: 0x178000 (1540096) bytes total, 0x5000 (20480) bytes wasted.
- </strong>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 <strong>Total size: Size: 0x178000 (1540096) bytes total, 0x5000 (20480) bytes wasted.
- </strong>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 <strong>GC Allocated Heap Size: Size: 0x19d90 (105872) bytes.
- </strong>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%
- <strong>18 Heap 44 0`1fa49000 ( 506.285 MB) 0.02% 0.00%</strong>
- 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 <strong>Total size: Size: 0x248000 (2392064) bytes total, 0x5000 (20480) bytes wasted.(内存使用不大,没异常)
- </strong>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 <strong>GC Allocated Heap Size: Size: 0x19d90 (105872) bytes.(GC 堆才 105 KB,也没问题)
- </strong>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 <strong>Heap 44 0`1fa49000 ( 506.285 MB) 0.02% 0.00%(这个是比较类似的)</strong>
- 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 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。
来源:https://www.cnblogs.com/PatrickLiu/p/18190864
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|