帝国cms 网站名称,贵阳网站开发公司,开发网上商城公司,怎么在qq上自己做网站最近和许多同事交流时#xff0c;发现好多人只是在IDE上debug#xff0c;但是gdb却一点都不了解#xff1b;校招新来的同事更是都没听过gdb这个工具#xff0c;所以在培训时给他们培训了一下#xff1b;另外好久也没写blog了#xff0c;刚好把这篇笔记简单分享一下。
0 …最近和许多同事交流时发现好多人只是在IDE上debug但是gdb却一点都不了解校招新来的同事更是都没听过gdb这个工具所以在培训时给他们培训了一下另外好久也没写blog了刚好把这篇笔记简单分享一下。
0 简介
GDB 全称“GNU symbolic debugger”GNU家族成员之一。 所谓调试Debug就是让代码一步一步慢慢执行跟踪程序的运行过程。比如可以让程序停在某个地方查看当前所有变量的值或者内存中的数据也可以让程序一次只执行一条或者几条语句看看程序到底执行了哪些代码。
也就是说通过调试程序我们可以监控程序执行的每一个细节包括变量的值、函数的调用过程、内存中数据、线程的调度等从而发现隐藏的错误或者低效的代码在我们日常coding debug时有时很难肉眼发现自己写的代码的问题之处这时GDB就排上用场了。
下载和安装这里不做说明放一个源码链接http://ftp.gnu.org/gnu/gdb/感兴趣的小伙伴可以下载看下。
1 常用debug命令
这里仅介绍些基础命令覆盖日常50-60%的使用简单调试够用了实际工程使用中肯定是不够的先掌握基本的命令遇到问题再具体学习有时间精力的小伙伴也可以找大全命令学习下。
1.1 汇总
GDB 的主要功能就是监控程序的执行流程。这也就意味着只有当源程序文件编译为可执行文件并执行时并且该文件中必须包含必要的调试信息比如各行代码所在的行号、包含程序中所有变量名称的列表又称为符号表等GDB才会派上用场。
所以在编译时需要使用 gcc/g -g 选项编译源文件才可生成满足 GDB 要求的可执行文件
1.2启动程序
根据不同场景的需要GDB 调试器提供了多种方式来启动目标程序其中最常用的就是run 指令其次为 start 指令。也就是说run和 start 指令都可以用来在 GDB 调试器中启动程序它们之间的区别是
默认情况下 run 指令会一直执行程序直到执行结束。如果程序中手动设置有断点则 run指令会执行程序至第一个断点处
start 指令会执行程序至main()主函数的起始位置即在main()函数的第一行语句处停止执行该行代码尚未执行。
1.3 break命令
break 命令可以用b 代替常用的语法格式有以下 2 种。
1、(gdb) break location // b location
2、(gdb) break ... if cond // b .. if cond其中第一种格式中location 用于指定打断点的具体位置其表示方式有多种如表 1 所示。 第二种格式中… 可以是表 1 中所有参数的值用于指定打断点的具体位置cond 为某个表达式。整体的含义为每次程序执行到 … 位置时都计算 cond 的值如果为 True则程序在该位置暂停反之程序继续执行。另外也可以用condition 为断点设置命中条件。
1.4 删除或禁用断点
1.4.1删除断点
如果之前建立的断点不再需要或者暂时不需要该如何删除或者禁用呢常用的方式有 2 种
使用 quit 命令退出调试然后重新对目标程序启动调试此方法会将消除上一次调试操作中建立的所有断点 使用专门删除或禁用断点的命令既可以删除某一个断点也可以删除全部断点。 无论是普通断点、观察断点还是捕捉断点都可以使用 clear 或者 delete 命令进行删除。
clear 命令可以删除指定位置处的所有断点常用的语法格式如下所示
(gdb) clear location参数location 通常为某一行代码的行号或者某个具体的函数名。当 location 参数为某个函数的函数名时表示删除位于该函数入口处的所有断点。
delete 命令可以缩写为 d通常用来删除所有断点也可以删除指定编号的各类型断点语法格式如下
delete [breakpoints] [num]其中breakpoints 参数可有可无num 参数为指定断点的编号其可以是delete 删除某一个断点而非全部。
如果不指定 num参数则 delete 命令会删除当前程序中存在的所有断点。
1.4.2禁用端点
禁用断点可以使用 disable 命令语法格式如下
disable [breakpoints] [num...]breakpoints 参数可有可无num…表示可以有多个参数每个参数都为要禁用断点的编号。如果指定 num…disable 命令会禁用指定编号的断点反之若不设定 num…则 disable 会禁用当前程序中所有的断点。
对于禁用的断点可以使用enable 命令激活该命令的语法格式有多种分别对应有不同的功能
1 enable [breakpoints] [num…] 激活用 num… 参数指定的多个断点如果不设定 num…表示激活所有禁用的断点2 enable [breakpoints] once num… 临时激活以 num… 为编号的多个断点但断点只能使用 1 次之后会自动回到禁用状态3 enable [breakpoints] count num… 临时激活以 num… 为编号的多个断点断点可以使用 count 次之后进入禁用状态4 enable [breakpoints] delete num… 激活 num… 为编号的多个断点但断点只能使用 1 次之后会被永久删除。
1.5查看变量或表达式的值
对于在调试期间查看某个变量或表达式的值GDB 调试器提供有 2 种方法即使用 print 命令或者 display命令。
1.5.1print
它的功能就是在 GDB 调试程序的过程中输出或者修改指定变量或者表达式的值。
print 命令可以缩写为 p最常用的语法格式如下所示
(gdb) print num
(gdb) p num其中参数 num 用来代指要查看或者修改的目标变量或者表达式。
当程序中包含多个作用域不同但名称相同的变量或表达式时可以借助::运算符明确指定要查看的目标变量或表达式。::运算符的语法格式如下
1(gdb) print file::variable
2(gdb) print function::variable其中 file用于指定具体的文件名funciton 用于指定具体所在函数的函数名variable表示要查看的目标变量或表达式。
另外print也可以打印出类或者结构体变量的值。
1.5.2 display
和 print 命令一样display 命令也用于调试阶段查看某个变量或表达式的值它们的区别是使用 display 命令查看变量或表达式的值每当程序暂停执行例如单步执行时GDB 调试器都会自动帮我们打印出来而 print 命令则不会。
也就是说使用 1 次 print 命令只能查看 1 次某个变量或表达式的值而同样使用 1 次 display 命令每次程序暂停执行时都会自动打印出目标变量或表达式的值。因此当我们想频繁查看某个变量或表达式的值从而观察它的变化情况时使用 display 命令可以一劳永逸。
display 命令没有缩写形式常用的语法格式如下 2 种
(gdb) display expr
(gdb) display/fmt expr1.6 GDB单步调试
根据实际场景的需要GDB 调试器共提供了 3 种可实现单步调试程序的方法即使用 next、step 和 until 命令。换句话说这 3 个命令都可以控制 GDB调试器每次仅执行 1 行代码但除此之外它们各自还有不同的功能。
1.6.1 next命令
next 是最常用来进行单步调试的命令其最大的特点是当遇到包含调用函数的语句时无论函数内部包含多少行代码next 指令都会一步执行完。也就是说对于调用的函数来说next 命令只会将其视作一行代码。
next 命令可以缩写为n 命令使用方法也很简单语法格式如下
(gdb) next count1.6.2 step命令 通常情况下step 命令和next命令的功能相同都是单步执行程序。不同之处在于当step 命令所执行的代码行中包含函数时会进入该函数内部并在函数第一行代码处停止执行。
step 命令可以缩写为 s命令用法和 next 命令相同语法格式如下
(gdb) step count1.6.3 until命令 until 命令可以简写为 u 命令有 2 种语法格式如下所示
1、(gdb) until
2、(gdb) until location其中参数 location为某一行代码的行号。
不带参数的 until命令可以使 GDB调试器快速运行完当前的循环体并运行至循环体外停止。注意until 命令并非任何情况下都会发挥这个作用只有当执行至循环体尾部最后一行代码时until命令才会发生此作用反之until命令和 next 命令的功能一样只是单步执行程序。
1.6.4 return命令
实际调试时在某个函数中调试一段时间后可能不需要再一步步执行到函数返回处希望直接执行完当前函数这时可以使用 finish命令。与finish 命令类似的还有 return 命令它们都可以结束当前执行的函数。
1.6.5 finish命令
finish 命令和 return命令的区别是finish命令会执行函数到正常退出而 return 命令是立即结束执行当前函数并返回也就是说如果当前函数还有剩余的代码未执行完毕也不会执行了。除此之外return命令还有一个功能即可以指定该函数的返回值。
1.6.6 jump命令
jump 命令的功能是直接跳到指定行继续执行程序其语法格式为
(gdb) jump location其中location 通常为某一行代码的行号。
也就是说jump 命令可以略过某些代码直接跳到 location处的代码继续执行程序。这意味着如果你跳过了某个变量对象的初始化代码直接执行操作该变量对象的代码很可能会导致程序崩溃或出现其它 Bug。另外如果 jump跳转到的位置后续没有断点那么 GDB会直接执行自跳转处开始的后续代码。
1.7 GDB search 命令
调试文件时某些时候可能会去找寻找某一行或者是某一部分的代码。可以使用 list 显示全部的源码然后进行查看。当源文件的代码量较少时我们可以使用这种方式搜索。如果源文件的代码量很大使用这种方式寻找效率会很低。所以 GDB中提供了相关的源代码搜索的的search命令。
search 命令的语法格式为
search regexp
reverse-search regexp第一项命令格式表示从当前行的开始向前搜索后一项表示从当前行开始向后搜索。其中regexp 就是正则表达式正则表达式描述了一种字符串匹配的模式可以用来检查一个串中是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串。很多的编程语言都支持使用正则表达式。
1.8 查看堆栈信息
1.8.1 backtrace 命令 (bt)
backtrace 命令用于打印当前调试环境中所有栈帧的信息常用的语法格式如下
(gdb) backtrace [-full] [n]其中用 [ ] 括起来的参数为可选项它们的含义分别为 n一个整数值当为正整数时表示打印最里层的 n 个栈帧的信息n为负整数时那么表示打印最外层n个栈帧的信息 -full打印栈帧信息的同时打印出局部变量的值。
注意当调试多线程程序时该命令仅用于打印当前线程中所有栈帧的信息。如果想要打印所有线程的栈帧信息应执行thread apply all backtrace命令。
1.8.2 frame 命令
frame命令的常用形式有 2 个
根据栈帧编号或者栈帧地址选定要查看的栈帧语法格式如下
(gdb) frame spec该命令可以将 spec 参数指定的栈帧选定为当前栈帧。spec 参数的值常用的指定方法有 3 种
通过栈帧的编号指定。0 为当前被调用函数对应的栈帧号最大编号的栈帧对应的函数通常就是 main() 主函数 借助栈帧的地址指定。栈帧地址可以通过 info frame 命令后续会讲打印出的信息中看到 通过函数的函数名指定。注意如果是类似递归函数其对应多个栈帧的话通过此方法指定的是编号最小的那个栈帧。 除此之外对于选定一个栈帧作为当前栈帧GDB 调试器还提供有up 和down两个命令。其中up命令的语法格式为
(gdb) up n其中 n为整数默认值为 1。该命令表示在当前栈帧编号假设为 m的基础上选定 mn为编号的栈帧作为新的当前栈帧。
相对地down 命令的语法格式为
(gdb) down n其中n为整数默认值为 1。该命令表示在当前栈帧编号假设为 m的基础上选定m-n 为编号的栈帧作为新的当前栈帧。
借助如下命令我们可以查看当前栈帧中存储的信息
(gdb) info frame该命令会依次打印出当前栈帧的如下信息
• 当前栈帧的编号以及栈帧的地址
• 当前栈帧对应函数的存储地址以及该函数被调用时的代码存储的地址
• 当前函数的调用者对应的栈帧的地址
• 编写此栈帧所用的编程语言
• 函数参数的存储地址以及值
• 函数中局部变量的存储地址
• 栈帧中存储的寄存器变量例如指令寄存器64位环境中用 rip 表示32为环境中用eip 表示、堆栈基指针寄存器64位环境用 rbp表示32位环境用 ebp表示等。除此之外还可以使用info args命令查看当前函数各个参数的值使用info locals命令查看当前函数中各局部变量的值。
2 读写内存寄存器
调试过程中要经常查看或者改写内存、寄存器的值操作如下
2.1 读取
读取某个变量的值: p var读取某个内存地址里的内容: x memaddr读取某个寄存器的值: info register
后面操作都以下面程序为例
int main(void)
{unsigned int *src base addr (unsigned int *)0x1c000292;srand(__get_rv_cycle() | get_rv_instret() | __RV_CSR_READ(CSR MCYCLE));//*src base addr 0x7788;int a 5;uint32 t rval rand();uint32 t hartid __RV_CSR_READ(CSR_MHARTID);rv_csr_t misa __RV_CSR_READ(CSR_MISA);printf(Hart %d, MISA: 0x%lx\r\n, hartid, misa);print_misa();for (int i e; i RUN_LOOPS; i ) {printf(%d: Hello World From Nuclei RISC-V Processor! rin,i)}simulation pass();
}2.1.1 读取变量值 p/x其中p为printx代表16进制
2.1.2 读取内存 其中x代表examine检查可以直接查看内存地址也可以通过print打印该地址的解引用值
另外使用x命令打印多条内存数据的格式为x/nfu addr。其中
n表示输出单元的个数f表示输出格式比如x是以16进制形式输出o是以8进制形式输出u表示一个单元的长度b是一个byteh是两个bytehalfwordw是四个bytewordg是八个bytegiant word。
2.1.3 查看寄存器信息
这里说的寄存器是通用寄存器不是某个ip的寄存器和内存一样操作
2.2 写操作
写操作一般用的不多但最好还是了解。
2.2.1 修改变量的值
set var name value2.2.2修改寄存器的值
set $register value修改通用寄存器
2.2.3修改pc值 2.2.4修改内存值 3 watchpoint使用
很多情况下程序的bug是由于某个变量或地址被莫名修改而导致的但是具体什么时候修改了该值我们很难定位到。
和breakpoint类似watchpoint用来观察数据或者地址变化breakpoint是指令断点观察点watchpoint功能可以监控程序中变量或表达式的值只要在运行过程中发生改变程序就会停止执行。可以说学会watchpoint能够实现让bug自动现身的效果。
3.1适用场景
数据污染变量异常变化导致bug内存泄漏踩了地址确定了某个异常变量但是该变量被多次使用、还会在各种循环内被操作。多线程场景线程切来切去不知道变量具体被哪个线程修改了。
3.2 watchpoint命令 3.3 使用演示
错误dump如下
1打印出现指令异常。当前PC值为0x1c0002942异常错误MCAUSE 是 2非法指令3猜测应该是程序哪里修改了指令可能是内存踩踏、内存泄漏导致0x1c000294处的指令被修改为非法指令0x00
int main(void)
{unsigned int *src base addr (unsigned int *)0x1c000292;srand(__get_rv_cycle() | get_rv_instret() | __RV_CSR_READ(CSR MCYCLE));//*src base addr 0x7788;int a 5;uint32 t rval rand();uint32 t hartid __RV_CSR_READ(CSR_MHARTID);rv_csr_t misa __RV_CSR_READ(CSR_MISA);printf(Hart %d, MISA: 0x%lx\r\n, hartid, misa);print_misa();for (int i e; i RUN_LOOPS; i ) {printf(%d: Hello World From Nuclei RISC-V Processor! rin,i)}simulation pass();
}4实际是刻意为之修改了指令内存地址5实际情况下是我们不知道哪里出了问题这时就已使用watchpoint来找出问题 6当观察点的内存内容被修改时cpu将会被hang住通过查看上下文锁定位置这里在99行还未执行上一句被修改
简单介绍了下gdb的基本使用内存读写命令和watchpoint抛砖引玉吧如有错误之处请在评论区指出。