kprobe及kretprobe的基于例子来调试分析其原理

一、背景

在之前的博客 register_kretprobe的使用及对抓取iowait程序的改进 里,我们使用了kretprobe来进一步对抓取iowait的程序进行优化。这篇博客里,我们基于例子来拆解kretprobe和kprobe的实现原理,搞清楚里面的一些细节。

在第二章里,我们给出实验的思路及相关源码及实验结果。在第三章里,我们给出相关说明。

二、例子的源码及步骤及实验结果

2.1 实验的思路

为了让实验基于的例子尽可能地纯粹和干净。我们基于一个我们自己insmod的一个ko里的一个函数来进行kprobe的打桩实验,也就是说先insmod一个ko来构造一个新的export_symbol的函数,然后kprobe和kretprobe是基于这个ko里的这个export_symbol的函数来进行打桩实验,这样可以不去影响原来内核里的逻辑以及原有的那些内核函数,让实验过程和实验结果更加纯粹。之前有篇博客详细讲了这块ko依赖ko里的基础函数的实现方式 insmod一个ko提供基础函数供后insmod的ko使用的方法

2.2 实验的源码

相关的一个目录结构如下:

上图里的testfunc.ko提供了一个export_symbol的函数,另外其编译出来的Module.symvers产出物也会被用于编译testkretprobe.ko的makefile所引用。

2.2.1 提供一个export_symbol的ko的源码

cpp 复制代码
#include <linux/module.h>
#include <linux/capability.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/ctype.h>
#include <linux/seq_file.h>
#include <linux/poll.h>
#include <linux/types.h>
#include <linux/ioctl.h>
#include <linux/errno.h>
#include <linux/stddef.h>
#include <linux/lockdep.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/wait.h>
#include <linux/init.h>
#include <asm/atomic.h>
#include <trace/events/workqueue.h>
#include <linux/sched/clock.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/interrupt.h>
#include <linux/tracepoint.h>
#include <trace/events/osmonitor.h>
#include <trace/events/sched.h>
#include <trace/events/irq.h>
#include <trace/events/kmem.h>
#include <linux/ptrace.h>
#include <linux/uaccess.h>
#include <asm/processor.h>
#include <linux/sched/task_stack.h>
#include <linux/nmi.h>
#include <linux/version.h>
#include <linux/sched/mm.h>
#include <asm/irq_regs.h>
#include <linux/kallsyms.h>
#include <linux/kprobes.h>
#include <linux/stop_machine.h>
#include <linux/perf_event.h>
#include <linux/file.h>
#include <linux/fscache.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhaoxin");
MODULE_DESCRIPTION("Module for kretprobe tasks.");
MODULE_VERSION("1.0");

#define USING_KRETPROBE 0

#if USING_KRETPROBE
struct kretprobe _kp1;
#else
struct kprobe _kp1;
#endif

extern int testfunc_zhaoxin(volatile int *p);

#if USING_KRETPROBE
static int kretprobe_entry_handler(struct kretprobe_instance * i_s,
				    struct pt_regs * i_p)
#else
int kprobecb_func_pre(struct kprobe* i_k, struct pt_regs* i_p)
#endif
{
#ifdef __x86_64__
    int *ptemp = (int*) i_p->di;
#elif defined(__aarch64__)
    int *ptemp = (int*) i_p->regs[0];
#else
#error
#endif
    printk("kprobecb_func_pre *input_parameter = %d\n", *ptemp);
    printk("preeempt_count=%llx\n", (u64)preempt_count());
    return 0;
}

volatile int _value;

#if USING_KRETPROBE
int kretprobe_ret_handler(struct kretprobe_instance *ri, struct pt_regs *i_p)
#else
void kprobecb_func_post(struct kprobe *p, struct pt_regs *i_p,
    unsigned long flags)
#endif
{
#ifdef __x86_64__
    int *ptemp = (int*) i_p->di;
#elif defined(__aarch64__)
    int *ptemp = (int*) i_p->regs[0];
#else
#error
#endif
    printk("kprobecb_func_post value = %d\n", _value);
    printk("preeempt_count=%llx\n", (u64)preempt_count());
    return 0;
}

int register_testfunc_zhaoxin_kprobe(void)
{
#if USING_KRETPROBE
    int ret;
    memset(&_kp1, 0, sizeof(_kp1));
    _kp1.entry_handler = kretprobe_entry_handler;
	_kp1.handler = kretprobe_ret_handler;
	_kp1.maxactive = 0;
	_kp1.kp.addr = (kprobe_opcode_t *)testfunc_zhaoxin;
    ret = register_kretprobe(&_kp1);
	if (ret < 0) {
		printk("register_kretprobe fail!\n");
		return -1;
	}
    printk("register_kretprobe success!\n");
    return 0;
#else
    int ret;
    memset(&_kp1, 0, sizeof(_kp1));
    _kp1.addr = (kprobe_opcode_t *)(0xffffffffc1415010);
    //_kp1.symbol_name = "testfunc_zhaoxin";
    _kp1.pre_handler = kprobecb_func_pre;
    _kp1.post_handler = kprobecb_func_post;
    ret = register_kprobe(&_kp1);
	if (ret < 0) {
		printk("register_kprobe fail!\n");
		return -1;
	}
    printk("register_kprobe success!\n");
    return 0;
#endif
}

static int __init testkretprobe_init(void)
{
    int ret;
    ret = register_testfunc_zhaoxin_kprobe();
    if (ret < 0) {
        return ret;
    }
    ret = testfunc_zhaoxin(&_value);
    printk("after testfunc_zhaoxin *input_parameter = %d, ret = %d\n", _value, ret);
    return 0;
}

void unregister_testfunc_zhaoxin_kprobe(void)
{
#if USING_KRETPROBE
    unregister_kretprobe(&_kp1);
#else
    unregister_kprobe(&_kp1);
#endif
}

static void __exit testkretprobe_exit(void)
{
    unregister_testfunc_zhaoxin_kprobe();
}

module_init(testkretprobe_init);
module_exit(testkretprobe_exit);

2.2.2 引用该symbol的ko的源码及Makefile

引用该symbol的ko的源码一(使用kprobe的方式的源码):

cpp 复制代码
#include <linux/module.h>
#include <linux/capability.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/ctype.h>
#include <linux/seq_file.h>
#include <linux/poll.h>
#include <linux/types.h>
#include <linux/ioctl.h>
#include <linux/errno.h>
#include <linux/stddef.h>
#include <linux/lockdep.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/wait.h>
#include <linux/init.h>
#include <asm/atomic.h>
#include <trace/events/workqueue.h>
#include <linux/sched/clock.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/interrupt.h>
#include <linux/tracepoint.h>
#include <trace/events/osmonitor.h>
#include <trace/events/sched.h>
#include <trace/events/irq.h>
#include <trace/events/kmem.h>
#include <linux/ptrace.h>
#include <linux/uaccess.h>
#include <asm/processor.h>
#include <linux/sched/task_stack.h>
#include <linux/nmi.h>
#include <linux/version.h>
#include <linux/sched/mm.h>
#include <asm/irq_regs.h>
#include <linux/kallsyms.h>
#include <linux/kprobes.h>
#include <linux/stop_machine.h>
#include <linux/perf_event.h>
#include <linux/file.h>
#include <linux/fscache.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhaoxin");
MODULE_DESCRIPTION("Module for kretprobe tasks.");
MODULE_VERSION("1.0");

struct kprobe _kp1;

extern int testfunc_zhaoxin(volatile int *p);

int kprobecb_func_pre(struct kprobe* i_k, struct pt_regs* i_p)
{
#ifdef __x86_64__
    int *ptemp = (int*) i_p->di;
#elif defined(__aarch64__)
    int *ptemp = (int*) i_p->regs[0];
#else
#error
#endif
    printk("kprobecb_func_pre *input_parameter = %d\n", *ptemp);
    return 0;
}

void kprobecb_func_post(struct kprobe *p, struct pt_regs *i_p,
    unsigned long flags)
{
#ifdef __x86_64__
    int *ptemp = (int*) i_p->di;
#elif defined(__aarch64__)
    int *ptemp = (int*) i_p->regs[0];
#else
#error
#endif
    printk("kprobecb_func_post *input_parameter = %d\n", *ptemp);
}

int register_testfunc_zhaoxin_kprobe(void)
{
    int ret;
    memset(&_kp1, 0, sizeof(_kp1));
    _kp1.symbol_name = "testfunc_zhaoxin";
    _kp1.pre_handler = kprobecb_func_pre;
    _kp1.post_handler = kprobecb_func_post;
    ret = register_kprobe(&_kp1);
	if (ret < 0) {
		printk("register_kprobe fail!\n");
		return -1;
	}
    printk("register_kprobe success!\n");
    return 0;
}

volatile int _value;

static int __init testkretprobe_init(void)
{
    int ret;
    ret = register_testfunc_zhaoxin_kprobe();
    if (ret < 0) {
        return ret;
    }
    testfunc_zhaoxin(&_value);
    printk("after testfunc_zhaoxin *input_parameter = %d\n", _value);
    return 0;
}

void unregister_testfunc_zhaoxin_kprobe(void)
{
#if 0
    unregister_kretprobe(&my_kretprobe);
#else
    unregister_kprobe(&_kp1);
#endif
}

static void __exit testkretprobe_exit(void)
{
    unregister_testfunc_zhaoxin_kprobe();
}

module_init(testkretprobe_init);
module_exit(testkretprobe_exit);

Makefile源码:

cpp 复制代码
obj-m += testkretprobe.o

TEST_DIR = $(CURDIR)

KBUILD_EXTRA_SYMBOLS = $(TEST_DIR)/testfunc/Module.symvers
export KBUILD_EXTRA_SYMBOLS

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) TEST_DIR=$(TEST_DIR) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) TEST_DIR=$(TEST_DIR) clean

引用该symbol的ko的源码二(使用kretprobe的方式的源码):

2.3 步骤和实验结果

如何查看当前运行的代码里实际的内容(代码段里实际的内容会和编译出来的vmlinux不一致,这是因为内核里有static_branch_likely及kprobe这样的动态修改代码段的功能),所以实际的代码段里的正在运行时的内容需要用工具去dump出bin再去查看。之前有一篇相关的博客 内核执行时动态的vmlinux的反汇编解析方法及static_branch_likely机制。下面的操作步骤省去与该模块相关的细节内容。

2.3.1 先做kprobe的实验(在函数的基址处进行kprobe)

可以如下图看到用kprobe的pre_handler和post_handler抓到的被testfunc_zhaoxin修改的数值的式样的,post_handler里打印出来的_value的数值并不是testfunc_zhaoxin运行结束后的数值:

通过下面的命令确定testfunc_zhaoxin函数的起始点的虚拟地址:

bash 复制代码
cat /proc/kallsyms | grep testfunc_zhaoxin

如下图,得到这个地址是0xffffffffc1415010到0xffffffffc1415040:

我们做一个kprobe前后的对比实验,看一下这个0xffffffffc1415010地址上的内容是发生如何的变更。

kprobe前:

bash 复制代码
insmod /usr/bin/testgetkmem.ko address=0xffffffffc1415010 size=0x30 filedir="beforekprobe.txt"

如上面的命令抓取到beforekprobe.txt里和insmod ko后抓到的afterkprobe.txt里,改变的内容如下图:

insmod ko前testfunc的部分的开头是ef 1f 44 00,搜索elf可以搜到是:

它是一句nop命令。

insmod ko后testfunc的部分的开头是e8 eb ff c7,搜索elf(搜索前三个字节)可以搜到是:

它是有关call跳转的。

事实上e8 eb是共性部分:

e8是call的操作吗,eb是段内直接短转的操作码。配合后面的offset就是跳转到__fentry__。

2.3.2 做kprobe的实验(在函数的基址加一些偏移处进行kprobe)

下一步,我们实验,在函数的基址加一些偏移处进行kprobe:

如上图修改不用函数名,而用具体的地址,这个地址是函数testfunc_zhaoxin的基址加0x6。

这样进行dump出来的二进制内容,用hexedit打开查看是:

如上图对比,是只改了一个字节(上图是x86平台),我们拷贝一份ko出来,通过hexedit编辑对应的二进制里的内容把B8改成CC,然后objdump -S来解析出elf文件得到如下图内容:

可以看到,如何插入到函数内部的话,是会直接用int3这种中断指令来替换原有的指令。

上面的设置的offset其实是自己根据汇编指令的位置来去计算的,如果随便设置一个offset是会失败的:

如下图,故意设置到一个指令的中间位置:

会报错:

2.3.3 再做kretprobe的实验

将源码里的USING_KRETPROBE宏设置为1:

运行得到下图情况:

可以看到kretprobe是可以打桩在函数返回的时刻:

我们来抓一下看看kretprobe执行前后的相关代码段的改变。

导出来后对比,可以发现,kretprobe的方式修改的内容也只是函数开头部分,也是E8EB开始:

而函数尾部并没有修改。

三、相关说明

3.1 实验总结

可以从上面 2.3.1 的实验可以看到kprobe的post_handler并不是函数返回时的时刻的状态。

从上面 2.3.2 的实验可以看到kprobe的这套api可以打桩在任意一个函数指令位置处,但是不能打桩到单条指令的开始和结束的中间。既然kprobe可以在任意位置打桩,为啥还需要kretprobe,这是因为函数返回的位置可能有多处。

从上面 2.3.3 的实验可以看到kretprobe可以打桩在函数开始和函数返回时刻,且kretprobe的返回值并不影响函数的返回值。

kprobe如果打桩到函数的开始的位置通常是用的call的汇编指令,如果是函数中间的短指令则用的int3中断指令。

3.2 kretprobe的实现原理

从上面 2.3.3 可以看到kretprobe的注册只是修改函数入口的地方的二进制,那么kretprobe是如何实现函数返回时的跳转呢?

这是因为kretprobe是在注册时是注册了自己的一个特殊的函数作为pre_handler(这个pre_handler就是基于kprobe的基址,填的pre_handler的函数指针):

然后在触发kprobe时,调用的是这个pre_handler_kretprobe接口,这个pre_handler_kretprobe接口继而调用下图里的rethook_hook接口,继而调用了arch_rethook_prepare(注意,这个调用链是在CONFIG_KRETPROBE_ON_RETHOOK打开时的调用链,在CONFIG_KRETPROBE_ON_RETHOOK不打开时调用链不同,后面会讲到):

而arch_rethook_prepare是一个arch的函数,在x86下是如下实现:

通过记录下来当前的stack[0]位置的函数的返回值,换成定义的arch_rethook_trampoline来让函数返回时先执行注册的函数。

对于CONFIG_KRETPROBE_ON_RETHOOK未打开的情况:

pre_handler_kretprobe调用了arch_prepare_kretprobe:

arm64平台的arch_prepare_kretprobe的实现如下(arm64平台就是不打开CONFIG_KRETPROBE_ON_RETHOOK的):

3.3 关于kretprobe的maxactive

maxactive的设置其实从原理上来说是上限的,上限就是2倍的possible cpu的数量,因为kprobe的执行期间是禁用抢占的:

所以就算有抢占也是在执行完kprobe逻辑后,在执行收尾逻辑时来发生抢占,这样算一个cpu这样的并发也就最多两个,乘上possible_cpu就是如下图里的上限:

当然也可以设置maxactive成0,让其用这个默认上限值。

相关推荐
小北方城市网2 小时前
微服务架构设计实战指南:从拆分到落地,构建高可用分布式系统
java·运维·数据库·分布式·python·微服务
开开心心_Every2 小时前
离线黑白照片上色工具:操作简单效果逼真
java·服务器·前端·学习·edge·c#·powerpoint
咕噜签名-铁蛋2 小时前
给服务器穿件“智能防弹衣“
服务器
Full Stack Developme2 小时前
达梦(DM8)基于 LBS(位置服务)教程
服务器·网络·数据库
桂花树下的猫2 小时前
ubuntu20.04上docker部署
运维·docker·容器
小李独爱秋2 小时前
计算机网络经典问题透视:端到端时延和时延抖动有什么区别?
运维·服务器·计算机网络·安全·web安全
`林中水滴`2 小时前
Linux系列:Ubuntu 防火墙命令
linux·ubuntu
自不量力的A同学2 小时前
Docker 29.1.4
运维·docker·容器
雾岛听蓝2 小时前
初识Linux
linux