目录
一 简介
二 功能:
三 命令:
四 调试准备:
五 开始调试:
5.1 添加断点:
5.2 条件编译
5.3 断点查看
5.4 断点删除:
5.5 查看源码
5.6 单步调试(逐过程):
5.7 断点调试:
5.8 单步跟踪(逐语句):
5.9 调试过程:
5.9.1 开始调试
5.9.2 查看变量
5.9.3 显示变量:
5.9.4 监控变量:
5.9.5 设置变量:
六 段错误追踪
6.1 设置核心文件大小:
6.2 设置核心文件路径:
6.3 追踪核心文件:
一 简介
- UNIX及UNIX-like下的调试工具。虽然它是命令行模式的调试工具,但是它的功能强大到你无法想象,能够让用户在程序运行时观察程序的内部结构和内存的使用情况。
二 功能:
GDB的功能很是强大,主要列出了几点常用且重要的。要值得注意的是只有debug程序可以进行GBD调试,如果是releas程序是不可以进行调试的。
- 条件调试: 可以自定义断点的调试方式,以及触发条件等。
- 变量监视: 可以动态的监视某个值的变化
- 动态改变程序执行环境:可以在断点处修改程序目前变量的值,从而在不重新运行程序的情况下,用预期的值来进行调试。
- 分析上下文: 可以分析出程序目前的上下文堆栈调用情况
- 段错误分析: 这个是个很重要的点,可以帮助我们找到程序崩溃的源码。
三 命令:
命令 | 作用 | 使用方式 |
gdb | 进入调试环境 | gdb 可执行文件名 |
r(run) | 重新开始运行可执行文件 | r或者run |
b(break) | 设置断点,可以针对不同文件,函数名设置 | b或者break |
info b | 查看断点信息 | info b 或者info break |
d | 删除断点 | d 断点编号 |
d break | 删除所有断点 | d break不能使用d b |
disable/enable | 使断点无效/有效 | 指定断点无效:disable 断点编号 使所有断点无效: disable b |
n(next) | 逐过程,相当于F10,不会进入函数体.单步执行 | n或者n 【数字】 可以一步也可以指定多步 |
s(step) | 逐语句【相当于F11,所有语句都会进入 | s或者s 【数字】 可以一步也可以指定多步 |
c(continue) | 从一个断点处,直接运行至下一个断点处【F5】 | c |
p | 打印变量名 | p 【变量名】 |
四 调试准备:
- 首先我们准备一段测试demo。
#include <stdio.h>
// 一个简单的函数,用于演示函数调用
void print_message(const char* message) {
int i = 0;
for(i = 0; i < 10; ++i)
{
printf("%d+%s\n", i, message);
}
}
int main() {
// 未初始化的指针
char* ptr = (char*)malloc(50);
// 尝试打印通过未初始化指针指向的数据,这将导致段错误
printf("The secret message is: %s\n", ptr);
// 正常情况下会调用此函数,但由于前面的段错误,可能无法执行到
print_message("This message should never appear");
return 0;
}
- 首先我们不带编译信息来编译这段程序,也就是release模式
- 然后我们使用gdb main命令来进行调试这个可执行程序
- 这时候我么会发现,No debugging symbols found in main),表示这个可执行程序并不能够进行调试。
gcc main.c -o main
- 所以在编译时我们应该加上-g选项,如果是其他编译工具,对应的生成debug下的可执行文件即可。
- 这时候Reading symbols from main... 就表示可以进行调试了。
- 还有两种方式也能够查看:使用readelf和objdump命令
五 开始调试:
- 使用gdb main(对应自己可执行程序文件)
- 设置断点: 如果未设置调试信息(断点)直接输入r(运行程序),那么就会和正常运行程序一样,直到程序结束。
5.1 添加断点:
有两种方式一种是指定行号,一种是指定函数名。
- break 文件名:行号 如果不写文件名那么会在主函数的对应行号设置断点
- break 文件名:函数名 如果不写文件名GBD会找到项目整个目录中所有包含这个函数名的地方并打上断点
这里我们分别使用两种方式打上两个断点,对于vi编辑器如果不知道行号我们可以在命令模式下输入
set number命令这样就可以看到行号了。
5.2 条件编译
- 对于断点调试是可以进行设置条件断点的。
- break 文件名: 行号 条件
- 下面这个条件编译也就是当这个指针等于0时才会触发这个断点。
5.3 断点查看
- info b: 可以看到断点编号 位置等信息
Num | 断点唯一标识号 |
Type | 断点的类型,比如全程序断点、条件断点、指令断点等。 |
Disp | 表示断点的处理方式。例如,keep 表示即使断点被命中,它也不会被自动删除;del 则会在首次命中后删除断点。 |
Enb | 表明断点是否启用。y 表示启用,n 表示禁用。 |
Address | 断点所在代码的地址。 |
What | 断点关联的具体位置,可以是源代码文件名和行号,或者是函数名,甚至是内存地址。 |
Cond | 如果设置了条件断点,这里会显示该条件表达式 |
Times | 断点被触发的次数 |
5.4 断点删除:
- d 断点编号: d 后面跟上断点编号
- d break : 删除所有断点,这里会提示是否全部删除
5.5 查看源码
- l: list 默认查看从第一行到20行前20行代码,后续可以直接按下回车查看后面代码,GDB会默认记住上一次的命令
- l 10,20: 查看第10行到第20行的代码,也就是指定行号
- l main.c:print_message: 查看main.c文件中的print_message函数的代码
- l main.c:20: 查看main.c 中第20行的代码(默认从此处后20行)
- l main.c:10,20: 同理查看main.c文件中第10到20行代码
5.6 单步调试(逐过程):
- n: next 从这个断点执行一行源程序代码,遇到函数也会直接调用并执行,不会进入函数.
- n 数字: 单步执行多少行,比如你确认下面的几行是没有问题的,你就可以直接跳过。
这里要注意的是在设置函数的断点时,如果是使用的是 break 函数名设置的断点,使用n是仍然会进入函数的,如果是使用break + 数字(这个函数所在的行号) 这样是可以跳过的。如果这样设置了我们可以利用s 函数名 或者 n 数字 这种方式也可以直接跳出函数。
5.7 断点调试:
- c:continue 会直接跳转到下一个断点处进行调试。
5.8 单步跟踪(逐语句):
- s:skip 执行一行源代码程序,但是如果遇到函数调用,会进入该函数
- s 函数名: 跳出该函数
5.9 调试过程:
5.9.1 开始调试
- 输入r开始运行程序,此时会在程序遇到的第一个断点处停下
5.9.2 查看变量
- print 变量名: 此时我们可以使用print来查看变量值
- print 函数名:变量名: 如果多个函数有同一个变量名我们需要前面加上函数名来区分
- print *ptr: 如果这个值是指针那么我们看到的将是地址,想要查看它的值我们需要解引用:
- 这里我们看到ptr的值是等于0的,这样恰好验证了我们当时的条件编译,如果我们不做初始化ptr那么此时ptr不等于0那么断点也就不会触发。
5.9.3 显示变量:
- display 变量: 跟踪查看一个变量,每次停下来都会显示它的值。
5.9.4 监控变量:
- watch 变量名: 这样在这个变量名改变时程序就会停下来,其实这也是一个断点,我们可以此时查看断点信息,就会发现它也生成了一个断点。
5.9.5 设置变量:
- set 变量 = 值: 这是很有用的,因为它可以让我们改变当前程序变量的值,按照我们想预期的值来执行后面的程序,而不需要重新运行程序。
- 这里我们在for循环中运行第一次的时候,此时的i = 0,我们可以直接设置i = 6 那么下一次就会直接从6开始执行for循环了。
- 当然要值得注意的是,如果报错,我们可以使用set variable 变量 = 值,因为直接使用命令这有可能会导致命令不清晰,GDB不知道是变量还是GBD内部的一些设置。
六 段错误追踪
我们有时候会遇到程序异常崩溃,然后会提示 Core Dump(段错误),这一般是程序调用了非法的内存地址,比如空指针,野指针等造成的,GDB也可以帮助我们找到这个地方。
6.1 设置核心文件大小:
- ulimit -c: 如果后面不跟具体大小,则是显示当前核心文件的大小,如果加上数字表示当前核心文件当前系统最大可以生成核心文件的大小(单位:字节(B))
- ulimit -c unlimited: 取消核心文件大小的限制
6.2 设置核心文件路径:
- 我们最好是将核心文件生成的路径指定,这样方便查找。
- %e表示程序名, %p表示进程id:这样生成的核心文件就会带上当前可执行文件的程序名和进程id了。
echo /home/data/coredump/core.%e.%p> /proc/sys/kernel/core_pattern
6.3 追踪核心文件:
- 这里我们首先把测试程序中指针初始化时给一个非法的地址,这样程序再读取这个地址的值时,就会异常崩溃,触发段错误。
- 然后我们直接运行这个可执行文件,就会发送段错误
- 此时我们查看之前设置的核心文件的生成路径/home/data/coredump就能看到生成的核心文件。
- 接下来我们进入gdb环境: 切记要使用gdb 可执行文件名 不要直接使用GDB进入GDB环境。
- 再进入之后我们使用core-file 核心文件名
- 最后我们使用bt命令查看堆栈信息,就能准确看到崩溃的位置和信息了。
- 我们可以看到确实是再main.c中的第18行导致的崩溃。