gdb是一个调试工具,一般来说GDB主要帮忙你完成下面的功能:
1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
3、当程序被停住时,可以检查此时你的程序中所发生的事。
4、动态的改变你程序的执行环境。
5、对程序崩溃产生的coredump进行检查
但是使用它有个基本前提,你需要阅读懂代码,gdb只是工具来辅助你检查程序内部。gdb运行依赖一些基础设施: linux ptrace断住,获取,调试符号表。
gdb调试时编译
gdb很多功能依赖debug信息,例如根据函数名设置断点,backtrace查看栈桢,info args等功能,所以在编译阶段就需要为gdb准备一些信息。
编译源代码时防止优化:-O0 -g
,其中 -O0 可以防止编译器优化,特别时对于局部变量或者只读变量, -g 要求生成的文件带有调试信息。
断点
查询断点信息:info b
设置断点
b *0x123456 // b *address
b main //b func
b test.c:100 //b filename:line
取消断点
disable 1 //取消断点,不会触发断点
del 1 //删除断点信息
断点触发时自动执行命令
commands 断点 断点 //选择断点列表,可以是多个
。。。 //执行命令,当为空时表示删除断点触发时执行的命令。
end
示例:在打印函数位置处入口地址处设置断点,打印字符串的内容
C++断点
因为在一个进程中除了函数名来区分还有命名空间这一层,需要将这一层加上
b 命名空间::函数名
怎么查看命名空间:
frameworks/base/libs/hwui/renderthread/RenderThread.cpp
namespace android {
using namespace uirenderer::renderthread;
namespace uirenderer {
namespace renderthread {
整个层次下来可能如下:
命名空间::类::方法
修改内容
修改寄存器内容
(gdb) set $pc=0x123456
修改某地址内存内容
GDB比C允许更多的隐式转换。你可以将整数值自由地存储到指针变量中,反之亦然,并且可以将任何结构转换为长度相同或更短的任何其他结构。要将值存储到内存中的任意位置,可以使用’{…}'结构,以在指定的地址生成指定类型的值。
(gdb) set {int}0x88888888 = 0x123456
修改变量内容
(gdb) whatis variable
type = int
(gdb) set var variable=0x123456
符号加载
有些场景需要我们手动加载符号表,远程调试时gdb server和gdb client不在同一个进程中或者相同的机器上,程序在运行时加载到随机地址。加载符号表有两种方式: file 和 add-symbol-file
file /path/to/program
:读取程序和符号表到内存中,它本身可以选择被调试的程序,附带的将符号表加载到gdb中,但是它并不能指定地址。
add-symbol-file /path/to/symbol-file start-addr
:将符号表加载到某个位置
远程调试
有些场景下需要进行远程调试,目标机上不具备gdb运行的条件和环境,例如嵌入式设备上内存,存储有限,需要远程调试,最典型的是 android 系统和应用调试,整个android的源码加上编译出来的中间文件有上百G,一般的手机上满足不了调试的需求。还有下面的调试 qemu 中的虚拟机也是通过远程调试。
服务端
服务端通常是通过 gdbserver 直接控制目标程序的运行并监听来自 gdb client 的请求,一般是通过网络端口来监听,还可以通过串口设备的直连进行通信
gdbserver64 :6000 ./a.out //从程序的启动开始调试,并监听本机6000端口
gdbserver64 :6000 --attach $(pid) //附着到已经运行的程序上,并监听本机6000端口
客户端
客户端通过需要首先连接到 gdbserver, 之后还可能需要做一些其他的设置之后才能正常调试
(gdb) target remote :6000
(gdb) file 带符号表的文件 //加载符号表
(gdb) set solib-search-path $KERNEL_OUTPUT //动态库的根路径
(gdb) set substitute-path /home/linux/kernel /root/mykernel //设置源文件的路径,在 /home/linux/kernel 中编译,但是现在位于/root/mykernel
//加载该目录下的所有文件
(gdb) info sharedlibrary //用 info sharedlibrary来查看目标库文件的符号表是否加载成功
对于android上的远程调试还需要一些特殊设置,adnroid设备和 host 一般是通过 adb 连接的,所以在 gdb client 连接 gdb server时还需要做一些特殊设置:
1. 通过 adb ip 方式连接 android 设备:adb connect $(ip)
2. 在 android 上开启 gdb server : gdbserver64 :6000 --attach $(pid)
3. host机器上通过adb进行端口转发设置:adb forward tcp:6000 tcp:6000 ,监听 host 上6000端口的请求并转发给 android 上 6000 端口
4. gdb client 连接 gdb server: (gdb) target remote :6000
gdb dump 进程内存
获取目标进程的虚拟地址映射
$ cat /proc/[pid]/maps
root@x86_64: # cat /proc/2436/maps |grep vdso
7fffd91f3000-7fffd91f5000 r-xp 00000000 00:00 0 [vdso]
dump内存地址
#### commands: dump memory file start-addr end-addr
(gdb) dump memory /home/linux/dump 0x7fffd91f3000 0x7fffd91f5000
查看二进制文件
使用vi打开二进制文件分析
vi dump
:%!xxd
使用hexdump查看二进制文件
hexdump-C dump
gdb调试qemu虚拟机
有两种使用方式,一种使用gdb调试qemu本身,另外一种是调试qemu加载的虚机内容。调试qemu本身时就和调试普通应用是同样的原理,通过ptrace附加到ptracee进程上,只不过qemu的参数通常比较多。而调试qemu运行的虚拟机的原理就比较复杂
调试qemu
gdb ./qemu-system-mips64el
(gdb) set args -M xxx -cpu ...
(gdb) b main
(gdb) run
而调试qemu中运行的虚拟机则更加复杂,qemu中内置了gdbserver模块,基于此可使用gdb实现对qemu虚拟机的远程调试,GDB/GDBSERVER调试模型的原理如下:
在GDB/GDBSERVER调试模型中,GDBSERVER是一个轻量级的GDB调试器,在调试过程中担任着调试代理的角色。在调试过程中,主机和目标机之间使用串口或者网络作为通信的通道。在主机上GDB通过这条通道使用一种基于ASCII的简单通讯协议RSP与在目标机上运行的GDBSERVER进行通讯。GDB发送指令,如内存、寄存器读写,GDBSERVER则首先与运行被调试程序映像的进程进行绑定,然后等待GDB发来的数据,对包含命令的数据包进行解析之后便进行相关处理,然后将结果返回给主机上的GDB。
RSP协议将GDB/GDBSERVER间通讯的内容更看做是数据包,数据包的内容都使用ASCII字符。每一个数据包都遵循这样的格式:$ <调试信息>#<校验码>
.
如上图所示,包的内容会以16进制的形式来编码(enhex),#后面的两位数字是校验码,具体的计算方式是数据包中所有字符求和再用256求余数。而数据包的内容,也就是RSP协议的载体,将会是gdb接收的命令。接受方在收到数据包之后,对数据包进行校验,若正确回应“+”,反之回应“-”。
当主机使用gdb对运行虚拟机的qemu进程进行调试,qemu中内置的gdbserver与虚拟机进行绑定,开启socket进行监听client连接,默认是tcp:1234端口,可以使用--gdb tcp:4567
选择任意端口。当client发送请求时,gdbserver收到包进行校验之后gdb_handle_packet进行分()发处理。如gdb调试端发送x/ <n/f/u> <addr>
表示读取addr处的内容,命令经RSP协议封装成数据包发送至qemu的gdbserver端,gdbserver收到数据包后对其进行校验,校验成功后进行解析处理并返回至gdb客户端。
以断点调试为例进行说明,gdb客户端对某个虚拟机的虚拟地址进行断点,qemu中会进行GVA->GPA->HVA地址的转换,并进行指令替换成int 3
。当执行到该位置时,触发debug异常陷入到kvm中,kvm检查到该进行进行VM_EXIT并向qemu发送该消息,qemu gdbserver接受到该信息之后转发给client,VM就在该状态下暂停。当client再次发送continue
信息给gdbserver,接到该信息之后将原始指令恢复,VM_ENTER并单步执行,在单步执行后又会触发debug异常,再次VM_EXIT到qemu的gdbserver。之后再次进行指令替换并恢复运行直至下一个debug异常。
gdbinit
gdb同样支持配置文件,在启动时自动执行配置文件中的命令,有以下几个地方可以控制/etc/gdbinit, ~/.gdbinit, ./.gdbinit
。可以通过添加gdb -nx
参数来禁止执行配置文件的命令。
通过配置文件对于重复性的调试比较友好,减少很多的重复设置的工作量。
target remote:1234
b main
commands 1
printf "%x",$pc
end
新版的gdb从安全角度进行了一些限制,默认./.gdbinit
不会加载,如果需要禁止这个安全功能,可以修改~/.gdbinit,添加set auto-load safe-path /
。如果只是想加载某个目录下的.gdbinit,可以在~/.gdbinit下添加add-auto-load-safe-path /home/linux/project/Demo/linux/gdb/.gdbinit
。
反汇编代码
gdb vmlinux
(gdb) list *(kvm_pgd_init+0x98) //查看偏移地址,带行号
(gdb) dis main //查看汇编代码
信息屏蔽
信号屏蔽
Thread 4 "CPU 0/KVM" received signal SIGUSR1, User defined signal 1.
(gdb) handle SIGUSR1 nostop
(gdb) handle SIGUSR1 noprint
线程创建退出
[New Thread 0xfff3bbc4e0 (LWP 8437)]
[Thread 0xfdb3ffeb60 (LWP 8412) exited]
(gdb) set print thread-events off
推荐阅读
gdb在线文档:https://sourceware.org/gdb/current/onlinedocs/gdb/