目录
(代码:linux 6.3.1,架构:arm64)
One look is worth a thousand words. —— Tess Flanders
相关链接:
【原创】Linux kprobe 实现原理 图文详解 (二) kretprobe实现原理
【原创】Linux kprobe 实现原理 图文详解 (三) kprobe跳转优化
【原创】Linux kprobe 实现原理 图文详解 (四) 指令替换、cpu stopper
一、kprobe 基本介绍
Kprobe 是 Linux内核 提供的一种动态调试机制,用于debug、动态跟踪、性能分析。Kprobe技术可以对内核函数进行探测,使得被探测指令执行的前后,可以执行用户注册的钩子函数(pre_handler、post_handler)。
Kprobe可以在大部分内核代码段插入探测点,除了一些分支类指令(如:br、exception、eret等)、以及Kprobe模块本身代码也无法被探测。

二、kprobe 实现原理

上图为内核image反汇编后某一个函数的前2条指令,假设采用kprobe对第一条指令“stp x29, x30, [sp, #-32]!”进行打桩探测。打桩后,kprobe触发的完整流程图如下:

1)register_kprobe内核接口,会将代码中 “被探测指令” 替换成 “断点指令BRK” (arm64架构),同时会分配 “指令槽”,用于存放 “被探测指令”、BRK指令;
2) 当程序执行到被探测指令(本例中为0xffff80000001ae70)时,由于代码段中对应指令已经被替换成BRK断点指令,于是会触发同步异常,陷入异常处理;
3)在 el1 sync 异常处理中,会根据 ESR寄存器 中的异常信息走brk_handler处理流程,一步步最终会调用到:register_kprobe注册时提供的 pre_handler 钩子函数;当其执行完后,会将任务寄存器上下文中的 PC 修改为 “指令槽地址”,然后eret从异常返回;
4)当从异常返回后,程序会跳转到指令槽,并执行其中保存的 “被探测指令 stp, x29, 30, [sp, #-32]!”(上图中的步骤4),执行完后 PC+4,接着执行下一条 BRK指令,再次触发同步异常陷入内核 el1 sync 同步异常处理流程;
5)在异常处理中,同样会根据esr信息,最终调用到 register_kprobe注册时提供的 post_handler 钩子函数;当该钩子函数执行完毕后,会将 regs->PC 重新设置为 “被探测指令 + 4” (即:被探测指令的下一条指令),然后异常返回后,程序就会回到原先的指令指令流程中去了。
三、BRK指令
arm64架构下指令异常指令是BRK,看完上述kprobe完成流程后,有些小伙伴心中可能会有些疑惑:BRK断点指令触发的2次同步异常,会调用到不同的钩子函数,内核是如何进行区分的?
要回答上述问题,就要来看一下ARM64手册中 “brk指令” 的编码方式:

可以看到:BRK指令 5~20 bit(imm16字段)代表该BRK指令的用途,触发异常时,这16 bits 会被记录到 ESR寄存器 中。
这样在同步异常处理时,就可以根据 ESR寄存器中的 “ESR_ELx_BRK64_ISS_COMMENT_MASK” 字段,区分判断出本次BRK异常的触发原因,进而调用不同的处理函数,最终分别执行到 kprobe被探测指令对应的 pre_handler、post_handler。



上图中,“BRK64_OPCODE_KPROBES”、“BRK64_OPCODE_KPROBES_SS” 分别就是:“替换被探测指令的BRK指令” 、以及 “指令槽中的BRK指令”。
ESR_EL1(Exception Syndrome Register)


当执行BRK指令触发异常陷入内核后,ESR寄存器中的EC字段为0b111100(即:0x3C), ISS字段的[0-15]位,为immediate field(即:BRK指令的imm16字段!)
例:修改PC寄存器中的命令为BRK64_OPCODE_KPROBES(0xd4200080),即: (AARCH64_BREAK_MON | (KPROBES_BRK_IMM << 5))。当CPU执行这条命令时,会触发异常陷入内核。陷入内核时,ESR寄存器中的内容的低16位为:0x0004(即:KPROBES_BRK_IMM)。
四、具体代码
1、kprobe数据结构
struct kprobe {
struct hlist_node hlist; // 用于挂载到全局哈希表kprobe_table上的node节点
struct list_head list; /* list of kprobes for multi-handler support (对于aggr_kprobe, 该成员为链表头. 对于普通kprobe, 该成员为挂载到aggr_kprobe->list的链表节点) */
unsigned long nmissed; /*count the number of times this probe was temporarily disarmed. 代表CPU正在执行其他kprobe过程中, hit到了该kprobe指令, 从到导致该kprobe指令仅被执行探测指令本身, 但未执行pre_handler和post_handler. 即: 该kprobe点被hit到,但pre_handler和post_handler未被执行的次数. */
kprobe_opcode_t *addr; /* location of the probe point */
const char *symbol_name; /* Allow user to indicate symbol name of the probe point */
unsigned int offset; /* Offset into the symbol */
kprobe_pre_handler_t pre_handler; /* Called before addr is executed. */
kprobe_post_handler_t post_handler; /* Called after addr is executed, unless... */
kprobe_opcode_t opcode; /* Saved opcode (which has been replaced with breakpoint) (被探测的指令内容) */
struct arch_specific_insn ainsn; /* copy of the original instruction (指令槽, 用于存放被探测指令和软断点指令) */
/*
* Indicates various status flags.
* Protected by kprobe_mutex after this kprobe is registered.
*/
u32 flags;
};
#define arch_specific_insn arch_probes_insn
struct arch_probe_insn {
probe_opcode_t *insn;
pstate_check_t *pstate_cc;
probes_handler_t *handler;
/* restore address after step xol */
unsigned long restore;
};
struct arch_specific_insn {
struct arch_probe_insn api;
};
2、注册kporbe
int register_kprobe(struct kprobe *p) {
kprobe_opcode_t *addr
addr = kprobe_addr(p)
p->addr = addr
ret = warn_kprobe_rereg(p)
if (ret)
return ret
/* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */
p->flags &= KPROBE_FLAG_DISABLED;
p->nmissed = 0;
INIT_LIST_HEAD(&p->list);
check_kprobe_address_safe // 检查probe地址安全性 (Detail see below)
mutex_lock(&kprobe_mutex)
old_p = get_kprobe(p->addr)
if (old_p)
...
cpus_read_lock()
mutex_lock(&text_mutex) // Prevent text modification
prepare_kprobe(p) {
/* Must ensure p->addr is really on ftrace */
if (kprobe_ftrace(p))
...
arch_prepare_kprobe(p) {
unsigned long probe_addr = (unsigned long)p->addr
/* copy instruction */
p->opcode = le32_to_cpu(*p->addr)
if (search_exception_tables(probe_addr))
return -EINVAL
enum probe_insn decoded = arm_kprobe_decode_insn(p->addr, &p->ainsn) /* decode instruction (Detail see below) */
switch (decoded) {
case INSN_REJECTED: /* insn not supported */
return -EINVAL;
case INSN_GOOD_NO_SLOT: /* insn need simulation */
p->ainsn.api.insn = NULL
break
case INSN_GOOD: /* instruction uses slot */
p->ainsn.api.insn = get_insn_slot()
if (!p->ainsn.api.insn)
return -ENOMEM
break
}
/* prepare the instruction. 即: 将2条指令写到指令槽中, 记录probe指令的下一条指令 */
if (p->ainsn.api.insn) {
### 分配保存两条指令的地址(ainsn.api.insn[0] 和 ainsn.api.insn[1]),在这2个指令地址上分别填写 探测的指令内容(即: p->opcode) 以及 软断点BRK指令.
### 同时在kprobe->ainsn.api.restore中记录探测指令的下一条指令.
###
### 【kprobe->ainsn.api.insn这个字段记录的两条指令地址的执行时机】
### BRK64_OPCODE_KPROBES断点指令触发后且执行完pre_handler后,就会执行kprobe->ainsn.api.insn中2条指令地址上的指令.
### 【具体实现】
### 通过KPROBES_BRK_IMM类型异常的handler, 将PC指向p->ainsn.api.insn
### kprobes_break_hook.fn
### A.K.A
### kprobe_breakpoint_handler
### kprobe_handler
### setup_singlestep
### instruction_pointer_set(regs, (unsigned long)p->ainsn.api.insn)
### regs->pc = (unsigned long)p->ainsn.api.insn
arch_prepare_ss_slot(p) {
kprobe_opcode_t *addr = p->ainsn.api.insn
void *addrs[] = {addr, addr + 1} // 指令地址
u32 insns[] = {p->opcode, BRK64_OPCODE_KPROBES_SS} // 指令内容
/* prepare insn slot. 将insns中保存的2条指令内容, 填写到p->ainsn.api.insn所代表的指令槽中 */
aarch64_insn_patch_text(addrs, insns, cnt = 2)
flush_icache_range((uintptr_t)addr, (uintptr_t)(addr + MAX_INSN_SIZE))
...//TODO
### 保存观测指令的下一条指令, 即: 观测指令地址 + 4
/* Needs restoring of return address after stepping xol. */
p->ainsn.api.restore = (unsigned long) p->addr + sizeof(kprobe_opcode_t)
}
} else {
arch_prepare_simulate(p) {
/* This instructions is not executed xol. No need to adjust the PC */
p->ainsn.api.restore = 0
}
}
}
}
mutex_unlock(&text_mutex)
cpus_read_unlock()
INIT_HLIST_NODE(&p->hlist)
hlist_add_head_rcu(&p->hlist, &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)])
### 若kprobe->flags含有(KPROBE_FLAG_DISABLED | KPROBE_FLAG_GONE)中的一位, 就不arm kprobe!!! 《《《《《《《《《《 !!!《《《《《《《《《《
if (!kprobes_all_disarmed && !kprobe_disabled(p))
arm_kprobe(p) // ##### <<<<< Install breakpoint for kprobe in text. (Details see below)
try_to_optimize_kprobe(p) /* Try to optimize kprobe */
mutex_unlock(&kprobe_mutex)
}
3、第一次 BRK(KPROBE_BRK_IMM)同步异常,触发后的处理流程
el1h_64_sync_handler(struct pt_regs *regs) { // arch/arm64/kernel/entry-common.c
unsigned long esr = read_sysreg(esr_el1)
switch (ESR_ELx_EC(esr)) {
case ESR_ELx_EC_BRK64:
el1_dbg(regs, esr) {
/*
* Handle IRQ/context state management when entering a debug exception from
* kernel mode. Before this function is called it is not safe to call regular
* kernel code, intrumentable code, or any code which may trigger an exception.
*/
arm64_enter_el1_dbg(regs)
do_debug_exception(far, esr, regs) {
const struct fault_info *inf = esr_to_debug_fault_info(esr)
return debug_fault_info + DBG_ESR_EVT(esr)
inf->fn(addr_if_watchpoint, esr, regs)
A.K.A
brk_handler {
call_break_hook(regs, esr) { // <<<<< used for kprobes
list = user_mode(regs) ? &user_break_hook : &kernel_break_hook
list_for_each_entry_rcu(hook, list, node) { // arch_init_kprobes中会将kprobes_break_hook注册到kernel_break_hook list
unsigned int comment = esr & ESR_ELx_BRK64_ISS_COMMENT_MASK
if ((comment & ~hook->mask) == hook->imm) // hook->imm: KPROBES_BRK_IMM
fn = hook->fn
}
return fn ? fn(regs, esr) : DBG_HOOK_ERROR
A.K.A
kprobe_breakpoint_handler { // ##### <<<< 执行观测指令前, 会先触发异常, 进到这里
kprobe_handler(regs) {
struct kprobe *p, *cur_kprobe
struct kprobe_ctlblk *kcb
unsigned long addr = instruction_pointer(regs)
kcb = get_kprobe_ctlblk()
cur_kprobe = kprobe_running()
p = get_kprobe((kprobe_opcode_t *) addr)
if (p) {
if (cur_kprobe)
if (reenter_kprobe(p, regs, kcb))
return
else {
/* Probe hit */
set_current_kprobe(p)
__this_cpu_write(current_kprobe, p)
kcb->kprobe_status = KPROBE_HIT_ACTIVE
### 1) 若没有注册pre_handler, 就继续走正常处理(单步执行probe指令, 并触发异常);
### 2) 若注册pre_handler, 且pre_handler执行完毕返回0, 也继续走正常处理;
### 3) 若注册pre_handler, 且pre_handler执行完毕返回非0, 则kprobes stops single stepping and just returns to the given address.
### This also means post_handler should not be called anymore.
if (!p->pre_handler || !p->pre_handler(p, regs)) {
setup_singlestep(struct kprobe *p = p, regs, kcb, int reenter = 0) {
kcb->kprobe_status = KPROBE_HIT_SS
if (p->ainsn.api.insn) {
slot = (unsigned long)p->ainsn.api.insn
kprobes_save_local_irqflag(kcb, regs) { // <<<<<<<<<< 将regs->PSTATE中DAIF置1, 异常处理返回后, PSTATE寄存器的D、A、I、F标志位都设置为1, 即: 关中断 (kprobe_breakpoint_ss_handler处理完从异常返回时开中断) >>>>>>>>>>
kcb->saved_irqflag = regs->pstate & DAIF_MASK
regs->pstate |= DAIF_MASK
}
instruction_pointer_set(regs, slot) // PC寄存器设为register_kprobe时准备好的slot
regs->pc = slot
} else {
/* insn simulation */
arch_simulate_insn(p, regs) {
struct kprobe_ctlblk *kcb = get_kprobe_ctlblk()
if (p->ainsn.api.handler)
p->ainsn.api.handler((u32)p->opcode, (long)p->addr, regs)
/* single step simulated, now go for post processing */
post_kprobe_handler(p, kcb, regs) {
...
}
}
}
}
} else
reset_current_kprobe()
__this_cpu_write(current_kprobe, NULL)
}
}
}
}
}
}
}
arm64_exit_el1_dbg(regs)
}
}
}
4、第二次 BRK(KPROBES_BRK_SS_IMM)同步异常,触发后的处理流程
el1h_64_sync_handler(struct pt_regs *regs) {
unsigned long esr = read_sysreg(esr_el1)
switch (ESR_ELx_EC(esr)) {
case ESR_ELx_EC_BRK64:
el1_dbg(regs, esr) {
arm64_enter_el1_dbg(regs)
do_debug_exception(far, esr, regs) {
const struct fault_info *inf = esr_to_debug_fault_info(esr)
return debug_fault_info + DBG_ESR_EVT(esr)
inf->fn(addr_if_watchpoint, esr, regs)
A.K.A
brk_handler {
call_break_hook(regs, esr) { // <<<<< used for kprobes
list = user_mode(regs) ? &user_break_hook : &kernel_break_hook
list_for_each_entry_rcu(hook, list, node) {
unsigned int comment = esr & ESR_ELx_BRK64_ISS_COMMENT_MASK
if ((comment & ~hook->mask) == hook->imm) // hook->imm: KPROBES_BRK_SS_IMM
fn = hook->fn
}
return fn ? fn(regs, esr) : DBG_HOOK_ERROR
A.K.A
kprobe_breakpoint_ss_handler { // ##### <<<< 执行观测指令后, 会先触发异常, 进到这里
struct kprobe_ctlblk *kcb = get_kprobe_ctlblk()
unsigned long addr = instruction_pointer(regs)
struct kprobe *cur = kprobe_running()
if (cur && (kcb->kprobe_status & (KPROBE_HIT_SS | KPROBE_REENTER)) &&
((unsigned long)&cur->ainsn.api.insn[1] == addr)) {
kprobes_restore_local_irqflag(kcb, regs) // <<<<<<<<<< 将regs->pstate恢复成触发BRK(KPROBES_BRK_IMM)同步异常时的值, 本次异常处理返回后, 会将PSTATE寄存器内容恢复, 即: 开中断 (kprobe_breakpoint_handler处理完从异常返回时关中断) >>>>>>>>>>
post_kprobe_handler(cur, kcb, regs) {
/* return addr restore if non-branching insn */
if (cur->ainsn.api.restore != 0) // 将probe指令的下一条直接填到PC中, 这样从异常处理返回后就会继续执行probe指令的下一条
instruction_pointer_set(regs, cur->ainsn.api.restore)
regs->pc = cur->ainsn.api.restore
/* restore back original saved kprobe variables and continue */
if (kcb->kprobe_status == KPROBE_REENTER) {
restore_previous_kprobe(kcb) {
__this_cpu_write(current_kprobe, kcb->prev_kprobe.kp)
kcb->kprobe_status = kcb->prev_kprobe.status
}
return
}
/* call post handler */
kcb->kprobe_status = KPROBE_HIT_SSDONE
if (cur->post_handler)
cur->post_handler(cur, regs, 0) // 调用用户注册的post_handler
reset_current_kprobe()
__this_cpu_write(current_kprobe, NULL)
}
return DBG_HOOK_HANDLED
}
/* not ours, kprobes should ignore it */
return DBG_HOOK_ERROR
}
}
}
}
arm64_exit_el1_dbg(regs)
}
}
}
以上就是 linux kprobe 的完整实现逻辑,感谢大伙的收看 !~
接下来将会陆续更新以下文章,敬请期待 ^_^
kretprobe详解、aggregative kprobe详解、reenter kprobe详解、kprobe跳转优化详解。
423




被折叠的 条评论
为什么被折叠?



