【Linux系统】—— 调试器 gdb/cgdb的使用
【Linux系统】—— 调试器 gdb/cgdb的使用
- 1 前置准备
- 2 快速认识 gdb
- 3 cgdb/gdb 的使用
- 3.1 简单认识 cgdb
- 3.2 打断点 / 删断点
- 3.3 逐过程 / 逐语句
- 3.4 查看变量
- 3.5 快速跳转
- 4 cgdb/gdb 调试技巧
- 4.1 watch
- 4.2 「set var」确定问题原因
- 4.3 条件断点
- 5 概念理解
- 6 gdb/cgdb 指令一览
1 前置准备
程序的发布方式有两种, debug 模式
和 release 模式
, Linux gcc/g++ 出来的⼆进制程序,默认是 release 模式。
程序要调试,必须是debug模式
要使用 gdb 调试,必须在使用 gcc/g++ 源代码⽣成⼆进制程序的时候,加上 「-g」 选项,如果没有添加,程序无法被编译
- 「-g」选项:让最后形成的可执行程序,添加调试信息 —— d e b u g debug debug 模式
$ gcc mycmd.c -o mycmd # 默认模式,不⽀持调试
$ file mycmd
mycmd: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically
linked, interpreter /lib64/ld-linux-px86-64.so.2,
BuildID[sha1]=82f5cbaada10a9987d9f325384861a88d278b160, for GNU/Linux
3.2.0, not stripped
$ gcc mycmd.c -o mycmd -g # debug模式
$ file mycmd
mycmd: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically
linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=3d5a2317809ef86c7827e9199cfefa622e3c187f, for GNU/Linux
3.2.0, with debug_info, not stripped
样例代码:
#include <stdio.h>
int Sum(int s, int e)
{
int result = 0;
for(int i = s; i <= e; i++)
{
result += i;
}
return result;
}
int main()
{
int start = 1;
int end = 100;
printf("I will begin\n");
int n = Sum(start, end);
printf("running done, result is: [%d-%d]=%d\n", start, end, n);
return 0;
}
2 快速认识 gdb
g d b gdb gdb下载:
- sudo apt install -y gdb / sudo yum install -y gdb
进入调试:
gdb 可执行文件
退出调试:
q(quit)
常见选项:
「-q」
:安静模式,不打印介绍信息和版本信息
查看源码:
l(list)
: 查看源程序代码,默认显示 10 行,按回车键继续看余下的。 后面可以跟文件名表明要看的指定文件,默认当前文件l(list) 【指定文件】
注:l(list)
后面加行号,查看指定行号的前后十行
3 cgdb/gdb 的使用
3.1 简单认识 cgdb
看了
g
d
b
gdb
gdb 的基础操作,许多小伙伴都会认为
g
d
b
gdb
gdb 难用。也难怪,我调试的时候连看个源代码都那么麻烦,我还怎么调试。
为了解决
g
d
b
gdb
gdb 难用的问题,可以使用 cgdb
进行调试
c g d b cgdb cgdb 和 g d b gdb gdb 的命令是一模一样的,但 c g d b cgdb cgdb 可以给我们动态呈现我们的代码
cgdb下载指令:
- Ubuntu: sudo apt-get install -y cgdb
- Centos: sudo yum install -y cgdb
cgdb 界面:
3.2 打断点 / 删断点
开始调试(相当于VS:F5):
r(run)
:程序开始运行,遇到断点停止否则运行至结束
程序运行中可以再次按:「r」,从新开始调试( gdb 会出现提示,按 y 即可)
设置断点(相当于VS:F9):
b(break)
:在某行设置断点 使用指令: b [行号] / b [指定文件:] [行号] / b [指定文件:][指定函数名]
注:给函数名打断点实际上是给函数的第一句指令打断点
查看断点:
- 使用指令:
info b
显示断点信息对应的含义:
- Num:断点编号
- Disp:断点执行一次之后是否有效 keep:有效 dis:无效
- Enb: 当前断点是否有效 y:有效 n:无效
- Address:断点内存地址
- What:位置
删除断点:
- 使用指令:
d(delete) 断点编号
注:删除断点只能根据断点编号删除,不能根据行号删除
2 号断点被删除后,3 号断点的编号并没有变成 2 号,新增的断点也是从 4 号开始。所以断点编号不会被修改,依次线性递增,除非退出再进入 gdb / cgdb。
禁用断点:
断点是可以被使能的,即用的时候打开,不用的使用关闭(不是删除)
为什么要禁用断点呢?我既然不用了,直接删掉它不就行了吗。每个断点打的位置都是有讲究,可能这次调试你不需要用这断点,便把断点删掉了,但下次再调试就不知道之前这个断点打在哪里了。
正确的做法是将断点使能(使之能或使之不能),这样的断点的痕迹还在,就能找到历史的调试痕迹
使用指令:
disable 断点编号
注:只有打断点是行号,其他对断点的操作都是断点编号
打开断点:
- 使用指令:
enable 断点编号
3.3 逐过程 / 逐语句
逐过程(相当于VS:F10)
- 使用指令:
n(next)
逐语句(相当于VS:F11)
- 使用指令:
s(step)
注:gdb / cgdb 会自动记录最新的指令,按 “回车” 可以自动执行最新的指令
3.4 查看变量
临时查看变量值
- 使用指令:
p 变量名
常久显示变量值(相当于VS的监视窗口)
- 使用指令:
display 变量名
删除显示的变量名
- 使用指令:
undisplay 常显示的编号
查看当前函数定义的所以临时变量
- 使用指令:
info locals
调用栈帧:
- 使用指令:
bt
Sum的函数栈帧 递归类的程序我们就可以用
bt
来看其调用栈
3.5 快速跳转
运行至下一个断点处(相当于VS:F5)
- 使用指令:
c(continue)
直接执行到当前函数返回处,然后停止
- 使用指令:
finish
跳出当前循环
- 使用指令:
until 行号
until
可以用来跳转至指定行,常用来跳出当前确定没出错的循环。严格意义上不是跳转,而是将循环当前循环跑完了
4 cgdb/gdb 调试技巧
4.1 watch
「watch」
可以用来监视变量或者某个表达式的变化,如果监视的表达式在程序运行期间的值发⽣变化,gdb会通知使用者
删除「watch」和删除断点一样,使用「d」指令
「watch」有什么用呢?
最主要的用途是:如果你有⼀些变量不应该修改,但是你怀疑它修改导致了问题,你可以「watch」它,如果变化了,就会通知你
4.2 「set var」确定问题原因
我们故意写个错误代码:我们设置一个标志位 flag,为 1 得到正数 result;-1 得到负的 result,但现在粗心的将 flag 的值设为 0
进入 gdb 利用「set var」调试
所以我们可以用「set var」确定并更改我们出错的原因
4.3 条件断点
int Sum(int s, int e)
{
int result = 0;
for(int i = s; i <= e; i++)
{
result += i;
}
return result * flag;
}
上述函数,如果我们想直接看 i = 10 时的各变量值怎么办,即跳过部分循环。
在VS调试中,我们往往是,添加语句 if(i == 10)
,并在该语句打断点
int Sum(int s, int e)
{
int result = 0;
for(int i = s; i <= e; i++)
{
if(i == 10)
printf("hehe"); //添加语句,并将断点打在该语句上
result += i;
}
return result * flag;
}
在 gdb 的调试中,我们可以用
条件判断
来做到
- 使用指令:
b 行号 if 条件
例如:result += i;
语句在第12行
删除条件断点的方法同样是用命令:「d」
给已经存在的断点设置条件
- 使用指令:
condition 断点编号 条件(注:没有if)
5 概念理解
调试的本质是什么?
调试的本质是帮我们解决问题吗?我们仔细想一想,好像 VS 的调试器、gdb/cgdb 等工具并没有帮我们解决,它们仅仅是给程序员追踪内存、查看变量等,真正去解决问题的还是我们自己
。
所以调试的第一件事情就是先找到问题。调试器就是帮我们找到问题的,找到问题后,我们要查看问题代码的上下文做排除,最后才能解决问题。解决问题并不是调试器帮我们解决问题,是我们自己解决问题。
所以,我们用调试器调试时,最核心的就是用调试器找到问题。
gdb 有很多命令是帮我们找到问题的,最核心的就是:
c
:通过断点进行分块,以块为单位快速定位区域finish
:确认问题是否在当前函数内until
:局部区域快速执行
如何快速找到问题所在呢?
比如现在有 100 行代码,我们可以分别在第 25 行、50 行、75 行打上断点,这样我们就利用断点完成了代码的分块
。让程序直接运行到各个断点处,就能知道是哪一块代码出现问题了。
在工程项目中,往往几十上百万行代码,又如何利用断点快速找到问题的根源呢?
我们可以现在项目的中间处打个断点,看问题出现那个部分;再在出问题部分的中间打个断点……以此类推,用二分查找的方式快速找到问题所在
断点的本质是什么?
断点的本质是对代码进行切块,以块为单位快速定位问题区
6 gdb/cgdb 指令一览
命令 | 作用 | 样例 |
---|---|---|
list/l | 显示源代码,从上次位置开始,每次列出10行 | list / l 10 |
list/l 函数名 | 列出指定函数的源代码 | list / l main |
list / l 文件名:行号 | 列出制定文件的源代码 | list / l mycode.c:1 |
r / run | 从程序开始连续执行 | run / r |
n / next | 单步执行,不进入函数内部 | n / next |
s / step | 单步执行,进入函数内部 | s/ step |
break / b [文件名:]行号 | 在指定行号设置断点 | break 10 / break test.c : 10 |
break / b 函数名 | 在函数开头设置断点 | break main |
info break / b | 查看当前所有断点的信息 | info break / b |
finish | 执行到当前函数返回,然后停止 | finish |
print / p 表达式 | 打印表达式的值 | print start+end |
p 变量 | 打印制定变量的值 | p x |
set var 变量 = 值 | 修改变量的值 | set var i = 10 |
continue / c | 从当前位置开始连续执行程序 | continue |
delete / d breakpoints | 删除所有断点 | delete breakpoints |
delete / d n | 删除序号为 n 的断点 | delete / d 1 |
disable breakpoints | 禁用所有断点 | disable breakpoints |
enable breakpoints | 启用所有断点 | enable breakpoints |
info / i breakpoints | 查看当前设置的断点列表 | indo breakpoints |
display 变量名 | 跟踪显示制定变量的值(每次停止时) | display x |
undisplay 编号 | 取消对指定变量的跟踪显示 | undisplay 1 |
until 行号 | 执行到指定行号 | until 20 |
backtrace / bt | 查看当前执行栈的各级函数调用及参数 | backtrace / bt |
info/i locals | 查看当前栈帧局部变量值 | info locals |
quit | 退出gdb调试器 | quit |
好啦,本期关于调试器 gdb/cgdb的使用就介绍到这里啦,希望本期博客能对你有所帮助。同时,如果有错误的地方请多多指正,让我们在 Linux 的学习路上一起进步!