GDB 常见使用介绍

官方文档

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

确定文件是否可以gdb调试

gdb 直接调试查看
  • 没有调试信息不能调试


    not gdb.png
  • readelf 查看段信息

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

  • info files 命令查看段信息

    info files
    

使用GDB调试

编译时使用-g编译选项
$ gcc -g program.c -o programname
在gdb中启动程序
$ gdb programname 
(gdb) run arg1 "arg2" ...
在gdb中重启程序
(gdb) kill
Kill the program being debugged? (y or n) y
(gdb) run
退出gdb
(gdb) quit
The program is running. Exit anyway? (y or n) 
调试已经运行的程序

假设已经获取到的进程PID=20829

$ gdb
(gdb) attach 20829

也可以直接使用gdb program pid,例如:

$ gdb attach 20829
当运行程序没有调试信息

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

$ gdb
(gdb) file a.out
单步调试
  • 单步执行
    (gdb) n
    
  • 单步进入函数
    (gdb) s
    
  • 查看函数参数
    (gdb) info args
    
  • 查看函数内的局部变量
    (gdb) info locals
    
until命令
  • 跳出函数

    (gdb) finish
    
  • 跳出循环
    until执行直到达到当前栈帧中当前行后的某一行(用于跳过循环、递归函数调用)

  • 跳到指定行

    until [num]
    
查看当前代码运行位置
(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
  • 更换源码目录

执行shell命令

在gdb命令行界面可以执行外部的Shell命令

(gdb) !ls
a.out test.c

或者:

(gdb) shell ls
a.out test.c

断点

断点的设置原理:在程序中设置断点,就是先将该位置的原来的指令保存,然后向该位置写入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 表示八字节
  • 以字符串的形式查看

    char *s = "hello world";  // 源码
    
    (gdb) x/s s
    0x4005a0: "hello world"
    
  • 以字符的形式查看

    (gdb) x/c s
    0x4005a0: 104 'h'
    
  • 以二进制查看

    (gdb) x/t s
    0x4005a0: 01101000
    
  • 以八进制查看

    (gdb) x/x s
    0x4005a0: 0x68
    

假设,需要把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中。

    image.png

内存dump

查看汇编代码
(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不会停住程序的运行,但会打出消息告诉你收到这种信号。
  • stop
    当被调试的程序收到信号时,GDB会停住你的程序。
  • noignore
    当被调试的程序收到信号时,GDB不处理信号。这表示,GDB会把这个信号交给被调试程序会处理。
  • ignore
    当被调试的程序收到信号时,GDB不会让被调试程序来处理这个信号。

结合core文件调试

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

gdb a.out xx.core
  • 启用内核转储功能

    $ ulimit -c
    0
    

    -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

  • 参数列表

    符号 意义
    %p insert pid into filename 添加 pid
    %u insert current uid into filename 添加当前 uid
    %g insert current gid into filename 添加当前 gid
    %s insert signal that caused the coredump into the filename 添加导致产生 core 的信号
    %t insert UNIX time that the coredump occurred into filename 添加 core 文件生成时的 unix 时间戳
    %h insert hostname where the coredump happened into filename 添加主机名
    %e insert coredumping executable name into filename 添加命令名
  • 没有符号信息找寻core的代码

    void dumpCrash()
    {
        char *pStr = "test_content";
        free(pStr);
    }
    
    int main()
    {
        dumpCrash();
    }
    
    image.png

    c++filt 可以从name mangling后的名字找到原函数。

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

    -----------------------------------
    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指针和虚指针
    TODO

除了上面代码错误触发生成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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • gdb是gcc的交互式调试器。 假设程序文件是a.c,内容是(这个程序有错误): #include int j; ...
    金石明镜阅读 945评论 0 1
  • 1. 编译时允许gdb: 在编译命令中加入-g gcc/g++ filename.c/cpp -Wall -o a...
    SetDefault__阅读 1,000评论 0 0
  • 1、file载入调试程序,同时加载符号表 2、core-file载入core dump程序映像,gdb命令行参数形...
    d3ark阅读 2,047评论 0 0
  • 爱到底是什么东西,为什么那么辛酸那么痛苦,只要还能握住它,到死还是不肯放弃,到死也是甘心。淡淡的感情从开始的第一次...
    阿昕小章鱼阅读 411评论 0 4
  • 創意禪繞話訓練第八天。 禪繞畫可以畫禪繞在哪裡?其實哪裡都可以,我們都可以去做嘗試。發現美,創造美,分享...
    观心客阅读 506评论 0 1