久趣下载站

当前位置: 首页 » 游戏攻略 » Advanced .Net Debugging 9:平台互用性

Advanced .Net Debugging 9:平台互用性



一、介绍



这是我的《

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





3.2、ExampleCore_7_02





3.3、ExampleCore_7_022(动态链接库,C++)





3.4、ExampleCore_7_03





3.5、ExampleCore_7_033(动态链接库,C++)




3.6、ExampleCore_7_04



3.7、ExampleCore_7_044(动态链接库,C++)



四、基础知识



在这一段内容中,有的小节可能会包含两个部分,分别是 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】这个函数。

有了地址,我们就可以针对这个地址下断点,执行命令【bp


00007ffd`e45b6980


】,或者【bp KERNEL32!

BeepImplementation

】这两种形式都是可以的。我们可以使用【bl】命令查看断点列表。

断点设置成功后,【g】直接运行调试器,它会在断点出暂停。

我们此时使用【!clrstack】命令看看栈回溯。

我们看到了 ExampleCore_7_01.Program.Main 方法调用了



ExampleCore_7_01.Program.Beep



方法。



ExampleCore_7_01.Program.Beep



方法对应的栈帧有如下一个前缀:



[InlinedCallFrame: 000000b3e0f9e6c8]





000000b3e0f9e6c8



这个地址我们使用【dp



000000b3e0f9e6c8



】命令查看一下它的内容。




00007ffd`0d09a548



针对这个地址,我们使用【ln



00007ffd`0d09a548



】命令,是什么东西。




00007ffd`0d09a548



这个地址就是 coreclr!InlinedCallFrame 的虚函数表(vftable)。我们继续使用【dp



00007ffd`0d09a548



】命令查看它的内容。




00007ffd`0cdf9d80



针对这个地址,我们继续使用【ln



00007ffd`0cdf9d80



】命令看看它的内容。




00007ffd`0d09a548



这个地址就是 coreclr!InlinedCallFrame 的虚函数表(vftable),我们可以使用【!u



00007ffd`0d09a548



】命令查看它的汇编源码。


2)、Windbg Preview 调试


我们编译项目,打开【Windbg Preview】用户态调试器,依次点击【文件】—-》【Launch executable】加载我们可执行程序 ExampleCore_7_01.exe,打开调试器的界面,程序已经处于中断状态。

当我们进入 Windbg 调试器界面后,我们使用【x kernel32!*beep*】命令,查找一下【Beep】这个函数。

然后我们在这个方法上下断点,通过【bp

KERNEL32!BeepImplementation 】命令下断点。

断点设置成功后,

然后继续【g】运行调试器,会在我们设置的断点出中断执行

红色标注的就是我们想要断住的方法。到了这里,我们看看当前的调用栈,使用【!clrstack】命令。

从【!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


】命令查看一下这个指针。




00007ffd`0d1da548



这个地址像一个代码地址,因此,我们使用【ln



00007ffd`0d1da548



】命令查看该地址能否被解析成代码。

当然,我们也可以使用【!

u 00007ffd`0d1da548

】命令,查看 coreclr!InlinedCallFrame 方法的源码,这个源码是汇编源码。

输出信息表明这个地址对应于对象


InlinedCallFrame


的虚函数表。我们可以进一步将虚函数表转储出来,执行命令【

dp


00007ffd`0d1da548


l4



】。




00007ffd`0cf39d80



并在这个函数地址上使用【ln



00007ffd`0cf39d80



】命令来观察它所包含的内容。


不是很难,就不多做解释了。



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】直接运行调试器,调试器会抛出异常,中断执行。

原著中说的是抛出“访问违例”的异常,我这里是没有看到,只是看到了内核态抛出了异常。

我按着原书的步骤来,如果我们想获取是哪行源代码出问题了,可以使用【!lines】命令

我们再使用【!clrstack】命令,查看一下托管线程调用栈,源代码的行号就会显示出来。



ExampleCore_7_02.Program.Main


栈帧的内容最后有一个数字 13,这个就是源代码的行号,出错的行号。

效果如图:

最开始的时候,我们的调试器抛出异常,有一个地址


00007ff9`c07ecf19


,这个地址就是错误代码,可以使用【!u


00007ff9`c07ecf19


】命令查看代码的内容。

就是抛出异常的代码。


2)、Windbg Preview 调试


编译项目,打开【Windbg Preview】,依次点击【文件】—【Launch executable】,加载我们的可执行程序 ExampleCore_7_02.exe,进入调试器。

我们直接【g】运行调试器,看到调试器抛出异常。

原书上说的是出现了异常,原因是访问违例,这里不是的,这么多年了,变化也不小。

我按着原书的步骤来,如果我们想获取是哪行源代码出问题了,可以使用【!lines】命令。

其实,在【Windbg Preview】里面不需要使用这个命令。原书的内容是使用了【k】命令,其实作用不大,我们其实可以直接使用【!clrstack】命令,看得更直接。

托管线程的调用栈很清楚,就不多说了。我们可以使用【!u

00007fff4fbfcf19

】查看一下这个地址的代码是什么。



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】方法下断点,但是执行不成功。

那我们就通过源码的方式直接给 C++ AsyncProcess 方法下断点。我们点击 Windbg 菜单栏,依次选择【Source】—>【Open Source File】,打开选择我们的 C++ 项目中的 ExampleCore_7_033.cpp 文件。效果如图:

断点设置完成后,我们直接执行【g】命令,继续运行调试器。效果如图:



【Windbg Preview】命令窗口展示如图:

执行效果如下:

我们继续执行【dv】命令,可以看到有一个 ptr,那就是我们从托管代码中传递到非托管代码中的委托,就是一个指针。




ptr


】这个字段在【Windbg Preview】里是可以点击的,如果是命令行调试就不可以了,比如:NTSD 等。如图:



我们可以使用【u

0x00007fff`3e063024

】命令,查看一下这个 ptr 是什么。

我们在【PCallback callback = (PCallback)lpParameter;】这行代码在下一个断点,也就是2秒后会执行这个回调。效果如图:

断点设置成功后,我们继续执行调试器,使用【g】命令。

如图:

到了这里,我们在使用【

u

0x00007fff`3e063024

】命令,查看一下这个 ptr 是什么东西。

我在 .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.5 MB ,和 492.8 MB 差的太远了,说明加载堆没异常。

我们继续使用【!eeheap -gc】命令查看一下 GC 堆是否有问题。

GC 堆的内容也不是很大,所以就不是托管堆的问题。

我们使用【!address -summary】命令查看一下进程整体的内存使用情况。

红色标注的和我们 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】命令查看一下加载堆有没有异常。

我们再使用【!eeheap -gc】命令查看一下 GC 堆有没有异常情况。

GC 堆也没出现内存暴涨的情况,没什么问题。

由于大部分内存消耗并不在托管堆上,因此,我们需要使用【!address -summary】来了解进程中内存使用的情况。

既然是在非托管堆上分配的,我们很容易就会联想到和 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 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。

猜你喜欢
本类排行