linux ptrace 图文详解(六) gdb单步调试
目录
一、gdb单步调试介绍
二、单步调试原理
三、MDSCR_EL1对单步调试的支持、及起作用时机
四、代码实现
五、总结
(代码:linux 6.3.1,架构:arm64)
One look is worth a thousand words. —— Tess Flanders
相关链接:
linux ptrace 图文详解(一)基础介绍
linux ptrace 图文详解(二) PTRACE_TRACEME 跟踪程序
linux ptrace 图文详解(三) PTRACE_ATTACH 跟踪程序
linux ptrace 图文详解(四) gdb设置软断点
linux ptrace 图文详解(五) gdb设置硬断点、观察点
一、gdb单步调试介绍
单步调试是一种调试技术,它允许开发者逐行执行程序代码,以便观察程序的执行流程和变量状态。这种调试方式对于理解程序逻辑、定位错误和验证程序行为至关重要。通过单步调试,开发者可以:
-
逐行执行代码,观察每一行代码的执行结果。
-
检查程序中变量的值和状态。
-
观察程序的控制流,包括函数调用和返回。
-
设置断点,以便在特定条件下暂停程序执行。
单步调试主要包括两种模式:逐行调试(step)和逐指令调试(stepi)。逐行调试以源代码行为单位,适合高层次的逻辑分析;逐指令调试以机器指令为单位,适用于底层分析,如汇编代码或驱动开发。GDB通过与操作系统提供的调试接口(如ptrace)协作,控制目标进程的执行,捕获其状态,并在每个步骤后返回控制权给开发者。
二、单步调试原理
gdb 单步调试 主要依赖硬件debug寄存器,以下是gdb单步调试的实现原理:
1)gdb通过ptrace(PTRACE_SINGLESTEP)系统调用陷入内核;
2)ptrace系统调用在内核中找到被调试任务的task_struct对象,并设置TIF_SINGLESTEP标志,代表该任务返回用户态执行时需要使能单步调试功能,并设置该任务内核task_struct对象的寄存器上下文中的spsr,置位DBG_SPSR_SS;
3)接着,将被调试程序添加到内核的ready list中,等待被调度运行,然后ptrace系统调用返回;
4)当内核调度器选中被调试程序调度运行时,会判断其是否置位TIF_SINGLESTEP标志,若置位了话,就将MDSCR_EL1寄存器的SS位置1,使能硬件的单步调试功能;
5)当被调试程序返回用户态执行完第一条指令后,硬件会主动触发一次同步异常并陷入内核;
6)在内核中的异常处理流程中,判断异常类型(EC)为ESR_ELx_EC_SOFTSTP_LOW,于是调用相应处理函数,最终给被调试任务发送一个SIGTRAP信号;
7)被调试任务从异常处理流程中返回后,在返回用户态前夕,检查发现有SIGTRAP信号,于是调用ptrace_signal,给父进程gdb发送信号,并唤醒gdb,最后将自己挂起。
三、MDSCR_EL1对单步调试的支持、及起作用时机
MDSCR(Monitor Debug System Control Register)中的第0位,是Software step control bit,用于single step。
在每次进入内核(kernel_entry)的时候、以及从内核返回用户态(ret_to_user)的时候,检查任务的flag是否有TIF_SINGLESTEP,然后调用disable_step_tsk、enable_step_tsk去清除、设置MDSCR_EL1寄存器中的DBG_MDSCR_SS位。(详见:DDI0487J_a_a-profile_architecture_reference_manual.pdf D2.12 Software Step exceptions)
可见,单步异常触发的时机是被调试任务在用户态执行完第一条指令后,立刻触发一个单步异常陷入内核。
四、代码实现
1、ptrace(PTRACE_SINGLESTEP) 内核实现
ptrace(PTRACESINGLESTEP)在内核中的实现,主要就是为被调试任务的task_struct对象中存储的寄存器上下文中的pstate置上DBG_SPSR_SS标志,代表当前任务需要单步调试,后续该任务被调度时,设置对应debug寄存器去使能单步调试功能。
ptrace_requestswitch (request)case PTRACE_SINGLESTEP:ptrace_resume(child, request, data = 0) {clear_task_syscall_work(child, SYSCALL_TRACE)if (is_singlestep(request)) {user_enable_single_step(task = child) {struct thread_info *ti = task_thread_info(task)if (!test_and_set_ti_thread_flag(ti, TIF_SINGLESTEP)) /* 1) Set TIF_SINGLESTEP to tracee. 若该位已设置则返回1, 否则返回0 */set_regs_spsr_ss(task_pt_regs(task)) {set_user_regs_spsr_ss(&(r)->user_regs)regs->pstate |= DBG_SPSR_SS /* 2) Set DBG_SPSR_SS into child's pstate */}}} else { /* ### if request is not PTRACE_SINGLESTEP, and clear flag */user_disable_single_stepclear_ti_thread_flag(task_thread_info(task), TIF_SINGLESTEP)}child->exit_code = datawake_up_state(child, __TASK_TRACED) { /* 3) Change child's state to TASK_RUNNING and enqueue it into rq */try_to_wake_up // if child->state equal to __TASK_TRACED, then wake up itttwu_queuettwu_do_activate {activate_task {enqueue_task(rq, p, flags)p->on_rq = TASK_ON_RQ_QUEUED}ttwu_do_wakeup {WRITE_ONCE(p->__state, TASK_RUNNING)}}}}
2、被调试程序在内核中被调度器选中运行时,针对单步调试的处理
在内核schedule过程中,会检查被调试任务是否标记了需要单步调试。若被标记了话,会设置相应的debug寄存器(MDSCR_EL1.SS位),随后返回用户态执行被调试程序。
entry_handler {kernel_entry \el, \regsize {save hardware contextldr x19, [tsk, #TSK_TI_FLAGS]disable_step_tsk x19, x20 // 4) 进入内核保存现场的时候, 若Task有TIF_SINGLESTEP位,A.K.A // 则将mdscr_el1的DBG_MDSCR_SS位置0{.macro disable_step_tsk, flgs, tmptbz \flgs, #TIF_SINGLESTEP, 9990fmrs \tmp, mdscr_el1bic \tmp, \tmp, #DBG_MDSCR_SSmsr mdscr_el1, \tmpisb // Synchronise with enable_dbg9990:.endm}}mov x0, spbl el\el\ht\()_\regsize\()_\label\()_handlerA.K.Ael0t_64_irq_handler {__el0_irq_handler_commonel0_interruptexit_to_user_mode {prepare_exit_to_user_modeif (unlikely(flags & _TIF_WORK_MASK)) {do_notify_resumeif (thread_flags & _TIF_NEED_RESCHED) {local_daif_restore(DAIF_PROCCTX_NOIRQ)schedule() { /* 5) schedule tracee */}}}}}.if \el == 0b ret_to_userA.K.ASYM_CODE_START_LOCAL(ret_to_user) {ldr x19, [tsk, #TSK_TI_FLAGS] // re-check for single-stepenable_step_tsk x19, x2A.K.A{.macro enable_step_tsk, flgs, tmptbz \flgs, #TIF_SINGLESTEP, 9990fmrs \tmp, mdscr_el1orr \tmp, \tmp, #DBG_MDSCR_SS /* 6) If task has TIF_SINGLESTEP, then set DBG_MDSCR_SS to MDSCR_EL1 */msr mdscr_el1, \tmp}kernel_exit 0 // It will restore general-purpose register}
}
3、单步调试异常触发后,内核中的处理
当被调试程序在用户态执行完第一条指令后,硬件就会主动触发同步异常陷入内核,异常类型为ESR_ELx_EC_SOFTSTP_LOW。在内核中的处理跟前几篇文章断点触发时的处理流程大体相似:将自身挂起,并发送信号给gdb。
entry_handler {kernel_entry \el, \regsize {}mov x0, spbl el\el\ht\()_\regsize\()_\label\()_handlerA.K.Ael0t_64_sync_handler {unsigned long esr = read_sysreg(esr_el1)switch (ESR_ELx_EC(esr)) // #define ESR_ELx_EC(esr) (((esr) & ESR_ELx_EC_MASK) >> ESR_ELx_EC_SHIFT) 获取ESR中的EC所对应的6位内容case ESR_ELx_EC_SOFTSTP_LOW: // #define ESR_ELx_EC_SOFTSTP_LOW (0x32, aka: 0b110010)el0_dbg {unsigned long far = read_sysreg(far_el1)enter_from_user_mode(regs)do_debug_exception(unsigned long addr_if_watchpoint = far,unsigned int esr = esr,struct pt_regs *regs = regs) {const struct fault_info *inf = esr_to_debug_fault_info(esr)return debug_fault_info + DBG_ESR_EVT(esr) // #define DBG_ESR_EVT(x) (((x) >> 27) & 0x7)inf->fn(addr_if_watchpoint, esr, regs)A.K.Asingle_step_handler {send_user_sigtrap(si_code = TRAP_TRACE) /* 7) Send SIG_TRAP with siginfo to tracee itself */arm64_force_sig_fault(SIGTRAP, si_code, instruction_pointer(regs), "User debug trap") {force_sig_fault(signo = SIGTRAP, code = TRAP_TRACE, (void __user *)far = instruction_pointer(regs))force_sig_fault_to_task(sig, code, addr, current) {struct kernel_siginfo infoinfo.si_signo = sig // SIGTRAPinfo.si_errno = 0info.si_code = code // TRAP_TRACEinfo.si_addr = addr // instruction_pointer(regs)force_sig_info_to_task(&info, t, HANDLER_CURRENT)}}if (!handler_found && user_mode(regs)) {user_rewind_single_step(current) /* 8) Re-enable single step for syscall restarting. */if (test_tsk_thread_flag(task, TIF_SINGLESTEP))set_regs_spsr_ss(task_pt_regs(task))set_user_regs_spsr_ss(&(r)->user_regs)regs->pstate |= DBG_SPSR_SS /* 9) Set DBG_SPSR_SS into pstate */}}}exit_to_user_mode(regs) {prepare_exit_to_user_modelocal_daif_maskdo_notify_resume {if (thread_flags & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))do_signal {get_signalptrace_signalptrace_stop(exit_code = signo, why = CLD_TRAPPED, 0, info) /* Stop tracee itself, and notify parent tracer */{current->last_siginfo = infocurrent->exit_code = exit_codedo_notify_parent_cldstop(current, true, why)info.si_signo = SIGCHLDinfo.si_code = why // A.K.A: CLD_TRAPPEDinfo.si_status = tsk->exit_code & 0x7f__group_send_sig_info(SIGCHLD, &info, parent)}}}}}}
}
五、总结
gdb的单步调试,主要依赖硬件debug寄存器(MDSCR_EL1),ptrace(PTRACE_SINGLESTEP) 的作用主要是给task置上需要单步调试的标志,后续被调试任务被调度运行时,会根据是否置位“单步调试”的位,去设置MDSCR_EL1中的SS位,使能单步调试。当程序返回用户态执行完第一条指令后,硬件会主动触发一个单步异常陷入内核。