Android arm64 在 ptrace 中使用硬件断点
发表于|更新于
|阅读量:
Android arm64 在 ptrace 中使用硬件断点
ARM64内核研究(五) | Ylarod’s Blog
gdb 系列(1) (hwbreakpoint\watchpoint)
正文
为了跟踪相应地址发生的事件(读、写、执行)是哪一条指令造成的,我们可以使用硬件断点来达成这一目标。
准备条件
需要内核启用 CONFIG_HAVE_HW_BREAKPOINT
和 CONFIG_HAVE_ARCH_TRACEHOOK
gki 上应该是默认打开的
https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/arch/arm64/Kconfig;l=217;drc=661dc19066ef0fdcb2db3e2542c45744a4067e87
注册断点
在 arm64 上,使用 PTRACE_GET/SETREGSET
调用来设置断点。内核的处理逻辑如下:
common/arch/arm64/kernel/ptrace.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #ifdef CONFIG_HAVE_HW_BREAKPOINT [REGSET_HW_BREAK] = { .core_note_type = NT_ARM_HW_BREAK, .n = sizeof(struct user_hwdebug_state) / sizeof(u32), .size = sizeof(u32), .align = sizeof(u32), .regset_get = hw_break_get, .set = hw_break_set, }, [REGSET_HW_WATCH] = { .core_note_type = NT_ARM_HW_WATCH, .n = sizeof(struct user_hwdebug_state) / sizeof(u32), .size = sizeof(u32), .align = sizeof(u32), .regset_get = hw_break_get, .set = hw_break_set, }, #endif
|
有两种断点类型:breakpoint 和 watchpoint 。breakpoint 就是断点,用于当程序运行到指定地址时发生中断。watchpoint 是观察点,当程序对地址进行读(LOAD)或写(STORE)的时候发生中断。
breakpoint 和 watchpoint 分别对应于 NT_ARM_HW_BREAK
和 NT_ARM_HW_WATCH
。可以在 elf.h 找到这些常量。
NT_ARM_HW_xxx
这些应该写入 ptrace 的 addr 参数。data 参数则是一个指向 struct user_hwdebug_state
的 iovec 。
附 ptrace 定义:
1 2
| long ptrace(enum __ptrace_request op, pid_t pid, void *addr, void *data);
|
user_hwdebug_state 结构体如下(sys/ptrace.h
):
1 2 3 4 5 6 7 8 9
| struct user_hwdebug_state { __u32 dbg_info; __u32 pad; struct { __u64 addr; __u32 ctrl; __u32 pad; } dbg_regs[16]; };
|
设置断点的时候我们需要修改 dbg_regs 。看上去有 16 个,实际上可用的要少于 16。
一般来说, breakpoint 可用个数为 6 , watchpoint 可用个数为 4
dbg_info 包含了可用调试寄存器的槽位个数(低 8 位),在 get 的时候可用,set 时忽略。
参见 ptrace_hbp_get_resource_info
dbg_regs 包含了调试寄存器的信息,其中,addr 就是要监视的地址,ctrl 是下面的结构,将一个 u32 按位编码分成四段:
common/arch/arm64/include/asm/hw_breakpoint.h
1 2 3 4 5 6 7
| struct arch_hw_breakpoint_ctrl { u32 __reserved : 19, len : 8, type : 2, privilege : 2, enabled : 1; };
|
len 表示监视地址的范围,可取 1-8 字节,实际的值如下:
1 2 3 4 5 6 7 8
| #define ARM_BREAKPOINT_LEN_1 0x1 #define ARM_BREAKPOINT_LEN_2 0x3 #define ARM_BREAKPOINT_LEN_3 0x7 #define ARM_BREAKPOINT_LEN_4 0xf #define ARM_BREAKPOINT_LEN_5 0x1f #define ARM_BREAKPOINT_LEN_6 0x3f #define ARM_BREAKPOINT_LEN_7 0x7f #define ARM_BREAKPOINT_LEN_8 0xff
|
type 表示断点类型,应该是 bitwise flag 。
对于 breakpoint ,只能为 execute (执行):
1 2
| #define ARM_BREAKPOINT_EXECUTE 0
|
对于 watchpoint ,只能为 load (读)或 store (写):
1 2 3
| #define ARM_BREAKPOINT_LOAD 1 #define ARM_BREAKPOINT_STORE 2
|
privilege 表示特权级,我们只能用到 EL0 :
1 2 3
| #define AARCH64_BREAKPOINT_EL1 1 #define AARCH64_BREAKPOINT_EL0 2
|
enabled 表示是否启用断点,0 表示禁用,1 表示启用。
代码实现
代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| struct arm64_breakpoint { uintptr_t addr; uint8_t len; uint8_t type; uint8_t privilege; bool enabled; };
static inline uint32_t encode_ctrl_reg(const arm64_breakpoint &ctrl) { uint32_t val = (ctrl.len << 5) | (ctrl.type << 3) | (ctrl.privilege << 1) | ctrl.enabled; return val; }
bool set_debug_registers(pid_t pid, uint32_t type, uint32_t n, arm64_breakpoint* items) { auto type_name = type == NT_ARM_HW_BREAK ? "breakpoint" : (type == NT_ARM_HW_WATCH ? "watchpoint" : "<?>"); struct iovec iov{}; struct user_hwdebug_state hwdebug{}; memset(&hwdebug, 0, sizeof(hwdebug)); iov.iov_base = &hwdebug; iov.iov_len = sizeof(hwdebug); if (ptrace(PTRACE_GETREGSET, pid, type, &iov) == -1) { PLOGE("set_debug_registers %s: get", type_name); return false; } auto limit = hwdebug.dbg_info & 0xff; if (limit < n) { LOGE("%s count exceed limit (%d > %d)", type_name, n, limit); return false; } for (int i = 0; i < n; i++) { hwdebug.dbg_regs[i].addr = items[i].addr; hwdebug.dbg_regs[i].ctrl = encode_ctrl_reg(items[i]); } iov.iov_len -= sizeof(hwdebug.dbg_regs[0]) * (16 - n); if (ptrace(PTRACE_SETREGSET, pid, type, &iov) == -1) { PLOGE("set_debug_registers %s: set", type_name); return false; } return true; }
|
需要注意:
- 内核处理设置断点是按照 iovec 的长度动态读取,因此如果要设置 n 个断点,只要从 0 开始写 n 个 hwdebug,dbg_regs
- 并且 iovec 的 iov_len 要设置为实际的断点个数的长度,即减去不需要的长度。
- 由于断点个数有限,因此设置超出了 dbg_info 所提供的个数的断点会返回错误 ENOSPC 。
- GETREGSET 返回的 dbg_info 似乎与原先 SETREGSET 的并不完全一致,因此建议自己维护断点信息,修改的时候不要使用 GETREGSET 的信息。
判断命中断点
命中断点会产生一个 SIGTRAP ,但是在使用 ptrace 的时候,显然收到 SIGTRAP 的情况不止一种。那么如何判断这个信号是不是命中断点呢?
观察内核中注册断点处理程序的代码:
common/arch/arm64/kernel/ptrace.c ptrace_hbp_create
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| static struct perf_event *ptrace_hbp_create(unsigned int note_type, struct task_struct *tsk, unsigned long idx) { bp = register_user_hw_breakpoint(&attr, ptrace_hbptriggered, NULL, tsk); if (IS_ERR(bp)) return bp; }
#ifdef CONFIG_HAVE_HW_BREAKPOINT
static void ptrace_hbptriggered(struct perf_event *bp, struct perf_sample_data *data, struct pt_regs *regs) { arm64_force_sig_fault(SIGTRAP, TRAP_HWBKPT, bkpt->trigger, desc); }
|
处理 ptrace 的断点的函数是 ptrace_hbptriggered
,是发送一个 si_code
为 TRAP_HWBKPT
的 SIGTRAP
。
更进一步可以发现会往 siginfo
的 si_addr
赋值。
因此,只要 signo == SIGTRAP && si_code == TRAP_HWBKPT
,这就是一个硬件断点触发的 trap ,且地址在 si_addr
中。
common/kernel/signal.c
1 2 3 4 5 6 7 8 9 10 11 12
| int force_sig_fault_to_task(int sig, int code, void __user *addr, struct task_struct *t) { struct kernel_siginfo info;
clear_siginfo(&info); info.si_signo = sig; info.si_errno = 0; info.si_code = code; info.si_addr = addr; return force_sig_info_to_task(&info, t, HANDLER_CURRENT); }
|
实际使用时的工作流
利用硬件断点,一般遵循以下的步骤:
注册断点 -> 等待断点命中 -> 禁用相应断点 -> 单步执行当前指令 -> 启用相应断点 -> 继续运行
在执行当前指令前,需要禁用相应的断点,否则 ptrace 让程序继续运行的时候,断点会在当前的 pc 地址再次触发。
而为了跟踪所有指令发生的事件,必须单步执行命中断点的这一条指令,在执行完成后,还要启用相应断点并继续执行。
硬件断点的行为
根据编写代码和观察可以得到下面的结论:
读
如果指令对断点地址执行读操作,则在指令执行前发出中断,即指令未执行,地址指向的内容未被读,pc 指向读指令的地址。
写
如果指令对断点地址执行写操作,则在指令执行前发出中断,即指令未执行,地址指向的内容未被写,pc 指向写指令的地址。
执行
如果 pc 进入了断点地址,则在当前位置中断。
所以这不能用于在「跳转指令处」中断。
其他行为
- 你可以给一个断点同时设置 ARM_BREAKPOINT_LOAD 和 ARM_BREAKPOINT_STORE ,效果是读写都会触发断点。
- 你也可以给同一个地址设置多个 watchpoint,例如分别设置读、写断点,它们都起作用。给同一个地址同时设置 breakpoint 和 watchpoint 也是允许的。
其他硬件断点相关
还可以使用 perf event 实现硬件断点
Android使用perf_event实现硬断点 | richar的技术集