1.1 Gdb对于linux应用程序调试实现机制
jtag调试方式直接通过jtag扫描链控制had模块来实现非侵入式调试。Jtag调试方式可以通过had向cpu的流水线中灌指令的方式控制cpu执行某条指令,以此实现cpu的寄存器、memory的读写;并且通过had控制cpu的debug mode于trace mode实现断点和单步。上述的功能即实现了jtag方式的基本的调试功能。相比于jtag调试方式,linux应用程序的调试则是通过linux内核提供的一套ptrace调试系统调用来实现上述的寄存器、memory的读写以及断点、单步等操作。那么下面介绍一下实现机制。
Ptrace系统调用相关知识就不在此介绍。
整个gdb对于linux应用程序都是基于ptrace系统调用实现的。所以,我们先了解一下linux内核对于系统调用的流程。如下图所示。
系统调用完,内核态返回到用户态之前会进行两次判断,分别是是否需要重新调度和是否有信号要处理,这是接下来讨论的关键所在。
进程调度:
Linux-2.6之后支持可抢占式任务调度。当一个进程正在执行时,如果出现另外一个优先级高的进程,当前进程就会被抢占。那么,下面问题来了,抢占的时机在哪呢?是不是任何时候都可以进行抢占呢?
被抢占有两种方式,一种是通过调用check_preempt_curr()函数将当前进程的need_sched(thread_info->flags的bit 2)置起来,在上述系统调用流程中,返回用户空间之前重新调度;还有一种是有些情况会直接调用schedule()函数重新进行调度。
内核其实也可以每次进入到内核态的时候都调用一次schedule(),重新计算一下下一个最适合执行的进程。但是这样开销太大,所以内核只在必要重新调度的时候才调用schedule()进行调度。
一般情况下,当一个新的进程被唤醒时会调用check_preempt_curr()函数判断是否发生抢占,如果新的被唤醒的进程的优先级高,那么就会将当前的进程的need_sched置位,表示需要重新调度。
重新调度发生的时机有:
(1) 内核态返回用户态;
(2) 中断服务程序返回内核进程上下文
(3) 内核中进程显示的调用schedule()
(4) 内核中有进程被阻塞或唤醒(其实也是显示地调用schedule(),以实现将进程移除、加入到调度队列)
上述四种情况中,单独介绍(2)中断处理。内核中有些代码的执行是不可抢占的,比如某些中断服务程序的执行,这时候需要加锁。进程的thread_info->preempt_count表征当前进程是不是加锁的。
信号处理:
当前进程在返回用户空间之前,需要进行对信号的处理。进程的thread_info->flags(第0 bit)表示该进程是否有信号待处理。进程的task_struct->pending表示当前进程有哪些待处理的信息,task_struct中的sig_handle表示该进程对每个信号的处理。用户可以通过调用signal()函数捕捉某信号,即改变某信号的处理方式,通过修改task_stuct的sig_handle。
介绍完毕上述linux内核的背景知识之后,下面回到gdb是如何使得linux应用程序的进程进入调试模式的。
这样被调试进程在执行完exec()系统调用之后,进程阻塞变成stopped状态,不会被调度,而debugger进程被唤醒,即debugger获取控制权,程序进入调试模式。这时候可以通过debugger输入命令控制被调试进程,比如寄存器的读写、内存的读写、单步、continue等。
这里介绍一下,debugger属于IO消耗类的进程,大部分时间属于阻塞状态,等待用户的输入,只有少部分进行cpu执行,所以优先级会比较高。
寄存器、内存的读写有相应的ptrace命令,实现也非常简单。Debugger执行ptrace系统调用,传入的参数是child进程。而child进程的寄存器都保存在该进程的内核栈中,通过传入的child进程可以访问到,从里面取出\写入数据再返回即可。
而对于continue的实现,也有PTRACE_CONTINUE,debugger进行ptrace系统调用,将child的thread_info->flags的第5位清零(TIF_SYSCALL_TRACE,不知道干嘛的)。然后wake_up_process(child)将被调试子进程唤醒。具体做法是将在唤醒子进程的过程中,会调用check_preempt_curr()检查当前进程是否被抢占,是滴,那么将debugger的进程的need_resched置位,并且将子进程唤醒,加入调度队列,然后debugger调用sys_wait()函数,将debugger进程的state由0(runnable)改成1(stopped)。这样就使得被调试子进程全速运行,debugger交出控制权。
而单步和continue的流程差不多,只是在wake_up_process()的之前,调用enable_singlestep(child),这个函数把child进程的内核栈中保存的psr的tm[1:0]设置成指令跟踪模式。这样当child进程被调度时,把上下文恢复时,cpu的psr的tm设置成指令跟踪模式,这样,child执行一条指令会产生一个单步异常。然后进入异常处理程序。这里又可以将child移出调度队列,将debugger唤醒。Debugger获取控制权。断点异常也是如此。