调试异常(ptrace single_step)在Linux内核的arm64中是如何工作的



在Linux内核aarch64中调用ptrace SINGLESTEP时会发生什么?

这个问题的Linux参考:5.15.5(最新稳定版本于2021年11月(。

术语:示踪剂(追踪的过程(和示踪物(追踪过程(。

通过对linux内核进行静态分析+ftracing,我试图重建ptrace SINGLESTEP调用时发生的确切情况。我试图在学习操作系统中写出我所理解的内容,但未能获得同样的行为。在问我的问题之前,让我总结一下我试图重建的过程:

  1. 在debug_monitor.c中启用单步执行:
/* ptrace API */
void user_enable_single_step(struct task_struct *task)
{
struct thread_info *ti = task_thread_info(task);
if (!test_and_set_ti_thread_flag(ti, TIF_SINGLESTEP))
set_regs_spsr_ss(task_pt_regs(task));
}
  1. 在arm64中,这包括设置单步位SPSR_EL1.SS(位置21(:
/*
* Single step API and exception handling.
*/
static void set_user_regs_spsr_ss(struct user_pt_regs *regs)
{
regs->pstate |= DBG_SPSR_SS;
}
  1. 我想,这应该会引发一个";调试异常";(用户级别,EL0(,在公共条目中捕获和处理。c:
static void noinstr el0_dbg(struct pt_regs *regs, unsigned long esr)
{
/* Only watchpoints write FAR_EL1, otherwise its UNKNOWN */
unsigned long far = read_sysreg(far_el1);
enter_from_user_mode(regs);
do_debug_exception(far, esr, regs);
local_daif_restore(DAIF_PROCCTX);
exit_to_user_mode(regs);
}
  1. do_debug_exception()在fault.c中定义,由于它是一个软件步骤,因此它应该调用debug_fault_info数据结构的函数early_brk64
/*
* __refdata because early_brk64 is __init, but the reference to it is
* clobbered at arch_initcall time.
* See traps.c and debug-monitors.c:debug_traps_init().
*/
static struct fault_info __refdata debug_fault_info[] = {
{ do_bad,   SIGTRAP,    TRAP_HWBKPT,    "hardware breakpoint"   },
{ do_bad,   SIGTRAP,    TRAP_HWBKPT,    "hardware single-step"  },
{ do_bad,   SIGTRAP,    TRAP_HWBKPT,    "hardware watchpoint"   },
{ do_bad,   SIGKILL,    SI_KERNEL,  "unknown 3"     },
{ do_bad,   SIGTRAP,    TRAP_BRKPT, "aarch32 BKPT"      },
{ do_bad,   SIGKILL,    SI_KERNEL,  "aarch32 vector catch"  },
{ early_brk64,  SIGTRAP,    TRAP_BRKPT, "aarch64 BRK"       },
{ do_bad,   SIGKILL,    SI_KERNEL,  "unknown 7"     },
};
  1. 后者在traps.c中定义,它调用bug_handler函数,该函数又调用arm64_skip_failing_instruction((:后者更新4字节的PC(tracee的((aarch64指令在32位上(:
void arm64_skip_faulting_instruction(struct pt_regs *regs, unsigned long size)
{
regs->pc += size;
/*
* If we were single stepping, we want to get the step exception after
* we return from the trap.
*/
if (user_mode(regs))
user_fastforward_single_step(current);
if (compat_user_mode(regs))
advance_itstate(regs);
else
regs->pstate &= ~PSR_BTYPE_MASK;
}
  1. 最后,调用user_fastforward_single_step,它依次调用clear_user_regs_spsr_ss,它只重置之前设置的ss位:
static void clear_user_regs_spsr_ss(struct user_pt_regs *regs)
{
regs->pstate &= ~DBG_SPSR_SS;
}

如果这个调用链是正确的,我就无法理解上下文切换(从tracer到tracee(在哪里以及如何发生。事实上,从这些调用中,第2-6点似乎与示踪剂有关。我没有注意到这个链中的上下文切换,但它应该注意到。

我尝试在学习操作系统中复制所有这些步骤。为了清楚起见,我在设置SPSR.SS位(点2(后未能生成异常,但我通过在MDSCR_EL1寄存器上设置SS位、MDE位和KDE位来强制生成硬件调试异常。

一旦我完成了这个技巧,我的步骤就得到了验证,但事实上,异常是由跟踪器捕获的(而不是由tracee捕获的(:跟踪器的PC被更新,等等。我认为我通过代码和ftrace的静态分析检索到的步骤并不完全正确。你能帮忙确定在哪里吗?

您希望查看信号传递代码内部,最终查看kernel/signal.c。这在do_debug_exception中启动,并调用arm64_notify_die。您可以将其追溯到force_sig_fault,然后我们将使用通用的独立于体系结构的信号代码。

断点异常导致SIGTRAP被传递到被跟踪的进程,由于它正在被跟踪,每个信号都会导致跟踪e停止,就像它被发送到SIGSTOP一样。此时会通知跟踪器,就像在子进程退出或停止时通知进程一样:如果它在waitpid上被阻止,那么waitpid现在将返回;如果不是,则对CCD_ 12的下一个调用将立即返回。跟踪器可以进行进一步的ptrace调用来检查或更改tracee的状态,并在准备好让tracee执行另一个步骤时调用ptrace(PTRACE_CONT)(类似于SIGCONT(。它将设置适当的标志,使tracee忽略SIGTRAP,否则通常会终止它

因此,这与父级在waitpid(pid, &status, WUNTRACED)中并且其子级接收到SIGSTOPSIGTSTP(例如,终端中的Ctrl-Z命中(的流程大致相同;相关的内核代码并不是专门用于调试的。特别是,没有从tracee直接切换到tracer的显式上下文。相反,tracee将停止,跟踪器将准备运行。然后CPU进入调度程序。跟踪器可能是下一个选择运行的进程,无论是运气好还是其他进程都在休眠;否则,它必须像其他人一样等待一段时间。示踪剂甚至可能在不同的岩心上运行,这也很好。


您的分析在步骤4中偏离了方向。断点处理程序在启动时初始化为early_brk64,但正如early_brk64上的注释所示,在启动期间,debug_traps_initsingle_step_handler挂起。然后我们通过send_user_sigtraparm64_force_sig_faultforce_sig_fault,这是在通用体系结构独立的内核代码中。

请注意,所有这些都在tracee的上下文中。

特别地,在此过程中不调用bug_handler()。该函数旨在处理内核错误,可能是通过OOPS杀死进程或使内核恐慌。early_brk64无条件地调用它,我认为这是因为它只在早期启动时作为处理程序安装,在任何用户空间进程存在之前,内核在任何情况下都不应该接受调试异常。

相关内容

最新更新