前言

之前学习了ftrace,继续学习内核跟踪技术kprobe。kprobe 是 linux 内核的一个重要特性, 是一个轻量级的内核调试工具, 同时它又是其他一些更高级的内核调试工具(比如 perf 、 ftrace、systemtap、eBPF)的 “基础设施”。

kprobe是一个Linux内核事件源,用于基于动态检测的跟踪器。

动态检测(Dynamic Instrumentation)是在软件运行时通过修改内存中的指令来插入插装例程,从而创建检测点。这类似于调试器在运行的软件中插入断点的方式。当触发断点时,调试器将执行流程传递给交互式调试器,而动态检测则会运行一个例程,然后继续执行目标软件。这种能力允许从任何正在运行的软件中创建自定义的性能统计数据。

一、Kprobes

1.1 Kprobes简介

kprobe在 Linux 内核主线 2.6.9 引入:

Add kprobes for analysing the Linux kernel by collecting debugging information non-disruptively

kprobes(内核探针)是基于动态检测的Linux内核事件源,用于跟踪器。kprobes可以跟踪任何内核函数或指令,并在Linux 2.6.9中首次发布(2004年)。由于kprobes暴露了可能在内核版本之间发生变化的原始内核函数和参数,它们被认为是不稳定的API。

kprobes可以在内部以不同的方式工作。标准方法是修改正在运行的内核代码的指令文本,以在需要的位置插入插装代码。当对函数的入口进行插装时,可以使用一种优化方法,即kprobes利用现有的Ftrace函数跟踪,因为它的开销较低。

Kprobes 使您能够动态地中断任何内核例程并无中断地收集调试和性能信息。您可以在几乎任何内核代码地址设置陷阱,并指定在遇到断点时要调用的处理程序例程。利用kprobes技术,内核开发人员可以在内核的绝大多数指定函数中动态的插入探测点来收集所需的调试状态信息而基本不影响内核原有的执行流程。

比如:通过 Kprobes 可以获取内核函数的执行流程,何时被调用,执行过程中的入参和返回值,在不修改现有代码的基础上,灵活的跟踪内核函数的执行。

目前有三种类型的探测:kprobes、kretprobes和jprobe,其中jprobe和kretprobe基于kprobe实现,分别应用于不同探测场景中。实际上,kprobe可以插入到内核中的任何指令上。当指定的函数返回时,将触发一个返回探测。

在通常情况下,基于 Kprobes 的检测被打包为内核模块。 模块的 init 函数安装(“注册”)一个或多个探测器,而 exit 函数取消注册它们。 诸如 register_kprobe() 之类的注册函数指定要插入探针的位置以及命中探针时要调用的处理程序。

还有 register_/unregister_*probes() 函数用于批量注册/注销一组 *probes。 当您必须一次注销大量探针时,这些功能可以加快注销过程。

1.2 Kprobes工作原理

在这里简单描述一下工作原理,后面会写一篇文章详细介绍。

注册 kprobe 后,Kprobes 会备份被探测的指令(程序原本正常的指令),并用断点指令(例如 i386 和 x86_64 上的 int3)替换被探测指令的第一个字节。

当 CPU 遇到断点指令时,会发生陷阱,保存 CPU 的寄存器,并通过 notifier_call_chain 机制将控制权传递给 Kprobes。 Kprobes 执行与 kprobe 相关的“pre_handler”,将 kprobe 结构的地址和保存的寄存器传递给处理程序。

接下来,kprobe单步执行前面所备份的被探测指令(程序原本正常的指令)。 在指令单步执行后,Kprobes 执行与 kprobe 关联的“post_handler”(如果有)。 然后继续执行探测点之后的指令。

单步指令的目的就是提供一个机会来执行kprobe–>post_hander函数。

将本来执行一条指令扩展成执行kprobe->pre_handler —> 指令 —> kprobe–>post_hander这样三步过程。

kprobes的技术原理并不仅仅包含存软件的实现方案,它也需要硬件架构提供支持。其中涉及硬件架构相关的是CPU的断点异常处理和单步调试技术,前者用于让程序的执行流程陷入到用户注册的回调函数中去,而后者则用于单步执行被探测点指令。

kprobe原理类似与GDB中的断点调试和单步调试。
kprobe涉及到程序指令的修改,这部分和体系结构相关:
x86_64:INT3指令。 (断点异常int3、 单步异常int1)
ARM64:BRK指令。

kprobe原理如下图所示:
在这里插入图片描述

二、kprobe API

2.1 dmeo

(1)
dmeo直接使用的是linux内核源码下的 samples/kprobes/文件下的例程:

#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 = {
	.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_X86
	pr_info("<%s> pre_handler: p->addr = 0x%p, ip = %lx, flags = 0x%lx\n",
		p->symbol_name, p->addr, regs->ip, regs->flags);
#endif

#ifdef CONFIG_ARM64
	pr_info("<%s> pre_handler: p->addr = 0x%p, 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_X86
	pr_info("<%s> post_handler: p->addr = 0x%p, flags = 0x%lx\n",
		p->symbol_name, p->addr, regs->flags);
#endif

#ifdef CONFIG_ARM64
	pr_info("<%s> post_handler: p->addr = 0x%p, 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 = 0x%p, 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.post_handler = handler_post;
	kp.fault_handler = handler_fault;

	ret = register_kprobe(&kp);
	if (ret < 0) {
		pr_err("register_kprobe failed, returned %d\n", ret);
		return ret;
	}
	pr_info("Planted kprobe at %p\n", kp.addr);
	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");

在这里插入图片描述
(2)
使用kprobe获取内核符号的地址,只要在/proc/kallsyms文件下出现的符号都可以使用kprobe获取内核符号的地址,例程如下:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/kprobes.h>

static struct kprobe sym_kp = { 
};


unsigned long *kprobe_find_kallsyms(char *name)
{
    unsigned long *sym_address; 

    sym_kp.symbol_name = name;
    register_kprobe(&sym_kp);
    sym_address = (void *)sym_kp.addr;
 
    return sym_address;
}

 
static int __init kprobe_init(void)
{
    unsigned long *_text_address;
    unsigned long *sct_address;
    
    _text_address = kprobe_find_kallsyms("_text");
    if(_text_address != NULL){
        printk("rkb: _text_address at %px\n", (void *)_text_address);
    } 
    else{
        printk("rkb: _text_address not found\n"); 
    }
    unregister_kprobe(&sym_kp);

    sct_address = kprobe_find_kallsyms("sys_call_table");
    if(sct_address != NULL){
        printk("rkb: sct_address at %px\n", (void *)sct_address);
    } 
    else{
        printk("rkb: sct_address not found\n"); 
    }
    unregister_kprobe(&sym_kp);

    return -1;
}
 

module_init(kprobe_init);
 
MODULE_LICENSE("GPL");

使用kprobe获取符号_text和sys_call_table的地址:

# dmesg -c
[2011595.356413] rkb: _text_address at ffffffc010080000
[2011595.386850] rkb: sct_address at ffffffc010bd1000

# cat /proc/kallsyms | grep "\<_text\>"
ffffffc010080000 T _text

# cat /proc/kallsyms | grep "\<sys_call_table\>"
ffffffc010bd1000 R sys_call_table

2.2 struct kprobe

// include/linux/kprobes.h

struct kprobe {
	struct hlist_node hlist;

	/* list of kprobes for multi-handler support */
	struct list_head list;

	/*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;

	/* 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;

	/*
	 * ... called if breakpoint trap occurs in probe handler.
	 * Return 1 if it handled break, otherwise kernel will see it.
	 */
	kprobe_break_handler_t break_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;
};
// include/linux/kprobes.h

/* Kprobe status flags */
#define KPROBE_FLAG_GONE	1 /* breakpoint has already gone */
#define KPROBE_FLAG_DISABLED	2 /* probe is temporarily disabled */
#define KPROBE_FLAG_OPTIMIZED	4 /*
								   * probe is really optimized.
								   * NOTE:
								   * this flag is only for optimized_kprobe.
								   */
#define KPROBE_FLAG_FTRACE	8 /* probe is using ftrace */

kprobe探测点的回调函数,pre_handler函数将在被探测指令被执行前回调,post_handler会在被探测指令执行完毕后回调(注意不是被探测函数),fault_handler会在内存访问出错时被调用。

typedef int (*kprobe_pre_handler_t) (struct kprobe *, struct pt_regs *);
typedef void (*kprobe_post_handler_t) (struct kprobe *, struct pt_regs *, unsigned long flags);
typedef int (*kprobe_fault_handler_t) (struct kprobe *, struct pt_regs *, int trapnr);

2.3 struct pt_regs

x86系统架构:

// arch/x86/include/asm/ptrace.h

struct pt_regs {
/*
 * C ABI says these regs are callee-preserved. They aren't saved on kernel entry
 * unless syscall needs a complete, fully filled "struct pt_regs".
 */
	unsigned long r15;
	unsigned long r14;
	unsigned long r13;
	unsigned long r12;
	unsigned long bp;
	unsigned long bx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
	unsigned long r11;
	unsigned long r10;
	unsigned long r9;
	unsigned long r8;
	unsigned long ax;
	unsigned long cx;
	unsigned long dx;
	unsigned long si;
	unsigned long di;
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
	unsigned long orig_ax;
/* Return frame for iretq */
	unsigned long ip;
	unsigned long cs;
	unsigned long flags;
	unsigned long sp;
	unsigned long ss;
/* top of stack page */
};

ARM64体系架构:

//arch/arm64/include/uapi/asm/ptrace.h
/*
 * User structures for general purpose, floating point and debug registers.
 */
struct user_pt_regs {
	__u64		regs[31];
	__u64		sp;
	__u64		pc;
	__u64		pstate;
};

//arch/arm64/include/asm/ptrace.h
/*
 * This struct defines the way the registers are stored on the stack during an
 * exception. Note that sizeof(struct pt_regs) has to be a multiple of 16 (for
 * stack alignment). struct user_pt_regs must form a prefix of struct pt_regs.
 */
struct pt_regs {
	union {
		struct user_pt_regs user_regs;
		struct {
			u64 regs[31];
			u64 sp;
			u64 pc;
			u64 pstate;
		};
	};
	u64 orig_x0;
	u64 syscallno;
	u64 orig_addr_limit;
	u64 unused;	// maintain 16 byte alignment
};

2.4 (un)register_kprobe

Kprobes API 为每种类型的探针包括一个“注册”函数和一个“取消注册”函数。 API 还包括“register_*probes”和“unregister_*probes”函数,用于(取消)注册探针数组。

在地址 kp->addr 处设置断点。 当断点被命中时,Kprobes 调用 kp->pre_handler。 被探测的指令单步执行后,Kprobe 调用 kp->post_handler。 任何或所有处理程序都可以为 NULL。 如果 kp->flags 设置为 KPROBE_FLAG_DISABLED,则该 kp 将被注册但被禁用,因此,它的处理程序在调用 enable_kprobe(kp) 之前不会被命中。

(1)
register_kprobe的使用:

#include <linux/kprobes.h>
int register_kprobe(struct kprobe *kp);

register_kprobe的原型:

//  kernel/kprobes.c

int register_kprobe(struct kprobe *p)
{
	int ret;
	struct kprobe *old_p;
	struct module *probed_mod;
	kprobe_opcode_t *addr;

	/* Adjust probe address from symbol */
	addr = kprobe_addr(p);
	if (IS_ERR(addr))
		return PTR_ERR(addr);
	p->addr = addr;

	ret = check_kprobe_rereg(p);
	if (ret)
		return ret;

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

	ret = check_kprobe_address_safe(p, &probed_mod);
	if (ret)
		return ret;

	mutex_lock(&kprobe_mutex);

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

	mutex_lock(&text_mutex);	/* Avoiding text modification */
	ret = prepare_kprobe(p);
	mutex_unlock(&text_mutex);
	if (ret)
		goto out;

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

	if (!kprobes_all_disarmed && !kprobe_disabled(p))
		arm_kprobe(p);

	/* Try to optimize kprobe */
	try_to_optimize_kprobe(p);

out:
	mutex_unlock(&kprobe_mutex);

	if (probed_mod)
		module_put(probed_mod);

	return ret;
}
EXPORT_SYMBOL_GPL(register_kprobe);

(2)
使用 pre-handler (kp->pre_handler) :
调用时 p 指向与断点关联的 kprobe,而 regs 指向包含在断点被击中时保存的寄存器的结构。 除非您是 Kprobes geek,否则请在此处返回 0。

#include <linux/kprobes.h>
#include <linux/ptrace.h>
int pre_handler(struct kprobe *p, struct pt_regs *regs);

使用post-handler (kp->post_handler):
p 和 regs 与 pre_handler 的描述相同。 标志似乎总是为零。

#include <linux/kprobes.h>
#include <linux/ptrace.h>
void post_handler(struct kprobe *p, struct pt_regs *regs, unsigned long flags);

(3)
unregister_kprobe的使用

#include <linux/kprobes.h>
void unregister_kprobe(struct kprobe *kp);
void unregister_kretprobe(struct kretprobe *rp);

unregister_kprobe的原型

//  kernel/kprobes.c
void unregister_kprobe(struct kprobe *p)
{
	unregister_kprobes(&p, 1);
}
EXPORT_SYMBOL_GPL(unregister_kprobe);

void unregister_kprobes(struct kprobe **kps, int num)
{
	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_sched();
	for (i = 0; i < num; i++)
		if (kps[i]->addr)
			__unregister_kprobe_bottom(kps[i]);
}
EXPORT_SYMBOL_GPL(unregister_kprobes);

2.5 symbol_name

/* For each probe you need to allocate a kprobe structure */
static struct kprobe kp = {
	.symbol_name	= symbol,
};

通过在 struct kprobe 中引入“symbol_name”字段,探测点地址解析现在将由内核负责。 现在可以执行以下操作:

kp.symbol_name = "symbol_name";

如果安装探针点的符号偏移量已知,则使用 struct kprobe 的“偏移量”字段。 该字段用于计算探测点。
指定 kprobe “symbol_name” 或 “addr”。 如果两者都指定,则 kprobe 注册将失败并显示 -EINVAL。
对于 CISC 架构(例如 i386 和 x86_64),kprobes 代码不会验证 kprobe.addr 是否位于指令边界。 谨慎使用“偏移”。

2.6 disable_*probe/enable_*probe

(1)disable_*probe

#include <linux/kprobes.h>
int disable_kprobe(struct kprobe *kp);
int disable_kretprobe(struct kretprobe *rp);

暂时禁用指定的 *probe。 您可以使用 enable_*probe() 再次启用它。 您必须指定已注册的探测器。

(2)enable_*probe

#include <linux/kprobes.h>
int enable_kprobe(struct kprobe *kp);
int enable_kretprobe(struct kretprobe *rp);

启用已被 disable_*probe() 禁用的 *probe。 您必须指定已注册的探测器。

三、Kprobes 特征和限制

Kprobes 允许在同一个地址进行多个探测。 此外,无法优化具有 post_handler 的探测点。 因此,如果您在优化的探测点安装带有 post_handler 的 kprobe,则探测点将自动取消优化。

通常,您可以在内核中的任何位置安装探针。 特别是,您可以探测中断处理程序。
本节讨论已知的异常:

(1)如果您尝试在实现 Kprobes 的代码中安装探针(主要是 kernel/kprobes.c 和 arch/*/kernel/kprobes.c,但也包括 do_page_fault 和 notifier_call_chain 等函数),则 register_*probe 函数将返回 -EINVAL。

(2)如果在可内联函数中安装探针,Kprobes 不会尝试追踪该函数的所有内联实例并在那里安装探针。gcc可能会自动将某些函数优化为内联函数,因此也可能没有看到预期的探测命中情况。

(3)探测处理程序可以修改被探测函数的环境——例如,通过修改内核数据结构,或通过修改 pt_regs 结构的内容(从断点返回时恢复到寄存器中)。 因此,可以使用 Kprobes,例如,安装错误修复程序或注入故障以进行测试。 当然,Kprobes 无法区分故意注入的故障和意外注入的故障。

(4)Kprobes不会试图阻止探测处理程序之间的冲突——例如,探测printk(),然后从探测处理程序调用printk()。如果探针处理程序命中探针,则第二个探针的处理程序不会在该实例中运行,并且第二个探针的 kprobe.nmissed 成员将递增。

(5)除了注册和注销期间,Kprobes 不使用互斥锁或分配内存(mutexes or allocate memory)。

(6)探测处理程序在禁用抢占或禁用中断的情况下运行,这取决于架构和优化状态。 (例如,kretprobe 处理程序和优化的 kprobe 处理程序在 x86/x86-64 上运行时不会禁用中断)。 在任何情况下,您的处理程序都不应让出 CPU(在探测处理程序中不要调用会放弃CPU的函数,例如,通过尝试获取信号量或等待 I/O)。

(7)由于返回探测是通过将返回地址替换为 trampoline 的地址来实现的,因此堆栈回溯和对 __builtin_return_address() 的调用通常会产生 trampoline 的地址,而不是 kretprobed 函数的实际返回地址。 ( __builtin_return_address() 仅用于检测和错误报告。)
备注:这一项与kretprobe有关。

(8)如果调用函数的次数与其返回的次数不匹配,则在该函数上注册返回探针可能会产生不是预期的结果。 在这种情况下,会打印一行:kretprobe BUG!: Processing kretprobe d000000000041aa8 @ c00000000004f48c。有了这些信息,就可以关联出导致问题的kretprobe的确切实例。比如:do_exit()函数会存在问题,而do_execve()函数和do_fork()函数不会。

(9)如果在进入或退出函数时,CPU 运行在当前任务的堆栈以外的堆栈上,则在该函数上注册返回探针可能会产生不希望的结果。 因此,Kprobes 不支持 x86_64 版本的 __switch_to() 上的返回探针(或 kprobes); 注册函数返回-EINVAL。

这些限制是由内核指令解码器检查的,所以开发者一般不用担心。

总结

主要介绍了kprobe的使用。大部分内容是翻译的Linux内核的Documentation/kprobes.txt。

参考资料

Linux 内核源码 4.10.0

https://www.kernel.org/doc/html/latest/trace/kprobes.html
https://www.cnblogs.com/arnoldlu/p/9752061.html
https://blog.csdn.net/luckyapple1028/article/details/52972315

Logo

更多推荐