常见WinDbg问题及解决方案
当你调试一个程序时,你最不想处理的是调试器不能正常工作。当你试图集中精力跟踪一个bug时,总是会因为次要的问题而被忽略,尤其是当调试器的问题导致你失去一个重新编程或者浪费了大量的时间等待调试器完成它,而调试器知道这需要永远做些什么的时候。
这是我时常会遇到的大量问题,所以我整理了一些常见问题的简短列表,人们很容易就会被这些问题绊倒(以及如何避免或解决它们)。
- 我正在使用ntsd,无法加载符号或大多数调试器扩展命令(!command)不工作。这通常意味着您启动了操作系统附带的ntsd(在Windows Vista之前),这比调试器包附带的ntsd要老得多。因为它在系统目录中,所以它将在您的可执行搜索路径中。要解决此问题,请使用调试器安装目录中的ntsd可执行文件。
- WinDbg处理模块加载事件需要很长的时间,它在一个CPU上使用最大处理器时间(旋转)。如果工作区中保存了许多跟踪模块加载事件(通过bu创建)的非限定断点,则通常会发生这种情况。当您处理的程序中有大量的修饰C++符号时,这个问题尤其明显,例如大量使用STL或其他模板类的程序的调试构建。由于强制立即加载所有模块的符号,非限定断点通常很昂贵,但它们还强制调试器对正在加载的模块中的每个符号(对于每个未解析的断点)取消修饰并执行模式匹配。
如果允许在默认工作区中保存大量不合格的断点,则无论调试的是哪个程序,都可能使调试器看起来非常慢。
为了避免被这个问题困扰,不要使用不合格的断点(没有modulename的断点!除非绝对必要。此外,如果您不需要在下次与调试器工作区的调试会话中保存所有断点(默认情况下,bu断点将保留在调试器工作区中,而bp断点在每次调试会话后都会消失),则在保存工作区之前清除所有断点通常是一个好主意。如果您习惯于每次附加到正在运行的进程时都保存工作区,并且您经常使用bu断点,则这会使用户默认工作区变得杂乱无章,如果不小心,可能会很快导致调试器性能非常差。
您可以使用bc命令删除断点(bc*删除所有断点),但您需要保存工作区以保留更改。如果问题已经到了甚至无法在合理的时间内完成模块加载,以便使用bc*清除保存的断点的程度,则可以删除HKCU\Software\Microsoft\Windbg\Workspaces注册表项和子项的内容,以使Windbg返回原始状态。这将清除已保存的调试器窗口位置和其他已保存的调试器设置,因此请将其作为最后手段使用。 - WinDbg处理模块加载事件需要很长的时间,但它不占用很多处理器时间。这通常意味着符号路径包括断开的HTTP符号存储链接或断开的UNC符号存储路径。符号路径中的非响应路径将导致任何尝试加载模块符号的操作需要很长时间才能完成,因为网络超时将反复发生。
使用!sym noise,后跟.reload/f以确定符号路径的哪个部分工作不正常。然后,修复或移除符号路径的冲突部分。
当调试位于数据包路径中的程序时,也可能会出现此问题,这些程序将数据包发送到符号路径上的某个位置。在这种情况下,我建议的典型解决方法是设置一个空符号路径,附加到有问题的进程,编写一个转储文件,然后从进程分离。然后,恢复正常符号路径并在调试器中打开转储文件,并发出.reload/f命令以强制提前预缓存所有符号。在下游存储缓存中预缓存所有符号后,将符号路径更改为仅引用下游存储缓存位置,而不引用任何UNC或HTTP符号服务器路径,并将调试器附加到包路径中的进程以进行符号服务器访问。 - WinDbg拒绝为我知道符号服务器有符号的模块加载符号。如果WinDbg以前尝试(但失败)下载模块的符号,则可能会出现此问题。dbghelp的symbol服务器支持中似乎存在一个bug,有时会导致部分下载的PDB文件留在下游存储缓存中。如果发生这种情况,以后访问模块符号的尝试将失败,并出现错误,说明找不到模块符号。
如果打开嘈杂符号加载(!sym noise),通常会给出一个更具描述性的错误。如果您看到有关E_PDB_CORRUPT的投诉,那么您可能是此问题的受害者。指示此问题的调试器输出如下所示:
DBGHELP: c:\symbols\ntdll.pdb\2744327E50A64B24A87BDDCFC7D435A02\ntdll.pdb – E_PDB_CORRUPT
如果遇到此问题,只需删除错误消息中命名的.pdb,然后通过.reload/f<modulename>命令重试加载符号。 - 当我附加到特定进程(如svchost实例)时,WinDbg将挂起并且永远不会返回。如果确信工作区中保存的符号路径已断开或模块加载跟踪断点不合格,并且调试器在附加到某个进程时从未返回(或附加到该进程时几乎总是在第一个命令之后挂起),则调试可能位于负责符号加载的代码路径中。
在调试svchost实例时,此问题尤其常见,因为在各种svchost实例中运行了许多重要但不相关的代码片段,其中一些代码对于网络符号服务器支持的工作至关重要。如果正在调试网络符号服务器支持的关键路径中的进程,并且有一个设置了网络组件的符号路径,则可能导致调试器在第一次尝试加载符号时死锁(永久挂起)。
一个可能导致这种情况的例子是,如果您正在调试与DNS缓存服务位于同一svchost实例中的代码。在这种情况下,当您尝试加载符号并且符号路径中有HTTP符号服务器链接时,调试器将死锁,因为当它尝试解析符号路径中引用的服务器的主机名时,它将尝试对DNS缓存服务进行RPC调用。因为在调试器恢复进程之前,DNS缓存服务不会响应,并且调试器在从RPC请求获得对DNS缓存服务的响应之前,也不会恢复进程,所以调试会话将无限期挂起。
请注意,如果只是调试符号服务器存储区的数据包路径中的某些内容,则通常会看到调试器在很长一段时间内变得无响应,但不会完全挂起。这是因为调试器可以处理网络超时(如果有点慢的话)并最终使对网络符号路径的请求失败。但是,如果调试器试图向正在调试的进程发出某种IPC请求,并且IPC请求没有任何内置超时(大多数本地IPC机制没有),那么调试器会话将永远丢失。
这个问题的解决方法类似于我通常建议用户如何处理模块加载缓慢或符号服务器访问失败的问题,该问题是使用符号路径中引用的符号服务器的数据包路径中的程序解决的。具体来说,可以通过从具有空符号路径的调试器实例中创建进程转储,然后分离并打开具有完整符号路径的转储,并强制下载所有符号,来预缓存进程的所有符号。然后,使用符号路径在实时进程上启动调试会话,该符号路径仅引用符号下载到的本地下游存储,以防止发生任何危险的网络访问。
另一个让你陷入这种调试器死锁问题的常见方法是,当你调试一个已经将某些东西放入剪贴板的程序时,使用剪贴板粘贴到WinDbg中。这会导致类似的死锁,因为WinDbg可能会在对剪贴板所有者的DDE请求中被阻止,而剪贴板所有者将永远不会由于被调试而响应。在这种情况下,解决方法只是在将文本复制或粘贴到WinDbg中或从WinDbg中复制或粘贴文本时要小心。 - 使用-remote或.server进行的远程调试不稳定,或在一段时间后停止正常工作。如果会话中的所有调试器运行的调试器版本不同,则可能会发生这种情况。
确保远程调试方案中的所有对等方都使用(相同的)最新调试器版本。如果将调试器版本与-remote混合并匹配,在我的经验中,事情往往会以奇怪且难以诊断的方式出现(对于调试器远程处理协议的向后或向前兼容性,似乎没有太多得体的支持)。
另外,在Windows2000上,调试器包的几个最新版本在远程调试模式下根本不起作用。据我所知,这是在最新版本中修复的。
Windows中的线程命名杂谈
Windows允许您为进程中的线程指定名称,然后调试器可以显示这些名称。这是一个很好的解决方案,但这是一个很好的解决方案。Windows 10 Creators更新(SetThreadDescription)中添加了一个新的线程命名API。Chrome现在使用SetThreadDescription来命名它的线程(当这个函数可用时)。chromiumrepo还包含一个工具,可以使用GetThreadDescription转储进程的所有线程名称。xperf/WPA支持SetThreadDescription API——线程名称会显示在CPU使用率图和通用事件图中,这非常棒。VS 2017 15.6(更新6)和Windows应用商店版本的windbg现在支持线程名称,包括实时调试和小型转储!name列现在包含您给定的任何名称(请注意未命名的TppWorkerThread,因为Windows仍然落后)。已经被注入到线程池中,等待最早更新的线程是2019年6月。我们看看效果如何。其他需要命名的Microsoft线程,因为它们最终会出现在其他开发人员的进程中,包括:
- DbgUiRemoteBreakin
- CManagerImpl::_DelegateThreadProc
- CRpcThreadCache::RpcWorkerThreadEntry
目前,在windbg的进程和线程窗口中,这些线程看起来都是相同的,并且让完全不同的线程看起来无法区分是个坏主意。这也使得在Chrome浏览器进程中很难找到Chrome忘记命名的线程。
Orbit Profiler支持使用GetThreadDescription()获取线程名称–太棒了!
下面是WPA中的线程名称示例,它使这些事件在进程和线程之间的分布更易于查看:
Visual Studio中的线程名称也非常棒,尤其是现在即使在进程启动很久之后附加线程名称也会出现:
线程名非常棒,应该会让Chrome调试变得简单一点。
看看WPA中线程名有多有用!
所有线程命名问题的状态摘要–剩下的唯一任务是Microsoft需要开始命名其线程:
示例代码中的Const正确性:Fixed
/analyze示例代码中的警告:已修复
调试器中的竞争条件:在VS2015更新2中修复
操作系统线程命名功能:在Windows10 Creators更新中添加(文档称1607/周年纪念版,但这是不正确的)
线程命名函数的工具支持:xperf/WPA、visualstudio和windbg的存储版本都显示线程名称。“经典”版本的windbg通过非常明显的命令“dx-g”显示线程名称@$curprocess.线程“–这里讨论了更多的想法
Windows需要为自己的线程命名,尤其是它注入到其他进程中的线程,比如ntdll.dll!TppWorkerThread(应命名为ThreadPoolWorker)。这也适用于创建线程的第三方库、注入线程的图形驱动程序等等。查找SetThreadDescription并使用它(如果可用)。并选择描述性但简短的线程名称
线程的imageNaming无疑是有帮助的。它在调试时提供了额外的上下文,可用的信息越多越好。右边的屏幕截图显示了调试UIforETW时visualstudio中threads窗口中的name列。
然而,线程命名存在许多缺陷,主要是因为它只是一个调试器约定,而不是一个操作系统特性。
出现主要缺陷是因为Windows上的线程命名是通过引发异常来实现的。有一个约定,调试器应该注意异常代码0x406D1388–是的,这只是一个没有内在含义的任意幻数–并在相关的异常记录中查找幻数值。调试器必须执行以下操作:
打电话给WaitForDebugEvent。如果出现异常,并且异常代码为0x406D1388,并且参数的数目是正确的值,则重新解释异常信息。查看该结构,如果dwType等于4096,dwFlags为零,则使用ReadProcessMemory从调试对象的内存中获取线程名称。
哦,从哪里开始…
这一准则的特殊性是显而易见的。debuggee设置一组参数,然后使用RaiseException向调试器发出信号。如果调试器支持线程命名,那么它将处理这个特定的异常(ExceptionCode、NumberOfParameters、dwType和dwFlags的匹配值),然后从调试对象的内存中读取线程名称。无论调试器是否支持异常,调试对象都会处理异常并继续。这是IPC的一种粗略的方法。
这种技术的好处是它存在。如果这个特性被建议作为一个操作系统的特性,那么它可能会在设计审查或规划中被捆绑几十年。使用这种黑客技术意味着visualstudio调试器团队(MS_VC_异常代码背叛了他们的手)可以实现调试器端,记录如何在客户端调用它,让它立即在所有版本的Windows上运行,然后重新开始工作。Windbg很容易实现了相同的特性,一切都很好。
但是,这种权宜之计给我们留下了一些问题。
不是缺陷
当我第一次在tweet上发布这些问题时,我得到了一个回复,声称64位构建的结构定义是错误的。这个机制确实很脆弱,但并没有被打破。Win32调试API的一个限制是调试器和调试对象的位必须匹配—32位进程不能附加到64位进程。而且,只要调试器和debuggee具有相同的结构布局,那么它是什么并不重要。由于调试器和调试对象将使用相同的编译器和相同的结构定义,因此它们将具有相同的布局,这就是所需的全部内容。
不需要使用pragma pack指令来强制执行结构的任何特定打包,但需要它们来确保两边都具有相同的布局。
visualstudio是32位的,它可以调试64位进程,但它通过使用IPC与64位调试器代理进行通信来实现这一点。它使用msvsmon.exe,所以64位进程的调试基本上是本地远程调试。
小问题
我想先解决这些问题,尽管它们不是严重的问题。
示例代码已很长时间没有更新。因为它的threadName参数被声明为char*,所以不能用constchar数组调用它,如果使用/Zc:strictStrings(非常方便)构建它,甚至不能用string常量调用它。这个小错误现在已经被粘贴到数千个代码基中。我建议在代码中修复这个问题,但是使用剪切粘贴编码意味着这个糟糕的示例将永远存在。现在修好了!
示例代码触发两个/analyze警告。其中一个抱怨筛选器表达式是常量,另一个则抱怨\uuExcept块为空。analyze团队应该将SetThreadName文档视为这些构造通常不是bug的证据,文档团队应该添加必要的警告抑制杂注。仍然相关的示例代码应该在高警告级别下编译干净。现在修好了!
大问题
所有这些大问题都源于设计的一个后果:如果在引发异常时没有调试器在监听,那么线程名称将永远丢失。
如果调试程序在线程命名完成后附加,则调试器将不知道它错过了这些异常。无法要求调试人员再次引发异常。我想调试人员可以每隔几秒钟重命名一次线程,但这充其量是一种减少危害的策略,如果在进程崩溃时附加调试器,它仍然会完全失败。当前的策略意味着大多数Windows错误报告崩溃都没有线程名。
事实证明,调试器并不是唯一可以从了解线程名称中获益的工具。探查器,如Windows Performance Toolkit(xperf)将通过使用thread name列得到极大的增强,按线程名分组将是非常棒的。但是,作为调试器附加到被分析的进程是一个糟糕的主意,所以Windows性能分析器(WPA)无法获取这些信息。
另外,两个主要的Windows调试器都有一个可以避免的争用条件,这会导致SetThreadName频繁地静默失败。如果线程创建事件在线程命名异常之前到达,则这两个调试器似乎只命名线程。如果您创建了一个线程,然后立即从creator线程中命名它,那么在线程开始运行之前引发异常是非常容易的(尤其是在多核处理器上)!修复不应该很难——只需修复调试器,这样它就可以处理以任意顺序显示这两个事件。容易的。在这些调试器修复其竞争条件之前,每个命名其线程的应用程序都必须非常小心。
解决它
想出一个解决所有这些问题的办法是一个有趣的练习。我可以很容易地创建一个导出SetThreadName函数的DLL。然后,这个DLL将与另一个进程通信,该进程将维护一个内存中的数据库,该数据库将线程id映射到名称。为了避免客户机上的速度减慢,这将是一个选择加入进程,可能是通过让程序执行LoadLibrary/GetProcAddress舞蹈来实现的,以查看是否安装了DLL/进程对。这很简单,但是这个想法有两个问题,严重到足以让我不去烦。
我天真的方法无法判断线程何时死亡,这意味着线程数据库将迅速增长到数千个条目。这些线程中的大多数都是未使用的,而且许多线程表示重用已命名但现在已死线程的id的线程。添加unsethreadname函数可以减少混乱,但无法解决问题,尤其是在线程突然退出时。在用户模式下解决这个问题而不引入竞争条件将是一个挑战。
更大的问题是线程命名API需要广泛的支持。我可以说服成千上万的开发人员采用一种新的线程命名API,但是如果没有工具支持,这将是毫无意义的。我希望这个线程名数据库由windbg、visualstudio调试器和ETW跟踪查询。我没有这些工具的源代码,也找不到在哪里创建拉请求。
事实证明,驱动程序可以使用PsSetCreateThreadNotifyRoutine可靠地跟踪线程的创建和销毁,从而创建一个线程名称数据库。但是,这仍然缺少对工具的支持,所以微软已经实施了一个官方的解决方案。
在操作系统中添加这种类型的工具是有先例的。gflags工具允许开发人员跟踪堆分配、对象创建者类型跟踪等等。现在是微软添加线程名称作为gflags选项的时候了。内核可以捕捉现有的异常,可以更新调试器和探查器来查询内核的线程名称数据库,这样世界会变得更好。
我希望微软也能修复当前设置中的const正确性,/analyze警告和竞争条件。三个都修好了。
在那之前如果您正在编写调试器,请考虑添加一个更健壮的线程命名机制。也许会流行起来。您还应该支持现有的标准,并在调试器中修复其竞争条件。如果您正在编写一个程序,其中您想命名您的线程,那么您所能做的就是稍微改善一下情况。可以用两种方法之一避免线程命名竞争条件。最简单的解决方案是让线程自己命名。这保证线程创建事件在异常事件之前到达,因为子线程在开始运行之前不能调用SetThreadName。如果您想创建线程,然后从creator thread中命名它们,那么您必须等到线程完全启动。这可以通过等待线程发出一个事件的信号来完成,或者如果这不方便,那么就等“一会儿”然后交叉手指。
您还应该将thread name参数设为constchar*,并使用“#pragma warning(disable:63206322)”来抑制伪/analyze警告
在崩溃转储中查找所有可能的上下文记录
如果您调试了一段时间的崩溃转储,那么您可能遇到了这样的情况:调试器提供的初始转储上下文对应于在处理初始异常时发生的第二个异常,该异常可能更接近您正在调查的问题中的原始基础问题。
这可能很烦人,因为“.ecxr”命令将指向次要故障异常的位置,而不是原始异常上下文本身。然而,在大多数情况下,原始的、主要的异常上下文仍然在堆栈上;人们只需要知道如何找到它。
有两种方法可以解决这个问题:
- 对于硬件生成的异常(access violations),可以查找堆栈上的ntdll!KiUserExceptionDispatcher,它以PCONTEXT和PEXCEPTION_RECORD作为参数。
- 对于软件生成的异常(如C++异常),情况会变得更糟。你可以找在堆栈上调用ntdll!RtlDispatchException,然后从那里获取PCONTEXT参数。
编写脚本增强windbg堆栈、内存窗口
因为要频繁地使用windbg分析堆栈、参数、以及内存状态,但是windbg的界面跟OD、IDA差距是很大的,
对于漏洞分析或者其它用途来说,用它来分析堆栈、内存是很不方便的,需要多次手工输入命令。
为了简化分析工作,自己闲着蛋疼的时候写了两个脚本来增强windbg的这些功能。
准备:下载以下脚本krnldbg.rar,放置在windbg的安装目录。
首先说函数调用栈及参数dump脚本,当windbg命中断点后,用快捷键”ctrl+n”增加一个命令窗口,
输入命令 $$>a<advstk ,即可将栈、及调用栈中的函数参数列出来,蓝色带链接的证明这个内存地址是可访问的,
所以他极有可能是一个指针,所以做了一个内存“链接”,点击后可直接使用另一个脚本查看该处的内存。
注: 如果advstk执行出错,请换用脚本advstk1,因为我使用的方法很笨,一些细节没处理好.
接上,跳转到内存查看脚本后,会把这个内存地址进行不同的方式解析,
如db,dd,du,da,目的是减少命令的输入次数,并且还会进一步解析这块内存中储存的是否为指针,
如果内存可访问,会进一步再给其加上链接,以便于分析时进一步跟踪。
当然也可以直接在命令窗口中输入命令 $$>a<advmem 0x06FAEF80 查看0x06FAEF80处内存情况
接着下来的问题是怎样让这个命令窗口像真正像windbg的view“窗口”那样即时刷新呢?
答案是在命令窗口上右键,勾选“Auto refresh”,如果不出意外,他就带上了自动刷新的功能了,如下图:
转自https://bbs.pediy.com/thread-139816.htm
再谈System.BadImageFormatException
今天,当我们继续学习.NET异常处理系列时,我们将查看System.BadImageFormatException。System.BadImageFormatException与GIF或JPG无关,而是在.NET应用程序尝试加载与当前公共语言运行库(CLR)所需的正确格式不匹配的动态链接库(.dll)或可执行文件(.exe)时发生。
在本文中,我们将看到System.BadImageFormatException在.NET异常层次结构中的确切位置,并查看System.BadImageFormatException的一些潜在原因,让我们开始讨论它!
如前所述,System.BadImageFormatException发生在非常特殊的情况下:当.NET试图使用.dll或.exe时,即以某种方式与当前公共语言运行库不兼容。“不兼容的公共语言运行时”的定义可能有所不同,但通常这意味着.NET版本(1.1、2.0等)或各种编译程序集的CPU类型(32位与64位)不匹配。
最后,System.BadImageFormatExceptions表示版本控制不兼容。对于许多现代软件应用程序,的主要版本通常包括打破兼容性问题,防止与以前版本的某些方面向后兼容。.NET程序集(.dll或.exe)基本相同,尝试使用包含不兼容项的两种不同类型的程序集通常会生成System.BadImageFormatException。
为了说明这一点,我们将通过几个不同的例子。我已经包含了下面的完整代码示例以供参考,之后我们将更详细地探讨细节:
usingSystem;usingSystem.Reflection;usingUtility;namespaceAirbrake.BadImageFormatException
{classProgram
{static void Main(string[] args)
{
LoadingNonDotNetLibraryExample();
Logging.Log("-----------------");
DifferingCPUExample();
Logging.Log("-----------------");
OldDotNetExample();
}private static voidLoadingNonDotNetLibraryExample()
{try{//Generate path to notepad.exe. string filePath = Environment.ExpandEnvironmentVariables("%windir%") + @"\System32\notepad.exe";
Assembly assem=Assembly.LoadFile(filePath);
}catch(System.BadImageFormatException exception)
{
Logging.Log(exception);
}
}private static voidDifferingCPUExample()
{try{//Load Utility.dll, a 64-bit assembly. Assembly assem = Assembly.LoadFrom(@".\Utility.dll");
Logging.Log(assem.ToString());
}catch(System.BadImageFormatException exception)
{
Logging.Log(exception);
}
}private static voidOldDotNetExample()
{try{//Load Author-1.1.dll (compiled in .NET 1.1). Assembly assem = Assembly.LoadFrom(@".\Author-1.1.dll");
Logging.Log(assem.ToString());
}catch(System.BadImageFormatException exception)
{
Logging.Log(exception);
}
}
}
}usingSystem;usingSystem.Diagnostics;namespaceUtility
{/// <summary> ///Houses all logging methods for various debug outputs./// </summary> public static classLogging
{/// <summary> ///Outputs to<see cref="System.Diagnostics.Debug.WriteLine"/>if DEBUG mode is enabled,///otherwise uses standard<see cref="Console.WriteLine"/>./// </summary> /// <param name="value">Value to be output to log.</param> public static void Log(objectvalue)
{#if DEBUGDebug.WriteLine(value);#elseConsole.WriteLine(value);#endif}/// <summary> ///When<see cref="Exception"/>parameter is passed, modifies the output to indicate///if<see cref="Exception"/>was expected, based on passed in `expected` parameter./// <para>Outputs the full<see cref="Exception"/>type and message.</para> /// </summary> /// <param name="exception">The<see cref="Exception"/>to output.</param> /// <param name="expected">Boolean indicating if<see cref="Exception"/>was expected.</param> public static void Log(Exception exception, bool expected = true)
{string value = $"[{(expected ?"EXPECTED":"UNEXPECTED")}] {exception.ToString()}: {exception.Message}";#if DEBUGDebug.WriteLine(value);#elseConsole.WriteLine(value);#endif}
}
}