kprobe原理分析
阅读数:185 评论数:0
跳转到新版页面分类
Linux
正文
一、概述
Linux内核提供了许多调试内核的方式,比如 ftrace,tracepoint,kprobe/uprobe 等可以跟踪内核函数的执行流程,获取内核执行信息。
kprobe 是内核的动态探测工具,它提供了一种调试机制,能够在不修改现有代码的基础上,跟踪内核函数的执行,并且它几乎可以探测任何一条内核指令。它的基本工作原理是:用户指定一个探测点(可以是内核函数,也可以是内核函数加偏移,或者一个内核地址),并且把一个用户定义的处理函数关联到该探测点,该探测点指定的地址将被替换为对应架构的 trap 指令(x86 的 int3,arm64 的 brk),当内核执行到该地址时,触发异常,在架构的异常处理中可以检测到探测点,并且探测点关联的函数将被执行,接着继续正常执行代码路径。
二、简单的例子
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#define MAX_SYMBOL_LEN 64
static char symbol[MAX_SYMBOL_LEN] = "_do_fork";
module_param_string(symbol, symbol, sizeof(symbol), 0644);
/* For each probe you need to allocate a kprobe structure */
static struct kprobe kp = {
.symbol_name = symbol,
};
/* kprobe pre_handler: called just before the probed instruction is executed */
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_ARM64
pr_info("<%s> pre_handler: p->addr = 0x%p, pc = 0x%lx,"
" pstate = 0x%lx\n",
p->symbol_name, p->addr, (long)regs->pc, (long)regs->pstate);
#endif
/* A dump_stack() here will give a stack backtrace */
return 0;
}
/* kprobe post_handler: called after the probed instruction is executed */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
#ifdef CONFIG_ARM64
pr_info("<%s> post_handler: p->addr = 0x%p, pstate = 0x%lx\n",
p->symbol_name, p->addr, (long)regs->pstate);
#endif
}
/*
* fault_handler: this is called if an exception is generated for any
* instruction within the pre- or post-handler, or when Kprobes
* single-steps the probed instruction.
*/
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
pr_info("fault_handler: p->addr = 0x%p, trap #%dn", p->addr, trapnr);
/* Return 0 because we don't handle the fault. */
return 0;
}
static int __init kprobe_init(void)
{
int ret;
kp.pre_handler = handler_pre;
kp.post_handler = handler_post;
kp.fault_handler = handler_fault;
ret = register_kprobe(&kp);
if (ret < 0) {
pr_err("register_kprobe failed, returned %d\n", ret);
return ret;
}
pr_info("Planted kprobe at %p\n", kp.addr);
return 0;
}
static void __exit kprobe_exit(void)
{
unregister_kprobe(&kp);
pr_info("kprobe at %p unregistered\n", kp.addr);
}
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");
上述代码中通过 register_kprobe 注册了一个探测点,该探测点指定了探测的内核函数是 _do_fork,注册成功后,一旦执行到 _do_fork 内核函数,相对应的回调则会被调用。
pre_handler | 当执行 _do_fork 之前被调用,当回调返回 0 时,则会单步执行原理指令 |
post_handler | 当对应指令执行后,会调用注册的 post_handler。 |
fault_handler | 当在 pre_handler 到 post_handler 之间发生 pagefault,则会调用 fault_handler。 |
三、原理分析
首先内核提供了一些 api 来注册移除 hook 点。
register_kprobe,unregister_kprobe,register_kprobes,unregister_kprobes
register_kretprobe,unregister_kretprobe。
通过 register_kprobe 可以注册一个 hook点,register_kprobes 可以一次性注册多个 hook 点。
register_kretprobe在函数返回处注册 hook。
1、struct kprobe
struct kprobe {
struct hlist_node hlist; // 注册到系统的全局链表节点。
struct list_head list; // 一个 hook 点可以注册多个 kprobe,这个链表用于将同一个 hook 点串联起来。
unsigned long nmissed; // 因断点指令不能重入处理, 当多个 kprobe 一起触发时会放弃执行后面的probe, 同时该计数增加。
kprobe_opcode_t *addr; // 探测点对应的地址, 用户在调用注册接口时可以指定地址, 也可以传入函数名让内核自己查找。
const char *symbol_name; // 探测点对应的函数名, 在注册 kprobe 时会将其翻译为十六进制地址并修改addr。
unsigned int offset; // 相对于入口点地址的偏移, 会在计算 addr 以后再加上 offset 得到最终的 addr。
kprobe_pre_handler_t pre_handler; // 在执行探测地址之前调用的 handler
kprobe_post_handler_t post_handler; // 在探测地址执行后调用的 handler
kprobe_fault_handler_t fault_handler; // 在 kprobe 运行期间触发异常则会调用该 handler
kprobe_opcode_t opcode; // 探测地址对应的原始指令
struct arch_specific_insn ainsn; // 架构相关结构,用于保存架构对应使用的数据。
u32 flags; // 当前 kprobe 的状态记录。
// 有如下状态
#define KPROBE_FLAG_GONE 1 // kprobe 已经取消,内部特殊状态
#define KPROBE_FLAG_DISABLED 2 // kprobe 暂时性的禁用
#define KPROBE_FLAG_OPTIMIZED 4 // 可以优化 kprobe,架构相关,arm64 没有优化
#define KPROBE_FLAG_FTRACE 8 // kprobe 可以被优化到 ftrace 上处理,目前只有 x86 会使用该状态。
}
// arm64 数据结构
struct arch_specific_insn {
struct arch_probe_insn api;
};
struct arch_probe_insn {
probe_opcode_t *insn; // 当探测地址的指令不是需要仿真的指令时,会分配一个指令槽,用于存放原始指令,以便于单步执行。
pstate_check_t *pstate_cc; // 对应 pstate 状态,会在合适实际修改,恢复。
probes_handler_t *handler; // 当对应指令是一个模拟指令时,对应的指令类型会有一个 handler 用于执行模拟指令运行。
/* restore address after step xol */
unsigned long restore; // 当非模拟指令时,会保存下一个执行的 pc 地址,为模拟指令时该值等于 0。
};
2、register_kprobe
int register_kprobe(struct kprobe *p)
{
...
/* Adjust probe address from symbol */
addr = kprobe_addr(p); ----------------------------------(1)
if (IS_ERR(addr))
return PTR_ERR(addr);
p->addr = addr;
...
ret = check_kprobe_rereg(p); ----------------------------(2)
...
p->flags &= KPROBE_FLAG_DISABLED; -----------------------(3)
p->nmissed = 0; -----------------------------------------(3)
...
ret = check_kprobe_address_safe(p, &probed_mod); --------(4)
...
// 从 hashtable 中获取该 hook 点原始的 kprobe,我们会将新的 kprobe 挂入原始 kp 的 list 中,后续有头 kp 调用。
old_p = get_kprobe(p->addr);
if (old_p) {
/* Since this may unoptimize old_p, locking text_mutex. */
ret = register_aggr_kprobe(old_p, p);
goto out;
}
...
cpus_read_lock();
/* Prevent text modification */
mutex_lock(&text_mutex);
ret = prepare_kprobe(p); --------------------------------(5)
mutex_unlock(&text_mutex);
cpus_read_unlock();
...
// 将设置好的 kprobe 加入到全局 hashtable 中
hlist_add_head_rcu(&p->hlist,
&kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);
...
if (!kprobes_all_disarmed && !kprobe_disabled(p)) {
// 当没有禁用 kprobe 时,调用 arm_kprobe 激活该 kprobe。
ret = arm_kprobe(p);
...
// 对于可以对 kprobe 的架构执行优化,arm64 为空
try_to_optimize_kprobe(p);
...
}
(1)首先会通过传入的 kprobe 结构获取到对应探测点的内核地址
kprobe_addr
-> _kprobe_addr(p->addr, p->symbol_name, p->offset);
static kprobe_opcode_t *_kprobe_addr(kprobe_opcode_t *addr,
const char *symbol_name, unsigned int offset)
{
// 在注册 kprobe 中不能同时指定 addr 和 symbol_name
if ((symbol_name && addr) || (!symbol_name && !addr))
goto invalid;
// 如果 name 存在,则通过 kall_symbol 模块获取到内核符号对应的内核地址
if (symbol_name) {
addr = kprobe_lookup_name(symbol_name, offset);
if (!addr)
return ERR_PTR(-ENOENT);
}
// 根据偏移返回实际我们要探测的内核地址,可能是内核函数的地址也可能时函数内部某个指令的地址。
addr = (kprobe_opcode_t *)(((char *)addr) + offset);
if (addr)
return addr;
...
...
}
(2)检测该 kprobe 是否是重复注册的,是重复注册会返回错误
check_kprobe_rereg
-> __get_valid_kprobe // 如果返回有 kprobe 则当前 kprobe 被注册了,返回错误。
static struct kprobe *__get_valid_kprobe(struct kprobe *p)
{
struct kprobe *ap, *list_p;
// 遍历全局 hashtable 查看对应地址是否已经有 kprobe。
ap = get_kprobe(p->addr);
if (unlikely(!ap)) // 如果为空则没有注册过。
return NULL;
// 如果现在注册的 kprobe 与原有的 kprobe 不相等,那么遍历原有 kprobe list 链表,看看是否是串联链表中的 kprobe。
if (p != ap) {
list_for_each_entry_rcu(list_p, &ap->list, list)
if (list_p == p)
/* kprobe p is a valid probe */
goto valid;
return NULL;
}
valid:
return ap;
}
struct kprobe *get_kprobe(void *addr)
{
struct hlist_head *head;
struct kprobe *p;
// 从全局 hashtable 得到对应数组的 hlist_head,再去遍历 hlist 获取 kprobe。
head = &kprobe_table[hash_ptr(addr, KPROBE_HASH_BITS)];
hlist_for_each_entry_rcu(p, head, hlist) {
if (p->addr == addr) // hashtable 中有对应的 kprobe
return p;
}
return NULL;
}
(3)用户指令标记 kprobe 的状态为禁用状态,其他状态不能设置。设置 nmissed 为 0,没有错过处理该 kprobe。
(4)检查探测地址是否是一个合法的地址
check_kprobe_address_safe
-> arch_check_ftrace_location
-> !kernel_text_address || within_kprobe_blacklist || jump_label_text_reserved
-> __module_text_address
arch_check_ftrace_location 检测地址是否也是一个 ftrace 的地址,如果是,并且对应架构支持 CONFIG_KPROBES_ON_FTRACE ,kprobe 附加在 ftrace 上则会为该 kprobe 添加 KPROBE_FLAG_FTRACE 标记。
目前 arm64 不支持附加在 ftrace 上,需要走原始路径。
接着检测是否是内核的文本段,只有文本段可以设置 kprobe,内核自定义的"黑名单"不能设置kprobe。
如果 arm64 中的 异常处理程序段,idmap段,hyp 文本段,search_execption 段,__kprobe 标记的内核函数,NOKPROBE_SYMBOL 申明的导出函数这些都属于 kprobe 黑名单,不能被设置为探测点。
(5)配置这个 kprobe
prepare_kprobe
-> arch_prepare_kprobe
int __kprobes arch_prepare_kprobe(struct kprobe *p)
{
unsigned long probe_addr = (unsigned long)p->addr;
...
// 检测探测点地址对齐
if (probe_addr & 0x3)
return -EINVAL;
// 获取到探测点对应指令
p->opcode = le32_to_cpu(*p->addr);
...
// 解码该指令
// 返回 INSN_REJECTED 指令不支持 kprobe
// INSN_GOOD_NO_SLOT 是一条模拟指令
// INSN_GOOD 是一条正常指令
switch (arm_kprobe_decode_insn(p->addr, &p->ainsn)) {
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 */
if (p->ainsn.api.insn)
arch_prepare_ss_slot(p); -----------1)
else
// 对于模拟指令,直接设置 p->ainsn.api.restore = 0,表示不需要从这里得到恢复地址。
arch_prepare_simulate(p);
return 0;
}
// 调用
arm_kprobe_decode_insn
-> arm_probe_decode_insn // 解码指令
-> aarch64_insn_is_steppable // 检测指令是否可以单步下,可以单步返回 INSN_GOOD
// 首先分支指令不能单步,因为他可能修改 pc,该 pc 与 xol 地址相关
// 异常指令,smc,hvc,eret 不可以单步。
// msr 指令可以使单步下有无法处理的情况出现,修改系统行为寄存器?。
// hit 指令相关,wfe,wfi,yield,sev,sevl 等也不能单步。
// pc 相对的 ldr/str* 和独占指令也会有问题,也不允许单步。
-> aarch64_insn_is_bcond
-> aarch64_insn_is_cbz
...
...
-> aarch64_insn_is_ldr_lit
...
这些属于模拟指令,会绑定各自对应的 api->handler,用于模拟执行指令,那么这些指令就是不需要分配一个 slot 来 cpu 执行的。标记为 INSN_GOOD_NO_SLOT。
// 剩下的情况则是不支持的指令,返回 INSN_REJECTED
对应 INSN_GOOD 指令,我们需要调用 get_insn_slot 来为该指令分配一个 slot 槽,后续单步执行会跳到该 slot 执行指令。而 INSN_GOOD_NO_SLOT 指令,在单步使将会调用上述绑定的 api->handler 来模拟执行该指令,所以不需要系统分配 slot。get_insn_slot 会预分配一些可执行页面,从页面中分配一个 slot 并返回地址给调用。该 slot 是可执行的,用于执行替换下来的指令。
当分配 slot 时,使用 arch_prepare_ss_slot 向 slot 槽中写入需要指令的指令。
static void __kprobes arch_prepare_ss_slot(struct kprobe *p)
{
/* prepare insn slot */
// 向 slot 槽写入原始指令
patch_text(p->ainsn.api.insn, p->opcode);
// 刷新对应地址的 icache。
flush_icache_range((uintptr_t) (p->ainsn.api.insn),
(uintptr_t) (p->ainsn.api.insn) +
MAX_INSN_SIZE * sizeof(kprobe_opcode_t));
// 设置吓一跳执行的指令,单步后使用。
/*
* Needs restoring of return address after stepping xol.
*/
p->ainsn.api.restore = (unsigned long) p->addr +
sizeof(kprobe_opcode_t);
}
3、arm_kprobe
kprobe 实现机制的重要函数时 arm_kprobe ,它会调用对应架构的 __arm_kprobe,来实际替换原始。
arm_kprobe
-> __arm_kprobe
-> arch_arm_kprobe
// 在 arm64 中,调用 patch_text 将 addr 对应的指令替换为了 BRK64_OPCODE_KPROBES 指令,该指令是一个 brk debug 指令,当触发时,系统会进入 debug 异常处理流程,从而调用 kprobe 相关处理函数。
-> patch_text(p->addr, BRK64_OPCODE_KPROBES);
-> optimize_kprobe
patch_text
-> aarch64_insn_patch_text
-> stop_machine_cpuslocked
-> aarch64_insn_patch_text_cb
static int __kprobes aarch64_insn_patch_text_cb(void *arg)
{
int i, ret = 0;
struct aarch64_insn_patch *pp = arg;
// 通过 stop_machine 机制使所有 cpu 进入停机状态,接着只有第一进入的 cpu 去调用 aarch64_insn_patch_text_nosync 来修改 insn,其他 cpu 在 else 中等待完成修改。
/* The first CPU becomes master */
if (atomic_inc_return(&pp->cpu_count) == 1) {
for (i = 0; ret == 0 && i < pp->insn_cnt; i++)
ret = aarch64_insn_patch_text_nosync(pp->text_addrs[i],
pp->new_insns[i]);
/* Notify other processors with an additional increment. */
atomic_inc(&pp->cpu_count);
} else {
while (atomic_read(&pp->cpu_count) <= num_online_cpus())
cpu_relax();
isb();
}
return ret;
}
aarch64_insn_patch_text_nosync
-> aarch64_insn_write
-> __aarch64_insn_write
// 通过 fixmap 机制,把地址重新映射出来,接着通过 probe_kernel_write 机制把新指令写入探测点。
-> waddr = patch_map(addr, FIX_TEXT_POKE0);
-> ret = probe_kernel_write(waddr, &insn, AARCH64_INSN_SIZE);
-> __flush_icache_range
至此,一个 kprobe 的注册就完成了,后续只要执行到探测点的 brk,则会进入 debug 模式来调用我们的回调函数。
4、kprobe的执行
首先看一下 arm64 的注册函数。
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" },
};
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;
}
在对应的 debug 相关异常中定义了一个数组,指示对应 debug 异常的类型,其中我们可以处理的是:
hardware breakpoint -> DBG_ESR_EVT_HWBP
hardware single-step -> DBG_ESR_EVT_HWSS
hardware watchpoint -> DBG_ESR_EVT_HWWP
aarch64 BRK -> DBG_ESR_EVT_BRK
通过 hook_debug_fault_code 即可像对应 debug 注册处理函数。对于 kprobe 主要涉及两个 debug 异常,第一个是 aarch64 BRK 异常,当系统第一册触发探测点的 brk 时,进入该异常,并在一系列验证后调用我们注册 pre_handler 表示在执行指令之前调用,第二个 hardware single-step 异常,当从 brk 返回后系统处于单步模式,执行一条指令后会进入该 debug 模式调用注册的 post_handler 回调,表示在指令执行后调用。注册代码如下:
static int __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, "ptrace BRK handler");
return 0;
}
arch_initcall(debug_traps_init);
第一次 通过 brk 指令进入 pre_handler 阶段:
entry
-> do_debug_exception
-> brk_handler
// 匹配到是我们手动修改替换的 BRK64_ESR_KPROBES(0x0004)指令,则调用 kprobe 处理。
-> if ((esr & BRK64_ESR_MASK) == BRK64_ESR_KPROBES)
-> kprobe_breakpoint_handler
-> kprobe_handler
static void __kprobes kprobe_handler(struct pt_regs *regs)
{
struct kprobe *p, *cur_kprobe;
struct kprobe_ctlblk *kcb;
unsigned long addr = instruction_pointer(regs);
// kcb 是一个 per_cpu 变量,用于记录执行 kprobe 期间的一些信息,比如执行状态,对应的 irqflags,单步的上下文信息等。
kcb = get_kprobe_ctlblk();
// kprobe 的执行可能是会重入的,所以,同上该变量也是一个 per_cpu 变量,记录当前正在运行 kprobe。
cur_kprobe = kprobe_running();
p = get_kprobe((kprobe_opcode_t *) addr);
if (p) {
// cur_kprobe 存在,说明 kprobe 正在运行,我们重入了,进入重入处理。
if (cur_kprobe) {
if (reenter_kprobe(p, regs, kcb))-----------------------------(3)
return;
} else {
// 首次命中 kprobe,设置 cur_kprobe 为当前 kprobe,修改 kcb 中状态为 KPROBE_HIT_ACTIVE。
/* Probe hit */
set_current_kprobe(p);
kcb->kprobe_status = KPROBE_HIT_ACTIVE;
// 判断当前 kprobe 是否有 pre_handler,如果有则执行 pre_handler 如果返回不为 0 则调用 reset_current_kprobe 清除 cur_kprobe,该 kprobe 执行完毕。
// 如果 pre_handler 不存在,或者 pre_handler 执行返回为 0,说明 post_handler 需要被执行,我们设置单步执行。
if (!p->pre_handler || !p->pre_handler(p, regs)) { ------------(1)
setup_singlestep(p, regs, kcb, 0);-------------------------(2)
} else
reset_current_kprobe();
}
}
}
对于该探测点如果只有一个kprobe 那么 pre_handler 对应的就是kprobe的pre_handler,如果该探测点有多个 kprobe,那么对应的 pre_handler 为 aggr_pre_handler,该 pre_handler 在注册kprobe中设置。
static int aggr_pre_handler(struct kprobe *p, struct pt_regs *regs)
{
struct kprobe *kp;
// 遍历首个 kprobe list 链表,一次调用链表中 kprobe 的 pre_handler。
list_for_each_entry_rcu(kp, &p->list, list) {
if (kp->pre_handler && likely(!kprobe_disabled(kp))) {
set_kprobe_instance(kp);
if (kp->pre_handler(kp, regs))
return 1;
}
reset_kprobe_instance();
}
return 0;
}
当 pre_handler 不存在或者 pre_handler 返回 0时,说明我们需要调用 post_handler,所以需要配置单步执行。
为什么需要单步执行?
首先我们把原来指令替换为了 brk,此时调用该地址时实则调用了 pre_handler,下一步我们需要调用原始指令,并且需要原始指令执行完成后接着调用 post_handler,为了实现这个机制,只有单步这个模式才能做到执行一条指令后进入 debug 模式,也是最合适的实现机制。
5、setup_singlestep
static void __kprobes setup_singlestep(struct kprobe *p,
struct pt_regs *regs,
struct kprobe_ctlblk *kcb, int reenter)
{
unsigned long slot;
if (reenter) { ------------------------------------------5)
save_previous_kprobe(kcb);
set_current_kprobe(p);
kcb->kprobe_status = KPROBE_REENTER;
} else {
kcb->kprobe_status = KPROBE_HIT_SS; ------------------1)
}
if (p->ainsn.api.insn) { ---------------------------------2)
/* prepare for single stepping */
slot = (unsigned long)p->ainsn.api.insn;
set_ss_context(kcb, slot); /* mark pending ss */
spsr_set_debug_flag(regs, 0);
/* IRQs and single stepping do not mix well. */
kprobes_save_local_irqflag(kcb, regs);
kernel_enable_single_step(regs);
instruction_pointer_set(regs, slot); -----------------3)
} else {
/* insn simulation */
arch_simulate_insn(p, regs); -------------------------4)
}
}
(1)当首次进入 kprobe时,设置kprobe状态为 KPROBE_HIT_SS,标记kprobe开始单步执行
(2)当api->insn 存在时,说明我们分配了 slot,我们需要在 slot 中执行原始指令。
set_ss_context 标记 ss_pending 为 true,单步挂起执行。match_addr 为操作地址 + opcode,用于单步中判断地址是否匹配。
spsr_set_debug_flag(regs,0) 清除 pstate 中 D debug 位。
kprobes_save_local_irqflag 保存原来的 pstate 信息,并且在新 pstate 中mask irq。
kernel_enable_single_step 激活单步模式。
(3)instruction_pointer_set(regs, slot); 设置下一条执行的指令,这里将 slot 槽的地址设置为了 pc,那么当 debug 返回时将会跳向 slot 地址所在位置执行,其中 slot 槽中存储的则是原始指令,并且由于激活了单步,一旦我们执行了 slot 槽中的指令我们又会进入单步的debug异常。
(4)当没有 insn 时,说明我们执行的是一个模拟指令,这里直接调用 arch_simulate_insn 来完成模拟执行。
static void __kprobes arch_simulate_insn(struct kprobe *p, struct pt_regs *regs)
{
struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
// api.handler 则会在解码 insn 阶段设置的 handler,在handler 中会模拟执行该指令
if (p->ainsn.api.handler)
p->ainsn.api.handler((u32)p->opcode, (long)p->addr, regs);
// 由于是模拟执行的,我们不需要通过单步执行来进入 post 处理阶段,这里直接进行 post_handler 调用处理。
/* single step simulated, now go for post processing */
post_kprobe_handler(kcb, regs);
}
(5)对应代码中的3号位
static int __kprobes reenter_kprobe(struct kprobe *p,
struct pt_regs *regs,
struct kprobe_ctlblk *kcb)
{
// 检查 kcb 状态,KPROBE_HIT_SSDONE(单步完成)和 KPROBE_HIT_ACTIVE(kprobe激活)
// 在重入阶段,这两个状态表明重入的kprobe是不会执行的,增加 nmissed 计数,以便告知系统发生了什么
// 在这种状态下调用setup_singlestep(p, regs, kcb, 1);设置重入状态的单步行为。
switch (kcb->kprobe_status) {
case KPROBE_HIT_SSDONE:
case KPROBE_HIT_ACTIVE:
kprobes_inc_nmissed_count(p);
setup_singlestep(p, regs, kcb, 1);
break;
// 如果程序正在单步执行,或者 kcb 已经被标记为了,那么执行抛出错误
case KPROBE_HIT_SS:
case KPROBE_REENTER:
pr_warn("Unrecoverable kprobe detected.\n");
dump_kprobe(p);
BUG();
break;
// 其他情况未知,报一个警告。
default:
WARN_ON(1);
return 0;
}
return 1;
}
(6)对应代码中的5号位
static void __kprobes save_previous_kprobe(struct kprobe_ctlblk *kcb)
{
// 保存之前那个 kprobe 的状态。
kcb->prev_kprobe.kp = kprobe_running();
kcb->prev_kprobe.status = kcb->kprobe_status;
}
接着调用 set_current_kprobe 更新当前 kprobe 为重入的 kprobe,并且标记自己状态为 KPROBE_REENTER。
至此,pre_handler 的处理就完成了,并且当该 debug 返回后,首先执行的是 slot 槽中原始指令,并且由于处于单步模式,当原始指令执行完成后,进入单步异常,单步异常入口如上所述是 single_step_handler。
6、single_step_handler
single_step_handler
-> kprobe_single_step_handler
int __kprobes
kprobe_single_step_handler(struct pt_regs *regs, unsigned int esr)
{
struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
int retval;
// 检查当前 cpu 的 kcb 的状态
/* return error if this is not our step */
retval = kprobe_ss_hit(kcb, instruction_pointer(regs));
// ok,kprobe 单步命中,进行 post handler 处理。
if (retval == DBG_HOOK_HANDLED) {
// 恢复原来 pstate 的值。
kprobes_restore_local_irqflag(kcb, regs);
// 清除单步状态,禁用单步
kernel_disable_single_step();
// 开始 post_handler 的处理。
post_kprobe_handler(kcb, regs);
}
return retval;
}
static int __kprobes
kprobe_ss_hit(struct kprobe_ctlblk *kcb, unsigned long addr)
{
// 如果 ss_pending 为 true 则是我们在 pre_handler 中设置的,检查 match_addr。
// 当都满足时,说明kprobe的单步执行命令清理 kcb并返回 DBG_HOOK_HANDLED
if ((kcb->ss_ctx.ss_pending)
&& (kcb->ss_ctx.match_addr == addr)) {
clear_ss_context(kcb); /* clear pending ss */
return DBG_HOOK_HANDLED;
}
/* not ours, kprobes should ignore it */
return DBG_HOOK_ERROR;
}
post_kprobe_handler 在 kprobe 重入中会跳过 pre_handler 执行而直接调用 post_kprobe_handler,以及正常流程的下调用 post_kprobe_handler。
static void __kprobes
post_kprobe_handler(struct kprobe_ctlblk *kcb, struct pt_regs *regs)
{
struct kprobe *cur = kprobe_running();
// 首次检查当前是否是 kprobe 运行,不是则直接返回。
if (!cur)
return;
// 当 restore 不为 0,我们使用了 slot,并在 slot 中执行了指令,现在从 restore 中恢复原来流程的 pc 指针。
/* return addr restore if non-branching insn */
if (cur->ainsn.api.restore != 0)
instruction_pointer_set(regs, cur->ainsn.api.restore);
// 如果状态是 KPROBE_REENTER,说明是kprobe 重入调用的 post_kprobe_handler,我们从 kcb 的prev 中恢复原来的 kprobe,以便于再次执行原来的 kprobe,并且返回,这次重入即可被处理掉。
/* restore back original saved kprobe variables and continue */
if (kcb->kprobe_status == KPROBE_REENTER) {
restore_previous_kprobe(kcb);
return;
}
// 正常流程,标记状态为单步完成,调用 post_handler。
/* call post handler */
kcb->kprobe_status = KPROBE_HIT_SSDONE;
if (cur->post_handler) {
/* post_handler can hit breakpoint and single step
* again, so we enable D-flag for recursive exception.
*/
cur->post_handler(cur, regs, 0);
}
// kprobe 执行完成,清除 cur_kprobe, 表示 kprobe 没有运行了
reset_current_kprobe();
}
至此 pre_handler 和 post _handler 的处理就完成了。
最后还剩 fault_handler。
该逻辑比较简单代码如下:
do_page_fault
-> notify_page_fault
-> if (kprobe_running() && kprobe_fault_handler(regs, esr))
int __kprobes kprobe_fault_handler(struct pt_regs *regs, unsigned int fsr)
{
struct kprobe *cur = kprobe_running();
struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
switch (kcb->kprobe_status) {
case KPROBE_HIT_SS:
case KPROBE_REENTER:
// 如果是单步执行触发的 page_fault,这是正常情况,重新指向该地址,由 do_page_fault 流程处理也错误。
instruction_pointer_set(regs, (unsigned long) cur->addr);
if (!instruction_pointer(regs))
BUG();
...
case KPROBE_HIT_ACTIVE:
case KPROBE_HIT_SSDONE:
// 如果是 pre_handler 和 post_handler 触发的 page_fault,则调用 fault_handler
if (cur->fault_handler && cur->fault_handler(cur, regs, fsr))
return 1;
// 如果没有 fault_handler,那么检查错误是否是在 __asm_extable 段中,进行最后可能的处理。
if (fixup_exception(regs))
return 1;
}
return 0;
}
// 返回 1 说明由 fault_handler 或者 fixup_exception 处理了该错误,do_page_fault 不需要做进一步处理,直接返回。
// 返回 0 错误没有得到处理,交由 do_page_fault 进行处理。
7、unregister_kprobe
unregister_kprobe
-> unregister_kprobes
void unregister_kprobes(struct kprobe **kps, int num)
{
int i;
if (num <= 0)
return;
mutex_lock(&kprobe_mutex);
for (i = 0; i < num; i++)
if (__unregister_kprobe_top(kps[i]) < 0)
kps[i]->addr = NULL;
mutex_unlock(&kprobe_mutex);
synchronize_rcu();
for (i = 0; i < num; i++)
if (kps[i]->addr)
__unregister_kprobe_bottom(kps[i]);
}
卸载分为两个步骤,分别是 __unregister_kprobe_top 和 __unregister_kprobe_bottom,
// 负责从链表中卸载该 kprobe
__unregister_kprobe_top
-> __disable_kprobe // 标记该 kprobe 为禁用状态,后续该 kprobe 不会被执行,方便卸载
-> arch_disarm_kprobe
// 恢复原始地址的 insn
-> patch_text(p->addr, p->opcode);
-> list_del_rcu
-> hlist_del_rcu
// 调用 synchronize_rcu 确保所有对当前 kprobe的访问结束,之后可以安全的移除释放该 kprobe
synchronize_rcu
// 释放该 kprobe 的数据,恢复原始指令。
__unregister_kprobe_bottom
-> free_aggr_kprobe
-> arch_remove_kprobe
-> free_insn_slot
-> kfree(kprobe)
8、register_kretprobe
kprobe 可以在内核的地址附加探测点来触发 pre_handler等回调的执行。kretprobe 则是在附加地址对应函数的返回处附加hook,可以在函数返回时被调用。它的实现机制是基于 kprobe的,原理是在要探测点注册一个 kprobe,该kprobe是内核定义的,内核会挂接自己定义的 pre_handler,一旦探测点执行,则是执行自定义的kprobe 的 pre_handler,,在 pre_handler 中手动捕获并保存当前位置的返回地址,并修改返回地址为一个跳板函数,跳板函数执行我们kretprobe中注册的handler,并在完成调用后恢复原始的返回地址,完成一个 kretprobe 的触发。
kretprobe 注册的数据结构:
struct kretprobe {
// kretporbe 注册内部构造的 kprobe,使用它来在 pre_handler 阶段修改函数返回地址为kretprobe的跳板地址
struct kprobe kp;
kretprobe_handler_t handler; // 函数返回时调用的 handler
kretprobe_handler_t entry_handler; // 当 kprobe 中的 pre_handler 触发时在 pre_handler 中调用 entry_handler
int maxactive; // kretprobe 可能多个 cpu 都会进入该流程,maxactive 限制最大同时能有多少个回调。
int nmissed; // 一些情况不会调用回调,记录触发后未调用的次数
size_t data_size; // 未使用?
struct hlist_head free_instances; // 上述根据 maxactive 大小,会实例化 struct kretprobe_instance 结构体,每一个 kretprobe_instance 是一个 kretprobe 的实例。
raw_spinlock_t lock;
};
// 记录一个 kretprobe 触发实例
struct kretprobe_instance {
struct hlist_node hlist; // 每一个正在使用的实例会存放到全局 hashtable 中
struct kretprobe *rp; // 实列对应自己所属的 kprobe
kprobe_opcode_t *ret_addr; // 实例对应的真实返回地址
struct task_struct *task; // 实例对应当前触发的 task,通过 task 计算 hash,在跳板函数中可以拿到自己对应实例
char data[0]; // ?
};
int register_kretprobe(struct kretprobe *rp)
{
...
// 检查探测点是不是一个合理的函数入口
if (!kprobe_on_func_entry(rp->kp.addr, rp->kp.symbol_name, rp->kp.offset))
return -EINVAL;
// 检查地址是否在黑名单中, 在则不能 hook
if (kretprobe_blacklist_size) {
addr = kprobe_addr(&rp->kp);
if (IS_ERR(addr))
return PTR_ERR(addr);
for (i = 0; kretprobe_blacklist[i].name != NULL; i++) {
if (kretprobe_blacklist[i].addr == addr)
return -EINVAL;
}
}
// 为内部构造的 kprobe 注册一个 pre_handler,该 pre_handler 将会去修改返回地址以此构造一个返回回调。
rp->kp.pre_handler = pre_handler_kretprobe;
rp->kp.post_handler = NULL;
rp->kp.fault_handler = NULL;
...
// 根据 maxactive 实例化 kretprobe_instance,每触发一次 kretprobe 使用一个 kretprobe_instance,最大同时可以有 maxactive 个进行处理。
/* Pre-allocate memory for max kretprobe instances */
if (rp->maxactive <= 0) {
#ifdef CONFIG_PREEMPT
rp->maxactive = max_t(unsigned int, 10, 2*num_possible_cpus());
#else
rp->maxactive = num_possible_cpus();
#endif
}
raw_spin_lock_init(&rp->lock);
INIT_HLIST_HEAD(&rp->free_instances);
for (i = 0; i < rp->maxactive; i++) {
inst = kmalloc(sizeof(struct kretprobe_instance) +
rp->data_size, GFP_KERNEL);
if (inst == NULL) {
free_rp_inst(rp);
return -ENOMEM;
}
INIT_HLIST_NODE(&inst->hlist);
hlist_add_head(&inst->hlist, &rp->free_instances);
}
...
// 将 kretprobe 内部构造的 kprobe 注册到系统中。
/* Establish function entry probe point */
ret = register_kprobe(&rp->kp);
}
从上可知,一切的逻辑在 kretprobe 中注册的 pre_handler。
当 kretprobe 中定义的探测点触发时调用 pre_handler:
static int pre_handler_kretprobe(struct kprobe *p, struct pt_regs *regs)
{
struct kretprobe *rp = container_of(p, struct kretprobe, kp);
...
// 如果当前上下文在 nmi 中,为了防止死锁,是不会进行 hook 的,只增加nmissed计数,让用户知道发生了什么。
if (unlikely(in_nmi())) {
rp->nmissed++;
return 0;
}
/* TODO: consider to only swap the RA after the last pre_handler fired */
// 将 current 做 hash,然后从 rp 对应的 instance 中取出一个实例使用,该实例以这个 hash 为 key 值存放到全局 hash 中,后续跳板函数,根据 task hash 得到该实例,做进一步处理。
hash = hash_ptr(current, KPROBE_HASH_BITS);
raw_spin_lock_irqsave(&rp->lock, flags);
// 如果 rp->free_instances 为空,则没有可用实例了,增加 nmissed ,并直接返回。
if (!hlist_empty(&rp->free_instances)) {
// 从链表中取出一个实例使用。
ri = hlist_entry(rp->free_instances.first,
struct kretprobe_instance, hlist);
hlist_del(&ri->hlist);
raw_spin_unlock_irqrestore(&rp->lock, flags);
ri->rp = rp;
ri->task = current;
// 在这里调用我们自己定义的 entry_handler,在进入函数之前调用。
if (rp->entry_handler && rp->entry_handler(ri, regs)) {
// 返回不为 0,说明有错,直接归还实例,并返回。
raw_spin_lock_irqsave(&rp->lock, flags);
hlist_add_head(&ri->hlist, &rp->free_instances);
raw_spin_unlock_irqrestore(&rp->lock, flags);
return 0;
}
// 开始预备返回 hook。
arch_prepare_kretprobe(ri, regs);
/* XXX(hch): why is there no hlist_move_head? */
INIT_HLIST_NODE(&ri->hlist);
kretprobe_table_lock(hash, &flags);
// 将实例根据 task hash 加入到全局 kretprobe_inst_table 中,后续返回处使用。
hlist_add_head(&ri->hlist, &kretprobe_inst_table[hash]);
kretprobe_table_unlock(hash, &flags);
} else {
rp->nmissed++;
raw_spin_unlock_irqrestore(&rp->lock, flags);
}
return 0;
}
// 对于 arm64 预备一个返回 hook,将实际的返回地址保存到 ret_addr 中,修改现有返回地址为自定义的跳板函数地址 kretprobe_trampoline。
void __kprobes arch_prepare_kretprobe(struct kretprobe_instance *ri,
struct pt_regs *regs)
{
ri->ret_addr = (kprobe_opcode_t *)regs->regs[30];
/* replace return addr (x30) with trampoline */
regs->regs[30] = (long)&kretprobe_trampoline;
}
ENTRY(kretprobe_trampoline)
sub sp, sp, #S_FRAME_SIZE
// 保存返回之前的寄存器信息,用于后续调用 trampoline_probe_handler 之后恢复原始的 pstate,栈信息。
save_all_base_regs
mov x0, sp
// 执行自定义的kretprobe 处理函数
bl trampoline_probe_handler
// 将 trampoline_probe_handler 的返回作为 lr 的新值,恢复原本的返回路径。
/*
* Replace trampoline address in lr with actual orig_ret_addr return
* address.
*/
mov lr, x0
// 恢复原始的 pstate,栈信息。
restore_all_base_regs
add sp, sp, #S_FRAME_SIZE
ret
ENDPROC(kretprobe_trampoline)
void __kprobes __used *trampoline_probe_handler(struct pt_regs *regs)
{
// 可能有多个 cpu 进入该处,通过 current 计算 hash 得到实际任务对应的 kretprobe 实例 rp。
hlist_for_each_entry_safe(ri, tmp, head, hlist) {
if (ri->task != current)
/* another task is sharing our hash bucket */
continue;
orig_ret_address = (unsigned long)ri->ret_addr;
if (orig_ret_address != trampoline_address)
/*
* This is the real return address. Any other
* instances associated with this task are for
* other calls deeper on the call stack
*/
break;
}
...
correct_ret_addr = ri->ret_addr;
hlist_for_each_entry_safe(ri, tmp, head, hlist) {
if (ri->task != current)
/* another task is sharing our hash bucket */
continue;
orig_ret_address = (unsigned long)ri->ret_addr;
if (ri->rp && ri->rp->handler) {
__this_cpu_write(current_kprobe, &ri->rp->kp);
get_kprobe_ctlblk()->kprobe_status = KPROBE_HIT_ACTIVE;
ri->ret_addr = correct_ret_addr;
// 调用kreprobe中注册的 handler,在函数返回前夕调用。
ri->rp->handler(ri, regs);
__this_cpu_write(current_kprobe, NULL);
}
// 将该实例返回 rp->free_instance,本次已处理完毕。
recycle_rp_inst(ri, &empty_rp);
if (orig_ret_address != trampoline_address)
/*
* This is the real return address. Any other
* instances associated with this task are for
* other calls deeper on the call stack
*/
break;
}
// 返回真实的返回地址。
return (void *)orig_ret_address;
}