更新

2024-07-05 本书已经出版 image 由于本库的草稿是我之前一个人写的,所以质量和正确性都不如经过两位作者和出版社编辑审阅和校正过的书稿。 如果你想阅读更加完善的版本,推荐购买正版书籍。

事后的分析

不仅正在运行的进程,一个debugee可以是一个core dump文件,通常在进程crash的时候由系统例行生成。系统也提供了API给应用程序或者工具来不杀掉进程的同时生成进程的core dump文件。这对调查像不间断服务器程序的性能或者很难访问的远程程序的线下分析是合适的。

一个core dump文件基本就是一个进程在生成core文件那一刻的内存镜像的快照。它可以被一个调试器就像活着的进程查看。比如,我们可以检查内存内容,列出一个线程的调用栈,打印变量等等。但是,一个core dump简单的是一个静态文件,它同活着的进程在内核里面对应的运行上下文有基本的区别。

结果是,进程不能安排到任何一个CPU和用户不能执行任何在core dump文件里面的代码。进程的状态只能够查看根本不能被改变。因此,我们不能够调用需要调用函数如类的operator方法的任何函数或者打印任何表达式。一个经常让某些人吃惊的例子是调试器拒绝打印一个表达式简单得像array[2],其中array是STL向量。

这是因为调试器必须调用std::vectoroperator []方法来计算表达式。同样的理由,打断点和单步运行代码在事后的分析也是不可能。

一个core dump文件有一个标志位表明它是为什么生成的,这也是你最先想知道的事情(我们将在第六章讨论更多core dump文件结构体的细节)。一些常见的原因是:

  1. 段错误,内存访问越界或者数据内存保护陷入。它表示程序在试图访问一个不属于分配给它的地址空间的内存地址或者内存被保护免于特定的操作(读、写或者是运行)。正在运行的指令试图从这个地址读取或者向这个地址写入,因而被硬件异常捕获。比如,一个野指针指向一块释放后的内存块和一个野指针指向一块随机地址;它可能会在使用任意一个地址访问内存的时候导致段错误。

  2. 总线错误。这个错误通常因为访问未对齐的数据导致。比如,从一个奇地址读取一个整形。一些架构允许这样的行为,带来潜在的性能消耗(x86)而另外一些(SPARC)则会让程序crash带着总线错误异常。

  3. 非法地址。这个异常抛出时是当一个程序的下一个运行指令不属于CPU的指令集。比如,一个函数持有一个指向堆段而不是代码段的不正确的地址。

  4. 没有处理的异常。C++程序会抛出异常且没有代码来捕获它。C++运行库有一个默认的处理函数来捕获异常,它做的是简单地生成一个core dump文件让后停止掉程序。

  5. 浮点数异常。除以0、太大或者太小的浮点数可能会导致这个错误。

一个事后分析的常见问题是core dump文件是不完全的或者截断的,我们只可以看到部分debugee的内存镜像。 因为一些重要数据对象不能访问,这通常会阻碍我们获取到造成crash的结论。比如调试器不能显示一些堆上的相关数据对象或者一个线程的调用栈,源于跟它们关联的内存没有被保存到core dump文件里面。

完全的core dump文件的大小约等于文件在产生时进程的大小除去加载的可执行二进制。一个core dump文件被截断有各种原因。比如默认的系统设置仅仅允许部分core dump;系统管理员可能只设置core dump文件大小到一个比较小的值避免过量的磁盘使用;仅仅是存core dump的设备没有足够多的磁盘空间。 如果任何上面的条件没有被满足,系统需要选择哪一部分内存镜像需要保存,丢弃剩下的。

core dump文件不包含任何二进制代码。相反它仅仅记录名字,大小,路径和加载地址和其他可执行文件或者库的信息。由于core dump文件经常用于线下分析,有时是另外一台机器上,这些二进制可能缺失或者安装了在不同的位置。用户需要负责设置正确的二进制和向调试器告知它们。如果根据用户提供的搜索路径,找到了不正确的二进制,调试往往打印出警告信息但是不会停止。结果可能有误导。这跟前面章节讨论的符号匹配是一样的。