了解和使用GDB调试-基础
启动调试
1. 哪些程序可以被调试
- 对于C和C++程序,编译时加上
-g参数,会保留调试信息,否则无法使用GDB进行调试。
2. 如何判断文件是否可以调试
- 直接使用
gdb 文件名运行,如果不可调试则会有相应提示。 - readelf查看段信息:
readelf -S helloWorld|grep debug - file查看strip状况:
file helloworld,如果最后提示为stripped,则说明文件的符号表信息和调试信息以及被去除。但是如果未 not stripped,也并不意味着一定可以进行gdb调试。
3. 无参程序启动调试
$ gdb helloWorld
(gdb)
(gdb) run
输入run命令,即可直接运行程序。
4. 带参程序启动调试
- 方式1:run命令时带上所需参数即可
$ gdb hello
(gdb)run Argument1
- 方式2:run命令前,使用
set args
$ gdb hello
(gdb) set args Argument1
(gdb) run
5. 调试core文件
core文件介绍:https://www.jianshu.com/p/e38a3f1cf7f7
- Linux下程序异常退出时,内核在当前工作目录下生成core文件,记录当时的内存映像和调试信息。可以使用gdb来查看core文件。
- 产生core文件的前提是编译时带上了
-g参数,并且core文件生成没有收到限制。 - 关注core文件的生成开关和大小限制。
- 关注core文件的名词和生成路径。
$ gdb [exec file] [core file]
6. 调试已运行程序
通过ps命令以运行的特点程序进程id
$ ps -ef|grep 进程名
或者可以自己编写测试文件,利用bg和fg来切换前后台。
使用attach直接调试相关进程id的进程,如果提示没有权限,可以sudo gdb
$ gdb
(gdb) attach 20829
7. 已运行程序且无调试信息
为了节省磁盘空间,已经运行的程序通常没有调试信息。并且不能停止当前程序进行重新编译调试,此时可以利用同样的代码,再编译一个带调试信息的版本。
$ gdb
(gdb) file hello
Reading symbols from hello...done.
(gdb)attach 20829
断点设置
1. 查看已设置的断点
命令info breakpoints 查看已设置的断点。
2. 根据行号设置断点
两种方式任一:
b 9 #break 可简写为b
b test.c:9
3. 根据函数名设置断点
当初程序调用到函数funcName时会断点
b funcName
4. 根据条件设置断点
特定行数 + 变量判断 组成条件设置,如果该条件成立,则形成断点可进行观察:
break test.c:23 if b==0
含义为:当b等于0时,程序将在23行断点。
condition命令有着类似作用:
condition 1 b==0
含义为:当b等于0时,产生断点1。
5. 根据规则设置断点
#用法:rbreak file:regex
rbreak .
rbreak test.c:. #对test.c中的所有函数设置断点
rbreak test.c:^print #对以print开头的函数设置断点
6. 设置临时断点
在某处的断点只生效一次,则可以设置临时断点:
tbreak test.c:l0 #在第10行设置临时断点
7. 跳过多次设置断点
对于某个断点处,前30次不会发生问题,可以跳过前30次:
ignore 1 30
其中1为通过info breakpoints查询的断点序号。
8. 根据表达式值变化来产生断点
观察某个特定表达式或者值,当其发生变化时,产生断点并打印相关内容:
watch a
# 当产生值变化时,会打印出
Hardware watchpoint 2: a
Old value = 12
New value = 11
rwatch和awatch同样可以设置观察点前者是当变量值被读时断住,后者是被读或者被改写时断住。
9. 禁用、启用、删除断点
对于暂时不需要使用,但是也不可删除的断点,可以选择暂时禁用
disable #禁用所有断点
disable bnum #禁用标号为bnum的断点
enable #启用所有断点
enable bnum #启用标号为bnum的断点
clear #删除当前行所有breakpoints
clear function #删除函数名为function处的断点
clear filename:function #删除文件filename中函数function处的断点
clear lineNum #删除行号为lineNum处的断点
clear f:lename:lineNum #删除文件filename中行号为lineNum处的断点
delete #删除所有breakpoints,watchpoints和catchpoints
delete bnum #删除断点号为bnum的断点
变量查看
1. 打印基本数据类型:变量、数组、字符串
使用print(可简写为p)打印变量内容:
(gdb) p a
$1 = 10
(gdb) p b
$2 = {1, 2, 3, 5}
(gdb) p c
$3 = "hello,shouwang"
(gdb)
可以在前面加上函数名或者文件名来区分同名变量:
(gdb) p 'testGdb.h'::a
$1 = 11
(gdb) p 'main'::b
$2 = {1, 2, 3, 5}
(gdb)
2. 打印指针指向内容
- 如果以打印普通变量的形式打印指针,则会打印出指针地址:
(gdb) p d
$1 = (int *) 0x602010
- 若需要打印指针所指向的内容,需要解引用:
(gdb) p *d
$2 = 0
(gdb) p *d@10
$3 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
(gdb)
通过@符号跟上想要打印的长度。
$在gdb中为上一个变量
(gdb) p *linkNode
(这里显示linkNode节点内容)
(gdb) p *$.next
(这里显示linkNode节点下一个节点的内容)
- 设置gdb变量和使用累加
(gdb) set $index=0
(gdb) p b[$index++]
$11 = 1
(gdb) p b[$index++]
$12 = 2
(gdb) p b[$index++]
$13 = 3
3. 按照特定格式打印变量
- x 按十六进制格式显示变量。
- d 按十进制格式显示变量。
- u 按十六进制格式显示无符号整型。
- o 按八进制格式显示变量。
- t 按二进制格式显示变量。
- a 按十六进制格式显示变量。
- c 按字符格式显示变量。
- f 按浮点数格式显示变量。
(gdb) p/x c
$19 = {0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x73, 0x68, 0x6f, 0x75, 0x77, 0x61,
0x6e, 0x67, 0x0}
(gdb)
4. 查看内存内容和寄存器内容
examine(简写为x)可以用来查看内存地址中的值:
x/[n][f][u] addr
其中:
- n 表示要显示的内存单元数,默认值为1
- f 表示要打印的格式,前面已经提到了格式控制字符
- u 要打印的单元长度
- b 字节
- h 半字,即双字节
- w 字,即四字节
- g 八字节
- addr 内存地址
(gdb) x/4tb &e # &e开始的4块字节内存,以二进制打印
0x7fffffffdbd4: 00000000 00000000 00001000 01000001
(gdb)
命令info registers可以查看寄存器内容:
(gdb)info registers
rax 0x0 0
rbx 0x0 0
rcx 0x7ffff7dd1b00 140737351850752
rdx 0x0 0
rsi 0x7ffff7dd1b30 140737351850800
rdi 0xffffffff 4294967295
rbp 0x7fffffffdc10 0x7fffffffdc10
5. 断点时自动打印变量内容
若希望程序在断点时自动打印某个变量的值,可以使用display命令:
(gdb) display e
1: e = 8.5
想要查看哪些变量被设置了display:
(gdb)into display
想要清除:
delete display num #num为前面变量前的编号,不带num时清除所有。
单步调试
1. 打印当前调试程序的源码
(gdb) list
2. 单步执行程序 next
若当前已经启动调试,停在某个断点处,使用next命令(简写为n)可以继续往下执行下一条语句。若跟上数字number,则表示执行number次:
$ gdb gdbStep #启动调试
(gdb)b 25 #将断点设置在12行
(gdb)run #运行程序
Breakpoint 1, main () at gdbStep.c:25
25 int b = 7;
(gdb) n #单步执行
26 printf("it will calc a + b\n");
(gdb) n 2 #执行两次
it will calc a + b
28 printf("%d + %d = %d\n",a,b,c);
(gdb)
3. 单步进入 step
若需要跟着进入函数内部查看情况,可以使用step命令(简写为s),单步跟踪导函数内部,前提为该函数有调试信息和源码信息。
$ gdb gdbStep #启动调试
(gdb) b 25 #在12行设置断点
Breakpoint 1 at 0x4005d3: file gdbStep.c, line 25.
(gdb) run #运行程序
Breakpoint 1, main () at gdbStep.c:25
25 int b = 7;
(gdb) s
26 printf("it will calc a + b\n");
(gdb) s #单步进入,但是并没有该函数的源文件信息
_IO_puts (str=0x4006b8 "it will calc a + b") at ioputs.c:33
33 ioputs.c: No such file or directory.
(gdb) finish #继续完成该函数调用
Run till exit from #0 _IO_puts (str=0x4006b8 "it will calc a + b")
at ioputs.c:33
it will calc a + b
main () at gdbStep.c:27
27 int c = add(a,b);
Value returned is $1 = 19
(gdb) s #单步进入,现在已经进入到了add函数内部
add (a=13, b=57) at gdbStep.c:6
6 int c = a + b;
s命令会尝试进入函数,但是如果没有该函数源码,需要跳过该函数执行,可使用finish命令,继续后面的执行。
s命令可以设置选项,选择是否默认跳过没有调试信息的函数:
(gdb) show step-mode
Mode of the step operation is off.
(gdb) set step-mode on
(gdb) set step-mode off
s命令为每次执行一条程序语句,可以使用stepi(简写为si),每次执行一条机器指令。
4. 继续执行到下一个断点 continue
使用continue命令(可简写为c),会继续执行当前程序,直到再次遇到断点。
5. 继续运行到指定行数位置 until
若我们希望在继续运行直到特定行数停住,可以使用until命令(简写为u):
6. 跳过执行 skip
skip可以在step时跳过一些不想关注的函数或者某个文件的代码:
$ gdb gdbStep
(gdb) b 27
Breakpoint 1 at 0x4005e4: file gdbStep.c, line 27.
(gdb) skip function add # step时跳过add函数
Function add will be skipped when stepping.
(gdb) info skip # 查看step情况
Num Type Enb What
1 function y add
(gdb) run
Starting program: /home/hyb/workspaces/gdb/gdbStep
it will calc a + b
Breakpoint 1, main () at gdbStep.c:27
27 int c = add(a,b);
(gdb) s
28 printf("%d + %d = %d\n",a,b,c);
(gdb)skip file gdbStep.c # 跳过文件内的所有函数
其他相关命令:
- skip delete [num] 删除skip
- skip enable [num] 使能skip
- skip disable [num] 去使能skip
源码查看
调试过程中一般需要对照整体或者部分源码查看,在GDB调试下快速查看源码或者对源码进行编辑。
1. 源码打印
- 直接打印源码:list命令(简写l)
- 列出指定行附近的源码:list命令 + 行号
(gdb) l 9
- 列出指导函数附件的源码:list命令 + 函数名
- 设置源码一次列出的行数,一般打印源码默认显示10行。通过listsize属性来设置。
(gdb) set listsize 20
(gdb) show listsize
- 列出指定行之间区域的源码:list + 起始行号 + 结束行号
(gdb) l 3,15 # 列出3到15行之间的源码
- 列出指导文件的源码
(gdb) l test.c:1
(gdb) l test.c:printNum1
(gdb) l test.c:1,test.c:3
2. 指定源码路径
查看源码之前,需要先确保程序能够关联到源码文件。但是当出现源码文件移动等情况时,无法直接通过lsit命令查看到源码。
- 场景1:源码文件移动
源码文件 main.c 移动到temp目录下,此时执行list命令
(gdb) l
1 main.c: No such file or directory.
(gdb)
通过dir命令重新指定源码路径:
(gdb) dir ./temp
Source directories searched: /home/hyb/workspaces/gdb/sourceCode/./temp:$cdir:$cwd
- 场景2:更换源码目录
全部源码文件移动到了另一个目录,可以使用上述场景1中的方式添加源码搜索路径,也可以使用set substitute-path from to将原来的路径替换为新的路径
通过readelf命令可以查看原来源码路径:
$ readelf main -p .debug_str
[ 0] long unsigned int
[ 12] short int
[ 1c] /home/hyb/workspaces/gdb/sourceCode
[ 40] main.c
(显示部分内容)
替换路径:
(gdb) set substitute-path /home/hyb/workspaces/gdb/sourceCode /home/hyb/workspaces/gdb/sourceCode/temp
(gdb) show substitute-path
List of all source path substitution rules:
`/home/hyb/workspaces/gdb/sourceCode' -> `/home/hyb/workspaces/gdb/sourceCode/temp'.
(gdb)
可以通过unset substitute-path [path]取消替换。
3. 编辑源码
启动调试后,若有编辑源码的需求,可以直接在gdb模式下进行编辑源码。gdb默认使用的编辑器为/bin/ex,可以设置替换编辑器:
$ EDITOR=/usr/bin/vim
$ export EDITOR
gdb调试模型下进行编辑源码,使用edit命令:
(gdb)edit 3 #编辑第三行
(gdb)edit printNum #编辑printNum函数
(gdb)edit test.c:5 #编辑test.c第五行
在vim编辑器下编辑保存完后,可以直接重新编译程序:
(gdb)shell gcc -g -o main main.c test.c
在gdb模式下执行shell命令,需要在命令前加上shell。
其他参考资料
命令汇总
命令再次列出:
- file:装入调试程序源文件
- kill:终止调试程序
- list:打印源代码
- break:设置断点
- run:执行程序
- quit:推出gdb
- step:单步进入
- next:单步执行
- continue:继续运行,直到下一个断点
- print:打印变量等
- watch:监视变量值的变化
- display:每次断点都会打印一次变量
- start:开始执行程序,并在main函数的第一条语句前停下来
- info:gdb相关信息
- set var name = value:设置变量的值
- backtrace:查看函数调用信息(堆栈)
- frame:查看栈帧
- return:强制函数返回
- where:列出当前程序运行的位置
- whatis:查看变量、函数的类型
- examine:查看内存内容
调试脚本补丁
- 反汇编
(gdb) disassemble 函数名
- GDB断点预置命令
GDB提供了一种功能,对于指定的断点,GDB允许用户预设一组操作(通常是调试命令),当断点被触发时,GDB会自动执行这组预设的操作。
先设置一个或者多个断点;再利用commands命令进行预设操作(id为断点id):
commands [id...]
command-list
end
- Debug过程中通过预置命令来快速修复已知的bug,避免多次编译的繁琐。
b *do_stuff
commands
printf "\n ESI = %d\n",$esi
set $esi=10
printf "\n ESI = %d\n",$esi
continue
end
# do_stuff函数入口处设置断点,通过修改esi寄存器来修改函数入参传递。
- 将上述可以写为一个热补丁文件,test.fix.1.加入silent命令,屏蔽断点被触发时的打印信息,避免视觉干扰。
# test.fix.1
b *do_stuff
commands
slient
set $esi=10
continue
end
- GDB重新调试运行,-x参数加载脚本文件
gdb test -x test.fix.1

浙公网安备 33010602011771号