官方文档
http://sourceware.org/gdb/current/onlinedocs/gdb/
确定文件是否可以gdb调试
gdb 直接调试查看
-
没有调试信息不能调试
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
中。
内存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(); }
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
可以用来锁定当前线程,只观察这个线程的运行情况, 当锁定这个线程时, 其他线程就处于了暂停状态。
也就是说你在当前线程执行 next
、step
、until
、finish
、return
命令时,其他线程是不会运行的。
set scheduler-locking off
用于关闭锁定当前线程。
set scheduler-locking step
也是用来锁定当前线程,当且仅当使用 next
或 step
命令做单步调试时会锁定当前线程。
如果你使用 until
、finish
、return
等线程内调试命令,但是它们不是单步命令,所以其他线程还是有机会运行的。相比较 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