gdb 入门使用

官方文档

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

GDB Front Ends - GDB Wiki (sourceware.org)

线上编译器:Online C Compiler - online editor (onlinegdb.com)

教程 介绍 · 100个gdb小技巧 · 看云 (kancloud.cn)

确定文件是否可以gdb调试

gdb 直接调试查看

  • 没有调试信息

    not gdb.png

  • readelf 查看段信息

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


使用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 hello 20829

当运行程序没有调试信息

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

$ gdb
(gdb) file a.out

单步调试

  • 单步执行
    (gdb) n
    
  • 单步进入函数
    (gdb) s
    

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中。


查看汇编代码

(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就会全部输出。


信号

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

  • 参数列表

    符号意义
    %pinsert pid into filename 添加 pid
    %uinsert current uid into filename 添加当前 uid
    %ginsert current gid into filename 添加当前 gid
    %sinsert signal that caused the coredump into the filename 添加导致产生 core 的信号
    %tinsert UNIX time that the coredump occurred into filename 添加 core 文件生成时的 unix 时间戳
    %hinsert hostname where the coredump happened into filename 添加主机名
    %einsert coredumping executable name into filename 添加命令名
  • 没有符号信息找寻core的代码

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

    image.png

     

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

  • 寻找this指针和虚指针


多线程调试

查看当前进程的所有线程

(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

gdb Debugging Full Example

GDB命令基础,让你的程序bug无处躲藏 | Deepzz's Blog

Altering (Debugging with GDB)

作者:0x007c00
链接:https://www.jianshu.com/p/e6af28e2566f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值