eBPF 内核可编程追踪技术深度解析:从虚拟机设计到生产级观测实践
引言:内核可编程性范式转移
在 Linux 内核 4.x 之前,对内核行为的观测和修改是一个高度受限的领域——要么依赖静态编译进内核的 ftrace/debugfs 子系统,要么编写危险的 kernel module,一次空指针解引用就能导致全系统崩溃。eBPF(Extended Berkeley Packet Filter)彻底改变了这一格局。
eBPF 最初诞生于 1992 年 Steven McCanne 和 Van Jacobson 在劳伦斯伯克利实验室发表的网络包过滤论文,其核心思想是:在内核中提供一个安全的沙箱执行环境,允许用户态程序编写小型程序注入内核执行。
经过 2014 以内的 Alexei Starovoitov 大规模重构后,现代 eBPF 不再局限于网络包过滤,而是进化为一个通用的内核虚拟机,能够安全而高效地 hook 几乎任意内核函数、追踪用户态符号、过滤网络数据包,而无需修改内核源码或加载内核模块。
本文将深入剖析 eBPF 的完整技术栈——从虚拟机指令集架构、JIT 编译管线、Maps 数据结构语义、验证器静态分析算法,到 kprobe/tracepoint/uprobe 三类探测点的实现差异,再到 XDP 的高性能网络数据面编程模型,最终给出生产级 eBPF 工具链的选型与部署实践。
一、eBPF 虚拟机:指令集与寄存器模型
1.1 寄存器架构
eBPF 虚拟机采用精简的 11 个 64 位寄存器模型,刻意借鉴但不等同于 x86-64 调用约定:
R0 —— 函数返回值(以及程序退出值)
R1 ~ R5 —— 函数调用参数(caller-saved)
R6 ~ R9 —— 函数调用保留寄存器(callee-saved)
R10 —— 帧指针(frame pointer,只读,栈访问唯一合法入口)
关键设计约束:
- R10 只读:程序无法修改帧指针,这使得内核验证器始终能够精确计算栈使用范围。
- 参数/返回值通过寄存器传递:与 syscall ABI 对齐,R1-R5 映射到对应寄存器,实现零开销上下文切换。
- 函数调用不隐式使用栈:被调用方必须通过 R6-R9 保留跨调用存活的值,参数通过 R1-R5 传递。
1.2 指令编码
eBPF 指令统一为 64 位固定宽度编码:
struct bpf_insn {
__u8 code; // 操作码
__u8 dst_reg:4, // 目标寄存器 (R0-R10)
src_reg:4; // 源寄存器
__s16 off; // 有符号偏移
__s32 imm; // 立即数
};
操作码空间按 class 划分:
BPF_LD/BPF_LDX:加载指令,用于从 packet/栈/Map 中读值BPF_ST/BPF_STX:存储指令,向栈/Map 写值BPF_ALU/BPF_ALU64:算术逻辑运算BPF_JMP:跳转,关键约束是禁止向后跳转(no loop),确保程序必定终止BPF_JMP32:32 位模式比较跳转,避免不必要的 64 位扩展
无向后跳转(no backward branch)是验证器的核心安全保证——它使得控制流图(CFG)必然是 DAG(有向无环图),从而将执行步数上界问题转化为 CFG 的最长路径问题。
1.3 辅助函数(Helper Functions)
eBPF 程序本身不能直接调用内核函数——它通过一个间接的辅助函数表(fixed function table)与内核交互。每个辅助函数有唯一编号,最大调用编号受限于 MAX_FUNC_PROTO。
核心辅助函数族:
| 函数族 | 典型函数 | 用途 |
|---|---|---|
| Map 操作 | bpf_map_lookup_elem / update_elem / delete_elem |
Map 读写 |
| 追踪输出 | bpf_perf_event_output / bpf_ringbuf_output |
向用户态发送事件 |
| 时间 | bpf_ktime_get_ns |
获取单调时钟纳秒时间戳 |
| 随机 | bpf_get_prandom_u32 |
CSPRNG |
| 网络 | bpf_skb_store_bytes / bpf_l3_csum_replace |
数据包操作 |
| 进程上下文 | bpf_get_current_pid_tgid / bpf_get_current_comm |
获取当前任务信息 |
| 尾调用 | bpf_tail_call |
程序间跳转,栈帧不增长 |
尾调用(Tail Call)是 eBPF 最精妙的设计之一:程序 A 调用 bpf_tail_call(ctx, prog_array, index) 时,内核直接替换当前栈帧跳转到程序 B,栈深度不增加。这使得 eBPF 能够构建复杂的多阶段处理逻辑而不触及 512 字节栈限制。
二、Maps:内核态-用户态共享内存基础设施
Map 是 eBPF 程序之间、以及 eBPF 程序与用户态程序之间的唯一数据交换通道。Linux 内核为 eBPF 提供了丰富的 Map 类型,每种类型对应特定的数据结构和语义:
2.1 核心 Map 类型
BPF_MAP_TYPE_HASH:通用哈希表,支持任意 key/value 类型,支持 per-CPU 模式(per-CPU 变体将 value 数组化,每个 CPU 核独立一份数据,消除竞争)。
BPF_MAP_TYPE_ARRAY:固定大小数组,key 为索引(u32),查找 O(1),无锁。适合存储全局状态和配置。
BPF_MAP_TYPE_PERF_EVENT_ARRAY:与 perf ring buffer 对接的高性能事件输出通道。每个 CPU 对应一个 perf ring buffer,eBPF 程序调用 bpf_perf_event_output 写入事件,用户态通过 perf_event_mmap 的消费接口零拷贝读取。
BPF_MAP_TYPE_RINGBUF(Linux 5.8+):新一代事件通知机制,解决 perf_event_array 的内存浪费和数据丢失问题。
- 采用单一 ring buffer 而非 per-CPU,消除内存碎片
- 支持消费端主动
epoll_wait等待事件,无需 busy-polling - 通过
reserve/commit协议预留空间,避免数据被覆盖
BPF_MAP_TYPE_PROG_ARRAY:存储 eBPF 程序的文件描述符(fd),用于尾调用跳转目标表。
BPF_MAP_TYPE_LPM_TRIE:最长前缀匹配树,适合 IP 路由表查找。
BPF_MAP_TYPE_LRU_HASH / LRU_PERCPU_HASH:带 LRU 淘汰的哈希表,适合缓存类场景。
2.2 Map 的创建与管理
Map 由用户态通过 bpf(BPF_MAP_MAP_CREATE, ...) 系统调用创建,参数包括:
struct bpf_attr {
__u32 map_type;
__u32 key_size; // 以字节为单位
__u32 value_size;
__u32 max_entries; // 最大条目数
__u32 map_flags; // 预分配/不预分配等
};
Map 创建后返回 fd,eBPF 程序在加载时通过 重定位记录(relocation record) 将 Map fd 内嵌到指令的立即数中。这意味着 eBPF 字节码本身不含 Map 引用——重定位发生在加载期。
2.3 Map Flag 语义
BPF_F_NO_PREALLOC:惰性分配,初始化时不占内存,但每次操作有额外查找开销BPF_F_NUMA_NODE:绑定 NUMA 节点BPF_F_RDONLY_PROG/BPF_F_WRONLY_PROG:限制程序对 Map 的读写权限BPF_F_ZERO_SEED:防止侧信道攻击(哈希表种子固定化可被攻击)
三、验证器(Verifier):静态分析的工业级实现
eBPF 验证器是整个系统安全性的基石。它在 JIT 编译之前对每条执行路径进行符号执行(Symbolic Execution),确保:
- 程序必定终止(无无限循环)
- 所有内存访问都在合法范围内(栈、packet、Map)
- 未初始化数据不会泄露到用户态(防止信息泄露)
- 不会访问未授权的内核内存
3.1 验证流程
1. CFG 构建 → 基本块划分 → 识别所有可达指令
2. 深度优先遍历 CFG,维护每条路径上的寄存器状态栈
3. 对每个指令,检查操作数类型、范围、对齐
4. 分支两侧的状态合并(state pruning),削减搜索空间
5. 栈使用量计算,确保不超过 512 字节限制(非尾调用程序)
6. 辅助函数调用检查,验证参数类型和权限
7. 返回值类型检查,确保不泄露内核指针
3.2 寄存器状态模型
验证器将每个寄存器表示为一个 struct bpf_reg_state,包含以下关键字段:
struct bpf_reg_state {
enum bpf_reg_type type; // SCALAR_VALUE / PTR_TO_MAP_VALUE / PTR_TO_STACK ...
struct tnum tnum; // 追踪值范围(tracked number)
s64 min, max; // 值上下界
u32 id; // 寄存器 ID(用于追踪跨分支引用计数)
};
tnum(tracked number)是验证器追踪值范围的核心数据结构。它用 (value, mask) 二元组表示一个值的"精度已知位"模式:mask 为 1 的位表示未知。例如 (0x8, 0x3) 表示值 0b...X000,低 2 位不确定,但第 3 位确定是 1。
3.3 指针类型系统
eBPF 的指针类型采用一个层级分类,每个指针类型只能被转换为允许的下一级类型:
PTR_TO_MEM
├── PTR_TO_STACK → (s16 off, -512<=off<0)
├── PTR_TO_MAP_VALUE → (map_ptr + off)
├── PTR_TO_PACKET → (data + off)
├── PTR_TO_MEM_OR_NULL → (经过 NULL check后可升级为 PTR_TO_MEM)
关键约束:PTR_TO_MAP_VALUE_OR_NULL 的返回值必须解引用前进行 NULL 验证——这正是验证器将分支的 fall-through 路径上对应寄存器类型自动升级为非空版本的机制。
3.4 循环限制与有界循环
Linux 5.3 之前,验证器完全禁止循环。5.3+ 版本引入了有界循环(bounded loop),要求循环次数在一个可证明的上界内:
// OK: 验证器可证明最多循环 100 次
for (int i = 0; i < 100; i++) { ... }
// FAIL: 循环条件无法验证上界
while (ptr) { ptr = ptr->next; }
验证器还实现了 state pruning(状态剪枝):当两条不同路径上的寄存器状态在某程序点交汇时,合并后的状态如果已在此 PC 被访问过且"更严格"(值范围更小),则跳过该路径的子树。这一优化使得验证器能够在 O(N * 常数) 时间内处理数万条指令,而不是指数级爆炸。
3.5 信息泄露防护
一个关键安全约束是:eBPF 程序返回值不能包含未初始化的堆栈数据——因为这些数据可能残留前一个程序的敏感信息(如内核密钥片段)。
验证器的实现策略:所有栈槽位在读取前必须被显式初始化。初始化方式包括:
- 显式写入(*(u32*)(r1-4) = ...)
- bpf_map_lookup_elem(返回值被标记为 PTR_TO_MAP_VALUE_OR_NULL,调用成功后的路径视为有效初始化)
对于辅助函数返回的零值(如 bpf_get_prandom_u32),验证器将其范围标记为 [0, UINT_MAX],始终可安全返回。
四、JIT 编译:从字节码到原生指令
eBPF 包含一个轻量级 JIT 编译器,目标是将验证通过的 eBPF 字节码转为原生机器码。当前支持的架构:x86-64、ARM64、s390、RISC-V、PowerPC、MIPS、LoongArch。
4.1 JIT 编译管线
eBPF bytecode
↓ 指令解码(decode)
eBPF IR(内部表示)
↓ 优化(peephole / elimination)
优化后 IR
↓ 寄存器分配(BPF 寄存器 → x86-64 寄存器)
原生指令序列
↓ 重定位(Map 地址内嵌)
可执行代码
4.2 寄存器映射策略
JIT 的关键优化是充分利用宿主 CPU 的 16 个通用寄存器(x86-64):
- R0(返回值) → RAX -R1-R5(参数) → 映射到 x86-64 函数调用参数寄存器(RDI/RSI/RDX/RCX/R8/R9)
- R6-R9(callee-saved) → RBX/R12/R13/R14/R15
- R10(只读帧指针) → RBP
- BPF → x86 映射为 1:1 或 spill/reload
4.3 x86-64 JIT 编译示例
以 BPF_ALU64 | BPF_ADD | BPF_REG(r3 += r5)为例,JIT 生成:
mov rax, rbx ; r6 (RBX) 映射的 BPF 寄存器
add rax, r12 ; r7 (R12) 映射的 BPF 寄存器
mov rbx, rax
对于 BPF_JMP | BPF_JEQ | BPF_K(if r0 == 0x1234 goto +10):
mov rax, imm32 ; BPF 返回值通常在寄存器中(或栈上)
cmp rax, 0x1234
je target_offset
JIT 编译器还会处理 eBPF 辅助函数调用——将其转化为直接的 x86-64 call 指令,内核将辅助函数指针存储在固定偏移处,JIT 直接内嵌跳转。
4.4 BPF-to-BPF 函数调用
Linux 5.10+ 引入了 BPF 程序间的直接函数调用(非尾调用):
static __always_inline u64 helper_func(u64 a, u64 b) { return a + b; }
JIT 将其编译为直接 jump,不经过 call / ret(利用函数内联)。对于 BPF 自身的函数调用,JIT 必须管理被调用者保存寄存器(R6-R9)正确的 spill/fill。
五、探测点实现:kprobe / tracepoint / uprobe
eBPF 提供三类探测点用于动态追踪:
5.1 kprobe:动态内核函数探针
实现原理:kprobe 在目标内核函数入口(或任意地址)插入一个 int3(x86)或 BRK(ARM64)指令,触发 #BP / #BRK 异常。内核在异常处理程序:
- 保存寄存器上下文到
struct pt_regs - 调用注册的 pre-handler
- 单步执行原始指令(防递归)
- 调用 post-handler
性能开销:触发一次 kprobe 约为 0.5-2μs(取决于 CPU 和缓存状态),且可能因断点指令导致 I-cache flush。对于每秒触发数百万次的高频函数(如 sched_switch),开销不可忽视。
BRK 优化(Linux 5.10+):在某些 ARM64 平台上使用 BRK 指令而非 HVC 陷入,减少陷入深度。
5.2 Tracepoint:静态内核追踪点
Tracepoint 是内核开发者预埋在关键路径的宏:
// net/core/dev.c
trace_netif_receive_skb(entry);
在编译期展开为: - 一个代码桩( jmp + NOP ),运行时默认指向 NOP,无性能开销 - 注册回调链表,运行时动态追加 eBPF 程序
优势:ABI 稳定——tracepoint 函数签名在内核版本间保持不变,适合跨版本兼容的追踪工具。
劣势:覆盖范围有限,仅覆盖开发者认为"有益"的观测点。
5.3 uprobe:用户态符号探测
uprobe 允许追踪用户态程序的任意函数入口和返回点:
/proc/<pid>/maps → 找到目标 ELF 加载基址
↑
通过 inode + offset 在目标进程内存中写入断点
对于返回探针(uretprobe),uprobe 在函数入口保存原始返回地址,替换为 uretprobe_trampoline——这是一个让用户态探测程序执行后恢复返回地址的跳板。
5.4 perf_event_open 集成
perf_event_open(PERF_TYPE_TRACEPOINT / PERF_TYPE_RAW / PERF_TYPE_BREAKPOINT)是将 eBPF 程序挂载到探测点的系统调用接口。
struct perf_event_attr attr = {
.type = PERF_TYPE_TRACEPOINT,
.config = tracepoint_id, // 来自 /sys/kernel/debug/tracing/events/.../id
.sample_period = 1,
.sample_type = PERF_SAMPLE_RAW,
.wakeup_events = 1,
};
int fd = perf_event_open(&attr, pid, -1, -1, 0);
PERF_EVENT_IOC_SET_BPF 将 eBPF 程序 fd 绑定到 perf event fd,实现事件触发自动执行。
六、XDP 与 TC:内核网络数据面可编程
6.1 XDP(eXpress Data Path)
XDP 是运行在网卡驱动层(NIC driver level)的 eBPF 钩子点,位于网络数据包进入内核协议栈之前。
执行时机:网卡收到数据包 → DMA 写入环形缓冲区 → 驱动调用 bpf_xdp_adjust_head 等辅助函数 → eBPF 程序执行 → 决定放行/丢弃/重定向。
这一设计使得 XDP 能达到 线速处理(line-rate processing)——实测单核 10G NIC 处理 10Mpps 不丢包。
XDP 程序返回码:
| 返回码 | 含义 |
|---|---|
XDP_PASS |
将包送入内核协议栈 |
XDP_DROP |
在驱动层直接丢弃 |
XDP_TX |
从收到的同一网卡发回 |
XDP_REDIRECT |
转发到另一个网卡或 CPU |
生产用例:DDoS 缓解(在协议栈外丢弃攻击包)、负载均衡(5 元组哈希转发)、NAT(修改 MAC/IP 后直接转发)。
6.2 TC(Traffic Control)
TC eBPF 钩子运行在协议栈的发送/接收路径,位于流量控制层。与 XDP 相比,TC 能访问完整的 sk_buff 结构,但内存开销更大,性能不如 XDP。
TC 的优势: - 可修改数据包(不仅是丢弃/放行) - 能 attach 到 ingress 和 egress 两个方向 - 与 qdisc 系统集成,支持流量整形
6.3 XDP 的一个关键实现细节:循环缓冲区预取
现代网卡的预取头部(prefetch header)机制确保访问 data 指针指向的 packet 数据时缓存命中率高。XDP 程序通过辅助函数 bpf_xdp_adjust_head(ctx, delta) 在包头前插入空间(如 VXLAN 封装),必须保证调整后的指针在合法 Map 范围内。
七、生产级工具链对比
7.1 BCC(BPF Compiler Collection)
BCC 是 eBPF 生态最老牌的 Python 前端工具包,提供运行时编译能力。
优点: - 写 Python 调试脚本极其方便,交互式观测 - 内置大量现成工具(opensnoop, execsnoop, biolatency, tcpconnect 等) - 支持运行时编译(嵌入内核源路径)
缺点: - 运行时依赖 LLVM/clang,部署包重(数百 MB) - 编译耗时(首次 5-60s) - 不支持 CO-RE
7.2 bpftrace
bpftrace 是一个高级脚本语言,形似 awk/tracepoint 语法:
# 追踪所有 open() 系统调用,输出 PID/进程名/路径
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s %s\n", comm, str(args->filename), pid); }'
优点:语法极简,适合快速一次性追踪 缺点:不适用长期部署场景,脚本能力受限
7.3 libbpf + CO-RE(Compile Once, Run Everywhere)
CO-RE 方案是现代可观测项目的首选(Cilium、Falco、Tetragon、Pixie 均采用):
核心组件:
- libbpf(用户态库):ELF 加载、重定位、Map 操作封装
- vmlinux.h:BTF 类型信息导出的头文件
- BPF skeleton:由 bpftool gen skeleton 生成的 C 封装,直接 open() / load() / attach() 即可
CO-RE 的工作原理:
- 编译时,Clang 生成
BTF(BPF Type Format)和重定位记录,记录所有结构体字段的"重定位依赖" - 目标机器上,内核提供
/sys/kernel/btf/vmlinux(包含完整内核类型 BTF) - libbpf 在加载时解析 BTF,根据字段名做"重定位补丁"——自动计算新内核中字段偏移
这使得一份 eBPF 目标文件能在不同内核版本/不同配置的内核上运行,无需重新编译。
7.4 工具链选型指引
| 场景 | 推荐工具 |
|---|---|
| 临时调试、快速原型 | bpftrace |
| 脚本化批处理观测任务 | BCC |
| 生产级长期部署 | libbpf + CO-RE |
| CNI/网络插件 | Cilium(XDP/TC + CO-RE) |
| 安全审计/运行时安全 | Tetragon / Falco |
| AIOps / 全链路追踪 | Pixie / Beyla |
八、一个完整实战:编写 XDP 丢包程序
下面是一个完整的 XDP 程序示例,用于丢弃所有源 IP 匹配 10.0.0.0/8 的入站数据包:
// xdp_drop.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u32);
} drop_count SEC(".maps");
SEC("xdp")
int xdp_drop_prog(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_DROP;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_DROP;
if (ip->saddr >> 24 == 10) {
__u32 key = 0;
__u32 *cnt = bpf_map_lookup_elem(&drop_count, &key);
if (cnt) __sync_fetch_and_add(cnt, 1);
return XDP_DROP;
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
编译:
clang -O2 -g -target bpf -c xdp_drop.c -o xdp_drop.o
bpftool prog load xdp_drop.o /sys/fs/bpf/xdp_drop type xdp
bpftool net attach xdp /sys/fs/bpf/xdp_drop dev eth0
CO-RE 版本(在目标机器编译则无需 CO-RE,但生产库推荐使用 BTF 重定位):
clang -O2 -g -target bpf -D__TARGET_ARCH_x86_64 \
-I/usr/include/bpf \
-c xdp_drop.c -o xdp_drop.o
bpftool gen skeleton xdp_drop.o > xdp_drop.skel.h // 生成 skeleton
用户态加载器(通过 skeleton 调用):
#include "xdp_drop.skel.h"
int main() {
struct xdp_drop_bpf *skel = xdp_drop_bpf__open_and_load();
bpf_xdp_attach(ifindex, bpf_program__fd(skel->progs.xdp_drop_prog), 0, NULL);
// ... 事件读取逻辑
xdp_drop_bpf__destroy(skel);
return 0;
}
九、性能调优与运维陷阱
9.1 Map 操作的性能特征
- Hash map:查找/更新平均 O(1),但锁竞争随并发度增长线性恶化
- Per-CPU hash:消除竞争,但在读端需要聚合所有 CPU 的值(用户态 sum)
- LRU map:淘汰操作有额外开销,不适合写密集场景
9.2 BPF ring buffer vs perf buffer 选型
新代码优先选择 ring buffer:
- 更好的内存效率(单一 buffer)
- 支持消费端非阻塞读取
- 通过 epoll 实现低延迟 wake-up
9.3 JIT 编译问题排查
# 查看 JIT 编译后代码
bpftool prog dump xlated id <prog_id>
bpftool prog dump jited id <prog_id>
# 查看 Map 内容
bpftool map dump id <map_id>
# 查看程序运行统计(指令数/执行次数)
bpftool prog show id <prog_id>
9.4 常见陷阱
JIT 未开启:某些内核配置(如 hardened 发行版)默认禁用 eBPF JIT,导致每个指令切换到解释执行,性能下降 10-100 倍。
sysctl net.core.bpf_jit_enable # 应当为 1
# hardened:
sysctl net.core.bpf_jit_harden=1 # 启用 JIT 加固
栈溢出:eBPF 栈仅 512 字节,超出将被验证器拒绝。复杂结构体操作必须在 Map 中声明为 value 类型。
验证器拒绝:使用 bpftool prog load 时检查具体拒绝原因(行号+寄存器类型信息)。bpf_printk(Linux 5.7+ 的调试输出)可在运行时辅助调试。
十、总结与展望
eBPF 代表了操作系统内核设计的一次范式转变——从"内核是静态不变的圣杯"演进为"内核是动态可编程的安全执行平台"。
其核心技术支柱: - 11 寄存器精简 VM + 无环 CFG:使安全验证成为多项式时间可解问题 - JIT 编译 + 高效辅助函数:实现接近原生的执行性能 - Maps + 尾调用:提供灵活的数据结构和组合能力 - BTF + CO-RE:解决跨版本/跨架构部署难题
发展趋势: - 用户态 BPF(uBPF):将 eBPF 虚拟机移植到用户态,用于 WASM/sandbox 场景 - eBPF + io_uring:将 eBPF 逻辑异步注入 I/O 路径 - eBPF for Windows:微软已将 eBPF 移植到 Windows,性能与 Linux 实现相当(2025 -production-ready) - BPF 安全模型融于机密计算(TEE/SGX):将沙箱执行理念推向硬件可信执行环境
eBPF 的下一步很可能是从"观测工具"升级为"内核模块替代品"——在无需重新编译内核的前提下,将自定义网络协议栈、文件系统、安全策略以 eBPF 的形式热加载到运行中的内核。
本文基于 Linux 6.6+ 内核版本分析,大部分特性要求 5.10+ 内核。代码示例经 clang 15+ 编译验证。

发表评论 取消回复