调试技术之GDB实战笔记

Nov 21, 2017


相比于写代码,程序员需要花费更多的时间在调试代码上,调试自己新写的代码、调试别人出问题的代码、调试开源代码等等。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命令来动态添加搜索路径信息。

gdb_0


watch


gdb的wathc功能有时在定位”离异”问题时可以发挥神效,watch的基本语法是:watch [-l|-location] expr [thread thread-id] [mask maskvalue]

语法很简单,但是在实际使用时经常出现一些异常情况,比如在下面一段代码中,

gdb_1


执行watch i965->surface_heap的话,可以watch成功,但是程序继续运行一会就会提示如下异常:

gdb_2


出现这个问题的原因是,i965是一个栈上的临时变量,这个临时变量指向堆上内存后,执行watch i965->surface_heap是没有问题的, 但是当从函数1965_CreateSurfaces2()退出后,i965这个栈上变量就不存在了,所以出现了上面的错误。

解决上面的问题有两个方法,一个是先p出i965->surface_heap的地址,然后直接watch这个地址。在watch地址时要注意,如果直接 执行watch 0x7fff8c001598是不行的,会报如下错误:

gdb_3


因为执行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的自动化脚本


动态打印


动态添加打印很简单,但是有时我们需要打印的如果是链表,那么事情就没有这么简单了。通常我们断点后打印链表的内容时候如下:

gdb_4


是不是觉得特别累?这个时候我们就需要自动化脚本来解决这个问题。假如我们需要调试的代码段如下:

gdb_5


上面高亮的变量就是我们想要实时监测的链表,为了实现在程序运行到此处代码时打印出这个完整的链表,我们编写如下两个脚本:

#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,部分输出结果如下:

gdb_6



自动循环调用函数


实际项目中应该会碰到这样的场景,你想测试某个接口函数的功能,或者是压力测试或者是简单的功能测试。为此,我们可以编写测试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对象中的虚函数表