【Linux】GDB底层调试原理

一、GDB调试模型
GDB调试包括2个程序:gdb程序和被调试程序。根据这2个程序是否运行在同一台电脑中,可以把GDB的调试模型分为2种: 1. 本地调试 2. 远程调试

本地调试:调试程序和被调试程序运行在同一台电脑中。
远程调试:调试程序运行在一台电脑中,被调试程序运行在另一台电脑中。

与本地调试相比,远程调试中多了一个GdbServer程序,它和目标程序都是运行在目标机中,可能是一台x86电脑或者是一个ARM板子。GDB与GdbServer之间通过网络或者串口进行通讯,通过RSP协议,全称是: GDB Remote Serial Protocol(GDB远程通信协议)。这个协议都是字符串组成,有固定的开始字符(’$’)和结束字符(’#’),最后还有两个十六进制的ASCII字符作为校验和。

二、GDB调试的系统调用

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 1;
    int b = 2;
    int c = a + b;
    printf("c = %d \n", c);
    return 0;
}

编译命令:

gcc -g test.c -o test

我们对可执行程序 test 进行调试,输入命令:

gdb ./tes

输出如下:


        在执行gdb ./test的时候,操作系统首先会启动gdb进程,这个进程会调用系统函数fork()来创建一个子进程,这个子进程做两件事情:

1. 调用系统函数ptrace(PTRACE_TRACEME,[其他参数]);

2. 通过execc来加载、执行可执行程序test,那么test程序就在这个子进程中开始执行了。

ptrace函数原型是:

#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

        ptrace系统函数是Linux内核提供的一个用于进程跟踪的系统调用,通过它,一个进程(gdb)可以读写另外一个进程(test)的指令空间、数据空间、堆栈和寄存器的值。而且gdb进程接管了test进程的所有信号,也就是说系统向test进程发送的所有信号,都被gdb进程接收到,这样一来,test进程的执行就被gdb控制了,从而达到调试的目的。

        也就是说,如果没有gdb调试,操作系统与目标进程之间是直接交互的;如果使用gdb来调试程序,那么操作系统发送给目标进程的信号就会被gdb截获,gdb根据信号的属性来决定:在继续运行目标程序时是否把当前截获的信号转交给目标程序,如此一来,目标程序就在gdb发来的信号指挥下进行相应的动作。

        但以上被调试的程序test是从头开始执行的,想要用gdb来调试一个已经处于执行中的服务进程,便需要对ptrace这个系统函数修改入参。

三、GDB调试已经执行的服务进程
        ptrace系统函数的第一个参数是一个枚举类型的值,其中重要的是2个:PTRACE_TRACEME和PTRACE_ATTACH。

        上面子进程(test)在调用ptrace系统函数时使用的参数是PTRACE_TRACEME,该参数会把操作系统对子进程(test)发送的所有信号转发给父进程(gdb)。

        如果想对一个已经执行的进程B进行调试,那么就要在gdb这个父进程中调用ptrace(PTRACE_ATTACH,[其他参数]),此时,gdb进程会attach(绑定)到已经执行的进程B,gdb把进程B收养成为自己的子进程,而子进程B的行为等同于它进行了一次 PTRACE_TRACEME操作。此时gdb进程会发送SIGSTOP信号给子进程B,子进程B接收到SIGSTOP信号后,就会暂停执行进入TASK_STOPED状态,表示自己准备好被调试了。

        不论是调试一个新进程,还是调试一个已经处于执行中状态的服务进程,通过ptrace系统调用,最终的结果都是:gdb程序是父进程,被调试程序是子进程,子进程的所有信号都被父进程gdb来接管,并且父进程gdb可查看、修改子进程的内部信息,包括:堆栈、寄存器等。

        关于绑定,有几个限制:不予许自我绑定,不允许多次绑定到同一个进程,不允许绑定1号进程。

四、断点的原理
继续刚才的例子,看一下编译出来的反汇编代码是什么样的,编译指令:

gcc -S test.c; cat test.s

        注:gcc参数 -c 只激活预处理,编译,和汇编,也就是指把程序做成obj文件;-S 只激活预处理和编译,就是指把文件编译成为汇编代码。

        上面说到,在执行gdb ./test之后,gdb就会fork出一个子进程,这个子进程首先调用ptrace然后执test程序,这样就准备好调试环境了。

        在调试窗口输入设置断点指令“break 5”,此时gdb做2件事情: 1. 对源码(int a = 1;)所对应的第10行汇编代码存储到断点链表中。 2. 在第10行汇编代码中,插入中断指令INT3,也就是说:第10行汇编代码被替换为INT3。

        然后,在调试窗口继续输入执行指令“run”(一直执行,直到遇到断点就暂停),汇编代码中PC指针(一个内部指针,指向即将执行的那行代码)执行这行代码时,发现是INT3指令,于是操作系统就发送一个SIGTRAP信号给test进程。

        此刻,第10行汇编代码被执行过了,PC指针就指向第11行了。

        上面已经说过,操作系统发给test的任何信号,都被gdb接管了,也就是说gdb会首先接收到这SIGTRAP这个信号,gdb发现当前汇编代码执行的是第10行,于是到断点链表中查找,发现链表中存储了这行的代码,说明这行被设置了断点。于是gdb又做了2个操作: 1. 把汇编代码中的第10行"INT3"替换为断点链表中原来的代码。 2. 把 PC 指针回退一步,也即是设置为指向第10 行。

然后,gdb继续等待用户的调试指令。

        此刻,就相当于下一条执行的指令是汇编代码中的第10行,也就是源码中的第5行。从我们调试者角度看,就是被调试程序在第5行断点处暂停了下来,此时我们可以继续输入其他调试指令来debug,比如:查看变量值、查看堆栈信息、修改局部变量的值等等。

五、next的原理
        还是以刚才的源代码和汇编代码为例,假设此时程序停止在源码的第6行,即汇编代码的第11行。在调试窗口输入单步执行指令next,目的是执行一行代码,也就是把源码中第6行代码执行完,然后停止在第7行。gdb在接收到next执行时,会计算出第7行源码,应该对应到汇编代码的第14行,在该行前插入INT3指令。gdb就控制汇编代码中的PC指针一直执行,直到第13行执行结束,也就是PC指向第14行时,就停止下来,然后继续等待用户输入调试指令。


 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值