Linux操作系统从入门到实战(十五)详细讲解Linux调试器 gdb/cgdb使用
- 前言
- 一、gdb/cgdb是什么?
- 1. 程序的两种发布模式(debug 和 release)
- 二、gdb/cgdb如何启动?
- 1. 准备工作
- 2. 启动 gdb/cgdb 调试器
- 2.1 启动 gdb
- 2.2 启动 cgdb(更友好的界面)
- 3. 核心操作(让程序在指定位置停下)
- 1. 给函数打断点(按函数名)
- 2. 给行打断点(按文件名+行号)
- 3. 查看已设置的断点
- 4. 运行程序 & 让程序在启动时停下
- 1. 运行程序:r 命令(run 的缩写)
- 2. 让程序“启动时就停下”(刚进入 main 就暂停)
- 5. 删除断点(不需要的断点可以移除)
- 1. 按断点编号删除:delete 断点编号
- 2. 按位置删除:clear 位置
- 三、gdb/cgdb的常见使用
- 1. 基础操作:启动与退出
- 2. 查看源代码:调试时“看到”代码
- 1. list 或 l:显示代码(默认每次10行)
- 2. list 函数名 或 l 函数名:查看指定函数的代码
- 3. list 文件名:行号 或 l 文件名:行号:查看指定文件的指定行
- 3 控制程序执行
- 1. run 或 r:启动程序(从头开始执行)
- 2. next 或 n:单步执行(不进函数)
- 3. step 或 s:单步执行(进函数)
- 4. continue 或 c:从当前位置继续运行
- 5. finish:执行到当前函数结束
- 6. until 行号 或 u 行号:快速执行到指定行
- 7. 断点管理
- 8. 查看和修改变量
- 1. print 变量 或 p 变量:打印变量值
- 2. print 表达式 或 p 表达式:计算表达式结果
- 3. set var 变量=值:临时修改变量值
- 4. display 变量:自动跟踪变量值
- 5. undisplay 编号:取消跟踪变量
- 9. 查看调用栈
- 1. backtrace 或 bt:查看函数调用栈
- 2. info locals或 i locals:查看当前函数的所有局部变量
- 常用命令速查表
前言
在之前的内容中,我们围绕 Linux 环境,系统讲解了 Git 版本控制系统的核心知识。
本篇博客将聚焦 调试工具领域,正式开启 GDB/CGDB 调试器 的全方位使用指南。
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的Linux知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12879535.html?spm=1001.2014.3001.5482
一、gdb/cgdb是什么?
- 简单说,它们是调试工具。
- 写程序时难免会出错(比如程序突然崩溃、结果不对),这时候就需要工具帮我们找出问题在哪行代码、为什么出错——gdb 就是干这个
- cgdb 可以理解成 gdb 的“增强版”,带了更友好的界面(比如能同时显示代码和调试信息),用起来更方便,但核心功能和 gdb 差不多。
1. 程序的两种发布模式(debug 和 release)
你写的代码(比如 .c
或 .cpp
文件),需要通过 gcc/g++
编译成能直接运行的程序(比如 Windows 里的 .exe
,Linux 里的可执行文件)。
编译时可以选两种模式:
-
debug 模式:
编译出来的程序会带很多“调试信息”(比如哪行代码对应程序里的哪个位置),就像给程序加了“标记”,方便 gdb 这类工具定位错误。但因为带了这些信息,程序会大一点,运行速度也稍慢。适合开发时用(写代码、找错阶段)。 -
release 模式:
编译时会去掉调试信息,还会做一些优化(让程序更小、运行更快)。适合最终发布给用户用(比如你写的工具、软件,给别人用的时候就用这种模式)。 -
关键:用 gdb 调试必须加
-g
Linux 里用 gcc/g++
编译时,默认是 release 模式(不加 -g
的话,编译出来的程序没有调试信息)。
- 这时候如果想用 gdb 调试,gdb 会“看不懂”程序(找不到代码位置),没法帮你找错。
所以,如果我们想在开发阶段用 gdb 调试,编译时必须加 -g
选项(告诉编译器:用 debug 模式,保留调试信息)。
比如:
gcc -g test.c -o test
(编译 test.c,生成带调试信息的程序 test,之后就能用 gdb 调试了)
二、gdb/cgdb如何启动?
1. 准备工作
在开始调试前,必须用 -g
选项编译程序(生成带调试信息的版本)。
我们需要有个简单的 C 程序 test.c
:
#include <stdio.h>// 加法函数
int add(int a, int b) {return a + b;
}int main() {int x = 5;int y = 3;int result = add(x, y);printf("结果是:%d\n", result);return 0;
}
编译命令(必须加 -g
):
gcc -g test.c -o test # 生成可执行文件 test,带调试信息
2. 启动 gdb/cgdb 调试器
2.1 启动 gdb
在终端输入以下命令,启动 gdb 并加载要调试的程序:
gdb ./test # ./test 是刚才编译好的程序
启动后会进入 gdb 的交互界面,显示类似这样的信息(最后一行是 (gdb)
提示符,等待输入命令):
GNU gdb (GDB) Red Hat Enterprise Linux 10.2-6.el9
...
Reading symbols from ./test...
(gdb) # 这里可以输入调试命令
2.2 启动 cgdb(更友好的界面)
cgdb 是 gdb 的“增强版”,能同时显示代码和调试信息,操作命令和 gdb 完全一样。
如果没安装,先在 CentOS 9 上安装:
sudo dnf install cgdb # 安装 cgdb
启动 cgdb 的命令和 gdb 类似:
cgdb ./test # 启动后界面会分成上下两部分,上面显示代码,下面是命令行
3. 核心操作(让程序在指定位置停下)
断点是调试的核心——程序运行到断点处会自动暂停,方便我们查看变量、一步步执行代码。
1. 给函数打断点(按函数名)
命令:b 函数名
(b 是 break 的缩写)
例子:给 add
函数设置断点
(gdb) b add # 给 add 函数设置断点
Breakpoint 1 at 0x400526: file test.c, line 5. # 提示:断点1已设置,在 test.c 第5行
2. 给行打断点(按文件名+行号)
命令:b 文件名:行号
例子:给 main
函数里的第8行(int result = add(x, y);
)设置断点
(gdb) b test.c:8 # 给 test.c 的第8行设置断点
Breakpoint 2 at 0x400540: file test.c, line 8. # 提示:断点2已设置
3. 查看已设置的断点
命令:info breakpoints
(或简写 info b
)
(gdb) info b # 查看所有断点
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400526 in add at test.c:5
2 breakpoint keep y 0x0000000000400540 in main at test.c:8
Num
是断点编号(后面删除断点会用到);Enb
是“是否启用”(y 表示启用,n 表示禁用)。
4. 运行程序 & 让程序在启动时停下
1. 运行程序:r 命令(run 的缩写)
在 gdb 中输入 r
,程序会开始运行,直到遇到断点才会暂停。
例子:我们已经设置了 add
函数和第8行的断点,运行程序:
(gdb) r # 启动程序运行
Starting program: /home/user/test Breakpoint 2, main () at test.c:8 # 程序在断点2(第8行)停下了
8 int result = add(x, y);
(gdb) # 此时可以进行下一步操作(如查看变量、单步执行等)
2. 让程序“启动时就停下”(刚进入 main 就暂停)
如果想让程序一启动就停下(还没执行任何逻辑),最常用的方法是给 main
函数设置断点:
(gdb) b main # 给 main 函数设置断点
Breakpoint 3 at 0x400535: file test.c, line 6.(gdb) r # 运行程序
Starting program: /home/user/test Breakpoint 3, main () at test.c:6 # 直接停在 main 函数的第一行代码
6 int x = 5;
(gdb) # 此时可以从程序入口开始一步步调试
5. 删除断点(不需要的断点可以移除)
1. 按断点编号删除:delete 断点编号
例子:删除编号为2的断点(之前设置的第8行断点)
(gdb) delete 2 # 删除断点2
(gdb) info b # 查看剩余断点,会发现编号2的断点已消失
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400526 in add at test.c:5
3 breakpoint keep y 0x0000000000400535 in main at test.c:6
2. 按位置删除:clear 位置
例子:删除 add
函数的断点
(gdb) clear add # 删除 add 函数的断点
Deleted breakpoint 1
如果想删除所有断点,直接输入 delete
(不加编号):
(gdb) delete # 删除所有断点
Delete all breakpoints? (y or n) y # 输入 y 确认
三、gdb/cgdb的常见使用
1. 基础操作:启动与退出
这是调试的“开门”和“关门”操作,必须先掌握。
命令 | 作用 | 例子 |
---|---|---|
gdb 程序名 | 启动 gdb 并加载程序 | gdb ./test |
cgdb 程序名 | 启动 cgdb 并加载程序 | cgdb ./test |
quit 或 q | 退出调试器 | 输入 q 后回车 |
ctrl + d | 快速退出(和 q 一样) | 按住 ctrl 再按 d |
2. 查看源代码:调试时“看到”代码
调试时需要知道当前在执行哪行代码,list
命令专门用来显示源代码。
1. list 或 l:显示代码(默认每次10行)
- 作用:从上次显示的位置继续往下列10行代码(第一次用从程序开头列)。
- 例子:
(gdb) l # 显示前10行代码(如果代码短,会显示全部) 1 #include <stdio.h> 2 3 // 加法函数 4 int add(int a, int b) { 5 return a + b; 6 } 7 8 int main() { 9 int x = 5; 10 int y = 3; (gdb) l # 再按一次,显示接下来的10行 11 int result = add(x, y); 12 printf("结果是:%d\n", result); 13 return 0; 14 }
2. list 函数名 或 l 函数名:查看指定函数的代码
- 作用:直接显示某个函数的源代码(方便定位函数逻辑)。
- 例子:查看
add
函数的代码(gdb) l add # 显示 add 函数的代码 4 int add(int a, int b) { 5 return a + b; 6 } 7 8 int main() { 9 int x = 5; 10 int y = 3; 11 int result = add(x, y);
3. list 文件名:行号 或 l 文件名:行号:查看指定文件的指定行
- 作用:如果程序有多个文件(比如
test.c
、calc.c
),可以精确指定查看某文件的某行。 - 例子:查看
test.c
的第8行(main
函数开始处)(gdb) l test.c:8 # 显示 test.c 第8行附近的代码 3 // 加法函数 4 int add(int a, int b) { 5 return a + b; 6 } 7 8 int main() { 9 int x = 5; 10 int y = 3; 11 int result = add(x, y); 12 printf("结果是:%d\n", result);
3 控制程序执行
这是调试的核心——控制程序执行节奏,想让它停就停,想让它走就走。
1. run 或 r:启动程序(从头开始执行)
- 作用:让程序从入口(如
main
函数)开始运行,直到遇到断点才停下(没断点就直接跑完)。 - 例子:之前设置了
main
函数断点,运行程序(gdb) r # 启动程序 Starting program: /home/user/test Breakpoint 1, main () at test.c:9 # 停在 main 函数的第9行 9 int x = 5;
2. next 或 n:单步执行(不进函数)
- 作用:执行当前行代码,然后停下;如果当前行是调用函数(比如
add(x,y)
),不进入函数内部,直接执行完整个函数再停下。 - 例子:当前停在
main
函数第9行,用n
执行下一步(gdb) n # 执行第9行(int x=5;),停到第10行 10 int y = 3; (gdb) n # 执行第10行(int y=3;),停到第11行 11 int result = add(x, y); (gdb) n # 执行第11行(调用 add 函数),不进入 add 内部,直接得到结果,停到第12行 12 printf("结果是:%d\n", result);
3. step 或 s:单步执行(进函数)
- 作用:和
next
类似,但如果当前行是调用函数(比如add(x,y)
),会进入函数内部,停在函数的第一行代码。 - 例子:同样停在第11行,用
s
执行(gdb) s # 执行第11行,进入 add 函数内部 add (a=5, b=3) at test.c:5 # 停在 add 函数的第5行 5 return a + b; (gdb) s # 执行 add 函数第5行,返回 main 函数 main () at test.c:12 # 回到 main 函数第12行 12 printf("结果是:%d\n", result);
4. continue 或 c:从当前位置继续运行
- 作用:程序在断点处停下后,用
c
让它继续往下跑,直到遇到下一个断点(或程序结束)。 - 例子:在
main
第9行停下后,想直接跑到add
函数断点(gdb) c # 从第9行继续运行 Continuing. Breakpoint 2, add (a=5, b=3) at test.c:5 # 遇到 add 函数断点停下 5 return a + b;
5. finish:执行到当前函数结束
- 作用:如果现在在某个函数内部(比如
add
函数),用finish
会执行完这个函数的所有代码,然后回到调用它的地方停下。 - 例子:在
add
函数内部时(gdb) finish # 执行完 add 函数 Run till exit from #0 add (a=5, b=3) at test.c:5 main () at test.c:12 # 回到 main 函数调用 add 的下一行 12 printf("结果是:%d\n", result); Value returned is $1 = 8 # 还会显示函数的返回值(8)
6. until 行号 或 u 行号:快速执行到指定行
- 作用:从当前位置直接跑到指定行停下(比一次次按
n
快,适合跳过中间无关代码)。 - 例子:当前在
main
第9行,想直接跑到第12行(gdb) u 12 # 执行到第12行停下 main () at test.c:12 12 printf("结果是:%d\n", result);
7. 断点管理
除了之前讲的“设置断点”和“删除断点”,还有两个常用操作:暂时禁用(不用删除,以后还能用)和重新启用。
命令 | 作用 | 例子 |
---|---|---|
info break 或 info b | 查看所有断点信息 | info b # 显示断点编号、位置等 |
disable 断点编号 | 禁用指定断点(暂时失效) | disable 1 # 禁用编号1的断点 |
enable 断点编号 | 启用指定断点(恢复生效) | enable 1 # 启用编号1的断点 |
disable breakpoints | 禁用所有断点 | disable breakpoints |
enable breakpoints | 启用所有断点 | enable breakpoints |
例子:
(gdb) info b # 查看当前有2个断点
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400526 in add at test.c:5
2 breakpoint keep y 0x0000000000400535 in main at test.c:9(gdb) disable 1 # 禁用断点1(add函数的断点)
(gdb) info b # 查看断点1的Enb变成n(禁用)
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000000000400526 in add at test.c:5
2 breakpoint keep y 0x0000000000400535 in main at test.c:9(gdb) enable 1 # 重新启用断点1
(gdb) info b # 断点1的Enb变回y(启用)
8. 查看和修改变量
调试的核心目的之一是看变量的值对不对,甚至可以临时改值测试。
1. print 变量 或 p 变量:打印变量值
- 作用:显示当前作用域内某个变量的值(比如
x
、result
)。 - 例子:当前在
main
第10行,打印x
和y
的值(gdb) p x # 打印 x 的值 $1 = 5 (gdb) p y # 打印 y 的值 $2 = 3
2. print 表达式 或 p 表达式:计算表达式结果
- 作用:不仅能打印变量,还能计算表达式(比如
x+y
、result*2
)。 - 例子:
(gdb) p x + y # 计算 x+y 的结果 $3 = 8 (gdb) p result * 2 # 假设 result 是8,计算 8*2 $4 = 16
3. set var 变量=值:临时修改变量值
- 作用:调试时发现变量值错了,可以临时修改,看程序后续执行是否正常(不用改代码重新编译)。
- 例子:把
y
的值从3改成10(gdb) p y # 原来 y 是3 $5 = 3 (gdb) set var y=10 # 临时改成10 (gdb) p y # 确认修改成功 $6 = 10 (gdb) n # 继续执行,此时 add(x,y) 会计算 5+10=15
4. display 变量:自动跟踪变量值
- 作用:设置后,每次程序暂停(比如单步执行、遇到断点),会自动显示这个变量的值(不用每次手动
p
)。 - 例子:跟踪
result
变量(gdb) display result # 开始跟踪 result 1: result = 0 # 初始值(还没赋值) (gdb) n # 执行到赋值 result 的行 11 int result = add(x, y); (gdb) n # 赋值后,自动显示 result 的新值 12 printf("结果是:%d\n", result); 1: result = 15 # 自动显示更新后的值
5. undisplay 编号:取消跟踪变量
- 作用:
display
会给每个跟踪的变量分配一个编号,用undisplay 编号
取消跟踪。 - 例子:取消跟踪上面的
result
(编号是1)(gdb) undisplay 1 # 取消编号1的跟踪 (gdb) n # 再执行,不会自动显示 result 了 13 return 0;
9. 查看调用栈
当程序调用多个函数时(比如 main
调用 add
),backtrace
能显示“谁调用了谁”,方便定位当前执行位置。
1. backtrace 或 bt:查看函数调用栈
- 作用:显示从程序启动到当前位置的所有函数调用关系(最上面是当前执行的函数,往下是调用它的函数)。
- 例子:在
add
函数内部时(gdb) bt # 查看调用栈 #0 add (a=5, b=10) at test.c:5 # 当前在 add 函数 #1 0x0000000000400554 in main () at test.c:11 # add 是被 main 第11行调用的
2. info locals或 i locals:查看当前函数的所有局部变量
- 作用:一次性显示当前所在函数(比如
add
或main
)的所有局部变量及其值,不用一个个print
。 - 例子:在
main
函数内部时(gdb) i locals # 显示 main 函数的局部变量 x = 5 y = 10 result = 15
常用命令速查表
为了方便记忆,整理成表格(按使用频率排序):
功能分类 | 命令 | 作用描述 |
---|---|---|
基础操作 | gdb 程序名 | 启动调试 |
q | 退出调试 | |
查看代码 | l / l 函数名 | 显示源代码(默认10行/指定函数) |
执行控制 | r | 启动程序 |
n | 单步执行(不进函数) | |
s | 单步执行(进函数) | |
c | 继续运行到下一个断点 | |
断点管理 | b 行号/函数名 | 设置断点 |
info b | 查看所有断点 | |
delete 编号 | 删除指定断点 | |
变量操作 | p 变量 | 打印变量值 |
set var 变量=值 | 修改变量值 | |
display 变量 | 自动跟踪变量值 | |
函数与栈 | finish | 执行到当前函数结束 |
bt | 查看函数调用栈 | |
i locals | 查看当前函数的局部变量 |
以上就是这篇博客的全部内容,下一篇我们将继续探索Linux的更多精彩内容
我的个人主页
欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的Linux知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12879535.html?spm=1001.2014.3001.5482
非常感谢您的阅读,喜欢的话记得三连哦 |