https://zhuanlan.zhihu.com/p/395115890?utm_id=0
写这个文档的目的,是让有一定软件开发基础的同学,可以较为快速地入门gdb的使用。gdb的命令非常多,在做了简短的介绍后,我挑选了最高频使用的命令/场景,希望能够让读者一个小时拥有即战力。
为了让读者对gdb有更直观的认识,同时提供了一个demo,可以下载编译,拿着demo来练手。
对于有过使用gdb经验的同学,在一段时间不用后,也可以拿这个文档来回顾下,恢复战力。
gdb是一个debugger,它可以帮助我们深入到正在运行的程序中去看到它到底在做什么,或者程序crash的时候正在做什么。
说人话,gdb是一个程序调试及分析工具,常用于linux下c/c++程序调试。主要提供以下四个功能:
gdb program
这种方式通常直接在target上执行,其中program是要启动的程序。
为了调试时能够获取更多的信息(比如获取到堆栈的函数名等),我们通常使用带调试信息的版本进行gdb调试。
使用gcc的"-g"选项可以在调试的时候加入调试信息。
以下是示例:
https://github.com/dlmu2001/gdb_tutorial 这里是一个实例程序
clone到本地后,执行编译命令, 直接在src目录下生成应用程序t1(linux环境下)
g++ -g --std=c++11 main.cpp -lpthread -o t1
执行"gdb t1"可以直接启动程序进行调试,使用r执行程序,调试完使用q命令退出。
图2-1 gdb启动程序实例图
在系统中,很多程序是开机启动的,而且依赖启动时的环境,并不允许我们使用"gdb program"启动调试。
这个时候可以使用attach命令调试一个已经运行的进程
attach pid
其中pid是程序的进程id,可以通过ps + grep 命令获取(可以参考ps命令)
gdb attach一个进程后,首先会暂停该进程,使用continue/c命令可以让程序继续进行
使用detach命令可以释放调试进程
示例:
先运行2.1中的t1
./t1
然后通过ps命令获取pid
ps aux|grep t1
假设为102474, 执行attach命令
gdb attach 102474
设置断点等
b main.cpp:155
然后"c"继续程序的执行
c
这个示例程序我们设计了在执行目录创建名字为"dump"的命令可以进入dump_items
echo 0>dump
这个时候程序会在我们main.cpp:155这里停下来
图2-2 attach已经在运行的程序
core file是进程运行时时的image,它包含了所有的进程调试信息,比如硬件寄存器内容,进程状态,进程数据。
程序crashed时的core file称之为core dump,通常地说,它就是犯罪现场,是程序crash那一刹那的内存快照。
我们可以使用gdb + core dump来定位crash发生的地点和原因, 这是系统调试中很常见的调试场景。
使用gdb调试core dump的命令是
gdb <executable_path> <coredump_file_path>
其中<executable_path> 表示要调试的程序的路径,<coredump_file_path>表示core dump文件的路径。
需要注意的是,如果你的程序是运行在一个容器内,那么这个<executable_path> 为容器这个program,常见的比如小程序。
在系统调试的时候,通常一个可执行程序会连接很多动态链接库,要得到这些动态链接库的比如符号表,就要设置符号路径,可以使用如下命令设置。
set sysroot <sym_path>
这里sym_path就是符号表的路径。
对于系统调试者来说,可以从公司构建系统去拿到symbols。
gdb + core dump + symbol , 形成了程序crash时基本现场信息。
其它的调试手段,几种启动方式一样,后面进行讲述。
示例:
我们使用t1来生成core dump文件, 如果你的linux版本无法生成core dump, 可以参考linux下生成core dump文件方法及设置
执行t1 : ./t1
在程序当前目录下执行12次 "echo 0 > add", 程序就会因为数组越界出现segment fault,并在当前目录下生成coredump文件, 在我的机器上,coredump文件命名是core.<pid>, 比如core.16025。
此时执行命令"gdb t1 core.16025 -q", 这里加了-q选项不显示版权和介绍信息
图2-3 gdb调试core dump
从调用堆栈可以看到,程序死在handle_add_request的std::makeshared操作中,这里访问size为11的item_names数组的第12个元素。
启动gdb后,在gdb提示符下输入以下命令可以获取到帮助,是记性不好的同学的福音。
help
help class
help command
gdb将命令将命令了几个class(比如stack, breakpoints, data等)
help 可以获取到class 列表, 而help class可以获取到命令列表,最后help command可以获取到command。
堆栈回溯是超高频使用的一个场景,特别是在分析coredump的时候,bt几乎是大部分coredump分析的前两个命令(另一个是set sysroot)。
bt指令可以打印出当前的调用堆栈, 在前面的操作中我们都有演示。
调用堆栈由连续的堆栈帧(stack frames)组成,每一帧(frame)对应一个函数调用,每一帧都包含了参数信息(args), 局部变量信息(locals), 寄存器信息(registers)和函数执行时的地址。
gdb使用一个数值来标识堆栈帧的层级,最后被调用到函数为0,往上值递增。
比如利用gdb调起示例程序,设置函数断点add_item, 然后在程序目录下执行
"echo 0> add", 可以让程序在add_item停下来,这个时候执行bt
图3-1 堆栈回溯
上图handle_add_request就是frame 1。
通过 "frame <number> " 指令可以切换程序到指定的栈帧,从而方便结合info命令查看对应栈帧的信息。
比如info locals获取局部变量信息
info args获取函数入参信息
info frame获得当前栈帧信息
图3-2 栈帧信息
set sysroot <path>是最常用的命令,用来设置被搜索符号文件路径的前缀,值只能是一个
如果被搜索的符号文件路径有多个,则使用set solib-search-path <path1:path2>命令
也可以使用symbol-file <filename>直接指定从<filename>读取符号表
通常系统里面较复杂的应用会链接多个动态库,可以使用info sharedlibrary来查看动态链接库的符号是否加载
p 和 x 是gdb用来查看数据最常用的两个命令。
p是print的缩写,通常使用如下命令来查看变量值
p /f <expr>
<expr>表示变量名, /f 则用来设置输出格式(具体定义见Output-Formats),可以省略,默认以16进制输出。常用格式如下:
示例:
图3-3 p命令查看变量
查看一块buffer/内存地址对应的内容是调试时的一个常见需求。x是examine的缩写,可以使用x命令来查看内存
x/nfu <addr>
其中<addr>表示内存地址,/nfu是命令参数,用来指定要查看的内存的size和格式。
常见用法示例
x/24sb 0x404da8 //字符串形式看一块内存
x/24cb 0x404da8 //以char形式看一块内存
x/24xb 0x404da8 //以16进制形式看一块内存
x/24db 0x404da8 //以10进制形式看一块内存
x/8xw $sp //查看sp开始的8个子的内容
x/I $pc //查看下一条指令
图3-4 x命令查看内存地址
设置程序执行参数有三种方法
"show args" 可以显示当前执行参数
"set evn varname [=value] "命令可以设置程序执行时的环境变量。
[=value]可选,如果不填,表示null
"unset env varname"命令则可以将varname从环境变量中移除
"show env [varname]"命令可以显示环境变量的值,如果[varname]没有传,则显示所有环境变量。
比如
set env USE_HTTPS=1
gdb提供的断点功能非常强大,如果你经常使用这个功能,建议认真阅读gdb文档Breakpoints这章节,可以提高效率。
directory <dirname> 命令可以增加一个目录到源代码搜索路径
set substitute-path <from> <to> 命令可以设置源代码路径替换规则
比如target上的image通常是在公司的编译服务器上编译的,android surfaceflinger的一个源文件的路径是/disk2/jekins/workspace/android/frameworks/native/services/surfaceflinger/Client.cpp,
而我本机surface_flinger目录地址是/home/ututu/project/surfaceflinger
可以设置
set substitute-path /disk2/jekins/workspace/android/frameworks/native/service/surfaceflinger /home/ubutu/project/surfaceflinger
这样查询源代码的时候会自动替换对应的源代码路径。
当然,如果你下载的是整个aosp,你可以直接使用directory命令设置搜索路径
directory /home/ubuntu/branch/android_latest
使用"list <location> "来显示源代码
这里<location>可以是当前源代码的行号或者函数名
比如
list 120
list add_item
不带location的list命令显示更多行数
list -则是在当前显示的第一行往前显示
disassemble
disassemble /m
disassemble命令可以查看汇编代码
当带上 /m选项的时候,则可以同时显示汇编和源代码
gdb断点功能非常丰富,如果你要非常频繁使用断点调试功能,gdb文档的Breakpoints这一章节值得花时间去看看。
break <location> 命令用来设置一个断点
<location>可以是 main.cpp:169 这种行号, 可以是main.cpp:add_item这种函数函数名。 break则可以简写为b。
info b可以用来列出当前的断点
delete breakpoints <ids> 可以用来清除一个或者多个断点
其中<ids>对应的是info b里面列出来的断点的id列表,如果有多个,用空格分开。我更经常使用的是 d <ids>这个简写命令。
比如
d 6 7
删除id为6和7的断点
也可以使用clear <location>命令来清除一个断点,这里的<location>和设置断点用的location是一个意思。
断点还可以暂时disable
disable <ids>
如果要恢复暂时disable的命令,则使用enable命令
enable <ids>
break <location> if <cond>
命令可以设置条件断点
示例:
图3-5 条件断点
广义的断点还包括watchpoints和catchpoints
watchpoints被称为数据断点,可以使用它来在表达式改变的时候停止执行,一般可以用来监控变量/内存值是否被读/写,从这个意义上,可以称之为读/写断点。
watch <expression>
rwatch <expression>
awatch <expression>
以上三个命令用来设置watchpoints,其中watch用来设置写断点,rwatch设置读断点,awatch设置读写断点。<expr>可以是变量,也可以是表达式。通常系统能够支持的watchpoint个数是有限的。
示例:
图3-6 watchpoints
可以使用Catchpoints来让debugger在某些程序事件,比如c++ exception,syscall,fork或者加载共享库的时候停下来。
catchpoints和watchpoints都依赖于target上系统的实现。
示例:
图3-7 catch-points
3.8 程序执行
当程序因为断点,停止执行的时候,使用如下命令来控制程序的执行。
r(un)
c(ontinue)
s(tep)
n(ext)
u(til)
finish
r命令用来执行程序,会从头开始。c命令则在当前断点下继续执行程序。s命令会跟踪到下一个指令,n命令则继续当前函数的下一个代码行。u命令让程序执行,直到指定的位置再停下来。finish指令则继续执行程序直到当前函数结束(也就是当前函数返回后,他的调用函数的下一行处停下来)。
3.8 多线程调试
"info threads" 命令可以列出当前运行的所有线程
"thread <id>" 可以切换线程
示例:
图3-8 多线程调试
设置断点的时候也可以指定线程
break <location> thread <id>
可以对所有的线程执行同一个命令
thread apply <thread-id-list | all> args
示例:
图3-9 多线程执行同一个命令
看到这里的同学,相信你一定有足够的耐心和悟性,现在需要的是拿起武器,在实战中积累你的经验!!!