添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
精彩文章免费看

GDB 常见使用介绍

官方文档

http://sourceware.org/gdb/current/onlinedocs/gdb/

确定文件是否可以gdb调试

gdb 直接调试查看

没有调试信息不能调试

  • readelf 查看段信息

  • file命令查看strip
    如果最后是stripped,则说明该文件的符号表信息和调试信息已被去除,不能使用gdb调试。但是not stripped的情况并不能说明能够被调试。

  • info files 命令查看段信息

    info files
    
    当运行程序没有调试信息

    为了节省磁盘空间,已经运行的程序通常没有调试信息,但如果又不能停止当前程序重新启动调试,那怎么办呢?
    可以采用同样的代码,再编译出一个带调试信息的版本,然后使用和前面提到的方式操作。对于attach方式,在attach之前,使用file命令即可。

    $ gdb
    (gdb) file a.out
    
    (gdb) n
    
  • 单步进入函数
    (gdb) s
    
  • 查看函数参数
    (gdb) info args
    
  • 查看函数内的局部变量
    (gdb) info locals
    
    (gdb) where
    #0  event_base_loop (base=0x602010, flags=0) at event.c:466
    #1  0x00007ffff7bbb5c7 in event_base_dispatch (event_base=0x602010) at event.c:405
    #2  0x0000000000400858 in main () at test.c:20
    
    查看源代码
    (gdb) l 
    
  • 设置源码一次列出行数
    l命令默认每次只显示10行。
    (gdb) set listsize 20
    (gdb) show listsize
    Number of source lines gdb will list by default is 20.
    
  • 列出指定行附近的源码
    (gdb) l main.cpp:8
    
  • 列出指定函数附近的源码
    (gdb) l printnum 
    
  • 列出指定行之间的源码
    (gdb) l 3,15
    
  • 列出指定文件的源码
    (gdb) l test.c:1
    
    指定源码路径

    在查看源码之前,首先要确保我们的程序能够关联到源码,一般来说,我们在自己的机器上加上-g参数编译完之后,使用gdb都能查看到源码,但是如果出现下面的情况呢?源码被移走。gdb调试就会提示找不到源码文件了,那么怎么办呢?可以使用dir命名指定源码路径

    (gdb) dir ./temp
    
  • 更换源码目录

    断点的设置原理:在程序中设置断点,就是先将该位置的原来的指令保存,然后向该位置写入int 3指令,当执行到int 3的时候,发生软中断,内核会给子进程发出SIGTRAP信号,当然这个信号会被转发给父进程。然后用保存的指令替换int 3,等待恢复运行。

    断点的实现原理:就是在指定的位置插入断点指令,当被调试的程序运行到断点的时候,产生SIGTRAP信号,该信号被gdb捕获并进行断点命中判定,当gdb判断出这次SIGTRAP是断点命中之后就会转入等待用户输入进行下一步处理,否则继续。

    (gdb) info b
    Num     Type           Disp Enb Address            What
    4       breakpoint     keep y   0x000000000040053f in main at test.c:6
    
    设置行号断点
    (gdb) break test.c:8 
    
    设置函数断点
  • 设置C函数断点
    (gdb) break func1
    
  • 设置C++函数断点
    因为C++ 具有多态的特性。
    (gdb) break TestClass::testFunc(int) 
    
  • 在匿名空间设置断点
    namespace 
        void bar()  {  }
    (gdb) b (anonymous namespace)::bar
    
    设置临时断点

    临时断点只会断住一次,然后会被移除掉。

    (gdb) tbreak 8
    
    设置条件断点
    b 11 if i == 3
    假设上面的断点号为1。
    
    condition 1 i == 0  
    
    (gdb) disable 2
    (gdb) info breakpoints
    Num Type           Disp Enb Address    What
    2   breakpoint     keep n   0x080483c3 in func2 at test.c:5
    3   breakpoint     keep y   0x080483da in func1 at test.c:10
    

    假设在某个地方,我们知道可能出错,前面30次都没有问题,在这里我们设置了断点,但是我们想跳过前面30次,可以使用ignore命令,如下:第一个参数表示断点编号,第二个表示跳过次数。

    (gdb) ignore 2 5
    Will ignore next 5 crossings of breakpoint 2.
    
    (gdb) save breakpoints file-name-to-save
    

    下次调试时,可以使用如下命令批量设置保存的断点:

    (gdb) source file-name-to-save
    

    有时候我们需要观察某个值或表达式,知道它在什么时候发生了变化,可以借助watch打下观察点。

    watch a
    

    注意:打观察点必须使程序运行起来,否则会出现No symbol "a" in current context.

    首先理解函数与调用栈的关系,参见:关于函数调用浅析

    查看调用栈
    (gdb) bt
    #0  func2 (x=30) at test.c:5
    #1  0x80483e6 in func1 (a=30) at test.c:10
    #2  0x8048414 in main (argc=1, argv=0xbffffaf4) at test.c:19
    #3  0x40037f5c in __libc_start_main () from /lib/libc.so.6
    (gdb) 
    
    (gdb) frame 2
    #2  0x8048414 in main (argc=1, argv=0xbffffaf4) at test.c:19
    19        x = func1(x);
    (gdb)
    
    向上或向下切换函数堆栈
    (gdb) up 2
    (gdb) down 2
    
    (gdb) info frame
    Stack level 2, frame at 0xbffffa8c:
    eip = 0x8048414 in main (test.c:19); saved eip 0x40037f5c
    called by frame at 0xbffffac8, caller of frame at 0xbffffa5c
    source language c.
    Arglist at 0xbffffa8c, args: argc=1, argv=0xbffffaf4
    Locals at 0xbffffa8c, Previous frame's sp is 0x0
    Saved registers:
    ebp at 0xbffffa8c, eip at 0xbffffa90
    (gdb) info locals
    x = 30
    s = 0x8048484 "Hello World!\n"
    (gdb) info args
    argc = 1
    argv = (char **) 0xbffffaf4
    

    最常见的使用便是使用print(可简写为p)打印变量内容。

    (gdb) p a
    

    当然有时候,多个函数或者多个文件会有同一个变量名,这个时候可以在前面加上函数名或者文件名来区分。

    (gdb) p 'main'::b
    
  • 按照特定格式打印变量
  • x 按十六进制格式显示变量。
  • d 按十进制格式显示变量。
  • u 按十六进制格式显示无符号整型。
  • o 按八进制格式显示变量。
  • t 按二进制格式显示变量。
  • a 按十六进制格式显示变量。
  • c 按字符格式显示变量。
  • f 按浮点数格式显示变量。
  • (gdb) p/x mask
    $16 = 0x1
    

    int *array = (int *) malloc (len * sizeof (int));

    (gdb) p *array@len
    
    查看变量类型
    (gdb) ptype el->fired
    type = struct aeFiredEvent {
        int fd;
        int mask;
    
    自动显示变量

    我们希望程序断住时,就显示某个变量的值,可以在断点被断住时使用display命令

    (gdb) display e
    
  • 查看哪些变量设置了display
    info display
    
  • 清除display
    del display [num]
    
  • 去使能display enbale
    disable display [num]
    
    打印派生类
    x/[n][f][u] [address]

  • n 表示显示内存长度,默认值为1
  • f 表示显示格式,如同上面打印变量定义
  • x 按十六进制格式显示变量。
  • d 按十进制格式显示变量。
  • u 按十六进制格式显示无符号整型。
  • o 按八进制格式显示变量。
  • t 按二进制格式显示变量。
  • a 按十六进制格式显示变量。
  • c 按字符格式显示变量。
  • f 按浮点数格式显示变量。
  • u 表示每次读取的字节数,默认是4bytes
  • b 表示单字节
  • h 表示双字节
  • w 表示四字节
  • g 表示八字节
  • 假设,需要把float变量e按照二进制的方式打印,并且打印单位时一字节。

    (gdb) x/4tb &e
    0x7fffffffdbd4:    00000000    00000000    00001000    01000001
    
    查看寄存器
    (gdb) info registers
    rax            0x4004f0 4195568
    rbx            0x0  0
    rcx            0x400510 4195600
    rdx            0x7fffffffe598   140737488348568
    rsi            0x7fffffffe588   140737488348552
    rdi            0x1  1
    rbp            0x7fffffffe4a0   0x7fffffffe4a0
    rsp            0x7fffffffe4a0   0x7fffffffe4a0
    r8             0x7ffff7dd6e80   140737351872128
    r9             0x0  0
    r10            0x7fffffffe2f0   140737488347888
    r11            0x7ffff7a3ca40   140737348094528
    r12            0x400400 4195328
    r13            0x7fffffffe580   140737488348544
    r14            0x0  0
    r15            0x0  0
    rip            0x400503 0x400503 <main+19>
    eflags         0x246    [ PF ZF IF ]
    cs             0x33 51
    ss             0x2b 43
    ds             0x0  0
    es             0x0  0
    fs             0x0  0
    gs             0x0  0
    

    实验1:探究x86参数传递

    #include <stdio.h>
    #include <stdlib.h>
    int v1 = 1;
    float v2 = 0.01;
    void func(int a, long b, short c, char d, long long e, float f, double g, int *h, float *i, char *j) 
        printf("a: %d, b: %ld, c: %d, d: %c, e: %lld\n"
             "f: %.3e, g: %.3e\n"
             "h: %p, i: %p, j: %p\n", a, b, c, d, e, f, g, h, i, j);                                                     
    int main()
        func(100, 35000, 5, 'A', 123456789LL, 3.14, 2.99792458e8, &v1, &v2, "string");
        return 0;
    
    (gdb) b *func
    (gdb) r
    (gdb) i r
    rax            0x41b1de784a000000 4733809291562057728
    rbx            0x0    0
    rcx            0x41   65
    rdx            0x5    5
    rsi            0x88b8 35000
    rdi            0x64   100
    rbp            0x7fffffffe490 0x7fffffffe490
    rsp            0x7fffffffe468 0x7fffffffe468
    r8             0x75bcd15  123456789
    r9             0x601034   6295604
    r10            0x7fffffffe2e0 140737488347872
    r11            0x7ffff7a3ca40 140737348094528
    r12            0x400440   4195392
    r13            0x7fffffffe570 140737488348528
    r14            0x0    0
    r15            0x0    0
    rip            0x400530   0x400530 <func>
    eflags         0x202  [ IF ]
    cs             0x33   51
    ss             0x2b   43
    ds             0x0    0
    es             0x0    0
    fs             0x0    0
    gs             0x0    0
    

    可以发现,开头的5个参数a,b,c, d,e分别保存到了rdi, rsi, rdx, rcx, 和 r8中。

    (gdb) disassemble main
    Dump of assembler code for function main:
       0x00000000004004f0 <+0>: push   %rbp
       0x00000000004004f1 <+1>: mov    %rsp,%rbp
       0x00000000004004f4 <+4>: mov    %edi,-0x14(%rbp)
       0x00000000004004f7 <+7>: mov    %rsi,-0x20(%rbp)
       0x00000000004004fb <+11>:    movq   $0x4005a0,-0x8(%rbp)
    => 0x0000000000400503 <+19>:    pop    %rbp
       0x0000000000400504 <+20>:    retq   
    End of assembler dump.
    

    print pretty

  • set print pretty on 打开该设置,结构体显示更好看。
  • set print pretty off
  • show print pretty
  • print elements

  • set print elements 300 更改打印字符串变量的长度
  • show print elements 查看设置的长度
  • set pagination off
    有时当gdb输出信息较多时,gdb会暂停输出。

    81 process 2639102  0xff04af84 in __lwp_park () from /usr/lib/libc.so.1
    80 process 2573566  0xff04af84 in __lwp_park () from /usr/lib/libc.so.1
    ---Type <return> to continue, or q <return> to quit---Quit
    

    使用set pagination off命令,这样gdb就会全部输出。

  • 打印STL
    脚本:stl-views.gdb
    直接在gdb终端 source stl-views.gdb ,支持常见的容器打印,如vector、map、list、string等。

  • GDB可以在你调试程序的时候处理任何一种信号,你可以告诉GDB需要处理哪一种信号,你可以要求GDB收到你所指定的信号时,马上停住正在运行的程序,以供你进行调试。

    handle <signal> <keywords>
    
  • nostop
    当被调试的程序收到信号时,GDB不会停住程序的运行,但会打出消息告诉你收到这种信号。
  • 当被调试的程序收到信号时,GDB会停住你的程序。
  • noignore
    当被调试的程序收到信号时,GDB不处理信号。这表示,GDB会把这个信号交给被调试程序会处理。
  • ignore
    当被调试的程序收到信号时,GDB不会让被调试程序来处理这个信号。
  • 结合core文件调试

    使用内核转储(core)的最大好处就是:它能保存问题发生时的状态,只要有问题发生时程序的可执行文件和内核转储,就可以知道进程当时的状态。

    gdb a.out xx.core
    

    启用内核转储功能

    $ ulimit -c
    

    -c选项表示内核转储文件的大小限制,0表示内核转储未打开。按照以下命令开启:

    $ ulimit -c unlimited
    $ ulimit -c
    unlimited
    

    ulimit -c unlimited 设置core文件大小不限制,这样设置是一次性的,会话结束就恢复原样。
    在用户的 ~/.bash_profile 里加上ulimit -c unlimited来让用户开启内核转储功能,再执行一下source ~/.bash_profile命令。
    在 /etc/profile 里加上ulimit -c unlimited来让所有用户开启内核转储功能,再执行一下source /etc/profile命令。

    core文件目录和命名规则
    在 /proc/sys/kernel/core_pattern 可以设置格式化的core文件保存位置和文件名。

  • 比如core-%e-%p-%t表示在当前目录生成"core-命令-pid-时间戳"为文件名的core文件。
  • 比如/cfg/core-%e-%p-%t表示在/cfg下生成"core-命令-pid-时间戳"为文件名的core文件
  • 注意:/proc/sys/kernel/core_pattern 不能直接编辑,可以用echo core-%e-%p-%t > /proc/sys/kernel/core_pattern

    发现函数调用栈里面会出现很多??的情况,这常发生于栈被写花,某些情况下手动进行修复。

    -----------------------------------
    Low addresses
    -----------------------------------
    0(%rsp)  | top of the stack frame 
             | (this is the same as -n(%rbp))
    ---------|-------------------------
    -n(%rbp) | variable sized stack frame
    -8(%rbp) | varied
    0(%rbp)  | previous stack frame address
    8(%rbp)  | return address
    -----------------------------------
    High addresses
    

    从上面的栈示意图可以发现,利用%rbp寄存器即可找到上一个函数的返回地址和栈底指针,
    再利用 addr2line 命令找到对应的代码行。这里举一个例子:

  • 寻找this指针和虚指针

    除了上面代码错误触发生成core文件之外,还可以使用gcore命令来将正在运行的程序内存映像dump出来

    gcore [进程号]
    

    多线程调试

    查看当前进程的所有线程
    (gdb) info threads
    
    切换线程栈
    (gdb) thread [线程号]
    
    打印线程堆栈
  • 打印所有线程堆栈
    thread apply all bt
    
  • 打印指定线程堆栈
    thread apply 5 bt  # 5 线程id
    
    调试时控制线程切换

    用gdb调试多线程程序时,一旦程序断住,所有的线程都处于暂停状态。此时当你调试其中一个线程时(比如执行“step”,“next”命令),所有的线程都会同时执行,有时候我们希望执行流一直在某个线程执行,而不是切换到其他线程。

    那我们可以使用:

    set scheduler-locking on/off/step
    
    set scheduler-locking on

    set scheduler-locking on 可以用来锁定当前线程,只观察这个线程的运行情况, 当锁定这个线程时, 其他线程就处于了暂停状态。
    也就是说你在当前线程执行 nextstepuntilfinishreturn 命令时,其他线程是不会运行的。

    set scheduler-locking off用于关闭锁定当前线程。

    set scheduler-locking step

    也是用来锁定当前线程,当且仅当使用 nextstep 命令做单步调试时会锁定当前线程。
    如果你使用 untilfinishreturn 等线程内调试命令,但是它们不是单步命令,所以其他线程还是有机会运行的。相比较 on 选项值,step 选项值给为单步调试提供了更加精细化的控制

    多进程调试

    gdb的调试默认是调试父进程的,但我们可以通过设置来选择调试哪个进程。

    (gdb) set follow-fork-mode parent/child
    

    如果选择了parent,这个时候就是进行gdb调试父进程。
    如果选择了child,这个时候就是进行gdb调试子进程。
    注意,在调试的过程中更改mode是没有用的,这种设置只对下一次fork后起作用。

    显示共享链接库信息
    (gdb) info sharedlibrary
    From        To          Syms Read   Shared Object Library
    0xff3b44a0  0xff3e3490  Yes (*)     /usr/lib/ld.so.1
    0xff3325f0  0xff33d4b4  Yes         /usr/local/lib/libhiredis.so.0.11
    0xff3137f0  0xff31a9f4  Yes (*)     /lib/libsocket.so.1
    1、 http://www.unknownroad.com/rtfm/gdbtut/gdbtoc.html
    2、 http://www.cnblogs.com/pannengzhi/p/5203467.html
    3、https://zhuanlan.zhihu.com/p/74897601
    4、https://zhuanlan.zhihu.com/p/46605905
    5、https://www.zhihu.com/collection/42904454?page=3
    6、https://wizardforcel.gitbooks.io/100-gdb-tips/content/call-func.html

  •