ARMv8 架构的 BRK
指令是用于生成一个软件断点的。当处理器执行到 BRK
指令时,会触发一个断点异常。
BRK 指令的格式如下:
BRK #<imm>
其中
是一个16
位的立即数,它可以在断点异常发生时将立即数保存到 ESR.ISS
域中,从可以用来区分不同目的的 BRK
断点指令。
下面是一个简单的例子:
MOV R0, #1
BRK #0x1234
MOV R0, #2
在这个例子中,当处理器执行到BRK #0x1234
这条指令时,并且可以在ESR.ISS
中看到BRK #0x1234
这条指令的立即数0x1234
。
需要注意的是,BRK指令只能在ARMv8及之后的ARM架构中使用。在早期的ARM架构中,生成软件断点通常使用的是SWI或BKPT指令。
上节内容介绍了 BRK 后面跟的立即数会在断点中断发生时,保存到ESR.ISS
中,那么我们看下linux 中 BRK 后面的立即数宏定义种类有哪些并分别作用是什么?
ARM64中BRK 立即数的定义位于文件 linux/arch/arm64/include/asm/brk-imm.h
中:
/*
* #imm16 values used for BRK instruction generation
* 0x004: for installing kprobes
* 0x005: for installing uprobes
* 0x006: for kprobe software single-step
* Allowed values for kgdb are 0x400 - 0x7ff
* 0x100: for triggering a fault on purpose (reserved)
* 0x400: for dynamic BRK instruction
* 0x401: for compile time BRK instruction
* 0x800: kernel-mode BUG() and WARN() traps
* 0x9xx: tag-based KASAN trap (allowed values 0x900 - 0x9ff)
*/
#define KPROBES_BRK_IMM 0x004
#define UPROBES_BRK_IMM 0x005
#define KPROBES_BRK_SS_IMM 0x006
#define FAULT_BRK_IMM 0x100
#define KGDB_DYN_DBG_BRK_IMM 0x400
#define KGDB_COMPILED_DBG_BRK_IMM 0x401
#define BUG_BRK_IMM 0x800
#define KASAN_BRK_IMM 0x900
#define KASAN_BRK_MASK 0x0ff
KPROBES_BRK_IMM
:这是用于 Kprobes 的BRK指令的立即数值。Kprobes是Linux内核中的一个动态追踪工具,它允许你在运行时插入断点到内核代码中;
UPROBES_BRK_IMM
:这是用于Uprobes的BRK指令的立即数值。Uprobes是Linux内核中的一个动态追踪工具,它允许你在运行时插入断点到用户空间程序中;
KPROBES_BRK_SS_IMM
:这是用于Kprobes的单步执行模式的BRK指令的立即数值;
FAULT_BRK_IMM
:这是用于处理页故障的BRK指令的立即数值;
KGDB_DYN_DBG_BRK_IMM
:这是用于KGDB(内核调试器)的动态调试的BRK指令的立即数值;
BUG_BRK_IMM
:这是用于BUG_ON宏的BRK指令的立即数值;BUG_ON是Linux内核中的一个宏,用于在满足某个条件时生成一个故障;
KASAN_BRK_IMM
:这是用于KASAN(内核地址无效访问检测器)的BRK指令的立即数值。
断点异常属于同步异常,所以我们需要从同步异常开始,ARMv8 的同步异常处理函数位于汇编文件linux/arch/arm64/kernel/entry.S
中,定义如下:
/*
* EL1 mode handlers.
*/
.align 6
SYM_CODE_START_LOCAL_NOALIGN(el1_sync)
kernel_entry 1
mov x0, sp
bl el1_sync_handler
kernel_exit 1
SYM_CODE_END(el1_sync)
从上面汇编代码可以看到将栈指针的值SP
赋值给X0
,然后跳转到函数el1_sync_handler
中,接下来继续跟踪该函数。
el1_sync_handler
函数的定义位于linux/arch/arm64/kernel/entry-common.c
中:
asmlinkage void noinstr el1_sync_handler(struct pt_regs *regs)
{
unsigned long esr = read_sysreg(esr_el1);
switch (ESR_ELx_EC(esr)) {
case ESR_ELx_EC_DABT_CUR:
case ESR_ELx_EC_IABT_CUR:
el1_abort(regs, esr);
break;
/*
* We don't handle ESR_ELx_EC_SP_ALIGN, since we will have hit a
* recursive exception when trying to push the initial pt_regs.
*/
case ESR_ELx_EC_PC_ALIGN:
el1_pc(regs, esr);
break;
case ESR_ELx_EC_SYS64:
case ESR_ELx_EC_UNKNOWN:
el1_undef(regs);
break;
case ESR_ELx_EC_BREAKPT_CUR:
case ESR_ELx_EC_SOFTSTP_CUR:
case ESR_ELx_EC_WATCHPT_CUR:
case ESR_ELx_EC_BRK64:
el1_dbg(regs, esr);
break;
case ESR_ELx_EC_FPAC:
el1_fpac(regs, esr);
break;
default:
el1_inv(regs, esr);
}
}
首先读取异常状态寄存器 ESR_EL1
的 EC
域 判断当前异常类型,然后根据异常类型跳转到对应的处理函数,本篇文章组要介绍 ARMv8/ARMv9 debug 相关的内容,所先只关注 el1_dbg
这个异常处理函数。
当异常类型为 Breakpoint Instruction exceptions,Breakpoint exceptions,Watchpoint exceptions,Software Step exceptions 四种中的一种时就会跳转执行 el1_dbg
函数。
gcc 编译器在汇编过程中调用c语言函数时传递参数有两种方法:一种是通过堆栈,另一种是通过寄存器。缺省时采用寄存器,假如你要在你的汇编过程中调用 c 语言函数,并且想通过堆栈传递参数,你定义的 c 函数时要在函数前加上宏asmlinkage
详细内容可以见 DDI0487_I_a_a-profile_architecture_reference_manual.pdf 中的 D2章节。
上节内容说到 当检查到异常类型为 debug 异常类型时就会执行el1_dbg 函数,该函数的实现如下:
static void noinstr el1_dbg(struct pt_regs *regs, unsigned long esr)
{
unsigned long far = read_sysreg(far_el1);
arm64_enter_el1_dbg(regs);
do_debug_exception(far, esr, regs);
arm64_exit_el1_dbg(regs);
}
该函数首先读取 far_el1
寄存器中产生导致异常发生的虚拟地址,然后再将虚拟地址,esr_el1的值,SP栈地址作为参数传给了 do_debug_exception
函数:
834 void do_debug_exception(unsigned long addr_if_watchpoint, unsigned int esr,
835 struct pt_regs *regs)
836 {
837 const struct fault_info *inf = esr_to_debug_fault_info(esr);
838 unsigned long pc = instruction_pointer(regs);
839
842 ...
843 debug_exception_enter(regs);
...
848 if (inf->fn(addr_if_watchpoint, esr, regs)) {
849 arm64_notify_die(inf->name, regs,
850 inf->sig, inf->code, (void __user *)pc, esr);
851 }
...
854 }
855 NOKPROBE_SYMBOL(do_debug_exception);
这里我们主要关注 837 行和 848行,这两行的作用是根据 ESR.EC
阈值判断当前异常类型,然后调佣该异常类型的处理函数。例如 BRK 软件断点异常的处理函数就是 linux/arch/arm64/kernel/debug-monitors.c
中的函数 brk_handler
。那么 brk_handler
异常的处理函数是如何注册的?
linux 对于类型相似的问题,比如许多类型相似 debug 异常,处理套路都是先定义一个全局的结构体数组(如 struct fault_info fault_info[]
, struct fault_info debug_fault_info[]
),然后将异常的处理函数,异常类型,异常描述等信息填入结构体数组中:
struct fault_info {
int (*fn)(unsigned long addr, unsigned int esr,
struct pt_regs *regs);
int sig;
int code;
const char *name;
};
/*
* __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" },
};
在异常发生的时候只要需要索引值,就可以直接调用到对应的异常处理函数。对于数组debug_fault_info[]
索引值的获取是根据 ESR.EC
的值计算来的:
static inline const struct fault_info *esr_to_debug_fault_info(unsigned int esr)
{
return debug_fault_info + DBG_ESR_EVT(esr);
}
#define DBG_ESR_EVT(x) (((x) >> 27) & 0x7)
宏 DBG_ESR_EVT
中右移27位是因为ESR_EL1
的bit26
开始时EC
域:
debug_fault_info
表中的内容是默认的一些异常的处理函数,对于 debug 异常的处理函数注册还需要在代码中调用 linux/arch/arm64/mm/fault.c
中的注册函数hook_debug_fault_code
来完成:
void __init hook_debug_fault_code(int nr,
int (*fn)(unsigned long, unsigned int, struct pt_regs *),
int sig, int code, const char *name)
{
BUG_ON(nr < 0 || nr >= ARRAY_SIZE(debug_fault_info));
debug_fault_info[nr].fn = fn;
debug_fault_info[nr].sig = sig;
debug_fault_info[nr].code = code;
debug_fault_info[nr].name = name;
}
对于 BRK
和单步执行的异常处理函数的注册是在linux/arch/arm64/kernel/debug-monitors.c
中函数 debug_traps_init(void)
中完成的:
void __init debug_traps_init(void)
{
hook_debug_fault_code(DBG_ESR_EVT_HWSS, single_step_handler, SIGTRAP,
TRAP_TRACE, "single-step handler");
hook_debug_fault_code(DBG_ESR_EVT_BRK, brk_handler, SIGTRAP,
TRAP_BRKPT, "BRK handler");
}
对于 watchpoint 和 breakpoint 的异常处理函数的注册位于linux/arch/arm64/kernel/hw_breakpoint.c
中的arch_hw_breakpoint_init(void)
函数中:
/*
* One-time initialisation.
*/
static int __init arch_hw_breakpoint_init(void)
{
...
/* Register debug fault handlers. */
hook_debug_fault_code(DBG_ESR_EVT_HWBP, breakpoint_handler, SIGTRAP,
TRAP_HWBKPT, "hw-breakpoint handler");
hook_debug_fault_code(DBG_ESR_EVT_HWWP, watchpoint_handler, SIGTRAP,
TRAP_HWBKPT, "hw-watchpoint handler");
...
}
arch_initcall(arch_hw_breakpoint_init);
由于本篇文章主要介绍 BRK
指令异常,所以还需要继续跟踪器异常处理函数 brk_handler
:
326 static int brk_handler(unsigned long unused, unsigned int esr,
327 struct pt_regs *regs)
328 {
329 if (call_break_hook(regs, esr) == DBG_HOOK_HANDLED)
330 return 0;
331
332 if (user_mode(regs)) {
333 send_user_sigtrap(TRAP_BRKPT);
334 } else {
335 pr_warn("Unexpected kernel BRK exception at EL1\n");
336 return -EFAULT;
337 }
338
339 return 0;
340 }
341 NOKPROBE_SYMBOL(brk_handler);
这里我们只关注第329行,它的作用是遍历注册到链表 kernel_break_hook
上的所有node, 比较 node 节点上的的立即数 imm
是否和 异常症状寄存器 ESR.ISS
域中的值是否匹配, 如果匹配成功就会调用它的 handler。
static LIST_HEAD(kernel_break_hook);
static int call_break_hook(struct pt_regs *regs, unsigned int esr)
{
struct break_hook *hook;
struct list_head *list;
int (*fn)(struct pt_regs *regs, unsigned int esr) = NULL;
list = user_mode(regs) ? &user_break_hook : &kernel_break_hook;
/*
* Since brk exception disables interrupt, this function is
* entirely not preemptible, and we can use rcu list safely here.
*/
list_for_each_entry_rcu(hook, list, node) {
unsigned int comment = esr & ESR_ELx_BRK64_ISS_COMMENT_MASK;
if ((comment & ~hook->mask) == hook->imm) // 比较 BRK 后面的立即数
fn = hook->fn;
}
return fn ? fn(regs, esr) : DBG_HOOK_ERROR;
}
NOKPROBE_SYMBOL(call_break_hook);
上文提到了当 debug 异常发生后,会遍历 kernel_break_hook
上的所有 node,那么我们看下有哪些类型的事件注册到这个链表上呢?
register_kernel_break_hook(&kgdb_brkpt_hook);
register_kernel_break_hook(&kgdb_compiled_brkpt_hook);
register_kernel_break_hook(&kprobes_break_hook);
register_kernel_break_hook(&kprobes_break_ss_hook);
register_kernel_break_hook(&bug_break_hook);
register_kernel_break_hook(&fault_break_hook);
register_kernel_break_hook(&kasan_break_hook);
我们在看下这些 BRK事件对应的处理函数:
static struct break_hook kgdb_brkpt_hook = {
.fn = kgdb_brk_fn,
.imm = KGDB_DYN_DBG_BRK_IMM,
};
static struct break_hook kgdb_compiled_brkpt_hook = {
.fn = kgdb_compiled_brk_fn,
.imm = KGDB_COMPILED_DBG_BRK_IMM,
};
static struct break_hook kprobes_break_hook = {
.imm = KPROBES_BRK_IMM,
.fn = kprobe_breakpoint_handler,
};
static struct break_hook kprobes_break_ss_hook = {
.imm = KPROBES_BRK_SS_IMM,
.fn = kprobe_breakpoint_ss_handler,
};
static struct break_hook bug_break_hook = {
.fn = bug_handler,
.imm = BUG_BRK_IMM,
};
static struct break_hook fault_break_hook = {
.fn = reserved_fault_handler,
.imm = FAULT_BRK_IMM,
};
static struct break_hook kasan_break_hook = {
.fn = kasan_handler,
.imm = KASAN_BRK_IMM,
.mask = KASAN_BRK_MASK,
};
这里挑我们最常用到的处理函数 bug_handler
来介绍:
static int bug_handler(struct pt_regs *regs, unsigned int esr)
{
switch (report_bug(regs->pc, regs)) {
case BUG_TRAP_TYPE_BUG:
die("Oops - BUG", regs, 0);
break;
case BUG_TRAP_TYPE_WARN:
break;
default:
/* unknown/unrecognised bug trap type */
return DBG_HOOK_ERROR;
}
/* If thread survives, skip over the BUG instruction and continue: */
arm64_skip_faulting_instruction(regs, AARCH64_INSN_SIZE);
return DBG_HOOK_HANDLED;
}
这里看到了我们经常遇到的 “Oops - BUG” 了。
到目前为止介绍了整个 BRK 点断指令的处理流程与对应的异常处理函数注册流程,那么我们什么时候会用到 BRK 指令呢?
在 linux中最常用的地方也就是 WARN
和 BUG
这两个地方,这里以BUG
为例进行介绍:
#define BUG() do { \
__BUG_FLAGS(0); \
unreachable(); \
} while (0)
#define __BUG_FLAGS(flags) \
asm volatile (__stringify(ASM_BUG_FLAGS(flags)));
#define ASM_BUG() ASM_BUG_FLAGS(0)
#define ASM_BUG_FLAGS(flags) \
__BUG_ENTRY(flags) \
brk BUG_BRK_IMM