【问题标题】:How does debug exceptions (ptrace single_step) work in arm64 on Linux kernel?调试异常(ptrace single_step)如何在 Linux 内核的 arm64 中工作?
【发布时间】:2022-01-06 22:28:21
【问题描述】:

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

此问题的 Linux 参考:5.15.5(2021 年 11 月最新稳定版)。

命名法:tracer(被跟踪的进程)和tracee(被跟踪的进程)。

通过静态分析 + ftracing linux 内核,我试图重建 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),在entry-common.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_faulting_instruction():后者更新了4字节的PC(被跟踪者)(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;
}

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

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

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

【问题讨论】:

    标签: linux linux-kernel arm arm64 ptrace


    【解决方案1】:

    您想查看信号传递代码的内部,最终位于kernel/signal.c。这是在do_debug_exception 中发起的,它调用arm64_notify_die。您可以将其追溯到force_sig_fault,然后我们将使用通用架构独立的信号代码。

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

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


    您的分析在第 4 步中偏离了方向。断点处理程序在启动时被初始化为 early_brk64,但正如 early_brk64 上的评论所暗示的,在启动期间,debug_traps_init 挂钩 single_step_handler 代替它。然后我们遍历send_user_sigtraparm64_force_sig_faultforce_sig_fault,这是通用架构无关的内核代码。

    请注意,所有这些都在被跟踪者的上下文中。

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

    【讨论】:

    • 非常有用,谢谢。如果可能的话,我还有几个与此答案相关的问题。首先,我完全迷失在链中:调用 PTRACE_SINGLESTEP -> 设置 SPSR.SS -> 为 tracee 生成异常 -> do_debug_exception() -> arm64_notify_die() -> 将 SIGTRAP 传递给被跟踪进程 -> ??? .其次,在步骤1中,异常是如何产生的?我通过设置 SPSR 的 SS 位未能生成异常。如果我这样做,它什么也不做。 3) 最后,如果early_brk 64 没有被调用,调用的是哪个函数? “做坏事”?谢谢!!
    • 还有 4),对不起,如果 early_brk64 没有被调用,单步 (PC += INSTRUCTION_SIZE) 会发生在哪里? :)
    • 我想所有的魔法都在 sigtrap 处理程序中,但我找不到它:(
    • @ParisHilton:至于(3),我认为single_step_handler 被调用而不是early_brk64。当我有更多时间时,我将不得不看看其他人。
    • 感谢 (3)。我已经验证过,我也这么认为。如果您对其他要点有方向,如果您有时间,我将不胜感激。
    猜你喜欢
    • 1970-01-01
    • 2014-07-18
    • 2022-08-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多