GDB调试 | 一壶漂泊浪迹天涯难入喉
Cobb 369 0

陈浩大佬文章中也有一些方法: 一、多线程调试 二、调试宏 三、源文件 四、条件断点 五、命令行参数 六、gdb的变量 七、x命令 八、command命令

GDB的常规应用(动态分析工具====目标===>进程)

1. 自定义程序的启动方式(指定影响程序运行的参数),指定了命令行参数(本质是main函数的参数),软件运行的结果就可能是不同的
2. 设置 条件断点(条件满足时暂停程序的执行,用于递归和循环语句)
3. 回溯检查导致程序异常结束的原因(Core Dump),gdb监视,如果访问0地址处,程序告诉你具体出问题的代码行
4. 动态改变程序执行流(定位问题的辅助方式),跳过某一行程序的执行
5. GDB是GNU项目中的调试器,能够跟踪或改变程序的执行
6. 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断点类型

0.1. 软件断点: 由非法指令异常实现(软件实现,中断),内存中执行
0.2. 硬件断点: 由硬件特性实现(数量有限),适用于直接在flash中运行的程序
0.3. 数据断点: 由硬件特性实现(数量有限),往往用来监视一片内存,一旦内存被访问了,程序的执行会立即停下来。

断点使用相关操作

---通过 函数名 设置断点,打断点如果指明 [] 里的条件,就是条件断点,只有在条件成立的时候,才能暂停执行
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使用方式完全一致

实验一:三次调试高效的定位和验证问题

点击获取实验程序1 点击获取实验程序2

-----------------------------------------------------------------------------
----------------------------第一次调试----------------------------------------
        技巧: 跳过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看程序挂在哪里,并且可以打印变量内容,来定位程序挂掉的原因

文章参考 https://item.taobao.com/item.htm?ft=t&id=612571963885

评论区

索引目录