本文最后更新于 314 天前,其中的信息可能已经有所发展或是发生改变。
内容目录
GDB的常规应用(动态分析工具==目标=>进程)
- 自定义程序的启动方式(指定影响程序运行的参数),指定了命令行参数(本质是main函数的参数),软件运行的结果就可能是不同的
- 设置 条件断点(条件满足时暂停程序的执行,用于递归和循环语句)
- 回溯检查导致程序异常结束的原因(Core Dump),gdb监视,如果访问0地址处,程序告诉你具体出问题的代码行
- 动态改变程序执行流(定位问题的辅助方式),跳过某一行程序的执行
- GDB是GNU项目中的调试器,能够跟踪或改变程序的执行
- GDB同时支持软件断点,硬件断点和数据断点。
GDB的启动方式
直接启动:
gdb
gdb test.out //这样是关注test.out文件产生的进程(file是gdb内部的指令,指明当前的gdb需要关注的进程)
gdb test.out core // 异常崩溃产生的core文件
动态链接:
gdb test.out pid // gdb监视test.out产生的某个进程,gdb动态跟踪进程的行为
示例一:直接启动
:~$ gdb // 启动
(gdb) file test.out // 载入目标程序 前两行 ==> gdb test.out gdb关注目标程序对应的进程
(gdb) set args arg1 arg2 // 指定程序执行时的命令行参数,指定启动test.out的参数
(gdb) run // 执行目标程序
示例二:动态链接(调试正在运行的程序)
:~$ sudo gdb // 用sudo启动gdb,要动态链接,必须这样,否则没权限
(gdb) attach 进程的pid号 // 链接到目标进程,链接成功后 (前两行 ==> gdb attach 进程的pid号)
// 目标进程停止执行(因为被gdb动态跟踪了,gdb链接进程后,会停止进程)
(gdb) continue // 恢复执行
ctrl c // 暂停执行
GDB断点类型
- 软件断点: 由非法指令异常实现(软件实现,中断),内存中执行
- 硬件断点: 由硬件特性实现(数量有限),适用于直接在flash中运行的程序
- 数据断点: 由硬件特性实现(数量有限),往往用来监视一片内存,一旦内存被访问了,程序的执行会立即停下来。
断点使用相关操作
---通过 函数名 设置断点,打断点如果指明 [] 里的条件,就是条件断点,只有在条件成立的时候,才能暂停执行
break func_name [ if var = value] // 程序调用对应的函数,程序的执行就被停止了(设置的断点总是有效的)
tbreak func_name [ if var = value] //(设置只有效一次的断点)
---通过 文件名和行号 设置断点
break file_name:line_num [ if var = value]
tbreak file_name:line_num [ if var = value]
断点的->查看/删除/改变
断点的查看 info breakpoints
断点删除 delete 1 2 n // 删除指定断点
delete breakpoints // 删除所有断点
断点改变状态 enable 1 2 n // 使得对应断点有效
enable breakpoints
disable 1 2 n // 使得对应断点无效
disable breakpoints
调试常用命令
变量查看 print name
变量设置 set var name=value
执行下一行代码 next
连续执行n行代码 next n
执行进入函数体 step
强制当前函数返回 return [value] // 以某个value值返回当前函数
运行至当前函数返回 finish // 执行完当前函数后暂停
执行至目标行 until line
跳转执行 jump line // 强制跳转
查看当前运行的代码片段 l[行号] => list
硬件断点及其应用
—当代码位于只读存储器(Flash)时,只能通过硬件断点调试
—硬件断点需要硬件支持,数量有限
—GDB中通过hbreak命令支持硬件断点
—hbreak与break使用方式完全一致
实验一:三次调试高效的定位和验证问题
-----------------------------------------------------------------------------
----------------------------第一次调试----------------------------------------
技巧: 跳过jump怀疑出错的地方func()
gdb test.out <-------这样启动,gdb关注的目标是test.out产生的进程
set args D.T.Software <-------设置启动参数
start <-------启动可执行程序,和run命令不同,run就是单纯的启动可执行程序,start启动可执行程序后立即暂停
Temporary breakpoint 1 at 0x40062c: file test.c, line 25. 系统提示设置一个临时断点(start启动程序停止了)
Starting program: /home/xiaoma/Linux/GDB/test.out D.T.Software
系统提示设置一个临时断点,也就是start启动之后,立即停在了main()函数入口处
Temporary breakpoint 1, main (argc=2, argv=0x7fffffffe1b8)
at test.c:25
25
Missing separate debuginfos, use: debuginfo-install glibc-2.17-307.el7.1.x86_64 libgcc-4.8.5-44.el7.x86_64 libstdc++-4.8.5-44.el7.x86_64
(gdb) break test.c:37 <-------打个软件断点 // fa[i%3]();
info breakpoints <-------查看断点是否打成功
continue <-------调用continue继续执行,程序将停止在打断点的地方
next 3 <-------next执行3次
print i <-------打印i变量 => $1 = 1 :$1指的是第一次打印
此时 i= 1,要执行完程序还有99次,怎办?
set var i=100 <-------设置i的值为100
tbreak test.c:43 <-------设置临时断点在 func()函数, 根据前面的执行,怀疑是func函数里的程序导致崩溃
如果改变执行过程,跳过func()函数的执行,直接之后后面的,如果后面的正常结束,肯定就是这个func()函数有问题
jump 45 <-------跳转到第45行执行
程序正常结束。。。。。。
----------------------------------------------------------------------------------
-----------------------------进行第二次调试----------------------------------------
技巧: 怀疑出错的地方func(),就调用func() 函数,但是不执行func() 函数的函数体
gdb test.out <-------
start <-------
tbreak func <-------通过函数名打断点
info breakpoints <-------断点打成功
continue <-------继续执行
为了确认func()函数有问题的,强制func() 返回,
刚进入函数就返回,说明func()被调用,只是func()函数体没有执行
return <-------强制func() 返回
continue <-------继续执行
程序正常结束。。。。。。
两次调试得到同一个结论,距离真相更近一步。。。。
----------------------------------------------------------------------------------
-----------------------进行第三次调试----------------------------------------------
技巧: 已经看出来具体错误了,不着急改代码,先验证解决方案(让g_pointer指向一片合法的空间),让gdb来验证解决方案,而不是直接修改代码,,,,这次使用硬件断点
gdb test.out <-------
start <-------
show can-use-hw-watchpoints <------- 查看gdb支持几个硬件断点
hbreak func <------- 在func()函数打硬件断点
info breakpoints <------- 查看有没有打上断点
continue <------- 就可以继续执行了
这时候,硬件断点就停下来了,停到了func()的第7行,但是还没执行第7行
print g_pointer <-------打印一下这个指针,发现值为0,那么现在向让g_pointer指向一片合法的内存,怎么做?设置一下就行,就和写C语言的代码一样
set var g_pointer=(int*)malloc(sizeof(int)) <------- 设置指针,指向一片合法的空间
print g_pointer <------- 再打印一下,发现现在g_pointer不为空了,不再指向0地址处了,这个时候,按照期望,程序该正常结束
continue <-------继续执行
[Inferior 1 (process 31809) exited normally]
程序正常结束。。。。。。
---------------------------------------------------------------------------------------------------
三次调试已经达到了期望的结果了,第三次调试成功验证了解决方案是可行的。
三次调试就定位了问题,并验证了解决方案,非常高效,
高效之处在于,在整个定位和验证过程中,根本没有改源代码,自然不用重新编译。
数据断点
数据断点
---GDB中支持数据断点的设置
---watch命令用于监听变量是否被改变(本质为硬件断点)
---watch命令用法: watch var_name (程序关注var_name变量,如果有一行代码改变了变量,程序停下来,告诉我们)
(不是像软件断点和硬件断点那样,当程序执行到某个特殊的代码行的时候,停止运行。
什么时候起作用? 就是监听的某个变量如果被改变,程序就会暂停,并且GDB会提示哪个变量被改变了
数据断点本质是硬件断点,需要硬件的支持才开起作用的,所以数据断点数量有限,非万不得已,不要使用)
GDB中内存查看
GDB中内存查看
--- GDB中可以检查任意内存区域中的数据 x :检查内存区域中的数据的命令
--- 命令语法: x /Nuf expression
N :需要打印的单元数
u :每个单元的大小(单位)| 单位: b(单字节) h(双字节) w(四字节) g(八字节)
f : 数据打印的格式
==> x(十六进制) d(有符号十进制) u(无符号十进制) o(八进制) t(二进制) a(地址) c(字符) f(浮点数)
示例1:
x /4bx 0x804a024 ,用x命令,查看0x804a024内存中的数据,用 /4bx表现数据参数(4 byte 十六进制)
示例2:判断系统的大小端
(gdb) set var = 1
(gdb) print /a &var <------- 打印变量的地址
$1 = 0x804a024 <var>
(gdb) x /4b 0x804a024
0x804a024 <var>: 0x01 0x00 0x00 0x00 // 意味着变量var的值 1,保存在低字节处,低地址存放低位数据
(gdb) x /1b 0x804a024
0x804a024 <var>: 0x01 // 验证一下结论(小端系统)
实验二:定位变量被修改位置和内存查看,判断系统大小端的方法
gcc -g -lpthread watch.c -o test.out <-------多线程编译
./test.out <------- 运行一下看现象
多线程交互的时候,可能意外的改写了某个变量 g_var,线程入口函数睡眠模拟正常工作,之后由于手误,意外的改写了全局变量 g_var, 改写之后,影响到了别的线程的工作了,main()最后的打印,五秒之后发现g_var值被改变了。。需要调试
调试:
gdb test.out <------- 启动调试
start <------- 启动指定的可执行程序,可执行程序的执行停在了main函数的入口处
watch g_var <------- 设置数据断点,感兴趣的变量是g_var
Hardware watchpoint 2: g_var 提示咱硬件的数据断点设置到了变量g_var上
info breakpoints <------- 查看断点是否设置成功
continue <------- 继续执行程序
[New Thread 0x7ffff77f0700 (LWP 36941)]
g_var = 0
g_var = 0
g_var = 0
g_var = 0
g_var = 0
[Switching to Thread 0x7ffff77f0700 (LWP 36941)]
Hardware watchpoint 2: g_var // 提示我们数据断点生效,g_var被改写
Old value = 0
New value = 1
thread_func (args=0x0) at watch.c:12 // 指定改写变量的 函数和行号,执行到第12行时被改写成功,是第11行改写的
12 }
(gdb) print g_var <------- 打印变量值,真的被改变了
继续展示x的应用
print /a &g_var <------- 打印变量的地址 0x601040
x /4bx 0x601040 <------- 查看与这个地址相关的内存中的值,/4bx:打印从这个地址开始,连续4哥byte的值,每个字节以十六进制来打印
0x601040 <g_var>: 0x01 0x00 0x00 0x00
x /1bx 0x601040 <------- 验证一下结论(小端系统)
0x601040 <g_var>: 0x01
continue <------- 继续执行
程序正常正确结束。。。。。
成功定位到哪一行代码修改了我们感兴趣的全局变量,并且通过x命令也能判断当前系统是个小端系统
函数调用栈的查看(backtrace 和 frame)
---backtrace 查看函数调用的顺序(函数调用栈的信息)
---frame N 切换到栈编号为N 的上下文中,可以查看到与函数相关的全部信息
---info frame 查看当前函数调用的栈帧信息。栈帧:函数活动记录(参数 返回地址 寄存器信息 局部变量 其它信息数据)
为啥要直到函数调用栈信息?
例:有一份开源代码,需要知道函数被谁调用,什么时候调用? 现在需要快速知道如何调用到当前函数的,怎么做? GDB
深入info命令:
info registers 查看当前寄存器的值
info args 查看当前函数调用栈上的参数的值
info locals 查看当前函数调用栈上所保存的局部变量的的值
info frame 查看当前栈帧的详细信息
info variables 查看程序中的变量符号
info functions 查看程序中的函数符号
实验三: 函数调用栈的查看
递归函数的调试具有一定的复杂度,开始调试,,,,ebp esp两个寄存器
gcc -g frame.c -o test.out <------- 编译一下
gdb <------- 启动gdb
file test.out <------- 载入调试文件
start <------- 启动可执行文件,可执行程序的执行停在了main函数的入口处
break sum if n==0 <------- 对感兴趣的函数打一个条件断点,一直到函数调用,n为0的时候为止
info breakpoints <------- 查看断点是否存在
continue <------- 继续执行
Breakpoint 2, sum (n=0) at frame.c:6 // 反馈咱程序在sum函数断下来 n=0
6 int ret = 0;
(gdb) backtrace <------- 查看函数调用的顺序(函数调用栈的信息)
#0 sum (n=0) at frame.c:6
#1 0x000000000040052c in sum (n=1) at frame.c:10
#2 0x000000000040052c in sum (n=2) at frame.c:10
#3 0x000000000040052c in sum (n=3) at frame.c:10
#4 0x000000000040052c in sum (n=4) at frame.c:10
#5 0x000000000040052c in sum (n=5) at frame.c:10
#6 0x000000000040052c in sum (n=6) at frame.c:10
#7 0x000000000040052c in sum (n=7) at frame.c:10
#8 0x000000000040052c in sum (n=8) at frame.c:10
#9 0x000000000040052c in sum (n=9) at frame.c:10
#10 0x000000000040052c in sum (n=10) at frame.c:10
#11 0x0000000000400554 in main () at frame.c:21
(gdb)
从下向上看,这样分析一目了然。
类比想一下,假设面对的是开源代码,我们感兴趣的函数是如何调用到的?backtrace 一目了然。所以说分析不熟悉的代码的时候,GDB也是一个很好的助手。
next <------- 下一步
next <------- 下一步,准备return
info args <------- 查看当前函数调用栈上的参数的值 n = 0
frame 7 <------- 切换到栈编号为7的上下文中(#7 0x000000000040052c in sum (n=7) at frame.c:10)查看参数的值以及局部变量的值
#7 0x000000000040052c in sum (n=7) at frame.c:10 // 现在已经切换到编号为7的栈帧所对应的函数调用上下文了
10 ret = n + sum(n-1); // 目前函数调用的上下文中,语句停留在这一行
(gdb)info args <------- 这时候打印函数参数的值 n = 7
info locals <------- 打印局部变量的值 ret = 0
frame 0 <------- 切换回来,继续分析
info registers <------- 查看当前上下文中重要寄存器的值
rdi 0x0 0
rbp 0x7fffffffded0 0x7fffffffded0
rsp 0x7fffffffdeb0 0x7fffffffdeb0 // 当前的rsp的值
r8 0x7ffff7dd5e80 140737351868032
r9 0x0 0
info frame <------- 查看当前栈帧的详细信息
Undefined info command: "fream". Try "help info".
(gdb) info frame
Stack level 0, frame at 0x7fffffffdee0:
rip = 0x400536 in sum (frame.c:13); saved rip 0x40052c
called by frame at 0x7fffffffdf10
source language c.
Arglist at 0x7fffffffded0, args: n=0
Locals at 0x7fffffffded0, Previous frame's sp is 0x7fffffffdee0 // 上一个sp的值,当前的esp的值在info registers中体现
Saved registers: // 这里说 调用函数前
rbp at 0x7fffffffded0, rip at 0x7fffffffded8 // 之前rbp的值被保存在了0x7fffffffded0地址处, 那么就打印地址中的内容
(gdb) x /1wx 0x7fffffffded0 <------- 打印地址中的内容
0x7fffffffded0: 0xffffdf00 // 意味着函数调用之前,rbp寄存器的值是0xffffdf00
next <-------
next <------- return回去了,意味着到了sum(1)的地方 和上面的做对比
info args <------- 打印参数 n = 1
info registers <------- 查看当前上下文中重要寄存器的值
rbp 0x7fffffffdf00 0x7fffffffdf00 // 被存储的rbp的值
rsp 0x7fffffffdee0 0x7fffffffdee0
一些调试中的小技巧
断点处自动打印 display /f expression
undisplay 取消之前的自动打印
查看程序中的符号 whatis
ptype
GDB中的代码查看 list
set listsize N
GDB中的shell操作 shell 也就是GDB可以直接使用Shell中命令
什么是调试?
本质是查看运行过充当中寄存器的值是不是我想要的,函数参数的值是不是我想要的,程序的运行究竟是哪里出错了。
实验四: 酷炫技巧操作
gdb <-------启动
shell cat tricks.c <------- 查看代码方式1,和shell环境无缝集成
shell gedit trick.c <------- 查看代码方式2,虚拟机上直接打开了文本,,,,在xshell需要安装相关
shell gcc -g tricks -o test.out <-------
file test.out <------- 载入可执行文件
start <-------
break tricks.c:18 <------- 设置断点
list tricks.c:18 <------- 打印第18行程序
set listsize 20 <------- 嫌弃每次打印的内容太少,设置一下,每次打印20行
show listsize <------- 查看设置是否成功
list tricks.c:18 <------- 再次打印,,,,20行了
continue <------- 继续执行
display /d i <------- 每次执行到这个断点的时候,就自动打印咱感兴趣的东西 i
display /d i*i <------- 打印 i*i的值
display /a &i <------- 打印 i的地址
continue <------- 继续打印 看现象,,多continue 几次,到结束为止
run <------- 程序执行到断点的位置
undisplay <------- 取消之前的自动打印
continue <------- 就没有自动打印了,,只能手工打印print
print /d i <------- 手工打印
whatis g_var <-------查看程序中的符号(类型)
ptype g_var <-------结果一样
whatis func <------- func类型是个函数
ptype func <------- 结果一样
(gdb) whatis struct ST <------- 查看结构体类型,不够详细
type = struct ST
(gdb) ptype struct ST <-------查看结构体类型,详细
type = struct ST {
int i;
int j;
}
list tricks.c:1 <------- 查看代码 确实是这样滴
info variables <------- 查看全局的变量符号
info functions <------- 查看程序中的函数符号
gdb调试种类
1. 调试一个新程序 gdb a.out
2. 调试正在运行的程序 gdb attach 进程的pid号
# gcc main.cc -lpthread
# bt 查看函数调用栈信息
# info threads 查看所有进程
# thread id 切换到指定线程进行调试(bt)
3. 调试coredump文件 gdb a.out core文件, bt、p等命令查看
gdb_调试coredump文件
coredump文件: 存储的是程序挂掉时,内存的数据信息, 分析程序挂掉的问题
linux默认core文件是关闭的,步骤如下:
1. ulimit -a 查看参数配置信息
2. ulimit -c unlimited 配置开启core文件,大小不做限制,让程序在奔溃时产生core文件。
3. 运行一个可以segment fault的程序,发现当前目录会产生core文件
4. gdb 可执行程序名 coredump文件名 开启调试
5. bt和p看程序挂在哪里,并且可以打印变量内容,来定位程序挂掉的原因
陈浩大佬文章中也有一些方法可参考:
一、多线程调试
二、调试宏
三、源文件
四、条件断点
五、命令行参数
六、gdb的变量
七、x命令
八、command命令