Linux kprobe原理(1)

enum {
X86_TRAP_DE = 0, /* 0, Divide-by-zero */
X86_TRAP_DB, /* 1, Debug */
X86_TRAP_NMI, /* 2, Non-maskable Interrupt */
X86_TRAP_BP, /* 3, Breakpoint */
X86_TRAP_OF, /* 4, Overflow */
X86_TRAP_BR, /* 5, Bound Range Exceeded */
X86_TRAP_UD, /* 6, Invalid Opcode */
X86_TRAP_NM, /* 7, Device Not Available */
X86_TRAP_DF, /* 8, Double Fault */
X86_TRAP_OLD_MF, /* 9, Coprocessor Segment Overrun */
X86_TRAP_TS, /* 10, Invalid TSS */
X86_TRAP_NP, /* 11, Segment Not Present */
X86_TRAP_SS, /* 12, Stack Segment Fault */
X86_TRAP_GP, /* 13, General Protection Fault */
X86_TRAP_PF, /* 14, Page Fault */
X86_TRAP_SPURIOUS, /* 15, Spurious Interrupt */
X86_TRAP_MF, /* 16, x87 Floating-Point Exception */
X86_TRAP_AC, /* 17, Alignment Check */
X86_TRAP_MC, /* 18, Machine Check */
X86_TRAP_XF, /* 19, SIMD Floating-Point Exception */
X86_TRAP_IRET = 32, /* 32, IRET Exception */
};



// linux-3.10/arch/x86/kernel/traps.c

/* May run on IST stack. */
dotraplinkage void __kprobes notrace do_int3(struct pt_regs *regs, long error_code)
{

//当 CPU 遇到断点指令时,会发生陷阱,保存 CPU 的寄存器,并通过 notifier_call_chain 机制将控制权传递给 Kprobes。
if (notify_die(DIE_INT3, “int3”, regs, error_code, X86_TRAP_BP,
SIGTRAP) == NOTIFY_STOP)
goto exit;

}


之后会执行通知链机制上注册的回调函数:kprobe\_exceptions\_notify,对于int 3 指令就是kprobe\_handler函数:



int3
–>do_int3
–>notify_die(DIE_INT3, “int3”, regs, error_code, X86_TRAP_BP,
SIGTRAP) == NOTIFY_STOP)
–>kprobe_exceptions_notify(){
case DIE_INT3:
if (kprobe_handler(args->regs))
ret = NOTIFY_STOP;
break;
}



/*
* Interrupts are disabled on entry as trap3 is an interrupt gate and they
* remain disabled throughout this function.
*/
static int __kprobes kprobe_handler(struct pt_regs *regs)
{
kprobe_opcode_t *addr;
struct kprobe *p;
struct kprobe_ctlblk *kcb;

addr = (kprobe\_opcode\_t \*)(regs->ip - sizeof(kprobe\_opcode\_t));
/\*

* We don’t want to be preempted for the entire
* duration of kprobe processing. We conditionally
* re-enable preemption at the end of this function,
* and also in reenter_kprobe() and setup_singlestep().
*/
preempt_disable();

kcb = get\_kprobe\_ctlblk();
p = get\_kprobe(addr);

if (p) {
	if (kprobe\_running()) {
		if (reenter\_kprobe(p, regs, kcb))
			return 1;
	} else {
		set\_current\_kprobe(p, regs, kcb);
		kcb->kprobe_status = KPROBE_HIT_ACTIVE;

		/\*

* If we have no pre-handler or it returned 0, we
* continue with normal processing. If we have a
* pre-handler and it returned non-zero, it prepped
* for calling the break_handler below on re-entry
* for jprobe processing, so get out doing nothing
* more here.
*/
if (!p->pre_handler || !p->pre_handler(p, regs))
setup_singlestep(p, regs, kcb, 0);
return 1;
}
} else if (*addr != BREAKPOINT_INSTRUCTION) {
/*
* The breakpoint instruction was removed right
* after we hit it. Another cpu has removed
* either a probepoint or a debugger breakpoint
* at this address. In either case, no further
* handling of this interrupt is appropriate.
* Back up over the (now missing) int3 and run
* the original instruction.
*/
regs->ip = (unsigned long)addr;
preempt_enable_no_resched();
return 1;
} else if (kprobe_running()) {
p = __this_cpu_read(current_kprobe);
if (p->break_handler && p->break_handler(p, regs)) {
if (!skip_singlestep(p, regs, kcb))
setup_singlestep(p, regs, kcb, 0);
return 1;
}
} /* else: not a kprobe fault; let the kernel handle it */

preempt\_enable\_no\_resched();
return 0;

}


对于kprobe我们主要分析这一部分:


与x86\_64有关的EFLAGS 寄存器的flag位:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/60a87169a2fa40de88a0b654f0867e60.png)  
 (1)TF Trap (bit 8):设置启用单步模式进行调试; 清除以禁用单步模式。 在单步模式下,处理器在每条指令后生成一个调试异常。 这允许在每条指令之后检查程序的执行状态。如果应用程序使用 POPF、POPFD 或 IRET 指令设置 TF 标志,则会在 POPF、POPFD 或 IRET 之后的指令之后生成调试异常。


(2)IF Interrupt enable (bit 9):控制处理器对可屏蔽硬件中断请求的响应,该标志设置为响应可屏蔽的硬件中断; 清除以禁止可屏蔽的硬件中断。 IF 标志不影响异常或不可屏蔽中断(NMI 中断)的生成。控制寄存器 CR4 中的 CPL、IOPL 和 VME 标志的状态决定了 IF 标志是否可以被 CLI、STI、POPF、POPFD 和 IRET 修改


set\_current\_kprobe设置struct kprobe \*p为当前正在处理的 probe点。



// linux-3.10/arch/x86/kernel/kprobes/core.c

static void __kprobes set_current_kprobe(struct kprobe *p, struct pt_regs *regs,
struct kprobe_ctlblk *kcb)
{
__this_cpu_write(current_kprobe, p);
kcb->kprobe_saved_flags = kcb->kprobe_old_flags
= (regs->flags & (X86_EFLAGS_TF | X86_EFLAGS_IF));
if (p->ainsn.if_modifier)
kcb->kprobe_saved_flags &= ~X86_EFLAGS_IF;
}



set\_current\_kprobe(p, regs, kcb);
kcb->kprobe_status = KPROBE_HIT_ACTIVE;

/\*

* If we have no pre-handler or it returned 0, we
* continue with normal processing. If we have a
* pre-handler and it returned non-zero, it prepped
* for calling the break_handler below on re-entry
* for jprobe processing, so get out doing nothing
* more here.
*/
if (!p->pre_handler || !p->pre_handler(p, regs))
setup_singlestep(p, regs, kcb, 0);
return 1;


这里在设置current\_kprobe全局变量的同时,还会同时设置kprobe\_saved\_flags和kprobe\_old\_flags的flag值,它们用于具体的架构指令相关处理。接下来处理pre\_handler回调函数,有注册的话就调用执行,然后调用setup\_singlestep启动单步执行。在调试完成后直接返回1。



static void __kprobes
setup_singlestep(struct kprobe *p, struct pt_regs *regs, struct kprobe_ctlblk *kcb, int reenter)
{
if (setup_detour_execution(p, regs, reenter))
return;

#if !defined(CONFIG_PREEMPT)
if (p->ainsn.boostable == 1 && !p->post_handler) {
/* Boost up – we can execute copied instructions directly */
if (!reenter)
reset_current_kprobe();
/*
* Reentering boosted probe doesn’t reset current_kprobe,
* nor set current_kprobe, because it doesn’t use single
* stepping.
*/
regs->ip = (unsigned long)p->ainsn.insn;
preempt_enable_no_resched();
return;
}
#endif
if (reenter) {
save_previous_kprobe(kcb);
set_current_kprobe(p, regs, kcb);
kcb->kprobe_status = KPROBE_REENTER;
} else
kcb->kprobe_status = KPROBE_HIT_SS;
/* Prepare real single stepping */
clear_btf();

//设置regs->flags中的TF位,开启单步调试
regs->flags |= X86_EFLAGS_TF;

//屏蔽regs->flags中的IF位,屏蔽中断
regs->flags &= ~X86_EFLAGS_IF;

/\* single step inline if the instruction is an int3 \*/
//指令寄存器地址改为前面保存的被探测指令(备份的原始指令)
if (p->opcode == BREAKPOINT_INSTRUCTION)
	regs->ip = (unsigned long)p->addr;
else
	regs->ip = (unsigned long)p->ainsn.insn;

}


单步执行,首先设置EFLAGS 寄存器flags中的TF位,并屏蔽IF位,同时把int3异常返回的指令寄存器地址改为前面保存的被探测指令,当int3异常返回时这些设置就会生效,即立即执行保存的原始指令(注意这里是在触发int3之前原来的上下文中执行,因此直接执行原始指令即可,无需特别的模拟操作)。该函数返回后do\_int3函数立即返回,由于EFLAGS 寄存器TF位被设置,在单步执行完被探测指令后立即触发debug异常,进入debug异常处理函数do\_debug,执行post\_kprobe\_handler函数,即post\_handler()。


### 3.3 do\_bug



dotraplinkage void __kprobes do_debug(struct pt_regs *regs, long error_code)
{

if (notify_die(DIE_DEBUG, “debug”, regs, PTR_ERR(&dr6), error_code,
SIGTRAP) == NOTIFY_STOP)

}


由于初始化时注册了内核通知链:kprobe\_exceptions\_nb,执行被探测指令期间若发生了内存异常,比如执行了debug指令, 将最优先调用kprobe\_exceptions\_notify函数。



/*
* Wrapper routine for handling exceptions.
*/
int __kprobes
kprobe_exceptions_notify(struct notifier_block *self, unsigned long val, void *data)
{

case DIE_DEBUG:
if (post_kprobe_handler(args->regs)) {
/*
* Reset the BS bit in dr6 (pointed by args->err) to
* denote completion of processing
*/
(*(unsigned long *)ERR_PTR(args->err)) &= ~DR_STEP;
ret = NOTIFY_STOP;
}
break;

}



/*
* Interrupts are disabled on entry as trap1 is an interrupt gate and they
* remain disabled throughout this function.
*/
static int __kprobes post_kprobe_handler(struct pt_regs *regs)
{
struct kprobe *cur = kprobe_running();
struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();

if (!cur)
	return 0;

resume\_execution(cur, regs, kcb);
regs->flags |= kcb->kprobe_saved_flags;

if ((kcb->kprobe_status != KPROBE_REENTER) && cur->post_handler) {
	kcb->kprobe_status = KPROBE_HIT_SSDONE;
	cur->post\_handler(cur, regs, 0);
}

/\* Restore back the original saved kprobes variables and continue. \*/
if (kcb->kprobe_status == KPROBE_REENTER) {
	restore\_previous\_kprobe(kcb);
	goto out;
}
reset\_current\_kprobe();

out:
preempt_enable_no_resched();

/\*

* if somebody else is singlestepping across a probe point, flags
* will have TF set, in which case, continue the remaining processing
* of do_debug, as if this is not a probe hit.
*/
if (regs->flags & X86_EFLAGS_TF)
return 0;

return 1;

}


首先调用resume\_execution函数将debug异常返回的下一条指令设置为被探测之后的指令,这样异常返回后程序的流程就会按正常的流程继续执行;然后恢复kprobe执行前保存的flags标识;接下来如果kprobe不是重入的并且设置了post\_handler回调函数,就设置kprobe\_status状态为KPROBE\_HIT\_SSDONE并调用post\_handler函数,即调用用户态设置的post\_handler回调函数。


## 四、Changing Execution Path


由于 kprobes 可以探测正在运行的内核代码,它可以更改寄存器集,包括指令指针。 此操作需要非常小心,例如保留堆栈帧,恢复执行路径等。因为它在运行的内核上运行并且需要深入了解计算机体系结构。


如果您更改 pre\_handler 中的指令指针(并设置其他相关寄存器),则必须返回 !0 以便 kprobes 停止单步执行并返回到给定地址。 这也意味着不应再调用 post\_handler。


请注意,在某些使用 TOC(Table of Contents)进行函数调用的架构上,此操作可能会更难,因为您必须在模块中为您的函数设置一个新的 TOC,并在从它返回后恢复旧的 TOC。


## 五、Return Probes


### 5.1 How Does a Return Probe Work


当您调用 register\_kretprobe() 时,Kprobes 在函数的入口处建立一个 kprobe。 当被探测的函数被调用并且这个探测被命中时,Kprobes 会保存一份返回地址的副本,并将返回地址替换为“trampoline”的地址。trampoline是一段任意代码——通常只是一条 nop 指令。 在启动时,Kprobes 在 trampoline 上注册一个 kprobe。


当被探测的函数执行它的 return instruction时,控制权传递给trampoline并且该探测被命中。 Kprobes 的 trampoline 处理程序调用与 kretprobe 关联的用户指定的返回处理程序,然后将保存的指令指针设置为保存的返回地址,这就是从陷阱返回后恢复执行的地方。


当被探测函数正在执行时,它的返回地址存储在一个 kretprobe\_instance 类型的对象中。 在调用 register\_kretprobe() 之前,用户设置 kretprobe 结构的 maxactive 字段来指定可以同时探测多少个指定函数的实例。 register\_kretprobe() 预分配指定数量的 kretprobe\_instance 对象。


例如,如果函数是非递归的并且在调用时持有自旋锁,那么 maxactive = 1 就足够了。 如果函数是非递归的并且永远不会放弃 CPU(例如,通过信号量或抢占),则 NR\_CPUS 应该足够了。 如果 maxactive <= 0,则设置为默认值。 如果启用了 CONFIG\_PREEMPT,则默认值为 max(10, 2\*NR\_CPUS)。 否则,默认值为 NR\_CPUS。


如果你将 maxactive 设置得太低,这不是一场灾难; 你只会错过一些探测。 在 kretprobe 结构中,nmissed 字段在注册返回探针时设置为零,并且每次进入被探测函数但没有可用于建立返回探针的 kretprobe\_instance 对象时递增。


### 5.2 Kretprobe entry-handler


Kretprobes 还提供了一个可选的用户指定的处理程序,它在函数入口上运行。 该处理程序是通过设置 kretprobe 结构的 entry\_handler 字段来指定的。 每当 kretprobe 放置在函数入口处的 kprobe 被命中时,都会调用用户定义的 entry\_handler,如果有的话。 如果 entry\_handler 返回 0(成功),则保证在函数返回时调用相应的返回处理程序。 如果 entry\_handler 返回非零错误,则 Kprobes 将返回地址保持原样,并且 kretprobe 对该特定函数实例没有进一步的影响。


使用与它们关联的唯一 kretprobe\_instance 对象来匹配多个入口和返回处理程序调用。 此外,用户还可以将每个返回实例的私有数据指定为每个 kretprobe\_instance 对象的一部分。 这在相应的用户条目和返回处理程序之间共享私有数据时特别有用。 每个私有数据对象的大小可以在 kretprobe 注册时通过设置 kretprobe 结构的 data\_size 字段来指定。 可以通过每个 kretprobe\_instance 对象的数据字段访问此数据。


如果输入了探测函数但没有可用的 kretprobe\_instance 对象,则除了增加 nmissed 计数外,还会跳过用户 entry\_handler 调用。


## 六、How Does Jump Optimization Work


关于kprobe的优化可以参考这篇文章:[linux kprobe实现原理](https://bbs.csdn.net/topics/618542503)


如果Linux 内核是使用 CONFIG\_OPTPROBES=y 构建的(目前此标志在 x86/x86-64 非抢占式内核上自动设置为 ‘y’)并且“debug.kprobes\_optimization”内核参数设置为 1 ,Kprobes 会尝试减少探测 - 通过在每个探测点使用跳转指令而不是断点指令来降低开销。


int 3 指令会产生一个 a trap ,比较耗时,可以用跳转指令替换断点指令,优化成jmp指令跳转到kprobe探测点。


当前的机器默认配置了 CONFIG\_OPTPROBES 选项:



[root@localhost ~]# cat /etc/centos-release
CentOS Linux release 7.6.1810 (Core)

[root@localhost ~]# uname -r
3.10.0-957.el7.x86_64



Kernel Performance Events And Counters

CONFIG_SLUB=y
CONFIG_PROFILING=y
CONFIG_TRACEPOINTS=y
CONFIG_CRASH_CORE=y
CONFIG_KEXEC_CORE=y
CONFIG_HOTPLUG_SMT=y
CONFIG_OPROFILE=m
CONFIG_OPROFILE_EVENT_MULTIPLEX=y
CONFIG_HAVE_OPROFILE=y
CONFIG_OPROFILE_NMI_TIMER=y
CONFIG_KPROBES=y
CONFIG_JUMP_LABEL=y

CONFIG_OPTPROBES=y //当前的机器配置了 CONFIG_OPTPROBES 选项


debug.kprobes\_optimization内核参数同样也设置为 1:



[root@localhost ~]# cat /proc/sys/debug/kprobes-optimization
1
[root@localhost ~]#


### 6.1 Init a Kprobe


注册一个 probe 后,在尝试此优化之前,Kprobes会在指定地址插入一个基于断点的普通kprobe。因此,即使无法优化这个特定的probepoint,也会有一个探针。


### 6.2 Safety Check


在优化探针之前,Kprobes会执行以下安全检查,不符合条件不可以进行优化:  
 (1)Kprobes 验证将被跳转指令替换的区域(“优化区域”)是否完全位于一个函数中。 (跳转指令是5个字节:near relative jump,因此可能会覆盖多个指令。)  
 (2)Kprobes 分析整个函数并验证没有跳转到优化区域,不能有跳转到这块要被优化区域的指令,这块区域将会被jmp覆盖,具体如下:  
 a:函数中不包含间接跳转(indirect jump);  
 b:该函数不包含导致异常的指令(因为由异常触发的修复代码可以跳回优化区域 - Kprobes 检查异常表以验证这一点);  
 c:没有到优化区域的近跳转(near jump)(除了第一个字节)。  
 (3)对于优化区域中的每条指令,Kprobes将验证该指令是否可以单独执行。


使用如下跳转指令(near jump)形式:



JMP 跳转指令:
0xE9(E9 cd) :Jump near 后面的4个字节是偏移:一个保存jmp本身的机器码,另4个保存偏移 -->总共5个字节


### 6.3 Preparing Detour Buffer


接下来,Kprobes准备了一个 Detour 缓冲区,其中包含以下指令序列:  
 (1)能够将cpu寄存器压栈(模拟int3的trap过程)。  
 (2)调用用户的探测处理程序的蹦床代码(trampoline code)。   
 (3)恢复寄存器的代码。  
 (4)来自优化区域的指令。  
 (5)跳转回原来的执行路径。


### 6.4 Pre-optimization


准备 Detour 缓冲区后,Kprobes验证以下情况是否存在:  
 (1)探针有一个 post\_handler。  
 (2)探测优化区域中的其他指令。  
 (3)探针被禁用。  
 在上述任何一种情况下,Kprobes 都不会开始优化探针。 由于这些是临时情况,如果情况发生变化,Kprobes 会尝试再次开始优化。


如果可以优化 kprobe,则 Kprobes 将 kprobe 排入优化列表,并启动 kprobe-optimizer 工作队列以对其进行优化。如果要优化的probepoint在优化之前被命中,则Kprobes通过将CPU的指令指针设置为 the detour buffer 中复制的代码,将控制权返回到原始指令路径,从而至少避免了单步执行。


### 6.5 Optimization


Kprobe-optimizer 不会立即插入跳转指令; 相反,它首先出于安全考虑调用 synchronize\_rcu(),因为 CPU 在执行优化区域的过程中可能会被中断。 synchronize\_rcu() 可以确保在调用 synchronize\_rcu() 时处于活动状态的所有中断都已完成,但前提是 CONFIG\_PREEMPT=n。 因此,此版本的 kprobe 优化仅支持具有 CONFIG\_PREEMPT=n 的内核。


centos 7.6 :3.10.0默认没有开启 CONFIG\_PREEMPT选项:



CONFIG_PREEMPT is not set


之后,Kprobe优化器调用stop\_machine(),使用text\_poke\_smp()将优化区域替换为 Detour 缓冲区的跳转指令。


### 6.6 Unoptimization


当一个优化了的kprobe未注册、禁用或被另一个kprobe阻止时,它将被取消优化。如果在优化完成之前发生这种情况,则kprobe将从优化列表中退出队列。如果优化已经完成,则使用text\_poke\_smp()将跳转替换为原始代码(第一个字节如果是int3断点除外)。假设第二条指令被中断,然后优化器在中断处理程序运行时用跳转地址替换第二条命令。当中断返回到原始地址时,如果没有有效的指令,这将会导致意外的结果。


注意:跳转优化会更改kprobe的pre\_handler行为。如果不进行优化,pre\_handler可以通过更改regs->ip并返回1来更改内核的执行路径,完成内核函数的hook。但是,当优化探针时,该更改将会被忽略,不能内核函数的hook了。


因此,如果要调整内核的执行路径,即hook,需要使用以下技术之一禁止优化:  
 (1)为kprobe的post\_handler指定一个空函数。  
 (2)执行“sysctl-w debug.krobes\_optimization=n”


### 6.7 Blacklist


Kprobes可以探测除Kprobes本身之外的大部分内核函数。这意味着有些函数kprobes无法探测。探测(捕获)此类函数可能会导致递归陷阱(例如 double fault),或者嵌套的探测处理程序可能永远不会被调用。Kprobes使用 a blacklist 来管理该功能,如果要将函数添加到 blacklist中,只需包含linux/kprobes.h并使用NOKPROBE\_SYMBOL()宏指定一个 blacklisted 函数即可。Kprobes根据 blacklist 检查给定的探测地址,如果给定地址在 blacklist 中,则拒绝注册。



// linux-4.10.1/include/linux/kprobes.h

#ifdef CONFIG_KPROBES
/*
* Blacklist ganerating macro. Specify functions which is not probed
* by using this macro.
*/
#define __NOKPROBE_SYMBOL(fname)
static unsigned long __used
__attribute__((section(“_kprobe_blacklist”)))
_kbl_addr_##fname = (unsigned long)fname;
#define NOKPROBE_SYMBOL(fname) __NOKPROBE_SYMBOL(fname)


可以通过 NOKPROBE\_SYMBOL 宏在内核源码中查询内核哪些函数不能被探测:




NOKPROBE_SYMBOL(__context_tracking_enter);
NOKPROBE_SYMBOL(get_kprobe);
NOKPROBE_SYMBOL(notifier_call_chain);
NOKPROBE_SYMBOL(preempt_count_add);
NOKPROBE_SYMBOL(perf_trace_buf_alloc);
NOKPROBE_SYMBOL(FETCH_FUNC_NAME(stack, type));
NOKPROBE_SYMBOL(PRINT_TYPE_FUNC_NAME(tname));


### 6.8 try\_to\_optimize\_kprobe



int register_kprobe(struct kprobe *p)
{

/* Try to optimize kprobe */
try_to_optimize_kprobe§;

}
EXPORT_SYMBOL_GPL(register_kprobe);



/*
* Prepare an optimized_kprobe and optimize it
* NOTE: p must be a normal registered kprobe
*/
static void try_to_optimize_kprobe(struct kprobe *p)
{
struct kprobe *ap;
struct optimized_kprobe *op;

/\* Impossible to optimize ftrace-based kprobe \*/
if (kprobe\_ftrace(p))
	return;

/\* For preparing optimization, jump\_label\_text\_reserved() is called \*/
jump\_label\_lock();
mutex\_lock(&text_mutex);

(1)分配新的 optimized_kprobe 并尝试准备优化的指令
ap = alloc\_aggr\_kprobe(p);
if (!ap)
	goto out;

op = container\_of(ap, struct optimized\_kprobe, kp);
if (!arch\_prepared\_optinsn(&op->optinsn)) {
	/\* If failed to setup optimizing, fallback to kprobe \*/
	arch\_remove\_optimized\_kprobe(op);
	kfree(op);
	goto out;
}

(2)将hlist中较早的kprobe替换为manager kprobe
init\_aggr\_kprobe(ap, p);

(3)开始优化 kprobe 点
optimize\_kprobe(ap);	/\* This just kicks optimizer thread \*/

out:
mutex_unlock(&text_mutex);
jump_label_unlock();
}



CONFIG_OPTPROBES=y



// linux-4.10.1/arch/x86/include/asm/kprobes.h

struct arch_optimized_insn {
/* copy of the original instructions */
kprobe_opcode_t copied_insn[RELATIVE_ADDR_SIZE];
/* detour code buffer */
kprobe_opcode_t *insn;
/* the size of instructions copied to detour code buffer */
size_t size;
};


为kprobe关联一个optimized\_kprobe对象,它有一个detour buffer,保存有一段指令,之后是通过jmp跳转回原始函数。



#ifdef CONFIG_OPTPROBES
/*
* Internal structure for direct jump optimized probe
*/
struct optimized_kprobe {
struct kprobe kp;
struct list_head list; /* list for optimizing queue */
struct arch_optimized_insn optinsn;
};


(1) alloc\_aggr\_kprobe:分配新的 optimized\_kprobe 并尝试准备优化的指令



/* Allocate new optimized_kprobe and try to prepare optimized instructions */
static struct kprobe *alloc_aggr_kprobe(struct kprobe *p)
{
struct optimized_kprobe *op;

op = kzalloc(sizeof(struct optimized\_kprobe), GFP_KERNEL);
if (!op)
	return NULL;

INIT\_LIST\_HEAD(&op->list);
op->kp.addr = p->addr;
arch\_prepare\_optimized\_kprobe(op, p);

return &op->kp;

}


x86\_64 相关优化kprobe的函数都在 /arch/x86/kernel/kprobes/opt.c 文件中



// linux-4.10.1/arch/x86/kernel/kprobes/opt.c

/*
* Copy replacing target instructions
* Target instructions MUST be relocatable (checked inside)
* This is called when new aggr(opt)probe is allocated or reused.
*/
int arch_prepare_optimized_kprobe(struct optimized_kprobe *op,
struct kprobe *__unused)
{
u8 *buf;
int ret;
long rel;

if (!can\_optimize((unsigned long)op->kp.addr))
	return -EILSEQ;

op->optinsn.insn = get\_optinsn\_slot();
if (!op->optinsn.insn)
	return -ENOMEM;

/\*

* Verify if the address gap is in 2GB range, because this uses
* a relative jump.
*/
rel = (long)op->optinsn.insn - (long)op->kp.addr + RELATIVEJUMP_SIZE;
if (abs(rel) > 0x7fffffff) {
__arch_remove_optimized_kprobe(op, 0);
return -ERANGE;
}

buf = (u8 \*)op->optinsn.insn;

/\* Copy instructions into the out-of-line buffer \*/
ret = copy\_optimized\_instructions(buf + TMPL_END_IDX, op->kp.addr);
if (ret < 0) {
	\_\_arch\_remove\_optimized\_kprobe(op, 0);
	return ret;
}
op->optinsn.size = ret;

/\* Copy arch-dep-instance from template \*/
memcpy(buf, &optprobe_template_entry, TMPL_END_IDX);

/\* Set probe information \*/
synthesize\_set\_arg1(buf + TMPL_MOVE_IDX, (unsigned long)op);

/\* Set probe function call \*/
synthesize\_relcall(buf + TMPL_CALL_IDX, optimized_callback);

/\* Set returning jmp instruction at the tail of out-of-line buffer \*/
synthesize\_reljump(buf + TMPL_END_IDX + op->optinsn.size,
		   (u8 \*)op->kp.addr + op->optinsn.size);

flush\_icache\_range((unsigned long) buf,
		   (unsigned long) buf + TMPL_END_IDX +
		   op->optinsn.size + RELATIVEJUMP_SIZE);
return 0;

}


arch\_optimize\_kprobes :用 relative jumps 替换 breakpoints (int3)。


jmp + 偏移的方式,偏移为32bit,所以共占据5个字节:



JMP 跳转指令: 
0xE9(E9 cd) :Jump near 后面的4个字节是偏移:一个保存jmp本身的机器码,另4个保存偏移  -->总共5个字节  

// linux-4.10.1/arch/x86/include/asm/kprobes.h

#define BREAKPOINT_INSTRUCTION	0xcc
#define RELATIVEJUMP_OPCODE 0xe9
#define RELATIVEJUMP_SIZE 5

// linux-4.10.1/arch/x86/kernel/kprobes/opt.c

/\*
 \* Replace breakpoints (int3) with relative jumps.
 \* Caller must call with locking kprobe\_mutex and text\_mutex.
 \*/
void arch\_optimize\_kprobes(struct list_head \*oplist)
{
	struct optimized_kprobe \*op, \*tmp;
	u8 insn_buf[RELATIVEJUMP_SIZE];

	list\_for\_each\_entry\_safe(op, tmp, oplist, list) {
		s32 rel = (s32)((long)op->optinsn.insn -
			((long)op->kp.addr + RELATIVEJUMP_SIZE));

		WARN\_ON(kprobe\_disabled(&op->kp));

		/\* Backup instructions which will be replaced by jump address \*/
		memcpy(op->optinsn.copied_insn, op->kp.addr + INT3_SIZE,
		       RELATIVE_ADDR_SIZE);

		insn_buf[0] = RELATIVEJUMP_OPCODE;
		\*(s32 \*)(&insn_buf[1]) = rel;

		text\_poke\_bp(op->kp.addr, insn_buf, RELATIVEJUMP_SIZE,
			     op->optinsn.insn);

		list\_del\_init(&op->list);
	}
}

/\* Replace a relative jump with a breakpoint (int3). \*/
void arch\_unoptimize\_kprobe(struct optimized_kprobe \*op)
{
	u8 insn_buf[RELATIVEJUMP_SIZE];

	/\* Set int3 to first byte for kprobes \*/
	insn_buf[0] = BREAKPOINT_INSTRUCTION;
	memcpy(insn_buf + 1, op->optinsn.copied_insn, RELATIVE_ADDR_SIZE);
	text\_poke\_bp(op->kp.addr, insn_buf, RELATIVEJUMP_SIZE,
		     op->optinsn.insn);
}


(2) init_aggr_kprobe:“manager kprobe”是必填字段,将hlist中较早的kprobe替换为manager kprobe。

/\*
 \* Fill in the required fields of the "manager kprobe". Replace the
 \* earlier kprobe in the hlist with the manager kprobe
 \*/
static void init\_aggr\_kprobe(struct kprobe \*ap, struct kprobe \*p)
{
	/\* Copy p's insn slot to ap \*/
	copy\_kprobe(p, ap);
	flush\_insn\_slot(ap);
	ap->addr = p->addr;
	ap->flags = p->flags & ~KPROBE_FLAG_OPTIMIZED;
	ap->pre_handler = aggr_pre_handler;
	ap->fault_handler = aggr_fault_handler;
	/\* We don't care the kprobe which has gone. \*/
	if (p->post_handler && !kprobe\_gone(p))
		ap->post_handler = aggr_post_handler;
	if (p->break_handler && !kprobe\_gone(p))
		ap->break_handler = aggr_break_handler;

	INIT\_LIST\_HEAD(&ap->list);
	INIT\_HLIST\_NODE(&ap->hlist);

	list\_add\_rcu(&p->list, &ap->list);
	hlist\_replace\_rcu(&p->hlist, &ap->hlist);
}

(3) optimize_kprobe:开始优化 kprobe 点。

/\* Optimize kprobe if p is ready to be optimized \*/
static void optimize\_kprobe(struct kprobe \*p)
{
	struct optimized\_kprobe \*op;

	/\* Check if the kprobe is disabled or not ready for optimization. \*/
	if (!kprobe\_optready(p) || !kprobes_allow_optimization ||
	    (kprobe\_disabled(p) || kprobes_all_disarmed))
		return;

	/\* Both of break\_handler and post\_handler are not supported. \*/
	if (p->break_handler || p->post_handler)
		return;

	op = container\_of(p, struct optimized\_kprobe, kp);

	/\* Check there is no other kprobes at the optimized instructions \*/
	if (arch\_check\_optimized\_kprobe(op) < 0)
		return;

	/\* Check if it is already optimized. \*/
	if (op->kp.flags & KPROBE_FLAG_OPTIMIZED)
		return;
	op->kp.flags |= KPROBE_FLAG_OPTIMIZED;

	if (!list\_empty(&op->list))
		/\* This is under unoptimizing. Just dequeue the probe \*/
		list\_del\_init(&op->list);
	else {
		list\_add(&op->list, &optimizing_list);
		kick\_kprobe\_optimizer();
	}
}

参考资料

为了做好运维面试路上的助攻手,特整理了上百道 【运维技术栈面试题集锦】 ,让你面试不慌心不跳,高薪offer怀里抱!

这次整理的面试题,小到shell、MySQL,大到K8s等云原生技术栈,不仅适合运维新人入行面试需要,还适用于想提升进阶跳槽加薪的运维朋友。

本份面试集锦涵盖了

  • 174 道运维工程师面试题
  • 128道k8s面试题
  • 108道shell脚本面试题
  • 200道Linux面试题
  • 51道docker面试题
  • 35道Jenkis面试题
  • 78道MongoDB面试题
  • 17道ansible面试题
  • 60道dubbo面试题
  • 53道kafka面试
  • 18道mysql面试题
  • 40道nginx面试题
  • 77道redis面试题
  • 28道zookeeper

总计 1000+ 道面试题, 内容 又全含金量又高

  • 174道运维工程师面试题

1、什么是运维?

2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?

3、现在给你三百台服务器,你怎么对他们进行管理?

4、简述raid0 raid1raid5二种工作模式的工作原理及特点

5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?

6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?

7、Tomcat和Resin有什么区别,工作中你怎么选择?

8、什么是中间件?什么是jdk?

9、讲述一下Tomcat8005、8009、8080三个端口的含义?

10、什么叫CDN?

11、什么叫网站灰度发布?

12、简述DNS进行域名解析的过程?

13、RabbitMQ是什么东西?

14、讲一下Keepalived的工作原理?

15、讲述一下LVS三种模式的工作过程?

16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?

17、如何重置mysql root密码?

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
17道ansible面试题

  • 60道dubbo面试题
  • 53道kafka面试
  • 18道mysql面试题
  • 40道nginx面试题
  • 77道redis面试题
  • 28道zookeeper

总计 1000+ 道面试题, 内容 又全含金量又高

  • 174道运维工程师面试题

1、什么是运维?

2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?

3、现在给你三百台服务器,你怎么对他们进行管理?

4、简述raid0 raid1raid5二种工作模式的工作原理及特点

5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?

6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?

7、Tomcat和Resin有什么区别,工作中你怎么选择?

8、什么是中间件?什么是jdk?

9、讲述一下Tomcat8005、8009、8080三个端口的含义?

10、什么叫CDN?

11、什么叫网站灰度发布?

12、简述DNS进行域名解析的过程?

13、RabbitMQ是什么东西?

14、讲一下Keepalived的工作原理?

15、讲述一下LVS三种模式的工作过程?

16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?

17、如何重置mysql root密码?

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值