程序崩溃处理
有人说C语言是低级语言,这有一部分原因是因为应用程序的内存管理大部分需要由程序员来实现。虽然这种方法非常有用,但是也给程序员添加了很多的麻烦。
也有人说,C语言是相对较小且容易学习的语言,然而,只有不考虑标准C语言库的典型实现时,C语言才比较小,这个库相当庞大,很多程序员认为C语言是易用语言,那是因为他们还没有遇到指针。
一般而言,程序错误会导致下面两件事情的发生:
- 导致程序做一些程序员没有打算做的事情;
- 导致程序崩溃
相信很多调试过程序的兄弟都碰到过段错误即segmentation fault
,则合格主要原因是试图在未经允许的情况下访问一个内存单元。硬件会感知这件事并执行对操作系统的跳转。
堆区域
调用malloc函数分配的内存;
栈区域
用来动态分配数据的空间,函数调用的数据(包括参数、局部变量和返回地址)都存储在栈上。
查看程序在Linux上的精确内存布局情况
可以通过使用info proc mappings来详细查看该程序在Linux上的精确内存布局情况,例如:
此时我们还可以看到这个进程号为14455,所以我们还可以通过文件/proc/14455/maps来查看该信息。通过这些信息,我们有可能看到文本和数据区域,以及堆和栈。
分配页策略
操作系统不会将不完整的页分配给程序,例如,如果要运行的程序总共大约有10000字节,如果完全加载,会占用3个内存页(一个页占4096个字节),它不会仅占用2.5个页,因为页是虚拟内存系统能够操作的最小内存单元,这是调试时要着重了解的情况,这也导致了程序的一些错误内存访问不会触发段错误,换言之,在调试会话期间,没有引起段错误并不能直接说明代码是没有问题的。
页的角色细节
当程序执行时,它会连续访问程序中的各个区域,导致硬件按照以下几种情况所示处理页表:
- 每次程序使用全局变量时,需要具有对数据区域的读写访问权限;
- 每次程序访问局部变量时,程序会访问栈,需要对栈区域具有读写访问权限;
- 每次程序进入或离开函数时,对该栈进行一次或多次访问,需要对栈区具有读写访问权限;
- 每次程序访问通过调用malloc或者new创建的存储器时,都会发生堆访问,也需要读写访问权限;
- 程序执行的每个机器指令是从文本区域取出的,因此需要具有读和执行文件;
信号
这里需要注意的是,进程抛出的信号,实际上没有任何内容发送给进程。所发生的事情只不过是操作系统将信号记录到进程表中,以便下次进程接收信号时得到CPU上的时间片,执行恰当的信号处理程序。
自定义信号的复杂性
使用GDB/DDD/Eclipse调试时,自定义信号处理程序可能会使程序变得复杂,无论是直接使用还是通过DDD GUI,每当发出任何信号时,GDB都会停止进程,所以,有可能意味着GDB会因为与调试无关的工作而频繁的停止,此时可以使用handle命令告诉GDB在某些信号发生时不要停止。
总线错误的原因
- 访问不存在的物理地址;
- 在很多架构上,要求访问32位量的机器指令要求字对齐,而导致视图在奇数号地址上访问具有4字节的数的指针错误可能引起总线错误。
总线错误是处理器层的异常,导致在Unix系统上发出SIGBUS信号,默认情况下,SIGBUS会导致转储内存并终止。
核心文件
有些信号表示让某个进程继续是不妥当的,甚至是不可能的,在这些情况中,默认动作是提前终止进程,并编写一个名为核心文件core file的文件,俗称转储核心。
核心文件包含程序崩溃时对程序状态的详细描述:栈的内容、CPU寄存器的内容、程序的静态分配变量的值。
我们可以通过file命令来查看文件的详细信息。
为什么需要核心文件
- 只有在运行了一段长时间后才发生段错误,所以在调试器中无法重新创建该错误;
- 程序的行为取决于随机的环境事件,因此再次运行程序可能不会再现段错误;
- 当新手用户运行程序时发生的段错误,需要发送核心文件给开发人员。
重载功能
当GDB注意到重新编译了程序后,它会自动加载新的可执行文件,因此不需要退出和重启GDB。
调试设计的内容
- 确认原则;
- 使用核心文件进行崩溃进程的“死后”分析;
- 纠正、编译并重新运行程序后,甚至不需要退出GDB;
- Printf()风格调试的不足之处;
- 利用你的智慧,这是无可替代的;
- 如果你过去使用printf风格调试的,就会发现使用printf跟踪这些程序错误中的部分错误原来有多难,虽然在调试中使用printf诊断代码有一定的好处,但是作为一种通用目的的工具,它远远不足以用来跟踪实际代码中发生的大部分程序错误。