Android arm64 在 ptrace 中使用硬件断点

ARM64内核研究(五) | Ylarod’s Blog

gdb 系列(1) (hwbreakpoint\watchpoint)

正文

为了跟踪相应地址发生的事件(读、写、执行)是哪一条指令造成的,我们可以使用硬件断点来达成这一目标。

准备条件

需要内核启用 CONFIG_HAVE_HW_BREAKPOINTCONFIG_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_BREAKNT_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
/* Breakpoint */
#define ARM_BREAKPOINT_EXECUTE 0

对于 watchpoint ,只能为 load (读)或 store (写):

1
2
3
/* Watchpoints */
#define ARM_BREAKPOINT_LOAD 1
#define ARM_BREAKPOINT_STORE 2

privilege 表示特权级,我们只能用到 EL0 :

1
2
3
/* Privilege Levels */
#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;
}

需要注意:

  1. 内核处理设置断点是按照 iovec 的长度动态读取,因此如果要设置 n 个断点,只要从 0 开始写 n 个 hwdebug,dbg_regs
  2. 并且 iovec 的 iov_len 要设置为实际的断点个数的长度,即减去不需要的长度。
  3. 由于断点个数有限,因此设置超出了 dbg_info 所提供的个数的断点会返回错误 ENOSPC 。
  4. 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
/*
* Handle hitting a 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_codeTRAP_HWBKPTSIGTRAP

更进一步可以发现会往 siginfosi_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 进入了断点地址,则在当前位置中断。

所以这不能用于在「跳转指令处」中断。

其他行为

  1. 你可以给一个断点同时设置 ARM_BREAKPOINT_LOAD 和 ARM_BREAKPOINT_STORE ,效果是读写都会触发断点。
  2. 你也可以给同一个地址设置多个 watchpoint,例如分别设置读、写断点,它们都起作用。给同一个地址同时设置 breakpoint 和 watchpoint 也是允许的。

其他硬件断点相关

还可以使用 perf event 实现硬件断点

Android使用perf_event实现硬断点 | richar的技术集