1、GDB简介
GDB(GNU Debugger)是GCC的调试工具。其功能强大,现描述如下:
GDB主要帮忙你完成下面四个方面的功能:
- 1.启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
- 2.可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
- 3.当程序被停住时,可以检查此时你的程序中所发生的事。
- 4.动态的改变你程序的执行环境。
2、GDB基本调试命令
2.1 准备
通过 gcc 的 -g
选项,将调试信息加到可执行文件中。
$ gcc -g hello.c -o hello
如果使用 Makefile 构建,一般要在 CFLAGS 中指定 -g
选项。
CFLAGS := -Wall -O2 -g
注意,给 GCC 编译器加上优化选项后,实际的执行顺序可能由于优化而与源代码顺序不同,因此利用调试器跟踪运行时,有时会执行到莫名奇妙的地方,从而造成混乱。
2.2 启动
使用命令 gdb 程序名
启动。
2.3 设置断点
可以在函数名和行号等上设置断点。程序到达断点就会自动暂停运行。此时可以查看该时刻的变量值,显示栈帧、重新设置断点或重新运行等。断点命令 break
可以简写为 b
,命令为 break <断点>
。
断点可以通过函数名、当前文件内的行号来设置,也可以先指定文件名再指定行号,还可以指定与暂停位置的偏移量,或者用地址来设置。格式:
格式 | 说明 |
---|---|
break <函数名> | 对当前正在执行的文件中的指定函数设置断点 |
break <行号> | 对当前正在执行的文件中的特定行设置断点 |
break <文件名:行号> | 对指定文件的指定行设置断点,最常用的设置断点方式 |
break <文件名:函数名> | 对指定文件的指定函数设置断点 |
break <+/-偏移量> | 当前指令行+/-偏移量出设置断点 |
break <*地址> | 指定地址处设置断点 |
设置好的断点可以通过 info break
确认。
2.4 运行
使用 run 参数
命令开始运行,其中参数为可执行程序的参数。如果设置了断点,会执行到设置了断点的位置后暂停运行。可以简写为 r
。
2.5 显示栈帧
backtrace
命令可以在遇到断点或异常而暂停执行时显示栈帧,该命令简写为 bt
。此外,backtrace 的别名还有 where
和 info stack
。
格式 | 说明 |
---|---|
bt | 显示所有栈帧 |
bt <N> | 只显示开头 N 个栈帧 |
bt <-N> | 只显示最后 N 个栈帧 |
bt full | 不仅显示 backtrace,还要显示局部变量 |
bt full <N> |
显示栈帧之后,就可以看出程序在何处停止,以及程序的调用路径。
2.6 显示变量
print
命令可以显示变量,可以简写为 p
。
格式:print 变量
。
2.7 显示寄存器
info registers
可以显示寄存器,简写为 info reg
。
在寄存器名之前添加 $,显示寄存器的内容,例如 p $eax
。
p/格式 $寄存器
可以指定寄存器的显示格式,例如 p/c $eax
。可使用的格式如下:
格式 | 说明 |
---|---|
x | 显示为十六进制数 |
d | 显示为十进制数 |
u | 显示为无符号十进制数 |
o | 显示为八进制数 |
t | 显示为二进制数 |
a | 地址 |
c | 显示为字符(ASCII) |
f | 浮点小数 |
s | 显示为字符串 |
i | 显示为机器语言,仅在显示内存的 x 命令中可用 |
程序指针可以写为 $pc,也可以写为 $eip,使用 p $pc
显示程序指针内容。程序指针指向当前程序的运行点的地址。
x
命令可以显示内存的内容,格式:x/<格式> <地址>
。例如 x/i $ps,显示汇编指令。
一般使用 x 命令时,格式为 x/<NFU> <ADDR>。此处 ADDR 为希望显示的地址,N 为重复次数,F 为前面的显示格式,U 代表的单位如下:
单位 | 说明 |
---|---|
b | 字节 |
h | 半字(2 字节) |
w | 字(4 字节)(默认值) |
g | 双字(8 字节) |
例如命令 x\10i $pc 显示从 pc 所指地址开始的 10 条指令。
反汇编命令 disassemble
,简写为 disas
。格式:
- disassemble。反汇编当前整个函数。
- disassemble 程序计数器。反汇编程序计数器所在函数的整个函数。
- disassemble 开始地址 结束地址。反汇编从开始地址到结束地址之间的部分。
2.8 单步执行
单步执行的意思时根据源代码一行一行地执行。执行源代码中一行的命令为 next
,简写为 n
。执行时如果遇到函数调用,想执行到函数内部,使用 step
命令,简写为 p
。
如果要逐条执行汇编指令,可以分别使用 nexti 和 stepi 命令。
2.9 继续运行
使用 continue
命令继续运行程序,简写为 c
。程序会在遇到断点后再次暂停运行。使用 continue <次数>
命令指定忽略断点的次数。
2.10 监视点
大型软件或大量使用指针的程序中,很难弄清变量在什么地方被改变,要想找到变量在何处被改变,可以使用 watch
命令(监视点,watchpoint)。格式如下:
- watch <表达式>。表达式发生变化时暂停运行。
- awatch <表达式>。表达式被访问、改变时暂停运行。
- rwatch <表达式>。表达式被访问时暂停运行。
需要注意,设置监视点可能会降低运行速度。
2.11 删除断点和监视点
delete
命令删除断点和监视点,简写为 d
。格式为 delete <断点编号>
,表示删除编号指示的断点或监视点,编号可以用命令 info b 查看。
clear
命令删除已定义的断点。可用命令包括:
clear <函数名>
clear <行号>
clear <文件名:行号>
clear <文件名:函数名>
disable
命令禁用断点。命令格式如下:
disable:禁用所有断点。
disable <断点编号>:禁用指定断点。
disable display <显示编号>:禁用 display 命令定义的自动显示。
disable mem <内存显示>:禁用 mem 命令定义的内存区域。
enable
命令用于启用断点。命令格式如下:
enable
enable <断点编号>
enable once <断点编号>:使指定的断点只启用一次。
enable delete <断点编号>
enable display <显示编号>
enable mem <内存显示>
2.12 其他断点
硬件断点(hbreak),适用于 ROM 空间等无法修改的内存区域中的程序,在有些架构中无法使用。
临时断点(tbreak)和临时硬件断点(thbreak),在运行到该处时暂停,此时断点会被删除,在只需要停止一次时用起来方便。
2.13 改变变量的值
格式:set variable <变量=表达式>
。例如命令 set variable options = 0,将变量 options 的值改成了 0。
2.14 生成内核转储文件
使用 generate-core-file
可将调试中的进程生成内核转储文件。通过内核转储文件和调试对象,查看生成转储文件时的运行历史。
gcore
命令可以从命令行直接生成内核转储文件。在命令行使用 gcore pid
,其中 pid 为进程号。该命令无须停止正在运行的程序以获得内核转储文件,当需要在其他机器上单独分析问题原因,或是分析客户现场发生的问题时十分有用。
3、内核转储
内核转储(core dump)的最大好处是,它能保存问题发生时的状态。只要有问题发生时程序的可执行文件和内核转储,就可以知道进程当前的状态。
3.1 启用内核转储
大多数 Linux 发行版默认关闭了内核转储功能,使用 ulimit
命令可以查看当前的内核转储功能是否有效。
$ ulimit -c
0
-c 选项表示内核转储文件的大小限制,0 表示内核转储无效。开启内核转储执行命令:
$ ulimit -c unlimited
or
$ ulimit -c 上限
unlimited 意思是不限制内核转储文件的大小,发生问题时进程的内存就可以全部转储到内核转储文件中。或者设置内核转储文件的上限,在参数中指定上限大小,单位为 Kb。
程序发生异常时会在当前目录下生成内核转储文件。例如程序 a.out 生成转储文件 core,使用以下方式启动 GDB:
$ gdb -c core ./a.out
使用 GDB 的 list 命令可以查看附近的源代码。命令使用方法
格式 | 说明 |
---|---|
list <linenum> | 显示程序第 linenum 行周围的源代码 |
list <function> | 显示函数名为 function 的函数的源代码 |
list | 显示当前行后面的源代码 |
list - | 显示当前行前面的源代码 |
set listsize <count> | 设置一次显示源代码的行数 |
show listsize | 查看当前 listsize 的设置 |
list <first>,<last> | 显示从 first 行到 last 行之间的源代码 |
list ,<last> | 显示从当前行到 last 行之间的源代码 |
list + | 往后显示源代码 |
3.2 在专用目录中生成内核转储
转储保存位置的完整路径可以通过 sysctl 变量 kernel.core_pattern 设置。在文件 /etc/sysctl.conf 中设置如下:
$ cat /etc/sysctl.conf
kernel.core_pattern = /var/core/%t-%e-%p-%c.core
kernel.core_uses_pid = 0
$ sysctl -p
此外,还可以在 /proc/sys/kernel 下修改设置。
/proc/sys/kernel/core_uses_pid 可以控制产生的 core 文件的文件名中是否添加 pid 作为扩展 ,如果添加则文件内容为 1 ,否则为 0。
proc/sys/kernel/core_pattern 可以设置格式化的 core 文件保存位置或文件名 ,可以这样修改 :
$ echo "/corefile/core-%e-%p-%t" > core_pattern
kernel.core_pattern 中可以设置的格式符如下:
格式符 | 说明 |
---|---|
%% | % 字符本身 |
%p | 被转储进程的进程 ID(PID) |
%u | 被转储进程的真实用户 ID(real UID) |
%g | 被转储进程的真实组 ID(real GID) |
%s | 引发转储的信号编号 |
%t | 转储时刻(从 1970/1/1 0:00 开始的秒数) |
%h | 主机名(同 uname(2) 返回的 nodename) |
%e | 可执行文件名 |
%c | 转储文件的大小上限(内核版本 2.6.24 后可用) |
3.3 使用用户模式辅助程序自动压缩内核转储文件
修改 /etc/sysctl.conf中 的 kernel.core_pattern 变量来设置。
$ cat /etc/sysctl.conf
kernel.core_pattern= | usr/local/sbin/core_helper %t %e %p %c
kernel.core_uses_pid= 0
$ sysctl -p
core_helper 的内容:
$cat usr/local/sbin/core_helper
#!/bin/sh
execgzip ->/var/core/$1-$2-$3-$4.core.gz
这样,发生内核转储时就会在 /var/core 下生成压缩的内核转储文件。
3.4 启用整个系统的内核转储功能
/etc/profile 文件中可以设置开启所有用户的内核转储功能,默认情况下禁止内核转储:
ulimit -S -c 0 > /dev/null 2>&1
将其修改为
ulimit -S -c unlimited > /dev/null 2>&1
接下来要让通过 init 脚本启动的守护进程的内核转储功能有效。在 /etc/sysconfig/init 文件中添加一行命令。
DAEMON_COREFILE_LIMIT='unlimited'
最后在 /etc/sysctl.conf 中加入以下设置。
fs.suid_dumpable=1
这个设置使得被 SUID 的程序也能内核转储。重新启动启动,就可以启用整个系统的内核转储。
在我使用的Ubuntu、Debian和移植的嵌入式Linux系统中,没有找到 /etc/sysconfig 文件夹。修改了文件 /etc/security/limits.conf,按照文件的内容提示修改,使 unlimited 永久生效。
3.4 利用内核转储掩码排除共享内存
多进程程序如果使用庞大的共享内存,内核转储时所有进程的共享内存全部转储,会对磁盘造成巨大的压力,转储过程也会加重系统的负载。由于共享内存的内容是相同的,只需要在某个进程中转储共享内存。
通过 /proc/<PID>/coredump_filter 进行设置。coredump_filter 使用比特掩码表示内存类型。如下所示:
比特掩码 | 内存类型 |
---|---|
比特 0 | 匿名专用内存 |
比特 1 | 匿名共享内存 |
比特 2 | file-backed 专用内从 |
比特 3 | file-backed 共享内存 |
比特 4 | ELF 文件映射(内核版本 2.6.24 后可用) |
要跳过所有的共享内存区段,应将值改位 1。
4、GDB调试技巧
4.1 attach 到进程
要调试已经启动的进程,或是调试陷入死循环而无法返回控制台的进程时,可以使用 attach
命令。格式:attach <pid>
,执行这一命令可以 attach 到进程 ID 为 pid 的进程上。
attach 之后就能使用普通的 gdb 命令。
gdb 和进程分离时使用 detach
命令,调试的进程就从 gdb 的控制下释放出来。进程被 detach 后继续运行。
进程信息可以用 info proc
命令显示。
守护者进程在启动好子进程后,会自动关闭主进程,如果没有设定监控模式的话,gdb 会提示断开与进程的链接。所以必须设定监控对象,设置命令为 set follow-fork-mode child/parent
。
4.2 条件断点
break <断点> if <条件>
,这条命令将测试给定的条件,如果为帧则暂停运行。
如果断点已经存在,condition <断点编号> <条件>
命令给断点添加触发条件,condition <断点编号>
命令删除指定编号断点的触发条件。
4.3 反复执行
ignore <断点编号> <次数>
:在编号指定的断点、监视点或捕获点忽略指定的次数。
continue <次数>
: 达到指定次数前,执行到断电时不暂停。
s/stepi/n/nexti <次数>:执行指定次数的相应命令。
finish:执行完当前函数后暂停。
until:执行完当前函数等代码块后暂停,如果是循环,则在执行完循环后暂停,常用于跳出循环。
until <地址>:执行到指定地址停止。
4.4 断点命令
commands
命令可以定义在断点终端后自动执行的命令。格式如下:
(gdb) commands <断点编号>
<命令>
...
end
4.5 值的历史
通过 print 命令显示过的值会记录在内部的值历史中。这些值通过 $ 进行引用,使用 show value 命令可以显示历史中的最后 10 个值。
变量 | 说明 |
---|---|
$ | 值历史的最后一个值 |
$n | 值历史的第 n 个值 |
$$ | 值历史的倒数第 2 个值 |
$$n | 值历史的倒数第 n 个值 |
$_ | x 命令显示过的最后的地址 |
$__ | x 命令显示过的最后的地址的值 |
$_eexitcode | 调试过程中的程序的返回代码 |
$bpnum | 最后设置的断点编号 |
还可以随意定义变量,变量以 $ 开头,由英文字母和数字组成。例如:
(gdb) set $i=0
(gdb) p $i
$i = 0
4.6 命令历史
show history
将命令历史保存到文件中,默认命令历史文件位于 ./.gdb_history。
set history expansion
show history expansion
可以使用 csh 风格的 ! 字符。
set history filename <文件名>
show history filename
将命令历史保存到文件中。可以通过环境变量 GDBHISTFILE 改变默认文件名。
set history save
show history save
启用命令历史保存到文件和恢复的功能。
set history size <数字>
show history size
可设置保存到命令历史中的命令数量。默认值为 256。
4.7 初始化文件(.gdbinit)
Linux 环境下的初始化文件为 .gdbinit。如果存在 .gdbinit 文件,GDB 会在启动之前将其作为命令文件运行。初始化文件和命令文件的运行顺序如下。
- 1、$HOME/.gdbinit。
- 2、运行命令行选项。
- 3、./.gdbinit。
- 4、通过 -x 选项给出的命令文件。
初始化文件和命令文件的语法相同。利用 define 命令可以自定义命令,document 命令给自定义命令添加说明,GDB 运行时使用 help <命令名> 可以查看定义的命令。示例如下:
define li
x/10i $pc
end
document li
list machine instruction
end
除了初始化文件,还可以把设置写在文件中,在运行 GDB 时读取这些文件。命令为 source <文件名>
,例如:
(gdb) source gdbcalc
(gdb) p $log10(10000.0)
$1 = 4
其中 gdbcalc 文件内容如下:
#!/usr/bin/gdb -x
file /usr/bin/gdb
start
set $e = 2.7182818284590452354
set $pi = 3.14159265358979323846
set $fabs = (double (*)(double)) fabs
set $sqrt = (double (*)(double)) sqrt
set $cbrt = (double (*)(double)) cbrt
set $exp = (double (*)(double)) exp
set $exp2 = (double (*)(double)) exp2
set $exp10 = (double (*)(double)) exp10
set $log = (double (*)(double)) log
set $log2 = (double (*)(double)) log2
set $log10 = (double (*)(double)) log10
set $pow = (double (*)(double, double)) pow
set $sin = (double (*)(double)) sin
set $cos = (double (*)(double)) cos
set $tan = (double (*)(double)) tan
set $asin = (double (*)(double)) asin
set $acos = (double (*)(double)) acos
set $atan = (double (*)(double)) atan
set $atan2 = (double (*)(double, double)) atan
set $sinh = (double (*)(double)) sinh
set $cosh = (double (*)(double)) cosh
set $tanh = (double (*)(double)) tanh
set $asinh = (double (*)(double)) asinh
set $acosh = (double (*)(double)) acosh
set $atanh = (double (*)(double)) atanh
5、总结
GDB 常用命令及缩略形式如下表:
命令 | 简写形式 | 说明 |
---|---|---|
backtrace | bt、where | 显示 backtrace |
break | 设置断点 | |
continue | c、cont | 继续运行 |
delete | d | 删除断点 |
finish | 运行到函数结束 | |
info breakpoints | 显示断点信息 | |
next | n | 执行下一行 |
p | 显示表达式 | |
run | r | 运行程序 |
step | s | 一次执行一行,包括函数内部 |
x | 显示内存内容 | |
until | u | 执行到指定行 |
directory | dir | 插入目录 |
disable | dis | 禁用断点 |
down | do | 在当前调用的栈帧中选择要显示的栈帧 |
edit | e | 编辑文件或函数 |
frame | f | 选择要显示的栈帧 |
forward-search | fo | 向前搜索 |
generate-core-file | gcore | 生成内核转储 |
help | h | 显示帮助一览 |
info | i | 显示信息 |
list | l | 显示函数或行 |
nexti | ni | 执行下一行(以汇编代码为单位) |
print-object | po | 显示目标信息 |
sharedlibrary | share | 加载共享库的符号 |
setpi | si | 执行下一行 |