简介
在文章gdb调试基础中,学习了gdb基础的调试命令和手法。但在实际开发过程中,除了调试手法之外,如何构建出适合生产环境的应用和依赖,gdb以什么样的规则还原崩溃现场的依赖,我们如何在开发环境下搭建出一套和生产环境类似的依赖路径,这些同样非常重要。基于以上问题,本篇文章给出了可行的方案和详细的回答,我们开始
传统coredump生成与调试
配置coredump生成
你的应用崩溃时,Ubuntu默认不会生成coredump,需要进行如下配置
bash
sudo sysctl -w kernel.core_pattern=core.%e.%p
ulimit -c unlimited
这样程序崩溃后,会在当前目录生成以core开头的dump文件。需要注意的是,这样只是临时修改,且只对当前shell生效
完成coredump的配置之后,还需要留意cmake的不同构建类型产生的dump文件存在的差异
cmake中的四种构建类型
cmake中有Debug、Release、RelWithDebInfo和MinSizeRel四种构件类型
- Debug:包含完整的调试信息,编译器不会对代码有任何优化(传递
-g编译参数) - Release:不包含调试信息,编译器会对代码进行优化,以提高运行速度(
-O3 -DNDEBUG) - RelWithDebInfo:包含调试信息,编译器会对代码进行一定程度的优化(
-O2 -g -DNDEBUG) - MinSizeRel:不包含调试信息,编译器会启用二进制文件大小的优化,这一般应用于资源受限的系统中(
-Os -DNDEBUG)
这里值得注意的是,cmake中设置CMAKE_BUILD_TYPE为其中某一种类型时,本质只是默认配置了向编译器传递的编译参数(主要是-g和-O),你可以在cmake文件中重新指定覆盖某种构建类型的默认行为
调试dump文件
完成前面的配置之后,你的应用崩溃时在当前目录会生成一个core开头的文件,这就是操作系统生成的coredump,笔者这里是core.gdb_lab.16872,然后直接使用gdb命令,如下
bash
gdb ./build/gdb_lab core.gdb_lab.16872
这样就进入了gdb的交互式界面,现在可以按照上一篇文章gdb调试基础介绍的那样开始调试你崩溃的应用了
这里需要留意,Debug和RelWithDebInfo两种构建类型产生的dump文件能够正常添加断点调试,能够正常打印变量信息,同时也包含文件名和代码行号等详细信息;Release和MinSizeRel这两种构建类型缺乏大量调试内容,gdb分析dump文件时,bt只能看见简单的堆栈信息
更适合生产环境的coredump方案
在生产环境中,往往不能使用上面的方案,因为带有调试符号的应用程序通常非常的大,这会为生产环境中部署带来麻烦,所以通常的做法是配置编译时携带符号信息,产物生成后将应用和依赖中的符号信息复制一份到单独的文件中,然后再将这些符号信息从你的应用或者依赖中剥离。当出现core dump时,再加载单独的符号文件,这样即能保证应用和依赖的体积,也能通过加载的符号文件直观的观察崩溃时应用的工作情况,两不误
编译带符号信息的产物
首先需要编译出携带详细符号信息的产物,在cmake中通常配置RelWithDebInfo,这样编译器会优化代码的同时还会在产物中保留符号信息。读者这里的产物是gdb_lab应用,可以通过readelf -S build/gdb_lab | grep debug 、file build/gdb_lab和nm build/gdb_lab三个命令查看gdb_lab中是否包含符号信息

如果存在的话会观察到以上类似的输出,其中file命令显示not stripped。我们来看下现在gdb_lab文件的大小

gdb_lab大小为2532448Byte,差不多2.4MiB,这已经比笔者的测试代码还大了。所以如果在生产环境中,还是使用这样的方案,那么对生产设备的硬件要求将会相当高。所以这就是需要将符号信息单独提取出来的意义
符号提取
首先,将gdb_lab中的符号信息提取到一个单独的文件中
bash
objcopy --only-keep-debug build/gdb_lab build/gdb_lab.debug
这样一来,在build目录中就会生成文件gdb_lab.debug,这就是gdb_lab文件中的符号信息
成功提取符号信息之后,就可以放心的将符号从gdb_lab中剥离了
bash
strip build/gdb_lab -o build/gdb_lab_stripped
执行以上命令后,gdb_lab_stripped就是将gdb_lab进行符号剥离后的应用,部署到生产环境时就使用gdb_lab_stripped
我们来观察一下三者的大小

可以看到gdb_lab.debug和gdb_lab_stripped的大小相加,差不多就是原始携带符号信息的gdb_lab的大小
我们同样使用先前的命令查看被剥离符号信息后的应用,观察

可以看到,这里的输出和前面的输出区别相当大,file命令输出stripped;nm命令输出no symbols
cmake自动化提取符号
在编译流水线中,这样手动提取符号当然不方便,可以通过cmake配置自动化的符号提取方案
cmake
if(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
add_custom_command(TARGET gdb_lab POST_BUILD
COMMAND ${CMAKE_OBJCOPY} --only-keep-debug
$<TARGET_FILE:gdb_lab>
${CMAKE_BINARY_DIR}/gdb_lab.debug
COMMAND ${CMAKE_STRIP} $<TARGET_FILE:gdb_lab> -o $<TARGET_FILE:gdb_lab>_stripped
COMMENT "Release: 分离调试符号到 symbols/gdb_lab.debug 并 strip"
)
endif()
这样,在完成构建后,符号文件和经过strip的应用就都在build目录下了。或者在add_custom_command中不进行strip,通过cmake安装时,使用
shell
cmake --install build/ --strip
这样,install中的产物就是将符号剥离后的产物了
调试符号剥离后的产物
接下来我们直接运行符号剥离后的应用gdb_lab_stripped,会产生一个coredump,笔者这里是core.gdb_lab_strippe.19187。按照之前的经验,我们使用gdb
shell
gdb ./build/gdb_lab_stripped core.gdb_lab_strippe.19187

发现,这里bt之后全是问号,这就是因为我们把符号剥离了,所以导致这里堆栈信息都是问号。还记得我们一开始使用objcopy 提取出来的符号文件吗?这时候就需要在gdb的交互式命令行中加载这个符号文件了
shell
symbol-file ./build/gdb_lab.debug

符号文件加载后,我们的堆栈清晰了很多,文件、函数、崩溃的行号都显示出来了,此时也可以打印当前栈帧中变量的值。这就是符号文件的作用
这里我们注意到,在调试符号分离的应用时,进入gdb交互界面之后,需要手动通过symbol-file命令加载符号文件,如果符号文件多了这会比较繁琐,有没有什么比较优雅的方法呢?当然有
通过build id关联符号文件
每次编译,产物中都会储存一个唯一的Build ID,我们可以通过这个Build ID将产物和符号文件进行关联,这样就不需要每次手动加载文件了
第一步就是确定产物的Build ID
shell
readelf -n ./build/gdb_lab_stripped | grep "Build ID"
笔者这里的Build ID为139f2905c87e07d3ee995767c64b4cb5684697fb
gdb在加载应用后,会去固定规则的目录下查找符号文件,规则如下
shell
/usr/lib/debug/.build-id/<前两位十六进制>/<剩余部分>.debug
这里的/usr/lib/debug目录在gdb交互式命令行中通过show debug-file-directory得到
对于笔者这里的产物来说,gdb会自动判断/usr/lib/debug/.build-id/13/9f2905c87e07d3ee995767c64b4cb5684697fb.debug文件是否存在,如果存在则会自动加载符号,反之则不会加载,需要手动加载
嗯,这好像也不是很方便
add-gnu-debuglink关联符号文件
上面一种自动加载符号文件的方案也没有减少太多工作量,通过add-gnu-debuglink的方式关联符号文件会方便很多
这种方式需要在编译阶段添加额外的步骤
cmake
if(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
add_custom_command(TARGET gdb_lab POST_BUILD
COMMAND ${CMAKE_OBJCOPY} --only-keep-debug
$<TARGET_FILE:gdb_lab>
${CMAKE_BINARY_DIR}/gdb_lab.debug
COMMAND ${CMAKE_STRIP} $<TARGET_FILE:gdb_lab> -o $<TARGET_FILE:gdb_lab>_stripped
COMMAND ${CMAKE_OBJCOPY}
--add-gnu-debuglink=gdb_lab.debug
$<TARGET_FILE:gdb_lab>_stripped
COMMENT "Release: 分离调试符号到 symbols/gdb_lab.debug 并 strip"
)
endif()
我们对比上面的cmake,这里通过objcopy将符号文件的文件名和strip之后的产物进行了关联,可以通过
shell
objdump -s -j .gnu_debuglink ./build/gdb_lab_stripped
这个命令查看关联关系是否成功建立,一切顺利的话将会输出gdb_lab.debug,即表示gdb_lab_stripped关联的符号文件名称为gdb_lab.debug
符号文件和产物的关联关系确定了,那么这种方式下gdb会去哪些目录下查找这个文件呢?
- 可执行文件所在目录:对于笔者这里来说,就是会尝试加载
./build/gdb_lab.debug - 可执行文件目录下的 .debug/ 子目录:笔者这里会尝试加载
./build/.debug/gdb_lab.debug - debug-file-directory(默认 /usr/lib/debug)拼接可执行文件的完整路径:笔者这里会尝试加载
/usr/lib/debug/home/zuoluo/SourceCode/gdbTest/build/gdb_lab.debug
以上三条规则就是通过add-gnu-debuglink建立关联关系后,gdb查找符号文件的规则,gdb如果成功找到符号文件则会输出Reading symbols from /home/zuoluo/SourceCode/gdbTest/build/.debug/gdb_lab.debug,可以留意gdb的输出内容
这要友好很多,更推荐这种方式
配置动态库加载路径
在实际开发中,崩溃环境现场通常远离我们的开发环境。coredump是应用在发生崩溃时操作系统为其生成的进程快照,这其中也包括应用依赖哪些动态库,以及这些动态库的位置,这些位置信息硬编码到coredump文件中,gdb在调试coredump文件时会使用硬编码的位置查找动态库并加载,info sharedlibrary命令直观的展示了哪些动态库被gdb加载了,哪些没有找到
所以在开发环境调试coredump文件时,设置gdb搜索和加载动态库的路径就显得非常关键
solib-search-path
在进入gdb的交互式界面之后,可以通过set solib-search-path的方式设置gdb搜索动态库的路径

上图可以看到,gdb还没有加载libgdblib.so库,bt时不能显示任何符号信息,info sharedlibrary也显示libgdblib.so是No的状态,这时候就需要我们手动设置动态库的搜索路径了

上图通过solib-search-path设置了搜索路径,gdb立刻尝试从新设置的路径中加载那些还未加载的动态库,这里就是libgdblib.so。因为我在相同路径下放置了对应动态库的符号文件,所以对应符号文件也自动加载了进来。如此一来,bt就能看到详细的堆栈信息了
可以看到堆栈中还存在一些无法解析的符号,那是因为笔者这里只加载了动态库的符号文件gdblib.debug,没有加载可执行程序的符号文件
这里还需要注意的一点是,如果在set solib-search-path之前,gdb就已经找到了需要加载的动态库,那么即使你通过set solib-search-path设置的目录下有需要的动态库,那么gdb也不会尝试重新加载,会继续使用已经加载的动态库
sysroot
通过set sysroot的方式可以设置路径前缀。怎么理解呢?
coredump中记录了应用崩溃时各个动态库的绝对路径,调试coredump时gdb尝试加载这些动态库,如果不存在则不加载
如果我们设置了sysroot,那么gdb就会将sysroot设置的路径拼接到从coredump中读取的路径之前,作为一个新的绝对路径去加载。这对于生产环境中依赖复杂的应用非常好使

笔者这里设置了/home/zuoluo/SourceCode/gdbTest为sysroot,gdb将sysroot的路径拼接到coredump中记录的路径之前
这里值得注意的是,即使在set sysroot之前gdb已经找到了对应的动态库,当设置了set sysroot之后,gdb也会按照设置的sysroot重新去查找和加载动态库,如果找不到,那么gdb不会使用原来加载成功的库,info sharedlibrary会显示未加载
.gdbinit自动化配置文件
谁也不想coredump,但是现实往往事与愿违,线上总是出现coredump,每一次的coredump在进入gdb调试界面时都需要执行差不多的命令配置符号环境、动态库搜索路径等,这会有大量重复性劳动,特别是一遇到工程比较大,同时依赖比较复杂的情况,这样每次都手动配置环境会大大增加调试负担。有没有什么解决方案呢?答案当然是有的------.gdbinit
.gdbinit一般放在家目录下,gdb在启动调试某个应用后,会去家目录读取这个文件,你在这个文件中写的任何调试配置命令都会被gdb执行,不再需要你手动去配置,这很优雅
当然,你也可以自定义文件路径,然后通过gdb命令行参数指定某个自动化配置文件
shell
gdb -x ./my.gdbinit ./build/bin/gdb_lab core.gdb_lab.7681
my.gdbinit文件中配置的指令就会被gdb正确的执行,非常方便
总结
本文介绍了coredump配置的方法,再此基础上给出了cmake工程下更适合生产环境的配置方案,回答了gdb在调试时的一些默认行为规则,了解掌握这些规则能够帮助我们设计更好的自动化配置方案,大大提高我们的调试效率