gdb调试原理及qemu中的gdbserver

Posted ccxikka

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gdb调试原理及qemu中的gdbserver相关的知识,希望对你有一定的参考价值。

(一)gdb调试原理

此部分转自:https://blog.csdn.net/u012658346/article/details/51159971     https://www.cnblogs.com/xsln/p/ptrace.html

gdb调试的原理是基于ptrace系统调用,ptrace()系统调用提供了一个方法,该方法使一个程序(追踪者)可以观察和控制另外一个程序(被追踪者)的执行,并检查和改变被追踪者的内存及寄存器。它主要用于实现断点调试和追踪系统调用。

当被追踪时,被追踪线程在接收信号时会被停止,即使那个信号是被忽略的也是如此(SIGKILL除外)。追踪程序会在一个调用waitpid(或者其他类wait系统调用)时收到通知,该调用会返回一个包含被追踪线程停止的原因的状态值。当被追踪线程停止时,追踪程序可以使用多种ptrace请求来检查和编辑被追踪线程。追踪程序可以让被追踪线程继续运行,有选择地忽略发过来的信号(甚至可以发送一个完全不同的信号给被追踪线程)

利用ptrace系统调用,可在被调试程序和gdb之间建立追踪关系。然后所有发送给被调试程序(被追踪线程)的信号(除SIGKILL)都会被gdb截获,gdb根据截获的信号,查看被调试程序相应的内存地址,并控制被调试的程序继续运行。

 

ptrace系统调用原型:

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

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

PTRACE_ATTACH: attach到一个指定的进程,使其成为当前进程跟踪的子进程,而子进程的行为等同于它进行了一次PTRACE_TRACEME操作。但是,需要注意的是,虽然当前进程成为被跟踪进程的父进程,但是子进程使用getppid()的到的仍将是其原始父进程的pid。当你在gdb中使用attach命令来跟踪一个指定进程/线程的时候,gdb就自动成为改进程的父进程,而被跟踪的进程则使用了一次PTRACE_TRACEME,gdb也就顺理成章的接管了这个进程。

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

 

gdb三种调试方式:
1)attach并调试一个已经运行的进程:
确定需要进行调试的进程id,运行gdb,输入attch pid,如:gdb 12345。gdb将对指定进行执行如下操作:ptrace(PTRACE_ATTACH,pid,0,0), 建立自己与进程号为pid的进程间的跟踪关系。即利用PTRACE_ATTACH,使自己变成被调试程序的父进程。用attach建立起来的跟踪关系,可以调用ptrace(PTRACE_DETACH,pid,...)来解除。注意attach进程时的权限问题,如一个非root权限的进程是不能attach到一个root进程上的 。
2)运行并调试一个新的进程,利用fork+execve执行被测试的程序,子进程在执行execve之前调用ptrace(PTRACE_TRACEME),建立了与父进程(debugger)的跟踪关系:
运行gdb,通过命令行参数或file指定目标调试程序,如gdb ./test 
输入run命令,gdb执行下述操作:
通过fork()系统调用创建一个新进程
在新创建的子进程中调用ptrace(PTRACE_TRACEME,0,0,0)
在子进程中通过execv()系统调用加载用户指定的可执行文件
3)远程调试目标主机上新创建的进程
gdb运行在调试机,gdbserver运行在目标机,通过二者之间定义的数据格式进行通信

gdb调试基础--信号

gdb调试的实现都是建立在信号的基础上的,在使用参数为PTRACE_TRACEME或PTRACE_ATTACH的ptrace系统调用建立调试关系后,交付给目标程序的任何信号首先都会被gdb截获。 因此gdb可以先行对信号进行相应处理,并根据信号的属性决定是否要将信号交付给目标程序。 

.断点原理:

  1)    断点的实现原理,就是在指定的位置插入断点指令,当被调试的程序运行到断点的时候,产生SIGTRAP信号。该信号被gdb捕获并进行断点命中判定,当gdb判断出这次SIGTRAP是断点命中之后就会转入等待用户输入进行下一步处理,否则继续。 

  2)    断点的设置原理: 在程序中设置断点,就是先将该位置的原来的指令保存,然后向该位置写入int 3。当执行到int 3的时候,发生软中断,内核会给子进程发出SIGTRAP信号,当然这个信号会被转发给父进程。然后用保存的指令替换int3,等待恢复运行。

  3)    断点命中判定:gdb把所有的断点位置都存放在一个链表中,命中判定即把被调试程序当前停止的位置和链表中的断点位置进行比较,看是断点产生的信号,还是无关信号。

  4)    条件断点的判定:原理同3),只是恢复断点处的指令后,再多加一步条件判断。若表达式为真,则触发断点。由于需要判断一次,因此加入条件断点后,不管有没有触发到条件断点,都会影响性能。在x86平台,某些硬件支持硬件断点,在条件断点处不插入int    3,而是插入一个其他指令,当程序走到这个地址的时候,不发出int 3信号,而是先去比较一下特定寄存器和某个地址的内容,再决定是否发送int 3。因此,当你的断点的位置会被程序频繁地“路过”时,尽量使用硬件断点,会对提高性能有帮助

单步跟踪:

next指令可以实现单步调试,即每次只执行一行语句。一行语句可能对应多条及其指令,当执行next指令时,gdb会计算下一条语句对应的第一条指令的地址,然后控制目标程序走到该位置停止。 

gdb调试基本命令

 

(二)qemu中的gdbserver

正常情况下进行远程调试需要被调试端安装有gdbserver程序,而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接收的命令。接受方在收到数据包之后,对数据包进行校验,若正确回应“+”,反之回应“-”。

RSP 协议中定义的主要命令可以分为 3 类:

(1)寄存器/内存读写命令

命令 g: 读所有寄存器的值

命令 G:写所有寄存器的值

命令 P: 写某个寄存器

命令 m: 读某个内存单元

命令 M:写某个内存单元

(2)程序控制命令

命令?: 报告上一次的信号

命令 s: 单步执行

命令 c: 继续执行

命令 k: 终止程序

(3)其它命令

命令 O:控制台输出(Console Output )

命令 E:出错回应(Error response)

当主机使用gdb调试时,gdb和qemu中内置的gdbserver就使用上述模型进行交互,从而对qemu虚拟机进行调试。如gdb调试端发送x/ <n/f/u> <addr>表示读取addr处的内容,命令经RSP协议封装成数据包发送至qemu的gdbserver端,gdbserver收到数据包后对其进行校验,校验成功后进行解析处理并返回至gdb客户端。

开启gdbserver之后,会等待来自gdb的连接请求,默认端口为1234,gdb使用ip和端口与gdbserver连接:

连接建立后会调用gdb_handlesig()等待stdin传来的gdb指令,调用gdb_read_byte()解析用户的输入,并对数据包进行校验,若校验正确调用gdb_handle_packet()进行gdb命令的处理。

如下解析字符为m时,表示读取某个内存单元,则调用函数target_memory_rw_debug()进行内存单元的读取,该函数最后调用cpu_memory_rw_debug()读取内存内容

当解析字符为g时,使用gdb_read_register读取寄存器信息,该函数会调用特定CPU类型的回调函数:

x86下调用如下函数,通过qemu为虚拟机维护的CPUX86State结构体得到虚拟机的寄存器信息:

类似地,插入一个断点时:

在kvm_enabled的情况下,调用kvm_insert_breakpoint:

该函数进行断点的插入并最终使用kvm_update_guest_debug向kvm更新客户机的debug状态,该函数调用kvm_invoke_set_guest_debug,进一步调用kvm_vcpu_ioctl(cpu, KVM_SET_GUEST_DEBUG,&dbg_data->dbg)执行ioctl至kvm中设置相关异常向量,BP(breakpoint,int3),DB(int 1)(插一句,可通过设置异常位图中的这两个位对上述指令进行拦截)

以上是关于gdb调试原理及qemu中的gdbserver的主要内容,如果未能解决你的问题,请参考以下文章

GDB调试qemu源码纪录

利用QEMU+GDB搭建Linux内核调试环境

开启内核地址随机化KASLR后, qemu 调试 kernel 不能设置断点

qemu+gdb+vscode 的一些神奇调试 tricks

gdb 远程qemu-arm调试

GDB+Qemu调试Linux代码