Linux开发必须要把gdb玩的666呀,建议先阅读下《C++ vector STL实现详解》。gdb玩的666可以摆脱调试中各种使用std::cout
打印输出变量的恶习,尤其对于大型项目,每次调试都需要增加std::cout
,单单编译时长都能让你发狂,严重影响开发效率。
gdb原生支持定义打印函数,了解类型后,可以自行定制打印函数。目前网上有提供针对stl标准容器的现成文件.gdbinit
,该文件会定义string、vector、map、set、deque、priority_queue
等等,需要可以直接下载这里, 把文件保存为.gdbinit
, 放在用户目录下~/.gdbinit
,每次执行gdb调试会自动加载.gdbinit
初始文件。
文件中vector定义如下
define pvector
if $argc == 0
help pvector
else
set $size = $arg0._M_impl._M_finish - $arg0._M_impl._M_start
set $capacity = $arg0._M_impl._M_end_of_storage - $arg0._M_impl._M_start
set $size_max = $size - 1
end
if $argc == 1
set $i = 0
while $i < $size
printf "elem[%u]: ", $i
p *($arg0._M_impl._M_start + $i)
set $i++
end
end
if $argc == 2
set $idx = $arg1
if $idx < 0 || $idx > $size_max
printf "idx1, idx2 are not in acceptable range: [0..%u].\n", $size_max
else
printf "elem[%u]: ", $idx
p *($arg0._M_impl._M_start + $idx)
end
end
if $argc == 3
set $start_idx = $arg1
set $stop_idx = $arg2
if $start_idx > $stop_idx
set $tmp_idx = $start_idx
set $start_idx = $stop_idx
set $stop_idx = $tmp_idx
end
if $start_idx < 0 || $stop_idx < 0 || $start_idx > $size_max || $stop_idx > $size_max
printf "idx1, idx2 are not in acceptable range: [0..%u].\n", $size_max
else
set $i = $start_idx
while $i <= $stop_idx
printf "elem[%u]: ", $i
p *($arg0._M_impl._M_start + $i)
set $i++
end
end
end
if $argc > 0
printf "Vector size = %u\n", $size
printf "Vector capacity = %u\n", $capacity
printf "Element "
whatis $arg0._M_impl._M_start
end
end
现在打印vector就很简单了,比如 :pvector v
(vector
变量), 上述定义支持:
pvector v # 打印所有内容
pvector v 0 # 打印位置为0的内容
pvector v 1 2 # 打印[1, 2]的内容
上述的配置无法支持unorder map
等C++11新增的容器,除非自己定义新的打印函数,不过也可以通过配置pretty printer来解决,默认安装gcc高版本(>4.6.0)情况下会自带pretty printer
,本文基于gcc-6自带的pretty printer
进行配置,否则可以通过svn来下载最新版本: svn co svn://gcc.gnu.org/svn/gcc/trunk/libstdc++-v3/python
,注意:libstdc++版本要与gcc版本对齐,否则不会正常工作。pretty printer
也支持用户的自定制,具体例子点击此处,实现变量具体打印类,已经gdb类型名字到打印类的映射。下面给出.gdbinit
中pretty printer
配置内容:
.gdbinit增加配置
python
import sys
sys.path.insert(0, '/usr/local/Cellar/gcc/6.4.0/share/gcc-6.4.0/python')
from libstdcxx.v6.printers import register_libstdcxx_printers
register_libstdcxx_printers (None)
end
其中需要在自己目录下找到libstdcxx
对应的路径,填写在sys.path.insert
变量里,如果不使用pretty printer
, 可以使用/r
参数原生打印。
p /r xxx
注意,在Mac系统中,如果使用gdb版本很高,笔者gdb版本为8.0.1,生成调试程序必须带有-gdwarf-3
参数,只有使用该参数,pretty printer
才能生效,调试信息兼容到高版本。Mac 中高版本gdb调试gcc高版本编译程序,需要编译使用-gdwarf-3。更具体的解释是:gcc版本大于4.8,缺省生成dwarf4版本的调试信息,pretty printer
基于dwarf3
版本,因此gdb中pretty printer
可能无法识别相应类型符号,需要通过选项-gdwarf-3
来生成低版本调试信息,简单编译例子:
g++-6 -g -gdwarf-3 -O0 -o test test.cpp
gdb可以装备VS Code:正常情况下VS Code调试控制台能够直接使用,比如打印map
; 需要在控制台输入:
-exec p cfg_
VS Code调试控制台运行gdb命令都需要加上-exec
,VS Code打印与原生gdb打印一致,如下图所示:
pretty print
时,需要在debug配置文件
launch.json
加入, github官方论坛上解释:VS Code为了追求性能,鼠标停靠变量上、监控区变量时会关闭
pretty print
,通过配置
launch.json
可以强行打开,配置如下:
"setupCommands": [{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}],
笔者launch.json
配置为
{
"version": "0.2.0",
"configurations": [
{
"name": "gdb",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceRoot}/xgboost",
// "args": ["${workspaceRoot}/demo/regression/machine.conf"],
"args": ["${workspaceRoot}/demo/binary_classification/mushroom.conf"],
"stopAtEntry": false,
"cwd": "${workspaceRoot}",
"environment": [],
"externalConsole": true,
"preLaunchTask": "make", // 代码有修改,先执行make编译任务
"setupCommands": [{ // 鼠标悬挂查看变量值,使用pretty print配置
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}],
// VS Code debug的日志信息,能清楚gdb加载和运行的各种过程,如果没有其他用途可以注释
"logging": {
"moduleLoad": false,
"trace": true,
"engineLogging": true,
"exceptions": true
// "traceResponse": true
},
"MIMode": "gdb"
}
]
}
效果如下图所示,可见代码变量与监控区的变量显示都是pretty print
形式。
实际工作中,公司内部库都是基于linux,需要在linux环境下编译程序,经常想快速了解公司内大神们写的模块源码,最好的方式就是debug整个流程。然而对于笔者来说,gdb用的还不是非常6,有时还需要查看文本源码,目前来说调试gdb不太友好,笔者还是喜欢基于IDE的debug调试过程。而公司开发机子系统为Mac,而Mac系统编译不了代码,为了方便debug,需要gdb远程调试功能,也就是说可以支持不同操作系统下的debug。gdb远程调试模式是本地gdb
+远程gdbserver
形式,这种形式也经常用于嵌入式的调试。
首先,安装gdb必须使用--with-all-targets
参数,因为默认安装是基于机子操作系统的结构体系,而远程调试的机子不一定与本机相同,使用该参数主要是适配远程各种平台的结构体系,当然可以下载gdb源码,修改配置中结构体系配置编译安装,这部分读者自行google,同时远程需要安装gdbserver
。
# 本地主机安装gdb
brew install gdb --with-all-targets
# 远程主机下安装gdb、gdbserver
apt-get install gdb
apt-get install gdbserver
远程主机编译程序,运行gdbserver服务监听特定端口:
g++ -g -O0 -o test test.cpp
gdbserver localhost:9999 test # localhost也可以改成远程主机IP:10.101.8.8
# killall gdbserver # kill进程
本地主机执行gdb,在gdb命令行中输入:
target remote 10.101.8.8:9999 # 连接远程
symbol-file test # 加入符号文件,可执行文件包含符号文件,test为远程主机编译,通过scp来拷贝到本机
continue # 执行调试过程,不是run,因为gdbserver已经启动程序了,后续就可以使用gdb过程。
特殊说明,symbol-file
给定符号文件即可,可以使用objcopy
对可执行文件进行符号文件剥离:
objcopy --only-keep-debug test test.symbol # 对应objcopy --strip-debug test test.bin 只剥离出可执行文件
# gdb 程序内加载symbol文件
symbol-file test.symbol # 使用符号文件即可
scp文件优化:远程debug需要拷贝远程编译文件或者符号文件,对于大型程序100MB+而言,除非网快,否则需要较长等待时间。优化scp拷贝回来的符号文件大小,符号文件剥离本身是个优化,但对于-O0 -g
生成某大型程序150MB,笔者发现剥离出的符号文件138MB,几乎占据整个可执行文件,约90%。使用-O2
高度优化的是可执行文件大小,符号文件并不减少,而且高度优化会使用分支预测,局部变量优化,函数内联等优化技术而造成代码运行逻辑不一致,尽管运行结果一致,不利于人的debug阅读逻辑。g++
编译生成debug信息是可配置的,-g
后面可以加入level等级,默认是2,level越高信息越全,最低level是1,能减少30%生成的符号文件大小,但是使用该符号文件无法调试,部分变量symbol会被删除。最后笔者只能通过压缩语句来减少文件大小的传输,能优化掉2/3的大小,这样一比较,也没太多必要剥离出符号文件。
tar -jcvf test.tar test
scp user@remote_host:xxx/test.tar .
tar -jxvf test.tar
VS Code也支持远程gdb调试,需要配置miDebuggerServerAddress
: "miDebuggerServerAddress": "10.101.8.8:9999"
, 同时必须给定可执行文件"program": "${workspaceRoot}/test"
, 但是该文件可以拷贝于远程,而不需要本地编译,这样就可以基于VS Code边调试,边浏览代码,同时可以设置断点。
{
"version": "0.2.0",
"configurations": [
{
"name":"gdb Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceRoot}/test",
"miDebuggerServerAddress": "10.101.8.8:9999",
"setupCommands": [{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"text": "set sysroot" //不加载远程so文件,不调试动态链接库,跳过read xxx.so from remote target,能减少每次调试准备时间
}
],
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceRoot}",
"environment": [],
"externalConsole": true,
"MIMode": "gdb"
}
]
}
这部分内容会持续更新,笔者会加入gdb实用配置。
1) gdb命令历史保存
set history filename ~/.gdb_history
set history save on
2) 断点保存与恢复
define bsave
shell rm -f brestore.txt
set logging file brestore.txt
set logging on
info break
set logging off
# reformat on-the-fly to a valid gdb command file
shell perl -n -e 'print "break $1\n" if /^\d+.+?(\S+)$/g' brestore.txt > brestore.gdb
end
define brestore
source brestore.gdb
end
每次退出前使用bsave
命令,每次进入gdb想恢复上一次设置的gdb设置断点,可以输入brestore
3) mac系统gdb额外设置
set startup-with-shell off