大纲
- 概述
gdb
的安装- 调试原理
- 示例
- 符号表的生成
概述
gdb
是什么之类的,这里就不说了,看这个文章的,多多少少都知道的,简而言之就是调试的!与之差不多的还有很多,eg:llvm
gdb
的安装
本文是在Msys2
的环境中进行的,所以这里说的是在Msys2
中如何安装gdb
;
使用pacman
安装
pacman -S gdb
安装完成后,直接敲入gdb
指令回车后可以看到类似以下的样子
调试原理
这里就以gdb
为例简述gdb
的底层调试原理。使用一下指令生成gdb
调试用的程序(debug
和release
文件的差异或在后续的文档中记录)
g++ -g hello.cpp -o hello
在g++
中的-g
是调试时使用的参数,通过-g
生成gdb
需要的调试文件,如果通过-g
生成的是汇编文件或者是机器码文件,则可以通过strip
去直接剔除debug信息直接生成release
版本的执行程序!后面会详细说明!
说完debug
程序的生成后,我们看看gdb
的一些基本原理!
GDB调试
包括2个程序:gdb程序
和被调试程序
。根据这2个程序是否运行在同一台电脑中,可以把GDB的调试
模型分为2种:
- 本地调试
- 远程调试
- 本地调试
gdb调试程序
和被调试程序
运行在同一台电脑中。
- 远程调试
调试程序运行在一台电脑中,被调试程序运行在另一台电脑中。
RSP协议,全称是: GDB Remote Serial Protocol(GDB远程通信协议)。 它们都是字符串,有固定的开始字符(‘$’)和结束字符(‘#’),最后还有两个十六进制的ASCII字符作为校验码
$: 开始符
s:单步指令
#:结束符
2个16进制ASCII符:校验码 ->(73)
gdb
常见的指令:
指令 | 说明 |
---|---|
run | 从头开始连续而非单步执行程序 |
next | 执行下一行语句 |
step | 执行下一行语句,如果有函数调用则进入函数 |
qiut | 退出 |
break n | 在n行设置断点 |
delete n | 删除n行的断点 |
enable n | 启动n行的断点 |
disable n | 禁用n行的断点 |
clear | 清理所有的断点 |
bt | 查看各级函数调用及参数 |
set args | 设置程序中变量的值 |
show args | 查看程序中变量的值 |
watch | 设置观察点 |
输出变量值 | |
continue | 执行到下一个断点 |
多线程指令 | 说明 |
---|---|
info threads | 显示当前可调试的所有线程 |
thread ID(1,2,3…) | 切换当前调试的线程为指定ID的线程 |
break thread_test.c:123 thread all | 在所有线程中相应的行上设置断点 |
thread apply ID1 ID2 command | 让一个或者多个线程执行GDB命令command |
thread apply all command | 让所有被调试线程执行GDB命令command |
set scheduler-locking 选项 command | 设置线程是以什么方式来执行命令 |
set scheduler-locking off | 不锁定任何线程,也就是所有线程都执行,这是默认值 |
set scheduler-locking on | 只有当前被调试程序会执行 |
set scheduler-locking on step | 在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行 |
以上仅仅是一部分,需要的可以自行google
完整的!不用担心记不住,知道有这些指令就行,用的时候找找就好,用多了就熟悉了!
示例
我们以一个简单的hello
程序为例:
#include <stdio.h>
int main(void) {
int num1 = 1;
int num2 = 2;
int sum = num1 + num2;
printf("sum = %d \n", sum);
return 0;
}
通过以下指令可以生成这个程序的debug
程序
g++ -g hello.cpp -o hello_debug
在正确的坏境中会生成一个hello_debug.exe
的文件,对可执行程序 hello_debug
进行调试,
启动gdb
输入命令:
gdb ./hello_debug
系统首先会启动gdb进程,这个进程会调用系统函数fork()
来创建一个子进程,这个子进程做两件事情: 1. 调用系统函数ptrace(PTRACE_TRACEME,[其他参数])
; 2. 通过execc
来加载、执行可执行程序hello_debug
,那么hello_debug
程序就在这个子进程中开始执行了。
函数ptrace:
#include <sys/ptrace.h> long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
ptrace
的说明在文章后面,这里只需要知道有这么一回事就可以了~
前文已经对hello_debug
进行了编译!展开其汇编码:
在执行了gdb ./hello_debug
后,hello_debug
其实是还没有启动的!
需要开始调试,还需要在gdb
下输入start
;
到了这里, 就能看到命令里提示的int num1 = 1;
这也说明调试程序已经执行到了该行(但是还没有执行, 仅仅只是执行到!)
通过打印,可以看到,num1
是0
并不是我们初始申明的1
;
p
(也是
断点和继续
- 下一步
next
指令可以使gdb
执行下一条指令(这里要注意的是,所谓的下一条指令,并不是汇编
级别的指令)
- 断点
回到start
指令的状态(可以通过重新启动gdb
)
刚start
的时候,gdb
是停留在line 4
的,即main()
之后的第一个行代码处;假设我们现在line 6
(即是int sum = num1 + num2;
处打上断点)
break 6
标记上断点后,使用c
(continue
)指令就能直接执行到断点处!
- 其他
gdb
的其他指令这里就不一一说了,可以自行探索!
符号表的生成
这里就补充一下符号表相关的部分说明!
首先前文已经说了debug
程序生成的时候使用了-g
生成;(前文的hello_debug
)。
系统提供了objcopy
供我们单独拷贝符号信息
,通过参数--only-keep-debug
使得copy的对象仅保留了debug
信息,即符号表。
objcopy --only-keep-debug hello_debug hello.debug
说到这里,不得不提一下release版本程序生成的另一种方式!
strip --strip-all hello_debug strip --strip-debug hello_debug
以上指令是剥离在
hello_debug
的信息,使得hello_debug
保留下来的执行程序和release
一样!(--strip-all
和--strip-debug
还是有区别的,这里就不细说明!)
按照前文的,在剥离符号表后,再次使用gdb hello_debug
(已经执行strip
后或者是release
),
得到的结果如下:
没有找到符号表,这样就导致我们需要debug
的时候就无法进行理想的断点了!这个时候,我们需要把已经剥离的符号表link
到程序中(将原本已经strip
的hello_debug
同为hello_release
):
objcopy --add-gnu-debuglink=./hello.debug hello_release
执行完成以上指令后,再次进行gdb,就能正确加载符号表了:
添加符号表还可以通过以下几种方式去绑定到程序中:
- 启动
gdb
时,参数-s
指定
gdb -e ./hello_release -s ./hello.debug
- 启动
gdb
后,通过set debug-file-directory
指定符号表目录
set debug-file-directory ./
- 启动
gdb
后,通过add-symbol-file
单独挂载符号表
add-symbol-file ./hello.debug
本来还想细说ptrace
的,但是内容有点多,到时再单独开一章!