ebpf学习-从linux内核源码开始

两只羊 Lv3

什么是ebpf

eBPF(extended Berkeley Packet Filter)是Linux内核3.18引入的革命性技术,它允许在内核空间运行用户定义的沙盒程序,无需修改内核代码或加载内核模块。传统的内核观测手段(如SystemTap、kprobe)需要编译内核模块,存在系统崩溃风险且灵活性差。eBPF通过JIT编译和验证器机制,在保证安全性的前提下,实现了动态、高性能的内核可编程能力。从网络包过滤到性能分析,从安全审计到容器网络,eBPF正在重新定义系统可观测性的边界。

嗯,大概意思就是,在以前的linux版本中,你想写一个内核模块,就要通过传统写.ko的方式,而这种通常一不注意就内核崩溃了。而eBPF则给用户态提供了在内核空间操作的沙箱环境,可以写一些自定义模块去观测改变内核。

在ebpf中,大概有这么几种挂载方式 tracepoint (raw_tracepoint),kprobe,uprobe,fprobe

tracepoint是官方预留的挂载点,非常稳定,版本的变化不会影响太大,比如说我们要拦截一个拦截openat的系统调用,只需要通过设定一个SEC的标致就好了

1
2
3
4
5
6
7
SEC("tracepoint/syscalls/sys_enter_openat")
int handle_openat(struct trace_event_raw_sys_enter *ctx) {
int dfd = ctx->args[0];
const char *filename = (const char *)ctx->args[1];
bpf_printk("Tracepoint: Opening file dfd=%d\n", dfd);
return 0;
}

Raw Tracepoint则是原生的参数,没有一个特定的结构体,需要我们根据内核源码去指定

1
2
3
4
5
6
7
8
9
10
11
12
13
// 同样拦截系统调用入口,但不走 syscalls 子类,直接挂在最底层
SEC("raw_tracepoint/sys_enter")
int handle_raw_sys_enter(struct bpf_raw_tracepoint_args *ctx) {
// ctx->args[0] 实际上是指向 struct pt_regs 的指针
struct pt_regs *regs = (struct pt_regs *)ctx->args[0];
// ctx->args[1] 是系统调用号
long syscall_id = ctx->args[1];

if (syscall_id == 257) { // 257 是 x86_64 的 openat
bpf_printk("Raw TP: Caught syscall %ld\n", syscall_id);
}
return 0;
}

kprobe为动态内核探针。tracepoint只能挂载官方预留的点,而kprobe几乎可以hook所有的内核函数,而且理论上可以hook任意偏移,而问题同样明显,很容易导致崩溃。其原理也是通过设置int3断点,触发异常后跳到ebpf逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 动态 Hook 内核真正的文件打开函数 do_sys_openat2
SEC("kprobe/do_sys_openat2")
int BPF_KPROBE(handle_kprobe_open, int dfd, const char *filename, struct open_how *how) {
// 使用 BPF_KPROBE 宏,libbpf 会自动帮你从 pt_regs 寄存器里把参数提取出来
bpf_printk("Kprobe: Target file %s\n", filename);
return 0;
}

// 对应函数的返回处
SEC("kretprobe/do_sys_openat2")
int BPF_KRETPROBE(handle_kretprobe_open, int ret) {
bpf_printk("Kretprobe: Return value (fd) = %d\n", ret);
return 0;
}

uprobe为动态用户态探针,用法和我们平常用到的hook原理差不多

1
2
3
4
5
6
7
// Hook libc 的 malloc 函数 (具体绑定的二进制路径在用户态 loader 程序中指定)
SEC("uprobe/libc:malloc")
int BPF_UPROBE(handle_malloc, size_t size) {
// 直接抓取用户进程调用 malloc 时传入的 size 参数
bpf_printk("Uprobe: App requested malloc(%lu)\n", size);
return 0;
}

fprobe较新内核上,结合BTF (BPF Type Format) 和 ftrace,它是用来取代 Kprobe 的。

还记得编写调试器的时候,和frida hook的区别吗,它不再使用抛出异常(int3),而是直接修改机器码实现Trampoline,性能开销几乎可以忽略不计。

1
2
3
4
5
6
7
// 使用 fentry Hook do_sys_openat2
SEC("fentry/do_sys_openat2")
int BPF_PROG(handle_fentry_open, int dfd, const char *filename, struct open_how *how) {
// 根本不需要管寄存器!BTF 魔法让你直接拿到原汁原味的 C 语言变量
bpf_printk("Fentry: High perf hook on %s\n", filename);
return 0;
}

上面所写的代码都是在内核态后端完成的,那么要怎么把数据传输回用户态,也就是工具的前端呢

有两种,perf (perf buffer)和ringbuf (BPF ring buffer)

在低版本下只能通过perf实现,它给每一个CPU核心分配了一个独立的环形缓冲区(ringbuf),那么问题也来了,假如我有128核的CPU,那只有一个CPU在处理缓冲,其他都空载,那就很浪费了。

而且由于不同的事件,比如处理open,read,write,本来是按照顺序严格执行的,但由于read所在的cpu核心缓冲区空闲先处理完,导致前端先接收到了read,这就导致顺序错乱了。

而且数据传输也要二次拷贝,要先把待传输的数据在ebpf栈上设置,再拷贝到内核缓冲区,然后再从内核缓冲区拷贝到用户态的内存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(int));
__uint(value_size, sizeof(int));
} events SEC(".maps");

SEC("uprobe")
int handle_dump(struct pt_regs *ctx) {
struct my_data data = {}; // 1. 先在 eBPF 程序的栈上分配空间

bpf_probe_read_user(&data.payload, sizeof(data.payload), (void *)PT_REGS_PARM1(ctx));

// 2. 将栈上的数据,拷贝发送到当前 CPU 的 Perf Buffer 中
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &data, sizeof(data));
return 0;
}

‘而现在的ringbuf,全部CPU核心全局共享一块缓冲区,使用自旋锁避免了并发时的竞争,现在的事件信息也严格按照时序,非常舒服,更重要的是其零拷贝的特性,在内核中直接分配出一块共享缓冲区操作,不需要再进行栈拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); // 共享池大小:256 KB
} events SEC(".maps");

SEC("uprobe")
int handle_dump(struct pt_regs *ctx) {
// 1. 预定空间 (Reserve):直接在全局 Ringbuf 里占好位置,拿到指针!
struct my_data *data = bpf_ringbuf_reserve(&events, sizeof(struct my_data), 0);
if (!data) {
return 0; // 缓冲区满了,丢弃
}

// 2. 直接向这块共享内存写入数据!(无需再占用栈空间)
bpf_probe_read_user(&data->payload, sizeof(data->payload), (void *)PT_REGS_PARM1(ctx));

// 3. 提交数据 (Submit):通知用户态可以来取了
bpf_ringbuf_submit(data, 0);

return 0;
}


syscall原理学习/tracepoint挂载点原理

那哪些才属于linux的内核空间呢,最常见的syscall的系统调用,比如一个open函数,在rax设置了open的系统调用号,执行了syscall后,CPU就从r3切换到r0。

尝试去翻看android源码进行学习,这里就以x86为例,https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/

进入内核态的第一行代码entry_SYSCALL_64,这里是纯汇编实现的(位于arch/x86/entry/entry_64.S)

这里主要是处理用户态的栈,切换到内核栈,然后就进入了do_syscall_64函数(位于arch/x86/entry/common.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
SYM_CODE_START(entry_SYSCALL_64)
UNWIND_HINT_ENTRY
ENDBR

swapgs
/* tss.sp2 is scratch space. */
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp

SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL)
ANNOTATE_NOENDBR

/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)
pushq %rax /* pt_regs->orig_ax */

PUSH_AND_CLEAR_REGS rax=$-ENOSYS

/* IRQs are off. */
movq %rsp, %rdi
/* Sign extend the lower 32bit as syscall numbers are treated as int */
movslq %eax, %rsi

/* clobbers %rax, make sure it is after saving the syscall nr */
IBRS_ENTER
UNTRAIN_RET
CLEAR_BRANCH_HISTORY

call do_syscall_64 /* returns with IRQs disabled */

/*
* Try to use SYSRET instead of IRET if we're returning to
* a completely clean 64-bit userspace context. If we're not,
* go to the slow exit path.
* In the Xen PV case we must use iret anyway.
*/

ALTERNATIVE "testb %al, %al; jz swapgs_restore_regs_and_return_to_usermode", \
"jmp swapgs_restore_regs_and_return_to_usermode", X86_FEATURE_XENPV

/*
* We win! This label is here just for ease of understanding
* perf profiles. Nothing jumps here.
*/
syscall_return_via_sysret:
IBRS_EXIT
POP_REGS pop_rdi=0

/*
* Now all regs are restored except RSP and RDI.
* Save old stack pointer and switch to trampoline stack.
*/
movq %rsp, %rdi
movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
UNWIND_HINT_END_OF_STACK

pushq RSP-RDI(%rdi) /* RSP */
pushq (%rdi) /* RDI */

/*
* We are on the trampoline stack. All regs except RDI are live.
* We can do future final exit work right here.
*/
STACKLEAK_ERASE_NOCLOBBER

SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi

popq %rdi
popq %rsp
SYM_INNER_LABEL(entry_SYSRETQ_unsafe_stack, SYM_L_GLOBAL)
ANNOTATE_NOENDBR
swapgs
CLEAR_CPU_BUFFERS
sysretq
SYM_INNER_LABEL(entry_SYSRETQ_end, SYM_L_GLOBAL)
ANNOTATE_NOENDBR
int3
SYM_CODE_END(entry_SYSCALL_64)

其中比较重要的就是这几句,将ax,也就是系统调用号先压入栈,然后再根据普通函数的调用约定构建struct pt_regs结构体

1
2
3
pushq	%rax					/* pt_regs->orig_ax */
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
call do_syscall_64 /* returns with IRQs disabled */

继续看看do_syscall_64的实现,nr即为系统调用号,这里还不是真正的入口,还是先进行了一波x86和x64的分发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
__visible noinstr bool do_syscall_64(struct pt_regs *regs, int nr)
{
nr = syscall_enter_from_user_mode(regs, nr);

instrumentation_begin();
add_random_kstack_offset();

if (!do_syscall_x64(regs, nr) && !do_syscall_x32(regs, nr) && nr != -1) {
/* Invalid system call, but still a system call. */
regs->ax = __x64_sys_ni_syscall(regs);
}

instrumentation_end();
syscall_exit_to_user_mode(regs);

/*
* Check that the register state is valid for using SYSRET to exit
* to userspace. Otherwise use the slower but fully capable IRET
* exit path.
*/

/* XEN PV guests always use the IRET path */
if (cpu_feature_enabled(X86_FEATURE_XENPV))
return false;

/* SYSRET requires RCX == RIP and R11 == EFLAGS */
if (unlikely(regs->cx != regs->ip || regs->r11 != regs->flags))
return false;

/* CS and SS must match the values set in MSR_STAR */
if (unlikely(regs->cs != __USER_CS || regs->ss != __USER_DS))
return false;

/*
* On Intel CPUs, SYSRET with non-canonical RCX/RIP will #GP
* in kernel space. This essentially lets the user take over
* the kernel, since userspace controls RSP.
*
* TASK_SIZE_MAX covers all user-accessible addresses other than
* the deprecated vsyscall page.
*/
if (unlikely(regs->ip >= TASK_SIZE_MAX))
return false;

/*
* SYSRET cannot restore RF. It can restore TF, but unlike IRET,
* restoring TF results in a trap from userspace immediately after
* SYSRET.
*/
if (unlikely(regs->flags & (X86_EFLAGS_RF | X86_EFLAGS_TF)))
return false;

/* Use SYSRET to exit to userspace */
return true;
}

继续追syscall_enter_from_user_mode,这里可以看到,nr已经确定命名为了syscall,也就是系统调用号

enter_from_user_mode(regs),用于准备RCU(Read-Copy Update,内核的一种锁机制/数据同步机制)

instrumentation_begin()用于解除追踪,ebpf在这之后才可以介入内核

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* syscall_enter_from_user_mode - Establish state and check and handle work
* before invoking a syscall
* @regs: Pointer to currents pt_regs
* @syscall: The syscall number
*
* Invoked from architecture specific syscall entry code with interrupts
* disabled. The calling code has to be non-instrumentable. When the
* function returns all state is correct, interrupts are enabled and the
* subsequent functions can be instrumented.
*
* This is the combination of enter_from_user_mode() and
* syscall_enter_from_user_mode_work() to be used when there is no
* architecture specific work to be done between the two.
*
* Returns: The original or a modified syscall number. See
* syscall_enter_from_user_mode_work() for further explanation.
*/
static __always_inline long syscall_enter_from_user_mode(struct pt_regs *regs, long syscall)
{
long ret;

enter_from_user_mode(regs);

instrumentation_begin();
local_irq_enable();
ret = syscall_enter_from_user_mode_work(regs, syscall);
instrumentation_end();

return ret;
}

继续跟syscall_enter_from_user_mode_work,可以看到,只有在SYSCALL_WORK_ENTER这个标志位开始后,才能进入trace流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* syscall_enter_from_user_mode_work - Check and handle work before invoking
* a syscall
* @regs: Pointer to currents pt_regs
* @syscall: The syscall number
*
* Invoked from architecture specific syscall entry code with interrupts
* enabled after invoking enter_from_user_mode(), enabling interrupts and
* extra architecture specific work.
*
* Returns: The original or a modified syscall number
*
* If the returned syscall number is -1 then the syscall should be
* skipped. In this case the caller may invoke syscall_set_error() or
* syscall_set_return_value() first. If neither of those are called and -1
* is returned, then the syscall will fail with ENOSYS.
*
* It handles the following work items:
*
* 1) syscall_work flag dependent invocations of
* ptrace_report_syscall_entry(), __secure_computing(), trace_sys_enter()
* 2) Invocation of audit_syscall_entry()
*/
static __always_inline long syscall_enter_from_user_mode_work(struct pt_regs *regs, long syscall)
{
unsigned long work = READ_ONCE(current_thread_info()->syscall_work);

if (work & SYSCALL_WORK_ENTER)
syscall = syscall_trace_enter(regs, work);

return syscall;
}

再往syscall_trace_enter追

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static __always_inline long syscall_trace_enter(struct pt_regs *regs, unsigned long work)
{
long syscall, ret = 0;

/*
* Handle Syscall User Dispatch. This must comes first, since
* the ABI here can be something that doesn't make sense for
* other syscall_work features.
*/
if (work & SYSCALL_WORK_SYSCALL_USER_DISPATCH) {
if (syscall_user_dispatch(regs))
return -1L;
}

/*
* User space got a time slice extension granted and relinquishes
* the CPU. The work stops the slice timer to avoid an extra round
* through hrtimer_interrupt().
*/
if (work & SYSCALL_WORK_SYSCALL_RSEQ_SLICE)
rseq_syscall_enter_work(syscall_get_nr(current, regs));

/* Handle ptrace */
if (work & (SYSCALL_WORK_SYSCALL_TRACE | SYSCALL_WORK_SYSCALL_EMU)) {
ret = arch_ptrace_report_syscall_entry(regs);
if (ret || (work & SYSCALL_WORK_SYSCALL_EMU))
return -1L;
}

/* Do seccomp after ptrace, to catch any tracer changes. */
if (work & SYSCALL_WORK_SECCOMP) {
ret = __secure_computing();
if (ret == -1L)
return ret;
}

/* Either of the above might have changed the syscall number */
syscall = syscall_get_nr(current, regs);

if (unlikely(work & SYSCALL_WORK_SYSCALL_TRACEPOINT))
syscall = trace_syscall_enter(regs, syscall);

syscall_enter_audit(regs, syscall);

return ret ? : syscall;
}

那么在最后可以清楚地看到ebpf中TRACEPOINT的支持

1
2
if (unlikely(work & SYSCALL_WORK_SYSCALL_TRACEPOINT))
syscall = trace_syscall_enter(regs, syscall);

也就是说其实其tracepoint优先级并不高,在他之前还有

User Dispatch:看看是不是用户态自己想搞模拟。

Ptrace:这是老牌调试器(如 GDB/Strace)的领地。由于 Ptrace 可能会修改寄存器甚至系统调用号,所以它必须排在前面。

Seccomp:这是内核的“安全沙箱”。它要根据 Ptrace 修改后的系统调用号来决定:这个操作允许吗?要不要杀掉进程?

进行看 trace_syscall_enter,这里可以明确地看到,是bpf的挂载点,也就是经典的sys_enter和sys_exit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: GPL-2.0

#include <linux/entry-common.h>

#define CREATE_TRACE_POINTS
#include <trace/events/syscalls.h>

/* Out of line to prevent tracepoint code duplication */

long trace_syscall_enter(struct pt_regs *regs, long syscall)
{
trace_sys_enter(regs, syscall);
/*
* Probes or BPF hooks in the tracepoint may have changed the
* system call number. Reread it.
*/
return syscall_get_nr(current, regs);
}

void trace_syscall_exit(struct pt_regs *regs, long ret)
{
trace_sys_exit(regs, ret);
}

trace_sys_enter和trace_sys_exit定义如下,可以看到大部分还是完成了传参的工作,那么真正执行我们ebpf模块,也就是应该有个回调函数调用的地方,应该在哪里呢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/* SPDX-License-Identifier: GPL-2.0 */
#undef TRACE_SYSTEM
#define TRACE_SYSTEM raw_syscalls
#undef TRACE_INCLUDE_FILE
#define TRACE_INCLUDE_FILE syscalls

#if !defined(_TRACE_EVENTS_SYSCALLS_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TRACE_EVENTS_SYSCALLS_H

#include <linux/tracepoint.h>

#include <asm/ptrace.h>
#include <asm/syscall.h>


#ifdef CONFIG_HAVE_SYSCALL_TRACEPOINTS

TRACE_EVENT_SYSCALL(sys_enter,

TP_PROTO(struct pt_regs *regs, long id),

TP_ARGS(regs, id),

TP_STRUCT__entry(
__field( long, id )
__array( unsigned long, args, 6 )
),

TP_fast_assign(
__entry->id = id;
syscall_get_arguments(current, regs, __entry->args);
),

TP_printk("NR %ld (%lx, %lx, %lx, %lx, %lx, %lx)",
__entry->id,
__entry->args[0], __entry->args[1], __entry->args[2],
__entry->args[3], __entry->args[4], __entry->args[5]),

syscall_regfunc, syscall_unregfunc
);

TRACE_EVENT_FLAGS(sys_enter, TRACE_EVENT_FL_CAP_ANY)

TRACE_EVENT_SYSCALL(sys_exit,

TP_PROTO(struct pt_regs *regs, long ret),

TP_ARGS(regs, ret),

TP_STRUCT__entry(
__field( long, id )
__field( long, ret )
),

TP_fast_assign(
__entry->id = syscall_get_nr(current, regs);
__entry->ret = ret;
),

TP_printk("NR %ld = %ld",
__entry->id, __entry->ret),

syscall_regfunc, syscall_unregfunc
);

TRACE_EVENT_FLAGS(sys_exit, TRACE_EVENT_FL_CAP_ANY)

#endif /* CONFIG_HAVE_SYSCALL_TRACEPOINTS */

#endif /* _TRACE_EVENTS_SYSCALLS_H */

/* This part must be outside protection */
#include <trace/define_trace.h>

翻了很久终于找到了TRACE_EVENT_SYSCALL的最底宏定义,前面基本都是一些别名封装

__DO_TRACE_CALL即为真正执行tracepoint的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define __DECLARE_TRACE_SYSCALL(name, proto, args, data_proto)		\
__DECLARE_TRACE_COMMON(name, PARAMS(proto), PARAMS(args), PARAMS(data_proto)) \
static inline void __do_trace_##name(proto) \
{ \
TRACEPOINT_CHECK(name) \
guard(rcu_tasks_trace)(); \
__DO_TRACE_CALL(name, TP_ARGS(args)); \
} \
static inline void trace_##name(proto) \
{ \
might_fault(); \
if (static_branch_unlikely(&__tracepoint_##name.key)) \
__do_trace_##name(args); \
if (IS_ENABLED(CONFIG_LOCKDEP)) { \
WARN_ONCE(!rcu_is_watching(), \
"RCU not watching for tracepoint"); \
} \
}

static_call(tp_func_##name),即最终执行了tracepoint的回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* Individual subsystem my have a separate configuration to
* enable their tracepoints. By default, this file will create
* the tracepoints if CONFIG_TRACEPOINTS is defined. If a subsystem
* wants to be able to disable its tracepoints from being created
* it can define NOTRACE before including the tracepoint headers.
*/
#if defined(CONFIG_TRACEPOINTS) && !defined(NOTRACE)
#define TRACEPOINTS_ENABLED
#endif

#ifdef TRACEPOINTS_ENABLED

#ifdef CONFIG_HAVE_STATIC_CALL
#define __DO_TRACE_CALL(name, args) \
do { \
struct tracepoint_func *it_func_ptr; \
void *__data; \
it_func_ptr = \
rcu_dereference_raw((&__tracepoint_##name)->funcs); \
if (it_func_ptr) { \
__data = (it_func_ptr)->data; \
static_call(tp_func_##name)(__data, args); \
} \
} while (0)
#else
#define __DO_TRACE_CALL(name, args) __traceiter_##name(NULL, args)
#endif /* CONFIG_HAVE_STATIC_CALL */

这里特别还注意,trace_##name中在do_trace还进行了一次static_branch_unlikely操作,这个if不是一个简单的判断。它是 Linux 内核的 Static Keys(Jump Labels)机制

当没有ebpf事件挂载时,这条if对应的机器码就是一个NOP,不会跳转到_do_trace##name。

1
2
3
... (前面的系统调用逻辑)
NOP (占 5 个字节)
... (后面的系统调用逻辑)

而当存在时,其实判断已经发生了,这里会直接生成一条JMP指令,直接到_do_trace##name。

1
2
3
... (前面的系统调用逻辑)
JMP do_trace (被篡改后的 5 字节)
... (后面的系统调用逻辑)

那为什么需要这种static keys的机制呢,因为现代CPU都拥有很高的流水线,CPU会进行分支预测,假如这里的ebpf状态发生了变化,那么后面实际的执行将发生很大变动,将惩罚多个CPU周期

对于系统调用(比如 read、write)这种每秒被调用成千上万次的基础设施,每次都去读内存、比较、做分支预测,累加起来的性能损耗是极其可观的。

为什么 NOP 是零损耗? 虽然 NOP 也是指令,但在现代超标量 CPU 中,译码器(Decoder)看到 NOP 后,往往会直接把它丢弃,根本不会把它送进后续的执行单元(Execution Unit)。它在流水线中的消耗几乎被完全抹平,等于 0 个周期

  • 标题: ebpf学习-从linux内核源码开始
  • 作者: 两只羊
  • 创建于 : 2026-05-09 09:35:43
  • 更新于 : 2026-05-09 17:37:01
  • 链接: https://twogoat.github.io/2026/05/09/ebpf学习-认识ebpf/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
ebpf学习-从linux内核源码开始