目录
1、Windbg动态调试
2、在Windbg中设置断点
2.1、在函数入口处设置断点
2.2、在函数内部某一行上设置断点
3、设置断点跟踪对打开远程调试开关接口的调用
3.1、编写演示代码
3.2、在Windbg中设置调用SetRemoteDebugOn接口的断点进行跟踪
4、最后
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html 最近在测试时发现,我们的软件在启动起来后会自动打开远程调试开关,而基于安全考虑,打开远程调试开关需要用户自己在设置中手动操作,不能自动打开。因为我们的软件模块有上百个dll库,涉及到多个开发组,无法确定到底是哪个模块自动打开的,后来想到可以使用Windbg动态调试设置断点去快速定位问题。本文详细讲述这一问题的排查过程。
一般在排查C++软件异常问题时,如果有包含异常上下文的dump文件,我们优先使用Windbg打开dump文件去进行静态分析。如果发生异常时没有生成dump文件,则需要将Windbg附加到目标进程上(或者使用Windbg启动目标程序)进行动态调试。
Windbg主要用来分析C++软件异常,除此之外,还可以在动态调试时帮我们辅助定位一些问题。我们可以在动态调试的Windbg中设置断点(设置代码段的地址),去对程序的运行轨迹进行跟踪。本案例中,正是通过在Windbg中设置断点定位问题的。
使用Windbg进行动态调试有两种方式:
1)直接将Windbg附加到已经运行起来的目标进程上。点击菜单栏中的File->Attach to a Process...。
2)可以直接使用Windbg启动目标程序。点击菜单栏中的File->Open Executabe...。如果问题出在程序启动的过程中,则使用此种方式。
本项目问题中,自动打开远程调试开关的操作应该是在程序启动时执行的,所以我们要用Windbg去启动程序的方式。
可以使用bp命令设置断点,可以将断点设置在函数入口处,也可以在函数内部设置断点。在函数内部某一行上设置断点,命中断点后可以查看到中断时函数中局部变量的值,变量的值可能是排查问题的重要线索。
在函数入口处设置断点,直接使用函数名就可以了,函数名就是函数在代码段的首地址。以netdll.dll库中的SetRemoteDebugOn函数为例,设置断点的命令为:
bp netdll!SetRemoteDebugOn
其中netdll就是所在dll库名称(不带.dll后缀),SetRemoteDebugOn就是函数名。
此处的SetRemoteDebugOn函数是netdll.dll库的导出函数,函数的符号是对外公开的,无需pdb符号库文件。如果要设置的函数是dll库内部的函数,非dll库的导出函数,则需要将库的pdb文件设置到Windbg中,因为内部函数需要pdb文件中的函数符号,否则Windbg没法识别。
此处说的函数内部的某一行,不是C++源码的某一行,而是二进制文件中汇编代码(二进制机器码)的某一行。因为程序最终运行的是二进制文件,执行的是二进制文件中的二进制代码(与汇编代码等价的,汇编代码是二进制代码的助记符)。
其实就是在所在函数(函数就是函数在代码段的首地址)基础上加上一个offset偏移值。但这个偏移值不是随意写的,需要使用IDA打开二进制文件查看汇编代码去确定。因为不同的汇编指令,其长度也是不一样的,只能设置汇编指令的地址,不能设置两条汇编指令地址中间的地址值,否则会无效,不会命中断点。
以开源库libcurl.dll为例,我们使用IDA Pro打开该库文件查看反汇编出来的汇编代码,找到该库的内部函数easy_perform,在该函数中的10007D2D行设置断点,如图所示:
要在IDA中看到库内部函数的符号,需要将pdb文件拿过来,放到libcurl.dll同级目录中,IDA回去自动加载。关于如何使用IDA反汇编工具,可以查看我之前写的文章:
IDA反汇编工具使用详解https://blog.csdn.net/chenlycly/article/details/120635120 要在指定的10007D2D行设置断点,需要计算这行汇编指令相对所在函数的偏移:
0x10007D2D - 0x10007D10 = 0x1D
所以设置断点的WIndbg命令为:
bp libcurl!easy_perform+0x1D
从下图也可以看出不同的汇编指令占的代码段内存的长度是不一样的:
注意,这个地方讲到的地址,都是代码段的地址,是二进制文件二进制代码(汇编代码)的地址。代码段的地址,要和数据段地址区分开来,变量占用的内存是数据段内存。
关于C++程序的内存分区,可以查看之前的文章:
实例详解C++程序的五大内存分区https://blog.csdn.net/chenlycly/article/details/120958761 此外,我们要说一下了解汇编代码的重要性,从此处我们也能看出一点端倪。熟悉汇编代码,不仅可以辅助排查C++程序问题,还可以理解高级语言无法理解的编程细节或代码执行细节的问题。如果要了解排查C++软件问题所需要掌握的汇编基础知识,可以查看我之前写的文章:
分析C++软件异常需要掌握的汇编知识汇总https://blog.csdn.net/chenlycly/article/details/124758670
为了跟踪是哪个模块自动打开远程调试开关,只要拿来底层库用打开远程调试的函数名称及所在的模块名,就可以在动态调试的Windbg中设置断点进行跟踪了。
此处不便展示项目中的相关模块和接口,也为了方便日后的视频课程的讲解,我特意写了一些测试代码,来讲述整个问题的跟踪过程。
使用Visual Studio创建了一个MFC主程序TestDlg.exe,以及一个包含打开远程调试接口SetRemoteDebugOn的netdll.dll动态库。在TestDlg.exe主程序的Test按钮的响应函数中调用netdll.dll库中的用于打开远程调试的API接口SetRemoteDebugOn。
动态库netdll.dll中API接口SetRemoteDebugOn定义如下:
主程序TestDlg.exe中的Test按钮的响应函数调用SetRemoteDebugOn的代码如下:
使用Windbg启动TestDlg.exe,使用Windbg动态调试。因为最终调用的是netdll.dll模块中的SetRemoteDebugOn接口去打开远程调试开关,所以只要对SetRemoteDebugOn接口入口处设置断点就可以了,即:bp netdll!SetRemoteDebugOn,如下所示:
我们使用bl命令查看当前设置的断点列表,如上所示。
然后点击TestDlg.exe程序窗口中的Test按钮:
在按钮的响应函数中调用SetRemoteDebugOn接口,这样就命中了刚才设置的断点,Windbg中断下来,使用kn命令查看此时的函数调用堆栈,我们就知道是哪个模块调用了SetRemoteDebugOn接口将远程调试关闭了。
查看命中断点时的函数调用堆栈如下所示:
从上图可以看出,TestDlg.exe模块调用了打开远程调试开关的接口,并且还能看到是TestDlg.exe模块中的CTestDlgDlg::OnBnClickedTest函数调用的。如果有pdb,还能看到具体是函数中的哪一行代码调用的!
本例中通过在Windbg中设置断点,快速地定位出打开远程调试开关的模块及函数信息。Windbg动态调试功能确实很有用,希望本文能给大家带来一定的启示和参考。