[Linux]GDB调试技巧

命令行参数

gdb有下面几种运行方式:

1
2
3
4
5
6
// 1. 通过coredump文件,或者存在的进程id分析,不会拉起新进程
gdb [options] [executable-file [core-file or process-id]]
// 2. 带参数运行程序
gdb [options] --args executable-file [inferior-arguments ...]
// 3. redhat等含有用于调试python的工具
// gdb [options] [--python|-P] script-file [script-arguments ...]

几个值得注意的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 和上面的类似,使用参数指定的
--args Arguments after executable-file are passed to inferior
--core=COREFILE Analyze the core dump COREFILE.
--pid=PID Attach to running process PID.
--exec=EXECFILE Use EXECFILE as the executable.
// 远程调试
-b BAUDRATE Set serial port baud rate used for remote debugging.
-l TIMEOUT Set timeout in seconds for remote debugging.
// 运行某个文件中的gdb指令
--command=FILE, -x Execute GDB commands from FILE.
// 运行某gdb指令,如gdb a.out -ex r开启文件并立即运行
--eval-command=COMMAND, -ex Execute a single GDB command.
--directory=DIR Search for source files in DIR.
--se=FILE Use FILE as symbol file and executable file.

简单的例子:coredump分析

gdb的一大用处是通过coredump文件分析程序哪里发生了错误。首先要打开coredump生成开关:

1
2
3
4
// 可以先运行 ulimit -a 查看所有限制,或者运行 ulimit -c 查看当前coredump设置
// 设置成无限可能会生成数GB的coredump文件
// 这条命令重启后无效
ulimit -c unlimited

coredump文件会默认生成在程序相同目录下。如果没有对应文件,可以查看/etc/sysctl.conf:

1
2
3
4
// 生成目录格式,%e 程序名 %p 进程id %s 信号 %t 时间 %e 命令名
kernel.core_pattern =/data/coredump/core%e%p
// 1表示使用procid命名,0表示不使用
kernel.core_uses_pid= 0

我们先编写一个简单的c程序main.c,它试图从非法地址读取数据:

1
2
3
4
int main(){
int a = *((int *)NULL);
return 0;
}

编译,一定要加上-g选项:

1
gcc -o test_worker main.c -g

运行,果然dump:

1
2
./test_worker
Segmentation fault (core dumped)

然后使用第一节里的第一种方式启动, 携带coredump文件:

1
gdb ./test_worker core.xxx.xxx

进去后就能看到dump的地方以及原因。使用bt查看coredump时栈顶的信息:

1
2
3
4
5
6
7
Core was generated by `./t'.
Program terminated with signal 11, Segmentation fault.
#0 0x0000000000400564 in main () at t.c:13
13 int a = *((int *)NULL);
Missing separate debuginfos, use: debuginfo-install glibc-2.17-106.el7_2.4.x86_64
(gdb) bt
#0 0x0000000000400564 in main () at t.c:13

GDB的用法

GDB Online Docs

GDB启动时会读取二进制文件的符号表,然后进入调试命令行。这里可以运行各种命令查看程序的符号表、变量、内存值以及控制程序的运行。

控制命令

  1. file 加载一个二进制文件,直接进入gdb时可以用这个加载。参数为文件名。
  2. run (或者r) 从头运行程序一直到断点。没有断点会一直运行结束,或者直到遇到异常。可以r < a.in 重定向输入输出。
  3. continue (或者c) 运行程序一直到下一个断点。
  4. next/step (或者n/s) 单步调试,next不会在行内进入函数体,step则会跳入函数体。 参数为跳多行。
  5. until (或者 u) 直接跳出当前循环。(但是还是会被断点卡住)。参数为跳到指定行。
  6. finish 直接运行到函数返回
  7. call 运行某函数,参数为函数和参数,如 call foo(2, "hello")
  8. 回车键 重复上个指令

断点

  1. break n (或者b) 在第n行打断点
  2. b main.c:n 指定文件打断点; b main 指定函数入口处打断点; b main:label 指定函数的标签处打断点
  3. b n if i == 5 满足条件打断点,对循环尤其有效。
  4. info b 查看断点号和信息,或者 i b
  5. delete no (或者d) 删除对应编号的断点; d breakpoints 删除所有断点
  6. clear lineno 删除对应行的断点
  7. disable/enable no 屏蔽/使用对应编号的断点

查看代码和变量

  1. list (或者l) 列出10行源文件。每次从上次结束的地方开始列。
  2. list lineno 列出某行的前后源码;l main 列出某函数的源码
  3. print exp (或者p) 打印任意变量、表达式、函数、字符串、数组的值
  4. display exp 每次单步完打印该表达式
  5. watch exp 如果该表达式值改变了,打印并停止程序
  6. whatis 查询变量,函数的类型
  7. info (或者i) 查询信息 i locals 所有变量的值 i args 所有参数的值 i function 函数信息 i frame 栈信息
    i program 程序信息 i threads 线程信息

堆栈

  1. bt 查看栈信息; bt n 栈顶n层;bt -n 栈底n层

    1
    2
    3
    (gdb) bt
    #0 foot () at t.c:7
    #1 0x0000000000400525 in main () at t.c:15
  2. frame (或者f)查看帧信息。上面一个#号(一层)是一帧。
    frame n 查看第n帧 ;up n 上面n帧; down n 下面n帧;frame addr 查看某地址的帧

  3. info frame n/addr 帧详细信息
    ip是下条命令的地址(pc),bp是栈底的地址,sp是栈顶的地址。
    64位机的帧信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    (gdb) i f 0
    // 帧地址
    Stack frame at 0x7fffffffe560:
    // rip:帧PC 帧函数名 saved rip:caller帧PC
    rip = 0x4004fd in foot (t.c:7); saved rip 0x400525
    // caller帧地址
    called by frame at 0x7fffffffe570
    source language c.
    // 帧参数地址
    Arglist at 0x7fffffffe550, args:
    // 帧局部变量地址,caller的栈顶地址
    Locals at 0x7fffffffe550, Previous frame sp is 0x7fffffffe560
    // 帧寄存器列表
    Saved registers:
    rbp at 0x7fffffffe550, rip at 0x7fffffffe558

    32位机的帧信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    (gdb) i f
    Stack level 0, frame at 0xbffff630:
    // eip:帧PC 帧函数名 saved eip:caller帧PC
    eip = 0x80483e4 in main (a.c:8); saved eip = 0xb7e31637
    source language c.
    // 帧参数地址
    Arglist at 0xbffff628, args:
    // 帧局部变量地址,caller的栈顶地址
    Locals at 0xbffff628, Previous frame sp is 0xbffff630
    // 帧寄存器列表
    Saved registers:
    ebp at 0xbffff628, eip at 0xbffff62c

内存

100 GDB Tips

gdb中使用x命令来打印内存的值,格式为x/nfu addr

含义为以f格式打印从addr开始的n个长度单元为u的内存值。参数具体含义如下:
a)n:输出单元的个数。
b)f:是输出格式。比如x是以16进制形式输出,o是以8进制形式输出,等等。
c)u:标明一个单元的长度。b是一个bytesh是两个bytes(halfword),w是四个bytes(word),g是八个bytes(giant word)。

比如对一个字符串arr查看内存:(这个数组越界踩掉了总共10 char的内容,每个char在32位机上是32bit)

1
2
3
4
5
6
7
8
9
(gdb) p arr
$4 = "\000\001\002\003"
// 显示11个byte,十六进制显示
(gdb) x/11xb arr
0x7fffffffe540: 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x7fffffffe548: 0x08 0x09 0x40
// 显示3个4-byte,十六进制显示
(gdb) x/3xw arr
0x7fffffffe540: 0x03020100 0x07060504 0x00400908

信号

这部分的详细内容在GDB Online Docs-5.4 Signals中。你可以自由地对程序进行发送信号(signal)、捕获信号(catch)或者处理信号(handle)。

线程

除了上文的查看线程信息之外,还可以查看详细的线程信息,参考GDB Online Docs-4 Threads,或者查看fork的情况,参考GDB Online Docs-5Forks