文章目录
一、前言二、调试版本与发布版本1、见见gdb2、程序员与测试人员3、为什么Release不能调试但DeBug可以调试❓ 三、使用gdb调试代码1、指令集汇总2、命令演示⌨ 行号显示⌨ 断点设置⌨ 查看断点信息⌨ 删除断点⌨ 开启 / 禁用断点⌨ 运行 / 调试⌨ 逐过程和逐语句⌨ 打印 / 追踪变量⌨ 查看函数调用⌨ 修改变量的值排查问题三剑客?⌨ 指定行号跳转⌨ 强制执行函数⌨ 跳转到下一断点 四、实战演练:Swap Two Numbers五、总结与提炼
一、前言
学习了【vim】知道了如何编辑一个代码文本学习了【gcc】知道了如何编译一个代码文本学习了【make/Makefile】知道了如何自动化构建一个代码文本但是如何对一段代码去进行调试呢,此时就要使用到Linux下的调试器gdb
了。对于这个调试器来说,不像是VS中那样的图形化界面形式,而是采用纯命令行的形式进行调试。可能我的讲解会比较晦涩难懂,在学习的过程中主要是会一些gdb下基本的操作即可
二、调试版本与发布版本
1、见见gdb
下面是本次调试所要使用到的代码
1 #include <stdio.h> 2 3 int AddToTop(int top) 4 { 5 printf("Enter AddToTop\n"); 6 7 int count = 0; 8 for(int i = 1;i <= top; ++i) 9 { 10 count += i; 11 } 12 13 printf("Quit AddToTop\n"); 14 return count; 15 } 16 17 int main(void) 18 { 19 int top = 100; 20 int ret = AddToTop(top); 21 22 printf("ret = %d\n", ret); 23 return 0; 24 }
下面是Makefile中的内容,用于自动化编译
1 mytest:test.c 2 gcc -o mytest test.c -std=c99 3 .PHONY:clean 4 clean: 5 rm -rf mytest
注:-std=c99
表示以c99的标准来编译代码
如果要进入gdb开始调试,那直接
gdb + 可执行程序
即可不过进去之后发现似乎有一些奇怪的内容,【no debugging symbols found】,翻译过来就是没有调试信息。那这是为何呢?是gdb出问题了吗? 先不要着急,如果有经常调试的通过就可以知道只有在【DeBug】的环境下才会有我们想要的调试信息,所以可以初步推断这可能不是一个【DeBug】版本的可执行程序先使用q(quit)
退出gdb 让我们先看下去,了解一下其他的知识再来解决这个问题
2、程序员与测试人员
接下去我们就来说说有关【DeBug】和【Release】版本的不同之处
?【Debug】—— 调试版本
?【Release】—— 发布版本
测试人员
进行测试,而且会给出自己的单元测试报告。对于测试人员来说所处的模式是【Release】,也就是将来客户要使用的这款软件的发布版本当测试在测的过程中,一定会发现一些问题。此时测试人员就会把报告再打回研发部。研发部做修改重新生成Release版本的可行性程序给到测试人员继续测试最后只有当测试通过了,再将生成的【单元测试报告】与产品经理进行核对之后没有问题,那这个软件才可以真正地面向市场? 3、为什么Release不能调试但DeBug可以调试❓
其实对于我们刚才直接make自动化生成的可执行程序是通过gcc直接编译产生得到的,它是一个【Release】版本的可执行程序,因此无法进行调试
若是我们想要使用gcc/g++
去生成一个可执行程序时,默认是【Release】版本的,而不是【DeBug】但若是我们想要去生成一个【DeBug】版本的可执行程序也是可以的,只需要修改一下我们的Makefile即可,给gcc后面带上一个-g
的命令选项,此时再去make一下的话生成的就是【DeBug】版本的了 为了之前的【Release】版本不被覆盖,我们将其重命名一下为
mytest-release
在生成【DeBug】版本后一样对其进行一个重命名为mytest-debug
通过观察上图中两个可执行文件的大小便可以发现虽然它们都是可执行程序,但是容量大小却不一样,这是为什么呢❓
因为以Release版本发布的软件是给客户的,客户是不需要调试信息的
往可执行程序里添加很多的调试信息意味着软件的体积会变大
一方面,用户下载需要时间了另一方面,用户下载好之后将软件启动、运行都需要更多的时间,体验不好。一般能不加就不加但是对于DeBug来说会自动加调试信息,容量体积比Release大
但是就这么说说太抽象了,我们得看看这个可执行文件里的调试信息究竟是怎样的?
readelf -S mytest-debug
在C生万物 | 程序环境和预处理中,我有说到过对于【可执行文件】它是一个二进制文件,若是查看它的源码就可以发现里面都是一堆乱码 当我们面对一堆二进制乱码措手不及的时候,给大家提到过一个东西叫做readelf
,其实它是Linux中的一个指令,可以用来读取【elf】格式的文件
-S
的命令选项以符号表的形式来进行读取这个文件,就以一个列表的形式展现出了这个带调试信息的可执行程序下面展现几个比较常见的。例如这里的 .rodata
就是read only data即只读全局数据区.data
就是已初始化全局数据区.bss
就是未初始化全局数据区 不过这些呢是这个可执行文件中的所有内容,若是我们只是先要查看一些debug的调试信息,就要对这些东西进行一个筛选才行此时就可以使用到grep
命令来进行一个筛选。便可以查看到所有的debug调试信息了 上面是查看【DeBug】版本下的调试信息,在【Release】版本有没有呢我们也是读取并搜寻一下这个文件便可以发现对于【Release】版本来说是不存在调试信息的,所以什么都没有被打印出来
【总结一下】
-g
选项 好,说到这里,对于调试相关背景就全部讲完了,接下去我们正式进入【gdb】的学习⌨️
三、使用gdb调试代码
1、指令集汇总
因为这个调试器是在Linux环境下的,是纯命令行模式,所以会有很多的指令,做好心里准备?
注:()括号里面是该指令的全称
l(list) 行号/函数名
—— 显示对应的code,每次10行
r(run)
—— F5【无断点直接运行、有断点从第一个断点处开始运行】
b(breakpoint) + 行号
—— 在那一行打断点
b 源文件:函数名
—— 在该函数的第一行打上断点
b 源文件:行号
—— 在该源文件中的这行加上一个断点吧
info b
—— 查看断点的信息
breakpoint already hit 1 time【此断点被命中一次】
d(delete) + 当前要删除断点的编号
—— 删除一个断点【不可以d + 行号】
d + breakpoints
—— 删除所有的断点
disable b(breakpoints)
—— 使所有断点无效【默认缺省】
enable b(breakpoints)
—— 使所有断点有效【默认缺省】
disable b(breakpoint) + 编号
—— 使一个断点无效【禁用断点】
enable b(breakpoint) + 编号
—— 使一个断点有效【开启断点】
enable breakpount
—— 使一个断点有效【开启断电】
n(next)
—— 逐过程【相当于F10,为了查找是哪个函数出错了】
s(step)
—— 逐语句【相当于F11,】
bt
—— 看到底层函数调用的过程【函数压栈】
set var
—— 修改变量的值
p(print) 变量名
—— 打印变量值
display
—— 跟踪查看一个变量,每次停下来都显示它的值【变量/结构体…】
undisplay + 变量名编号
—— 取消对先前设置的那些变量的跟踪
排查问题三剑客?
until + 行号
—— 进行指定位置跳转,执行完区间代码 finish
—— 在一个函数内部,执行到当前函数返回,然后停下来等待命令 c(continue)
—— 从一个断点处,直接运行至下一个断点处【VS下不断按F5】 2、命令演示
看了上面的这些命令后,相信你一定回到了刚开始学习Linux指令的时候那种恐惧感,不过没关系,我会一一地演示这些指令,让你在看完本文后有一个基本的调试能力?
首先我们进入到gdb,然后它会等待我们输入指令⌨ 行号显示
l(list) 行号/函数名
—— 显示对应的code,每次10行
Enter
几次就可以了,gdb会自动记忆你上次敲入的指令 ⌨ 断点设置
b + 行号
—— 在那一行打断点
b 源文件:函数名
—— 在该函数的第一行打上断点
b 源文件:行号
—— 在该源文件中的这行加上一个断点
⌨ 查看断点信息
info b
—— 查看断点的信息
b/breakpoint
来简要介绍一下断点的一些字段信息
breakpoint already hit 1 time
即此断点被命中1次 ⌨ 删除断点
d + 当前要删除断点的编号
—— 删除一个断点【不可以d + 行号】
d + breakpoints
—— 删除所有的断点
⌨ 开启 / 禁用断点
disable b(breakpoints)
—— 使所有断点无效【默认缺省】
enable b(breakpoints)
—— 使所有断点有效【默认缺省】
disable b(breakpoint) + 编号
—— 使一个断点无效【禁用断点】
enable b(breakpoint) + 编号
—— 使一个断点有效【开启断点】
其实对于禁用断点和启用断点,VS中也是有的,它叫做【空断点】。我们一起来看看
⌨ 运行 / 调试
r(run)
—— F5【无断点直接运行、有断点从第一个断点处开始运行】
⌨ 逐过程和逐语句
n(next)
—— 逐过程【相当于F10,为了查找是哪个函数出错了】
s(step)
—— 逐语句【相当于F11,一次走一条代码,可进入函数,同样的库函数也会进入】
AddToTop
这个函数,若是你在printf()语句要执行时按下【s】的话gdb就会进入printf()库函数内部去执行,这里就不展示了 接下去我们可以就继续【n】,然后进行逐过程调试,来到for循环中,那么逐过程也就是变量i的累加和计数器count的累加,所以会反复执行(通过图中最左侧可以看出是第8行和第10行在反复执行)可以看到后面我没有再按【n】了,但是依旧会执行上面的步骤,这点上面也有提到过,因为gdb会自动化记忆你上一次执行过的命令,所以若是不想再敲了,直接Enter
就可以了 ⌨ 打印 / 追踪变量
p(print) 变量名
—— 打印变量值
i
的值和count
的值发生了变化 但是你不觉得这样每次去打印会显得很繁琐吗,那一定会的,所以我们有更好的办法?
display
—— 跟踪查看一个变量,每次停下来都显示它的值【变量/结构体…】
undisplay + 变量名编号
—— 取消对先前设置的那些变量的跟踪
display
,那就有undisplay
⌨ 查看函数调用
bt
—— 看到底层函数调用的过程【函数压栈】
AddToTop
函数和main
函数也呈现这样的关系。此时我们就可以通过【bt】这个指令来查看函数压栈的过程,此时便可以看到因为 ⌨ 修改变量的值
set var
—— 修改变量的值
我们也可以到VS中来看一下条件断点是如何设置的
在右击选择【条件】后,就可以输入你本次执行代码想要从哪个条件开始,然后按下Enter,启动调试之后就会从你设置的条件处开始执行
排查问题三剑客?
掌握了上面的这些,你就可以在Linux下调一些简单的代码了,不过想做到高效地进行调试,就需要学习一下【三剑客】
⌨ 指定行号跳转
until + 行号
—— 进行指定位置跳转,执行完区间代码
until 13
之后,程序直接就给出了我们最终的结果count,而且即将要执行最后的打印语句,说明我们跳转成功了 不过呢,在我使用这个【until】的时候,遇到了一个bug,此时我在循环内部,也就是第10行的位置打了一个断点,用过VS调试器的都知道一直按F5就可以立即执行下一次循环。我在上面是没有打上断点的,所以使用【until】的时候也没有出现问题,但是在下面我在循环内部设置了一个断点,此时再去使用【until 13】进行跳转的时候就出现问题了,并没有马上跳转到指定的行号,而是接着执行循环中的内容 ⌨ 强制执行函数
finish
—— 在一个函数内部,执行到当前函数返回,然后停下来等待命令
finish
,可以看到它直接回到了调用函数的位置,returned了一个返回值 然后可以看到,在获取到返回值后,也就直接进行了printf打印 ⌨ 跳转到下一断点
c(continue)
—— 从一个断点处,直接运行至下一个断点处【VS下不断按F5】
四、实战演练:Swap Two Numbers
?纸上得来终觉浅,绝知此事要躬行?
————————————
以下是本次调试所要使用到的代码,相信你一定非常熟悉了,也就是使用指针交换两数,如果想看细节讲解可以看看我的函数章节
1 #include <stdio.h> 2 3 void swap(int* x, int* y) 4 { 5 int t = *x; 6 *x = *y; 7 *y = t; 8 } 9 int main(void) 10 { 11 int a = 10; 12 int b = 20; 13 14 printf("a = %d, b = %d\n", a, b); 15 16 swap(&a, &b); 17 18 printf("a = %d, b = %d\n", a, b); 19 return 0; 20 }
首先我们在程序第16行设置上一个断点,然后【r】从第15行开始运行 然后我们使用【s】进入到swap函数中,因为我首先不想调试,想先立马看看运行结果,但是此时又已经进入调试了,那么我们就可以使用到【finish】来立马执行完这个函数,然后观察一下结果可以看到,最后打印出结果的时候a和b的值确实发生了交换 既然清楚了二者会进行一个交换,接下去我们就逐语句【n】进行一个单步追踪吧因为提前看了执行结果,所以我们要重新开始调试,按下【r】即可,它会询问你是否需要重新开始调试,选择y之后就可以重新从16行开始进行调试 首先通过【display】记录一下两个变量的值和地址 接着按【s】进入到swap函数里,追踪一下指针x和指针y的内容,也就是它们所存放的地址,就可以看到,函数内部已经接受到了这两个变量的地址 但是我们后面不想再查看它们了,只需要看值是否被修改,此时就可以取消对它们的跟踪【undisplay】 然后对我们要观察的值变化继续做一个追踪并且在执行完第一个语句t = *x
时,临时变量t中已经存放了变量a的值,也就是指针所指向的那块空间中的值 接下去执行*x = *y
,此时*x中的值就发生了变化,因为指针x可以直接找到变量a的地址,所以可以对其中的内容做修改,就变为了20 接下去执行*y = t
,同理,指针y可以直接找到变量b的地址,所以可以对其中的内容做修改,将原本保存在临时变量t中的10赋值给到*y,也就修改了其中的内容 再按【n】的话这个swap函数就执行结束了,回到了main函数,就可以清楚地看到函数内部的修改带动了函数外部值的变化,真正地通过【传址调用】交换了两个数 敲黑板:重点来了
但是我不仅想让你看到这些,更加重要的一点不知你有没有发现,刚才我们在main函数中追踪的变量a和变量b的值与地址,到了swap()中就不见了,这是为什么呢? 其实就是因为两个函数的函数栈帧不同,变量a和变量b位于main函数的函数栈帧中;而指针变量x和指针变量y位于swap函数的函数栈帧中,是互不相干的,临时变量除了函数栈帧就会被销毁【如果听不懂这些请看反汇编深挖【函数栈帧】的创建和销毁】 但是因为在swap()内部接受到了外部两个变量的地址,所以在函数内部就可以通过这两个指针找到这两个变量的地址,从而对这两个空间中的内容做一个修改的操作通过以上的实验演练,你是否对调试器GDB的使用更上一层楼?
五、总结与提炼
最后来总结一下本文所学习的内容?
在本文的开始,我们在初见GDB的时候发现了【no debugging symbols
】的报错信息,于是便谈到了一个可执行程序的【DeBug】版本和【Release】版本,知道了对于前者来说是我们程序员使用的环境,是存在调试信息的;对于后者来说是测试人员所处的环境,是不存在调试信息的。而对于gcc/g++而言默认生成的可执行程序就是【Release】版本的,因此我们要加上一个-g
命令选项使其在make之后生成一个【DeBug】版本的可执行程序,这样就可以进行调试了接着我们正式进入到了调试器GDB的使用,介绍了很多相关的指令,这些都是我整理出来的常见指令,其实对于GDB来说还有很多指令,但是真正常用的也就这些,只要你认真地看下来,将它们都使用熟练了,相信你的调试功底会大幅度提升,对你在VS中的调试也是有所帮助的最后,在学习了GDB的许多调试指令后,我们便进行了【Swap Two Numbers】的实战演练,不仅巩固了C语言中有关传址调用以及函数栈帧的相关指知识,而且使我们对于调试器GDB的使用更上一层楼↑ 以上就是本文要介绍的所有内容,感谢您的阅读❤️