kprobe 内核实现原理

编程入门 行业动态 更新时间:2024-10-23 23:34:42

kprobe <a href=https://www.elefans.com/category/jswz/34/1769575.html style=内核实现原理"/>

kprobe 内核实现原理

kprobe是linux内核的一个重要的特性,是其他内核调试工具(perf,systemtap)的基础设施,同时内核BPF也是依赖于kprobe。

Kprobe结构体

< include/linux/kprobe.h >

struct kprobe {struct hlist_node hlist;        /* 所有注册的kprobe都会添加到kprobe_table哈希表中,hlist成员用来链接到某个槽位中 *//* list of kprobes for multi-handler support */struct list_head list;                   /* 链接一个地址上注册的多个kprobe *//*count the number of times this probe was temporarily disarmed */unsigned long nmissed;         * 记录当前的probe没有被处理的次数 *//* location of the probe point */kprobe_opcode_t *addr;                /* 探测点地址 *//* Allow user to indicate symbol name of the probe point */const char *symbol_name;          /* 探测点函数名 *//* Offset into the symbol */unsigned int offset;                        /* 探测点在函数内的偏移 *//* Called before addr is executed. */kprobe_pre_handler_t pre_handler;           /* 在单步执行原始的指令前会被调用 *//* Called after addr is executed, unless... */kprobe_post_handler_t post_handler;         /* 在单步执行原始的指令后会被调用 *//* Saved opcode (which has been replaced with breakpoint) */kprobe_opcode_t opcode;/* copy of the original instruction */struct arch_specific_insn ainsn;                /* 保存平台相关的被探测指令和下一条指令 *//** Indicates various status flags.* Protected by kprobe_mutex after this kprobe is registered.*/u32 flags;                                          /* 状态标记 */
};

源码分析

init_kprobes()

初始化入口

< kernel/kprobe.c >

static int __init init_kprobes(void)
{int i, err = 0;/* FIXME allocate the probe table, currently defined statically *//* initialize all list heads */for (i = 0; i < KPROBE_TABLE_SIZE; i++)INIT_HLIST_HEAD(&kprobe_table[i]);                    /* 初始化用于存储 kprobe 模块的哈希表 */err = populate_kprobe_blacklist(__start_kprobe_blacklist,__stop_kprobe_blacklist);if (err)pr_err("Failed to populate blacklist (error %d), kprobes not restricted, be careful using them!\n", err);if (kretprobe_blacklist_size) {/* lookup the function address from its name */for (i = 0; kretprobe_blacklist[i].name != NULL; i++) {kretprobe_blacklist[i].addr =kprobe_lookup_name(kretprobe_blacklist[i].name, 0);if (!kretprobe_blacklist[i].addr)pr_err("Failed to lookup symbol '%s' for kretprobe blacklist. Maybe the target function is removed or renamed.\n",kretprobe_blacklist[i].name);}}/* By default, kprobes are armed */kprobes_all_disarmed = false;#if defined(CONFIG_OPTPROBES) && defined(__ARCH_WANT_KPROBES_INSN_SLOT)/* Init 'kprobe_optinsn_slots' for allocation */kprobe_optinsn_slots.insn_size = MAX_OPTINSN_SIZE;
#endiferr = arch_init_kprobes();                                         /* 初始化CPU架构相关 */if (!err)err = register_die_notifier(&kprobe_exceptions_nb);           /* 注册die通知链*/if (!err)err = register_module_notifier(&kprobe_module_nb);         /* 注册模块通知链 */kprobes_initialized = (err == 0);kprobe_sysctls_init();return err;
}

注册kprobe总体流程

以arm为例:

register_kprobe() -> arm_kprobe() -> __arm_kprobe() -> arch_arm_kprobe()

register_kprobe()

< kernel/kprobe.c >

int register_kprobe(struct kprobe *p)
{int ret;struct kprobe *old_p;struct module *probed_mod;kprobe_opcode_t *addr;bool on_func_entry;/* Adjust probe address from symbol */addr = _kprobe_addr(p->addr, p->symbol_name, p->offset, &on_func_entry);if (IS_ERR(addr))return PTR_ERR(addr);p->addr = addr;ret = warn_kprobe_rereg(p);        /* 防止同一个kprobe被重复注册 */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);/* 1. 判断被注册的函数是否位于内核的代码段内,或位于不能探测的kprobe实现路径中 * 2. 判断被探测的地址是否属于某一个模块,并且位于模块的text section内* 3. 如果被探测的地址位于模块的init地址段内,但该段代码区间已被释放,则直接退出 */ret = check_kprobe_address_safe(p, &probed_mod);if (ret)return ret;mutex_lock(&kprobe_mutex);if (on_func_entry)p->flags |= KPROBE_FLAG_ON_FUNC_ENTRY;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);                              /* 保存被跟踪指令的值 */mutex_unlock(&text_mutex);cpus_read_unlock();if (ret)goto out;INIT_HLIST_NODE(&p->hlist);hlist_add_head_rcu(&p->hlist,&kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);               /* 将kprobe加入到相应的hash表内 */if (!kprobes_all_disarmed && !kprobe_disabled(p)) {ret = arm_kprobe(p);                                          /* 将探测点的指令码修改为arm_kprobe */if (ret) {hlist_del_rcu(&p->hlist);synchronize_rcu();goto out;}}/* Try to optimize kprobe */try_to_optimize_kprobe(p);
out:mutex_unlock(&kprobe_mutex);if (probed_mod)module_put(probed_mod);return ret;
}

arm_kprobe()

< kernel/kprobes.c >

static int arm_kprobe(struct kprobe *kp)
{if (unlikely(kprobe_ftrace(kp)))return arm_kprobe_ftrace(kp);cpus_read_lock();mutex_lock(&text_mutex);__arm_kprobe(kp);mutex_unlock(&text_mutex);cpus_read_unlock();return 0;
}

__arm_kprobe()

< kernel/kprobes.c >

/* Put a breakpoint for a probe. */
static void __arm_kprobe(struct kprobe *p)
{struct kprobe *_p;lockdep_assert_held(&text_mutex);/* Find the overlapping optimized kprobes. */_p = get_optimized_kprobe(p->addr);if (unlikely(_p))/* Fallback to unoptimized kprobe */unoptimize_kprobe(_p, true);arch_arm_kprobe(p);                                  /* 替换探测点指令为BRK64_OPCODE_KPROBES指令 */optimize_kprobe(p);	/* Try to optimize (add kprobe to a list) */
}

在函数arch_arm_kprobe(p);之前,都是通用的注册流程,然后就是和体系结构相关的实现了

prepare_kprobe()

< kernel/kprobes.c >

static int prepare_kprobe(struct kprobe *p)
{/* Must ensure p->addr is really on ftrace */if (kprobe_ftrace(p))return arch_prepare_kprobe_ftrace(p);return arch_prepare_kprobe(p);
}

arch_prepare_kprobe()

< arch/arm64/kernel/kprobes/kprobes.c >

int __kprobes arch_prepare_kprobe(struct kprobe *p)
{unsigned long probe_addr = (unsigned long)p->addr;if (probe_addr & 0x3)return -EINVAL;/* copy instruction */p->opcode = le32_to_cpu(*p->addr);if (search_exception_tables(probe_addr))return -EINVAL;/* decode instruction */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);                     /* 将指令存放到slot中,记录下一条指令到p->ainsn.api.insn */elsearch_prepare_simulate(p);return 0;
}

arch_arm_kprobe()

< arch/arm64/kernel/kprobes/kprobes.c >

/* arm kprobe: install breakpoint in text */
void __kprobes arch_arm_kprobe(struct kprobe *p)
{void *addr = p->addr;                                              /* 原地址 */u32 insn = BRK64_OPCODE_KPROBES;          /* 替换后的指令 */aarch64_insn_patch_text(&addr, &insn, 1);
}

X86系统实现方式

x86系统kprobe是通过arch_arm_kprobe实现最终注册功能

这里留意下细节,名字里带着arm,难怪搜arch_x86_kprobe 搜不到

arch_arm_kprobe()

< arch/x86/kernel/kprobes/core.c >

void arch_arm_kprobe(struct kprobe *p)
{u8 int3 = INT3_INSN_OPCODE;        /* 替换后的指令 */text_poke(p->addr, &int3, 1);text_poke_sync();perf_event_text_poke(p->addr, &p->opcode, 1, &int3, 1);
}

这个函数主要作用是要安装断点,这里安装断点的原理和GDB断点应该是一样的,

GDB安装断点的原理

在执行gdb ./test后,gdb就会fork出一个子进程,这个子进程首先调用ptrace然后执test程序,这样就准备好调试环境了。

gdb在断点中执行两件事

  1. 对源码所对应断点处代码存储到断点链表中。

  2. 然后插入中断指令INT3,也就是说:汇编代码中的原来指令被替换为INT3。

addr对应位置的指令修改为INT3指令,进程在执行时,发现是INT3指令,于是操作系统就发送一个SIGTRAP信号给test进程。

操作系统发给test的任何信号,都被gdb接管了,也就是说gdb会首先接收到这SIGTRAP个信号,gdb发现当前汇编代码执行的是INT3指令,

于是gdb又做了2个操作:

  1. 将INT3指令替换为断点处注册的执行函数,执行完成后,再执行被INT3替换的指令。

  2. x86系统,INT3_INSN_OPCODE是要替换的指令,然后进一步替换为要执行的函数地址

这些都执行完成后,再执行最初被替换的指令

触发kprobe探测和回调

debug_traps_init()

kprobe的触发和处理是通过brk exception和single step单步exception执行的

< arch/arm/kernel/debug-monitors.c >

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");
}

kprobe_handler()

< arch/arm64/kernel/kprobes/kprobes.c >

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 = 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);kcb->kprobe_status = KPROBE_HIT_ACTIVE;/** If we have no pre-handler or it returned 0, we* continue with normal processing.  If we have a* pre-handler and it returned non-zero, it will* modify the execution path and no need to single* stepping. Let's just reset current kprobe and exit.*/if (!p->pre_handler || !p->pre_handler(p, regs)) {setup_singlestep(p, regs, kcb, 0);} elsereset_current_kprobe();}}/** The breakpoint instruction was removed right* after we hit it.  Another cpu has removed* either a probepoint or a debugger breakpoint* at this address.  In either case, no further* handling of this interrupt is appropriate.* Return back to original instruction, and continue.*/
}

kprobe的使用方法

方法一:

要编写一个 kprobe 内核模块。

方法二:

基于Ftrace的/sys/kernel/debug/tracing/kprobe_events接口。

方法三:

通过perf工具。

方法四:

使用eBPF。

更多推荐

kprobe 内核实现原理

本文发布于:2023-11-16 08:08:54,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1614679.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:内核   原理   kprobe

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!