kprobe 原理详细分析

linux kprobe使用 kprobe 阅读详情

kprobe实现原理分析

1 简介

linux内核提供了许多调试内核的方式,比如 ftrace,tracepoint,kprobe/uprobe 等可以跟踪内核函数的执行流程,获取内核执行信息,这里主要详细分析 kprobe的实现机制。(还有一个 uprobe,机制和 kprobe 相同,只是探测的是用户态程序,这里不介绍)

kprobe 是内核的动态探测工具,它提供了一种调试机制,能够在不修改现有代码的基础上,灵活的跟踪内核函数的执行,并且它几乎可以探测任何一条内核指令。它的基本工作原理是:用户指定一个探测点(可以是内核函数,也可以是内核函数加偏移,或者一个内核地址),并且把一个用户定义的处理函数关联到该探测点,该探测点指定的地址将被替换为对应架构的 trap 指令(x86 的 int3,arm64 的 brk),当内核执行到该地址时,触发异常,在架构的异常处理中可以检测到探测点,并且探测点关联的函数将被执行,接着继续正常执行代码路径。

2 一个简单的例子

#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_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_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");

上述代码中通过 register_kprobe 注册了一个探测点,该探测点指定了探测的内核函数是 _do_fork,注册成功后,一旦执行到 _do_fork 内核函数,相对应的回调则会被调用。

  • pre_handler
    当执行 _do_fork 之前被调用,当回调返回 0 时,则会单步执行原理指令,并且单步执行后调用 post_handler。
  • post_handler
    当对应指令执行后,会调用注册的 post_handler。
  • fault_handler
    当在 pre_handler 到 post_handler 之间发生 pagefault,则会调用 fault_handler。

通过这个例子可以看到 kprobe 通过修改指令方式几乎是可以探测任意的内核地址(有一些地址范围不允许探测,__kprobe 段,预留段等等。。。),从而获取内核信息。

3 原理分析

首先内核提供了一些 api 来注册移除 hook 点。
register_kprobe,unregister_kprobe,register_kprobes,unregister_kprobes
register_kretprobe,unregister_kretprobe。。。

通过 register_kprobe 可以注册一个 hook点,register_kprobes 可以一次性注册多个 hook 点。

上述可以在对应内核地址注册 hook 点来执行 kprobe,同样的这里基于 kprobe 的能力,还提供了 register_kretprobe。。。在函数返回处注册 hook,意思是对对应注册的 hook 点,检测返回地址,并在函数返回时调用注册的 handler,我们常见到的注册方式即是内核函数返回点。

3.1 struct kprobe

看一下注册的 kprobe 结构体的数据结构:

struct kprobe {
    struct hlist_node hlist; // 注册到系统的全局链表节点。
    
    struct list_head list; // 一个 hook 点可以注册多个 kprobe,这个链表用于将同一个 hook 点串联起来。
    unsigned long nmissed; // 因断点指令不能重入处理, 当多个 kprobe 一起触发时会放弃执行后面的probe, 同时该计数增加。
    
    kprobe_opcode_t *addr; // 探测点对应的地址, 用户在调用注册接口时可以指定地址, 也可以传入函数名让内核自己查找。
    
    const char *symbol_name; // 探测点对应的函数名, 在注册 kprobe 时会将其翻译为十六进制地址并修改addr。
    unsigned int offset; // 相对于入口点地址的偏移, 会在计算 addr 以后再加上 offset 得到最终的 addr。
	kprobe_pre_handler_t pre_handler; // 在执行探测地址之前调用的 handler

	kprobe_post_handler_t post_handler; // 在探测地址执行后调用的 handler

	kprobe_fault_handler_t fault_handler; // 在 kprobe 运行期间触发异常则会调用该 handler

	kprobe_opcode_t opcode; // 探测地址对应的原始指令

	struct arch_specific_insn ainsn; // 架构相关结构,用于保存架构对应使用的数据。

	u32 flags; // 当前 kprobe 的状态记录。
    // 有如下状态
#define KPROBE_FLAG_GONE	1	// kprobe 已经取消,内部特殊状态
#define KPROBE_FLAG_DISABLED	2 // kprobe 暂时性的禁用
#define KPROBE_FLAG_OPTIMIZED	4 // 可以优化 kprobe,架构相关,arm64 没有优化
#define KPROBE_FLAG_FTRACE	8 // kprobe 可以被优化到 ftrace 上处理,目前只有 x86 会使用该状态。
}

// arm64 数据结构
struct arch_specific_insn {
	struct arch_probe_insn api;
};

struct arch_probe_insn {
	probe_opcode_t *insn; // 当探测地址的指令不是需要仿真的指令时,会分配一个指令槽,用于存放原始指令,以便于单步执行。
	pstate_check_t *pstate_cc; // 对应 pstate 状态,会在合适实际修改,恢复。
	probes_handler_t *handler; // 当对应指令是一个模拟指令时,对应的指令类型会有一个 handler 用于执行模拟指令运行。
	/* restore address after step xol */
	unsigned long restore; // 当非模拟指令时,会保存下一个执行的 pc 地址,为模拟指令时该值等于 0。
};
3.2 register_kprobe

注册一个探测点函数的重要部分:

int register_kprobe(struct kprobe *p)
{
...
    /* Adjust probe address from symbol */
	addr = kprobe_addr(p); ----------------------------------(1)
	if (IS_ERR(addr))
		return PTR_ERR(addr);
	p->addr = addr;
...
    ret = check_kprobe_rereg(p); ----------------------------(2)
...
	p->flags &= KPROBE_FLAG_DISABLED; -----------------------(3)
	p->nmissed = 0; -----------------------------------------(3)
...
    ret = check_kprobe_address_safe(p, &probed_mod); --------(4)
...
    // 从 hashtable 中获取该 hook 点原始的 kprobe,我们会将新的 kprobe 挂入原始 kp 的 list 中,后续有头 kp 调用。
	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;
	}
...
	cpus_read_lock();
	/* Prevent text modification */
	mutex_lock(&text_mutex);
	ret = prepare_kprobe(p); --------------------------------(5)
	mutex_unlock(&text_mutex);
	cpus_read_unlock();
...
    // 将设置好的 kprobe 加入到全局 hashtable 中
	hlist_add_head_rcu(&p->hlist,
		       &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);
...
	if (!kprobes_all_disarmed && !kprobe_disabled(p)) {
        // 当没有禁用 kprobe 时,调用 arm_kprobe 激活该 kprobe。
		ret = arm_kprobe(p);
...
    // 对于可以对 kprobe 的架构执行优化,arm64 为空
	try_to_optimize_kprobe(p);
...    
}

(1)首先会通过传入的 kprobe 结构获取到对应探测点的内核地址

kprobe_addr
    -> _kprobe_addr(p->addr, p->symbol_name, p->offset);

static kprobe_opcode_t *_kprobe_addr(kprobe_opcode_t *addr,
			const char *symbol_name, unsigned int offset)
{
    // 在注册 kprobe 中不能同时指定 addr 和 symbol_name
	if ((symbol_name && addr) || (!symbol_name && !addr))
		goto invalid;

    // 如果 name 存在,则通过 kall_symbol 模块获取到内核符号对应的内核地址
	if (symbol_name) {
		addr = kprobe_lookup_name(symbol_name, offset);
		if (!addr)
			return ERR_PTR(-ENOENT);
	}

    // 根据偏移返回实际我们要探测的内核地址,可能是内核函数的地址也可能时函数内部某个指令的地址。
	addr = (kprobe_opcode_t *)(((char *)addr) + offset);
	if (addr)
		return addr;
...
...
}

(2)检测该 kprobe 是否是重复注册的,是重复注册会返回错误。

check_kprobe_rereg
    -> __get_valid_kprobe // 如果返回有 kprobe 则当前 kprobe 被注册了,返回错误。

static struct kprobe *__get_valid_kprobe(struct kprobe *p)
{
	struct kprobe *ap, *list_p;

	// 遍历全局 hashtable 查看对应地址是否已经有 kprobe。
	ap = get_kprobe(p->addr);
	if (unlikely(!ap)) // 如果为空则没有注册过。
		return NULL;

    // 如果现在注册的 kprobe 与原有的 kprobe 不相等,那么遍历原有 kprobe list 链表,看看是否是串联链表中的 kprobe。
	if (p != ap) {
		list_for_each_entry_rcu(list_p, &ap->list, list)
			if (list_p == p)
			/* kprobe p is a valid probe */
				goto valid;
		return NULL;
	}
valid:
	return ap;
}

struct kprobe *get_kprobe(void *addr)
{
	struct hlist_head *head;
	struct kprobe *p;

    // 从全局 hashtable 得到对应数组的 hlist_head,再去遍历 hlist 获取 kprobe。
	head = &kprobe_table[hash_ptr(addr, KPROBE_HASH_BITS)];
	hlist_for_each_entry_rcu(p, head, hlist) {
		if (p->addr == addr) // hashtable 中有对应的 kprobe
			return p;
	}

	return NULL;
}

(3)用户指令标记 kprobe 的状态为禁用状态,其他状态不能设置。设置 nmissed 为 0,没有错过处理该 kprobe。
(4)检查探测地址是否是一个合法的地址

check_kprobe_address_safe
    -> arch_check_ftrace_location
    -> !kernel_text_address || within_kprobe_blacklist || jump_label_text_reserved
    -> __module_text_address

arch_check_ftrace_location 检测地址是否也是一个 ftrace 的地址,如果是,并且对应架构支持 CONFIG_KPROBES_ON_FTRACE kprobe 附加在 ftrace 上则会为该 kprobe 添加 KPROBE_FLAG_FTRACE 标记。

目前 arm64 不支持附加在 ftrace 上,需要走原始路径。

接着检测是否是内核的文本段,只有文本段可以设置 kprobe,内核自定义的"黑名单"不能设置kprobe。
如果 arm64 中的 异常处理程序段,idmap段,hyp 文本段,search_execption 段,__kprobe 标记的内核函数,NOKPROBE_SYMBOL 申明的导出函数这些都属于 kprobe 黑名单,不能被设置为探测点。
(5)配置这个 kprobe

prepare_kprobe
	-> arch_prepare_kprobe

int __kprobes arch_prepare_kprobe(struct kprobe *p)
{
    unsigned long probe_addr = (unsigned long)p->addr;
...
    // 检测探测点地址对齐
	if (probe_addr & 0x3)
		return -EINVAL;

	// 获取到探测点对应指令
	p->opcode = le32_to_cpu(*p->addr);

...

	// 解码该指令
    // 返回 INSN_REJECTED 指令不支持 kprobe
    // INSN_GOOD_NO_SLOT 是一条模拟指令
    // INSN_GOOD 是一条正常指令
	switch (arm_kprobe_decode_insn(p->addr, &p->ainsn)) {
	case INSN_REJECTED:	/* insn not supported */
		return -EINVAL;

	case INSN_GOOD_NO_SLOT:	/* insn need simulation */
		p->ainsn.api.insn = NULL;
		break;

	case INSN_GOOD:	/* instruction uses slot */
		p->ainsn.api.insn = get_insn_slot();
		if (!p->ainsn.api.insn)
			return -ENOMEM;
		break;
	}

	/* prepare the instruction */
	if (p->ainsn.api.insn)
		arch_prepare_ss_slot(p); -----------1)
	else
        // 对于模拟指令,直接设置 p->ainsn.api.restore = 0,表示不需要从这里得到恢复地址。
		arch_prepare_simulate(p);

	return 0;
}

// 调用
arm_kprobe_decode_insn
    -> arm_probe_decode_insn // 解码指令
		-> aarch64_insn_is_steppable // 检测指令是否可以单步下,可以单步返回 INSN_GOOD
		// 首先分支指令不能单步,因为他可能修改 pc,该 pc 与 xol 地址相关
    	// 异常指令,smc,hvc,eret 不可以单步。
    	// msr 指令可以使单步下有无法处理的情况出现,修改系统行为寄存器?。
    	// hit 指令相关,wfe,wfi,yield,sev,sevl 等也不能单步。
    	// pc 相对的 ldr/str* 和独占指令也会有问题,也不允许单步。
		-> aarch64_insn_is_bcond
    	-> aarch64_insn_is_cbz
    	...
    	...
    	-> aarch64_insn_is_ldr_lit
    	...
    	这些属于模拟指令,会绑定各自对应的 api->handler,用于模拟执行指令,那么这些指令就是不需要分配一个 slot 来 cpu 执行的。标记为 INSN_GOOD_NO_SLOT。
    	// 剩下的情况则是不支持的指令,返回 INSN_REJECTED

对应 INSN_GOOD 指令,我们需要调用 get_insn_slot 来为该指令分配一个 slot 槽,后续单步执行会跳到该 slot 执行指令。而 INSN_GOOD_NO_SLOT 指令,在单步使将会调用上述绑定的 api->handler 来模拟执行该指令,所以不需要系统分配 slot。get_insn_slot 会预分配一些可执行页面,从页面中分配一个 slot 并返回地址给调用。该 slot 是可执行的,用于执行替换下来的指令。

1)当分配 slot 时,使用 arch_prepare_ss_slot 向 slot 槽中写入需要指令的指令。

static void __kprobes arch_prepare_ss_slot(struct kprobe *p)
{
	/* prepare insn slot */
    // 向 slot 槽写入原始指令
	patch_text(p->ainsn.api.insn, p->opcode);

    // 刷新对应地址的 icache。
	flush_icache_range((uintptr_t) (p->ainsn.api.insn),
			   (uintptr_t) (p->ainsn.api.insn) +
			   MAX_INSN_SIZE * sizeof(kprobe_opcode_t));

    // 设置吓一跳执行的指令,单步后使用。
	/*
	 * Needs restoring of return address after stepping xol.
	 */
	p->ainsn.api.restore = (unsigned long) p->addr +
	  sizeof(kprobe_opcode_t);
}
3.3 arm_kprobe

kprobe 实现机制的重要函数时 arm_kprobe ,它会调用对应架构的 __arm_kprobe,来实际替换原始。

arm_kprobe 
	-> __arm_kprobe
		-> arch_arm_kprobe
			// 在 arm64 中,调用 patch_text 将 addr 对应的指令替换为了 BRK64_OPCODE_KPROBES 指令,该指令是一个 brk debug 指令,当触发时,系统会进入 debug 异常处理流程,从而调用 kprobe 相关处理函数。
			-> patch_text(p->addr, BRK64_OPCODE_KPROBES);
		-> optimize_kprobe

patch_text
	-> aarch64_insn_patch_text
    	-> stop_machine_cpuslocked
            -> aarch64_insn_patch_text_cb

static int __kprobes aarch64_insn_patch_text_cb(void *arg)
{
	int i, ret = 0;
	struct aarch64_insn_patch *pp = arg;

    // 通过 stop_machine 机制使所有 cpu 进入停机状态,接着只有第一进入的 cpu 去调用 aarch64_insn_patch_text_nosync 来修改 insn,其他 cpu 在 else 中等待完成修改。
	/* The first CPU becomes master */
	if (atomic_inc_return(&pp->cpu_count) == 1) {
		for (i = 0; ret == 0 && i < pp->insn_cnt; i++)
			ret = aarch64_insn_patch_text_nosync(pp->text_addrs[i],
							     pp->new_insns[i]);
		/* Notify other processors with an additional increment. */
		atomic_inc(&pp->cpu_count);
	} else {
		while (atomic_read(&pp->cpu_count) <= num_online_cpus())
			cpu_relax();
		isb();
	}

	return ret;
}

aarch64_insn_patch_text_nosync
    -> aarch64_insn_write
    	-> __aarch64_insn_write
    		// 通过 fixmap 机制,把地址重新映射出来,接着通过 probe_kernel_write 机制把新指令写入探测点。
    		-> waddr = patch_map(addr, FIX_TEXT_POKE0);
			-> ret = probe_kernel_write(waddr, &insn, AARCH64_INSN_SIZE);
    -> __flush_icache_range

至此,一个 kprobe 的注册就完成了,后续只要执行到探测点的 brk,则会进入 debug 模式来调用我们的回调函数。

3.4 kprobe 的执行

首先看一下 arm64 的注册函数。

static struct fault_info __refdata debug_fault_info[] = {
	{ do_bad,	SIGTRAP,	TRAP_HWBKPT,	"hardware breakpoint"	},
	{ do_bad,	SIGTRAP,	TRAP_HWBKPT,	"hardware single-step"	},
	{ do_bad,	SIGTRAP,	TRAP_HWBKPT,	"hardware watchpoint"	},
	{ do_bad,	SIGKILL,	SI_KERNEL,	"unknown 3"		},
	{ do_bad,	SIGTRAP,	TRAP_BRKPT,	"aarch32 BKPT"		},
	{ do_bad,	SIGKILL,	SI_KERNEL,	"aarch32 vector catch"	},
	{ early_brk64,	SIGTRAP,	TRAP_BRKPT,	"aarch64 BRK"		},
	{ do_bad,	SIGKILL,	SI_KERNEL,	"unknown 7"		},
};

void __init hook_debug_fault_code(int nr,
				  int (*fn)(unsigned long, unsigned int, struct pt_regs *),
				  int sig, int code, const char *name)
{
	BUG_ON(nr < 0 || nr >= ARRAY_SIZE(debug_fault_info));

	debug_fault_info[nr].fn		= fn;
	debug_fault_info[nr].sig	= sig;
	debug_fault_info[nr].code	= code;
	debug_fault_info[nr].name	= name;
}

在对应的 debug 相关异常中定义了一个数组,指示对应 debug 异常的类型,其中我们可以处理的是:

hardware breakpoint -> DBG_ESR_EVT_HWBP
hardware single-step -> DBG_ESR_EVT_HWSS
hardware watchpoint -> DBG_ESR_EVT_HWWP
aarch64 BRK -> DBG_ESR_EVT_BRK

通过 hook_debug_fault_code 即可像对应 debug 注册处理函数。对于 kprobe 主要涉及两个 debug 异常,第一个是 aarch64 BRK 异常,当系统第一册触发探测点的 brk 时,进入该异常,并在一系列验证后调用我们注册 pre_handler 表示在执行指令之前调用,第二个 hardware single-step 异常,当从 brk 返回后系统处于单步模式,执行一条指令后会进入该 debug 模式调用注册的 post_handler 回调,表示在指令执行后调用。注册代码如下:

static int __init debug_traps_init(void)
{
	hook_debug_fault_code(DBG_ESR_EVT_HWSS, single_step_handler, SIGTRAP,
			      TRAP_TRACE, "single-step handler");
	hook_debug_fault_code(DBG_ESR_EVT_BRK, brk_handler, SIGTRAP,
			      TRAP_BRKPT, "ptrace BRK handler");
	return 0;
}
arch_initcall(debug_traps_init);

第一次 通过 brk 指令进入 pre_handler 阶段:

entry
    -> do_debug_exception
		-> brk_handler
    		// 匹配到是我们手动修改替换的 BRK64_ESR_KPROBES(0x0004)指令,则调用 kprobe 处理。
    		-> if ((esr & BRK64_ESR_MASK) == BRK64_ESR_KPROBES)
                -> kprobe_breakpoint_handler
                	-> kprobe_handler

static void __kprobes kprobe_handler(struct pt_regs *regs)
{
	struct kprobe *p, *cur_kprobe;
	struct kprobe_ctlblk *kcb;
	unsigned long addr = instruction_pointer(regs);

    // kcb 是一个 per_cpu 变量,用于记录执行 kprobe 期间的一些信息,比如执行状态,对应的 irqflags,单步的上下文信息等。
	kcb = get_kprobe_ctlblk();
    // kprobe 的执行可能是会重入的,所以,同上该变量也是一个 per_cpu 变量,记录当前正在运行 kprobe。 
	cur_kprobe = kprobe_running();

	p = get_kprobe((kprobe_opcode_t *) addr);

	if (p) {
        // cur_kprobe 存在,说明 kprobe 正在运行,我们重入了,进入重入处理。
		if (cur_kprobe) {
			if (reenter_kprobe(p, regs, kcb))-----------------------------(3)
				return;
		} else {
            // 首次命中 kprobe,设置 cur_kprobe 为当前 kprobe,修改 kcb 中状态为 KPROBE_HIT_ACTIVE。
			/* Probe hit */
			set_current_kprobe(p);
			kcb->kprobe_status = KPROBE_HIT_ACTIVE;

            // 判断当前 kprobe 是否有 pre_handler,如果有则执行 pre_handler 如果返回不为 0 则调用 reset_current_kprobe 清除 cur_kprobe,该 kprobe 执行完毕。
            // 如果 pre_handler 不存在,或者 pre_handler 执行返回为 0,说明 post_handler 需要被执行,我们设置单步执行。
			if (!p->pre_handler || !p->pre_handler(p, regs)) { ------------(1)
				setup_singlestep(p, regs, kcb, 0);-------------------------(2)
			} else
				reset_current_kprobe();
		}
	}
}

(1)对于该探测点如果只有一个kprobe 那么 pre_handler 对应的就是kprobe的pre_handler,如果该探测点有多个 kprobe,那么对应的 pre_handler 为 aggr_pre_handler,该 pre_handler 在注册kprobe中设置。

static int aggr_pre_handler(struct kprobe *p, struct pt_regs *regs)
{
	struct kprobe *kp;

    // 遍历首个 kprobe list 链表,一次调用链表中 kprobe 的 pre_handler。
	list_for_each_entry_rcu(kp, &p->list, list) {
		if (kp->pre_handler && likely(!kprobe_disabled(kp))) {
			set_kprobe_instance(kp);
			if (kp->pre_handler(kp, regs))
				return 1;
		}
		reset_kprobe_instance();
	}
	return 0;
}

(2)当 pre_handler 不存在或者 pre_handler 返回 0时,说明我们需要调用 post_handler,所以需要配置单步执行。

为什么需要单步执行?

首先我们把原来指令替换为了 brk,此时调用该地址时实则调用了 pre_handler,下一步我们需要调用原始指令,并且需要原始指令执行完成后接着调用 post_handler,为了实现这个机制,只有单步这个模式才能做到执行一条指令后进入 debug 模式,也是最合适的实现机制。

setup_singlestep 代码如下:

static void __kprobes setup_singlestep(struct kprobe *p,
				       struct pt_regs *regs,
				       struct kprobe_ctlblk *kcb, int reenter)
{
	unsigned long slot;

	if (reenter) { ------------------------------------------5)
		save_previous_kprobe(kcb);
		set_current_kprobe(p);
		kcb->kprobe_status = KPROBE_REENTER;
	} else {
		kcb->kprobe_status = KPROBE_HIT_SS; ------------------1)
	}


	if (p->ainsn.api.insn) { ---------------------------------2)
		/* prepare for single stepping */
		slot = (unsigned long)p->ainsn.api.insn;

		set_ss_context(kcb, slot);	/* mark pending ss */

		spsr_set_debug_flag(regs, 0);

		/* IRQs and single stepping do not mix well. */
		kprobes_save_local_irqflag(kcb, regs);
		kernel_enable_single_step(regs);
		instruction_pointer_set(regs, slot); -----------------3)
	} else {
		/* insn simulation */
		arch_simulate_insn(p, regs); -------------------------4)
	}
}

1)当首次进入 kprobe时,设置kprobe状态为 KPROBE_HIT_SS,标记kprobe开始单步执行

2)当api->insn 存在时,说明我们分配了 slot,我们需要在 slot 中执行原始指令。

set_ss_context 标记 ss_pending 为 true,单步挂起执行。match_addr 为操作地址 + opcode,用于单步中判断地址是否匹配。

spsr_set_debug_flag(regs,0) 清除 pstate 中 D debug 位。

kprobes_save_local_irqflag 保存原来的 pstate 信息,并且在新 pstate 中mask irq。

kernel_enable_single_step 激活单步模式。

3)instruction_pointer_set(regs, slot); 设置下一条执行的指令,这里将 slot 槽的地址设置为了 pc,那么当 debug 返回时将会跳向 slot 地址所在位置执行,其中 slot 槽中存储的则是原始指令,并且由于激活了单步,一旦我们执行了 slot 槽中的指令我们又会进入单步的debug异常。

4)当没有 insn 时,说明我们执行的是一个模拟指令,这里直接调用 arch_simulate_insn 来完成模拟执行。

static void __kprobes arch_simulate_insn(struct kprobe *p, struct pt_regs *regs)
{
	struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();

   	// api.handler 则会在解码 insn 阶段设置的 handler,在handler 中会模拟执行该指令
	if (p->ainsn.api.handler)
		p->ainsn.api.handler((u32)p->opcode, (long)p->addr, regs);

    // 由于是模拟执行的,我们不需要通过单步执行来进入 post 处理阶段,这里直接进行 post_handler 调用处理。
	/* single step simulated, now go for post processing */
	post_kprobe_handler(kcb, regs);
}

上述(3)和 5)均是 kprobe 重入的处理, 首先看(3):

static int __kprobes reenter_kprobe(struct kprobe *p,
				    struct pt_regs *regs,
				    struct kprobe_ctlblk *kcb)
{
    // 检查 kcb 状态,KPROBE_HIT_SSDONE(单步完成)和 KPROBE_HIT_ACTIVE(kprobe激活)
    // 在重入阶段,这两个状态表明重入的kprobe是不会执行的,增加 nmissed 计数,以便告知系统发生了什么
    // 在这种状态下调用setup_singlestep(p, regs, kcb, 1);设置重入状态的单步行为。
	switch (kcb->kprobe_status) {
	case KPROBE_HIT_SSDONE:
	case KPROBE_HIT_ACTIVE:
		kprobes_inc_nmissed_count(p);
		setup_singlestep(p, regs, kcb, 1);
		break;
    // 如果程序正在单步执行,或者 kcb 已经被标记为了,那么执行抛出错误
	case KPROBE_HIT_SS:
	case KPROBE_REENTER:
		pr_warn("Unrecoverable kprobe detected.\n");
		dump_kprobe(p);
		BUG();
		break;
    // 其他情况未知,报一个警告。
	default:
		WARN_ON(1);
		return 0;
	}

	return 1;
}

再来看 5),也就是重入中调用的 setup_singlestep(p, regs, kcb, 1) 处理。

首先调用 save_previous_kprobe

static void __kprobes save_previous_kprobe(struct kprobe_ctlblk *kcb)
{
    // 保存之前那个 kprobe 的状态。
	kcb->prev_kprobe.kp = kprobe_running();
	kcb->prev_kprobe.status = kcb->kprobe_status;
}

接着调用 set_current_kprobe 更新当前 kprobe 为重入的 kprobe,并且标记自己状态为 KPROBE_REENTER。

至此,pre_handler 的处理就完成了,并且当该 debug 返回后,首先执行的是 slot 槽中原始指令,并且由于处于单步模式,当原始指令执行完成后,进入单步异常,单步异常入口如上所述是 single_step_handler。

3.4.1 single_step_handler
single_step_handler
    -> kprobe_single_step_handler

int __kprobes
kprobe_single_step_handler(struct pt_regs *regs, unsigned int esr)
{
	struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
	int retval;

	// 检查当前 cpu 的 kcb 的状态
	/* return error if this is not our step */
	retval = kprobe_ss_hit(kcb, instruction_pointer(regs));

   	// ok,kprobe 单步命中,进行 post handler 处理。
	if (retval == DBG_HOOK_HANDLED) {
        // 恢复原来 pstate 的值。
		kprobes_restore_local_irqflag(kcb, regs);
        // 清除单步状态,禁用单步
		kernel_disable_single_step();

        // 开始 post_handler 的处理。
		post_kprobe_handler(kcb, regs);
	}

	return retval;
}

static int __kprobes
kprobe_ss_hit(struct kprobe_ctlblk *kcb, unsigned long addr)
{
    // 如果 ss_pending 为 true 则是我们在 pre_handler 中设置的,检查 match_addr。
    // 当都满足时,说明kprobe的单步执行命令清理 kcb并返回 DBG_HOOK_HANDLED
	if ((kcb->ss_ctx.ss_pending)
	    && (kcb->ss_ctx.match_addr == addr)) {
		clear_ss_context(kcb);	/* clear pending ss */
		return DBG_HOOK_HANDLED;
	}
	/* not ours, kprobes should ignore it */
	return DBG_HOOK_ERROR;
}

post_kprobe_handler 在 kprobe 重入中会跳过 pre_handler 执行而直接调用 post_kprobe_handler,以及正常流程的下调用 post_kprobe_handler。

static void __kprobes
post_kprobe_handler(struct kprobe_ctlblk *kcb, struct pt_regs *regs)
{
	struct kprobe *cur = kprobe_running();
	// 首次检查当前是否是 kprobe 运行,不是则直接返回。
	if (!cur)
		return;

    // 当 restore 不为 0,我们使用了 slot,并在 slot 中执行了指令,现在从 restore 中恢复原来流程的 pc 指针。
	/* return addr restore if non-branching insn */
	if (cur->ainsn.api.restore != 0)
		instruction_pointer_set(regs, cur->ainsn.api.restore);

    // 如果状态是 KPROBE_REENTER,说明是kprobe 重入调用的 post_kprobe_handler,我们从 kcb 的prev 中恢复原来的 kprobe,以便于再次执行原来的 kprobe,并且返回,这次重入即可被处理掉。
	/* restore back original saved kprobe variables and continue */
	if (kcb->kprobe_status == KPROBE_REENTER) {
		restore_previous_kprobe(kcb);
		return;
	}
    // 正常流程,标记状态为单步完成,调用 post_handler。
	/* call post handler */
	kcb->kprobe_status = KPROBE_HIT_SSDONE;
	if (cur->post_handler)	{
		/* post_handler can hit breakpoint and single step
		 * again, so we enable D-flag for recursive exception.
		 */
		cur->post_handler(cur, regs, 0);
	}
	// kprobe 执行完成,清除 cur_kprobe, 表示 kprobe 没有运行了
	reset_current_kprobe();
}

至此 pre_handler 和 post _handler 的处理就完成了。

最后还剩 fault_handler。

该逻辑比较简单代码如下:

do_page_fault
    -> notify_page_fault
    	-> if (kprobe_running() && kprobe_fault_handler(regs, esr))

int __kprobes kprobe_fault_handler(struct pt_regs *regs, unsigned int fsr)
{
	struct kprobe *cur = kprobe_running();
	struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();

	switch (kcb->kprobe_status) {
	case KPROBE_HIT_SS:
	case KPROBE_REENTER:
		// 如果是单步执行触发的 page_fault,这是正常情况,重新指向该地址,由 do_page_fault 流程处理也错误。
		instruction_pointer_set(regs, (unsigned long) cur->addr);
		if (!instruction_pointer(regs))
			BUG();
...
	case KPROBE_HIT_ACTIVE:
	case KPROBE_HIT_SSDONE:

		// 如果是 pre_handler 和 post_handler 触发的 page_fault,则调用 fault_handler
		if (cur->fault_handler && cur->fault_handler(cur, regs, fsr))
			return 1;

		// 如果没有 fault_handler,那么检查错误是否是在 __asm_extable 段中,进行最后可能的处理。
		if (fixup_exception(regs))
			return 1;
	}
	return 0;
}

// 返回 1 说明由 fault_handler 或者 fixup_exception 处理了该错误,do_page_fault 不需要做进一步处理,直接返回。
// 返回 0 错误没有得到处理,交由 do_page_fault 进行处理。

至此 kprobe 的实现机制分析完毕。

接着看看如何移除一个 kprobe,移除kprobe 调用 unregister_kprobe。

unregister_kprobe
    -> unregister_kprobes

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_rcu();
	for (i = 0; i < num; i++)
		if (kps[i]->addr)
			__unregister_kprobe_bottom(kps[i]);
}
 

卸载分为两个步骤,分别是 __unregister_kprobe_top 和 __unregister_kprobe_bottom,

// 负责从链表中卸载该 kprobe
__unregister_kprobe_top
    -> __disable_kprobe // 标记该 kprobe 为禁用状态,后续该 kprobe 不会被执行,方便卸载
    	-> arch_disarm_kprobe
    		// 恢复原始地址的 insn
    		-> patch_text(p->addr, p->opcode);
    -> list_del_rcu
    -> hlist_del_rcu

// 调用 synchronize_rcu 确保所有对当前 kprobe的访问结束,之后可以安全的移除释放该 kprobe
synchronize_rcu
    
// 释放该 kprobe 的数据,恢复原始指令。
__unregister_kprobe_bottom
    -> free_aggr_kprobe
    	-> arch_remove_kprobe
    		-> free_insn_slot
    	-> kfree(kprobe)

最后看一下 register_kretprobe 的实现原理:

kprobe 可以在内核的地址附加探测点来触发 pre_handler等回调的执行。kretprobe 则是在附加地址对应函数的返回处附加hook,可以在函数返回时被调用。它的实现机制是基于 kprobe的,原理是在要探测点注册一个 kprobe,该kprobe是内核定义的,内核会挂接自己定义的 pre_handler,一旦探测点执行,则是执行自定义的kprobe 的 pre_handler,在 pre_handler 中手动捕获并保存当前位置的返回地址,并修改返回地址为一个跳板函数,跳板函数执行我们kretprobe中注册的handler,并在完成调用后恢复原始的返回地址,完成一个 kretprobe 的触发。

kretprobe 注册的数据结构:

struct kretprobe {
    // kretporbe 注册内部构造的 kprobe,使用它来在 pre_handler 阶段修改函数返回地址为kretprobe的跳板地址
	struct kprobe kp;
	kretprobe_handler_t handler; // 函数返回时调用的 handler
	kretprobe_handler_t entry_handler; // 当 kprobe 中的 pre_handler 触发时在 pre_handler 中调用 entry_handler
	int maxactive; // kretprobe 可能多个 cpu 都会进入该流程,maxactive 限制最大同时能有多少个回调。
	int nmissed; // 一些情况不会调用回调,记录触发后未调用的次数
	size_t data_size; // 未使用?
	struct hlist_head free_instances; // 上述根据 maxactive 大小,会实例化 struct kretprobe_instance 结构体,每一个 kretprobe_instance 是一个 kretprobe 的实例。
	raw_spinlock_t lock;
};

// 记录一个 kretprobe 触发实例
struct kretprobe_instance {
	struct hlist_node hlist; // 每一个正在使用的实例会存放到全局 hashtable 中
	struct kretprobe *rp; // 实列对应自己所属的 kprobe
	kprobe_opcode_t *ret_addr; // 实例对应的真实返回地址
	struct task_struct *task; // 实例对应当前触发的 task,通过 task 计算 hash,在跳板函数中可以拿到自己对应实例
	char data[0]; // ?
};
int register_kretprobe(struct kretprobe *rp)
{
...
    // 检查探测点是不是一个合理的函数入口
    if (!kprobe_on_func_entry(rp->kp.addr, rp->kp.symbol_name, rp->kp.offset))
        return -EINVAL;

    // 检查地址是否在黑名单中, 在则不能 hook
    if (kretprobe_blacklist_size) {
		addr = kprobe_addr(&rp->kp);
		if (IS_ERR(addr))
			return PTR_ERR(addr);

		for (i = 0; kretprobe_blacklist[i].name != NULL; i++) {
			if (kretprobe_blacklist[i].addr == addr)
				return -EINVAL;
		}
	}
    
    // 为内部构造的 kprobe 注册一个 pre_handler,该 pre_handler 将会去修改返回地址以此构造一个返回回调。
    rp->kp.pre_handler = pre_handler_kretprobe;
	rp->kp.post_handler = NULL;
	rp->kp.fault_handler = NULL;
...
    // 根据 maxactive 实例化 kretprobe_instance,每触发一次 kretprobe 使用一个 kretprobe_instance,最大同时可以有 maxactive 个进行处理。
	/* Pre-allocate memory for max kretprobe instances */
	if (rp->maxactive <= 0) {
#ifdef CONFIG_PREEMPT
		rp->maxactive = max_t(unsigned int, 10, 2*num_possible_cpus());
#else
		rp->maxactive = num_possible_cpus();
#endif
	}
	raw_spin_lock_init(&rp->lock);
	INIT_HLIST_HEAD(&rp->free_instances);
	for (i = 0; i < rp->maxactive; i++) {
		inst = kmalloc(sizeof(struct kretprobe_instance) +
			       rp->data_size, GFP_KERNEL);
		if (inst == NULL) {
			free_rp_inst(rp);
			return -ENOMEM;
		}
		INIT_HLIST_NODE(&inst->hlist);
		hlist_add_head(&inst->hlist, &rp->free_instances);
	}
...
    // 将 kretprobe 内部构造的 kprobe 注册到系统中。
	/* Establish function entry probe point */
	ret = register_kprobe(&rp->kp);
}

从上可知,一切的逻辑在 kretprobe 中注册的 pre_handler。

当 kretprobe 中定义的探测点触发时调用 pre_handler:

static int pre_handler_kretprobe(struct kprobe *p, struct pt_regs *regs)
{
    struct kretprobe *rp = container_of(p, struct kretprobe, kp);
 ...
    // 如果当前上下文在 nmi 中,为了防止死锁,是不会进行 hook 的,只增加nmissed计数,让用户知道发生了什么。
    if (unlikely(in_nmi())) {
		rp->nmissed++;
		return 0;
	}

	/* TODO: consider to only swap the RA after the last pre_handler fired */
 	// 将 current 做 hash,然后从 rp 对应的 instance 中取出一个实例使用,该实例以这个 hash 为 key 值存放到全局 hash 中,后续跳板函数,根据 task hash 得到该实例,做进一步处理。
	hash = hash_ptr(current, KPROBE_HASH_BITS);
	raw_spin_lock_irqsave(&rp->lock, flags);
    // 如果 rp->free_instances 为空,则没有可用实例了,增加 nmissed ,并直接返回。
	if (!hlist_empty(&rp->free_instances)) {
        // 从链表中取出一个实例使用。
		ri = hlist_entry(rp->free_instances.first,
				struct kretprobe_instance, hlist);
		hlist_del(&ri->hlist);
		raw_spin_unlock_irqrestore(&rp->lock, flags);

		ri->rp = rp;
		ri->task = current;

        // 在这里调用我们自己定义的 entry_handler,在进入函数之前调用。
		if (rp->entry_handler && rp->entry_handler(ri, regs)) {
            // 返回不为 0,说明有错,直接归还实例,并返回。
			raw_spin_lock_irqsave(&rp->lock, flags);
			hlist_add_head(&ri->hlist, &rp->free_instances);
			raw_spin_unlock_irqrestore(&rp->lock, flags);
			return 0;
		}

        // 开始预备返回 hook。
		arch_prepare_kretprobe(ri, regs);

		/* XXX(hch): why is there no hlist_move_head? */
		INIT_HLIST_NODE(&ri->hlist);
		kretprobe_table_lock(hash, &flags);
        // 将实例根据 task hash 加入到全局 kretprobe_inst_table 中,后续返回处使用。
		hlist_add_head(&ri->hlist, &kretprobe_inst_table[hash]);
		kretprobe_table_unlock(hash, &flags);
	} else {
		rp->nmissed++;
		raw_spin_unlock_irqrestore(&rp->lock, flags);
	}
	return 0;
}

// 对于 arm64 预备一个返回 hook,将实际的返回地址保存到 ret_addr 中,修改现有返回地址为自定义的跳板函数地址 kretprobe_trampoline。
void __kprobes arch_prepare_kretprobe(struct kretprobe_instance *ri,
				      struct pt_regs *regs)
{
	ri->ret_addr = (kprobe_opcode_t *)regs->regs[30];

	/* replace return addr (x30) with trampoline */
	regs->regs[30] = (long)&kretprobe_trampoline;
}


ENTRY(kretprobe_trampoline)
	sub sp, sp, #S_FRAME_SIZE

    // 保存返回之前的寄存器信息,用于后续调用 trampoline_probe_handler 之后恢复原始的 pstate,栈信息。
	save_all_base_regs

	mov x0, sp
    // 执行自定义的kretprobe 处理函数
	bl trampoline_probe_handler
    // 将 trampoline_probe_handler 的返回作为 lr 的新值,恢复原本的返回路径。
	/*
	 * Replace trampoline address in lr with actual orig_ret_addr return
	 * address.
	 */
	mov lr, x0

    // 恢复原始的 pstate,栈信息。
	restore_all_base_regs

	add sp, sp, #S_FRAME_SIZE
	ret

ENDPROC(kretprobe_trampoline)
    
void __kprobes __used *trampoline_probe_handler(struct pt_regs *regs)
{
    // 可能有多个 cpu 进入该处,通过 current 计算 hash 得到实际任务对应的 kretprobe 实例 rp。
	hlist_for_each_entry_safe(ri, tmp, head, hlist) {
		if (ri->task != current)
			/* another task is sharing our hash bucket */
			continue;

		orig_ret_address = (unsigned long)ri->ret_addr;

		if (orig_ret_address != trampoline_address)
			/*
			 * This is the real return address. Any other
			 * instances associated with this task are for
			 * other calls deeper on the call stack
			 */
			break;
	}
...
	correct_ret_addr = ri->ret_addr;
	hlist_for_each_entry_safe(ri, tmp, head, hlist) {
		if (ri->task != current)
			/* another task is sharing our hash bucket */
			continue;

		orig_ret_address = (unsigned long)ri->ret_addr;
		if (ri->rp && ri->rp->handler) {
			__this_cpu_write(current_kprobe, &ri->rp->kp);
			get_kprobe_ctlblk()->kprobe_status = KPROBE_HIT_ACTIVE;
			ri->ret_addr = correct_ret_addr;
            // 调用kreprobe中注册的 handler,在函数返回前夕调用。
			ri->rp->handler(ri, regs);
			__this_cpu_write(current_kprobe, NULL);
		}

        // 将该实例返回 rp->free_instance,本次已处理完毕。
		recycle_rp_inst(ri, &empty_rp);

		if (orig_ret_address != trampoline_address)
			/*
			 * This is the real return address. Any other
			 * instances associated with this task are for
			 * other calls deeper on the call stack
			 */
			break;
	}
    // 返回真实的返回地址。
	return (void *)orig_ret_address;
}

至此,一个 kretprobe 的注册到整个调用逻辑分析完毕。

4 总结

kprobe 机制可以动态在内核任意合规地址注册探测点,使其实现灵活的内核调试机制,它也是后续各种调试基础以及拓展接口,其中 kretprobe 就是在 kprobe 基础上实现的一个捕获函数入口及返回 hook 的机制实现。

eBPF示例:x86-64平台上的kprobe 讲解 libbpf-bootstrap 中 kprobe 在x86-64平台上的示例代码; 重点讲: BPF_KPROBE 宏展开; 低版本内核,不支持CO-RE,如何重新实现kprobe示例功能; 阅读详情

相关推荐

Go+eBPF kprobe 禁止运行指定程序

本文详细介绍了如何使用Go语言结合eBPF技术,通过kprobe机制拦截Linux系统调用execve来禁止指定程序的运行。首先解释了项目背景与应用场景,如防止服务器重启、未经授权的文件下载等。接着深入探讨了实现原理,包括kprobe的工作方式和如何利用它监听execve调用,以及使用bpf_override_return API修改函数返回值以阻止程序执行。

Linux内核研究者 2106

Linux kprobe的使用

Linux kprobe的简单使用

小立仔 5331

trace系列4 - kprobe学习笔记

1.前言 本文主要是根据阅码场 《Linux内核tracers的实现原理与应用》视频课程在aarch64上的实践。通过观察钩子函数的创建过程以及替换过程,理解trace的原理。本文同样以blk_update_request函数为例进行说明function trace kprobe的工作原理,此处的kprobe是基于function trace来实现。 kernel版本:5.10 平台:arm64 2. function trace钩子函数替换过程 2.1 编译阶段 同Linux ftrace学习笔记中编

HZero 3597

Linux kprobe原理

Linux kprobe原理,以及其优化简介

小立仔 3974

手把手教你用Matlab实现Copula函数:从理论到代码的完整指南

本文提供了一份在MATLAB中实现Copula函数的完整实战指南。从理解Sklar定理和常见Copula家族(如高斯、t、Clayton)的核心概念出发,详细讲解了数据预处理、边缘分布拟合、概率积分变换等关键步骤。通过具体代码示例,手把手演示了Copula模型的拟合、评估、蒙特卡洛模拟,并进阶应用于金融风险度量(如VaR计算)和条件分布分析,帮助读者掌握这一强大的多变量依赖建模工具。

y9z0a1b的博客 949

USB2.0基础知识总结

目录 1、协议层: 1.1、域分类 1.2 、USB传输方式: 1、协议层: 域和包定义:包是一次传输的单位,每个包由多个域组成; USB中是按照LSB进行字节传输; 1.1、域分类 1.1.1: 同步域(SYNC field) 所有的包都开始于同步域,在低速和全速通信中SYNC field用7个0和1个1来表示。 由于USB2.0使用了NRZI编码(0用翻转来表示,1用不翻转表示),所以00000001就是上图那样。 ...

qq_32938605的博客 4403

Linux内核调试技术之kprobes:基本原理与使用

Linux kprobes技术是一种可以跟踪内核函数执行状态的轻量级内核调试技术,利用kprobes技术可以在运行的内核中动态的插入探测点,当内核运行到该探测点后可以执行用户预定义的回调函数,以收集所需的调试状态信息而基本不影响内核原有的执行流程。{return 0;}.symbol_name = "do_fork", // 要追踪的内核函数为 do_fork.pre_handler = handler_pre // pre_handler 回调函数。

Aspiresky的专栏 3160

Pixhawk RPi CM4 Baseboard 树莓派CM4安装Ubuntu20.04 server 配置ros mavros mavsdk

cm4配置ros mavros连接pixhawk4 Pixhawk RPi CM4 Baseboard

baidu_41704597的博客 2120

kprobe实现原理解析

一、简介 kprobe是内核的动态探测工具,几乎可以探测任何一条内核指令。kprobe根据探测点类型可分为三种: kprobes, jprobes和kretprobes (也叫返回探测点)。 kprobes是可以被插入到内核的任何指令位置的探测点,jprobes则只能被插入到一个内核函数的入口,而kretprobes则是在指定的内核函数返回时才被执行。 kprobe...

hjkfcz的博客 3485

Livox MID-360与FAST-LIO2实战:从驱动部署到参数调优的完整指南

激光雷达惯性里程计(LIO)是机器人定位与建图(SLAM)领域的核心技术,它通过紧耦合融合激光雷达点云与惯性测量单元(IMU)数据,实现高精度、低漂移的位姿估计。其核心原理在于利用IMU的高频数据预测运动,并用激光雷达的精确测距信息进行校正与优化,从而在快速运动或特征缺失场景下保持鲁棒性。这项技术的工程价值在于为自动驾驶、移动机器人及三维重建提供了可靠的前端里程计解决方案。针对Livox MID-360固态雷达与FAST-LIO2这一热门组合,其应用场景广泛覆盖了室内外导航与地图构建。本文聚焦于**驱动部署

weixin_29997523的博客 440

老款 NUC 复活计划:装 Ubuntu 部署 OpenClaw 本地 AI

老款 NUC 变身本地 AI 服务器指南 本文详细介绍了如何将老旧华硕 NUC 设备改造为本地 AI 服务器。主要内容包括: 硬件评估:列出老款 NUC 的最低配置要求(i3 6代/8GB内存/256GB SSD),并提供兼容性检查命令和性能预期表 系统准备:指导备份数据、下载 Ubuntu 22.04 LTS 和制作启动盘的方法(Windows/Linux双平台) 安装流程:从 BIOS 设置到 Ubuntu 安装的完整步骤,包括推荐的分区方案 系统优化:禁用不必要的服务、内核参数调优、Swap 配置和

博客 7600

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

Linux内核 eBPF基础 kprobe原理源码分析:基本介绍与使用 荣涛 2021年5月11日

RToax 8403

Python爬虫实战:深度解析携程/飞猪酒店评论API与大数据采集

本文详细介绍了如何通过Python爬虫技术获取携程和飞猪平台的酒店评论数据。我们涵盖了从API分析、反爬虫对策到数据存储和处理的完整流程。API逆向分析:通过浏览器开发者工具发现和分析API接口反爬虫对策:使用请求头随机化、IP代理、浏览器自动化等技术异步处理:使用asyncio提高爬取效率数据质量:实现数据清洗和质量检查机制系统架构:构建可扩展的分布式爬虫系统未来改进方向:机器学习识别验证码分布式爬虫架构实时数据流处理更智能的反反爬虫策略。

2201_76125261的博客 596

Linux内核调试技术——kprobe使用与实现(五)

Linux内核调试技术——kprobe使用与实现(一)Linux内核调试技术——kprobe使用与实现(二)Linux内核调试技术——kprobe使用与实现(三)Linu...

Linux知识积累 944

ICML-2025《Learning from Sample Stability for Deep Clustering》

用于指导深度聚类模型的训练,从而提升模型对困难样本的学习能力,最终改善整体聚类性能。首先,利用K-means在原始样本(非增强)的表征上进行聚类,得到初始聚类分配。其目的是鼓励模型对同一稳定样本在连续epoch中产生相近的表征。输出差异大)会产生更大的损失值,从而引导模型优先优化这些困难样本。此损失借鉴了NT-Xent损失的思想,但将正样本对定义为。最后,通过对比学习优化聚类中心,将同一聚类在。为了获得更具代表性的聚类中心,论文提出。为了学习更鲁棒的表征,模型在在线网络。首先,论文定义了样本。

Christo的博客 823

十、Teigha.Net示例的源代码下载链接,以及英文的chm说明文件和相关案例

目录官方案例demo下载Teigha.net 英文版手册下载Teigha.net Entity实体中文翻译Teigha4.0 解决闪烁问题demo下载Teigha4.0 收录demo案例类似CAD看图王软件无AutoCAD环境下操作显示CAD文件的功能源码,整个示例模块源码 官方案例demo下载 在之前找到的官方案例3_03版本中包含32位与64位dll 其中的chm的说明文件内容也不完全,后面有更为详细的英文版的chm; 示例项目中,主要包括一个winfrom程序与控制台程序,OdWriteExMgd

... 3512

分析kprobe原理

Linux kprobe原理分析

greenandblue的专栏 745

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

Linux内核中Kprobes调试技术的实现

内核开发者一直在试图寻找一种快捷高效的内核调试手段,用于内核开发之中。高效的调试技术有利于提高内核开发效率,缩短内核开发周期。 本文研究了一种新型的内核调试技术―Kprobes , Kprobes 是一个轻量级的内核调试工具,利用Kprobes 技术可以在运行的内核中动态的插入探测点,在探测点处执行用户预定义的操作。本文首先根据Kprobes 在Linux 内核中的源码实现,针对Linu...

云计算? 170

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

Linux内核调试技术——jprobe使用与实现

前一篇博文介绍了kprobes的原理与kprobe的使用与实现方式,本文介绍kprobes中的另外一种探測技术jprobe。它基于kprobe实现,不能在函数的任何位置插入探測点,仅仅能在函数的入口处探測。一般用于监測函数的入參值。本文首先通过一个简单的演示样例介绍jprobe的使用方式。然后通过源代码具体分析jprobe的实现流程。 内核源代码:Linux-4.1.x 实验...

weixin_30768175的博客 197

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

深入ftrace kprobe原理解析

至此,我们知道Kprobe实现的本质是breakpoint和single-step的结合,这一点和大多数调试工具一样,比如kgdb/gdb。当 kprobe 被注册后,内核会将对应地址的指令进行拷贝并替换为断点指令(比如 X86 中的int 3)随后当内核执行到对应地址时,中断会被触发从而执行流程会被重定向到我们注册的函数当对应地址的原始指令执行完后,内核会再次执行从而实现指令级别的内核动态监控。也就是说,kprobe 不仅可以跟踪任意带有符号的内核函数,也可以跟踪函数中间的任意指令。

奇小葩 5457

【原创】Linux kprobe 实现原理 图文详解 (一) kprobe实现原理

图文详解 linux kprobe 底层实现原理

h_b__k的博客 2107

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

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

zhaoxin1989的博客 2786

内核调测工具kprobe之原理篇

上篇文章我们讲了Kprobe的用法,这次我们一起看下其实现的原理。在上次的模块例子中插入dump_stack函数,获得调用栈的情况,根据栈来反推其调用流程:Calltrace: [&lt...

Peter的专栏 1449

kprobe 内核实现原理

kprobe内核实现原理代码分析

OS Developer的博客 663

linux kprobe实现原理

linux kprobes kprobes出现了非常长的时间,最早是2.6.9版本中,最早能够追溯到2004年,距离现在15年,一直没有为人所知,但却是隐藏在诸多技术后的一个基础组件,例如ftrace,perf,SystemTap,LTTng还有最近非常火热的ebpf. kprobes是什么? 它是kernel probes(我翻译成探针),是一种debug机制,它是由IBM开发的,最初计划用来和...

faxiang1230的专栏 1830
上一篇: 早期内存分配器 memblock 详解
下一篇: tracepoint 原理详细分析
内核新视界
博客等级 码龄8年 478粉丝 44原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值