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

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

[[434928]]

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

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

#include <linux/kernel.h> 
#include <linux/module.h> 
#include <linux/kprobes.h> 
 
#define MAX_SYMBOL_LEN  64 
// 要 hanck 的内核函数名 
static char symbol[MAX_SYMBOL_LEN] = "_do_fork"
module_param_string(symbol, symbol, sizeof(symbol), 0644); 
static struct kprobe kp = { 
    .symbol_name    = symbol, 
}; 
 
// 执行系统函数前被执行的钩子 
static int __kprobes handler_pre(struct kprobe *p, struct pt_regs *regs){ 
    // ... 

 
// 执行系统函数的单条指令后执行的钩子(不是执行完系统函数) 
static void __kprobes handler_post(struct kprobe *p, struct pt_regs *regs, 
                unsigned long flags){ 
    // ... 

 
// 钩子执行出错或者单条执行执行出错时被执行函数static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr){ 
    // ... 

 
static int __init kprobe_init(void){ 
    int ret; 
    // 设置钩子 
    kp.pre_handler = handler_pre; 
    kp.post_handler = handler_post; 
    kp.fault_handler = handler_fault; 
    // 安装钩子 
    register_kprobe(&kp); 
    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"); 
  • 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.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.

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

int register_kprobe(struct kprobe *p){ 
    int ret; 
    struct kprobe *old_p; 
    struct module *probed_mod; 
    kprobe_opcode_t *addr; 
 
    // 通过系统函数名找到对应的地址,内核维护了这个数据 
    addr = kprobe_addr(p); 
    // 记录这个地址 
    p->addr = addr; 
    p->flags &= KPROBE_FLAG_DISABLED; 
    p->nmissed = 0; 
    INIT_LIST_HEAD(&p->list); 
    // 之前是否已经存在钩子,是的话就插入存在的列表,否则插入一个新的记录 
    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
    } 
    // 把被 hack 的系统函数的指令保存到 probe 结构体,因为下面要覆盖这块内存 
    /* 
        prepare_kprobe => 
            unsigned long addr = (unsigned long) p->addr; 
            unsigned long *kprobe_addr = (unsigned long *)(addr & ~0xFULL); 
            memcpy(&p->opcode, kprobe_addr, sizeof(kprobe_opcode_t)); 
            memcpy(p->ainsn.insn, kprobe_addr, sizeof(kprobe_opcode_t)); 
    */ 
    ret = prepare_kprobe(p); 
 
    INIT_HLIST_NODE(&p->hlist); 
    // 插入内核维护的哈希表 
    hlist_add_head_rcu(&p->hlist, 
               &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]); 
    // hack 掉系统函数所在内存的内容 
    arm_kprobe(p); 

  • 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.

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

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

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

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

static bool do_int3(struct pt_regs *regs){ 
    kprobe_int3_handler(regs);}int kprobe_int3_handler(struct pt_regs *regs){ 
    kprobe_opcode_t *addr; 
    struct kprobe *p; 
    struct kprobe_ctlblk *kcb; 
    addr = (kprobe_opcode_t *)(regs->ip - sizeof(kprobe_opcode_t)); 
 
    kcb = get_kprobe_ctlblk(); 
    // 通过地址从 probe  哈希表拿到对应的 probe 结构体 
    p = get_kprobe(addr); 
 
    set_current_kprobe(p, regs, kcb); 
    kcb->kprobe_status = KPROBE_HIT_ACTIVE; 
 
    // 执行 pre_handler 钩子  
    if (!p->pre_handler || !p->pre_handler(p, regs)) 
        setup_singlestep(p, regs, kcb, 0); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

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

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

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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

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

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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

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

struct my_data { 
    ktime_t entry_stamp; 
}; 
 
// 记录函数执行开始时间 
static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs){ 
    struct my_data *data; 
    data = (struct my_data *)ri->data; 
    data->entry_stamp = ktime_get(); 
    return 0; 

 
// 记录函数执行结束时间 
static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs){ 
    unsigned long retval = regs_return_value(regs); 
    struct my_data *data = (struct my_data *)ri->data; 
    s64 delta; 
    ktime_t now; 
 
    now = ktime_get(); 
    delta = ktime_to_ns(ktime_sub(now, data->entry_stamp)); 
    return 0; 

 
static struct kretprobe my_kretprobe = { 
    // 函数返回前执行 
    .handler        = ret_handler, 
    // 函数开始前执行 
    .entry_handler      = entry_handler, 
    .data_size      = sizeof(struct my_data), 
    /* Probe up to 20 instances concurrently. */ 
    .maxactive      = 20, 
}; 
 
static char func_name[NAME_MAX] = "_do_fork"
module_param_string(func, func_name, NAME_MAX, S_IRUGO); 
my_kretprobe.kp.symbol_name = func_name; 
// 注册 
register_kretprobe(&my_kretprobe); 
  • 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.

我们可以看到可以通过 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解释器系统

2021-10-06 09:46:17

trace-cmd追踪内核Linux

2025-03-07 08:30:00

pwruLinux网络包追踪

2023-03-01 23:56:11

2023-03-01 23:53:30

Linuxshutdown进程

2023-03-10 14:56:37

Linuxconnect系统

2025-01-02 11:06:22

2023-02-28 09:47:42

2014-04-01 16:52:10

SUSEkGraftLinux内核

2021-09-30 09:43:11

Linux内核Zstd补丁

2023-11-24 11:24:16

Linux系统

2024-04-15 11:24:32

库存跟踪技术NFC蓝牙

2017-01-12 19:15:03

Linux内核调试自构proc

2022-05-24 12:34:32

Docker容器Linux容器进程

2020-11-13 18:33:38

跨镜追踪识别技术人脸识别

2018-06-04 16:20:56

Linux动态路由Quagga

2013-09-24 10:48:32

Google追踪技术Cookies

2013-09-25 17:02:50

追踪技术CookiesGoogle
点赞
收藏

51CTO技术栈公众号