调试器工做原理

调试器工做原理(1):基础篇

本文是一系列探究调试器工做原理的文章的第一篇。我还不肯定这个系列须要包括多少篇文章以及它们所涵盖的主题,但我打算从基础知识开始提及。html

关于本文linux

我打算在这篇文章中介绍关于Linux下的调试器实现的主要组成部分——ptrace系统调用。本文中出现的代码都在32位的Ubuntu系统上开发。请注意,这里出现的代码是同平台紧密相关的,但移植到别的平台上应该不会太难。ios

动机c++

要想理解咱们究竟要作什么,试着想象一下调试器是如何工做的。调试器能够启动某些进程,而后对其进行调试,或者将本身自己关联到一个已存在的进程之上。它能够单步运行代码,设置断点而后运行程序,检查变量的值以及跟踪调用栈。许多调试器已经拥有了一些高级特性,好比执行表达式并在被调试进程的地址空间中调用函数,甚至能够直接修改进程的代码并观察修改后的程序行为。git

尽管现代的调试器都是复杂的大型程序,但使人惊讶的是构建调试器的基础确是如此的简单。调试器只用到了几个由操做系统以及编译器/连接器提供的基础服务,剩下的仅仅就是简单的编程问题了。(可查阅维基百科中关于这个词条的解释,做者是在反讽)程序员

Linux下的调试——ptracegithub

Linux下调试器拥有一个瑞士军刀般的工具,这就是ptrace系统调用。这是一个功能众多且至关复杂的工具,能容许一个进程控制另外一个进程的运行,并且能够监视和渗入到进程内部。ptrace自己须要一本中等篇幅的书才能对其进行完整的解释,这就是为何我只打算经过例子把重点放在它的实际用途上。让咱们继续深刻探寻。web

 

遍历进程的代码redis

我如今要写一个在“跟踪”模式下运行的进程的例子,这里咱们要单步遍历这个进程的代码——由CPU所执行的机器码(汇编指令)。我会在这里给出例子代码,解释每一个部分,本文结尾处你能够经过连接下载一份完整的C程序文件,能够自行编译执行并研究。从高层设计来讲,咱们要写一个程序,它产生一个子进程用来执行一个用户指定的命令,而父进程跟踪这个子进程。首先,main函数是这样的:shell

int main(int argc, char** argv)
{
    pid_t child_pid;
 
    if (argc < 2) {
        fprintf(stderr, "Expected a program name as argument\n");
        return -1;
    }
 
    child_pid = fork();
    if (child_pid == 0)
        run_target(argv[1]);
    else if (child_pid > 0)
        run_debugger(child_pid);
    else {
        perror("fork");
        return -1;
    }
 
    return 0;
}

 

代码至关简单,咱们经过fork产生一个新的子进程。随后的if语句块处理子进程(这里称为“目标进程”),而else if语句块处理父进程(这里称为“调试器”)。下面是目标进程:

这部分最有意思的地方在ptrace调用。ptrace的原型是(在sys/ptrace.h):

long ptrace(enum __ptrace_request request,  pid_t pid, void *addr,  void *data);
 

第一个参数是request,能够是预约义的以PTRACE_打头的常量值。第二个参数指定了进程id,第三以及第四个参数是地址和指向数据的指针,用来对内存作操做。上面代码段中的ptrace调用使用了PTRACE_TRACEME请求,这表示这个子进程要求操做系统内核容许它的父进程对其跟踪。这个请求在man手册中解释的很是清楚:

“代表这个进程由它的父进程来跟踪。任何发给这个进程的信号(除了SIGKILL)将致使该进程中止运行,而它的父进程会经过wait()得到通知。另外,该进程以后全部对exec()的调用都将使操做系统产生一个SIGTRAP信号发送给它,这让父进程有机会在新程序开始执行以前得到对子进程的控制权。若是不但愿由父进程来跟踪的话,那就不该该使用这个请求。(pid、addr、data被忽略)”

我已经把这个例子中咱们感兴趣的地方高亮显示了。注意,run_target在ptrace调用以后紧接着作的是经过execl来调用咱们指定的程序。这里就会像咱们高亮显示的部分所解释的那样,操做系统内核会在子进程开始执行execl中指定的程序以前中止该进程,并发送一个信号给父进程。

所以,是时候看看父进程须要作些什么了:

void run_debugger(pid_t child_pid)
{
    int wait_status;
    unsigned icounter = 0;
    procmsg("debugger started\n");
 
    /* Wait for child to stop on its first instruction */
    wait(&wait_status);
 
    while (WIFSTOPPED(wait_status)) {
        icounter++;
        /* Make the child execute another instruction */
        if (ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) < 0) {
            perror("ptrace");
            return;
        }
 
        /* Wait for child to stop on its next instruction */
        wait(&wait_status);
    }
 
    procmsg("the child executed %u instructions\n", icounter);
}

经过上面的代码咱们能够回顾一下,一旦子进程开始执行exec调用,它就会中止而后接收到一个SIGTRAP信号。父进程经过第一个wait调用正在等待这个事件发生。一旦子进程中止(若是子进程因为发送的信号而中止运行,WIFSTOPPED就返回true),父进程就去检查这个事件。

父进程接下来要作的是本文中最有意思的地方。父进程经过PTRACE_SINGLESTEP以及子进程的id号来调用ptrace。这么作是告诉操做系统——请从新启动子进程,但当子进程执行了下一条指令后再将其中止。而后父进程再次等待子进程的中止,整个循环继续得以执行。当从wait中获得的不是关于子进程中止的信号时,循环结束。在正常运行这个跟踪程序时,会获得子进程正常退出(WIFEXITED会返回true)的信号。

icounter会统计子进程执行的指令数量。所以咱们这个简单的例子实际上仍是作了点有用的事情——经过在命令行上指定一个程序名,咱们的例子会执行这个指定的程序,而后统计出从开始到结束该程序执行过的CPU指令总数。让咱们看看实际运行的状况。

 

实际测试

我编译了下面这个简单的程序,而后在咱们的跟踪程序下执行:

#include <stdio.h>
int main()
{
    printf(“Hello, world!\n”);
    return 0;
}

令我惊讶的是,咱们的跟踪程序运行了很长的时间而后报告显示一共有超过100000条指令获得了执行。仅仅只是一个简单的printf调用,为何会这样?答案很是有意思。默认状况下,Linux中的gcc编译器会动态连接到C运行时库。这意味着任何程序在运行时首先要作的事情是加载动态库。这须要不少代码实现——记住,咱们这个简单的跟踪程序会针对每一条被执行的指令计数,不只仅是main函数,而是整个进程。

所以,当我采用-static标志静态连接这个测试程序时(注意到可执行文件所以增长了500KB的大小,由于它静态连接了C运行时库),咱们的跟踪程序报告显示只有7000条左右的指令被执行了。这仍是很是多,但若是你了解到libc的初始化工做仍然先于main的执行,而清理工做会在main以后执行,那么这就彻底说得通了。并且,printf也是一个复杂的函数。

咱们仍是不知足于此,我但愿能看到一些可检测的东西,例如我能够从总体上看到每一条须要被执行的指令是什么。这一点咱们能够经过汇编代码来获得。所以我把这个“Hello,world”程序汇编(gcc -S)为以下的汇编码:

section    .text
    ; The _start symbol must be declared for the linker (ld)
    global _start
 
_start:
 
    ; Prepare arguments for the sys_write system call:
    ;   - eax: system call number (sys_write)
    ;   - ebx: file descriptor (stdout)
    ;   - ecx: pointer to string
    ;   - edx: string length
    mov    edx, len
    mov    ecx, msg
    mov    ebx, 1
    mov    eax, 4
 
    ; Execute the sys_write system call
    int    0x80
 
    ; Execute sys_exit
    mov    eax, 1
    int    0x80
 
section   .data
msg db    'Hello, world!', 0xa
len equ    $ - msg

这就足够了。如今跟踪程序会报告有7条指令获得了执行,我能够很容易地从汇编代码来验证这一点。

 

深刻指令流

汇编码程序得以让我为你们介绍ptrace的另外一个强大的功能——详细检查被跟踪进程的状态。下面是run_debugger函数的另外一个版本:

void run_debugger(pid_t child_pid)
{
    int wait_status;
    unsigned icounter = 0;
    procmsg("debugger started\n");
 
    /* Wait for child to stop on its first instruction */
    wait(&wait_status);
 
    while (WIFSTOPPED(wait_status)) {
        icounter++;
        struct user_regs_struct regs;
        ptrace(PTRACE_GETREGS, child_pid, 0, ®s);
        unsigned instr = ptrace(PTRACE_PEEKTEXT, child_pid, regs.eip, 0);
 
        procmsg("icounter = %u.  EIP = 0x%08x.  instr = 0x%08x\n",
                    icounter, regs.eip, instr);
 
        /* Make the child execute another instruction */
        if (ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) < 0) {
            perror("ptrace");
            return;
        }
 
        /* Wait for child to stop on its next instruction */
        wait(&wait_status);
    }
 
    procmsg("the child executed %u instructions\n", icounter);
}

同前个版本相比,惟一的不一样之处在于while循环的开始几行。这里有两个新的ptrace调用。第一个读取进程的寄存器值到一个结构体中。结构体user_regs_struct定义在sys/user.h中。这儿有个有趣的地方——若是你打开这个头文件看看,靠近文件顶端的地方有一条这样的注释:

/* 本文件的惟一目的是为GDB,且只为GDB所用。对于这个文件,不要看的太多。除了GDB之外不要用于任何其余目的,除非你知道你正在作什么。*/

如今,我不知道你是怎么想的,但我感受咱们正处于正确的跑道上。不管如何,回到咱们的例子上来。一旦咱们将全部的寄存器值获取到regs中,咱们就能够经过PTRACE_PEEKTEXT标志以及将regs.eip(x86架构上的扩展指令指针)作参数传入ptrace来调用。咱们所获得的就是指令。让咱们在汇编代码上运行这个新版的跟踪程序。

$ simple_tracer traced_helloworld
[5700] debugger started
[5701] target started. will run 'traced_helloworld'
[5700] icounter = 1.  EIP = 0x08048080.  instr = 0x00000eba
[5700] icounter = 2.  EIP = 0x08048085.  instr = 0x0490a0b9
[5700] icounter = 3.  EIP = 0x0804808a.  instr = 0x000001bb
[5700] icounter = 4.  EIP = 0x0804808f.  instr = 0x000004b8
[5700] icounter = 5.  EIP = 0x08048094.  instr = 0x01b880cd
Hello, world!
[5700] icounter = 6.  EIP = 0x08048096.  instr = 0x000001b8
[5700] icounter = 7.  EIP = 0x0804809b.  instr = 0x000080cd
[5700] the child executed 7 instructions
 

OK,因此如今除了icounter之外,咱们还能看到指令指针以及每一步的指令。如何验证这是否正确呢?能够经过在可执行文件上执行objdump –d来实现:

$ objdump -d traced_helloworld
 
traced_helloworld:     file format elf32-i386
 
Disassembly of section .text:
 
08048080 <.text>:
8048080:     ba 0e 00 00 00          mov    $0xe,%edx
8048085:     b9 a0 90 04 08          mov    $0x80490a0,%ecx
804808a:     bb 01 00 00 00          mov    $0x1,%ebx
804808f:     b8 04 00 00 00          mov    $0x4,%eax
8048094:     cd 80                   int    $0x80
8048096:     b8 01 00 00 00          mov    $0x1,%eax
804809b:     cd 80                   int    $0x80
 

用这份输出对比咱们的跟踪程序输出,应该很容易观察到相同的地方。

 

关联到运行中的进程上

你已经知道了调试器也能够关联到已经处于运行状态的进程上。看到这里,你应该不会感到惊讶,这也是经过ptrace来实现的。这须要经过PTRACE_ATTACH请求。这里我不会给出一段样例代码,由于经过咱们已经看到的代码,这应该很容易实现。基于教学的目的,这里采用的方法更为便捷(由于咱们能够在子进程刚启动时马上将它中止)。

 

代码

本文给出的这个简单的跟踪程序的完整代码(更高级一点,能够将具体指令打印出来)能够在这里找到。程序经过-Wall –pedantic –std=c99编译选项在4.4版的gcc上编译。

 

结论及下一步要作的

诚然,本文并无涵盖太多的内容——咱们离一个真正可用的调试器还差的很远。可是,我但愿这篇文章至少已经揭开了调试过程的神秘面纱。ptrace是一个拥有许多功能的系统调用,目前咱们只展现了其中少数几种功能。

可以单步执行代码是颇有用处的,但做用有限。以“Hello, world”为例,要到达main函数,须要先遍历好几千条初始化C运行时库的指令。这就不太方便了。咱们所但愿的理想方案是能够在main函数入口处设置一个断点,从断点处开始单步执行。下一篇文章中我将向您展现该如何实现断点机制。

 

参考文献

写做本文时我发现下面这些文章颇有帮助:

 

 

 

 

调试器工做原理(2):实现断点

 

本文是关于调试器工做原理探究系列的第二篇。在开始阅读本文前,请先确保你已经读过本系列的第一篇(基础篇)

本文的主要内容

这里我将说明调试器中的断点机制是如何实现的。断点机制是调试器的两大主要支柱之一 ——另外一个是在被调试进程的内存空间中查看变量的值。咱们已经在第一篇文章中稍微涉及到了一些监视被调试进程的知识,但断点机制仍然仍是个迷。阅读完本文以后,这将再也不是什么秘密了。

软中断

要在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信号。

这就是所有——真的!如今回顾一下本系列文章的第一篇,跟踪(调试器)进程能够得到全部其子进程(或者被关联到的进程)所获得信号的通知,如今你知道咱们该作什么了吧?

就是这样,再没有什么计算机体系结构方面的东东了,该写代码了。

手动设定断点

如今我要展现如何在程序中设定断点。用于这个示例的目标程序以下:

section    .text
    ; The _start symbol must be declared for the linker (ld)
    global _start
 
_start:
 
    ; Prepare arguments for the sys_write system call:
    ;   - eax: system call number (sys_write)
    ;   - ebx: file descriptor (stdout)
    ;   - ecx: pointer to string
    ;   - edx: string length
    mov     edx, len1
    mov     ecx, msg1
    mov     ebx, 1
    mov     eax, 4
 
    ; Execute the sys_write system call
    int     0x80
 
    ; Now print the other message
    mov     edx, len2
    mov     ecx, msg2
    mov     ebx, 1
    mov     eax, 4
    int     0x80
 
    ; Execute sys_exit
    mov     eax, 1
    int     0x80
 
section    .data
 
msg1    db      'Hello,', 0xa
len1    equ     $ - msg1
msg2    db      'world!', 0xa
len2    equ     $ - msg2

 

我如今使用的是汇编语言,这是为了不当使用C语言时涉及到的编译和符号的问题。上面列出的程序功能就是在一行中打印“Hello,”,而后在下一行中打印“world!”。这个例子与上一篇文章中用到的例子很类似。

我但愿设定的断点位置应该在第一条打印以后,但刚好在第二条打印以前。咱们就让断点打在第一个int 0x80指令以后吧,也就是mov edx, len2。首先,我须要知道这条指令对应的地址是什么。运行objdump –d:

 

traced_printer2:     file format elf32-i386
 
Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000033  08048080  08048080  00000080  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         0000000e  080490b4  080490b4  000000b4  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 
Disassembly of section .text:
 
08048080 <.text>:
8048080:     ba 07 00 00 00          mov    $0x7,%edx
8048085:     b9 b4 90 04 08          mov    $0x80490b4,%ecx
804808a:     bb 01 00 00 00          mov    $0x1,%ebx
804808f:     b8 04 00 00 00          mov    $0x4,%eax
8048094:     cd 80                   int    $0x80
8048096:     ba 07 00 00 00          mov    $0x7,%edx
804809b:     b9 bb 90 04 08          mov    $0x80490bb,%ecx
80480a0:     bb 01 00 00 00          mov    $0x1,%ebx
80480a5:     b8 04 00 00 00          mov    $0x4,%eax
80480aa:     cd 80                   int    $0x80
80480ac:     b8 01 00 00 00          mov    $0x1,%eax
80480b1:     cd 80                   int    $0x80

 

经过上面的输出,咱们知道要设定的断点地址是0x8048096。等等,真正的调试器不是像这样工做的,对吧?真正的调试器能够根据代码行数或者函数名称来设定断点,而不是基于什么内存地址吧?很是正确。可是咱们离那个标准还差的远——若是要像真正的调试器那样设定断点,咱们还须要涵盖符号表以及调试信息方面的知识,这须要用另外一篇文章来讲明。至于如今,咱们还必须得经过内存地址来设定断点。

看到这里我真的很想再扯一点题外话,因此你有两个选择。若是你真的对于为何地址是0x8048096,以及这表明什么意思很是感兴趣的话,接着看下一节。若是你对此毫无兴趣,只是想看看怎么设定断点,能够略过这一部分。

题外话——进程地址空间以及入口点

坦白的说,0x8048096自己并无太大意义,这只不过是相对可执行镜像的代码段(text section)开始处的一个偏移量。若是你仔细看看前面objdump出来的结果,你会发现代码段的起始位置是0x08048080。这告诉了操做系统要将代码段映射到进程虚拟地址空间的这个位置上。在Linux上,这些地址能够是绝对地址(好比,有的可执行镜像加载到内存中时是不可重定位的),由于在虚拟内存系统中,每一个进程都有本身独立的内存空间,并把整个32位的地址空间都看作是属于本身的(称为线性地址)。

若是咱们经过readelf工具来检查可执行文件的ELF头,咱们将获得以下输出:

$ readelf -h traced_printer2
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                                 ELF32
  Data:                                  2's complement, little endian
  Version:                               1 (current)
  OS/ABI:                                UNIX - System V
  ABI Version:                           0
  Type:                                  EXEC (Executable file)
  Machine:                               Intel 80386
  Version:                               0x1
  Entry point address:                   0x8048080
  Start of program headers:              52 (bytes into file)
  Start of section headers:              220 (bytes into file)
  Flags:                                 0x0
  Size of this header:                   52 (bytes)
  Size of program headers:               32 (bytes)
  Number of program headers:             2
  Size of section headers:               40 (bytes)
  Number of section headers:             4
  Section header string table index:     3

 

注意,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一个子进程,而后对其跟踪)。不管如何,本文结尾处会给出完整源码的连接。

/* Obtain and show child's instruction pointer */
ptrace(PTRACE_GETREGS, child_pid, 0, ®s);
procmsg("Child started. EIP = 0x%08x\n", regs.eip);
 
/* Look at the word at the address we're interested in */
unsigned addr = 0x8048096;
unsigned data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);
procmsg("Original data at 0x%08x: 0x%08x\n", addr, data);

 

这里调试器从被跟踪进程中获取到指令指针,而后检查当前位于地址0x8048096处的字长内容。运行本文前面列出的汇编码程序,将打印出:

[13028] Child started. EIP = 0x08048080
[13028] Original data at 0x08048096: 0x000007ba

 

目前为止一切顺利,下一步:

/* Write the trap instruction 'int 3' into the address */
unsigned data_with_trap = (data & 0xFFFFFF00) | 0xCC;
ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data_with_trap);
 
/* See what's there again... */
unsigned readback_data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);
procmsg("After trap, data at 0x%08x: 0x%08x\n", addr, readback_data);

 

注意看咱们是如何将int 3指令插入到目标地址上的。这部分代码将打印出:

[13028] After trap, data at 0x08048096: 0x000007cc
 
再一次如同预计的那样——0xba被0xcc取代了。调试器如今运行子进程而后等待子进程在断点处中止住。
/* Let the child run to the breakpoint and wait for it to
** reach it
*/
ptrace(PTRACE_CONT, child_pid, 0, 0);
 
wait(&wait_status);
if (WIFSTOPPED(wait_status)) {
    procmsg("Child got a signal: %s\n", strsignal(WSTOPSIG(wait_status)));
}
else {
    perror("wait");
    return;
}
 
/* See where the child is now */
ptrace(PTRACE_GETREGS, child_pid, 0, ®s);
procmsg("Child stopped at EIP = 0x%08x\n", regs.eip);
 
 
这段代码打印出:
Hello,
[13028] Child got a signal: Trace/breakpoint trap
[13028] Child stopped at EIP = 0x08048097
 
 

注意,“Hello,”在断点以前打印出来了——同咱们计划的同样。同时咱们发现子进程已经中止运行了——就在这个单字节的陷阱指令执行以后。

/* Remove the breakpoint by restoring the previous data
** at the target address, and unwind the EIP back by 1 to
** let the CPU execute the original instruction that was
** there.
*/
ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data);
regs.eip -= 1;
ptrace(PTRACE_SETREGS, child_pid, 0, ®s);
 
/* The child can continue running now */
ptrace(PTRACE_CONT, child_pid, 0, 0);
 
 

这会使子进程打印出“world!”而后退出,同以前计划的同样。

注意,咱们这里并无从新加载断点。这能够在单步模式下执行,而后将陷阱指令加回去,再作PTRACE_CONT就能够了。本文稍后介绍的debug库实现了这个功能。

更多关于int 3指令

如今是回过头来讲说int 3指令的好机会,以及解释一下Intel手册中对这条指令的奇怪说明。

“这个单字节形式很是有价值,由于这样能够经过一个断点来替换掉任何指令的第一个字节,包括其它的单字节指令也是同样,而不会覆盖到其它的操做码。”

x86架构上的int指令占用2个字节——0xcd加上中断号。int 3的二进制形式能够被编码为cd 03,但这里有一个特殊的单字节指令0xcc以一样的做用而被保留。为何要这样作呢?由于这容许咱们在插入一个断点时覆盖到的指令不会多于一条。这很重要,考虑下面的示例代码:

.. some code ..
    jz    foo
    dec   eax
foo:
    call  bar
    .. some code ..
 
 

假设咱们要在dec eax上设定断点。这刚好是条单字节指令(操做码是0x48)。若是替换为断点的指令长度超过1字节,咱们就被迫改写了接下来的下一条指令(call),这可能会产生一些彻底非法的行为。考虑一下条件分支jz foo,这时进程可能不会在dec eax处中止下来(咱们在此设定的断点,改写了原来的指令),而是直接执行了后面的非法指令。

经过对int 3指令采用一个特殊的单字节编码就能解决这个问题。由于x86架构上指令最短的长度就是1字节,这样咱们能够保证只有咱们但愿中止的那条指令被修改。

封装细节

前面几节中的示例代码展现了许多底层的细节,这些能够很容易地经过API进行封装。我已经作了一些封装,使其成为一个小型的调试库——debuglib。代码在本文末尾处能够下载。这里我只想介绍下它的用法,咱们要开始调试C程序了。

跟踪C程序

目前为止为了简单起见我把重点放在对汇编程序的跟踪上了。如今升一级来看看咱们该如何跟踪一个C程序。

其实事情并无很大的不一样——只是如今有点难以找到放置断点的位置。考虑以下这个简单的C程序:

#include <stdio.h>
 
void do_stuff()
{
    printf("Hello, ");
}
 
int main()
{
    for (int i = 0; i < 4; ++i)
        do_stuff();
    printf("world!\n");
    return 0;
}
 
 
假设我想在do_stuff的入口处设置一个断点。我将请出咱们的老朋友objdump来反汇编可执行文件,但获得的输出太多。其实,查看text段不太管用,由于这里面包含了大量的初始化C运行时库的代码,我目前对此并不感兴趣。因此,咱们只须要在dump出来的结果里看do_stuff部分就行了。
080483e4 <do_stuff>:
80483e4:     55                      push   %ebp
80483e5:     89 e5                   mov    %esp,%ebp
80483e7:     83 ec 18                sub    $0x18,%esp
80483ea:     c7 04 24 f0 84 04 08    movl   $0x80484f0,(%esp)
80483f1:     e8 22 ff ff ff          call   8048318 <puts@plt>
80483f6:     c9                      leave
80483f7:     c3                      ret
 
 
好的,因此咱们应该把断点设定在0x080483e4上,这是do_stuff的第一条指令。另外,因为这个函数是在循环体中调用的,咱们但愿在循环所有结束前保留断点,让程序能够在每一轮循环中都在断点处停下。我将使用debuglib来简化代码编写。这里是完整的调试器函数:
void run_debugger(pid_t child_pid)
{
    procmsg("debugger started\n");
 
    /* Wait for child to stop on its first instruction */
    wait(0);
    procmsg("child now at EIP = 0x%08x\n", get_child_eip(child_pid));
 
    /* Create breakpoint and run to it*/
    debug_breakpoint* bp = create_breakpoint(child_pid, (void*)0x080483e4);
    procmsg("breakpoint created\n");
    ptrace(PTRACE_CONT, child_pid, 0, 0);
    wait(0);
 
    /* Loop as long as the child didn't exit */
    while (1) {
        /* The child is stopped at a breakpoint here. Resume its
        ** execution until it either exits or hits the
        ** breakpoint again.
        */
        procmsg("child stopped at breakpoint. EIP = 0x%08X\n", get_child_eip(child_pid));
        procmsg("resuming\n");
        int rc = resume_from_breakpoint(child_pid, bp);
 
        if (rc == 0) {
            procmsg("child exited\n");
            break;
        }
        else if (rc == 1) {
            continue;
        }
        else {
            procmsg("unexpected: %d\n", rc);
            break;
        }
    }
 
    cleanup_breakpoint(bp);
}
 
 
咱们不用手动修改EIP指针以及目标进程的内存空间,咱们只须要经过create_breakpoint, resume_from_breakpoint以及cleanup_breakpoint来操做就能够了。咱们来看看当跟踪这个简单的C程序后的打印输出:
 
$ bp_use_lib traced_c_loop
[13363] debugger started
[13364] target started. will run 'traced_c_loop'
[13363] child now at EIP = 0x00a37850
[13363] breakpoint created
[13363] child stopped at breakpoint. EIP = 0x080483E5
[13363] resuming
Hello,
[13363] child stopped at breakpoint. EIP = 0x080483E5
[13363] resuming
Hello,
[13363] child stopped at breakpoint. EIP = 0x080483E5
[13363] resuming
Hello,
[13363] child stopped at breakpoint. EIP = 0x080483E5
[13363] resuming
Hello,
world!
[13363] child exited
 

跟预计的状况如出一辙!

代码

这里是完整的源码。在文件夹中你会发现:

debuglib.h以及debuglib.c——封装了调试器的一些内部工做。

bp_manual.c —— 本文一开始介绍的“手动”式设定断点。用到了debuglib库中的一些样板代码。

bp_use_lib.c—— 大部分代码用到了debuglib,这就是本文中用于说明跟踪一个C程序中的循环的示例代码。

结论及下一步要作的

咱们已经涵盖了如何在调试器中实现断点机制。尽管实现细节根据操做系统的不一样而有所区别,但只要你使用的是x86架构的处理器,那么一切变化都基于相同的主题——在咱们但愿中止的指令上将其替换为int 3。

我敢确定,有些读者就像我同样,对于经过指定原始地址来设定断点的作法不会感到很激动。咱们更但愿说“在do_stuff上停住”,甚至是“在do_stuff的这一行上停住”,而后调试器就能照办。在下一篇文章中,我将向您展现这是如何作到的。

 

 

 

 

 

调试器工做原理(3):调试信息

本文是调试器工做原理探究系列的第三篇,在阅读前请先确保已经读过本系列的第一第二

本篇主要内容

在本文中我将向你们解释关于调试器是如何在机器码中寻找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的可执行文件来作下实验。

#include <stdio.h>
 
void do_stuff(int my_arg)
{
    int my_local = my_arg + 2;
    int i;
 
    for (i = 0; i < my_local; ++i)
        printf("i = %d\n", i);
}
 
int main()
{
    do_stuff(2);
    return 0;
}
 
 
经过objdump –h导出ELF可执行文件中的段头信息,咱们注意到其中有几个段的名字是以.debug_打头的,这些就是DWARF格式的调试段:
26 .debug_aranges 00000020  00000000  00000000  00001037
                 CONTENTS, READONLY, DEBUGGING
27 .debug_pubnames 00000028  00000000  00000000  00001057
                 CONTENTS, READONLY, DEBUGGING
28 .debug_info   000000cc  00000000  00000000  0000107f
                 CONTENTS, READONLY, DEBUGGING
29 .debug_abbrev 0000008a  00000000  00000000  0000114b
                 CONTENTS, READONLY, DEBUGGING
30 .debug_line   0000006b  00000000  00000000  000011d5
                 CONTENTS, READONLY, DEBUGGING
31 .debug_frame  00000044  00000000  00000000  00001240
                 CONTENTS, READONLY, DEBUGGING
32 .debug_str    000000ae  00000000  00000000  00001284
                 CONTENTS, READONLY, DEBUGGING
33 .debug_loc    00000058  00000000  00000000  00001332
                 CONTENTS, READONLY, DEBUGGING
 
 

每行的第一个数字表示每一个段的大小,而最后一个数字表示距离ELF文件开始处的偏移量。调试器就是利用这个信息来从可执行文件中读取相关的段信息。如今,让咱们经过一些实际的例子来看看如何在DWARF中找寻有用的调试信息。

定位函数

当咱们在调试程序时,一个最为基本的操做就是在某些函数中设置断点,指望调试器能在函数入口处将程序断下。要完成这个功能,调试器必须具备某种可以从源代码中的函数名称到机器码中该函数的起始指令间相映射的能力。

这个信息能够经过从DWARF中的.debug_info段获取到。在咱们继续以前,先说点背景知识。DWARF的基本描述实体被称为调试信息表项(Debugging Information Entry —— DIE),每一个DIE有一个标签——包含它的类型,以及一组属性。各个DIE之间经过兄弟和孩子结点互相连接,属性值能够指向其余的DIE。

咱们运行

objdumpdwarf=info traceprog2
 
 
获得的输出很是长,对于这个例子,咱们只用关注这几行就能够了:
<1><71>: Abbrev Number: 5 (DW_TAG_subprogram)
    <72>   DW_AT_external    : 1
    <73>   DW_AT_name        : (...): do_stuff
    <77>   DW_AT_decl_file   : 1
    <78>   DW_AT_decl_line   : 4
    <79>   DW_AT_prototyped  : 1
    <7a>   DW_AT_low_pc      : 0x8048604
    <7e>   DW_AT_high_pc     : 0x804863e
    <82>   DW_AT_frame_base  : 0x0      (location list)
    <86>   DW_AT_sibling     : <0xb3>
 
<1><b3>: Abbrev Number: 9 (DW_TAG_subprogram)
    <b4>   DW_AT_external    : 1
    <b5>   DW_AT_name        : (...): main
    <b9>   DW_AT_decl_file   : 1
    <ba>   DW_AT_decl_line   : 14
    <bb>   DW_AT_type        : <0x4b>
    <bf>   DW_AT_low_pc      : 0x804863e
    <c3>   DW_AT_high_pc     : 0x804865a
<c7>   DW_AT_frame_base  : 0x2c     (location list)
 
 
这里有两个被标记为DW_TAG_subprogram的DIE,从DWARF的角度看这就是函数。注意,这里do_stuff和main都各有一个表项。这里有许多有趣的属性,但咱们感兴趣的是DW_AT_low_pc。这就是函数起始处的程序计数器的值(x86下的EIP)。注意,对于do_stuff来讲,这个值是0x8048604。如今让咱们看看,经过objdump –d作反汇编后这个地址是什么:
 
08048604 <do_stuff>:
8048604:       55           push   ebp
8048605:       89 e5        mov    ebp,esp
8048607:       83 ec 28     sub    esp,0x28
804860a:       8b 45 08     mov    eax,DWORD PTR [ebp+0x8]
804860d:       83 c0 02     add    eax,0x2
8048610:       89 45 f4     mov    DWORD PTR [ebp-0xc],eax
8048613:       c7 45 (...)  mov    DWORD PTR [ebp-0x10],0x0
804861a:       eb 18        jmp    8048634 <do_stuff+0x30>
804861c:       b8 20 (...)  mov    eax,0x8048720
8048621:       8b 55 f0     mov    edx,DWORD PTR [ebp-0x10]
8048624:       89 54 24 04  mov    DWORD PTR [esp+0x4],edx
8048628:       89 04 24     mov    DWORD PTR [esp],eax
804862b:       e8 04 (...)  call   8048534 <printf@plt>
8048630:       83 45 f0 01  add    DWORD PTR [ebp-0x10],0x1
8048634:       8b 45 f0     mov    eax,DWORD PTR [ebp-0x10]
8048637:       3b 45 f4     cmp    eax,DWORD PTR [ebp-0xc]
804863a:       7c e0        jl     804861c <do_stuff+0x18>
804863c:       c9           leave
804863d:       c3           ret
 
 

没错,从反汇编结果来看0x8048604确实就是函数do_stuff的起始地址。所以,这里调试器就同函数和它们在可执行文件中的位置确立了映射关系。

定位变量

假设咱们确实在do_stuff中的断点处停了下来。咱们但愿调试器可以告诉咱们my_local变量的值,调试器怎么知道去哪里找到相关的信息呢?这可比定位函数要难多了,由于变量能够在全局数据区,能够在栈上,甚至是在寄存器中。另外,具备相同名称的变量在不一样的词法做用域中可能有不一样的值。调试信息必须可以反映出全部这些变化,而DWARF确实能作到这些。

我不会涵盖全部的可能状况,做为例子,我将只展现调试器如何在do_stuff函数中定位到变量my_local。咱们从.debug_info段开始,再次看看do_stuff这一项,这一次咱们也看看其余的子项:

<1><71>: Abbrev Number: 5 (DW_TAG_subprogram)
    <72>   DW_AT_external    : 1
    <73>   DW_AT_name        : (...): do_stuff
    <77>   DW_AT_decl_file   : 1
    <78>   DW_AT_decl_line   : 4
    <79>   DW_AT_prototyped  : 1
    <7a>   DW_AT_low_pc      : 0x8048604
    <7e>   DW_AT_high_pc     : 0x804863e
    <82>   DW_AT_frame_base  : 0x0      (location list)
    <86>   DW_AT_sibling     : <0xb3>
<2><8a>: Abbrev Number: 6 (DW_TAG_formal_parameter)
    <8b>   DW_AT_name        : (...): my_arg
    <8f>   DW_AT_decl_file   : 1
    <90>   DW_AT_decl_line   : 4
    <91>   DW_AT_type        : <0x4b>
    <95>   DW_AT_location    : (...)       (DW_OP_fbreg: 0)
<2><98>: Abbrev Number: 7 (DW_TAG_variable)
    <99>   DW_AT_name        : (...): my_local
    <9d>   DW_AT_decl_file   : 1
    <9e>   DW_AT_decl_line   : 6
    <9f>   DW_AT_type        : <0x4b>
    <a3>   DW_AT_location    : (...)      (DW_OP_fbreg: -20)
<2><a6>: Abbrev Number: 8 (DW_TAG_variable)
    <a7>   DW_AT_name        : i
    <a9>   DW_AT_decl_file   : 1
    <aa>   DW_AT_decl_line   : 7
    <ab>   DW_AT_type        : <0x4b>
<af>   DW_AT_location    : (...)      (DW_OP_fbreg: -24)
 
 

注意每个表项中第一个尖括号里的数字,这表示嵌套层次——在这个例子中带有<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的输出:

$ objdump --dwarf=loc tracedprog2
 
tracedprog2:     file format elf32-i386
 
Contents of the .debug_loc section:
 
    Offset   Begin    End      Expression
    00000000 08048604 08048605 (DW_OP_breg4: 4 )
    00000000 08048605 08048607 (DW_OP_breg4: 8 )
    00000000 08048607 0804863e (DW_OP_breg5: 8 )
    00000000 <End of list>
    0000002c 0804863e 0804863f (DW_OP_breg4: 4 )
    0000002c 0804863f 08048641 (DW_OP_breg4: 8 )
    0000002c 08048641 0804865a (DW_OP_breg5: 8 )
0000002c <End of list>
 
 

关于位置信息,咱们这里感兴趣的就是第一个。对于调试器可能定位到的每个地址,它都会指定当前栈帧到变量间的偏移量,而这个偏移就是经过寄存器来计算的。对于x86体系结构,bpreg4表明esp寄存器,而bpreg5表明ebp寄存器。

让咱们再看看do_stuff的开头几条指令:

08048604 <do_stuff>:
8048604:       55          push   ebp
8048605:       89 e5       mov    ebp,esp
8048607:       83 ec 28    sub    esp,0x28
804860a:       8b 45 08    mov    eax,DWORD PTR [ebp+0x8]
804860d:       83 c0 02    add    eax,0x2
8048610:       89 45 f4    mov    DWORD PTR [ebp-0xc],eax

 

注意,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段中,能够按照可读的形式进行解读:

$ objdump --dwarf=decodedline tracedprog2
 
tracedprog2:     file format elf32-i386
 
Decoded dump of debug contents of section .debug_line:
 
CU: /home/eliben/eli/eliben-code/debugger/tracedprog2.c:
File name           Line number    Starting address
tracedprog2.c                5           0x8048604
tracedprog2.c                6           0x804860a
tracedprog2.c                9           0x8048613
tracedprog2.c               10           0x804861c
tracedprog2.c                9           0x8048630
tracedprog2.c               11           0x804863c
tracedprog2.c               15           0x804863e
tracedprog2.c               16           0x8048647
tracedprog2.c               17           0x8048653
tracedprog2.c               18           0x8048658

 

不难看出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程序产生的输出:

$ dwarf_get_func_addr tracedprog2
DW_TAG_subprogram: 'do_stuff'
low pc  : 0x08048604
high pc : 0x0804863e
DW_TAG_subprogram: 'main'
low pc  : 0x0804863e
high pc : 0x0804865a

 

libdwarf的文档很是好(见本文的参考文献部分),花点时间看看,对于本文中提到的DWARF段信息你处理起来就应该没什么问题了。

结论及下一步

调试信息只是一个简单的概念,具体实现细节可能至关复杂。但最终咱们知道了调试器是如何从可执行文件中找出同源代码之间的关系。有了调试信息在手,调试器为用户所能识别的源代码和数据结构同可执行文件之间架起了一座桥。

本文加上以前的两篇文章总结了调试器内部的工做原理。经过这一系列文章,再加上一点编程工做就应该能够在Linux下建立一个具备基本功能的调试器。

至于下一步,我还不肯定。也许我会就此终结这一系列文章,也许我会再写一些高级主题好比backtrace,甚至Windows系统上的调试。读者们也能够为从此这一系列文章提供意见和想法。不要客气,请随意在评论栏或经过Email给我提些建议吧。

 

 

 

几个主要软件调试方法及调试原则

调试(Debug)

 

软件调试是在进行了成功的测试以后才开始的工做,它与软件测试不一样,调试的任务是进一步诊断和改正程序中潜在的错误。

 

调试活动由两部分组成:

u  肯定程序中可疑错误的确切性质和位置

u  对程序(设计,编码)进行修改,排除这个错误

 

调试工做是一个具备很强技巧性的工做

 

软件运行失效或出现问题,每每只是潜在错误的外部表现,而外部表现与内在缘由之间经常没有明显的联系,若是要找出真正的缘由,排除潜在的错误,不是一件易事。

 

能够说,调试是经过现象,找出缘由的一个思惟分析的过程。

 

调试步骤:

(1)      从错误的外部表现形式入手,肯定程序中出错位置

(2)      研究有关部分的程序,找出错误的内在缘由

(3)      修改设计代码,以排除这个错误

(4)      重复进行暴露了这个错误的原始测试或某些有关测试。

 

 

从技术角度来看查找错误的难度在于:

 

u  现象与缘由所处的位置可能相距甚远

u  当其余错误获得纠正时,这一错误所表现出的现象可能会暂时消失,但并为实际排除

u  现象其实是由一些非错误缘由(例如,舍入不精确)引发的

u  现象多是因为一些不容易发现的人为错误引发的

u  错误是因为时序问题引发的,与处理过程无关

u  现象是因为难于精确再现的输入状态(例如,实时应用中输入顺序不肯定)引发

u  现象多是周期出现的,在软,硬件结合的嵌入式系统中经常遇到

 

 

几种主要的调试方法

 

调试的关键在于推断程序内部的错误位置及缘由,能够采用如下方法:

 

强行排错

 

这种调试方法目前使用较多,效率较低,它不须要过多的思考,比较省脑筋。例如:

经过内存所有打印来调试,在这大量的数据中寻找出错的位置。

u  在程序特定位置设置打印语句,把打印语句插在出错的源程序的各个关键变量改变部位,重要分支部位,子程序调用部位,跟踪程序的执行,监视重要变量的变化

自动调用工具,利用某些程序语言的调试功能或专门的交互式调试工具,分析程序的动态过程,而没必要修改程序。

 

应用以上任一种方法以前,都应当对错误的征兆进行全面完全的分析,得出对出错位置及错误性质的推测,再使用一种适当的调试方法来检验推测的正确性。

 

回溯法调试

 

这是在小程序中经常使用的一种有效的调试方法,一旦发现了错误,人们先分析错误的征兆,肯定最早发现“症状“的位置

而后,人工沿程序的控制流程,向回追踪源程序代码,直到找到错误根源或肯定错误产生的范围,

例如,程序中发现错误处是某个打印语句,经过输出值可推断程序在这一点上变量的值,再从这一点出发,回溯程序的执行过程,反复思考:“若是程序在这一点上的状态(变量的值)是这样,那么程序在上一点的状态必定是这样···“直到找到错误所在。

 

 

 

概括法调试

 

概括法是一种从特殊推断通常的系统化思考方法,概括法调试的基本思想是:从一些线索(错误征兆)着手,经过分析它们之间的关系来找出错误

 

u  收集有关的数据,列出全部已知的测试用例和程序执行结果,看哪些输入数据的运行结果是正确的,哪些输入数据的运行通过是有错误的

u  组织数据

因为概括法是从特殊到通常的推断过程,因此须要组织整理数据,以发现规律

 

常以3W1H形式组织可用的数据

“What“列出通常现象

“Where“说明发现现象的地点

“When“列出现象发生时全部已知状况

“How“说明现象的范围和量级


“Yes“描述出现错误的3W1H;

“No“做为比较,描述了没有错误的3W1H,经过分析找出矛盾来

 

提出假设

分析线索之间的关系,利用在线索结构中观察到的矛盾现象,设计一个或多个关于出错缘由的假设,若是一个假设也提不出来,概括过程就须要收集更多的数据,此时,应当再设计与执行一些测试用例,以得到更多的数据。

 

证实假设

把假设与原始线索或数据进行比较,若它能彻底解释一切现象,则假设获得证实,不然,认为假设不合理,或不彻底,或是存在多个错误,以至只能消除部分错误

 

 

演绎法调试

 

演绎法是一种从通常原理或前提出发,通过排除和精华的过程来推导出结论的思考方法,演绎法排错是测试人员首先根据已有的测试用例,设想及枚举出全部可能出错的缘由做为假设,而后再用原始测试数据或新的测试,从中逐个排除不可能正确的假设,最后,再用测试数据验证余下的假设确是出错的缘由。

 

列举全部可能出错缘由的假设,把全部可能的错误缘由列成表,经过它们,能够组织,分析现有数据

利用已有的测试数据,排除不正确的假设

仔细分析已有的数据,寻找矛盾,力求排除前一步列出全部缘由,若是全部缘由都被排除了,则须要补充一些数据(测试用例),以创建新的假设。

 

改进余下的假设

利用已知的线索,进一步改进余下的假设,使之更具体化,以即可以精确地肯定出错位置

 

证实余下的假设

 

 

调试原则

n  在调试方面,许多原则本质上是心理学方面的问题,调试由两部分组成,调试原则也分红两组。

肯定错误的性质和位置的原则

u  用头脑去分析思考与错误征兆有关的信息

u  避开死胡同

u  只把调试工具当作辅助手段来使用,利用调试工具,能够帮助思考,但不能代替思考

u  避免用试探法,最多只能把它当作最后手段

 

修改错误的原则

 

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, &regs);
   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. 二进制调试器
  • 4. 基本的符号调试器(源码调试器)
  • 5. 命令行符号调试器
  • 6. 全屏文本模式调试器
  • 7. 图形用户接口调试器
  • 8. 集成开发环境调试器

调试器的设计和开发要遵循四个关键的原则:

  • 1. 在开发过程当中,不能改变被调试程序的行为;
  • 2. 提供真实可靠的调试信息;
  • 3. 提供详细的信息,是调试人员知道他们调试到代码的哪一行而且知道他们是怎么到达的;
  • 4. 很是不幸的是,咱们使用的调试老是不能知足咱们的需求。

按照划分的标准不一样,调试器主要分为一下几类:

  • 1. 源码调试器与机器码调试器
  • 2. 单独的调试器与集成开发环境的调试器
  • 3. 第四代语言调试器与第三代语言调试器
  • 4. 操做系统内核调试器与应用程序调试器
  • 5. 利用处理器提供的功能的调试器与利用自行仿真处理器进行调试的调试器

 

调试器的架构

调试器之间的区别更多的是体如今他们展示给用户的窗口。至于底层结构都是很相近的。下图展现了调试器的整体架构:

调试器内核

调试器服务于全部的调试器视图。包括进程控制,执行引擎,表达式计算,符号表管理四部分。

操做系统接口

调试器内核为了访问被调试程序,必须使用操做系统提供的一系列例程。 

 

 

硬件调试功能

调试器控制被调试程序的能力主要是依靠硬件支持和操做系统的调试机制。调试器须要最少三种的硬件功能的支持:

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  进入函数

 

查看程序的上下文信息

通常要查找程序的上下文信息主要有如下几种方法:

  • 源代码窗口

经过源代码查看程序执行到代码的那一部分

  • 程序堆栈

程序堆栈是由硬件,操做系统和编译器共同支持的:

硬件:    提供堆栈指针;

操做系统:为每一个进程创建堆栈空间,并管理堆栈。一旦堆栈溢出,而产生一个错误;

  • 汇编级调试:反汇编,查看寄存器,查看内存

 

 

 

 

结合程序崩溃后的core文件分析bug

引言

    在《I/O的效率比较》中,咱们在修改图1程序的BUF_SIZE为8388608时,运行程序出现崩溃,以下图1:
    
    图1. 段错误
    通常而言,致使程序段错误的缘由以下:
  • 内存访问出错,这类问题的典型表明就是数组越界。
  • 非法内存访问,出现这类问题主要是程序试图访问内核段内存而产生的错误。
  • 栈溢出, Linux默认给一个进程分配的栈空间大小为8M,所以你的数组开得过大的话会出现这种问题。
    首先咱们先看一下系统默认分配的资源:
$ 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
    能够看到默认分配的栈大小为8M。而恰好咱们的代码里的栈大小调到了8M,所以出现了段错误。
    那么有没有一种更直接明了的方法来识别和分析应用程序崩溃产生的bug呢? 有,那就是经过程序崩溃后产生的core文件。

core文件

何为core文件.

    core dump又叫内核转储, 在Unix系统中,核心映像(core image)就是“进程”执行当时的内存内容,当进程发生错误或收到“信号”而终止执行时,系统会将核心映像写入一个文件,以做为调试之用,这就是所谓的核心转储(core dump)。而core文件通常产生在进程的当前工做目录下。
    因此core文件中只是程序的内存映像, 若是在编译时加入调试信息的话,那么还会有调试信息。

如何产生core文件

    咱们运行了a.out程序出现了“段错误”,但没有产生core文件。这是由于系统默认core文件的大小为0,因此没有建立。能够用ulimit命令查看和修改core文件的大小。 
$ ulimit -c 0     <--------- c选项指定修改core文件的大小
$ ulimit -c 1000   <--------指定了core文件大小为1000KB, 若是设置的大小小于core文件,则对core文件截取
$ ulimit -c unlimited   <---------------对core文件的大小不作限制
    若是想让修改永久生效,则须要修改配置文件,如.bash_profile、/etc/profile或/etc/security/limits.conf
    咱们回到上面的代码演示,把core文件的大小调成不限制,再执行a.out,就能够在当前目录看到core文件了。
    
    另外补充一些资料,说明一些状况也不会产生core文件。
  1. 进程是设置-用户-ID,并且当前用户并不是程序文件的全部者; 
  2. 进程是设置-组-ID,并且当前用户并不是该程序文件的组全部者; 
  3. 用户没有写当前工做目录的许可权; 
  4. 文件太大。core文件的许可权(假定该文件在此以前并不存在)一般是用户读/写,组读和其余读。 

为何须要core文件

    关于core产生的缘由不少,好比过去一些Unix的版本不支持现代Linux上这种gdb直接附着到进程上进行调试的机制,须要先向进程发送终止信号,而后用工具阅读core文件。在Linux上,咱们就能够使用kill向一个指定的进程发送信号或者使用gcore命令来使其主动出core并退出。
    若是从浅层次的缘由上来说,出core意味着当前进程存在BUG,须要程序员修复。
    从深层次的缘由上讲,是当前进程触犯了某些OS层级的保护机制,逼迫OS向当前进程发送诸如SIGSEGV(即signal 11)之类的信号, 例如访问空指针或数组越界出core,其实是触犯了OS的内存管理,访问了非当前进程的内存空间,OS须要经过出core来进行警示,这就好像一我的身体内存在病毒,免疫系统就会经过发热来警示,并致使人体发烧是一个道理(有意思的是,并非每次数组越界都会出Core,这和OS的内存管理中虚拟页面分配大小和边界有关,即便不出core,也颇有可能读到脏数据,引发后续程序行为紊乱,这是一种很难追查的BUG)。

core文件的名称和生成路径 

    默认状况下core的文件名叫"core"
    /proc/sys/kernel/core_uses_pid能够控制core文件的文件名中是否添加pid做为扩展
  • 文件内容为1,表示添加pid做为扩展名,生成的core文件格式为core.PID
  • 为0则表示生成的core文件统一命名为core.
    如何修改这个文件的内容?
$ echo "0" > /proc/sys/kernel/core_uses_pid
    /proc/sys/kernel/core_pattern文件用于定制core的文件名,通常使用%配合不一样的字符:
  • %p  出core进程的PID
  • %u  出core进程的UID
  • %s  形成core的signal号
  • %t  出core的时间,从1970-01-0100:00:00开始的秒数
  • %e  出core进程对应的可执行文件名

如何阅读core文件

    产生了core文件以后,就是如何查看core文件,并肯定问题所在,进行修复。为此,咱们不妨先来看看core文件的格式,多了解一些core文件。
$ 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'
    首先能够明确一点,core文件的格式ELF格式,经过使用readelf -h命令来查看更详细内容
$ 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
    了解了这些以后,咱们来看看如何阅读core文件,并从中追查BUG。在Linux下,通常读取core的命令为:
$ gdb exec_file core_file
    使用gdb,先从可执行文件中读取符号表信息,而后读取core文件。若是不与可执行文件搅合在一块儿能够吗?答案是不行,由于core文件中没有符号表信息,没法进行调试,能够使用以下命令来验证:
$ 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格式文件中没有符号表信息
    结合上面知识点,咱们分别编译带-g的目标可执行mycat_debug和不带-g的目标可执行mycat,会发现mycat_debug的文件大小稍微大一些。使用readelf命令得出的结果比较报告,详细见附件-readelf报告.html
    各自执行产生的core文件,再使用objdump命令得出的结果比较报告,详细见附件-objdump报告.html
    最后咱们各自使用gdb读取core文件,得出的结果比较报告,详细见附件-gdb_core报告.html
    若是咱们强制使用gdb mycat, 接着是带有调试信息的core文件,gdb会有什么提示呢?
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 ()
    接下来重点来看,为啥产生段错误?
    使用gdb mycat_debug core.2037可见:
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;
可知程序段错误,代码是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
    其中可见指令指针rip指向地址为0x400957, 咱们用x命令来查看内存地址中的值。具体帮助查看 gdb调试 - 查看内存一节
    (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
    这条movl指令要把当即数0送到-0x800014(%rbp)这个地址去,其中rbp存储的是帧指针,其地址是 0x7ffc4b59d660,而-0x800014显然是个负数,十进制是 8388628,且栈空间是由高地址向低地址延伸,见图2,那么n的栈地址就是-0x800014(%rbp),也就是$rbp-8388628。当咱们尝试访问此地址时


图2. 典型的存储空间安排
 
(gdb) x /b 0x7ffc4ad9d64c
0x7ffc4ad9d64c: Cannot access memory at address 0x7ffc4ad9d64c
能够看到没法访问此内存地址,这是由于它已经超过了OS容许的范围。

ulimit命令参数及用法

功能说明:控制shell程序的资源。
补充说明:ulimit为shell内建指令,可用来控制shell执行程序的资源。
参  数: 
  • -a   显示目前资源限制的设定。 
  • -c   设定core文件的最大值,单位为KB。 
  • -d    <数据节区大小> 程序数据节区的最大值,单位为KB。 
  • -f     <文件大小> shell所能创建的最大文件,单位为区块。 
  • -H  设定资源的硬性限制,也就是管理员所设下的限制。 
  • -m    <内存大小> 指定可以使用内存的上限,单位为KB。
  • -n     <文件数目> 指定同一时间最多可开启的文件数。
  • -p     <缓冲区大小> 指定管道缓冲区的大小,单位512字节。
  • -s     <堆叠大小> 指定堆叠的上限,单位为KB。
  • -S  设定资源的弹性限制。 
  • -t   指定CPU使用时间的上限,单位为秒。 
  • -u    <程序数目> 用户最多可开启的程序数目。 
  • -v    <虚拟内存大小>  指定可以使用的虚拟内存上限,单位为KB。

参考

 

 

详解coredump

一,什么是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 程序文件名

相关文章
相关标签/搜索