目录
1、概述
2、汇编指令能最直接反映异常崩溃的原因
2.1、查看异常Code码及对应的异常类型
2.2、查看发生崩溃的那条汇编指令
3、阅读汇编代码上下文需要掌握一定的基础汇编知识
4、Windbg中显示的函数调用堆栈中的C++代码行号,和最新的代码对不上了
5、Windbg中指示的发生崩溃的C++代码行上有多个函数调用,很难判断哪个函数调用出问题了
6、最后
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931 在分析C++软件异常崩溃时,可能需要使用IDA工具去查看exe或dll二进制文件的汇编代码去辅助定位问题。今天我们就来讨论一下使用IDA工具去查看汇编代码相关细节问题。
1、概述
一般我们会在C++软件中内置异常捕获模块(比如Google开源的CrashReport异常捕获库),当软件发生异常崩溃时,异常捕获就会感知到,将异常发生时的上下文信息保存到dump文件中。事后我们取来dump文件,用Windbg打开,使用.ecxr命令切换到发生遗产的上下文,然后使用kn、kp或kv命令查看发生异常时的函数调用堆栈。根据函数调用堆栈中的代码行号,去查看C++源代码,就能很快定位问题了。
一般异常崩溃是发生在某个线程中,导致所在进程发生崩溃,只要切换到这个出问题的线程中查看发生异常时的函数调用堆栈即可。
在查看出问题的C++源码上下问去确定发生问题的原因时,我们甚至需要到Windbg查看函数调用堆栈中某个函数中的局部变量或所在C++类对象中成员变量的值,去辅助分析问题。项目中我们多次使用到该方法去快速定位问题,之前也写了几篇这方面的分析实例,可以参见下列文章:
通过查看Windbg中的变量值去定位C++软件异常问题https://blog.csdn.net/chenlycly/article/details/125731044通过查看windbg中变量值去定位C++软件异常的又一典型案例分享https://blog.csdn.net/chenlycly/article/details/125793532
但在部分场景下仅使用Windbg分析还不够,还需要使用IDA工具去查看发生异常的模块的汇编代码上下文,将C++源码与汇编代码结合着看,去找出引发问题的原因。
2、汇编指令能最直接反映异常崩溃的原因
C++软件发生崩溃,最终是崩溃在某个模块的某条汇编指令上,汇编指令会最直接最本真的反映出为啥会崩溃,所以我们在用Windbg分析异常时首先需要去看一下发生崩溃的那条汇编指令及发生崩溃的异常Code值,有时通过这两点信息可以初步估计出发生异常的原因。
2.1、查看异常Code码及对应的异常类型
查看异常Code码及对应的异常类型,有时能助我们快速地定位问题。用Windbg打开dump文件时就能看到异常Code码及对应的异常类型,比如下图:
异常Code码是0xc00000fd,该错误码的含义是Stack overflow,即线程栈溢出,即问题线程当前的函数调用堆栈中的所有函数占用的栈空间超过了给线程分配的栈空间上限。
线程的栈空间是有上限的,函数中的局部变量是在栈上分配内存的,主调函数传递给被调函数的参数也是通过栈传递的(将参数值push到栈上)。用户创建线程时,系统会给每个线程分配指定大小的栈空间,Windows系统中默认分配1MB栈空间,Linux系统中默认会分配2MB的栈空间。
看到上述异常类型,我们就能明确了,是某个线程发生线程栈溢出了,结合引发线程栈溢出的常见原因:
1)函数递归调用的深度过深
因为一直在递归调用,在到达最底下的那层调用之前,递归函数一直没返回,栈空间一直没有释放,导致当前线程占用的栈空间越来越多,达到上限。
2)消息上触发函数的死循环调用
消息触发的函数死循环调用,因为死循环调用了,函数的栈空间一直没释放,导致当前线程占用的栈空间越来越多。这个问题我们在实际项目中遇到过两次。
3)定义了一个占用内存很大的局部变量
比如定义了一个很庞大的结构体,在一个函数中用该结构体定义了一个局部变量,假设该结构体接近或者大于1MB,则会直接导致线程栈溢出。
4)函数中使用switch...case语句,包含了大量的case分支
每个case分支中都定义了局部变量,导致当前函数占用了大量的栈空间。case分支中的局部变量的生命周期是在case分支中的,即代码运行到对应的case分支中时该分支中的局部变量才有“生命”,但其实这个局部变量的栈空间已经在函数入口处分配好栈空间了,并不是代码执行到case子句中才分配栈空间的。这点可以通过编写测试代码,查看函数入口处给当前函数分配栈空间的汇编代码就能看出来了,可以先顶一个变量查看汇编代码看看分配了多少栈空间,然后再增加一个变量,看看分配的栈空间是否变大。
然后使用kn、kp或kv命令查看发生异常的线程的函数调用堆栈,看看函数调用堆栈中都调用了哪些函数,去做进一步分析,很快就能确定问题了。
2.2、查看发生崩溃的那条汇编指令
分析异常时,除了先看异常Code码及其对应的异常类型,我们还要去看发生崩溃的那条汇编指令。通过查看发生异常崩溃的汇编指令,我们可能得到一些线索,初步估计出可能是什么原因引发的。
Windbg打开dump文件后,不需要执行任何Windbg命令,会首先看到发生异常的异常Code码机器对应的异常类型,那如何去查看发生异常的那条汇编指令呢?其实很简单,只要输入.ecxr命令,执行该命令切换到发生异常的上下文线程中,并将发生异常的那条汇编指令及当时的各个寄存器的值,如下所示:
上图中发生异常的汇编指令中访问了地址为0x00000000的内存地址,在Windows系统中,小于64KB地址值的内存是禁止访问的,所以触发了内存访问违例,导致该条汇编指令发生崩溃。
该条发生异常的汇编指令中,是去访问以ecx寄存器中的内容为地址的内存的,查看此时的ecx寄存器的值为0,而在X86汇编中ecx可以用来传递C++对象首地址的,所以我们初步怀疑可能是空指针引发的问题。然后,再去查看发生异常时的线程的函数调用堆栈及对应的C++源码去进一步分析。
3、阅读汇编代码上下文需要掌握一定的基础汇编知识
要去阅读汇编代码的上下文,是需要掌握一定的汇编基础知识的,比如了解一些常用寄存器的用途、熟悉一些常用的汇编指令、了解函数调用时的栈分布、了解C++虚函数调用的汇编代码实现(虚函数调用时的二次寻址)等。
这里简单的提一下常用寄存器的用途:
在X86汇编指令中,EAX主要用于存放函数调用的返回值;在调用C++成员函数时会使用ECX寄存器用来传递C++对象地址;ESI是源地址寄存器,EDI是目的地址寄存器,主要用于内存拷贝的串操作指令中,比如memcpy的汇编实现中。
关于分析C++软件异常需要掌握的基础汇编知识,这里就不再赘述了,可以参见我之前写的文章:
分析C++软件异常需要掌握的汇编知识汇总https://blog.csdn.net/chenlycly/article/details/124758670
4、Windbg中显示的函数调用堆栈中的C++代码行号,和最新的代码对不上了
在有pdb文件的情况下,要将pdb文件的路径设置到Windbg中。Windbg在加载到pdb文件后,在函数调用堆栈中不仅能显示具体的函数名称,还能显示函数中对应代码的行号,效果如下:
TestDlg!CTestDlgDlg::OnBnClickedButton1+0x67 [d:\vs2010projects\testdlg\testdlg\testdlgdlg.cpp @ 489]
函数CTestDlgDlg::OnBnClickedButton1位于TestDlg.exe模块中,对应的testdlgdlg.cpp源文件的行号为489,于是打开testdlgdlg.cpp文件,找到该文件的489行,如下所示:
这是我之前故意写的一段引发异常的测试代码,代码中定义了一个结构体指针变量pInfo,初始化为空(NULL),然后没有给该指针赋一个有效的结构体地址,直接用这个空指针去访问结构体中的成员cbSize,所以访问了一个地址很小的内存,所以触发了内存访问违例。
上面的是一个完整的简单示例,但在实际项目中,发生异常崩溃的软件版本可能是几个月或者几年之前的,Windbg中显示的行号是很早之前的cpp文件代码了,最新的cpp文件代码相对这个出问题的版本做了很多修改,所以行号和最新的代码完全对不上了。
这时候就需要使用IDA去查看发生异常的模块的汇编代码上下文了,看看到底是那一行代码引起的,一般还是要和最新的代码对比着看,看看最新的代码中哪一行代码。接着上面的示例,我们用IDA打开TestDlg.exe二进制文件,查看出问题的函数CTestDlgDlg::OnBnClickedButton1的汇编代码,如下所示:
这个地方要注意一下,IDA也要用到pdb符号库文件,将pdb文件放置在目标二进制文件的同级目录中,IDA会到目标二进制文件所在的目录中搜索其需要的pdb文件。有了pdb文件中符号,IDA打开的汇编代码中就会显示具体函数名和变量标识,以及大量注释信息。
没有注释信息,直接去阅读汇编代码,是很难读懂的,除非你很强的汇编功底。在实际工作中,是将汇编代码、注释及C++源码结合起来看的,这比单纯地阅读汇编代码要简单很多的!
对于如何使用IDA查看汇编代码上下文,这个地方就不展开了,后面会专门写一篇文章进行介绍。关于IDA工具的介绍,可以参看之前写的文章:
IDA反汇编工具使用详解https://blog.csdn.net/chenlycly/article/details/120635120
5、Windbg中指示的发生崩溃的C++代码行上有多个函数调用,很难判断哪个函数调用出问题了
Windbg中指示的发生崩溃的C++代码行上有多个函数调用(比如if语句中有多个条件的组合判断),很难直接判断是哪个函数调用出问题了,可以查看汇编代码去确定到底是哪个函数调用出的问题,比如如下的if条件判断语句:
if (pContainer->IsVisible() && GetTargetImplPtr()->IsReady() && pDataProcImpl->IsBuildFinish)
{
// 代码省略
}
有人可能会问,既然是if语句的条件中有多个函数调用,是不是应该代码运行到函数内部去了,为啥问题出在函数调用处呢?可能是调用类成员函数时使用的类对象指针有问题,有可能调用的是虚函数,在真正去调用虚函数之前,要到虚函数表中找到虚函数表中的函数地址,即需要进行虚函数表的二次寻址,崩溃可能就发何时能在虚函数的二次寻址的过程中。
如果调用的不是虚函数,则不会崩溃在函数调用处,如果有崩溃,则肯定是崩溃在成员函数内部,因为成员函数内部要通过传入的类对象地址去访问类对象的数据成员,因为类对象地址有问题了(比如类对象地址为NULL,或者是一个已经释放的类对象地址),则通过该类对象访问其数据成员时则会产生异常。
对于一行上包含多个C++代码调用,无法确定到底是哪个部分出的问题,此时就需要结合汇编代码,查看汇编代码的上下文去分析,去确定发生崩溃的代码在哪一个部分了。
6、最后
IDA是业界最强大的静态反汇编工具,功能非常强大,我们在分析C++软件异常问题时,只是简单地使用IDA工具,即用IDA打开二进制文件查看文件中的汇编代码,以辅助分析问题。本文并没有详细讲述IDA工具的功能,感兴趣的朋友,可以去阅读一下IDA经典书籍《IDA Pro权威指南》。