gdb调试0基础教程
调试
我会先介绍一遍基础的调试知识,随后用一个精心准备代码示例来调试找到问题所在,尽可能用到所有常用的调试命令,希望你能跟着我说的尝试一遍,相信完成之后你会稍微体会到gdb
的强大之处
编译
想要能够调试必须让编译携带调试信息,需要带上-g
命令
g++ main.cpp -o main -Wall -g # Wall是用于输出警告信息建议加上,想要调试一定要用gdb
如果没有安装则安装一下gdb
sudo apt install gdb
开始调试
使用如下命令进入调试
gdb main #main为可执行程序
退出使用
quit
调试常用命令
我会在这里给你列举一下调试常用的命令,不做示例讲解,随后在进行示例演示,以便于日后你查看(你可以使用简写也能使用括号里的全称)
r(run)
运行程序,或者是重新运行q(quit)
退出gdbb(break
打断点n(next)
一行行运行逐过程,不会进入函数体s(step)
单步运行,会进入函数体fin(finish)
类似与vscode的单步跳出,你可以跳出函数c(continue)
继续运行程序直到他停住或者终止i(info)
查看信息i b
查看所有断点信息i locals
查看所有局部变量l(ls)
下翻文件,显示10行d(delete)
删除断点d 1
删除1号断点,如果不指定则直接删除所有ig(ignorance)
忽略断点 例如ig 1 10
忽略一号断点十次p(print)
打印变量值p x
打印x的值 也可以修改表达式的值p x =10
ba(backtrace)
显示调用堆栈f(frame)
选择堆栈condition
为已有断点添加条件dis(disable)
禁用断点en(enable)
启用断点pt(ptype)
打印变量类型disp(display)
显示表达式的值lay(layout)
显示layout asm
显示汇编layout src
显示文件
设置命令行参数
set args arg1 arg2 arg3 # 注意set args为命令 arg1为你想要设置的第一个参数
假设我们编写了这样的c++程序,他需要命令行参数
#include <iostream>int main(int argn, char *args[])
{ using namespace std;cout << "共有 " << argn << "个参数"<<endl;for(int i=0;i<argn;i++){cout << args[i]<<" ";}cout << endl;return 0;
}
先进行编译
g++ main.cpp -o main -Wall -g
测试调用
进入gdb调试
使用run
直接运行,可以看到默认的启动命令是/home/wxy/main
,gdb
支持前缀原则,你用r
也能运行
使用set args
设置命令行参数
示例
将用一个简单的例子带你了解一下调试的完整流程,使用的代码如下
//fun.h
int factorial(int n){ if(n==0){return 1;} return n*factorial(n-1);
}
一个非常简单的计算阶乘的函数
//main.cpp#include <iostream>#include "fun.h"int main(int argc, char* argv[]){using namespace std;int FAC=stoi(argv[1]);for(int i=0;i<FAC;i++){cout << i << "!= " << factorial(i) << endl;}return 0;}
一个接受一个参数用于生成阶乘并打印的函数
编译
g++ main.cpp -o main -g // 注意加上-g我们需要调试信息
试运行一下
没有问题,但是在参数为20的时候发现出现了负数,这不正常,希望能调试一下看看(聪明的你知道这是超出了int
的范围,不过先不管)
开始调试
进入调试
gdb main
我们可以使用l(list)
来查看一下当前的文件
你可以使用l filename
来查看别的文件
例如我们这里有fun.h
l fun.h:1 // 显示1行附近的内容
你也可以用函数名
注意我们现在需要一个参数,直接用r
运行是不行的
通过 set args 20
来设定参数
或者r 20
这样附加参数
这样程序可以直接运行了
随后需要加上断点
cout << i << "!= " << factorial(i) << endl;
我们需要在这一行加上断点
这是在第八行所以直接
(gdb)b 8
接下来可以试着运行一下r
如何使程序可以在函数里面中断呢?
你也许想到了单步调试使用s(step)
可以看到这进入了<<
的重载运算符,并不是我们想要的factorial
你可以使用fin(finish)
来跳出这个过程
可以看到这个的返回值
再使用一次单步调试,可以看到这里是为了输出!=
继续跳出
再按一次s
可以看到进入了我们想要的factorial
用l
查看一下我们处于fun.h
文件之中了
此时你可以直接用
b 5
为第5行加上断点
按c(continue)
可以让程序运行
可以看到停到了我们新加的断点
再按一次c
可以看到程序跳出了递归,没有终端因为没有运行到第五行直接从if
返回了,打印输出了
1!=1
使用i b (info break)
来查看断点信息
可以看到1号断点已经命中了两次,2号则是1次
现在删除2号
d 2
在实际的过程中,大部分情况下你其实没必要这样单步调试过去,完全可以直接给别的文件直接加上断点
b fun.h:5
现在有一个问题17!
是出现问题的,我们需要跳到第17次循环,可以利用我们提到的(ig)ignore
使用p(print)
打印一下i
的值
可以看到当前是第二次循环
我们想跳到第17次,可以忽略1号断点14次来试试看
可以看到info
信息显示也能看到设置了忽略
开始按c
运行!
oops!出现了点小问题,三号断点会影响我们的循环,我们还需要他,所以请不要删除,可以使用dis(disable)
禁用他就好
可以看到Enb
这一位被我们设置成了n
即代表no
即被我们禁用了,他将不会再起作用
继续运行c(continue)
可以看到现在准确地停在了i=17
的位置,接下来别忘了启用我们禁用的断点,使用(en)enable
可以看到成功启用,然后你可以注意到1号断点即使使用了ignore
也能让其显示正确的被命中次数
继续运行
接下来我们换一种更简单的方式来进行中断,我们想看到factorial的逐级运算结果,想先进入到最深层的递归,可以看到递归的退出条件是n=0
,所以我们利用(cond)condition
来为3号断点加上条件
cond 3 n==1
只有在n==1
时断点才停止
注意你也可以在创建断点时为其加上条件
b fun.h:5 if n==1
注意这个if
是必不可少的
基本上支持c语言的大部分逻辑操作
开始运行
准确的停到了n==1
时,比你计算命中次数方便地多
可以再使用cond
删除断点条件
cond 3
按s
单步运行,可以看到马上要退出循环了
我们可以用bt(backtrace)
来查看一下堆栈的信息
可以看到当前是#0
我们可以通过f(frame)
来选择堆栈号
我们跳到了main
的堆栈
试一下我们学的打印本地变量的操作
info locals
这可以帮助你查看每个堆栈的信息
回到当前的堆栈
单步运行,马上要执行返回了
可以利用layout src
打开界面查看一下
继续单步运行
可以看到layout
帮我们直接显示了当前代码执行的位置,如果你喜欢可以使用
即将返回
这里我又单步了一次,但是你可以注意到没有返回值,你可以使用fin
查看到返回值
可以看到当前的n为2,我们可以预想到下一个返回值应该是1*2=2
,结果如我们所料
设置一个display
这样每次fin
的时候都会帮我打印n
的值
可以利用layout asm
打开汇编代码
你可以看到我们的返回值其实是在寄存器eax
里面,所以其实你可以利用他来打印返回值
p $eax
注意你不输入指令直接按下回车即可重复上一条命令
你可以看到12的阶乘为479001600这没有问题,但是13的阶乘为1932053504,这比实际的13的阶乘6227020800要小,这是由于整形的表示最大的值也只有2147483647发生了溢出
12!的二进制为 0001 1100 1000 1100 1111 1100 0000 0000 479001600
13!的二进制为 0001 0111 0011 0010 1000 1100 1100 0000 0000
发生了异常,由于使用的是int
所以只会有32为保留
0111 0011 0010 1000 1100 1100 0000 0000 正好就是 1932053504
结束
充分运用好例子里面提供的指令,在调试的时候基本够用了,如果你想了解更多的指令内容可以用help 指令
的形式查看
gdb
的内容可能比你想象的要多非常多,你可以在这里查看,有一本书专门的讲解,如果你需要的功能非常复杂的话,可以看这里