gdb调试基础
简介
笔者此前一直做互联网应用层开发,最近换工作,新工作的方向更加偏向底层,以前通过Xcode、Visual Studio进行调试的伎俩在这里不能再用,故有了这篇文章
希望看完这篇文章大家可以跟着笔者一起入门gdb调试,熟悉gdb调试的一些基础概念和调试手法,在碰到相关问题时能够使用掌握的调试手段进行解决
调试编译配置
并不是任何应用在编译完成后即可进行调试,你需要对一些编译选项进行配置,最终的编译产物才能很好的被调试器跟踪。下面笔者将会通过cmake进行编译前的编译选项配置
- 配置
Debug模式:在cmake中可以通过CMAKE_BUILD_TYPE=Debug设置编译产物为Debug包,这样产物中将会有大量调试信息,这很重要 - 通过CMAKE_CXX_FLAGS_DEBUG和CMAKE_C_FLAGS_DEBUG这两个cmake变量添加编译选项
-g3:生成最详细的调试信息(包括宏定义)-O0:禁止进行代码优化,保证源码和指令的一致性-fno-omit-frame-pointer:强制编译器保留函数调用时的“栈帧指针”,栈帧指针是一个指向当前函数栈帧起始位置的指针,调试器通过栈帧指针可以方便的进行调用链回溯。不过GCC在某些优化等级下会使用-fomit-frame-pointer,忽略栈帧指针,通过其它方式计算得到起始地址,这样可以优化性能,但是不利于调试器进行调试-fno-inline:禁止编译器对所有函数进行内联优化。编译器在对某些短小的函数进行内联优化之后,无法在内联函数内添加断点,也看不到对该函数的调用栈帧,所以在编译调试版本时需要关闭内联优化
完成以上编译配置后即可编译生成产物
调试
笔者这里在main函数中写了13个调试的demo,这些demo里面涉及各个方面的调试手段,我将会选择一些相对重要的调试手段进行分享
加载程序
gdb build/gdb_lab笔者这里的被调试的程序为./build/gdb_lab,以上命令为加载被调试程序,它作用有以下三个
- 启动GDB调试器
- 读取
./build/gdb_lab的符号表和调试信息 - 进入GDB的交互式命令行提示符
除此之外它什么也不做。我们将会看到如下输出
程序不会运行,等待我们触发
触发程序运行
在进入GDB交互式命令行之后,需要我们去触发程序运行。一般有如下命令
run(r):run命令(或者其缩写r),这会让被调试程序直接从头开始运行,它不会等你start:start命令,这会让执行流在main函数入口处暂停,等候你的发落next(n):进行下一步,这很简单,执行流会运行到下一个指令处,然后在控制台输出这期间的打印信息step(s):步入命令,和next类似,不同点在于,next遇到函数时不会进入函数,但是step步入指令遇到函数时会进入函数;s 5表示连续执行5次步入continue(c):continue命令,如果是通过start命令触发的可执行程序,在next一段代码之后,如果想快速的执行完后续的代码或者快速的运行到下一个断点处,那么这个命令符合你的需求,它将快速执行完剩下的代码,或者执行到下一个断点处
断点
gdb中,通过break(b)设置断点,设置断点的方式有多种
break 行号:在指定行号处设置断点。如果该行是一个函数调用,那么执行流不会进入函数,next时执行流会执行该行后面的代码。值得注意的是,如果指定的行没有代码,那么gdb会将断点设置到后面最近的代码行处break 函数名:在指定函数处设置断点,这会让执行流进入该函数,然后不执行任何其它指令立刻停止,等待后续命令break 文件名:行号:和上一个指令效果相同,不同点在于这里指定了行号所属的文件,而上一个命令默认为当前文件中的行号break 位置 if 条件:即在某个位置满足某个条件时执行流才停止,如果想在循环中进行断点,这将会很有用。值得注意的是,if条件中的变量需要在指定位置是可见的
以上是断点的设置,在调试中,难免想删除一些断点,或者临时开关一些断点,这在gdb中也有对应的工具可以用
info break:列出所有已经设置好的断点,Num列表示断点编号、Enb列表示断点是否启用,y表示打开,n表示关闭、Address表示断点地址、What表示断点所在文件的具体位置、Type表示断点类型,普通断点类型为breakpointdelete:删除所有断点delete 断点编号:delete或者d删除指定编号的断点disable 断点编号:临时关闭指定断点enable 断点编号:重新打开某个断点clear 函数名:删除通过break 函数名设置的断点
在进入断点后,如果想查看变量信息,可以使用print(p)指令查看
p i:打印变量i的值p &i:打印变量i的地址p array[5]:打印数组array的第五个元素p *ptr:打印指针ptr指向的内容p ds.id:打印某个结构体的成员变量,ds为该结构体的一个实例p func():调用函数,并打印函数的返回值。这很有意思,这里只是print调用了函数,真正的执行流其实还没有变化,同时你还可以通过print多次调用func(),如果函数内有静态变量,对于静态变量的修改也会表现出来
断点后,如果想查看当前的调用栈,那么可以使用backtrace(bt)进行查看
另外bt full命令在bt命令的基础上会额外打印每一层栈帧中局部变量的当前值
进入断点后,还可以通过list命令查看当前断点位置附近的代码
list:列出当前断点附近的代码list 行号:列出指定行附近的代码list 文件名:行号:同上list 函数名:列出指定函数附近的代码list ,:继续显示后面的代码,即翻页
栈帧切换
当函数调用栈层次比较深时,调用栈往往比较复杂,在print当前的变量时,我们可能又忘了再上一层某个变量传入时的值为多少,这个时候就需要有栈帧切换的能力,这能够方便的让我们看到多层调用栈中,不同层次的上下文情况
下面笔者以一个递归调用为例讲解栈帧切换
上图为一个递归函数,现在笔者经过多次步入之后,调用栈如下
可以看到,已经有很多层的调用栈了,这个时候使用print打印变量n时,n在当前调用层次中为1。这是一个简单的例子,当我们现实中想要查看多层调用栈中,不同层次中的变量值时,就需要进行栈帧切换,主要涉及up、down和 frame这三个命令
down:down命令会切换到下一层的栈帧,例如当前栈帧为0,这已经是最下一层的栈帧了(栈地址由上到下进行增长),所以不能再切换、这个时候p n将会输出1up:up命令会切换到上一层的栈帧,例如当前栈帧为0,所以上一层栈帧为1。这个时候p n将会输出2frame 指定层:这将会切换到指定层,切换完成后print就可以打印对应栈帧中的局部变量
以上三个命令即为栈帧切换的核心命令,值得注意的是,刚才提到的切换栈帧都是建立在不改变程序运行状态的前提下,再此前提下让你“穿越”到调用链的不同层次中去观察对应层次中的变量和上下文
观察点(watch)
观察点(watch)类似一种断点,但是和普通断点(break)不同,观察点不是监视某一行代码,而是监视某个变量或者内存地址,当这个变量发生变化时,程序会立即停止进入断点状态,然后打印新值和旧值
watch global_counter:监控global_counter变量被写watch g_sensor.temperature:监控某个结构体实例的某个字段被写rwatch:读观察点,监控变量被读取awatch:读写观察点,监控变量被读或者写
总结
这篇文章主要一起学习了gdb入门的基础概念,包括断点和观察点、栈帧切换等内容
还有信号的调试、多线程调试、内存问题( ASan)的调试以及Core Dump离线分析的内容笔者还在学习中,这部分后续会继续更新,一起无限进步!!!
PS:
今年对于笔者来说注定是不平凡的一年,笔者毅然结束了九年的杭州生活和学习之旅,来到重庆开启下一段旅行。九年间发生了好多事情
那年吃完三个菜的“年夜饭”去逛街,过年的街上空荡荡的也很冷,一家人被突如其来的经济变故打击得手足无措,沉默。就这样毫无目地逛着,也不知道要到哪里去,要怎么走;
那年母亲为了省一两块钱不坐公交,走高速公路四个小时上下班的日子;
那年母亲第一次出门打工,上夜班,白天睡不着,但是每次视频都说很适应,都说挺好的;
那年全家在外打工,妹妹没钥匙回家进不了屋,楼下远远的哭着看了一眼这个苦苦支撑的家,就又回了学校;
那年为了能够把这个家拉回正轨,不允许自己有一刻的歇息,毕业典礼结束两天马上进入工作;换工作只谈待遇,不谈强度,中间只让自己休息一周;手术后一个月回到杭州每天杵拐上下班,下班后腿已经肿了一圈
那堪回首!
时间来到2026年,母亲有了自己的小事业,妹妹找到了稳定体面待遇可观的工作,我紧绷的弦可以松了
做好自己当下力所能及的一切,剩下的就交给时间,下个九年、二十年、亦或一生,才不会后悔,我有尽力,没有让生命中最重要的人失望
