gdb对于linux应用程序调试实现机制浅析

本文介绍了GDB在Linux环境下利用ptrace系统调用调试应用程序的实现机制。内容包括进程调度、信号处理、以及如何通过ptrace实现寄存器和内存的读写、单步执行和断点设置。调试器GDB作为高优先级进程,通过ptrace命令控制被调试进程进入调试模式,允许用户进行各种调试操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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获取控制权。断点异常也是如此。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值