Linux 调试技术 Kprobe

目录

  • 用途:
  • 一、技术背景
    • 1.1 kprobes的特点与使用限制
    • 1.2 kprobe原理
  • 二、 基于kprobe探测模块的探测方式
    • 2.1、struct kprobe结构体
    • 2.2 kprobe API函数
    • 2.3 示例代码
    • 参考资料:

用途:

判断内核函数是否被调用,获取调用上下文、入参以及返回值。

一、技术背景

如果需要知道内核函数是否被调用、被调用上下文、入参以及返回值,比较简单的方法是加printk,但是效率低。

利用kprobe技术,用户可以自定义自己的回调函数,可以再几乎所有的函数中动态插入探测点。

当内核执行流程执行到指定的探测函数时,会调用该回调函数,用户即可收集所需的信息了,同时内核最后还会回到原本的正常执行流程。如果用户已经收集足够的信息,不再需要继续探测,则同样可以动态的移除探测点。

kprobes技术包括的3种探测手段分别时kprobe、jprobe和kretprobe。

首先kprobe是最基本的探测方式,是实现后两种的基础,它可以在任意的位置放置探测点(就连函数内部的某条指令处也可以),它提供了探测点的调用前、调用后和内存访问出错3种回调方式,分别是pre_handler、post_handler和fault_handler,其中pre_handler函数将在被探测指令被执行前回调,post_handler会在被探测指令执行完毕后回调(注意不是被探测函数),fault_handler会在内存访问出错时被调用;jprobe基于kprobe实现,它用于获取被探测函数的入参值;最后kretprobe从名字种就可以看出其用途了,它同样基于kprobe实现,用于获取被探测函数的返回值。

1.1 kprobes的特点与使用限制

1、kprobes允许在同一个被被探测位置注册多个kprobe,但是目前jprobe却不可以;同时也不允许以其他的jprobe回掉函数和kprobe的post_handler回调函数作为被探测点。

2、一般情况下,可以探测内核中的任何函数,包括中断处理函数。不过在kernel/kprobes.c和arch/*/kernel/kprobes.c程序中用于实现kprobes自身的函数是不允许被探测的,另外还有do_page_fault和notifier_call_chain;

3、如果以一个内联函数为探测点,则kprobes可能无法保证对该函数的所有实例都注册探测点。由于gcc可能会自动将某些函数优化为内联函数,因此可能无法达到用户预期的探测效果;

4、一个探测点的回调函数可能会修改被探测函数运行的上下文,例如通过修改内核的数据结构或者保存与struct pt_regs结构体中的触发探测之前寄存器信息。因此kprobes可以被用来安装bug修复代码或者注入故障测试代码;

5、kprobes会避免在处理探测点函数时再次调用另一个探测点的回调函数,例如在printk()函数上注册了探测点,则在它的回调函数中可能再次调用printk函数,此时将不再触发printk探测点的回调,仅仅时增加了kprobe结构体中nmissed字段的数值;

6、在kprobes的注册和注销过程中不会使用mutex锁和动态的申请内存;

7、kprobes回调函数的运行期间是关闭内核抢占的,同时也可能在关闭中断的情况下执行,具体要视CPU架构而定。因此不论在何种情况下,在回调函数中不要调用会放弃CPU的函数(如信号量、mutex锁等);

8、kretprobe通过替换返回地址为预定义的trampoline的地址来实现,因此栈回溯和gcc内嵌函数__builtin_return_address()调用将返回trampoline的地址而不是真正的被探测函数的返回地址;

9、如果一个函数的调用此处和返回次数不相等,则在类似这样的函数上注册kretprobe将可能不会达到预期的效果,例如do_exit()函数会存在问题,而do_execve()函数和do_fork()函数不会;

10、如果当在进入和退出一个函数时,CPU运行在非当前任务所有的栈上,那么往该函数上注册kretprobe可能会导致不可预料的后果,因此,kprobes不支持在X86_64的结构下为__switch_to()函数注册kretprobe,将直接返回-EINVAL。

1.2 kprobe原理

kprobe工作具体流程见下图:
在这里插入图片描述

1、当用户注册一个探测点后,kprobe首先备份被探测点的对应指令,然后将原始指令的入口点替换为断点指令,该指令是CPU架构相关的,如i386和x86_64是int3,arm是设置一个未定义指令(目前的x86_64架构支持一种跳转优化方案Jump Optimization,内核需开启CONFIG_OPTPROBES选项,该种方案使用跳转指令来代替断点指令);
2、当CPU流程执行到探测点的断点指令时,就触发了一个trap,在trap处理流程中会保存当前CPU的寄存器信息并调用对应的trap处理函数,该处理函数会设置kprobe的调用状态并调用用户注册的pre_handler回调函数,kprobe会向该函数传递注册的struct kprobe结构地址以及保存的CPU寄存器信息;
3、随后kprobe单步执行前面所拷贝的被探测指令,具体执行方式各个架构不尽相同,arm会在异常处理流程中使用模拟函数执行,而x86_64架构则会设置单步调试flag并回到异常触发前的流程中执行;
4、在单步执行完成后,kprobe执行用户注册的post_handler回调函数;
5、最后,执行流程回到被探测指令之后的正常流程继续执行。

二、 基于kprobe探测模块的探测方式

2.1、struct kprobe结构体

内核提供了struct kprobe表示一个探测点,以及一系列API接口,用户可以通过这些接口实现回调函数并实现struct kprobe结构,然后将它注册到内核的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 */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;------------------------------------被探测点指令执行之后调用的回调函数。kprobe_fault_handler_t fault_handler;----------------------------------在执行pre_handler、post_handler或单步执行被探测指令时出现内存异常则会调用该回调函数。kprobe_break_handler_t break_handler;----------------------------------在执行某一kprobe过程中出发了断点指令后会调用该函数,用于实现jprobe。kprobe_opcode_t opcode;------------------------------------------------保存的被探测点原始指令。struct arch_specific_insn ainsn;---------------------------------------被复制的被探测点的原始指令,用于单步执行,架构强相关。u32 flags;-------------------------------------------------------------状态标记。
};

2.2 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探测点的名称、地址、偏移

2.3 示例代码

下面以内核中samples/kprobes/kprobe_example.c为例,介绍如何使用kprobe进行内核函数探测。
该kprobe实例实现了_do_fork的探测,该函数会在fork系统调用或者kernel_kthread创建内核线程时被调用。
对原%p修改为%pF后,可读性更强。可以显示函数名称以及偏移量。

#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)
{
#ifdef CONFIG_X86pr_info("<%s> pre_handler: p->addr = %pF, ip = %lx, flags = 0x%lx\n",p->symbol_name, p->addr, regs->ip, regs->flags);
#endif
#ifdef CONFIG_ARM64pr_info("<%s> pre_handler: p->addr = %pF, 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_X86pr_info("<%s> post_handler: p->addr = %pF, flags = 0x%lx\n",p->symbol_name, p->addr, regs->flags);
#endif
#ifdef CONFIG_ARM64pr_info("<%s> post_handler: p->addr = %pF, 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 = %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");

模块的编译Makefile如下:

obj-m := kprobe_example.oCROSS_COMPILE=''KDIR := /lib/modules/$(shell uname -r)/build
all:make -C $(KDIR) M=$(PWD) modules 
clean:rm -f *.ko *.o *.mod.o *.mod.c .*.cmd *.symvers  modul*

执行结果如下:

[ 9363.905687] Planted kprobe at _do_fork+0x0/0x3f0
[ 9366.924852] <_do_fork> pre_handler: p->addr = _do_fork+0x0/0x3f0, ip = ffffffff88a86a61, flags = 0x246
[ 9366.924858] <_do_fork> post_handler: p->addr = _do_fork+0x0/0x3f0, flags = 0x246
[ 9366.932935] <_do_fork> pre_handler: p->addr = _do_fork+0x0/0x3f0, ip = ffffffff88a86a61, flags = 0x246
[ 9366.932938] <_do_fork> post_handler: p->addr = _do_fork+0x0/0x3f0, flags = 0x246
[ 9366.957594] kprobe at _do_fork+0x0/0x3f0 unregistered

可以通过sudo cat /proc/kallsyms | grep l_do_fork来验证地址和符号是否对应。
若没有sudo,看不到真实的地址。


参考资料:

  1. Linux kprobe调试技术使用
  2. Linux内核调试技术——kprobe使用与实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/123549.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

三秋农忙,自动驾驶农机保驾护航

“三秋”&#xff08;收获、播种、整地&#xff09;是一年中重要而忙碌的农事季节&#xff0c;水稻、棉花、玉米等农作物收获时期&#xff0c;也是小麦、蔬菜等秋种的好时间&#xff0c;还是各类农作物进入秋管的重要期。 随者农业科技发展&#xff0c;北斗导航农机自动驾驶系…

英语语法基础--思维导图

思维导图通常用于可视化和整理信息&#xff0c;而英文语法非常广泛且复杂&#xff0c;无法在一个简单的思维导图中完整表示。然而&#xff0c;我可以提供一个简化版本的英文语法思维导图&#xff0c;列出一些主要的语法概念和部分示例。 请注意&#xff0c;这只是一个基本的概…

【牛客刷题】反转固定区间链表、每k个节点一组反转

链表内指定区间反转_牛客题霸_牛客网 ListNode* reverseList(ListNode* head, ListNode* tail) {ListNode* pre nullptr;ListNode* cur head;while (cur ! tail) { 最后cur就是tailListNode* temp cur->next;cur->next pre;pre cur;cur temp;}return pre;}ListNode…

Debian12系统下LAMP环境中Nubuilder4.5的安装

一、环境搭建 按照官方的说法&#xff0c;Apache2和Nginx都可以的&#xff0c;实际上&#xff0c;你最好直接按照 Mariadb\Apache2\Php8.2 这个顺序&#xff0c;搭建LAMP环境较好。不然各种调试&#xff0c;还不一定能够成功。 相关搭建方法&#xff0c;属于一般操作&#xf…

金融信创,软件规划需关注自主安全及生态建设

软件信创化&#xff0c;就是信息技术软件应用创新发展的意思&#xff08;简称为“信创”&#xff09;。 相信在中国&#xff0c;企业对于“信创化”这个概念并不陌生。「国强则民强」&#xff0c;今年来中国经济的快速发展&#xff0c;受到了各大欧美强国的“卡脖子”操作的影…

UDP协议结构及其注意事项

UDP报文结构 UDP报文结构主要是由两个部分组成的&#xff1a;UDP头部和数据部分。 UDP头部 源端口号&#xff1a;16位字段&#xff0c;指示数据发送方的端口号。目的端口号&#xff1a;16位字段&#xff0c;指示数据接收方端口号。UDP报文长度&#xff1a;16位字段&#xff0…

Java HashSet

HashSet 基于 HashMap 来实现的&#xff0c;是一个不允许有重复元素的集合。 HashSet 允许有 null 值。 HashSet 是无序的&#xff0c;即不会记录插入的顺序。 HashSet 不是线程安全的&#xff0c; 如果多个线程尝试同时修改 HashSet&#xff0c;则最终结果是不确定的。 您必…

【项目】Reactor模式的服务器

目录 Reactor完整代码连接 前置知识&#xff1a; 1.普通的epoll读写有什么问题&#xff1f; 2.Connection内的回调函数是什么 3.服务器的初始化&#xff08;Connection只是使用的一个结构体&#xff09; 4.等待就绪事件&#xff1a;有事件就绪&#xff0c;对使用Connectio…

配置keil生成asm汇编文件

简介&#xff1a;ASM是汇编语言源程序的扩展名&#xff1b;程序在编译的过程中&#xff0c;会将源代码编译会汇编代码&#xff0c;一步步生成可执行文件&#xff1b; 1&#xff1a;keil中options的配置 这个语法应该是根据工程工程哪里的配置名称来的&#xff0c;也可以使用固…

伦敦银线性回归分析

在金融市场中&#xff0c;商品的价格一段时间内总是会围绕着一条线性回归趋势线&#xff0c;在两侧波动并沿着这条趋势线方向发展。当价格在波动过程中偏离趋势线距离太大了&#xff0c;就会再次向趋势线靠拢。 波浪理论认为商品的价格走势都是波浪式发展的&#xff0c;无论处于…

vue中实现签名画板

特意封装成了一个组件&#xff0c;签名之后会生成一张图片 signBoard.vue <template><el-drawer title"签名" :visible.sync"isShowBoard" append-to-body :show-close"false" :before-close"closeBoard" size"50%&quo…

【Unity每日一记】WheelColider组件汽车游戏的关键

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

.netcore对传输类设置区分大小

.Net Core中内置了对Json的转化与解析 可将PropertyNameCaseInsensitive false 设置为区分大小写。

vue-别名路径联想提示的配置

在根路径下&#xff0c;新建 jsconfig.json 文件&#xff0c;即可 在输入 自动联想到src目录。 代码如下&#xff1a; // 别名路径联想提示&#xff1a;输入自动联想 {"compilerOptions":{"baseUrl":"./","paths": {"/*":[…

Vue2项目练手——通用后台管理项目第二节

Vue2项目练手——通用后台管理项目 路由限制重复跳转CommonAside.vue 顶部header组件搭建与样式修改右边用户菜单栏使用的组件图片CommonHeader.vue Vuex实现左侧折叠文件目录store/index.jsstore/tab.jsmain.jsCommonHeader.vueCommonAside.vueMain.vue 路由限制重复跳转 路由…

如何提高抖音直播间的人气(从15个方面为你解答)

抖音直播是一项非常受欢迎的内容创作方式&#xff0c;但是在直播过程中若是没有足够的人气&#xff0c;会让主播感到非常沮丧。如何才能提高抖音直播间的人气呢&#xff1f;本文将从15个方面为你一一解答。 一、打造独特个性的直播形象 在抖音直播中&#xff0c;每一个主播都有…

vue-elementPlus自动按需导入和主题定制

elementPlus自动按需导入 装包 -> 配置 1. 装包&#xff08;主包和两个插件包&#xff09; $ npm install element-plus --save npm install -D unplugin-vue-components unplugin-auto-import 2. 配置 在vite.config.js文件中配置&#xff0c;配置完重启&#xff08;n…

Vue3+Element Plus实现el-table跨行显示(非脚手架)

Vue3Element Plus实现el-table跨行显示 app组件内容使用:span-method"objectSpanMethod"自定义方法实现跨行显示查询方法初始化挂载新建一个html即可进行测试&#xff0c;完整代码如下效果图 app组件内容 <div id"app"><!-- 远程搜索 --><e…

stable diffusion实践操作-复制-清空-保存提示词

系列文章目录 stable diffusion实践操作 stable diffusion实践操作-webUI教程 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 系列文章目录前言一、右上生成图标附近按钮介绍1. 箭头介绍&#xff08;复现别人的…

这可能是最全面的Python入门手册了!

无论是学习任何一门语言&#xff0c;基础知识一定要扎实&#xff0c;基础功非常的重要&#xff0c;找到一个合适的学习方法和资料会让你少走很多弯路&#xff0c; 你的进步速度也会快很多&#xff0c;无论我们学习的目的是什么&#xff0c;不得不说Python真的是一门值得付出时间…