Linux 内核动态追踪技术的实现

系统 Linux
kprobe 是内核提供的动态追踪技术机制,它允许动态安装内核模块的方式安装系统钩子,非常强大。下面先看一个内核中的例子。

[[434928]]

之前的文章介绍了基于 tracepoint 静态追踪技术的实现,本文再介绍基于 kprobe 的动态追踪即使的实现。同样,动态追踪也是排查问题的利器。

kprobe 是内核提供的动态追踪技术机制,它允许动态安装内核模块的方式安装系统钩子,非常强大。下面先看一个内核中的例子。

  1. #include <linux/kernel.h> 
  2. #include <linux/module.h> 
  3. #include <linux/kprobes.h> 
  4.  
  5. #define MAX_SYMBOL_LEN  64 
  6. // 要 hanck 的内核函数名 
  7. static char symbol[MAX_SYMBOL_LEN] = "_do_fork"
  8. module_param_string(symbol, symbol, sizeof(symbol), 0644); 
  9. static struct kprobe kp = { 
  10.     .symbol_name    = symbol, 
  11. }; 
  12.  
  13. // 执行系统函数前被执行的钩子 
  14. static int __kprobes handler_pre(struct kprobe *p, struct pt_regs *regs){ 
  15.     // ... 
  16.  
  17. // 执行系统函数的单条指令后执行的钩子(不是执行完系统函数) 
  18. static void __kprobes handler_post(struct kprobe *p, struct pt_regs *regs, 
  19.                 unsigned long flags){ 
  20.     // ... 
  21.  
  22. // 钩子执行出错或者单条执行执行出错时被执行函数static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr){ 
  23.     // ... 
  24.  
  25. static int __init kprobe_init(void){ 
  26.     int ret; 
  27.     // 设置钩子 
  28.     kp.pre_handler = handler_pre; 
  29.     kp.post_handler = handler_post; 
  30.     kp.fault_handler = handler_fault; 
  31.     // 安装钩子 
  32.     register_kprobe(&kp); 
  33.     return 0; 
  34.  
  35. static void __exit kprobe_exit(void){ 
  36.     unregister_kprobe(&kp); 
  37.     pr_info("kprobe at %p unregistered\n", kp.addr); 
  38.  
  39. // 安装进内核后的初始化和注销函数 
  40. module_init(kprobe_init) 
  41. module_exit(kprobe_exit) 
  42. MODULE_LICENSE("GPL"); 

设置完 kprobe 后,通过 register_kprobe 注册到内核。

  1. int register_kprobe(struct kprobe *p){ 
  2.     int ret; 
  3.     struct kprobe *old_p; 
  4.     struct module *probed_mod; 
  5.     kprobe_opcode_t *addr; 
  6.  
  7.     // 通过系统函数名找到对应的地址,内核维护了这个数据 
  8.     addr = kprobe_addr(p); 
  9.     // 记录这个地址 
  10.     p->addr = addr; 
  11.     p->flags &= KPROBE_FLAG_DISABLED; 
  12.     p->nmissed = 0; 
  13.     INIT_LIST_HEAD(&p->list); 
  14.     // 之前是否已经存在钩子,是的话就插入存在的列表,否则插入一个新的记录 
  15.     old_p = get_kprobe(p->addr); 
  16.     if (old_p) { 
  17.         /* Since this may unoptimize old_p, locking text_mutex. */ 
  18.         ret = register_aggr_kprobe(old_p, p); 
  19.         goto out
  20.     } 
  21.     // 把被 hack 的系统函数的指令保存到 probe 结构体,因为下面要覆盖这块内存 
  22.     /* 
  23.         prepare_kprobe => 
  24.             unsigned long addr = (unsigned long) p->addr; 
  25.             unsigned long *kprobe_addr = (unsigned long *)(addr & ~0xFULL); 
  26.             memcpy(&p->opcode, kprobe_addr, sizeof(kprobe_opcode_t)); 
  27.             memcpy(p->ainsn.insn, kprobe_addr, sizeof(kprobe_opcode_t)); 
  28.     */ 
  29.     ret = prepare_kprobe(p); 
  30.  
  31.     INIT_HLIST_NODE(&p->hlist); 
  32.     // 插入内核维护的哈希表 
  33.     hlist_add_head_rcu(&p->hlist, 
  34.                &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]); 
  35.     // hack 掉系统函数所在内存的内容 
  36.     arm_kprobe(p); 

注册一个 probe,首先是通过被 hack 的函数名找到对应的地址,然后保存这个地址对应内存的信息,接着把 probe 插入哈希表,最后调用 arm_kprobe 函数 hack 掉系统函数所在内存的内容。看一下 arm_kprobe。

  1. void arch_arm_kprobe(struct kprobe *p){ 
  2.     // #define INT3_INSN_OPCODE 0xCC 
  3.     u8 int3 = INT3_INSN_OPCODE; 
  4.     // 把 int3 的内存复制到 addr 
  5.     text_poke(p->addr, &int3, 1); 
  6.     text_poke_sync(); 
  7.     perf_event_text_poke(p->addr, &p->opcode, 1, &int3, 1); 

0xCC 是 intel 架构下 int3 对应的指令。所以这里就是把被 hack 函数对应指令的前面部分改成 int3。完成 hack。当执行到系统函数的时候,就会执行 int3,从而触发 trap,并执行对应的处理函数 do_int3(这里比较复杂,我也没有深入分析,大概是这个流程)。

  1. static bool do_int3(struct pt_regs *regs){ 
  2.     kprobe_int3_handler(regs);}int kprobe_int3_handler(struct pt_regs *regs){ 
  3.     kprobe_opcode_t *addr; 
  4.     struct kprobe *p; 
  5.     struct kprobe_ctlblk *kcb; 
  6.     addr = (kprobe_opcode_t *)(regs->ip - sizeof(kprobe_opcode_t)); 
  7.  
  8.     kcb = get_kprobe_ctlblk(); 
  9.     // 通过地址从 probe  哈希表拿到对应的 probe 结构体 
  10.     p = get_kprobe(addr); 
  11.  
  12.     set_current_kprobe(p, regs, kcb); 
  13.     kcb->kprobe_status = KPROBE_HIT_ACTIVE; 
  14.  
  15.     // 执行 pre_handler 钩子  
  16.     if (!p->pre_handler || !p->pre_handler(p, regs)) 
  17.         setup_singlestep(p, regs, kcb, 0); 

执行完。pre_handler 钩子后,会通过 setup_singlestep 设置单步执行 flag。

  1. static void setup_singlestep(struct kprobe *p, struct pt_regs *regs, 
  2.                  struct kprobe_ctlblk *kcb, int reenter){ 
  3.     // 修改寄存器的值 
  4.     // 设置 eflags 寄存器的 tf 位,允许单步调试 
  5.     regs->flags |= X86_EFLAGS_TF; 
  6.     regs->flags &= ~X86_EFLAGS_IF; 
  7.     // 设置下一条指令为系统函数的指令 
  8.     if (p->opcode == INT3_INSN_OPCODE) 
  9.         regs->ip = (unsigned long)p->addr; 
  10.     else 
  11.         regs->ip = (unsigned long)p->ainsn.insn; 

setup_singlestep 首先设置了允许单步调试,也就是说执行下一条指令后会触发一个 trap,从而执行一个处理函数。并设置了下一条指令为被 hack 函数对应的指令,这是在注册 probe 时保存下来的。触发单步调试的 trap 后,最终会执行到 kprobe_debug_handler

  1. int kprobe_debug_handler(struct pt_regs *regs){ 
  2.     struct kprobe *cur = kprobe_running(); 
  3.     struct kprobe_ctlblk *kcb = get_kprobe_ctlblk(); 
  4.     // 恢复指令为系统函数的指令 
  5.     resume_execution(cur, regs, kcb); 
  6.     regs->flags |= kcb->kprobe_saved_flags; 
  7.     // 执行 post 钩子 
  8.     if ((kcb->kprobe_status != KPROBE_REENTER) && cur->post_handler) { 
  9.         kcb->kprobe_status = KPROBE_HIT_SSDONE; 
  10.         cur->post_handler(cur, regs, 0); 
  11.     } 

在单步调试的 trap 处理函数中,会执行 post 钩子,并恢复真正的系统函数执行。这就完成了整个过程。

我们可以看到 kprobe 可以在系统函数执行前执行我们的钩子,另外内核还提供了另外一个机制 kretprobe 用于在系统函数执行后返回前安装钩子。下面通过一个例子大致看一下 kretprobe。

  1. struct my_data { 
  2.     ktime_t entry_stamp; 
  3. }; 
  4.  
  5. // 记录函数执行开始时间 
  6. static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs){ 
  7.     struct my_data *data; 
  8.     data = (struct my_data *)ri->data; 
  9.     data->entry_stamp = ktime_get(); 
  10.     return 0; 
  11.  
  12. // 记录函数执行结束时间 
  13. static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs){ 
  14.     unsigned long retval = regs_return_value(regs); 
  15.     struct my_data *data = (struct my_data *)ri->data; 
  16.     s64 delta; 
  17.     ktime_t now; 
  18.  
  19.     now = ktime_get(); 
  20.     delta = ktime_to_ns(ktime_sub(now, data->entry_stamp)); 
  21.     return 0; 
  22.  
  23. static struct kretprobe my_kretprobe = { 
  24.     // 函数返回前执行 
  25.     .handler        = ret_handler, 
  26.     // 函数开始前执行 
  27.     .entry_handler      = entry_handler, 
  28.     .data_size      = sizeof(struct my_data), 
  29.     /* Probe up to 20 instances concurrently. */ 
  30.     .maxactive      = 20, 
  31. }; 
  32.  
  33. static char func_name[NAME_MAX] = "_do_fork"
  34. module_param_string(func, func_name, NAME_MAX, S_IRUGO); 
  35. my_kretprobe.kp.symbol_name = func_name; 
  36. // 注册 
  37. register_kretprobe(&my_kretprobe); 

我们可以看到可以通过 kretprobe 计算系统函数的耗时。kretprobe 是基于 kprobe 实现的,主要逻辑是通过通过 kprobe 注册一个 pre_handler,在 pre_handler 中 hack 掉函数的栈,因为函数执行时,返回地址是存在栈中的,把这个内存改成一段内核的代码,等到函数执行完后,弹出返回地址时,就会执行内核 hack 的代码,从而执行我们的钩子,执行完后再跳回到真正的返回地址继续执行。

 

总结:内核通过劫持的方式实现了 kprobe,基于 kprobe 的动态追踪技术可谓是非常复杂而强大,我们可以利用这个机制,动态修改逻辑,收集信息。不过实现过于复杂,涉及到对 CPU 架构和内存模型的了解,本文也是大致分析了一下流程,有兴趣的同学可以自行查看源码。

 

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2021-11-14 07:29:55

Linux 内核静态追踪Linux 系统

2016-12-08 09:57:09

LinuxDTrace技术

2022-03-03 18:18:53

BPF解释器系统

2023-03-01 23:53:30

Linuxshutdown进程

2023-03-10 14:56:37

Linuxconnect系统

2023-03-01 23:56:11

2021-10-06 09:46:17

trace-cmd追踪内核Linux

2023-02-28 09:47:42

2014-04-01 16:52:10

SUSEkGraftLinux内核

2023-11-24 11:24:16

Linux系统

2021-09-30 09:43:11

Linux内核Zstd补丁

2017-01-12 19:15:03

Linux内核调试自构proc

2022-05-24 12:34:32

Docker容器Linux容器进程

2020-08-23 18:10:16

机器狗人工智能系统

2018-06-04 16:20:56

Linux动态路由Quagga

2013-09-24 10:48:32

Google追踪技术Cookies

2013-09-25 17:02:50

追踪技术CookiesGoogle

2022-05-23 08:23:24

链路追踪SleuthSpring

2021-07-15 11:35:27

Python自动追踪代码

2013-11-05 09:58:39

Linux内核
点赞
收藏

51CTO技术栈公众号