分类 调试 下的文章

DML是一种非常简单的标记语言,它帮助基于现有命令的输出发现和执行新命令。许多WinDbg命令(以及扩展命令)都支持DML。例如,下面是lm D命令,它显示DML输出:

 

在上面的命令输出中,当我单击“SillyThreadPool”链接时,调试器为我执行了另一个命令lmDvmSillyThreadPool,它显示模块信息。同样,还有一个链接缺陷可以帮助我探索该模块中的符号和函数。

第一件事。您不必记住一堆奇怪的命令后缀就可以得到DML输出。有一个全局开关可以打开,.prefer_dml 1,这会导致许多内置命令和扩展命令显示dml输出。例如,下面是打开该开关后kn命令的输出:

 

单击时,此处的链接将切换到该帧并显示源代码和本地变量(执行的命令是.frame 0nN;dv/t/v)。
调试器脚本和扩展也可以生成DML输出。在调试器脚本中,只需使用.printf/D命令,并在其中嵌入DML链接。在调试器扩展中,可以使用IDebugClient::ControlledOutput函数输出DML。例如,下面显示单击时执行另一个命令的链接:

.printf /D "<exec cmd=\"lm m ntdll\">lm ntdll</exec>\n\n"

 

我敢打赌你不知道。应用程序本身可以向调试器输出DML命令!只需要使用outputDebugStringAPI,并在其中嵌入神奇的字符串<?dml?>. 该魔法令牌之后的所有内容都将被解释为DML字符串,并相应地显示在调试器中。例如,假设我们的应用程序中有以下代码:

OutputDebugString(L"Entered thread pool demo app.\n<?dml?><exec cmd=\"!uniqstack\">Dump unique stacks</exec>\n");

然后,调试器遇到此调试输出时将显示命令链接:

 

下一个命令是.dml_flow。此命令旨在通过将反汇编函数拆分为代码块并帮助使用DML链接在块之间导航,使其更易于读取。你自己试验这个命令比我用语言解释要容易得多,但总的来说,你提供了两个地址——一个开始地址和一个目标地址——这个命令帮助你理解从开始地址可以到达目标的代码路径。

 

前一个屏幕截图中的链接指向进入和退出屏幕上显示的基本代码块的跳转路径。

正在悠哉,突然支持的同事过来说,某个用户软件启动不了了。详细情况是:由于出现某些问题,支持同事给这个客户重装了软件,然后就启动不了了,后来把安装目录改了名字,就能运行了。虽然客户能用了,但我很不理解为什么改了个目录名就能运行呢。于是恢复现场,重现故障,不管37二十一,抓个dmp先。

用windbg打开dmp

Windows 7 Version 7601 (Service Pack 1) MP (8 procs) Free x86 compatible
Product: WinNt, suite: SingleUserTS
Machine Name:
Debug session time: Tue Sep  1 14:42:17.000 2020 (UTC + 8:00)
System Uptime: 0 days 0:09:59.840
Process Uptime: 0 days 0:00:05.000
................................................................
...............................................................
This dump file has an exception of interest stored in it.
The stored exception information can be accessed via .ecxr.
(202c.1094): Unknown exception - code c0000374 (first/second chance not available)
eax=00000000 ebx=00000000 ecx=7fffffff edx=00000000 esi=02630000 edi=0000202c
eip=7708f8d1 esp=0020e864 ebp=0020e8e8 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200246
ntdll!NtWaitForSingleObject+0x15:
7708f8d1 83c404          add     esp,4

可知,在id=1094的线程发生了c0000374(STATUS_HEAP_CORRUPTION ) 异常。

查看下栈,输入kv

0:000> kv
 # ChildEBP RetAddr  Args to Child              
00 0020e864 77118897 00000100 00000001 00000000 ntdll!NtWaitForSingleObject+0x15 (FPO: [3,0,0])
01 0020e8e8 771189c5 0020ea88 0020ead8 00000000 ntdll!RtlReportExceptionEx+0x14b (FPO: [Non-Fpo])
02 0020e940 7713ea7e 0020ea88 0020ead8 00000000 ntdll!RtlReportException+0x86 (FPO: [Non-Fpo])
03 0020e954 7713eafb c0000374 0020e988 770e4fb4 ntdll!RtlpTerminateFailureFilter+0x14 (FPO: [Non-Fpo])
04 0020e960 770e4fb4 00000000 0020efb4 7709d100 ntdll!RtlReportCriticalFailure+0x67 (FPO: [SEH])
05 0020e974 770e4e59 00000000 00000000 00000000 ntdll!_EH4_CallFilterFunc+0x12 (FPO: [Uses EBP] [0,0,4])
06 0020e99c 770d34a1 fffffffe 0020efa4 0020ead8 ntdll!_except_handler4+0x8e (FPO: [Non-Fpo])
07 0020e9c0 770d3473 0020ea88 0020efa4 0020ead8 ntdll!ExecuteHandler2+0x26 (FPO: [Uses EBP] [5,3,1])
08 0020e9e4 770d3414 0020ea88 0020efa4 0020ead8 ntdll!ExecuteHandler+0x24 (FPO: [5,0,3])
09 0020ea70 77080133 0020ea88 0020ead8 0020ea88 ntdll!RtlDispatchException+0x127 (FPO: [Non-Fpo])
0a 0020ea70 7713eaeb 0020ea88 0020ead8 0020ea88 ntdll!KiUserExceptionDispatcher+0xf (FPO: [2,0,0]) (CONTEXT @ 0020ead8)
0b 0020efb4 7713f9f1 c0000374 77174270 0020eff8 ntdll!RtlReportCriticalFailure+0x57 (FPO: [Non-Fpo])
0c 0020efc4 7713fad1 00000002 773ec360 00000000 ntdll!RtlpReportHeapFailure+0x21 (FPO: [Non-Fpo])
0d 0020eff8 770ed97c 00000009 006b0000 007149e2 ntdll!RtlpLogHeapFailure+0xa1 (FPO: [Non-Fpo])
0e 0020f028 6dd431f7 006b0000 00000000 007149e2 ntdll!RtlFreeHeap+0x64 (FPO: [Non-Fpo])
0f 0020f03c 6dd5c6d4 007149e2 00000000 0071c978 apphelp!SdbFree+0x22 (FPO: [Non-Fpo])
10 0020f080 6dd4db99 006b1ff6 0071c978 0020f288 apphelp!SdbpBuildLayerInfo+0x3d9 (FPO: [Non-Fpo])
11 0020f144 6dd4c490 006b1ff6 0071c978 0020f288 apphelp!SdbTraceQueryResult+0xd6 (FPO: [Non-Fpo])
12 0020f260 6dd4c3be 006b1ff6 0071c978 0020f288 apphelp!SeiInit+0xcb (FPO: [Non-Fpo])
13 0020f454 770d2cae 0020f560 00070000 00000000 apphelp!SE_InstallBeforeInit+0x67 (FPO: [Non-Fpo])
14 0020f470 770d2cd0 00718b90 0020f560 00070000 ntdll!LdrpLoadShimEngine+0xdc (FPO: [Non-Fpo])
15 0020f5f8 770b9f31 0020f66c 77070000 773edad0 ntdll!LdrpInitializeProcess+0x137f (FPO: [Non-Fpo])
16 0020f648 770a9799 0020f66c 77070000 00000000 ntdll!_LdrpInitialize+0x78 (FPO: [Non-Fpo])
17 0020f658 00000000 0020f66c 77070000 00000000 ntdll!LdrInitializeThunk+0x10 (FPO: [Non-Fpo])
看来是根兼容性有关阿。栈根我之前遇到问题一样(参考<记一次因兼容性问题崩溃>)。

于是远程客户机设置兼容性,发现设置过兼容性,但不行。于是我打开注册表HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers,发现里面有很多个关于我们程序设置项,先手动删掉这些项。在双击程序,成功跑起来了。

到了这里,我又有点迷惑了,之前是需要设置兼容,为什么这次又要删掉兼容的设置项呢,看来需要研究下,因为这是个问题,我还不懂。有知道的朋友也可以直接告诉我。

 

我知道你现在在想什么。断点?真的吗?关于断点有什么需要掌握的?你按F9然后停在代码行。如果你是这么想的,这篇文章是给你的:继续读

 

Visual Studio提供了一组相当丰富的断点类型和操作,可以使您的调试体验更加舒适和高效。我们将在这篇文章中讨论四种断点。

条件断点

定义断点时,可以将其与条件关联(右键单击断点并选择何时命中)。在这种情况下,您几乎可以使用任何可以在即时窗口中计算的C++/C#语句,包括复杂表达式。调试器将在每次到达断点时计算表达式的值,并且仅当其计算结果为true时才会中断。

 

请注意,尽管断点可能只偶尔中断,但它仍然会减慢应用程序的速度,因为调试器必须在应用程序暂停时计算表达式,然后才能恢复它

追踪点---Tracepoints

断点不必中断应用程序的执行。在许多情况下,您只需要检查某些变量或表达式的当前值,然后立即继续执行。这不需要更改代码以包含跟踪语句:您只需将断点与跟踪操作相关联,跟踪操作将向Visual Studio输出窗口显示您想要的任何消息。同样,您可以显示复杂的表达式。

 

函数断点

即使没有函数的源代码,也可以在函数中设置断点。只需转到Debug>New Breakpoint>Break at函数并指定函数名。这也可以从“调用堆栈”窗口中获得-只需单击一个帧并按F9在该函数上放置一个断点。

 

数据断点

修改特定内存位置时,可以使用数据断点停止调试器。这个超级强大的特性对于多线程应用程序和各种内存损坏场景非常有用,在这些场景中,内存位置会以意外的方式修改。与源断点不同,您不会将断点放在代码行上。相反,您可以使用Debug>New Breakpoint>newdatabreakpoint菜单项指定内存位置。

 

不幸的是,这个特性只适用于C/C++应用程序。当托管对象在几代之间升级或发生堆压缩时,它们在堆中移动,使它们无法跟踪。

总结

如您所见,通过组合条件断点和跟踪点,您可以获得应用程序执行的跟踪,而无需向代码中添加任何跟踪语句或重新启动调试会话。通过添加函数断点和数据断点,可以进一步提高调试工作的灵活性。享受您的Visual Studio断点!

到目前为止,我还没有写过一篇博客文章,给出关于做绩效工作的哲学建议。但是最近我想也许是时候写这样一个条目了,因为我见过很多人认真地看一些性能计数器(通常是不正确的)或其他数据,并问了很多问题,比如“这个分配率太高了吗?”,“在我看来它太高了。”或者“我的size太大了,对吧?",在他们没有足够的证据来证明这样的调查和问题是合理的之前,这似乎是个大问题。
现在,如果你只是问些问题来满足你的好奇心,那就太好了。我很乐意回答您的问题,也很乐意为您指出要阅读的文档。但对于那些被要求调查与绩效相关的问题的人,尤其是在期限临近时,我的建议是“在试图找到解决方案之前,先了解问题所在”。根据证据来决定要看什么,而不是基于你对这个领域的知识的缺乏,除非你已经穷尽了你所知道的领域。在你问与GC相关的问题之前,问问你自己你是否认为GC实际上就是问题所在。如果你不能回答这个问题,那么你就不能好好利用你的时间去问与GC相关的问题。
我见过很多例子,当托管应用程序中出现问题时,人们会立即怀疑GC,而没有任何证据支持这种怀疑。然后他们开始问问题——通常是非常随机的——希望他们能在不了解问题是什么的情况下找到解决问题的方法。这不符合逻辑,是吗?所以别这样!

因此,您如何知道要解决的问题是什么,我建议您:

1、了解基础知识真的很有帮助。

什么是基础?一般来说,性能归结为两件事——内存和CPU。了解这两个方面的基本知识有助于确定要看哪一个方面。显然这需要大量的阅读和实验。我将列出一些内存基础知识,以帮助您入门:内存的一些基本原理。

每个进程都有自己独立的虚拟地址空间;同一台机器上的所有进程共享物理内存(如果有页面文件,还可以加上页文件)。在32位上,默认情况下,每个进程都有一个2GB的用户模式虚拟地址空间。
作为应用程序的作者,您使用的是虚拟地址空间—您永远不会直接操作物理内存。如果您正在编写本机代码,您通常通过某种win32堆API(crt堆或进程堆或您创建的堆)使用虚拟地址空间—这些堆API将代表您分配和释放虚拟内存;如果您正在编写托管代码,GC将代表您分配/释放虚拟内存。
虚拟地址空间可能会变得支离破碎-换句话说,地址空间中可能存在“洞”(空闲块)。当请求一个VM分配时,VM管理器需要找到一个足够大的空闲块来满足分配请求-如果只有几个空闲块的总和足够大,它将无法工作。这意味着即使你有2GB,你也不一定看到所有2GB都被使用了。
虚拟机可以处于不同的状态-空闲、保留和提交。免费很容易。有时,人们会困惑于保守和承诺之间的区别。首先,你需要认识到它们是不同的状态。Reserved是说“我想让这个内存区域供我自己使用”。在您保留了一个VM块之后,该块不能用于满足其他保留请求。此时,您还不能将任何数据存储在该内存块中—要做到这一点,您必须提交它,这意味着您必须用一些物理存储来备份数据,以便在其中存储数据。当您通过性能计数器查看内存时,请确保您看到的是正确的。如果要保留的空间或提交的空间不足,则可能会耗尽内存。
如果你有一个页面文件(默认情况下是这样),即使你的物理内存压力很低,你也可以使用它。当你的物理内存压力第一次变大,操作系统需要在物理内存中腾出空间来存储其他数据时,它会在页面文件中备份当前物理内存中的一些数据。在需要数据之前,这些数据不会被分页,这样您就可以进入物理内存负载非常低的情况下观察分页。

2、了解您的性能要求是必须的。

如果你正在编写一个服务器应用程序,很可能你想使用所有可用的内存和CPU,因为人们会完全精巧地使用机器来运行你的应用程序,那么为什么要浪费资源呢?如果你必须在另一台机器上运行的应用程序完全不同,那么你就知道如何在另一台机器上编写故事了。没有“你必须让你的应用程序尽可能少地使用内存”这样的规则。
当你认为有问题的时候,就去挖掘它,而不是去猜测可能出了什么问题。看看是谁在用你的内存。如果您认为托管堆使用了太多内存,请查看原因。使用过多内存的托管堆通常意味着您在应用程序中存活太多对象。看看那些幸存者是怎么活下来的。

真是个不可思议的巧合。仅隔几天,我就要解决两个与嵌套异常处理程序有关的问题。具体来说,导致堆栈溢出的嵌套异常的无限循环。这是一个非常致命的组合。堆栈溢出对于调试来说是一个极其严重的错误;嵌套异常意味着异常处理程序遇到了一个异常,这是不可能的;更糟糕的是,堆栈损坏也在幕后发生。请继续阅读以了解诊断嵌套异常的技巧,以及首先可能导致这些异常的原因。

案例1:VC异常过滤器中的读取错误

客户机有多个转储文件供我查看,它们都显示了一个非常疯狂的模式。应用程序中会发生异常,这是完全可以预料和处理的异常。但是,它不会被正常处理,而是会导致无限的嵌套异常级联,最终导致进程崩溃,出现堆栈溢出。为了简洁起见,这里有一个简短的图片:

0:000> kcn 10000
... <repeated hundreds more times>
19e9 <Unloaded_Helper.dll>
19ea NestedExceptions1!exception_filter
19eb NestedExceptions1!trigger_exception
19ec MSVCR120D!_EH4_CallFilterFunc
19ed MSVCR120D!_except_handler4_common
19ee NestedExceptions1!_except_handler4
19ef ntdll!ExecuteHandler2
19f0 ntdll!ExecuteHandler
19f1 ntdll!KiUserExceptionDispatcher
19f2 <Unloaded_Helper.dll>
19f3 NestedExceptions1!exception_filter
19f4 NestedExceptions1!trigger_exception
19f5 MSVCR120D!_EH4_CallFilterFunc
19f6 MSVCR120D!_except_handler4_common
19f7 NestedExceptions1!_except_handler4
19f8 ntdll!ExecuteHandler2
19f9 ntdll!ExecuteHandler
19fa ntdll!KiUserExceptionDispatcher
19fb <Unloaded_Helper.dll>
19fc NestedExceptions1!exception_filter
19fd NestedExceptions1!trigger_exception
19fe MSVCR120D!_EH4_CallFilterFunc
19ff MSVCR120D!_except_handler4_common
1a00 NestedExceptions1!_except_handler4
1a01 ntdll!ExecuteHandler2
1a02 ntdll!ExecuteHandler
1a03 ntdll!KiUserExceptionDispatcher
1a04 NestedExceptions1!trigger_exception
1a05 NestedExceptions1!main
1a06 NestedExceptions1!__tmainCRTStartup
1a07 NestedExceptions1!mainCRTStartup
1a08 kernel32!BaseThreadInitThunk
1a09 ntdll!__RtlUserThreadStart
1a0a ntdll!_RtlUserThreadStart

在前面的调用堆栈中,很明显exception_filter试图调用卸载的DLL(Helper.DLL)中的函数。这又会导致一个异常(很可能是访问冲突),该异常会将控制权转移到异常过滤器,而我们在嵌套的异常循环中处于非常深的位置。顺便说一下,如果您知道要查找什么,那么很容易跟踪异常链。以下是几帧的kb输出:

006deb54 00a85706 00000000 00000000 00000000 <Unloaded_Helper.dll>+0x6666665e
006dec28 00a85eab 006dec4c 0f4e3924 00000000 NestedExceptions1!exception_filter+0x26
006dec30 0f4e3924 00000000 00000000 00000000 NestedExceptions1!trigger_exception+0x6b
006dec44 0f4e9268 006deda0 006dedf0 00000001 MSVCR120D!_EH4_CallFilterFunc+0x12
006dec7c 00a866d2 00a90000 00a81041 006deda0 MSVCR120D!_except_handler4_common+0xb8
006dec9c 7794c881 006deda0 006df734 006dedf0 NestedExceptions1!_except_handler4+0x22
006decc0 7794c853 006deda0 006df734 006dedf0 ntdll!ExecuteHandler2+0x26
006ded88 7794c6bb 006deda0 006dedf0006deda0 ntdll!ExecuteHandler+0x24
006ded88 58b3115e 006deda0 006dedf0 006deda0 ntdll!KiUserExceptionDispatcher+0xf
006df0d4 00a85706 00000000 00000000 00000000 <Unloaded_Helper.dll>+0x6666665e
006df1a8 00a85eab 006df1cc 0f4e3924 00000000 NestedExceptions1!exception_filter+0x26
006df1b0 0f4e3924 00000000 00000000 00000000 NestedExceptions1!trigger_exception+0x6b
006df1c4 0f4e9268 006df324 006df374 00000001 MSVCR120D!_EH4_CallFilterFunc+0x12
006df1fc 00a866d2 00a90000 00a81041 006df324 MSVCR120D!_except_handler4_common+0xb8
006df21c 7794c881 006df324 006df734 006df374 NestedExceptions1!_except_handler4+0x22
006df240 7794c853 006df324 006df734 006df374 ntdll!ExecuteHandler2+0x26
006df30c 7794c6bb 006df324 006df374 006df324 ntdll!ExecuteHandler+0x24
006df30c 00a85e8f 006df324 006df374 006df324 ntdll!KiUserExceptionDispatcher+0xf
006df744 00a86068 00000000 00000000 7ebab000 NestedExceptions1!trigger_exception+0x4f
006df818 00a86a79 00000001 00c37b88 00c35178 NestedExceptions1!main+0x28
006df868 00a86c6d 006df87c 76dc919f 7ebab000 NestedExceptions1!__tmainCRTStartup+0x199
006df870 76dc919f 7ebab000 006df8c0 77960bbb NestedExceptions1!mainCRTStartup+0xd
006df87c 77960bbb 7ebab000 35ed4a97 00000000 kernel32!BaseThreadInitThunk+0xe
006df8c0 77960b91 ffffffff 7794c9d2 00000000 ntdll!__RtlUserThreadStart+0x20
006df8d0 00000000 00a812d5 7ebab000 00000000 ntdll!_RtlUserThreadStart+0x1b

 突出显示的值(ntdll!ExecuteHandler的第二个参数)是上下文记录,前面的值是异常记录。您可以使用.cxr和.exr命令在WinDbg中检查它们:

 0:000> .exr 006df324 E

xceptionAddress: 00a85e8f (NestedExceptions1!trigger_exception+0x0000004f)

ExceptionCode: c0000005 (Access violation)

ExceptionFlags: 00000000

NumberParameters: 2

Parameter[0]: 00000001

Parameter[1]: 00000000

Attempt to write to address 00000000

0:000> .cxr 006dedf0

eax=cccccccc ebx=00000000 ecx=00000000 edx=00000000 esi=006df0dc edi=006df1a8 eip=58b3115e esp=006df0d8 ebp=006df1a8 iopl=0 nv up ei pl nz na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206

<Unloaded_Helper.dll>+0x6666665e: 58b3115e ?? ???

0:000> .exr 006deda0

ExceptionAddress: 58b3115e (<Unloaded_Helper.dll>+0x0006666665e)

ExceptionCode: c0000005 (Access violation)

ExceptionFlags: 00000010

NumberParameters: 2

Parameter[0]: 00000008

Parameter[1]: 58b3115e

Attempt to execute non-executable address 58b3115e

这表明原始异常是trigger_exception函数中的访问冲突,但它被另一个访问冲突所掩盖,该访问冲突是由从卸载的DLL执行代码引起的。

但最初的问题比卸载的DLL更微妙。在实际应用中,exception_filter实际上是由VisualC++处理的异常过滤器,用于处理C++异常:msvcr*!__InternalCxxFrameHandler。不知何故,它会触发嵌套异常,异常代码是0xc0000060xc0000006 (IN_PAGE_ERROR: in-page I/O error) 和 I/O error code 0xc000020c (STATUS_CONNECTION_DISCONNECTED)。该嵌套异常会将控制权再次传输到__InternalCxxFrameHandler,并会一次又一次地命中相同的嵌套错误。

有了这些信息,我们开始调查。导致嵌套异常的实际内存访问是一个数据结构传递给了 __InternalCxxFrameHandler处理程序,它存储在二进制文件中,并包含由C++编译器发出的异常处理信息。此信息不经常访问-仅在发生异常时才需要。

最后一个难题是应用程序是从一个网络驱动器运行的,这个驱动器被大量的机器访问,给服务器的网络连接带来了很大的负载。因此,工作流程如下:

  1. 应用程序的初始化路径早期发生异常
  2. 还发生连接错误,导致应用程序二进制文件所在的网络驱动器暂时断开连接
  3. VisualC++异常过滤器需要访问存储在应用程序二进制文件中的数据结构,但数据结构尚未从网络驱动器缓存在RAM中,因为只有在异常发生时才访问该数据结构。
  4. 异常筛选器随后失败,并出现I/O错误,该错误再次将控制权传递给异常筛选器-导致异常的嵌套循环

通过编写访问大型只读全局数据结构(只读全局数据也存储在应用程序二进制文件中)的异常过滤器,并从网络驱动器运行示例应用程序,我能够重建此问题。当我在导致异常之前禁用了网络适配器时,我们得到了上面描述的症状。

我们是怎么解决这个问题的?事实证明,VisualC++有一个名为/SWAPRUN的链接器设置(具体地说,/SWAPRUN:NET),它可以用来指示系统在执行之前将应用程序二进制加载到内存中。这意味着应用程序不可能开始运行,但由于连接问题,二进制文件的某些部分会突然变得不可用。显然,首先最好避免连接问题,但是考虑到网络本身是不可靠的。

案例2:导致堆栈溢出的堆栈损坏

第二个案例展示了导致堆栈溢出的嵌套异常链。唯一的区别是堆栈也已损坏。下面是堆栈底部的一些帧:

0:000> kn
... <repeated hundreds more times>
f04 002be8c4 7794c881 0xcccccccc
f05 002be8e8 7794c853 ntdll!ExecuteHandler2+0x26
f06 002be9b0 7794c6bb ntdll!ExecuteHandler+0x24
f07 002be9b0 cccccccc ntdll!KiUserExceptionDispatcher+0xf
f08 002becfc 7794c881 0xcccccccc
f09 002bed20 7794c853 ntdll!ExecuteHandler2+0x26
f0a 002bede8 7794c6bb ntdll!ExecuteHandler+0x24
f0b 002bede8 cccccccc ntdll!KiUserExceptionDispatcher+0xf
f0c 002bf134 7794c881 0xcccccccc
f0d 002bf158 7794c853 ntdll!ExecuteHandler2+0x26
f0e 002bf224 7794c6bb ntdll!ExecuteHandler+0x24
f0f 002bf224 010b51d5 ntdll!KiUserExceptionDispatcher+0xf
f10 002bf674 cccccccc NestedExceptions2!trigger_exception+0x65
f11 002bf748 010b5cd9 0xcccccccc
f12 002bf798 010b5ecd NestedExceptions2!__tmainCRTStartup+0x199
f13 002bf7a0 76dc919f NestedExceptions2!mainCRTStartup+0xd
f14 002bf7ac 77960bbb kernel32!BaseThreadInitThunk+0xe
f15 002bf7f0 77960b91 ntdll!__RtlUserThreadStart+0x20
f16 002bf800 00000000 ntdll!_RtlUserThreadStart+0x1b

堆栈上的0xCCCCCC地址看起来像是堆栈损坏的必然征兆。实际上,您可能已经有了一个有效的假设:堆栈已被0xcccccccc损坏,ntdll中的异常处理代码以某种方式尝试从地址0xcccccccc执行代码。这会导致嵌套异常,我们的情况与case#1相同。如果我们查看异常记录,我们可以确认。(第一个异常记录是根本原因,第二个异常记录是由于无效的异常处理程序造成的):

0:000> .exr 002bf23c 
ExceptionAddress: 010b51d5 (NestedExceptions2!trigger_exception+0x00000065)
 ExceptionCode: c0000005 (Access violation)
 ExceptionFlags: 00000000
NumberParameters: 2
 Parameter[0]: 00000001
 Parameter[1]: 00000000
Attempt to write to address 00000000
0:000> .exr 002bee00 
ExceptionAddress: cccccccc
 ExceptionCode: c0000005 (Access violation)
 ExceptionFlags: 00000010
NumberParameters: 2
 Parameter[0]: 00000000
 Parameter[1]: cccccccc
Attempt to read from address cccccccc

但也有一些微妙之处。为什么ntdll试图从无效地址0xcccccccc执行代码?答案是,在32位Windows应用程序中,异常过滤器的地址存储在堆栈中,作为名为异常注册记录的数据结构的一部分。如果该结构已损坏,则ntdll中的异常处理代码可能会尝试执行无效地址,认为它是指向异常筛选器的指针。
但这实际上是一个相当严重的安全漏洞!如果攻击者可以覆盖异常注册记录,然后触发异常,则可以执行任意代码。事实上,开发异常注册记录是克服Visual C++ 2003(/GS标志)中引入的堆栈防御的方法之一。
幸运的是,Windows有一些技巧。首先,Visual C++ 2003引入了一个链接器标志,称为。当此标志可用时,链接器在二进制文件中嵌入所有有效异常处理程序的目录。在运行时,Windows可以检查即将运行的异常处理程序是否在有效异常处理程序列表中。如果不是,它将直接拒绝执行无效的异常处理程序,并停止进程-这比尝试执行未知的异常处理程序要安全得多。

其次,Windows Vista SP1和Windows Server 2008引入了另一种系统范围的保护机制SEHOP(结构化异常处理覆盖保护)。默认情况下,在客户端操作系统(Windows Vista、Windows 7、Windows 8)上禁用SEHOP,在服务器操作系统(Windows server 2008和Windows server 2012)上启用SEHOP。SEHOP是一个相当简单的防御:当它打开时,每个线程在链的开头都有一个虚拟的异常注册记录。当发生异常时,ntdll中的异常处理代码将验证当前异常处理程序链是否以该虚拟异常注册记录终止。如果没有,则系统假设异常处理程序链已被篡改,并终止进程。

当这些防御打开时,上述情况根本不可能发生。将阻止将控件传输到0xCCCCCC,就像它是异常处理程序一样,并且该进程将突然终止。因此,我们可以得出结论,发生此崩溃的系统不符合现代安全指南:应用程序应该使用/SAFESEH编译,并且系统应该启用SEHOP。顺便说一下,从Windows7开始,您可以使用ImageFileExecutionOptions注册表项为单个进程配置SEHOP,而不会影响系统的其余部分。
例如,下面是使用/SAFESEH编译二进制文件时发生的情况(注意,启用软件DEP的/NXCOMPAT标志也是必需的):

:000> kn
 # ChildEBP RetAddr 
00 008aee54 779c625f ntdll!NtWaitForMultipleObjects+0xc
01 008af2b8 779c5e38 ntdll!RtlReportExceptionEx+0x3eb
02 008af314 779e81bf ntdll!RtlReportException+0x9b
03 008af394 7798b2e3 ntdll!RtlInvalidHandlerDetected+0x4e04 008af3ec 7797734a ntdll!RtlIsValidHandler+0x13f1a
05 008af484 7794c6bb ntdll!RtlDispatchException+0xfc
06 008af484 01284ef5 ntdll!KiUserExceptionDispatcher+0xf
07 008af8d4 cccccccc NestedExceptions2!trigger_exception+0x65
WARNING: Frame IP not in any known module. Following frames may be wrong.
08 008af9a8 012858c9 0xcccccccc
09 008af9f8 01285a0d NestedExceptions2!__tmainCRTStartup+0x199
0a 008afa00 76dc919f NestedExceptions2!mainCRTStartup+0xd
0b 008afa0c 77960bbb kernel32!BaseThreadInitThunk+0xe
0c 008afa50 77960b91 ntdll!__RtlUserThreadStart+0x20
0d 008afa60 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> !analyze -v
... <removed for brevity>
DEFAULT_BUCKET_ID: APPLICATION_FAULT
PROCESS_NAME: NestedExceptions2.exe
ERROR_CODE: (NTSTATUS) 0xc00001a5 - An invalid exception handler routine has been detected.
EXCEPTION_CODE: (NTSTATUS) 0xc00001a5 - An invalid exception handler routine has been detected.
... <removed for brevity>

重要的是,不存在嵌套异常的无限循环。进程立即终止,并调用Windows错误报告。另一方面,当为特定进程启用SEHOP时,它会阻止无效的异常处理程序执行。在一个典型的WER转储文件中,结果显示为原始异常(本应正常处理)最终未处理:

0:000> !analyze -v

... FAULTING_IP: NestedExceptions2!trigger_exception+65 002c51d5 c705000000002a000000 mov dword ptr ds:[0],2Ah

EXCEPTION_RECORD: ffffffff -- (.exr 0xffffffffffffffff)

ExceptionAddress: 002c51d5 (NestedExceptions2!trigger_exception+0x00000065)

ExceptionCode: c0000005 (Access violation)

ExceptionFlags: 00000008

NumberParameters: 2

Parameter[0]: 00000001

Parameter[1]: 00000000 Attempt to write to address 00000000 ...

BUGCHECK_STR: APPLICATION_FAULT_NULL_POINTER_WRITE_SEHOP

PRIMARY_PROBLEM_CLASS: NULL_POINTER_WRITE_SEHOP

DEFAULT_BUCKET_ID: NULL_POINTER_WRITE_SEHOP ...

FAILURE_BUCKET_ID: NULL_POINTER_WRITE_SEHOP_c0000005_NestedExceptions2.exe!trigger_exception

BUCKET_ID: APPLICATION_FAULT_NULL_POINTER_WRITE_SEHOP_nestedexceptions2!trigger_exception+65

ANALYSIS_SOURCE: UM FAILURE_ID_HASH_STRING: um:null_pointer_write_sehop_c0000005_nestedexceptions2.exe!trigger_exception ...

但有一个很好的暗示是SEHOP参与了其中——分析中“SEHOP”这个词出现了多次。我不确定是否有更好的方法来确定SEHOP的参与,但这对我来说已经足够了

结论

首先,在处理嵌套异常的无限循环时,不要惊慌。您需要标识启动链的原始异常,然后查找重复模式。链中某个点上的异常筛选器/处理程序必须已失败,并且一系列控制传输将返回到同一个异常筛选器。
其次,结构化异常处理是一种易受攻击的机制,特别是在32位应用程序中。确保使用编译器和操作系统提供的所有保护:SafeSEH、DEP和SEHOP。