大纲
- 概述
 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的,但是内容有点多,到时再单独开一章!