Linux内核 eBPF基础 kprobe原理源码分析:源码分析

荣涛 2021年5月11日

在 《Linux内核 eBPF基础:kprobe原理源码分析:基本介绍与使用》中已经介绍了kprobe的基本原理,本文将基于linux5.10.13内核进行源码分析,相关注释代码在https://github.com/Rtoax/linux-5.10.13查看。

1. 引言

1.1. kprobe API

内核使用kprobe,可以使用register_kprobe()/unregister_kprobe()进行注册/卸载,还可以临时关闭/使能探测点。

int register_kprobe(struct kprobe *p);//注册kprobe探测点
void unregister_kprobe(struct kprobe *p);//卸载kprobe探测点
int register_kprobes(struct kprobe **kps, int num);//注册多个kprobe探测点
void unregister_kprobes(struct kprobe **kps, int num);//卸载多个kprobe探测点
int disable_kprobe(struct kprobe *kp);//暂停指定定kprobe探测点
int enable_kprobe(struct kprobe *kp);//恢复指定kprobe探测点
void dump_kprobe(struct kprobe *kp);//打印指定kprobe探测点的名称、地址、偏移

对于kretprobe同样有一套接口:

int register_kretprobe(struct kretprobe *rp);
void unregister_kretprobe(struct kretprobe *rp);
int register_kretprobes(struct kretprobe **rps, int num);
void unregister_kretprobes(struct kretprobe **rps, int num);
int disable_kretprobe(struct kretprobe *kp);
int enable_kretprobe(struct kretprobe *kp);

在5.10.13代码samples\kprobes\kprobe_example.c中,probe的是kernel_clone函数,在我的环境中3.10.0-1062.el7.x86_64没有这个sym(可以使用cat /proc/kallsyms查看),我使用do_fork替换(do_fork不至于被调用太多,但又不会没有)。

1.2. samples\kprobes\kprobe_example.c

#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 = {//定义实例kp并初始化symbol_name为"_do_fork",将探测_do_fork函数。.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)
{pr_info("<%s> pre_handler: p->addr = %pF, ip = %lx, flags = 0x%lx\n",p->symbol_name, p->addr, regs->ip, regs->flags);/* 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)
{pr_info("<%s> post_handler: p->addr = %pF, flags = 0x%lx\n",p->symbol_name, p->addr, regs->flags);
}/** 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 = %pF, 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的三个回调函数。kp.post_handler = handler_post;kp.fault_handler = handler_fault;ret = register_kprobe(&kp);//注册kp探测点到内核。if (ret < 0) {pr_err("register_kprobe failed, returned %d\n", ret);return ret;}pr_info("Planted kprobe at %pF\n", kp.addr);return 0;
}static void __exit kprobe_exit(void)
{unregister_kprobe(&kp);pr_info("kprobe at %pF unregistered\n", kp.addr);
}module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");

下面将结合samples\kprobes\kprobe_example.c实例代码,对kprobe源代码进行分析。

2. 数据结构

2.1. struct kprobe

struct kprobe { /*  */struct hlist_node hlist;    /* 被用于kprobe全局hash,索引值为被探测点的地址。 *//* list of kprobes for multi-handler support */struct list_head list;      /* 用于链接同一被探测点的不同探测kprobe。 *//*count the number of times this probe was temporarily disarmed 如果 kprobe 嵌套,增加nmissed字段的数值*/unsigned long nmissed;      /*  *//* 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;        /* 被探测点在函数内部的偏移,用于探测函数内核的指令,如果该值为0表示函数的入口。 *//* Called before addr is executed. 在被探测指令被执行前回调*/kprobe_pre_handler_t pre_handler;   /* Called after addr is executed, unless... 在被探测指令执行完毕后回调(注意不是被探测函数)*/kprobe_post_handler_t post_handler;/** ... called if executing addr causes a fault (eg. page fault).* Return 1 if it handled fault, otherwise kernel will see it.*/kprobe_fault_handler_t fault_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;  /* 状态标记。 */
};

其中回调函数的执行流程为:

2.2. struct kretprobe

struct kretprobe {struct kprobe kp;kretprobe_handler_t handler;kretprobe_handler_t entry_handler;int maxactive;int nmissed;size_t data_size;struct hlist_head free_instances;raw_spinlock_t lock;
};

3. API

3.1. register_kprobe - 注册kprobe

首先调用kprobe_addr查找函数名。

3.1.1. kprobe_addr - 获取内核符号地址

调用_kprobe_addr

static kprobe_opcode_t *kprobe_addr(struct kprobe *p)
{return _kprobe_addr(p->addr, p->symbol_name, p->offset);
}

调用关系为:

register_kprobekprobe_addr_kprobe_addrkprobe_lookup_namekallsyms_lookup_namekallsyms_expand_symbol      1.首先从内核函数中查找module_kallsyms_lookup_name 2.如果没找到再从模块中查找

函数kallsyms_sym_address如下,遍历内核符号:

static unsigned long kallsyms_sym_address(int idx)
{if (!IS_ENABLED(CONFIG_KALLSYMS_BASE_RELATIVE))return kallsyms_addresses[idx];/* values are unsigned offsets if --absolute-percpu is not in effect */if (!IS_ENABLED(CONFIG_KALLSYMS_ABSOLUTE_PERCPU))return kallsyms_relative_base + (u32)kallsyms_offsets[idx];/* ...otherwise, positive offsets are absolute values */if (kallsyms_offsets[idx] >= 0)return kallsyms_offsets[idx];/* ...and negative offsets are relative to kallsyms_relative_base - 1 */return kallsyms_relative_base - 1 - kallsyms_offsets[idx];
}

函数module_kallsyms_lookup_name如下:

/* Look for this name: can be of form module:name. */
unsigned long module_kallsyms_lookup_name(const char *name)
{struct module *mod;char *colon;unsigned long ret = 0;/* Don't lock: we're in enough trouble already. */preempt_disable();if ((colon = strnchr(name, MODULE_NAME_LEN, ':')) != NULL) {if ((mod = find_module_all(name, colon - name, false)) != NULL)ret = find_kallsyms_symbol_value(mod, colon+1);} else {/* 遍历模块 */list_for_each_entry_rcu(mod, &modules, list) {if (mod->state == MODULE_STATE_UNFORMED)continue;if ((ret = find_kallsyms_symbol_value(mod, name)) != 0)break;}}preempt_enable();return ret;
}

以上步骤获取了指令地址addr = kprobe_addr(p);,如下图instr2

并将addr赋值给kprobe结构p->addr = addr;

接着,检测kprobe是否可用,使用函数check_kprobe_rereg,该函数调用__get_valid_kprobe,该接口用于从同一被探测点的不同探测kprobe的链表中查找当前的kprobe是否存在,如果存在,返回kprobe地址,不存在则返回NULL。而在check_kprobe_rereg中,如果返回值为NULL,恰恰证明这个kprobe是一个新的kprobe(可用)。

接着,将初始化一些字段:

 /* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */p->flags &= KPROBE_FLAG_DISABLED;p->nmissed = 0;INIT_LIST_HEAD(&p->list);

3.1.2. check_kprobe_address_safe - 检测kprobe是否安全

因为kprobe常常结合ftrace使用,所以在内核编译过程中是默认开启CONFIG_KPROBES_ON_FTRACE的。

check_kprobe_address_safe流程:

  • 首先从ftrace_pages_start中二分查找struct dyn_ftrace结构,如果找到,讲给kprobe标志flags置位p->flags |= KPROBE_FLAG_FTRACE;
  • 关闭抢占preempt_disable();
  • 一些条件判断
    • kprobe的地址不可以不是内核代码段地址;
    • kprobe的地址不可以是kprobe黑名单中的地址;
    • kprobe的地址不可以是jump_label预留地址;
    • kprobe的地址不可一世static_call预留地址;
  • 使用__module_text_address检测是否为模块中的地址,如果是,将赋值给check_kprobe_address_safe函数入参的struct module二级指针;
  • 使能抢占preempt_enable();

接下来的这段代码,是处理复杂情况的kprobe(本文不讨论):

 old_p = get_kprobe(p->addr);if (old_p) {/* Since this may unoptimize old_p, locking text_mutex. 这是该地址上的第二个或后续kprobe-处理复杂情况*/ret = register_aggr_kprobe(old_p, p);goto out;}

3.1.3. prepare_kprobe

static int prepare_kprobe(struct kprobe *p)
{if (!kprobe_ftrace(p))return arch_prepare_kprobe(p);return arch_prepare_kprobe_ftrace(p);
}

kprobe_ftrace判断了标志位KPROBE_FLAG_FTRACE,而该标志位在check_kprobe_address_safe被置位(可能);

接着,将kprobe添加至哈希表kprobe_table中:

 INIT_HLIST_NODE(&p->hlist);hlist_add_head_rcu(&p->hlist,&kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);

3.1.4. arm_kprobe - 处理

接着就是将kprobe映射到内核代码段中的操作了。

 if (!kprobes_all_disarmed && !kprobe_disabled(p)) {ret = arm_kprobe(p);if (ret) {hlist_del_rcu(&p->hlist);synchronize_rcu();goto out;}}

其中kprobes_all_disarmed默认处理,如下:

static int __init init_kprobes(void)
{.../* By default, kprobes are armed */kprobes_all_disarmed = false;...
}
early_initcall(init_kprobes);

接着执行arm_kprobe函数,内部其他步骤不多看,直接看调用的__arm_kprobe,在此函数中其余unlikely和optimize部分也不看了,直接看arch_arm_kprobe

3.1.5. arch_arm_kprobe - 用int3替换代码段

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

其中int3指令INT3_INSN_OPCODE等于0xCC,接着调用text_poke,这个函数值得仔细阅读下,其内部调用__text_poke

3.1.6. __text_poke - 找到代码段页,用opcode替换

首先使用core_kernel_text判断是否为内核代码段,函数实现非常简单:

int notrace core_kernel_text(unsigned long addr)
{if (addr >= (unsigned long)_stext &&addr < (unsigned long)_etext)return 1;if (system_state < SYSTEM_RUNNING &&init_kernel_text(addr))return 1;return 0;
}

如果不是core代码段,那么可能是模块内的地址,因为模块中使用vmalloc申请内存,所以这里使用vmalloc_to_page查找页(该函数的实现不在这里讲解)。

若是core代码段,则直接使用函数virt_to_page获取页帧。因为本文不涉及内存管理相关介绍,所以直接跳转到用int3替换原指令的代码处:

 kasan_disable_current();memcpy((u8 *)poking_addr + offset_in_page(addr), opcode, len);kasan_enable_current();

如下图:

这个理给出一个实例:

3.2. register_kprobes - 略

int register_kprobes(struct kprobe **kps, int num)  //注册多个kprobe探测点
{int i, ret = 0;if (num <= 0)return -EINVAL;for (i = 0; i < num; i++) {ret = register_kprobe(kps[i]);if (ret < 0) {if (i > 0)unregister_kprobes(kps, i);break;}}return ret;
}
EXPORT_SYMBOL_GPL(register_kprobes);

3.3. unregister_kprobe - 注销kprobe

注销函数本质上只有一个:

void unregister_kprobe(struct kprobe *p)    //卸载kprobe探测点
{unregister_kprobes(&p, 1);
}
EXPORT_SYMBOL_GPL(unregister_kprobe);

移步unregister_kprobes

3.4. unregister_kprobes - 注销kprobes

函数实现如下:

void unregister_kprobes(struct kprobe **kps, int num)   //卸载多个kprobe探测点
{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]);
}
EXPORT_SYMBOL_GPL(unregister_kprobes);

整体操作和注册基本上是相反的,此处给出调用栈:

unregister_kprobes__unregister_kprobe_top__disable_kprobedisarm_kprobe__disarm_kprobearch_disarm_kprobetext_poke    -> 将int3 替换为原来的指令__unregister_kprobe_bottomlist_del

4. 参考和相关链接

  • 内核注释版代码:https://github.com/Rtoax/linux-5.10.13
  • 《Linux内核 eBPF基础:kprobe原理源码分析:基本介绍与使用》
  • Linux内核 eBPF基础:kprobe原理源码分析:源码分析
  • 《Linux内核:kprobe机制-探测点》
  • 《Linux eBPF:bcc 用法和原理初探之 kprobes 注入》
  • 《Linux内核调试技术——kprobe使用与实现》
  • 《Linux kprobe调试技术使用》
  • linux-5.10.13/Documentation/trace/kprobes.rst

Linux内核 eBPF基础:kprobe原理源码分析:源码分析相关推荐

  1. Linux内核 eBPF基础:kprobe原理源码分析:基本介绍与使用示例

    Linux内核 eBPF基础 kprobe原理源码分析:基本介绍与使用示例 荣涛 2021年5月11日 kprobe调试技术是为了便于跟踪内核函数执行状态所设计的一种轻量级内核调试技术. 利用kpro ...

  2. Linux内核 eBPF基础:Tracepoint原理源码分析

    Linux内核 eBPF基础 Tracepoint原理源码分析 荣涛 2021年5月10日 1. 基本原理 需要注意的几点: 本文将从sched_switch相关的tracepoint展开: 关于st ...

  3. Linux内核 eBPF基础:ftrace源码分析:过滤函数和开启追踪

    Linux内核 eBPF基础 ftrace基础:过滤函数和开启追踪 荣涛 2021年5月12日 本文相关注释代码:https://github.com/Rtoax/linux-5.10.13 上篇文章 ...

  4. Linux内核 eBPF基础:ftrace基础-ftrace_init初始化

    Linux内核 eBPF基础 ftrace基础:ftrace_init初始化 荣涛 2021年5月12日 本文相关注释代码:https://github.com/Rtoax/linux-5.10.13 ...

  5. Linux内核 eBPF基础:perf(1):perf_event在内核中的初始化

    Linux内核 eBPF基础 perf(1):perf_event在内核中的初始化 荣涛 2021年5月12日 本文相关注释代码:https://github.com/Rtoax/linux-5.10 ...

  6. Linux内核 eBPF基础:perf(2):perf性能管理单元PMU的注册

    Linux内核 eBPF基础 perf(2):性能管理单元PMU的注册 荣涛 2021年5月18日 本文相关注释代码:https://github.com/Rtoax/linux-5.10.13 Li ...

  7. Linux内核 eBPF基础:perf(4)perf_event_open系统调用与用户手册详解

    Linux内核 eBPF基础 perf(4)perf_event_open系统调用与用户手册详解 荣涛 2021年5月19日 本文相关注释代码:https://github.com/Rtoax/lin ...

  8. Linux内核 eBPF基础:BCC (BPF Compiler Collection)

    目录 BCC包括的一些工具 安装BCC 常用工具示例 capable tcpconnect tcptop 扩展工具 简单示例 使用BPF_PERF_OUTPUT 用户自定义探针示例 参考 BPF Co ...

  9. Linux内核 eBPF基础: 探索USDT探针

    目录 Motivation Tracing System Overview Terminology术语 Evoluction of Linux Tracing Linux Tracing Techni ...

最新文章

  1. asp.net html5 缓存,ASP.NET 缓存有效时间设置解决思路
  2. openstack中RemoteError: AgentNotFoundByTypeHost解决
  3. 命名空间不能直接包含_C++程序中可以命名的5种元素
  4. 通过Runtime源码了解关联对象的实现
  5. javascript中最最最常用的方法封装
  6. linux资源利用率检查_使用free命令查看实际内存占用(可用内存)
  7. 20180429 xlVBA套打单据自适应列宽
  8. 设计模式---命令模式
  9. BootStrap--CSS组件
  10. outlook 2010邮件传输接口错误解决一例
  11. python库下载(包括一些pip安装不成功的库下载)
  12. 最适合编程训练的三大OJ(从易到难)
  13. bootstrap bootbox 属性及用法
  14. ffmpeg设置h264编码IDR间隔
  15. java写到txt乱码_java读取txt文本发生乱码的解决方法
  16. 网络环路检测定位技术的发展过程
  17. AVM环视:系统搭建整体流程
  18. 不同windows服务器之间同步文件,WindowsServer2016配置DFS实现两个服务器之间文件同步...
  19. 反恐精英在线服务器名称,反恐精英1.6 国内服务器IP大全
  20. coco2dx精灵和背景遮挡_cocos2dx番外篇——更换精灵图片

热门文章

  1. 及时复盘的好处_如何做好2020的年终复盘?
  2. js文件中使用jstl或者其他标签
  3. MySQL学习-子查询及limit分页
  4. 我的渣渣java实训
  5. Spring cloud开发内存占用过高解决方法
  6. Spring Boot动态修改日志级别
  7. Shell基础(一):Shell基础应用、简单Shell脚本的设计、使用Shell变量、变量的扩展应用...
  8. thinkphp-session与cookie
  9. Excel 作复合饼图和双轴柱形图
  10. 有关Activity的Launch mode 以及Intent的setFlags(转载)