GDB的工作原理及SkyEye远程调试

2021-10-25 15:15:06 digiproto 2

01

GDB简介


GDB:GNU debugger 是UNIX及UNIX-like下强大的命令行调试工具。

图片关键词

▲GDB调试整体架构


可以发现GDB调试不管是本地调试还是远程调试,都是基于ptrace系统调用来实现的。


02

Ptrace简介


Ptrace系统调用的原型:


#include long ptrace(enum __ptrace_request request,pid_t pid, void *addr, void *data);



说明:Ptrace系统调用提供了一种方法来让父进程可以观察和控制其它进程的执行,检查和改变其核心映像以及寄存器。主要用来实现断点调试和系统调用跟踪。

(且先不去管内核中是怎么实现Ptrace的)。GDB就是通过调用这个系统调用,然后通过一些参数来控制其他进程的执行的。


下面我们来看Ptrace函数中Request参数的一些主要选项:


Request参数的主要选项:


PTRACE_TRACEME:由子进程调用,表示本进程将被其父进程跟踪,交付给这个进程的所有信号,即使信号是忽略处理的(除SIGKILL之外),都将使其停止,父进程将通过wait()获知这一情况。


这是什么意思呢?我们可以结合到GDB上来看。如果在GDB中run一个程序,首先GDB会创建一个子进程,然后该子进程调用Ptrace系统调用,参数就是PTRACE_TRACEME,然后调用一个exec执行程序。基本过程是这样,细节上可能会有出入。需要注意的是,这个选项PTRACE_TRACEME是由子进程调用的而不是父进程!


PTRACE_ATTACH:attach到一个指定的进程,使其成为当前进程跟踪的子进程,而子进程的行为等同于它进行了一次PTRACE_TRACEME操作。但是,需要注意的是,虽然当前进程成为被跟踪进程的父进程,但是子进程使用getppid()的到的仍将是其原始父进程的pid。


当你在GDB中使用attach命令来跟踪一个指定进程/线程的时候,GDB就自动成为该进程的父进程,而被跟踪的进程则使用了一次PTRACE_TRACEME,GDB也就顺理成章的接管了这个进程。


PTRACE_CONT:继续运行之前停止的子进程。可同时向子进程交付指定的信号。


需要注意的另一点是,使用GDB调试过多线程/进程的人应该都知道,当子进程遇到一个信号的时候,GDB就会截获这个信号,并将子进程暂停下来。这是为什么呢?


实际上,在使用参数为PTRACE_TRACEME或PTRACE_ATTACH的ptrace系统调用建立调试关系之后,交付给目标程序的任何信号(除 SIGKILL之外)都将被GDB先行截获,或在远程调试中被GDBserver截获并通知GDB。GDB因此有机会对信号进行相应处理,并根据信号的属性决定在继续目标程序运行时是否将之前截获的信号实际交付给目标程序。


03

GDB三种调试方式


3.1. attach并调试一个已经运行的进程

  • 确定需要进行调试的进程id

  • 运行GDB,输入attch pid,如:GDB 12345。GDB将对指定进行执行如下操作:ptrace (PTRACE_ATTACH,pid,0,0)


3.2. 运行并调试一个新的进程

  • 运行GDB,通过命令行参数或file指定目标调试程序如GDB ./test

  • 输入run命令,GDB执行下述操作:

通过fork()系统调用创建一个新进程

在新创建的子进程中调用ptrace(PTRACE_TRACEME,0,0,0)

在子进程中通过execv()系统调用加载用户指定的可执行文件。


3.3. 远程调试目标主机上新创建的进程

  • GDB运行在调试机,GDBserver运行在目标机,两者之间的通信数据格式由GDB远程串行协议(Remote Serial Proctocl)定义。

  • GDBsever的启动方式相当于运行并调试一个新创建的进程。


04

GDB调试的基础—信号


GDB调试的实现都是建立在信号的基础上的,在使用参数为PTRACE_TRACEME或PTRACE_ATTACH的ptrace系统调用建立调试关系后,交付给目标程序的任何信号首先都会被GDB截获。


因此GDB可以先行对信号进行相应处理,并根据信号的属性决定是否要将信号交付给目标程序。


单步调试

Next指令可以实现单步调试,即每次只执行一行语句。一行语句可能对应多条及其指令,当执行next指令时,分析其实现机制首先需要理解step range以及step_range_start和step_range_end的概念。执行next命令时,GDB会计算当前停止语句对应的第一条指令的地址作为step_range_start,然后计算当前停止位置下一行的c语句的第一条指令的地址作为step_range_end,随后控制目标程序,从当前停止位置开始走指令集单步,直至pc超出step range为止。


Next命令的结束条件:

pc=step_range_end

图片关键词

▲图 4.1 next命令实现示意图


之所以不能简单的判断pc是否达到step_range_end,是因为step_range_end仅仅是c源代码意义上的下一行第一条指令的地址,目标程序实际运行未必就会到达那里,因此,next命令的结束条件可以理解为只要pc离开当前源代码即可。


Next命令跨越函数调用的过程:

  • 从当前停止位置开始走指令级单步

  • 走到子函数第一条指令时发现是函数调用,就在函数返回地址插入一个临时断点

  • 让目标程序继续运行,通过子函数体,直至遇到之前插入的临时断点

  • 继续走指令集单步,直至满足next命令的结束条件为止

图片关键词

▲图 4.2 next跨越函数实现示意图

Step命令next命令完全一样,也是实现c源代码级的单步,对于简单的语句,step完全等同于next。唯一不同的是,若单步过程中遇到函数调用,step命令将停止在子函数的起始处,而不是将其跨越(无调试信息的子函数除外)。


Nexti命令实现指令集单步,和next命令类似,nexti命令单步过程中不会进入子函数调用。


Stepi命令实现指令集单步,而且是严格的指令集单步,每次直接走一条指令后立即停止,不在区分是否存在函数调用。


设置断点:

断点功能的实现就是在指定位置插入断点指令,使目标程序运行至该处时产生SIGTRAP信号,该信号被GDB捕获,通过断点地址的匹配确定是否命中断点。


断点命中的判断:目标程序遇到断点,并不一定就需要停下来,该停就停,不该停的还是要继续跑。只要真正需要停止运行的情况才认为是断点命中。是否命中断点的判断因素主要有以下这些:

  • 导致目标程序本次停止运行的信号是不是SIGTRAP

  • 在GDB维护的断点链表中是否存在一个断点的地址与目标程序本次停止位置匹配

  • 若断点存在条件,此时条件是否满足

  • 断点的忽略次数此时是否为0

  • 若断点只针对某个线程有效,那么遇到该断点的线程是否就是断点所设定的线程。

  • 若前两个条件之一不满足,则认为目标程序停止是因随机信号而停止。

  • 若后三个条件之一不满足,则认为目标程序本次没有命中断点,GDB会让其继续运行。


GDB将断点实际插入目标程序的时机:

当用户通过break命令设置一个断点时,这个断点不会立即生效,因为此时GDB只是在内部的断点链表中为这个断点新创建了一个节点而已。GDB会在用户下次继续发出目标程序运行的指令时,将所有断点插入目标程序中。与此相呼应,当目标程序停止时,GDB会将所有断点暂时从目标程序中清除。


断点命中失败的情况下,跨越断点继续运行的过程:

  • 清除断点

  • 单步到断点的下一条指令

  • 恢复断点

  • 继续目标程序运行


05

SkyEye支持GDB远程调试


5.1 SkyEye基本介绍

SkyEye是一款支持 ARM、TI DSP、PowerPC、X86、SPARC,龙芯,飞腾等多种处理器体系架构的指令级别仿真平台。


SkyEye便是通过3.3的方式进行远程调试。


图片关键词

▲图 4.3 next跨越函数实现示意图


同时提供了SkyEye 命令行工具和SkyEye界面两种形式进行调试。


5.2 启动SkyEye命令行对源代码进行调试

1. 使用SkyEye命令行工具与GDB以及工程建立连接

2. 在main函数前进行断点

3. 执行至断点处

4. 进行「step」单步操作

5. 进行「next」单步操作

6. 进行「continue」继续执行

图片关键词

▲图 5.1 命令行调试示意图


5.3 启动SkyEye界面对源代码进行调试

1. 执行单步跳入(step)

2. 进行断点(breakpoint)

3. 单步跳过(next)

4. 继续运行(continue)

图片关键词

▲图 5.2 SkyEye界面调试图


5.4 提供查看变量、寄存器等功能

图片关键词

▲图 5.3 SkyEye界面调试查看变量寄存器图


5.5 同时可以切换至指令集调试

图片关键词

▲图 5.4 SkyEye界面指令集调试图


5.6 具体信息展现在控制台处

图片关键词

▲图 5.5 SkyEye界面调试输出信息图


电话咨询
在线咨询
解决方案
QQ客服