相比于写代码,程序员需要花费更多的时间在调试代码上,调试自己新写的代码、调试别人出问题的代码、调试开源代码等等。gdb在调试程序方面的重要性不用多说,熟练 掌握使用gdb的基本功能可以解决很多问题;精通gdb的强大的高级功能可以极大的提高调试程序的效率。
网上关于gdb的文章铺天盖地,但是很多都是纸上谈兵,简单的罗列一些基本命令。本文从平时项目中遇到的实际问题出发,记录如何使用gdb的各种命令和功能,同时还有 gdb源码实现细节以及源码编译安装方面的问题记录。
源码编译GDB
根据gdb编译环境、运行环境以及运行模式的不同,编译前的配置和编译方式有较大不同。下面以常见的x86和hisi嵌入式平台为例,说明在编译服务器(x86架构)上的编译和配置命令。
-
x86平台上,gdb以独立程序运行。
配置命令:
--prefix=$HOME
-
hisi平台上,gdb以独立程序运行。
配置命令:
--host=arm-hisiv300-linux --target=arm-hisiv300-linux --prefix=$HOME
-
hisi平台上,gdb以server模式运行在目标板上,gdb客户端运行在编译服务器上。
先进入源码gdb文件夹内执行配置:
--host=x86_64-unknown-linux-gnu --target=arm-hisiv300-linux --prefix=$HOME
然后进入gdb/gdbserver文件夹执行配置:
--host=arm-hisiv300-linux --target=arm-hisiv300-linux
如果hisi的编译环境没有install在默认路径,还得额外配置一下,比如hisi的编译器在
/home/hisi/bin
目录下,hisi系统的头文件在/home/hisi/include
目录下,那么要再执行make
前,先执行export PATH=$PATH:/home/hisi/bin
,然后进入gdb/gdbserver重新配置:CFLAGS=-I/home/hisi/include --host=arm-hisiv300-linux --target=arm-hisiv300-linux
然后先在gdb根目录下执行:make, 再进入gdb/gdbserver目录下执行:make
基本功能
gdb分析交叉编译环境下的coredump
如果项目中存在交叉编译的程序出现crash生成coredump,那么直接在电脑上运行gdb是肯定不行的。这时候需要下面2个东西才行:
- toolchain
- rootfs
toolchain就是可以在你编译主机上进行编译的工具链,比如里面含有toolchain/3.0/bin/mips-linux-gdb
这样的gdb程序
rootfs就是运行目标程序的主机上的根目录,里面含有了所有目标程序依赖的library,比如libc.so libpthread.so等等
步骤
- 创建gdbinit.txt文件
# Set filesystem root folder
set sysroot $sysroot_release
# Prepend prepared filesystem path
set solib-absolute-prefix $sysroot_release
# If external debug symbols are used, set correct references to them
set substitute-path /usr/src/debug $sysroot_release/usr/src/debug
set debug-file-directory $sysroot_release/usr/lib/debug
# Set library path
set solib-search-path $sysroot_release/usr/lib/
- 执行
bin/mips-linux-gdb <file of elf> core-xxxx -ix ./gdbinit.txt
基本设置项
打印输出长度设置
在gdb中p一个长字符串时可能出现打印不完全的情况,这是因为gdb中对print命令的输出有默认长度限制,使用命令set print elements number-of-elements
来
设置限制的长度值,当number-of-elements = 0时,即执行set print elements 0
,意思就是不对print输出长度做如何限制。
查看源代码
gdb之所以能够知道对应的源代码,是因为调试版本的可执行程序中记录了源代码的位置信息。因为只是记录了源码的位置,所以如果 要让gdb打印出源码的话,显然需要gdb去某个目录中去寻找相应的.c文件才行。
默认情况下,gdb在编译时目录($cdir
)中去搜索,如果失败则在当前目录($cwd
)中去搜索。如果这两个目录都不方便存放我们的源代码,
我们可以通过--directory
参数指定源代码的搜索位置,或者在运行时执行directory
命令来动态添加搜索路径信息。
watch
gdb的wathc功能有时在定位”离异”问题时可以发挥神效,watch的基本语法是:watch [-l|-location] expr [thread thread-id] [mask maskvalue]
。
语法很简单,但是在实际使用时经常出现一些异常情况,比如在下面一段代码中,
执行watch i965->surface_heap
的话,可以watch成功,但是程序继续运行一会就会提示如下异常:
出现这个问题的原因是,i965是一个栈上的临时变量,这个临时变量指向堆上内存后,执行watch i965->surface_heap
是没有问题的,
但是当从函数1965_CreateSurfaces2()退出后,i965这个栈上变量就不存在了,所以出现了上面的错误。
解决上面的问题有两个方法,一个是先p出i965->surface_heap的地址,然后直接watch这个地址。在watch地址时要注意,如果直接
执行watch 0x7fff8c001598
是不行的,会报如下错误:
因为执行watch 0x7fff8c001598
时,gdb显然不知道要监控的地址大小是多少,是一个字节还是几个字节,所以正确的方法是执行
watch *(struct object_heap *)0x7fff8c001598
。通过强制类型可以让gdb知道需要监控几个字节的数据,另外最前面的*
表示监控的是这个
地址里面的数据,而不是监控这个地址。
上面这个方法有点麻烦,因为你得去源码中寻找这个地址的类型,所以gdb提供了另外一个很方便的方法解决这个问题,那就是上面语法中的-l
选项。
执行watch -l i965->surface_heap
就可以搞定了,是不是很给力?
网上官方关于watch -l 选项的说明让人不是很清楚,正确的解释借用stack overflow上的一句回答:
If you’re using a fixed address because you want to watch a variable outside the local scope, use watch -l localptr->member instead.
上面的i965正好就是这个localptr。
另外要注意的是,watch一个变量只会在该变量值被改变,也就是被写时断点下来,如果需要在一个变量被读时才停下,则需要使用rwatch命令, 如果是读写都要断点下来则使用awatch命令,两命令个语法和watch一样。
gdb的自动化脚本
动态打印
动态添加打印很简单,但是有时我们需要打印的如果是链表,那么事情就没有这么简单了。通常我们断点后打印链表的内容时候如下:
是不是觉得特别累?这个时候我们就需要自动化脚本来解决这个问题。假如我们需要调试的代码段如下:
上面高亮的变量就是我们想要实时监测的链表,为了实现在程序运行到此处代码时打印出这个完整的链表,我们编写如下两个脚本:
#file nama: p_output_modes
define p_help
set $mode = $arg0
while($mode)
printf "mode name %s\n", $mode->name
set $mode = $mode->next
end
end
#file name: randr.script
set pagination off
set print thread-events off
source p_output_modes
b xf86Crtc.c:1667
comm
silent
p output_modes
p_help output_modes
c
end
然后我们执行:gdb /usr/bin/Xorg -p 3107 -x randr.script
,部分输出结果如下:
自动循环调用函数
实际项目中应该会碰到这样的场景,你想测试某个接口函数的功能,或者是压力测试或者是简单的功能测试。为此,我们可以编写测试demo,但是如果这个接口函数依赖的其他库太多,编译这个demo是挺费事的话。这时gdb的自动化脚本的使用可以让事情变得非常简单轻松,尤其是需要进行压力测试时。
#vo.script
set pagination off
set print thread-events off
set $iter = 0
set $cnt = 0
set $port = 0
b dhssm_displaylink_platform.c:328
comm
silent
set $port = ($iter / 2) % 3
set $cnt = $cnt % 2
printf "will put %s hdmi%d\n", $cnt == 0 ? "off" : "on", $port
call VO_SwitchPort($port, $cnt)
set $iter++
set $cnt++
c
end
上面脚本主要实现的功能就是测试VO_SwitchPort()这个接口函数的功能,压力测试hdmi0~hdmi2开关功能。
基本命令
cpp特有的
- info vtbl xxx - 查看xxx对象中的虚函数表