Linux x86平台下程序崩溃的调试方法及量化分析

时间:2022-08-19 11:48:39

Linux x86平台下程序崩溃的调试方法及量化分析

摘 要:Linux中的段错误是编程中经常遇到的问题,往往导致程序崩溃。本文对段错误产生的原因,结合程序运行的过程,对段错误进行量化分析。

关键词:段错误;量化分析;程序调试

中图分类号:TP316.81

在Linux系统上做过程序开发的人一定都遇到过“段错误”(Segmentation fault),随之程序异常退出。初学编程的人往往对此束手无策,不知道发生了什么事情,应该如何进行调试。其实Linux下的段错误和Windows平台上臭名昭著的“该程序执行了非法操作,即将被关闭”错误本质上是相同的,绝大部分都是对内存的非法访问而导致的。

很多新手程序员对于段错误往往无从下手,或是只能通过原始的方式,例如在程序中添加许多的printf语句来跟踪程序的执行。这样往往效率低下,因此掌握一些调试技巧对于提高调试效率而言是十分重要的,使用正确的调试工具和方法往往能够事半功倍,帮助准确定位程序出错的地方,从而找到引发该错误的根本原因(root cause)。

1 段错误产生的原因

在Linux下程序崩溃基本上都是由于内存非法访问造成的,当内存非法访问发生时,CPU会产生一个软中断信号,如SIGSEGV,而该软中断信号的默认处理就是程序退出并产生一个core dump文件,该文件保存了程序崩溃时的现场,包括CPU寄存器的值,内存栈和堆里的数据。这些数据加上程序的二进制文件(即编译后的可执行文件)和程序源代码就是我们进行分析的基础。

2 程序的运行过程

在调试程序之前我们需要了解一下我们的程序是怎么执行的。我们写的C源码经过编译链接后生成机器代码,也就是汇编指令组成的可执行文件,在Linux中是ELF(Executable and Linkable Format)格式的可执行文件。汇编指令对内存和寄存器进行操作。而在X86所有的寄存器中,EAX,EBP,ESP,EIP是几个最重要的寄存器。

EAX:通用寄存器,并用于保存函数返回值。被调函数返回时将返回值放入EAX,调用者从EAX中获取返回值。

ESP:栈顶寄存器,指向工作栈的栈顶。每当进入一个函数时,会通过修改ESP在栈中开辟一块空间供本函数使用。当退出一个函数时,ESP会恢复原值。

EBP:栈底寄存器,指向当前函数的栈底。每当进入一个函数时,该函数会将原来的(即调用它的函数的)EBP保存在栈中,然后将原来的ESP作为新的EBP,即EBP指向当前函数的栈底。

EIP:当前正在执行的汇编指令的地址。

函数的进入和退出都对应着对程序工作栈的修改,需要特别注意的是在X86中,栈是往低地址方向增长。所以进入一个函数分配栈空间是对ESP进行减操作(sub),而退出一个函数时是进行加(add)操作。每个函数在栈上都有自己一块空间,称为该函数的栈帧(stack frame)。如果函数f1()调用了f2(),目前正在执行函数f2()中的代码,那么工作栈将会有如图1的布局:

图1

表中的内存位置的写法是x86的基址寻址的表达方式(采用GDB使用的AT&T格式),例如-4(%esp)代表的是地址为ESP寄存器的值减去4的内存单元的值。

3 实例分析

我们来看一个经过简化的例子。我们有一个程序执行时出现崩溃,产生了core dump文件。用gdb调试工具打开coredump文件可以看到如图2输出:

图2

可以看出该程序发生了段错误,收到了一个SIGSEGV。同时GDB还指出了出错的指令位于f2()函数的0x08048426地址。我们通过disassemble命令查看f2()的汇编代码如图3:

图3

可以看到0x08048426的指令是mov(%eax),%edx,其含义是将EAX寄存器当作指针使用,将其所指向的内存的内容取到EDX中。这句指令出错意味着EAX寄存器中存放的是非法的内存地址,该地址不可读。我们可以通过info registers命令来查看EAX以及其它寄存器的值(部分)如图4:

图4

结果显示EAX的值是0,即空指针NULL,显然该地址是不可访问的,所以CPU产生了一个软中断信号SIGSEGV。由此我们从汇编代码的层次找到了程序崩溃的直接原因,但这还不够,我们需要继续分析为什么EAX寄存器是0。我们顺藤摸瓜,查看EAX的值是从何而来。我们继续查看f2()的汇编代码可以发现上一条指令0x08048423即mov0x8(%ebp),%eax这条指令给EAX寄存器赋了值。我们知道mov是一条赋值指令,0x8(%ebp)我们已经讲到,是f1()传递给f2()的第1个参数,由此可以知道f2()的第一个参数的值为0,即p为空指针NULL,因此此处程序崩溃的原因是传递给f2()的参数为空指针,而f2()在使用前未对其进行检查导致程序崩溃。

4 其它可能导致段错误的情形

上面例子是由于访问非法指针引起的段错误,是在编程中,特别是初学者常犯的一种错误。除了非法指针外还有一些其他类型的段错误,比如:(1)写局部变量数组时越界。由于局部变量数组是在栈上的,越界意味着覆盖栈的其他部分,导致程序无法继续执行;(2)栈溢出。程序的栈的空间是有限的,如果函数嵌套层次太多,例如递归调用层数过多,每次调用都会分配一块栈空间,导致栈溢出;(3)修改内存只读区的内容,双引号中的字符串,例如”abcd”是存放在只读区中的,如果你尝试通过指针去修改字符串的内容就会导致段错误。

5 结束语

本文介绍的调试方法虽然是基于Linux和x86的,但其思想同样适用于其他操作系统和硬件平台。另外,掌握程序的调试技巧固然十分重要,但更重要的提高自身的编程水平和养成良好的编程习惯,这样才能写出高质量的程序。毕竟程序调试是一种逆向工程,引入一个bug十分容易,而找到它往往需要付出很大的时间和精力的成本。

参考文献:

[1]The Santa Cruz Operation.Inc.System V Application Binary Interface Intel386 Architecture Processor Supplement Fourth Edition[M],1997.

[2]Randal E.Bryant.David R.O’puter Systems A Programmer’s Perspective,Pittsburgh[M],2001.

作者简介:徐伶伶(1981.09-),女,江苏太仓人,研究生,讲师,计算机应用技术;赵静女(1981-),山东青岛人,研究生,讲师,计算机应用技术。

作者单位:青岛工学院,山东青岛 266300

基金项目: “基于本体的教育信息化共享平台研制”(项目编号:2012KY009)。

上一篇:基于PHP和MySQL的小型应用设计 下一篇:车载电子海拔高度系统设计