本文是一系列探究调试器工做原理的文章的第一篇。我还不肯定这个系列须要包括多少篇文章以及它们所涵盖的主题,但我打算从基础知识开始提及。html
关于本文linux
我打算在这篇文章中介绍关于Linux下的调试器实现的主要组成部分——ptrace系统调用。本文中出现的代码都在32位的Ubuntu系统上开发。请注意,这里出现的代码是同平台紧密相关的,但移植到别的平台上应该不会太难。ios
动机c++
要想理解咱们究竟要作什么,试着想象一下调试器是如何工做的。调试器能够启动某些进程,而后对其进行调试,或者将本身自己关联到一个已存在的进程之上。它能够单步运行代码,设置断点而后运行程序,检查变量的值以及跟踪调用栈。许多调试器已经拥有了一些高级特性,好比执行表达式并在被调试进程的地址空间中调用函数,甚至能够直接修改进程的代码并观察修改后的程序行为。git
尽管现代的调试器都是复杂的大型程序,但使人惊讶的是构建调试器的基础确是如此的简单。调试器只用到了几个由操做系统以及编译器/连接器提供的基础服务,剩下的仅仅就是简单的编程问题了。(可查阅维基百科中关于这个词条的解释,做者是在反讽)程序员
Linux下的调试——ptracegithub
Linux下调试器拥有一个瑞士军刀般的工具,这就是ptrace系统调用。这是一个功能众多且至关复杂的工具,能容许一个进程控制另外一个进程的运行,并且能够监视和渗入到进程内部。ptrace自己须要一本中等篇幅的书才能对其进行完整的解释,这就是为何我只打算经过例子把重点放在它的实际用途上。让咱们继续深刻探寻。web
遍历进程的代码redis
我如今要写一个在“跟踪”模式下运行的进程的例子,这里咱们要单步遍历这个进程的代码——由CPU所执行的机器码(汇编指令)。我会在这里给出例子代码,解释每一个部分,本文结尾处你能够经过连接下载一份完整的C程序文件,能够自行编译执行并研究。从高层设计来讲,咱们要写一个程序,它产生一个子进程用来执行一个用户指定的命令,而父进程跟踪这个子进程。首先,main函数是这样的:shell
代码至关简单,咱们经过fork产生一个新的子进程。随后的if语句块处理子进程(这里称为“目标进程”),而else if语句块处理父进程(这里称为“调试器”)。下面是目标进程:
这部分最有意思的地方在ptrace调用。ptrace的原型是(在sys/ptrace.h):
第一个参数是request,能够是预约义的以PTRACE_打头的常量值。第二个参数指定了进程id,第三以及第四个参数是地址和指向数据的指针,用来对内存作操做。上面代码段中的ptrace调用使用了PTRACE_TRACEME请求,这表示这个子进程要求操做系统内核容许它的父进程对其跟踪。这个请求在man手册中解释的很是清楚:
“代表这个进程由它的父进程来跟踪。任何发给这个进程的信号(除了SIGKILL)将致使该进程中止运行,而它的父进程会经过wait()得到通知。另外,该进程以后全部对exec()的调用都将使操做系统产生一个SIGTRAP信号发送给它,这让父进程有机会在新程序开始执行以前得到对子进程的控制权。若是不但愿由父进程来跟踪的话,那就不该该使用这个请求。(pid、addr、data被忽略)”
我已经把这个例子中咱们感兴趣的地方高亮显示了。注意,run_target在ptrace调用以后紧接着作的是经过execl来调用咱们指定的程序。这里就会像咱们高亮显示的部分所解释的那样,操做系统内核会在子进程开始执行execl中指定的程序以前中止该进程,并发送一个信号给父进程。
所以,是时候看看父进程须要作些什么了:
经过上面的代码咱们能够回顾一下,一旦子进程开始执行exec调用,它就会中止而后接收到一个SIGTRAP信号。父进程经过第一个wait调用正在等待这个事件发生。一旦子进程中止(若是子进程因为发送的信号而中止运行,WIFSTOPPED就返回true),父进程就去检查这个事件。
父进程接下来要作的是本文中最有意思的地方。父进程经过PTRACE_SINGLESTEP以及子进程的id号来调用ptrace。这么作是告诉操做系统——请从新启动子进程,但当子进程执行了下一条指令后再将其中止。而后父进程再次等待子进程的中止,整个循环继续得以执行。当从wait中获得的不是关于子进程中止的信号时,循环结束。在正常运行这个跟踪程序时,会获得子进程正常退出(WIFEXITED会返回true)的信号。
icounter会统计子进程执行的指令数量。所以咱们这个简单的例子实际上仍是作了点有用的事情——经过在命令行上指定一个程序名,咱们的例子会执行这个指定的程序,而后统计出从开始到结束该程序执行过的CPU指令总数。让咱们看看实际运行的状况。
实际测试
我编译了下面这个简单的程序,而后在咱们的跟踪程序下执行:
令我惊讶的是,咱们的跟踪程序运行了很长的时间而后报告显示一共有超过100000条指令获得了执行。仅仅只是一个简单的printf调用,为何会这样?答案很是有意思。默认状况下,Linux中的gcc编译器会动态连接到C运行时库。这意味着任何程序在运行时首先要作的事情是加载动态库。这须要不少代码实现——记住,咱们这个简单的跟踪程序会针对每一条被执行的指令计数,不只仅是main函数,而是整个进程。
所以,当我采用-static标志静态连接这个测试程序时(注意到可执行文件所以增长了500KB的大小,由于它静态连接了C运行时库),咱们的跟踪程序报告显示只有7000条左右的指令被执行了。这仍是很是多,但若是你了解到libc的初始化工做仍然先于main的执行,而清理工做会在main以后执行,那么这就彻底说得通了。并且,printf也是一个复杂的函数。
咱们仍是不知足于此,我但愿能看到一些可检测的东西,例如我能够从总体上看到每一条须要被执行的指令是什么。这一点咱们能够经过汇编代码来获得。所以我把这个“Hello,world”程序汇编(gcc -S)为以下的汇编码:
这就足够了。如今跟踪程序会报告有7条指令获得了执行,我能够很容易地从汇编代码来验证这一点。
深刻指令流
汇编码程序得以让我为你们介绍ptrace的另外一个强大的功能——详细检查被跟踪进程的状态。下面是run_debugger函数的另外一个版本:
同前个版本相比,惟一的不一样之处在于while循环的开始几行。这里有两个新的ptrace调用。第一个读取进程的寄存器值到一个结构体中。结构体user_regs_struct定义在sys/user.h中。这儿有个有趣的地方——若是你打开这个头文件看看,靠近文件顶端的地方有一条这样的注释:
/* 本文件的惟一目的是为GDB,且只为GDB所用。对于这个文件,不要看的太多。除了GDB之外不要用于任何其余目的,除非你知道你正在作什么。*/
如今,我不知道你是怎么想的,但我感受咱们正处于正确的跑道上。不管如何,回到咱们的例子上来。一旦咱们将全部的寄存器值获取到regs中,咱们就能够经过PTRACE_PEEKTEXT标志以及将regs.eip(x86架构上的扩展指令指针)作参数传入ptrace来调用。咱们所获得的就是指令。让咱们在汇编代码上运行这个新版的跟踪程序。
OK,因此如今除了icounter之外,咱们还能看到指令指针以及每一步的指令。如何验证这是否正确呢?能够经过在可执行文件上执行objdump –d来实现:
用这份输出对比咱们的跟踪程序输出,应该很容易观察到相同的地方。
关联到运行中的进程上
你已经知道了调试器也能够关联到已经处于运行状态的进程上。看到这里,你应该不会感到惊讶,这也是经过ptrace来实现的。这须要经过PTRACE_ATTACH请求。这里我不会给出一段样例代码,由于经过咱们已经看到的代码,这应该很容易实现。基于教学的目的,这里采用的方法更为便捷(由于咱们能够在子进程刚启动时马上将它中止)。
代码
本文给出的这个简单的跟踪程序的完整代码(更高级一点,能够将具体指令打印出来)能够在这里找到。程序经过-Wall –pedantic –std=c99编译选项在4.4版的gcc上编译。
结论及下一步要作的
诚然,本文并无涵盖太多的内容——咱们离一个真正可用的调试器还差的很远。可是,我但愿这篇文章至少已经揭开了调试过程的神秘面纱。ptrace是一个拥有许多功能的系统调用,目前咱们只展现了其中少数几种功能。
可以单步执行代码是颇有用处的,但做用有限。以“Hello, world”为例,要到达main函数,须要先遍历好几千条初始化C运行时库的指令。这就不太方便了。咱们所但愿的理想方案是能够在main函数入口处设置一个断点,从断点处开始单步执行。下一篇文章中我将向您展现该如何实现断点机制。
参考文献
写做本文时我发现下面这些文章颇有帮助:
本文是关于调试器工做原理探究系列的第二篇。在开始阅读本文前,请先确保你已经读过本系列的第一篇(基础篇)。
本文的主要内容
这里我将说明调试器中的断点机制是如何实现的。断点机制是调试器的两大主要支柱之一 ——另外一个是在被调试进程的内存空间中查看变量的值。咱们已经在第一篇文章中稍微涉及到了一些监视被调试进程的知识,但断点机制仍然仍是个迷。阅读完本文以后,这将再也不是什么秘密了。
软中断
要在x86体系结构上实现断点咱们要用到软中断(也称为“陷阱”trap)。在咱们深刻细节以前,我想先大体解释一下中断和陷阱的概念。
CPU有一个单独的执行序列,会一条指令一条指令的顺序执行。要处理相似IO或者硬件时钟这样的异步事件时CPU就要用到中断。硬件中断一般是一个专门的电信号,链接到一个特殊的“响应电路”上。这个电路会感知中断的到来,而后会使CPU中止当前的执行流,保存当前的状态,而后跳转到一个预约义的地址处去执行,这个地址上会有一个中断处理例程。当中断处理例程完成它的工做后,CPU就从以前中止的地方恢复执行。
软中断的原理相似,但实际上有一点不一样。CPU支持特殊的指令容许经过软件来模拟一个中断。当执行到这个指令时,CPU将其当作一个中断——中止当前正常的执行流,保存状态而后跳转到一个处理例程中执行。这种“陷阱”让许多现代的操做系统得以有效完成不少复杂任务(任务调度、虚拟内存、内存保护、调试等)。
一些编程错误(好比除0操做)也被CPU当作一个“陷阱”,一般被认为是“异常”。这里软中断同硬件中断之间的界限就变得模糊了,由于这里很难说这种异常究竟是硬件中断仍是软中断引发的。我有些偏离主题了,让咱们回到关于断点的讨论上来。
关于int 3指令
看过前一节后,如今我能够简单地说断点就是经过CPU的特殊指令——int 3来实现的。int就是x86体系结构中的“陷阱指令”——对预约义的中断处理例程的调用。x86支持int指令带有一个8位的操做数,用来指定所发生的中断号。所以,理论上能够支持256种“陷阱”。前32个由CPU本身保留,这里第3号就是咱们感兴趣的——称为“trap to debugger”。
很少说了,我这里就引用“圣经”中的原话吧(这里的圣经就是Intel’s Architecture software developer’s manual, volume2A):
“INT 3指令产生一个特殊的单字节操做码(CC),这是用来调用调试异常处理例程的。(这个单字节形式很是有价值,由于这样能够经过一个断点来替换掉任何指令的第一个字节,包括其它的单字节指令也是同样,而不会覆盖到其它的操做码)。”
上面这段话很是重要,但如今解释它仍是太早,咱们稍后再来看。
使用int 3指令
是的,懂得事物背后的原理是很棒的,可是这到底意味着什么?咱们该如何使用int 3来实现断点机制?套用常见的编程问答中出现的对话——请用代码说话!
实际上这真的很是简单。一旦你的进程执行到int 3指令时,操做系统就将它暂停。在Linux上(本文关注的是Linux平台),这会给该进程发送一个SIGTRAP信号。
这就是所有——真的!如今回顾一下本系列文章的第一篇,跟踪(调试器)进程能够得到全部其子进程(或者被关联到的进程)所获得信号的通知,如今你知道咱们该作什么了吧?
就是这样,再没有什么计算机体系结构方面的东东了,该写代码了。
手动设定断点
如今我要展现如何在程序中设定断点。用于这个示例的目标程序以下:
我如今使用的是汇编语言,这是为了不当使用C语言时涉及到的编译和符号的问题。上面列出的程序功能就是在一行中打印“Hello,”,而后在下一行中打印“world!”。这个例子与上一篇文章中用到的例子很类似。
我但愿设定的断点位置应该在第一条打印以后,但刚好在第二条打印以前。咱们就让断点打在第一个int 0x80指令以后吧,也就是mov edx, len2。首先,我须要知道这条指令对应的地址是什么。运行objdump –d:
经过上面的输出,咱们知道要设定的断点地址是0x8048096。等等,真正的调试器不是像这样工做的,对吧?真正的调试器能够根据代码行数或者函数名称来设定断点,而不是基于什么内存地址吧?很是正确。可是咱们离那个标准还差的远——若是要像真正的调试器那样设定断点,咱们还须要涵盖符号表以及调试信息方面的知识,这须要用另外一篇文章来讲明。至于如今,咱们还必须得经过内存地址来设定断点。
看到这里我真的很想再扯一点题外话,因此你有两个选择。若是你真的对于为何地址是0x8048096,以及这表明什么意思很是感兴趣的话,接着看下一节。若是你对此毫无兴趣,只是想看看怎么设定断点,能够略过这一部分。
题外话——进程地址空间以及入口点
坦白的说,0x8048096自己并无太大意义,这只不过是相对可执行镜像的代码段(text section)开始处的一个偏移量。若是你仔细看看前面objdump出来的结果,你会发现代码段的起始位置是0x08048080。这告诉了操做系统要将代码段映射到进程虚拟地址空间的这个位置上。在Linux上,这些地址能够是绝对地址(好比,有的可执行镜像加载到内存中时是不可重定位的),由于在虚拟内存系统中,每一个进程都有本身独立的内存空间,并把整个32位的地址空间都看作是属于本身的(称为线性地址)。
若是咱们经过readelf工具来检查可执行文件的ELF头,咱们将获得以下输出:
注意,ELF头的“entry point address”一样指向的是0x8048080。所以,若是咱们把ELF文件中的这个部分解释给操做系统的话,就表示:
1. 将代码段映射到地址0x8048080处
2. 从入口点处开始执行——地址0x8048080
可是,为何是0x8048080呢?它的出现是因为历史缘由引发的。每一个进程的地址空间的前128MB被保留给栈空间了(注:这一部分缘由可参考Linkers and Loaders)。128MB恰好是0x80000000,可执行镜像中的其余段能够从这里开始。0x8048080是Linux下的连接器ld所使用的默认入口点。这个入口点能够经过传递参数-Ttext给ld来进行修改。
所以,获得的结论是这个地址并无什么特别的,咱们能够自由地修改它。只要ELF可执行文件的结构正确且在ELF头中的入口点地址同程序代码段(text section)的实际起始地址相吻合就OK了。
经过int 3指令在调试器中设定断点
要在被调试进程中的某个目标地址上设定一个断点,调试器须要作下面两件事情:
1. 保存目标地址上的数据
2. 将目标地址上的第一个字节替换为int 3指令
而后,当调试器向操做系统请求开始运行进程时(经过前一篇文章中提到的PTRACE_CONT),进程最终必定会碰到int 3指令。此时进程中止,操做系统将发送一个信号。这时就是调试器再次出马的时候了,接收到一个其子进程(或被跟踪进程)中止的信号,而后调试器要作下面几件事:
1. 在目标地址上用原来的指令替换掉int 3
2. 将被跟踪进程中的指令指针向后递减1。这么作是必须的,由于如今指令指针指向的是已经执行过的int 3以后的下一条指令。
3. 因为进程此时仍然是中止的,用户能够同被调试进程进行某种形式的交互。这里调试器可让你查看变量的值,检查调用栈等等。
4. 当用户但愿进程继续运行时,调试器负责将断点再次加到目标地址上(因为在第一步中断点已经被移除了),除非用户但愿取消断点。
让咱们看看这些步骤如何转化为实际的代码。咱们将沿用第一篇文章中展现过的调试器“模版”(fork一个子进程,而后对其跟踪)。不管如何,本文结尾处会给出完整源码的连接。
这里调试器从被跟踪进程中获取到指令指针,而后检查当前位于地址0x8048096处的字长内容。运行本文前面列出的汇编码程序,将打印出:
目前为止一切顺利,下一步:
注意看咱们是如何将int 3指令插入到目标地址上的。这部分代码将打印出:
注意,“Hello,”在断点以前打印出来了——同咱们计划的同样。同时咱们发现子进程已经中止运行了——就在这个单字节的陷阱指令执行以后。
这会使子进程打印出“world!”而后退出,同以前计划的同样。
注意,咱们这里并无从新加载断点。这能够在单步模式下执行,而后将陷阱指令加回去,再作PTRACE_CONT就能够了。本文稍后介绍的debug库实现了这个功能。
更多关于int 3指令
如今是回过头来讲说int 3指令的好机会,以及解释一下Intel手册中对这条指令的奇怪说明。
“这个单字节形式很是有价值,由于这样能够经过一个断点来替换掉任何指令的第一个字节,包括其它的单字节指令也是同样,而不会覆盖到其它的操做码。”
x86架构上的int指令占用2个字节——0xcd加上中断号。int 3的二进制形式能够被编码为cd 03,但这里有一个特殊的单字节指令0xcc以一样的做用而被保留。为何要这样作呢?由于这容许咱们在插入一个断点时覆盖到的指令不会多于一条。这很重要,考虑下面的示例代码:
假设咱们要在dec eax上设定断点。这刚好是条单字节指令(操做码是0x48)。若是替换为断点的指令长度超过1字节,咱们就被迫改写了接下来的下一条指令(call),这可能会产生一些彻底非法的行为。考虑一下条件分支jz foo,这时进程可能不会在dec eax处中止下来(咱们在此设定的断点,改写了原来的指令),而是直接执行了后面的非法指令。
经过对int 3指令采用一个特殊的单字节编码就能解决这个问题。由于x86架构上指令最短的长度就是1字节,这样咱们能够保证只有咱们但愿中止的那条指令被修改。
封装细节
前面几节中的示例代码展现了许多底层的细节,这些能够很容易地经过API进行封装。我已经作了一些封装,使其成为一个小型的调试库——debuglib。代码在本文末尾处能够下载。这里我只想介绍下它的用法,咱们要开始调试C程序了。
跟踪C程序
目前为止为了简单起见我把重点放在对汇编程序的跟踪上了。如今升一级来看看咱们该如何跟踪一个C程序。
其实事情并无很大的不一样——只是如今有点难以找到放置断点的位置。考虑以下这个简单的C程序:
跟预计的状况如出一辙!
代码
这里是完整的源码。在文件夹中你会发现:
debuglib.h以及debuglib.c——封装了调试器的一些内部工做。
bp_manual.c —— 本文一开始介绍的“手动”式设定断点。用到了debuglib库中的一些样板代码。
bp_use_lib.c—— 大部分代码用到了debuglib,这就是本文中用于说明跟踪一个C程序中的循环的示例代码。
结论及下一步要作的
咱们已经涵盖了如何在调试器中实现断点机制。尽管实现细节根据操做系统的不一样而有所区别,但只要你使用的是x86架构的处理器,那么一切变化都基于相同的主题——在咱们但愿中止的指令上将其替换为int 3。
我敢确定,有些读者就像我同样,对于经过指定原始地址来设定断点的作法不会感到很激动。咱们更但愿说“在do_stuff上停住”,甚至是“在do_stuff的这一行上停住”,而后调试器就能照办。在下一篇文章中,我将向您展现这是如何作到的。
本文是调试器工做原理探究系列的第三篇,在阅读前请先确保已经读过本系列的第一和第二篇。
本篇主要内容
在本文中我将向你们解释关于调试器是如何在机器码中寻找C函数以及变量的,以及调试器使用了何种数据可以在C源代码的行号和机器码中来回映射。
调试信息
现代的编译器在转换高级语言程序代码上作得十分出色,可以将源代码中漂亮的缩进、嵌套的控制结构以及任意类型的变量全都转化为一长串的比特流——这就是机器码。这么作的惟一目的就是但愿程序能在目标CPU上尽量快的运行。大多数的C代码都被转化为一些机器码指令。变量散落在各处——在栈空间里、在寄存器里,甚至彻底被编译器优化掉。结构体和对象甚至在生成的目标代码中根本不存在——它们只不过是对内存缓冲区中偏移量的抽象化表示。
那么当你在某些函数的入口处设置断点时,调试器如何知道该在哪里中止目标进程的运行呢?当你但愿查看一个变量的值时,调试器又是如何找到它并展现给你呢?答案就是——调试信息。
调试信息是在编译器生成机器码的时候一块儿产生的。它表明着可执行程序和源代码之间的关系。这个信息以预约义的格式进行编码,并同机器码一块儿存储。许多年以来,针对不一样的平台和可执行文件,人们发明了许多这样的编码格式。因为本文的主要目的不是介绍这些格式的历史渊源,而是为您展现它们的工做原理,因此咱们只介绍一种最重要的格式,这就是DWARF。做为Linux以及其余类Unix平台上的ELF可执行文件的调试信息格式,现在的DWARF能够说是无处不在。
ELF文件中的DWARF格式
根据维基百科上的词条解释,DWARF是同ELF可执行文件格式一同设计出来的,尽管在理论上DWARF也可以嵌入到其它的对象文件格式中。
DWARF是一种复杂的格式,在多种体系结构和操做系统上通过多年的探索以后,人们才在以前的格式基础上建立了DWARF。它确定是很复杂的,由于它解决了一个很是棘手的问题——为任意类型的高级语言和调试器之间提供调试信息,支持任意一种平台和应用程序二进制接口(ABI)。要彻底解释清楚这个主题,本文就显得太微不足道了。说实话,我也不理解其中的全部角落。本文我将采起更加实践的方法,只介绍足量的DWARF相关知识,可以阐明实际工做中调试信息是如何发挥其做用的就能够了。
ELF文件中的调试段
首先,让咱们看看DWARF格式信息处在ELF文件中的什么位置上。ELF能够为每一个目标文件定义任意多个段(section)。而Section header表中则定义了实际存在有哪些段,以及它们的名称。不一样的工具以各自特殊的方式来处理这些不一样的段,好比连接器只寻找它关注的段信息,而调试器则只关注其余的段。
咱们经过下面的C代码构建一个名为traceprog2的可执行文件来作下实验。
每行的第一个数字表示每一个段的大小,而最后一个数字表示距离ELF文件开始处的偏移量。调试器就是利用这个信息来从可执行文件中读取相关的段信息。如今,让咱们经过一些实际的例子来看看如何在DWARF中找寻有用的调试信息。
定位函数
当咱们在调试程序时,一个最为基本的操做就是在某些函数中设置断点,指望调试器能在函数入口处将程序断下。要完成这个功能,调试器必须具备某种可以从源代码中的函数名称到机器码中该函数的起始指令间相映射的能力。
这个信息能够经过从DWARF中的.debug_info段获取到。在咱们继续以前,先说点背景知识。DWARF的基本描述实体被称为调试信息表项(Debugging Information Entry —— DIE),每一个DIE有一个标签——包含它的类型,以及一组属性。各个DIE之间经过兄弟和孩子结点互相连接,属性值能够指向其余的DIE。
咱们运行
没错,从反汇编结果来看0x8048604确实就是函数do_stuff的起始地址。所以,这里调试器就同函数和它们在可执行文件中的位置确立了映射关系。
定位变量
假设咱们确实在do_stuff中的断点处停了下来。咱们但愿调试器可以告诉咱们my_local变量的值,调试器怎么知道去哪里找到相关的信息呢?这可比定位函数要难多了,由于变量能够在全局数据区,能够在栈上,甚至是在寄存器中。另外,具备相同名称的变量在不一样的词法做用域中可能有不一样的值。调试信息必须可以反映出全部这些变化,而DWARF确实能作到这些。
我不会涵盖全部的可能状况,做为例子,我将只展现调试器如何在do_stuff函数中定位到变量my_local。咱们从.debug_info段开始,再次看看do_stuff这一项,这一次咱们也看看其余的子项:
注意每个表项中第一个尖括号里的数字,这表示嵌套层次——在这个例子中带有<2>的表项都是表项<1>的子项。所以咱们知道变量my_local(以DW_TAG_variable做为标签)是函数do_stuff的一个子项。调试器一样还对变量的类型感兴趣,这样才能正确的显示变量的值。这里my_local的类型根据DW_AT_type标签可知为<0x4b>。若是查看objdump的输出,咱们会发现这是一个有符号4字节整数。
要在执行进程的内存映像中实际定位到变量,调试器须要检查DW_AT_location属性。对于my_local来讲,这个属性为DW_OP_fberg: -20。这表示变量存储在从所包含它的函数的DW_AT_frame_base属性开始偏移-20处,而DW_AT_frame_base正表明了该函数的栈帧起始点。
函数do_stuff的DW_AT_frame_base属性的值是0x0(location list),这表示该值必需要在location list段去查询。咱们看看objdump的输出:
关于位置信息,咱们这里感兴趣的就是第一个。对于调试器可能定位到的每个地址,它都会指定当前栈帧到变量间的偏移量,而这个偏移就是经过寄存器来计算的。对于x86体系结构,bpreg4表明esp寄存器,而bpreg5表明ebp寄存器。
让咱们再看看do_stuff的开头几条指令:
注意,ebp只有在第二条指令执行后才与咱们创建起关联,对于前两个地址,基地址由前面列出的位置信息中的esp计算得出。一旦获得了ebp的有效值,就能够很方便的计算出与它之间的偏移量。由于以后ebp保持不变,而esp会随着数据压栈和出栈不断移动。
那么这到底为咱们定位变量my_local留下了什么线索?咱们感兴趣的只是在地址0x8048610上的指令执行事后my_local的值(这里my_local的值会经过eax寄存器计算,然后放入内存)。所以调试器须要用到DW_OP_breg5: 8 基址来定位。如今回顾一下my_local的DW_AT_location属性:DW_OP_fbreg: -20。作下算数:从基址开始偏移-20,那就是ebp – 20,再偏移+8,咱们获得ebp – 12。如今再看看反汇编输出,注意到数据确实是从eax寄存器中获得的,而ebp – 12就是my_local存储的位置。
定位到行号
当我说到在调试信息中寻找函数时,我撒了个小小的谎。当咱们调试C源代码并在函数中放置了一个断点时,咱们一般并不会对第一条机器码指令感兴趣。咱们真正感兴趣的是函数中的第一行C代码。
这就是为何DWARF在可执行文件中对C源码到机器码地址作了所有映射。这部分信息包含在.debug_line段中,能够按照可读的形式进行解读:
不难看出C源码同反汇编输出之间的关系。第5行源码指向函数do_stuff的入口点——地址0x8040604。接下第6行源码,当在do_stuff上设置断点时,这里就是调试器实际应该停下的地方,它指向地址0x804860a——刚过do_stuff的开场白。这个行信息可以方便的在C源码的行号同指令地址间创建双向的映射关系。
1. 当在某一行上设定断点时,调试器将利用行信息找到实际应该陷入的地址(还记得前一篇中的int 3指令吗?)
2. 当某个指令引发段错误时,调试器会利用行信息反过来找出源代码中的行号,并告诉用户。
libdwarf —— 在程序中访问DWARF
经过命令行工具来访问DWARF信息这虽然有用但还不能彻底令咱们满意。做为程序员,咱们但愿知道应该如何写出实际的代码来解析DWARF格式并从中读取咱们须要的信息。
天然的,一种方法就是拿起DWARF规范开始钻研。还记得每一个人都告诉你永远不要本身手动解析HTML,而应该使用函数库来作吗?没错,若是你要手动解析DWARF的话状况会更糟糕,DWARF比HTML要复杂的多。本文展现的只是冰山一角而已。更困难的是,在实际的目标文件中,这些信息大部分都以很是紧凑和压缩的方式进行编码处理。
所以咱们要走另外一条路,使用一个函数库来同DWARF打交道。我知道的这类函数库主要有两个:
1. BFD(libbfd),GNU binutils就是使用的它,包括本文中屡次使用到的工具objdump,ld(GNU连接器),以及as(GNU汇编器)。
2. libdwarf —— 同它的老大哥libelf同样,为Solaris以及FreeBSD系统上的工具服务。
我这里选择了libdwarf,由于对我来讲它看起来没那么神秘,并且license更加自由(LGPL,BFD是GPL)。
因为libdwarf自身很是复杂,须要不少代码来操做。我这里不打算把全部代码贴出来,但你能够下载,而后本身编译运行。要编译这个文件,你须要安装libelf以及libdwarf,并在编译时为连接器提供-lelf以及-ldwarf标志。
这个演示程序接收一个可执行文件,并打印出程序中的函数名称同函数入口点地址。下面是本文用以演示的C程序产生的输出:
libdwarf的文档很是好(见本文的参考文献部分),花点时间看看,对于本文中提到的DWARF段信息你处理起来就应该没什么问题了。
结论及下一步
调试信息只是一个简单的概念,具体实现细节可能至关复杂。但最终咱们知道了调试器是如何从可执行文件中找出同源代码之间的关系。有了调试信息在手,调试器为用户所能识别的源代码和数据结构同可执行文件之间架起了一座桥。
本文加上以前的两篇文章总结了调试器内部的工做原理。经过这一系列文章,再加上一点编程工做就应该能够在Linux下建立一个具备基本功能的调试器。
至于下一步,我还不肯定。也许我会就此终结这一系列文章,也许我会再写一些高级主题好比backtrace,甚至Windows系统上的调试。读者们也能够为从此这一系列文章提供意见和想法。不要客气,请随意在评论栏或经过Email给我提些建议吧。
调试(Debug)
软件调试是在进行了成功的测试以后才开始的工做,它与软件测试不一样,调试的任务是进一步诊断和改正程序中潜在的错误。
调试活动由两部分组成:
u 肯定程序中可疑错误的确切性质和位置
u 对程序(设计,编码)进行修改,排除这个错误
调试工做是一个具备很强技巧性的工做
软件运行失效或出现问题,每每只是潜在错误的外部表现,而外部表现与内在缘由之间经常没有明显的联系,若是要找出真正的缘由,排除潜在的错误,不是一件易事。
能够说,调试是经过现象,找出缘由的一个思惟分析的过程。
调试步骤:
(1) 从错误的外部表现形式入手,肯定程序中出错位置
(2) 研究有关部分的程序,找出错误的内在缘由
(3) 修改设计代码,以排除这个错误
(4) 重复进行暴露了这个错误的原始测试或某些有关测试。
从技术角度来看查找错误的难度在于:
u 现象与缘由所处的位置可能相距甚远
u 当其余错误获得纠正时,这一错误所表现出的现象可能会暂时消失,但并为实际排除
u 现象其实是由一些非错误缘由(例如,舍入不精确)引发的
u 现象多是因为一些不容易发现的人为错误引发的
u 错误是因为时序问题引发的,与处理过程无关
u 现象是因为难于精确再现的输入状态(例如,实时应用中输入顺序不肯定)引发
u 现象多是周期出现的,在软,硬件结合的嵌入式系统中经常遇到
几种主要的调试方法
调试的关键在于推断程序内部的错误位置及缘由,能够采用如下方法:
强行排错
这种调试方法目前使用较多,效率较低,它不须要过多的思考,比较省脑筋。例如:
u 经过内存所有打印来调试,在这大量的数据中寻找出错的位置。
u 在程序特定位置设置打印语句,把打印语句插在出错的源程序的各个关键变量改变部位,重要分支部位,子程序调用部位,跟踪程序的执行,监视重要变量的变化
u 自动调用工具,利用某些程序语言的调试功能或专门的交互式调试工具,分析程序的动态过程,而没必要修改程序。
应用以上任一种方法以前,都应当对错误的征兆进行全面完全的分析,得出对出错位置及错误性质的推测,再使用一种适当的调试方法来检验推测的正确性。
回溯法调试
这是在小程序中经常使用的一种有效的调试方法,一旦发现了错误,人们先分析错误的征兆,肯定最早发现“症状“的位置
而后,人工沿程序的控制流程,向回追踪源程序代码,直到找到错误根源或肯定错误产生的范围,
例如,程序中发现错误处是某个打印语句,经过输出值可推断程序在这一点上变量的值,再从这一点出发,回溯程序的执行过程,反复思考:“若是程序在这一点上的状态(变量的值)是这样,那么程序在上一点的状态必定是这样···“直到找到错误所在。
概括法调试
概括法是一种从特殊推断通常的系统化思考方法,概括法调试的基本思想是:从一些线索(错误征兆)着手,经过分析它们之间的关系来找出错误
u 收集有关的数据,列出全部已知的测试用例和程序执行结果,看哪些输入数据的运行结果是正确的,哪些输入数据的运行通过是有错误的
u 组织数据
因为概括法是从特殊到通常的推断过程,因此须要组织整理数据,以发现规律
常以3W1H形式组织可用的数据
“What“列出通常现象
“Where“说明发现现象的地点
“When“列出现象发生时全部已知状况
“How“说明现象的范围和量级
“Yes“描述出现错误的3W1H;
“No“做为比较,描述了没有错误的3W1H,经过分析找出矛盾来
u 提出假设
分析线索之间的关系,利用在线索结构中观察到的矛盾现象,设计一个或多个关于出错缘由的假设,若是一个假设也提不出来,概括过程就须要收集更多的数据,此时,应当再设计与执行一些测试用例,以得到更多的数据。
u 证实假设
把假设与原始线索或数据进行比较,若它能彻底解释一切现象,则假设获得证实,不然,认为假设不合理,或不彻底,或是存在多个错误,以至只能消除部分错误
演绎法调试
演绎法是一种从通常原理或前提出发,通过排除和精华的过程来推导出结论的思考方法,演绎法排错是测试人员首先根据已有的测试用例,设想及枚举出全部可能出错的缘由做为假设,而后再用原始测试数据或新的测试,从中逐个排除不可能正确的假设,最后,再用测试数据验证余下的假设确是出错的缘由。
u 列举全部可能出错缘由的假设,把全部可能的错误缘由列成表,经过它们,能够组织,分析现有数据
u 利用已有的测试数据,排除不正确的假设
仔细分析已有的数据,寻找矛盾,力求排除前一步列出全部缘由,若是全部缘由都被排除了,则须要补充一些数据(测试用例),以创建新的假设。
u 改进余下的假设
利用已知的线索,进一步改进余下的假设,使之更具体化,以即可以精确地肯定出错位置
u 证实余下的假设
调试原则
n 在调试方面,许多原则本质上是心理学方面的问题,调试由两部分组成,调试原则也分红两组。
n 肯定错误的性质和位置的原则
u 用头脑去分析思考与错误征兆有关的信息
u 避开死胡同
u 只把调试工具当作辅助手段来使用,利用调试工具,能够帮助思考,但不能代替思考
u 避免用试探法,最多只能把它当作最后手段
n 修改错误的原则
u 在出现错误的地方,颇有可能还有别的错误
u 修改错误的一个常见失误是只修改了这个错误的征兆或这个错误的表现,而没有修改错误的自己。
u 小心修正一个错误的同时有可能会引入新的错误
u 修改错误的过程将迫令人们暂时回到程序设计阶段
u 修改源代码程序,不要改变目标代码
本文将从应用程序、编译器和调试器三个层次来说解,在不一样的层次,有不一样的方法,这些方法有各本身的长处和局限。了解这些知识,一方面知足一下新手的好奇心,另外一方面也可能有用得着的时候。
从应用程序的角度
最好的状况是从设计到编码都扎扎实实的,避免把错误引入到程序中来,这才是解决问题的根本之道。问题在于,理想状况并不存在,现实中存在着大量有内存错误的程序,若是内存错误很容易避免,JAVA/C#的优点将不会那么突出了。
对于内存错误,应用程序本身能作的很是有限。但因为这类内存错误很是典型,所占比例很是大,所付出的努力与所得的回报相比是很是划算的,仍然值得研究。
前面咱们讲了,堆里面的内存是由内存管理器管理的。从应用程序的角度来看,咱们能作到的就是打内存管理器的主意。其实原理很简单:
对付内存泄露。重载内存管理函数,在分配时,把这块内存的记录到一个链表中,在释放时,从链表中删除吧,在程序退出时,检查链表是否为空,若是不为空,则说明有内存泄露,不然说明没有泄露。固然,为了查出是哪里的泄露,在链表还要记录是谁分配的,一般记录文件名和行号就好了。
对付内存越界/野指针。对这二者,咱们只能检查一些典型的状况,对其它一些状况无能为力,但效果仍然不错。其方法以下(源于《Comparing and contrasting the runtime error detection technologies》):
l 首尾在加保护边界值
Header
Leading guard(0xFC)
User data(0xEB)
Tailing guard(0xFC)
在内存分配时,内存管理器按如上结构填充分配出来的内存。其中Header是管理器本身用的,先后各有几个字节的guard数据,它们的值是固定的。当内存释放时,内存管理器检查这些guard数据是否被修改,若是被修改,说明有写越界。
它的工做机制注定了有它的局限性: 只能检查写越界,不能检查读越界,并且只能检查连续性的写越界,对于跳跃性的写越界无能为力。
l 填充空闲内存
空闲内存(0xDD)
内存被释放以后,它的内容填充成固定的值。这样,从指针指向的内存的数据,能够大体判断这个指针是不是野指针。
它一样有它的局限:程序要主动判断才行。若是野指针指向的内存当即被从新分配了,它又被填充成前面那个结构,这时也没法检查出来。
从编译器的角度
boundschecker和purify的实现均可以归于编译器一级。前者采用一种称为CTI(compile-time instrumentation)的技术。VC的编译不是要分几个阶段吗?boundschecker在预处理和编译两个阶段之间,对源文件进行修改。它对全部内存分配释放、内存读写、指针赋值和指针计算等全部内存相关的操做进行分析,并插入本身的代码。好比:
Before
if (m_hsession) gblHandles->ReleaseUserHandle( m_hsession );
if (m_dberr) delete m_dberr;
After
if (m_hsession) {
_Insight_stack_call(0);
gblHandles->ReleaseUserHandle(m_hsession);
_Insight_after_call();
}
_Insight_ptra_check(1994, (void **) &m_dberr, (void *) m_dberr);
if (m_dberr) {
_Insight_deletea(1994, (void **) &m_dberr, (void *) m_dberr, 0);
delete m_dberr;
}
Purify则采用一种称为OCI(object code insertion)的技术。不一样的是,它对可执行文件的每条指令进行分析,找出全部内存分配释放、内存读写、指针赋值和指针计算等全部内存相关的操做,用本身的指令代替原始的指令。
boundschecker和purify是商业软件,它们的实现是保密的,甚至拥有专利的,没法对其研究,只能找一些皮毛性的介绍。不管是CTI仍是OCI这样的名称,多少有些神秘感。其实它们的实现原理并不复杂,经过对valgrind和gcc的bounds checker扩展进行一些粗浅的研究,咱们能够知道它们的大体原理。
gcc的bounds checker基本上能够与boundschecker对应起来,都是对源代码进行修改,以达到控制内存操做功能,如malloc/free等内存管理函数、memcpy/strcpy/memset等内存读取函数和指针运算等。Valgrind则与Purify相似,都是经过对目标代码进行修改,来达到一样的目的。
Valgrind对可执行文件进行修改,因此不须要从新编译程序。但它并非在执行前对可执行文件和全部相关的共享库进行一次性修改,而是和应用程序在同一个进程中运行,动态的修改即将执行的下一段代码。
Valgrind是插件式设计的。Core部分负责对应用程序的总体控制,并把即将修改的代码,转换成一种中间格式,这种格式相似于RISC指令,而后把中间代码传给插件。插件根据要求对中间代码修改,而后把修改后的结果交给core。core接下来把修改后的中间代码转换成原始的x86指令,并执行它。
因而可知,不管是boundschecker、purify、gcc的bounds checker,仍是Valgrind,修改源代码也罢,修改二进制也罢,都是代码进行修改。究竟要修改什么,修改为什么样子呢?别急,下面咱们就要来介绍:
管理全部内存块。不管是堆、栈仍是全局变量,只要有指针引用它,它就被记录到一个全局表中。记录的信息包括内存块的起始地址和大小等。要作到这一点并不难:对于在堆里分配的动态内存,能够经过重载内存管理函数来实现。对于全局变量等静态内存,能够从符号表中获得这些信息。
拦截全部的指针计算。对于指针进行乘除等运算一般意义不大,最多见运算是对指针加减一个偏移量,如++p、p=p+n、p=a[n]等。全部这些有意义的指针操做,都要受到检查。再也不是由一条简单的汇编指令来完成,而是由一个函数来完成。
有了以上两点保证,要检查内存错误就很是容易了:好比要检查++p是否有效,首先在全局表中查找p指向的内存块,若是没有找到,说明p是野指针。若是找到了,再检查p+1是否在这块内存范围内,若是不是,那就是越界访问,不然是正常的了。怎么样,简单吧,不管是全局内存、堆仍是栈,不管是读仍是写,无一可以逃过出工具的法眼。
代码赏析(源于tcc):
对指针运算进行检查:
void *__bound_ptr_add(void *p, int offset)
{
unsigned long addr = (unsigned long)p;
BoundEntry *e;
#if defined(BOUND_DEBUG)
printf("add: 0x%x %d\n", (int)p, offset);
#endif
e = __bound_t1[addr >> (BOUND_T2_BITS + BOUND_T3_BITS)];
e = (BoundEntry *)((char *)e +
((addr >> (BOUND_T3_BITS - BOUND_E_BITS)) &
((BOUND_T2_SIZE - 1) << BOUND_E_BITS)));
addr -= e->start;
if (addr > e->size) {
e = __bound_find_region(e, p);
addr = (unsigned long)p - e->start;
}
addr += offset;
if (addr > e->size)
return INVALID_POINTER;
return p + offset;
}
static void __bound_check(const void *p, size_t size)
{
if (size == 0)
return;
p = __bound_ptr_add((void *)p, size);
if (p == INVALID_POINTER)
bound_error("invalid pointer");
}
重载内存管理函数:
void *__bound_malloc(size_t size, const void *caller)
{
void *ptr;
ptr = libc_malloc(size + 1);
if (!ptr)
return NULL;
__bound_new_region(ptr, size);
return ptr;
}
void __bound_free(void *ptr, const void *caller)
{
if (ptr == NULL)
return;
if (__bound_delete_region(ptr) != 0)
bound_error("freeing invalid region");
libc_free(ptr);
}
重载内存操做函数:
void *__bound_memcpy(void *dst, const void *src, size_t size)
{
__bound_check(dst, size);
__bound_check(src, size);
if (src >= dst && src < dst + size)
bound_error("overlapping regions in memcpy()");
return memcpy(dst, src, size);
}
从调试器的角度
如今有OS的支持,实现一个调试器变得很是简单,至少原理再也不神秘。这里咱们简要介绍一下win32和linux中的调试器实现原理。
在Win32下,实现调试器主要经过两个函数:WaitForDebugEvent和ContinueDebugEvent。下面是一个调试器的基本模型(源于: 《Debugging Applications for Microsoft .NET and Microsoft Windows》)
void main ( void )
{
CreateProcess ( ..., DEBUG_ONLY_THIS_PROCESS ,... ) ;
while ( 1 == WaitForDebugEvent ( ... ) )
{
if ( EXIT_PROCESS )
{
break ;
}
ContinueDebugEvent ( ... ) ;
}
}
由调试器起动被调试的进程,并指定DEBUG_ONLY_THIS_PROCESS标志。按Win32下事件驱动的一向原则,由被调试的进程主动上报调试事件,调试器而后作相应的处理。
在linux下,实现调试器只要一个函数就好了:ptrace。下面是个简单示例:(源于《Playing with ptrace》)。
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/user.h>
int main(int argc, char *argv[])
{ pid_t traced_process;
struct user_regs_struct regs;
long ins;
if(argc != 2) {
printf("Usage: %s <pid to be traced>\n",
argv[0], argv[1]);
exit(1);
}
traced_process = atoi(argv[1]);
ptrace(PTRACE_ATTACH, traced_process,
NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, traced_process,
NULL, ®s);
ins = ptrace(PTRACE_PEEKTEXT, traced_process,
regs.eip, NULL);
printf("EIP: %lx Instruction executed: %lx\n",
regs.eip, ins);
ptrace(PTRACE_DETACH, traced_process,
NULL, NULL);
return 0;
}
因为篇幅有限,这里对于调试器的实现不做深刻讨论,主要是给新手指一个方向。之后如有时间,再写个专题来介绍linux下的调试器和ptrace自己的实现方法。
严格的讲,调试器是帮助程序员跟踪,分隔和从软件中移除bug的工具。它帮助程序员更进一步理解程序。一开始,主要是开发人员使用它,后来测试人员,维护人员也开始使用它。
调试器的发展历程:
调试器的设计和开发要遵循四个关键的原则:
按照划分的标准不一样,调试器主要分为一下几类:
调试器之间的区别更多的是体如今他们展示给用户的窗口。至于底层结构都是很相近的。下图展现了调试器的整体架构:
调试器服务于全部的调试器视图。包括进程控制,执行引擎,表达式计算,符号表管理四部分。
调试器内核为了访问被调试程序,必须使用操做系统提供的一系列例程。
调试器控制被调试程序的能力主要是依靠硬件支持和操做系统的调试机制。调试器须要最少三种的硬件功能的支持:
1. 提供设置断点的方法;
2. 通知操做系统发生中断或者陷阱的功能;
3. 当中断或者陷阱发生时,直接读写寄存器,包括程序计数器。
通用的硬件调试机制
1. 断点支持
断点功能是经过特定的指令来实现的。对于变长指令的处理器,断点指令一般是最短的指令,下图给出了四个处理器的断点指令:
2. 单步调试支持
单步调试是指执行一条指令就产生一次中断,是用户能够查找每条指令的执行状态。通常的处理器都提供一个模式位来实现单步调试功能。
3. 错误检测支持
错误检测功能是指当操做系统检测到错误发生时,他通知调试器被它调试的程序发生了错误。
4. 检测点支持
用来查看被调试程序的地址空间(数据空间)。
5. 多线程支持
6. 多处理器支持
调试器的操做系统支持功能
为了控制一个被调试程序的过程,调试器须要一种机制去通知操做系统该可执行文件但愿被控制。即一旦被调试程序因为某些缘由中止的时候,调试器须要获取详细的信息使得他知道被调试程序是什么缘由形成他中止的。
调试器是用户级的程序,并非操做系统的一部分,并不能运行特权级指令,所以,它只能经过调用操做系统的系统调用来实现对特权级指令的访问。
调试器运行被调试程序,并将控制权转交给被调试程序,须要进行上下文切换。在一个简单的断点功能实现,有6个主要的转换:
1. 当调试器运行到断点指令的时候,产生陷阱跳转到操做系统;
2. 经过操做系统,跳转到调试器,调试器开始运行;
3. 调试器请求被调试程序的状态信息,该请求送到操做系统进行处理;
4. 转换到被调试程序文本以获取信息,被调试程序激活;
5. 返回信息给操做系统;
6. 转换到调试器以处理信息。
一旦使用图形界面调试器,过程会更加的复杂。
对于多线程调试的支持;
l 一旦进程建立和删除,操做系统必须通知调试器;
l 可以询问和设置特定进程的进程状态;
l 可以检测到应用程序中止,或者线程中止。
例子:UNIX ptrace()
UNIX ptrace 是操做系统支持调试器的一个真实的API。
控制执行
调试器的核心是它的进程控制和运行控制。为了可以调试程序,调试器必须可以对被调试程序进行状态设置,断点设置,运行进程,终止进程。
控制执行主要包含一下几个功能:
1. 建立被调试程序
调试器作的第一件工做,就是建立被调试程序。通常经过两种手段:一种是为调试程序建立被调试进程,另外一种是将调试器附到被调试进程上。
2. 附到被调试进程
当一个进程发生错误异常,而且在被刷出(内存刷新)内存的时候,容许调试器挂到出错进程以此来检查内存镜像。这个时候,用户不能再继续执行进程。
3. 设置断点
设置断点的功能是在可执行文本中插入特殊的指令来实现的。当程序执行到该特殊指令的时候,就产生陷阱,陷到操做系统。
4. 使被调试程序运行
当调试中断产生的时候,调试器属于激活进程,而被调试程序属于未激活进程。调试器产生一个系统中断请求恢复被调用函数的执行,操做系统对被调试程序进行上下文切换,恢复被调用程序的现场状态,而后执行被调用程序。
执行区间的调试事件生成类型:
l 断点,单步调试事件
l 线程建立/删除事件
l 进程建立/删除事件
l 检测点事件
l 模块加载/卸载事件
l 异常事件
l 其余事件
断点和单步调试
断点一般须要两层的表示:
l 逻辑表示:指在源代码中设置的断点,用来告诉用户的;
l 物理表示:指真实的在机器码中写入,是用来告诉物理机器的。断点必须存储写入位置的机器指令,以便可以在移除断点的时候恢复原来的指令。
断点存在条件断点。
断点存在多对一的关系,即多个用户在同一个地方设置断点(多个逻辑断点对应一个物理断点),固然也有多对多的关系。下图展现了这样的一个关系:
临时断点
临时断点是指只运行一次的断点。
内部断点
内部断点对用户是不可见的。他们是被调试器设置的。
通常主要用于:
l 单步调试:内部断点和运行到内部断点;
l 跳出函数:在函数返回地址设置内部断点;
l 进入函数
查看程序的上下文信息
通常要查找程序的上下文信息主要有如下几种方法:
经过源代码查看程序执行到代码的那一部分
程序堆栈是由硬件,操做系统和编译器共同支持的:
硬件: 提供堆栈指针;
操做系统:为每一个进程创建堆栈空间,并管理堆栈。一旦堆栈溢出,而产生一个错误;
$ ulimit -a
core file size (blocks, -c) unlimited
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7884
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7884
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
$ ulimit -c 0 <--------- c选项指定修改core文件的大小
$ ulimit -c 1000 <--------指定了core文件大小为1000KB, 若是设置的大小小于core文件,则对core文件截取
$ ulimit -c unlimited <---------------对core文件的大小不作限制
$ echo "0" > /proc/sys/kernel/core_uses_pid
$ file core.4244
core.4244: ELF 64-bit LSB core file x86-64, version 1 (SYSV), SVR4-style, from '/home/fireway/study/temp/a.out'
$ readelf -h core.4244
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: CORE (Core 文件)
Machine: Advanced Micro Devices X86-64
Version: 0x1
入口点地址: 0x0
程序头起点: 64 (bytes into file)
Start of section headers: 0 (bytes into file)
标志: 0x0
本头的大小: 64 (字节)
程序头大小: 56 (字节)
Number of program headers: 19
节头大小: 0 (字节)
节头数量: 0
字符串表索引节头: 0
$ gdb exec_file core_file
$ objdump -x core.4244 | tail
26 load16 00001000 00007ffff7ffe000 0000000000000000 0003f000 2**12
CONTENTS, ALLOC, LOAD
27 load17 00801000 00007fffff7fe000 0000000000000000 00040000 2**12
CONTENTS, ALLOC, LOAD
28 load18 00001000 ffffffffff600000 0000000000000000 00841000 2**12
CONTENTS, ALLOC, LOAD, READONLY, CODE
SYMBOL TABLE:
no symbols <----------------- 代表当前的ELF格式文件中没有符号表信息
Reading symbols from mycat...(no debugging symbols found)...done.
warning: core file may not match specified executable file.
[New LWP 2037]
Core was generated by `./mycat_debug'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000000000400957 in main ()
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from mycat_debug...done.
[New LWP 2037]
Core was generated by `./mycat_debug'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 main () at io1.c:16
16 int n = 0;
(gdb) info f
Stack level 0, frame at 0x7ffc4b59d670:
rip = 0x400957 in main (io1.c:16); saved rip = 0x7fc5c0d5aec5
source language c.
Arglist at 0x7ffc4b59d660, args:
Locals at 0x7ffc4b59d660, Previous frame's sp is 0x7ffc4b59d670
Saved registers:
rbp at 0x7ffc4b59d660, rip at 0x7ffc4b59d668
(gdb) x/5i 0x400957 或者x/5i $rip
=> 0x400957 <main+26>:movl $0x0,-0x800014(%rbp)
0x400961 <main+36>:lea -0x800010(%rbp),%rax
0x400968 <main+43>:mov $0x800000,%edx
0x40096d <main+48>:mov $0x0,%esi
0x400972 <main+53>:mov %rax,%rdi
(gdb) x /b 0x7ffc4ad9d64c
0x7ffc4ad9d64c: Cannot access memory at address 0x7ffc4ad9d64c
一,什么是coredump
咱们常常听到你们说到程序core掉了,须要定位解决,这里说的大部分是指对应程序因为各类异常或者bug致使在运行过程当中异常退出或者停止,而且在知足必定条件下(这里为何说须要知足必定的条件呢?下面会分析)会产生一个叫作core的文件。
一般状况下,core文件会包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各类函数调用堆栈信息等,咱们能够理解为是程序工做当前状态存储生成第一个文件,许多的程序出错的时候都会产生一个core文件,经过工具分析这个文件,咱们能够定位到程序异常退出的时候对应的堆栈调用等信息,找出问题所在并进行及时解决。
二,coredump文件的存储位置
core文件默认的存储位置与对应的可执行程序在同一目录下,文件名是core,你们能够经过下面的命令看到core文件的存在位置:
cat /proc/sys/kernel/core_pattern
缺省值是core
注意:这里是指在进程当前工做目录的下建立。一般与程序在相同的路径下。但若是程序中调用了chdir函数,则有可能改变了当前工做目录。这时core文件建立在chdir指定的路径下。有好多程序崩溃了,咱们却找不到core文件放在什么位置。和chdir函数就有关系。固然程序崩溃了不必定都产生 core文件。
以下程序代码:则会把生成的core文件存储在/data/coredump/wd,而不是你们认为的跟可执行文件在同一目录。
经过下面的命令能够更改coredump文件的存储位置,若你但愿把core文件生成到/data/coredump/core目录下:
echo “/data/coredump/core”> /proc/sys/kernel/core_pattern
注意,这里当前用户必须具备对/proc/sys/kernel/core_pattern的写权限。
缺省状况下,内核在coredump时所产生的core文件放在与该程序相同的目录中,而且文件名固定为core。很显然,若是有多个程序产生core文件,或者同一个程序屡次崩溃,就会重复覆盖同一个core文件,所以咱们有必要对不一样程序生成的core文件进行分别命名。
咱们经过修改kernel的参数,能够指定内核所生成的coredump文件的文件名。例如,使用下面的命令使kernel生成名字为core.filename.pid格式的core dump文件:
echo “/data/coredump/core.%e.%p” >/proc/sys/kernel/core_pattern
这样配置后,产生的core文件中将带有崩溃的程序名、以及它的进程ID。上面的%e和%p会被替换成程序文件名以及进程ID。
若是在上述文件名中包含目录分隔符“/”,那么所生成的core文件将会被放到指定的目录中。 须要说明的是,在内核中还有一个与coredump相关的设置,就是/proc/sys/kernel/core_uses_pid。若是这个文件的内容被配置成1,那么即便core_pattern中没有设置%p,最后生成的core dump文件名仍会加上进程ID。
三,如何判断一个文件是coredump文件?
在类unix系统下,coredump文件自己主要的格式也是ELF格式,所以,咱们能够经过readelf命令进行判断。
能够看到ELF文件头的Type字段的类型是:CORE (Core file)
能够经过简单的file命令进行快速判断:
四,产生coredum的一些条件总结
1, 产生coredump的条件,首先须要确认当前会话的ulimit –c,若为0,则不会产生对应的coredump,须要进行修改和设置。
ulimit -c unlimited (能够产生coredump且不受大小限制)
若想甚至对应的字符大小,则能够指定:
ulimit –c [size]
能够看出,这里的size的单位是blocks,通常1block=512bytes
如:
ulimit –c 4 (注意,这里的size若是过小,则可能不会产生对应的core文件,笔者设置过ulimit –c 1的时候,系统并不生成core文件,并尝试了1,2,3均没法产生core,至少须要4才生成core文件)
但当前设置的ulimit只对当前会话有效,若想系统均有效,则须要进行以下设置:
Ø 在/etc/profile中加入如下一行,这将容许生成coredump文件
ulimit-c unlimited
Ø 在rc.local中加入如下一行,这将使程序崩溃时生成的coredump文件位于/data/coredump/目录下:
echo /data/coredump/core.%e.%p> /proc/sys/kernel/core_pattern
注意rc.local在不一样的环境,存储的目录可能不一样,susu下可能在/etc/rc.d/rc.local
更多ulimit的命令使用,能够参考:http://baike.baidu.com/view/4832100.htm
这些须要有root权限, 在ubuntu下每次从新打开中断都须要从新输入上面的ulimit命令, 来设置core大小为无限.
2, 当前用户,即执行对应程序的用户具备对写入core目录的写权限以及有足够的空间。
3, 几种不会产生core文件的状况说明:
The core file will not be generated if
(a) the process was set-user-ID and the current user is not the owner of the program file, or
(b) the process was set-group-ID and the current user is not the group owner of the file,
(c) the user does not have permission to write in the current working directory,
(d) the file already exists and the user does not have permission to write to it, or
(e) the file is too big (recall the RLIMIT_CORE limit in Section 7.11). The permissions of the core file (assuming that the file doesn't already exist) are usually user-read and user-write, although Mac OS X sets only user-read.
五,coredump产生的几种可能状况
形成程序coredump的缘由有不少,这里总结一些比较经常使用的经验吧:
1,内存访问越界
a) 因为使用错误的下标,致使数组访问越界。
b) 搜索字符串时,依靠字符串结束符来判断字符串是否结束,可是字符串没有正常的使用结束符。
c) 使用strcpy, strcat, sprintf, strcmp,strcasecmp等字符串操做函数,将目标字符串读/写爆。应该使用strncpy, strlcpy, strncat, strlcat, snprintf, strncmp, strncasecmp等函数防止读写越界。
2,多线程程序使用了线程不安全的函数。
应该使用下面这些可重入的函数,它们很容易被用错:
asctime_r(3c) gethostbyname_r(3n) getservbyname_r(3n)ctermid_r(3s) gethostent_r(3n) getservbyport_r(3n) ctime_r(3c) getlogin_r(3c)getservent_r(3n) fgetgrent_r(3c) getnetbyaddr_r(3n) getspent_r(3c)fgetpwent_r(3c) getnetbyname_r(3n) getspnam_r(3c) fgetspent_r(3c)getnetent_r(3n) gmtime_r(3c) gamma_r(3m) getnetgrent_r(3n) lgamma_r(3m) getauclassent_r(3)getprotobyname_r(3n) localtime_r(3c) getauclassnam_r(3) etprotobynumber_r(3n)nis_sperror_r(3n) getauevent_r(3) getprotoent_r(3n) rand_r(3c) getauevnam_r(3)getpwent_r(3c) readdir_r(3c) getauevnum_r(3) getpwnam_r(3c) strtok_r(3c) getgrent_r(3c)getpwuid_r(3c) tmpnam_r(3s) getgrgid_r(3c) getrpcbyname_r(3n) ttyname_r(3c)getgrnam_r(3c) getrpcbynumber_r(3n) gethostbyaddr_r(3n) getrpcent_r(3n)
3,多线程读写的数据未加锁保护。
对于会被多个线程同时访问的全局数据,应该注意加锁保护,不然很容易形成coredump
4,非法指针
a) 使用空指针
b) 随意使用指针转换。一个指向一段内存的指针,除非肯定这段内存原先就分配为某种结构或类型,或者这种结构或类型的数组,不然不要将它转换为这种结构或类型的指针,而应该将这段内存拷贝到一个这种结构或类型中,再访问这个结构或类型。这是由于若是这段内存的开始地址不是按照这种结构或类型对齐的,那么访问它时就很容易由于bus error而core dump。
5,堆栈溢出
不要使用大的局部变量(由于局部变量都分配在栈上),这样容易形成堆栈溢出,破坏系统的栈和堆结构,致使出现莫名其妙的错误。
六,利用gdb进行coredump的定位
其实分析coredump的工具备不少,如今大部分类unix系统都提供了分析coredump文件的工具,不过,咱们常常用到的工具是gdb。
这里咱们以程序为例子来讲明如何进行定位。
1, 段错误 – segmentfault
Ø 咱们写一段代码往受到系统保护的地址写内容。
Ø 按以下方式进行编译和执行,注意这里须要-g选项编译。
能够看到,当输入12的时候,系统提示段错误而且core dumped
Ø 咱们进入对应的core文件生成目录,优先确认是否core文件格式并启用gdb进行调试。
从红色方框截图能够看到,程序停止是由于信号11,且从bt(backtrace)命令(或者where)能够看到函数的调用栈,即程序执行到coremain.cpp的第5行,且里面调用scanf 函数,而该函数其实内部会调用_IO_vfscanf_internal()函数。
接下来咱们继续用gdb,进行调试对应的程序。
记住几个经常使用的gdb命令:
l(list) ,显示源代码,而且能够看到对应的行号;
b(break)x, x是行号,表示在对应的行号位置设置断点;
p(print)x, x是变量名,表示打印变量x的值
r(run), 表示继续执行到断点的位置
n(next),表示执行下一步
c(continue),表示继续执行
q(quit),表示退出gdb
启动gdb,注意该程序编译须要-g选项进行。
注: SIGSEGV 11 Core Invalid memoryreference
七,附注:
1, gdb的查看源码
显示源代码
GDB 能够打印出所调试程序的源代码,固然,在程序编译时必定要加上-g的参数,把源程序信息编译到执行文件中。否则就看不到源程序了。当程序停下来之后,GDB会报告程序停在了那个文件的第几行上。你能够用list命令来打印程序的源代码。仍是来看一看查看源代码的GDB命令吧。
list<linenum>
显示程序第linenum行的周围的源程序。
list<function>
显示函数名为function的函数的源程序。
list
显示当前行后面的源程序。
list -
显示当前行前面的源程序。
通常是打印当前行的上5行和下5行,若是显示函数是是上2行下8行,默认是10行,固然,你也能够定制显示的范围,使用下面命令能够设置一次显示源程序的行数。
setlistsize <count>
设置一次显示源代码的行数。
showlistsize
查看当前listsize的设置。
list命令还有下面的用法:
list<first>, <last>
显示从first行到last行之间的源代码。
list ,<last>
显示从当前行到last行之间的源代码。
list +
日后显示源代码。
通常来讲在list后面能够跟如下这些参数:
<linenum> 行号。
<+offset> 当前行号的正偏移量。
<-offset> 当前行号的负偏移量。
<filename:linenum> 哪一个文件的哪一行。
<function> 函数名。
<filename:function>哪一个文件中的哪一个函数。
<*address> 程序运行时的语句在内存中的地址。
2, 一些经常使用signal的含义
SIGABRT:调用abort函数时产生此信号。进程异常终止。
SIGBUS:指示一个实现定义的硬件故障。
SIGEMT:指示一个实现定义的硬件故障。EMT这一名字来自PDP-11的emulator trap 指令。
SIGFPE:此信号表示一个算术运算异常,例如除以0,浮点溢出等。
SIGILL:此信号指示进程已执行一条非法硬件指令。4.3BSD由abort函数产生此信号。SIGABRT如今被用于此。
SIGIOT:这指示一个实现定义的硬件故障。IOT这个名字来自于PDP-11对于输入/输出TRAP(input/outputTRAP)指令的缩写。系统V的早期版本,由abort函数产生此信号。SIGABRT如今被用于此。
SIGQUIT:当用户在终端上按退出键(通常采用Ctrl-/)时,产生此信号,并送至前台进
程组中的全部进程。此信号不只终止前台进程组(如SIGINT所作的那样),同时产生一个core文件。
SIGSEGV:指示进程进行了一次无效的存储访问。名字SEGV表示“段违例(segmentationviolation)”。
SIGSYS:指示一个无效的系统调用。因为某种未知缘由,进程执行了一条系统调用指令,但其指示系统调用类型的参数倒是无效的。
SIGTRAP:指示一个实现定义的硬件故障。此信号名来自于PDP-11的TRAP指令。
SIGXCPUSVR4和4.3+BSD支持资源限制的概念。若是进程超过了其软C P U时间限制,则产生此信号。
SIGXFSZ:若是进程超过了其软文件长度限制,则SVR4和4.3+BSD产生此信号。
3, Core_pattern的格式
能够在core_pattern模板中使用变量还不少,见下面的列表:
%% 单个%字符
%p 所dump进程的进程ID
%u 所dump进程的实际用户ID
%g 所dump进程的实际组ID
%s 致使本次core dump的信号
%t core dump的时间 (由1970年1月1日计起的秒数)
%h 主机名
%e 程序文件名
利用Core Dump调试程序
[描述]
这里介绍Linux环境下使用gdb结合core dump文件进行程序的调试和定位。
[简介]
当用户程序运行,可能会因为某些缘由发生崩溃(crash),这个时候能够产生一个Core Dump文件,记录程序发生崩溃时候内存的运行情况。这个Core Dump文件,通常名称为core或者core.pid(pid就是应用程序运行时候的pid号),它能够帮助咱们找出程序崩溃的缘由。
对于一个运行出错的程序,咱们能够有多种方法调试它,以便发生错误的缘由:a)经过阅读代码;b)经过在代码中设置一些打印语句(插旗子);c)经过使用gdb设置断点来跟踪程序的运行。可是这些方法对于调试程序运行崩溃这样相似的错误,定位都不够迅速,若是程序代码不少的话,显然前面的方法有不少缺陷。在后面,咱们来看看另一种能够定位错误的方法:d)使用gdb结合Core Dump文件来迅速定位到这个错误。这个方法,若是程序运行崩溃,那么能够迅速找到致使程序崩溃的缘由。
固然,调试程序,没有哪一个方法是最好的,这里只对最后一种方法重点讲解,实际过程当中,每每根据须要和其余方法结合使用。
[举例]
下面,给出一个实际的操做过程,描述咱们使用gdb调试工具,结合Core Dump文件,定位程序崩溃的位置。
1、程序源代码
下面是咱们的程序源代码:
1 #include <iostream>
2 using std::cerr;
3 using std::endl;
4
5 void my_print(int d1, int d2);
6 int main(int argc, char *argv[])
7 {
8 int a = 1;
9 int b = 2;
10 my_print(a,b);
11 return 0;
12 }
13
14 void my_print(int d1, int d2)
15 {
16 int *p1=&d1;
17 int *p2 = NULL;
18 cerr<<"first is:"<<*p1<<",second is:"<<*p2<<endl;
19 }
这里,程序代码不多,咱们能够直接经过代码看到这个程序第17行的p2是NULL,而18行却用*p2来进行引用,明显这样访问一个空的地址是一个错误(也许咱们的初衷是使用p2来指向d2)。
咱们能够有多种方法调试这个程序,以便发生上面的错误:a)经过阅读代码;b)经过在代码中设置一些打印语句(插旗子);c)经过使用gdb设置断点来跟踪程序的运行。可是这些方法对于这个程序中相似的错误,定位都不够迅速,若是程序代码不少的话,显然前面的方法有不少缺陷。在后面,咱们来看看另一种能够定位错误的方法:d)使用gdb结合Core Dump文件来迅速定位到这个错误。
2、编译程序:
编译过程以下:
[root@lv-k test]# ls
main.cpp
[root@lv-k test]# g++ -g main.cpp
[root@lv-k test]# ls
a.out main.cpp
这样,编译main.cpp生成了可执行文件a.out,必定注意,由于咱们要使用gdb进行调试,因此咱们使用'g++'的'-g'选项。
3、运行程序
运行过程以下:
[root@lv-k test]# ./a.out
段错误
[root@lv-k test]# ls
a.out main.cpp
这里,如咱们所指望的,会打印段错误的信息,可是并无生成Core Dump文件。
配置生成Core Dump文件的选项,并生成Core Dump:
[root@lv-k test]# ulimit -c unlimited
[root@lv-k test]# ./a.out
段错误 (core dumped)
[root@lv-k test]# ls
a.out core.30557 main.cpp
这里,咱们看到,使用'ulimit'配置以后,程序崩溃的时候就会生成Core Dump文件了,这里的文件是core.30557,文件名称不一样的系统生成的名称有一点不一样,这里linux生成的名称是:"core"+".pid"。
4、调试程序
使用Core Dump文件,结合gdb工具进行调试,过程以下:
1)初步定位:
[root@lv-k test]# gdb a.out core.30557
GNU gdb (GDB) Red Hat Enterprise Linux (7.0.1-23.el5_5.2)
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/test/a.out...done.
Reading symbols from /usr/lib/libstdc++.so.6...(no debugging symbols found)...done.
Loaded symbols for /usr/lib/libstdc++.so.6
Reading symbols from /lib/libm.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/libm.so.6
Reading symbols from /lib/libgcc_s.so.1...(no debugging symbols found)...done.
Loaded symbols for /lib/libgcc_s.so.1
Reading symbols from /lib/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
Core was generated by `./a.out'.
Program terminated with signal 11, Segmentation fault.
#0 0x0804870e in my_print (d1=1, d2=2) at main.cpp:18
18 cerr<<"first is:"<<*p1<<",second is:"<<*p2<<endl;
这里,咱们就进入了gdb的调试交互界面,看到gdb直接定位到致使程序出错的位置了。咱们还能够使用以下命令:"#gdb a.out --core=core.30557"。
经过错误,咱们知道程序因为"signal 11"致使终止,若是想要大体了解"signal 11",那么咱们可查看signal的man手册:
#man 7 signal
这样,在输出的信息中咱们能够看见“SIGSEGV 11 Core Invalid memory reference”这样的字样,意思是说,signal(信号)11表示非法内存引用。注意这里使用"man 7 signal"而不是"man signal",由于咱们要查看的不是signal函数或者signal命令,而是signal的其余信息,其余的信息在man手册的第7节,具体须要了解一些使用man的命令。
2)查看具体调用关系
(gdb) bt
#0 0x0804870e in my_print (d1=1, d2=2) at main.cpp:18
#1 0x08048799 in main (argc=<value optimized out>, argv=<value optimized out>) at main.cpp:10
这里,咱们经过backtrace(简写为bt)命令能够看到,致使崩溃那条语句是经过什么调用途径被调用到的。
3)设置断点,并进行调试等:
(gdb) b main.cpp:10
Breakpoint 1 at 0x8048787: file main.cpp, line 10.
(gdb) r
Starting program: /root/test/a.out
Breakpoint 1, main (argc=<value optimized out>, argv=<value optimized out>) at main.cpp:10
10 my_print(a,b);
(gdb) s
my_print (d1=1, d2=2) at main.cpp:16
16 int *p1=&d1;
(gdb) n
17 int *p2 = NULL;
(gdb) n
18 cerr<<"first is:"<<*p1<<",second is:"<<*p2<<endl;
(gdb) p d1
$1 = 1
(gdb) p d2
$2 = 2
(gdb) p *p1
$1 = 1
(gdb) p *p2
Cannot access memory at address 0x0
(gdb) n
Program received signal SIGSEGV, Segmentation fault.
0x0804870e in my_print (d1=1, d2=2) at main.cpp:18
18 cerr<<"first is:"<<*p1<<",second is:"<<*p2<<endl;
这里,咱们在开始初步的定位的基础上,经过设置断点(break),运行(run),gdb的单步跟进(step),单步跳过(next),变量的打印(print)等各类gdb命令,来了解产生崩溃时候的具体状况,肯定产生崩溃的缘由。
4)退出gdb:
(gdb) q
A debugging session is active.
Inferior 3 [process 30584] will be killed.
Inferior 1 [process 1] will be killed.
Quit anyway? (y or n) y
Quitting: Couldn't get registers: 没有那个进程.
[root@lv-k test]#
[root@lv-k test]# ls
a.out core.30557 core.30609 main.cpp
这里,咱们看到又产生了一个core文件。由于刚才调试,致使又产生了一个core文件。实际,若是咱们只使用"gdb a.out core.30557"初步定位以后,不进行调试就退出gdb的话,就不会再生成core文件。
5、修正错误
1)经过上面的过程咱们最终修正错误,获得正确的源代码以下:
1 #include <iostream>
2 using std::cerr;
3 using std::endl;
4
5 void my_print(int d1, int d2);
6 int main(int argc, char *argv[])
7 {
8 int a = 1;
9 int b = 2;
10 my_print(a,b);
11 return 0;
12 }
13
14 void my_print(int d1, int d2)
15 {
16 int *p1=&d1;
17 //int *p2 = NULL;//lvkai-
18 int *p2 = &d2;//lvkai+
19 cerr<<"first is:"<<*p1<<",second is:"<<*p2<<endl;
20 }
2)编译并运行这个程序,最终产生结果以下:
[root@lv-k test]# g++ main.cpp
[root@lv-k test]# ls
a.out main.cpp
[root@lv-k test]# ./a.out
first is:1,second is:2
这里,获得了咱们预期的结果。
另外,有个小技巧,若是对Makefile有些了解的话能够充分利用make的隐含规则来编译单个源文件的程序,
过程以下:
[root@lv-k test]# ls
main.cpp
[root@lv-k test]# make main
g++ main.cpp -o main
[root@lv-k test]# ls
main main.cpp
[root@lv-k test]# ./main
first is:1,second is:2
这里注意,make的目标参数必须是源文件"main.cpp"去掉后缀以后的"main",等价于"g++ main.cpp -o main",这样编译的命令比较简单。
[其它]
其它内容有待添加。
认真地工做而且思考,是最好的老师。在工做的过程当中思考本身所缺少的技术,以及学习他人的经验,才能在工做中有所收获。这篇文章原来是工做中个人一个同事加朋友的经验,我站在这样的经验的基础上,进行了这样总结。
一,什么是coredump
咱们常常听到你们说到程序core掉了,须要定位解决,这里说的大部分是指对应程序因为各类异常或者bug致使在运行过程当中异常退出或者停止,而且在知足必定条件下(这里为何说须要知足必定的条件呢?下面会分析)会产生一个叫作core的文件。
一般状况下,core文件会包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各类函数调用堆栈信息等,咱们能够理解为是程序工做当前状态存储生成第一个文件,许多的程序出错的时候都会产生一个core文件,经过工具分析这个文件,咱们能够定位到程序异常退出的时候对应的堆栈调用等信息,找出问题所在并进行及时解决。
二,coredump文件的存储位置
core文件默认的存储位置与对应的可执行程序在同一目录下,文件名是core,你们能够经过下面的命令看到core文件的存在位置:
cat /proc/sys/kernel/core_pattern
缺省值是core
注意:这里是指在进程当前工做目录的下建立。一般与程序在相同的路径下。但若是程序中调用了chdir函数,则有可能改变了当前工做目录。这时core文件建立在chdir指定的路径下。有好多程序崩溃了,咱们却找不到core文件放在什么位置。和chdir函数就有关系。固然程序崩溃了不必定都产生 core文件。
以下程序代码:则会把生成的core文件存储在/data/coredump/wd,而不是你们认为的跟可执行文件在同一目录。
经过下面的命令能够更改coredump文件的存储位置,若你但愿把core文件生成到/data/coredump/core目录下:
echo “/data/coredump/core”> /proc/sys/kernel/core_pattern
注意,这里当前用户必须具备对/proc/sys/kernel/core_pattern的写权限。
缺省状况下,内核在coredump时所产生的core文件放在与该程序相同的目录中,而且文件名固定为core。很显然,若是有多个程序产生core文件,或者同一个程序屡次崩溃,就会重复覆盖同一个core文件,所以咱们有必要对不一样程序生成的core文件进行分别命名。
咱们经过修改kernel的参数,能够指定内核所生成的coredump文件的文件名。例如,使用下面的命令使kernel生成名字为core.filename.pid格式的core dump文件:
echo “/data/coredump/core.%e.%p” >/proc/sys/kernel/core_pattern
这样配置后,产生的core文件中将带有崩溃的程序名、以及它的进程ID。上面的%e和%p会被替换成程序文件名以及进程ID。
若是在上述文件名中包含目录分隔符“/”,那么所生成的core文件将会被放到指定的目录中。 须要说明的是,在内核中还有一个与coredump相关的设置,就是/proc/sys/kernel/core_uses_pid。若是这个文件的内容被配置成1,那么即便core_pattern中没有设置%p,最后生成的core dump文件名仍会加上进程ID。
三,如何判断一个文件是coredump文件?
在类unix系统下,coredump文件自己主要的格式也是ELF格式,所以,咱们能够经过readelf命令进行判断。
能够看到ELF文件头的Type字段的类型是:CORE (Core file)
能够经过简单的file命令进行快速判断:
四,产生coredum的一些条件总结
1, 产生coredump的条件,首先须要确认当前会话的ulimit –c,若为0,则不会产生对应的coredump,须要进行修改和设置。
ulimit -c unlimited (能够产生coredump且不受大小限制)
若想甚至对应的字符大小,则能够指定:
ulimit –c [size]
能够看出,这里的size的单位是blocks,通常1block=512bytes
如:
ulimit –c 4 (注意,这里的size若是过小,则可能不会产生对应的core文件,笔者设置过ulimit –c 1的时候,系统并不生成core文件,并尝试了1,2,3均没法产生core,至少须要4才生成core文件)
但当前设置的ulimit只对当前会话有效,若想系统均有效,则须要进行以下设置:
Ø 在/etc/profile中加入如下一行,这将容许生成coredump文件
ulimit-c unlimited
Ø 在rc.local中加入如下一行,这将使程序崩溃时生成的coredump文件位于/data/coredump/目录下:
echo /data/coredump/core.%e.%p> /proc/sys/kernel/core_pattern
注意rc.local在不一样的环境,存储的目录可能不一样,susu下可能在/etc/rc.d/rc.local
更多ulimit的命令使用,能够参考:http://baike.baidu.com/view/4832100.htm
这些须要有root权限, 在ubuntu下每次从新打开中断都须要从新输入上面的ulimit命令, 来设置core大小为无限.
2, 当前用户,即执行对应程序的用户具备对写入core目录的写权限以及有足够的空间。
3, 几种不会产生core文件的状况说明:
The core file will not be generated if
(a) the process was set-user-ID and the current user is not the owner of the program file, or
(b) the process was set-group-ID and the current user is not the group owner of the file,
(c) the user does not have permission to write in the current working directory,
(d) the file already exists and the user does not have permission to write to it, or
(e) the file is too big (recall the RLIMIT_CORE limit in Section 7.11). The permissions of the core file (assuming that the file doesn't already exist) are usually user-read and user-write, although Mac OS X sets only user-read.
五,coredump产生的几种可能状况
形成程序coredump的缘由有不少,这里总结一些比较经常使用的经验吧:
1,内存访问越界
a) 因为使用错误的下标,致使数组访问越界。
b) 搜索字符串时,依靠字符串结束符来判断字符串是否结束,可是字符串没有正常的使用结束符。
c) 使用strcpy, strcat, sprintf, strcmp,strcasecmp等字符串操做函数,将目标字符串读/写爆。应该使用strncpy, strlcpy, strncat, strlcat, snprintf, strncmp, strncasecmp等函数防止读写越界。
2,多线程程序使用了线程不安全的函数。
应该使用下面这些可重入的函数,它们很容易被用错:
asctime_r(3c) gethostbyname_r(3n) getservbyname_r(3n)ctermid_r(3s) gethostent_r(3n) getservbyport_r(3n) ctime_r(3c) getlogin_r(3c)getservent_r(3n) fgetgrent_r(3c) getnetbyaddr_r(3n) getspent_r(3c)fgetpwent_r(3c) getnetbyname_r(3n) getspnam_r(3c) fgetspent_r(3c)getnetent_r(3n) gmtime_r(3c) gamma_r(3m) getnetgrent_r(3n) lgamma_r(3m) getauclassent_r(3)getprotobyname_r(3n) localtime_r(3c) getauclassnam_r(3) etprotobynumber_r(3n)nis_sperror_r(3n) getauevent_r(3) getprotoent_r(3n) rand_r(3c) getauevnam_r(3)getpwent_r(3c) readdir_r(3c) getauevnum_r(3) getpwnam_r(3c) strtok_r(3c) getgrent_r(3c)getpwuid_r(3c) tmpnam_r(3s) getgrgid_r(3c) getrpcbyname_r(3n) ttyname_r(3c)getgrnam_r(3c) getrpcbynumber_r(3n) gethostbyaddr_r(3n) getrpcent_r(3n)
3,多线程读写的数据未加锁保护。
对于会被多个线程同时访问的全局数据,应该注意加锁保护,不然很容易形成coredump
4,非法指针
a) 使用空指针
b) 随意使用指针转换。一个指向一段内存的指针,除非肯定这段内存原先就分配为某种结构或类型,或者这种结构或类型的数组,不然不要将它转换为这种结构或类型的指针,而应该将这段内存拷贝到一个这种结构或类型中,再访问这个结构或类型。这是由于若是这段内存的开始地址不是按照这种结构或类型对齐的,那么访问它时就很容易由于bus error而core dump。
5,堆栈溢出
不要使用大的局部变量(由于局部变量都分配在栈上),这样容易形成堆栈溢出,破坏系统的栈和堆结构,致使出现莫名其妙的错误。
六,利用gdb进行coredump的定位
其实分析coredump的工具备不少,如今大部分类unix系统都提供了分析coredump文件的工具,不过,咱们常常用到的工具是gdb。
这里咱们以程序为例子来讲明如何进行定位。
1, 段错误 – segmentfault
Ø 咱们写一段代码往受到系统保护的地址写内容。
Ø 按以下方式进行编译和执行,注意这里须要-g选项编译。
能够看到,当输入12的时候,系统提示段错误而且core dumped
Ø 咱们进入对应的core文件生成目录,优先确认是否core文件格式并启用gdb进行调试。
从红色方框截图能够看到,程序停止是由于信号11,且从bt(backtrace)命令(或者where)能够看到函数的调用栈,即程序执行到coremain.cpp的第5行,且里面调用scanf 函数,而该函数其实内部会调用_IO_vfscanf_internal()函数。
接下来咱们继续用gdb,进行调试对应的程序。
记住几个经常使用的gdb命令:
l(list) ,显示源代码,而且能够看到对应的行号;
b(break)x, x是行号,表示在对应的行号位置设置断点;
p(print)x, x是变量名,表示打印变量x的值
r(run), 表示继续执行到断点的位置
n(next),表示执行下一步
c(continue),表示继续执行
q(quit),表示退出gdb
启动gdb,注意该程序编译须要-g选项进行。
注: SIGSEGV 11 Core Invalid memoryreference
七,附注:
1, gdb的查看源码
显示源代码
GDB 能够打印出所调试程序的源代码,固然,在程序编译时必定要加上-g的参数,把源程序信息编译到执行文件中。否则就看不到源程序了。当程序停下来之后,GDB会报告程序停在了那个文件的第几行上。你能够用list命令来打印程序的源代码。仍是来看一看查看源代码的GDB命令吧。
list<linenum>
显示程序第linenum行的周围的源程序。
list<function>
显示函数名为function的函数的源程序。
list
显示当前行后面的源程序。
list -
显示当前行前面的源程序。
通常是打印当前行的上5行和下5行,若是显示函数是是上2行下8行,默认是10行,固然,你也能够定制显示的范围,使用下面命令能够设置一次显示源程序的行数。
setlistsize <count>
设置一次显示源代码的行数。
showlistsize
查看当前listsize的设置。
list命令还有下面的用法:
list<first>, <last>
显示从first行到last行之间的源代码。
list ,<last>
显示从当前行到last行之间的源代码。
list +
日后显示源代码。
通常来讲在list后面能够跟如下这些参数:
<linenum> 行号。
<+offset> 当前行号的正偏移量。
<-offset> 当前行号的负偏移量。
<filename:linenum> 哪一个文件的哪一行。
<function> 函数名。
<filename:function>哪一个文件中的哪一个函数。
<*address> 程序运行时的语句在内存中的地址。
2, 一些经常使用signal的含义
SIGABRT:调用abort函数时产生此信号。进程异常终止。
SIGBUS:指示一个实现定义的硬件故障。
SIGEMT:指示一个实现定义的硬件故障。EMT这一名字来自PDP-11的emulator trap 指令。
SIGFPE:此信号表示一个算术运算异常,例如除以0,浮点溢出等。
SIGILL:此信号指示进程已执行一条非法硬件指令。4.3BSD由abort函数产生此信号。SIGABRT如今被用于此。
SIGIOT:这指示一个实现定义的硬件故障。IOT这个名字来自于PDP-11对于输入/输出TRAP(input/outputTRAP)指令的缩写。系统V的早期版本,由abort函数产生此信号。SIGABRT如今被用于此。
SIGQUIT:当用户在终端上按退出键(通常采用Ctrl-/)时,产生此信号,并送至前台进
程组中的全部进程。此信号不只终止前台进程组(如SIGINT所作的那样),同时产生一个core文件。
SIGSEGV:指示进程进行了一次无效的存储访问。名字SEGV表示“段违例(segmentationviolation)”。
SIGSYS:指示一个无效的系统调用。因为某种未知缘由,进程执行了一条系统调用指令,但其指示系统调用类型的参数倒是无效的。
SIGTRAP:指示一个实现定义的硬件故障。此信号名来自于PDP-11的TRAP指令。
SIGXCPUSVR4和4.3+BSD支持资源限制的概念。若是进程超过了其软C P U时间限制,则产生此信号。
SIGXFSZ:若是进程超过了其软文件长度限制,则SVR4和4.3+BSD产生此信号。
3, Core_pattern的格式
能够在core_pattern模板中使用变量还不少,见下面的列表:
%% 单个%字符
%p 所dump进程的进程ID
%u 所dump进程的实际用户ID
%g 所dump进程的实际组ID
%s 致使本次core dump的信号
%t core dump的时间 (由1970年1月1日计起的秒数)
%h 主机名
%e 程序文件名