- C++服务器开发精髓
- 张远龙
- 9537字
- 2021-07-23 18:22:23
2.5 gdb常用命令详解——利用gdb调试Redis
为了结合实践,这里以使用gdb调试Redis源码为例介绍每个命令,当然,本节只讲解gdb中一些常用命令的基础用法,下节会讲解gdb中的一些高级用法。
2.5.1 gdb常用调试命令概览和说明
下面给出了一个常用命令列表,后面会结合具体的例子详细介绍每个命令的用法。
上表只列举了一些常用命令,未列举一些不常用的命令(如file命令)。不建议读者刻意记忆这些命令,因为命令较多,建议读者找几个程序实际练习一下,这样就容易记住了。表中“命令缩写”那一栏,是笔者平时对命令的简写输入,读者可以采用,也可以不采用。一个命令可以简写成什么样子,gdb没有强行规定,但读者在简写gdb命令时需要遵循如下两个规则。
(1)一个命令在简写时,不能让gdb出现多个选择,若出现多个选择,gdb就不知道对应哪个命令了。举个例子,输入th命令,而th命令对应的命令有thread和thbreak(上表没有列出),这样gdb就不知道要使用哪个命令了。需要更具体地输入,gdb才能识别:
(2)gdb的某些命令虽然对应多个选择,但是有些命令的简写是确定的,比如r是run命令的简写,虽然有人输入r时可能是想使用return命令。
总之,如果记不清楚某个命令的简写,就可以直接使用该命令的全写,每个命令都是很常见的英文单词,通俗易懂且不难记忆。
2.5.2 用gdb调试Redis前的准备工作
本节逐一介绍上面每个命令的使用方法,会介绍一些很有用的调试细节和使用技巧。如果还不熟悉gdb调试,则建议认真阅读本节。
为了结合实践,这里仍以调试 Redis 源码为例来介绍每个命令。Redis 的最新源码下载地址可以在 Redis 官网获得,本书用到的 Redis 版本是 6.0.3。使用 wget 命令将 Redis源码文件下载下来:
下载完成后解压缩:
进入生成的redis-6.0.3目录并使用makefile进行编译,为了方便调试,我们需要生成调试符号并且关闭编译器优化选项,操作如下:
-g选项表示生成调试符号,-o0选项表示关闭优化,-j 4选项表示同时开启4 个进程进行编译,加快编译速度。Redis 是纯 C 项目,使用的编译器是 gcc,所以这里设置编译器的选项时使用的是 CFLAGS 选项;对于 C++项目,使用的编译器一般是g++,相应的编译器选项是CXXFLAGS,请注意区别。
如果在编译过程中出现如下错误:
则可以改用以下命令来编译,这是由于系统没有安装jemalloc库,可以修改编译参数,让Redis使用系统默认的malloc而不是jemalloc:
编译成功后,进入src目录,使用gdb启动redis-server程序:
2.5.3 run命令
在默认情况下,gdb+filename只是attach到一个调试文件,并没有启动这个程序,我们需要输入run命令启动这个程序(run命令被简写成r):
这就是 redis-server 的启动界面。如果程序已经启动,则再次输入 run 命令就会重启程序。我们在 gdb界面按 Ctrl+C组合键(界面中的^C)让程序中断,再次输入 r命令,gdb会提示我们是否重启程序,输入y确认重启:
2.5.4 continue命令
在程序触发断点或者使用 Ctrl+C 组合键中断后,如果我们想让程序继续运行,则只需输入 continue 命令即可(简写成 c)。当然,如果通过 continue 命令让程序在继续运行的过程中触发设置的程序断点,则程序会在断点处中断:
2.5.5 break命令
break 命令即我们添加断点的命令,可以将其简写成 b。我们可以使用以下方式添加断点:
在以上程序中提到的三种方式是添加断点的常用方式。举个例子,对于一般的Linux程序来说,main 函数是程序入口函数,redis-server 也不例外,如果我们知道了函数的名称,就可以直接利用函数的名称添加一个断点。这里以在main函数处设置断点为例,执行如下命令:
添加好后,使用run命令重启程序,就可以触发这个断点了,gdb会停在断点处:
redis-server 默认的端口号是 6379。我们知道,无论上层如何封装,这个端口号最终肯定是通过操作系统的socket API bind函数绑定上去的,我们通过文件搜索,可以找到调用这个函数的文件,它位于anet.c文件第455行,如下图所示。
使用break命令在这个地方添加一个断点:
由于程序绑定的端口号是在redis-server启动时初始化的,所以为了能触发这个断点,我们再次使用run命令重启这个程序,程序第1次会触发main函数处的断点,输入continue命令继续运行,接着触发anet.c文件第455行的断点:
现在断点停在anet.c文件第455行,我们可以直接使用“break+行号”添加断点,例如可以在第458、464、466行分别添加一个断点,看看这个函数执行完毕后走哪个return语句退出。通过“b 行号”形式添加三个断点,操作如下:
添加三个断点后,通过continue命令继续运行程序,程序运行到第466行中断,说明该函数执行了第466行的return语句:
至此,先调用 bind函数再调用 listen函数,会发现 redis-server已绑定端口并成功开启了监听。我们可以再打开一个 Shell 窗口验证一下,结果证实 6379 端口已经处于监听状态:
2.5.6 tbreak命令
break 命令用于添加一个永久断点;tbreak 命令用于添加一个临时临时断点,其第 1个字母“t”表示“temporarily(临时的)”,也就是说,通过这个命令添加的断点是临时的。临时断点,指的是一旦该断点触发了一次,就会被自动删除。添加断点的方法与上面介绍的break命令一样,这里不再赘述:
我们在以上代码中使用tbreak命令在main函数处添加了一个临时断点,第1次启动程序并触发断点后,再次重新运行程序,不再触发断点,因为这个临时断点已被删除,此时redis-server直接启动成功。
2.5.7 backtrace与frame命令
backtrace 可简写成 bt,用于查看当前所在线程的调用堆栈。现在我们的 redis-server中断在anet.c文件第466行,可以通过backtrace命令查看当前的调用堆栈。
这里一共有 6层堆栈,堆栈编号为#0~#5,顶层是 main函数,底层是断点所在的anetListen函数。如果我们想切换到其他堆栈处,则可以使用frame命令,frame命令的使用方法如下:
frame命令可被简写成f。这里依次切换至堆栈#1、#2、#3、#4、#5,然后切换回#0,操作如下图所示。
通过对上面的各个堆栈进行查看,我们可以得出这里的调用层级关系,即main函数在第5128行调用了initServer函数,initServer函数在第2792行调用了listenToPort函数,listenToPort 函数在第 2648 行调用了 anetTcp6Server 函数,anetTcp6Server 函数在第 524行调用了_anetTcpServer函数,_anetTcpServer函数在第501行调用了anetListen函数,当前断点正好位于anetListen函数中。
2.5.8 info break、enable、disable、delete命令
在程序中加了很多断点以后,若想查看加了哪些断点,则可以使用info break命令(简写成info b),如下图所示。
通过上图,我们可以得到如下信息:目前一共添加了5个断点,断点1、2、5已经触发一次,其他断点未触发;每个断点的位置(所在的文件和行号)、内存地址、断点启用和禁用状态信息也一目了然地展示出来。
如果我们想禁用某个断点,则使用 disable 断点编号就可以禁用这个断点了,被禁用的断点不会再被触发;被禁用的断点可以使用enable断点编号并重新开启:
使用disable 1后,第1个断点Enb一栏的值由y变成n,断点1不会再触发,即程序不会在main函数处中断,程序一直到断点2处才会停下来:
如果disable和enable命令不加断点编号,则分别表示禁用和启用所有断点:
使用delete编号可以删除某个断点,例如delete 2 3表示要删除断点2和断点3:
同样,如果输入delete时不加命令号,则表示删除所有断点。
2.5.9 list命令
list命令和后面介绍的print命令是gdb调试中使用频率最高的命令。list命令用于查看当前断点附近的代码,可以简写成l。我们使用frame命令切换到上文的堆栈#4处,然后输入list命令查看下效果。
可以发现断点“停在”第 2792 行,输入 list 命令以后,会显示第 2792 行前后的 10行代码(第2787~2796行)。
再次输入list命令试一下:
代码继续往后显示 10行(第 2797~2806行),也就是说,第 1次输入 list命令时会显示断点前后的10行代码,继续输入list命令时每次都会接着向后显示10行代码,一直到文件结束。
list+命令(即list加号)可以从当前代码位置向下显示10行代码(向文件末尾方向),这和连续输入多条list命令的效果是一样的。list-命令(即list 减号)可以从当前代码位置向上显示10行代码(往文件开始方向)。操作效果如下:
list默认显示的行数量可以通过修改gdb的相关配置来实现,由于我们一般不会修改这个配置值,因此这里就不介绍了。
list不仅可以显示当前断点处的代码,也可以显示其他文件某一行的代码,读者可以在gdb中输入help list命令来查看更多的用法:
在上面的帮助信息中介绍了可以使用 list FILE:LINENUM 显示某个文件某一行处的代码。我们使用 gdb的目的是调试,所以我们更关心的是断点附近的代码,而不是通过gdb 阅读代码。对于阅读代码,gdb 并不是一个好工具。比如,笔者用 gdb 调试 Redis,用VSCode或者Visual Studio阅读代码,界面如下图所示。
2.5.10 print与ptype命令
print命令可以被简写成 p。通过 print命令可以在调试过程中方便地查看变量的值,也可以修改当前内存中的变量值。我们切换到堆栈#4,打印以下三个变量:
这里使用print命令分别打印出server.port、server.ipfd、server.ipfd_count的值。其中,server.ipfd显示{0<repeats 16 times>},这是gdb显示字符串和字符数组的特有方式,当一个字符串变量、字符数组或者连续的内存值重复若干次时,g d b就会以这种模式显示,以节约显示空间。
通过 print命令不仅可以输出变量的值,也可以输出特定表达式的计算结果,甚至可以输出一些函数的执行结果。
举个例子,我们可以输入 p&server.port来输出server.port的地址。对于C++对象,我们可以通过p this显示当前对象的地址,也可以通过p*this列出当前对象的各个成员变量的值,如果有三个变量可以相加(假设变量名分别为 a、b、c),则可以使用p a+b+c来打印这三个变量的值。
假设func是一个可以执行的函数,则通过p func()命令就可以输出该变量的执行结果。以一种常见的情形为例,在某个时刻,某个系统函数执行失败了,通过系统变量 errno得到一个错误码,我们可以使用p strerror(errno)将这个错误码对应的文字信息打印出来,这样就不用费劲地在man手册上查找这个错误码对应的错误含义了。
通过 print命令不仅可以输出表达式的结果,还可以修改变量的值,我们尝试将上文中的端口号从6379改成6400试试:
当然,一个变量的值在修改后能否起作用,要看这个变量的具体位置和作用。举个例子,对于表达式int a=b/c,如果将c修改成0,程序就会产生除零异常。又如,对于如下代码:
如果在循环过程中通过print命令将 j的大小由100改成 1000,那么这个循环将输出 i的值1000次。
print输出变量值时可以指定输出格式,命令使用格式如下:
format常见的取值如下。
◎ o octal,八进制显示。
◎ x hex,十六进制显示。
◎ d decimal,十进制显示。
◎ u unsigned decimal,无符号十进制显示。
◎ t binary,二进制显示。
◎ f float,浮点值显示。
◎ a address,内存地址格式显示(与十六进制相似)。
◎ i instruction,指令格式显示。
◎ s string,字符串格式显示。
◎ z hex,zero padded on the left,十六进制左侧补0显示。
对于完整的格式和用法,可以在gdb中输入help x查看,演示如下:
总结起来,通过 print命令,我们不仅可以查看程序运行过程中各个变量的状态值,也可以通过临时修改变量的值来控制程序的行为。
gdb还有另一个命令ptype,顾名思义,其含义是“print type”,就是输出一个变量的类型。例如试着输出redis堆栈#4处的变量server和变量server.port的类型:
可以看到,对于一个复合数据类型的变量,ptype不仅列出了这个变量的类型(这里是一个名为redisServer的结构体),而且详细列出了每个成员变量的字段名,有了这个功能,我们在调试时就不必去代码文件中翻看某个变量的类型定义了。
2.5.11 info与thread命令
info命令是一个复合指令,可以用来查看当前进程所有线程的运行情况。这里还是以redis-server 进程为例进行演示,使用 delete 命令删掉所有断点,然后使用 run 命令重启redis-server,等程序正常启动后,我们按 Ctrl+C 组合键(代码中的^C)将程序中断,然后使用info threads查看进程当前的所有线程信息和这些线程分别中断在何处。
通过info threads的输出,我们知道redis-server正常启动后一共产生了4个线程,其中有 1 个主线程和 3 个工作线程,线程编号(Id 那一列)分别是 1、2、3、4。3 个工作线程(2、3、4)分别阻塞在Linux API pthread_cond_wait处,而主线程1阻塞在epoll_wait处。注意,第 1栏的名称虽然叫作 Id,但第 1栏的数值并不是线程的 Id;第 3栏有个括号,内容为LWP 5029,这个5029才是当前线程真正的线程Id。那么LWP是什么意思呢?在Linux系统早期的内核里其实不存在真正的线程实现,当时所有的线程都是用进程实现的,这些模拟线程的进程被称为Light Weight Process(轻量级进程),之后版本的Linux系统内核有了真正的线程实现,但该名称仍被保留了下来。
读者可能有个疑问:怎么知道线程1是主线程,线程2、3、4是工作线程呢?是不是因为线程1前面有个星号(*)?错,线程编号前面的星号表示gdb当前作用于哪个线程,不是说标了星号就是主线程。当前有 4 个线程,也就有 4 个调用堆栈,如果此时输入backtrace 命令查看调用堆栈,则由于 gdb当前作用于线程 1,所以通过 backtrace命令显示的是线程1的调用堆栈:
看到了吧!堆栈#4的main函数也证明了线程编号为1的线程是主线程。
那么如何切换到其他线程呢?我们可以通过thread线程编号命令切换到指定的线程。例如我们想切换到线程2,则只需输入thread 2命令即可,接着输入bt命令就能查看这个线程的调用堆栈了:
所以利用info threads命令可以调试多线程程序。当然,使用gdb调试多线程程序存在一个很麻烦的问题,在之后的章节中会有介绍。
将gdb切换到哪个线程,哪个线程前面就被加上星号标记,例如我们把gdb当前作用的线程切换到线程2上之后,线程2前面就被加上了星号。
info命令还可以用来查看当前函数的参数值,组合命令是info args。我们找个函数来试一下这个命令:
以上代码片段先切回到主线程 1,然后切回到堆栈#2。堆栈#2 调用处的函数是aeProcessEvents,这个函数一共有两个参数,分别是 eventLoop和 tvp,使用 info args命令可以输出这两个参数的值。eventLoop 是一个指针类型的参数,对于指针类型的参数,gdb默认会输出该变量的指针值。如果想输出该指针指向的对象的值,则可以在变量名前面加上星号,即解引用操作符(*),这里使用p*eventLoop即可:
如果还要查看其成员值,则继续使用变量名->字段名即可(如p eventLoop->maxfd),我们在前面介绍print命令时已经介绍过这种用法,这里不再赘述。
info命令的功能远非上面介绍的三种,读者可以在 gdb 中输入 help info 查看更多的info组合命令的用法。
2.5.12 next、step、until、finish、return、jump命令
之所以把这几个命令放在一起,是因为它们是用gdb调试程序时最常用的几个控制流命令。next命令可被简写为n,用于让gdb跳到下一行代码。这里跳到下一行代码不是说一定要跳到离代码最近的下一行,而是根据程序逻辑跳转到相应的位置。举个例子:
如果 gdb中断在以上代码第 2行,此时输入 next命令,则gdb将跳到第7行,因为这里的if条件不满足。
在gdb命令行界面直接按下回车键,默认是将最近一条命令重新执行一遍。所以,当我们使用 next 命令单步调试时,不必反复输入 n 命令,在输入一次 n命令之后想再次输入next命令时直接按回车键就可以了。
上面的执行过程等价于输入第1个n后直接回车:
next命令的调试术语叫“单步步过(step over)”,即遇到函数调用时不进入函数体内部,而是直接跳过。下面说的step命令就是“单步步入(step into)”,顾名思义,就是遇到函数调用时进入函数内部。step可被简写为 s。举个例子,在 redis-server的 main函数中有个spt_init(argc,argv)函数调用,我们停在这一行时,输入s将进入这个函数内部:
这里演示一下,先用b main在main函数处加一个断点,然后使用r命令重新跑程序,会触发刚才加在main函数处的断点,然后使用n命令让程序走到spt_init(argc,argv)函数调用处,再输入s命令就可以进入该函数中了:
说到step命令,我们还有一个需要注意的地方,就是当函数的参数也是函数调用时,使用step命令会依次进入各个函数中,顺序是怎样的呢?举个例子,看看下面这段代码:
以上代码的程序入口函数是 main函数。在第 22行中,func3使用 func1和 func2 的返回值作为自己的参数。我们在第22行输入step命令,会先进入哪个函数中呢?这就需要补充一个知识点了——函数调用方式。
常用的函数调用方式有__cdecl 和__stdcall,C++的非静态成员函数的调用方式是__thiscall,函数参数的传递在本质上是函数参数的入栈。对于这三种函数调用方式,参数的入栈顺序都是从右向左的。在这段代码中,由于没有显式标明函数的调用方式,所以采用的函数调用方式是__cdecl,这是C/C++中全局函数和类静态方法的默认调用方式。
因此,当我们在第22行代码处输入step时,先进入的是func2;当从func2返回时再次输入step命令,会接着进入func1;当从func1返回时,两个参数已经计算出来了,这时会最终进入func3。读者只有理解这一点,在遇到这样的代码时,才能根据需要进入自己想要的函数中调试。
在实际调试时,我们在某个函数中调试一会儿后,不希望再一步步地执行到函数返回处,而是希望直接执行完当前函数并回到上一层调用处,这时可以使用finish命令。与finish命令类似的还有return命令。return命令用于结束执行当前函数,同时指定该函数的返回值。这里需要注意二者的区别:finish命令用于执行完整的函数体,然后正常返回到上层调用中;return命令用于立即从函数的当前位置结束并返回到上层调用中,也就是说,如果使用了return命令,则在当前函数还有剩余的代码未执行完毕时,也不会再执行了。我们用一个例子来验证一下:
在main函数处加一个断点,然后运行程序;在第15行使用step命令进入func函数中;接着使用next命令单步运行到代码第8行,直接输入return命令,这样func函数剩余的代码就不会接着执行了,所以printf("b=%d.\n",b);行没有输出。同时,由于我们没有在return命令中指定这个函数的返回值,所以最终在 main函数中得到的变量 c的值是一个脏数据。这就验证了上面的结论:return命令立即从函数当前位置结束并返回到上一层调用中。验证过程如下:
我们再次用return命令指定一个值试一下,这样我们得到的变量c的值应该就是我们指定的值了。验证过程如下:
仔细观察以上代码,我们用 return命令修改了函数的返回值,当使用 print命令打印c的值时,c的值也确实被修改成了9999。
再次对比使用finish命令结束函数执行的结果:
结果和我们预期的一样,finish正常结束了我们的函数,剩余的代码也会被正常执行。因此c的值是17。
实际调试时,还有一个 until 命令,可被简写为u,我们使用这个命令让程序运行到指定的行停下来。还是以redis-server的代码为例:
以上是redis-server中initServer函数的部分代码,位于文件server.c中。当gdb停在第2740行时(注意:这里的行号以在gdb调试器中显示的为准,不是源码文件中的行号,由于存在条件编译,部分代码可能不会被编译到可执行文件中,所以实际的调试符号文件中的行号与源码文件中的行号可能不会完全一致),我们可以通过输入u 2774命令让gdb直接跳到第 2774行,这样就能快速执行完第 2740~2774 行中间的代码(不包括第 2774行)。当然,我们可以先在第2774行加一个断点,然后使用continue命令运行到这一行来达到同样的效果,但是使用until命令显然更方便:
jump命令的基本用法如下:
location可以是程序的行号或者函数的地址,jump会让程序执行流跳转到指定的位置执行,其行为也是不可控的,例如跳过了某个对象的初始化代码,直接执行操作该对象的代码,可能会导致程序崩溃或其他意外操作。jump命令可被简写为j,但是不可被简写为jmp,使用该命令时有一个注意事项:如果 jump 跳转到的位置没有设置断点,那么 gdb执行完跳转操作后会继续向下执行。举个例子:
假设我们的断点的初始位置在第3行(代码A),那么这时我们使用jump 6,程序会跳过代码B和C的执行,执行完代码D(跳转点),程序并不会停在第6行,而是继续执行后续代码,因此如果我们想查看执行跳转处代码的结果,就需要在第6、7或8行代码处设置断点。
通过 jump 命令除了可以跳过一些代码的执行,还可以执行一些我们想执行的代码,而这些代码在正常逻辑下可能并不会被执行。当然,根据实际的程序逻辑,可能会产生一些非预期结果,这需要我们自行斟酌使用。举个例子,假设现在有如下代码:
我们在第4、14行设置一个断点,在触发第4行的断点后,在正常情况下程序执行流会走 else 分支,我们可以使用 jump 7 强行让程序执行 if分支,接着 gdb 会因触发第 14行的断点停下来,此时我们接着执行jump 11,程序会将else分支中的代码重新执行一遍。整个操作过程如下:
redis-server在入口函数main处调用了initServer,我们使用b initServer、b 2753、b 2755分别在这个函数入口处、第2753行、第2755行增加3个断点,然后使用run命令重新运行程序。触发第1个断点后,输入c命令继续运行,然后触发第2753行的断点,接着输入jump 2755。以下是操作过程:
程序跳过了第2754行的代码。第2754行的代码用于获取当前的进程id:
由于这一行被跳过,所以 server.pid 的值应该是一个无效的值,我们可以使用 print命令将这个值打印出来看一下:
结果是0,这个0值是Redis初始化时设置的。
gdb的jump命令的作用,与使用Visual Studio调试时通过鼠标将程序当前的执行点从一个位置拖到另一个位置的作用一样。
2.5.13 disassemble命令
在某些场景下,我们可能要通过查看某段代码的汇编指令去排查问题,或者在调试一些不含调试信息的 release 版程序时,只能通过反汇编代码定位问题。在此类场景下,disassemble 命令就派上用场了。disassemble 会输出当前函数的汇编指令,例如在 Redis的initServer函数中执行该命令,会输出initServer函数的汇编指令,操作如下:
gdb的反汇编格式默认为AT&T格式,可以通过show disassembly-flavor查看当前的反汇编格式。如果习惯阅读intel汇编格式,则可以使用set disassembly-flavor intel命令来设置。操作如下:
disassemble 命令在程序崩溃后产生 core 文件,且在无对应的调试符号时非常有用,此时可以通过分析汇编代码排查一些问题。
2.5.14 set args与show args命令
很多程序都需要我们传递命令行参数。在gdb调试中使用gdb filename args这种形式给被调试的程序传递命令行参数是行不通的。正确的做法是在用gdb attach程序后,使用run 命令之前,使用 set args 命令行参数来指定被调试程序的命令行参数。还是以redis-server 为例,redis 在启动时可以指定一个命令行参数,即它的配置文件,位于redis-server 文件的上一层目录下。所以我们可以在 gdb 中这样传递这个参数:set args../redis.conf,可以通过show args查看命令行参数是否设置成功:
如果在单个命令行参数之间有空格,则可以使用引号将参数包裹起来:
如果想清除已经设置好的命令行参数,则使用set args不加任何参数即可:
2.5.15 watch命令
watch是一个强大的命令,可以用来监视一个变量或者一段内存,当这个变量或者该内存处的值发生变化时,gdb就会中断。监视某个变量或者某个内存地址会产生一个观察点(watch point)。
比如有一个面试题:有一个变量的值被意外修改,单步调试或者挨个检查使用该变量的代码,工作量非常大,那么如何快速定位该变量被修改的位置呢?其实,面试官想要的答案是“通过数据断点”。watch命令可能通过添加硬件断点来达到监视数据变化的目的。watch命令的使用方式是watch变量名或内存地址,一个观察点一般有以下几种格式。
(1)整形变量:
(2)指针类型:
注意:watch p与**watch*p**是有区别的,前者是查看*(&p),是p变量本身;后者是p所指的内存的内容,一般是我们所需的,我们在大多数情况下要看某内存地址上的数据如何变化。
(3)监视一个数组或内存区间:
这里是对buf的128个数据进行了监视。
需要注意的是:当设置的观察点是一个局部变量时,局部变量失效后,观察点也会失效。例如在观察点失效时,gdb可能会提示如下信息:
2.5.16 display命令
display 命令用于监视变量或者内存的值,每次 gdb 中断,都会自动输出这些被监视变量或内存的值。例如,某个程序有一些全局变量,在每次触发断点后gdb中断下来时,我们都希望自动输出这些全局变量的最新值,这时就可以使用display命令了。display命令的使用格式是display 变量名/内存地址/寄存器名:
在以上代码中,我们使用 display 命令分别监视寄存器 ebp 和寄存器 eax,要求 ebp寄存器分别使用十进制和十六进制两种形式输出其值,这样每次 gdb中断下来时,都会自动将这些寄存器的值输出。我们可以使用info display查看当前已经监视了哪些值,使用delete display清除全部被监视的变量,使用delete display 编号移除对指定变量的监视。操作演示如下:
2.5.17 dir命令
读者可能会遇到这样的场景:使用gdb调试时,生成可执行文件的机器和实际执行该可执行程序的机器不是同一台机器,例如大多数企业产生目标服务程序的机器是编译机器,即发版机,然后把发版机产生的可执行程序拿到生产机器上执行。这时如果可执行程序崩溃,我们用gdb调试core文件时,gdb就会提示“No such file or directory”,如下所示:
或者由于一些原因,编译时的源码文件被挪动了位置,使用gdb调试时也会出现上述情况。
gcc/g++编译出来的可执行程序并不包含完整的源码,-g 只是加了一个可执行程序与源码之间的位置映射关系,我们可以通过dir命令重新定位这种关系。
dir命令的使用格式如下:
SourcePath1、SourcePath2、SourcePath3 指的就是需要设置的源码目录,gdb 会依次到这些目录下搜索相应的源文件。
以上面的错误提示为例,原来的 AsyncLog.cpp 文件位于/home/flamingoserver/base/目录下,由于这个目录被挪动,所以 gdb提示找不到该文件。现在假设该文件被移动到/home/zhangyl/flamingoserver/base/目录下,我们只需在 gdb调试中执行 dir/home/zhangyl/flamingoserver/base/,即可重定向可执行程序与源码的位置关系:
(gdb)
使用 dir命令重新定位源文件的位置之后,gdb就不会再提示这样的错误了,我们此时也可以使用gdb的其他命令(如list命令)查看源码。
如果要查看当前设置了哪些源码搜索路径,则可以使用show dir命令:
dir命令不加参数时,表示清空当前已设置的源码搜索路径: