最近和许多同事交流时,发现好多人只是在IDE上debug,但是gdb却一点都不了解;校招新来的同事更是都没听过gdb这个工具,很多人也只会加各种打印信息(当然这也不失为一种很重要且有效的方式),所以在培训时给他们培训了一下;另外好久也没写blog了,刚好把这篇笔记简单分享一下。
如果是gdb已经相对熟悉了,前面介绍汇总,可以浏览跳过直接寻找需要的内容。
————————————————————————————————————————————
更新一:
最近有时间,优化下gdb调试这篇博客,把dgb进阶的一些调试手段加入进来;另外结合实战来讲解下。
0 简介
GDB 全称“GNU symbolic debugger”,GNU家族成员之一。
所谓调试(Debug),就是让代码一步一步慢慢执行,跟踪程序的运行过程。比如,可以让程序停在某个地方,查看当前所有变量的值,或者内存中的数据;也可以让程序一次只执行一条或者几条语句,看看程序到底执行了哪些代码。
也就是说,通过调试程序,我们可以监控程序执行的每一个细节,包括变量的值、函数的调用过程、内存中数据、线程的调度等,从而发现隐藏的错误或者低效的代码;在我们日常coding debug时,有时很难肉眼发现自己写的代码的问题之处,这时GDB就排上用场了。
下载和安装这里不做说明,放一个源码链接:http://ftp.gnu.org/gnu/gdb/,感兴趣的小伙伴,可以下载看下。
1 常用debug命令
这里介绍的基础命令,覆盖日常80%以上使用场景,简单调试够用了,实际工程使用中可能无法满足全部调试,先掌握基本的命令,遇到问题再具体学习(有时间精力的小伙伴也可以找大全命令学习下)。
1.1 常用命令汇总
GDB 的主要功能就是监控程序的执行流程。这也就意味着,只有当源程序文件编译为可执行文件并执行时,并且该文件中必须包含必要的调试信息(比如各行代码所在的行号、包含程序中所有变量名称的列表(又称为符号表)等),GDB才会派上用场。
所以在编译时需要使用 gcc/g++ "-g" 选项编译源文件,才可生成满足 GDB 要求的可执行文件
| 调试命令 (缩写) | 作用 |
|---|---|
(gdb) break (b) | 在源代码指定的某一行设置断点,其中xxx用于指定具体打断点位置。 |
(gdb) run (r) | 执行被调试的程序,其会自动在第一个断点处暂停执行。 |
(gdb) continue (c) | 当程序在某一断点处停止后,用该指令可以继续执行,直至遇到断点或者程序结束。 |
(gdb) next (n) | 令程序一行代码一行代码的执行(区别:next不会进入函数内部,step会进入内部)。 |
(gdb) step (s) | 如果有调用函数,进入调用的函数内部;否则,和 next 命令的功能一样。 |
(gdb) until (u) | 当不想一个循环体内单步跟踪时,单纯使用 until 命令,可以运行程序直到退出循环体。 |
(gdb) until (u) location | 直至到达特定位置(如行号n)停止。until n 命令中,n 为某一行代码的行号,该命令会使程序运行至第 n 行代码处停止。 |
(gdb) print (p) | 打印指定变量的值,其中指的就是某一变量名(还可以打印结构体,这个很好用)。 |
(gdb) list (l) | 显示源程序代码的内容,包括各行代码所在的行号。 |
(gdb) finish (fi) | 结束当前正在执行的函数,并在跳出函数后暂停程序的执行。 |
(gdb) return | 结束当前调用函数并返回指定值,到上一层函数调用处停止程序执行。 |
(gdb) jump (j) | 使程序从当前要执行的代码处,直接跳转到指定位置处继续执行后续的代码。 |
(gdb) quit (q) | 终止调试。 |
当然还有很多,比如:
backtrace(bt)可以用来查看调用关系,和linux里dump_stack()一样,在复杂调用链下,清晰追踪调用关系;info(i)可以查看断点,查看watchpiont,读写编译器内置的全局寄存器,查看线程等;shell xxx可以调用bash支持的命令- …
等等,后面
1.2 启动程序
根据不同场景的需要,GDB 调试器提供了多种方式来启动目标程序,其中最常用的就是run 指令,其次为 start 指令。也就是说,run和 start 指令都可以用来在 GDB 调试器中启动程序,它们之间的区别是:
默认情况下:
run 指令会一直执行程序,直到执行结束。如果程序中手动设置有断点,则 run指令会执行程序至第一个断点处;
start 指令会执行程序至main()主函数的起始位置,即在main()函数的第一行语句处停止执行(该行代码尚未执行)。
当然用continue (c)也是可以的,其中不同可以自行跑下感受。
1.3 断点命令
1.3.1 普通断点(break)
break 命令(可以用b 代替)常用的语法格式有以下 2 种。
1、(gdb) break location // b location
2、(gdb) break ... if cond // b .. if cond
其中,第一种格式中,location 用于指定打断点的具体位置,其表示方式有多种,如表 1 所示。
location的值 | 含义 |
|---|---|
linenum | linenum 是一个整数,表示要打断点处代码的行号。要知道,程序中各行代码都有对应的行号,可通过执行 l(小写的 L)命令看到。 |
filename:linenum | filename 表示源程序文件名;linenum 为整数,表示具体行数。整体的意思是在指令文件 filename 中的第 linenum 行打断点。 |
+ offset | offset 为整数(假设值为 2),+offset 表示以当前程序暂停位置(例如第 4 行)为准,向后数 offset 行处(第 6 行)打断点。 |
- offset | offset 为整数,-offset 表示以当前程序暂停位置为准,向前数 offset 行处打断点。 |
function | function 表示程序中包含的函数的函数名,即 break 命令会在该函数内部的开头位置打断点,程序会执行到该函数第一行代码处暂停。 |
filename:function | filename 表示源程序文件名;function 表示程序中函数的函数名。整体的意思是在指定文件 filename 中 function 函数的开头位置打断点。 |
第二种格式中,… 可以是表 1 中所有参数的值,用于指定打断点的具体位置;cond 为某个表达式。整体的含义为:每次程序执行到 … 位置时都计算 cond 的值,如果为 True,则程序在该位置暂停;反之,程序继续执行。另外也可以用condition 为断点设置命中条件。
1.3.2 观察断点(watchpoint)
对于监控 C、C++ 程序中某变量或表达式的值是否发生改变,如果发生改变则程序暂停。watch 命令的语法非常简单
语法:
(gdb) watch cond
其中,cond 指的就是要监控的变量、表达式或者内存地址。同样类似的,还有 rwatch 和 awatch,可自行学习了解,不多赘述。
格式:
| 命令 | 含义 |
|---|---|
watch a | 观察变量a |
watch a*b+c | 观察表达式a*b+c |
watch *0x123456 | 观察地址0x123456 |
1.3.3 捕捉断点(catch)
捕捉断点的作用是,监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标时间发生,则程序停止执行。
格式:
(gdb) catch event
其中,event 参数表示要监控的具体事件。对于使用 GDB 调试 C、C++ 程序,常用的 event 事件类型如表所示:
| 事件 | 含义 |
|---|---|
throw [exception] | 当程序中抛出 exception 指定类型异常时,程序停止执行。如果不指定异常类型(即省略 exception),则表示只要程序发生异常,程序就停止执行。 |
catch [exception] | 当程序中捕获到 exception 异常时,程序停止执行。exception 参数也可以省略,表示无论程序中捕获到哪种异常,程序都暂停执行。 |
load [regexp] | 其中,regexp 表示目标动态库的名称,load 命令表示当 regexp 动态库加载时程序停止执行;regexp 参数也可以省略,此时只要程序中某一动态库被加载,程序就会暂停执行。 |
unload [regexp] | 其中,regexp 表示目标动态库的名称,unload 命令表示当 regexp 动态库被卸载时,程序暂停执行。regexp 参数也可以省略,此时只要程序中某一动态库被卸载,程序就会暂停执行。 |
NOTE:
- 无论各类型的断点实现原理是有软硬断点之分的,普通断点直接用bread打的其实是软件断点,硬件断点需要用
hardbreak(hb)来实现,比如当使用xip执行程序时,会发现软件断点失效了,具体原理原理也不复杂,感兴趣可以自行搜索学习下。 - 永久断点和临时断点,比如在
break前加上"t"变为tbreak,就变成了临时断点,前者的断点是永久的,后者是一次性的。
1.4 查看、删除或禁用断点
1.4.1 查看断点
使用info break(i b)可以来查看断点信息。
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x88005026 in main at main.c:108
breakpoint already hit 1 time
2 hw watchpoint keep y xExampleSoftwareTimer
3 catchpoint keep y exception throw
matching: int
分别打了一个普通断点、观察断点和捕捉断点,另外可以看到断点状态全为永久(keep),enable(y)。
1.4.2 删除断点
如果之前建立的断点不再需要或者暂时不需要,该如何删除或者禁用呢?常用的方式有 2 种:
- 使用 quit 命令退出调试,然后重新对目标程序启动调试,此方法会将消除上一次调试操作中建立的所有断点(算不上正常方式);
- 使用专门删除或禁用断点的命令,既可以删除某一个断点,也可以删除全部断点。
无论是普通断点、观察断点还是捕捉断点,都可以使用 clear 或者 delete 命令进行删除。
clear 命令可以删除指定位置处的所有断点,常用的语法格式如下所示:
(gdb) clear location
参数location 通常为某一行代码的行号或者某个具体的函数名。当 location 参数为某个函数的函数名时,表示删除位于该函数入口处的所有断点。
delete 命令(可以缩写为 d)通常用来删除所有断点,也可以删除指定编号的各类型断点,语法格式如下:
delete [breakpoints] [num]
其中,breakpoints 参数可有可无,num 参数为指定断点的编号,其可以是delete 删除某一个断点,而非全部。
如果不指定 num参数,则 delete 命令会删除当前程序中存在的所有断点。
1.4.3 禁用端点
禁用断点可以使用 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也可以打印出类或者结构体变量的值。
下面简单展示下打印结构体变量(部分):
(gdb) set print pretty #设置打印格式更好看
(gdb) p/x g_sd_handle #p/x 其中/x表示以16进展显示
$1 = {
Instance = 0x99f04000,
Sdio_init = {
ClkDiv = 0x64,
AutoStop = 0x0,
ClkStop = 0x0,
HighWidthMode = 0x1,
1.5.2 display
和 print 命令一样,display 命令也用于调试阶段查看某个变量或表达式的值,它们的区别是,使用 display 命令查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 调试器都会自动帮我们打印出来,而 print 命令则不会。
也就是说,使用 1 次 print 命令只能查看 1 次某个变量或表达式的值,而同样使用 1 次 display 命令,每次程序暂停执行时都会自动打印出目标变量或表达式的值。因此,当我们想频繁查看某个变量或表达式的值从而观察它的变化情况时,使用 display 命令可以一劳永逸。
display 命令没有缩写形式,常用的语法格式如下 2 种:
(gdb) display expr
(gdb) display/fmt expr
1.6 GDB单步调试
根据实际场景的需要,GDB 调试器共提供了 3 种可实现单步调试程序的方法,即使用 next、step 和 until 命令。换句话说,这 3 个命令都可以控制 GDB调试器每次仅执行 1 行代码,但除此之外,它们各自还有不同的功能。
1.6.1 next命令
next 是最常用来进行单步调试的命令,其最大的特点是当遇到包含调用函数的语句时,无论函数内部包含多少行代码,next 指令都会一步执行完。也就是说,对于调用的函数来说,next 命令只会将其视作一行代码。
next 命令可以缩写为n 命令,使用方法也很简单,语法格式如下:
(gdb) next count
1.6.2 step命令
通常情况下,step 命令和next命令的功能相同,都是单步执行程序。不同之处在于,当step 命令所执行的代码行中包含函数时,会进入该函数内部,并在函数第一行代码处停止执行。
step 命令可以缩写为 s命令,用法和 next 命令相同,语法格式如下:
(gdb) step count
1.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)的基础上,选定 m+n为编号的栈帧作为新的当前栈帧。
相对地,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
后面操作都以下面程序为例:
typedef struct {
uint8_t a;
uint8_t b;
uint16_t c;
uint32_t d;
} test_desc;
int main(void)
{
uint32_t hartid = __RV_CSR_READ(CSR_MHARTID);
rv_csr_t misa = __RV_CSR_READ(CSR_MISA);
uint32_t *test_addr = (uint32_t *)0x1c000292;
int val = 5;
test_desc desc = {0x11,0x22,0x33,0x55aa7799};
test_desc *desc_p = &desc;
srand(__get_rv_cycle() | __get_rv_instret() | __RV_CSR_READ(CSR_MCYCLE));
uint32_t rval = rand();
printf("Hart %d, MISA: 0x%lx\r\n", hartid, misa);
while(1) {}
}
2.1.1 读取变量值
- 普通变量
(gdb) p val
$1 = 16
(gdb) p/x val
$2 = 0x10
- p/x:其中p为print,x代表16进制
- 结构体
(gdb) p/x desc #直接读取结构体
$4 = {a = 0x11, b = 0x22, c = 0x33, d = 0x55aa7799}
(gdb) p/x desc_p #结构体指针注意解引用
$5 = 0x0
(gdb) p/x *desc_p
$6 = {a = 0x11, b = 0x22, c = 0x33, d = 0x55aa7799}
(gdb) p/x *(test_desc *)test_addr #解析硬件描述符时很有用(这里随便映射一个地址)
$7 = {a = 0x6f, b = 0x17, c = 0xf02a, d = 0x9d102e2}
2.1.2 读取内存
(gdb) x/x 0x1c000292
0x1c000292: 0xf02a176f
(gdb) p/x 0x1c000292
$3 = 0x1c000292
- 其中x代表examine,检查可以直接查看内存地址
- 也可以通过print打印该地址的解引用值
另外,使用x命令打印多条内存数据的格式为x/nfu addr。其中:
- n 表示输出单元的个数;
- f 表示输出格式,比如x是以16进制形式输出,o是以8进制形式输出;
- u 表示一个单元的长度,b是一个byte,h是两个byte(halfword),w是四个byte(word),g是八个byte(giant word)。
2.1.3 查看寄存器信息
这里说的寄存器是通用寄存器或者CSR寄存器(gdb知道名字),不是某个ip的寄存器(和内存一样操作)
(gdb) info reg mtvec mepc
mtvec 0x88000303 -2013265149
mepc 0x88001188 -2013261432
(gdb) p/x $mtvec
$5 = 0x88000303
(gdb) info registers
ra 0x88000dca 0x88000dca <main+390>
sp 0x8803ff70 0x8803ff70
gp 0x88020858 0x88020858
tp 0x880204c8 0x880204c8 <hart_id>
t0 0x8 8
t1 0x2 2
t2 0x0 0
fp 0x88040000 0x88040000
s1 0xa5a5a5a5 -1515870811
a0 0x1a 26
a1 0xa 10
a2 0x1a 26
a3 0x1a 26
...
2.2 写操作
写操作一般用的不多,但最好还是了解。
2.2.1 修改变量的值
set variable <name> = <value>
set var <name> = <value>
(gdb) p/x val
$1 = 0x10
(gdb) set var val=0x11
(gdb) p/x val
$2 = 0x11
2.2.2修改寄存器的值
set $<register> = <value>
修改通用寄存器
(gdb) p/x $a2
$3 = 0x1
(gdb) set $a2=0x12345678
(gdb) p/x $a2
$4 = 0x12345678
2.2.3 修改pc值

2.2.4 修改内存值
(gdb) x/x 0x1c000292
0x1c000292: 0x12345678
(gdb) set *(unsigned int*)0x1c000292=0x12345678
(gdb) x/x 0x1c000292
0x1c000292: 0x12345678
3 watchpoint使用
很多情况下,程序的bug是由于某个变量或地址被莫名修改而导致的,但是具体什么时候修改了该值,我们很难定位到。
和breakpoint类似,watchpoint用来观察数据或者地址变化,breakpoint是指令断点;观察点watchpoint功能,可以监控程序中变量或表达式的值,只要在运行过程中发生改变,程序就会停止执行。可以说学会watchpoint,能够实现让bug自动现身的效果。
3.1适用场景
- 数据污染,变量异常变化导致bug
- 内存泄漏,踩了地址
- 确定了某个异常变量,但是该变量被多次使用、还会在各种循环内被操作。
- 多线程场景,线程切来切去,不知道变量具体被哪个线程修改了。
3.2 watchpoint命令

3.3 使用演示
错误dump如下:

- 1)打印出现指令异常。当前PC值为0x1c000294
- 2)异常错误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,抛砖引玉吧,如有错误之处请在评论区指出。

1564

被折叠的 条评论
为什么被折叠?



