

eBPF Talk: freplace on x86【汇编慎入】
source link: https://asphaltt.github.io/post/ebpf-talk-41-freplace-on-x86/
freplace
技术便是对 prologue
进行 poke
的简单应用。
TL;DR 究竟有多简单呢?就是将 prologue
里的第一条 nop
指令替换成 jmp
指令;jmp
后不再回来继续执行原来的函数,是谓 freplace
。
freplace
例子
demo 先行。源代码链接:ebpf-freplace。
// tcp-connecting.c
__noinline int
stub_handler()
{
bpf_printk("freplace, stub handler\n");
return 0;
}
typedef struct event {
__be32 saddr, daddr;
__be16 sport, dport;
} __attribute__((packed)) event_t;
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
} events SEC(".maps");
static __noinline
void handle_new_connection(struct pt_regs *ctx, struct sock *sk)
{
event_t ev = {};
ev.saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
ev.daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
ev.sport = BPF_CORE_READ(sk, __sk_common.skc_num);
ev.dport = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &ev, sizeof(ev));
}
SEC("kprobe/tcp_connect")
int k_tcp_connect(struct pt_regs *ctx)
{
struct sock *sk;
sk = (typeof(sk))PT_REGS_PARM1(ctx);
handle_new_connection(ctx, sk);
return stub_handler();
}
SEC("kprobe/inet_csk_complete_hashdance")
int k_icsk_complete_hashdance(struct pt_regs *ctx)
{
struct sock *sk;
sk = (typeof(sk))PT_REGS_PARM2(ctx);
handle_new_connection(ctx, sk);
return stub_handler();
}
没有进行 freplace
时:
# cat /sys/kernel/debug/tracing/trace_pipe
curl-5875 [002] dN.31 3444.754757: bpf_trace_printk: freplace, stub handler
// freplace.c
SEC("freplace/stub_handler")
int freplace_handler()
{
bpf_printk("freplace, replaced handler\n");
return 0;
}
进行 freplace
后:
# cat /sys/kernel/debug/tracing/trace_pipe
<idle>-0 [001] d.s51 2714.385269: bpf_trace_printk: freplace, replaced handler
freplace
实现原理
先看看 Go 代码是怎么调用 bpf()
系统调用的。
// ${cilium/ebpf}/link/tracing.go
func AttachFreplace(targetProg *ebpf.Program, name string, prog *ebpf.Program) (Link, error) {
// ...
link, err := AttachRawLink(RawLinkOptions{
Target: target,
Program: prog,
Attach: ebpf.AttachNone,
BTF: typeID,
})
// ...
return &tracing{*link}, nil
}
// ${cilium/ebpf}/link/link.go
func AttachRawLink(opts RawLinkOptions) (*RawLink, error) {
if err := haveBPFLink(); err != nil {
return nil, err
}
if opts.Target < 0 {
return nil, fmt.Errorf("invalid target: %s", sys.ErrClosedFd)
}
progFd := opts.Program.FD()
if progFd < 0 {
return nil, fmt.Errorf("invalid program: %s", sys.ErrClosedFd)
}
attr := sys.LinkCreateAttr{
TargetFd: uint32(opts.Target),
ProgFd: uint32(progFd),
AttachType: sys.AttachType(opts.Attach),
TargetBtfId: uint32(opts.BTF),
Flags: opts.Flags,
}
fd, err := sys.LinkCreate(&attr)
if err != nil {
return nil, fmt.Errorf("create link: %w", err)
}
return &RawLink{fd, ""}, nil
}
// ${cilium/ebpf}/internal/sys/types.go
func LinkCreate(attr *LinkCreateAttr) (*FD, error) {
fd, err := BPF(BPF_LINK_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
if err != nil {
return nil, err
}
return NewFD(int(fd))
}
由上面的代码片段可知,调用的是 bpf()
系统调用中的 BPF_LINK_CREATE
命令。
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size) // ${KERNEL}/kernel/bpf/syscall.c
|-->__sys_bpf()
|-->link_create()
|-->bpf_tracing_prog_attach()
|-->struct bpf_attach_target_info tgt_info = {};
|-->bpf_check_attach_target(NULL, prog, tgt_prog, btf_id, &tgt_info); // ${KERNEL}/kernel/bpf/verifier.c
| |-->addr = (long) tgt_prog->bpf_func;
| |-->tgt_info->tgt_addr = addr;
|-->tr = bpf_trampoline_get(key, &tgt_info); // lookup old trampoline by key, or create a new one
| |-->tr->func.addr = (void *)tgt_info->tgt_addr;
|-->bpf_link_init()
|-->bpf_link_prime()
|-->bpf_trampoline_link_prog() // ${KERNEL}/kernel/bpf/trampoline.c
| |-->__bpf_trampoline_link_prog()
| |-->bpf_arch_text_poke(tr->func.addr, BPF_MOD_JUMP, NULL, link->link.prog->bpf_func);
|-->bpf_link_settle()
看到 bpf_arch_text_poke()
函数时,便可知道上面代码片段的主要处理逻辑:
- 查询目标 bpf prog 的信息,特别是 bpf prog 的入口地址。
- 生成一个
trampoline
对象。 - 调用
bpf_arch_text_poke()
将目标 bpf prog 的prologue
的第一条nop
指令 live patch 成jmp
指令,jmp
到当前的freplace
bpf prog 的入口地址。
复习一下 eBPF Talk: perilogue on x86【汇编慎入】 中的
prologue of bpf2bpf
。
bpf2bpf
函数的第一条指令是 5 个字节大小的 nop
指令。
复习一下 eBPF Talk: poke on x86【汇编慎入】 中的
JIT on x86
。
// ${KERNEL}/arch/x86/net/bpf_jit_comp.c
bpf_int_jit_compile()
|-->do_jit()
|-->emit_prologue()
static void emit_prologue(u8 **pprog, u32 stack_depth, bool ebpf_from_cbpf,
bool tail_call_reachable, bool is_subprog)
{
u8 *prog = *pprog;
/* BPF trampoline can be made to work without these nops,
* but let's waste 5 bytes for now and optimize later
*/
EMIT_ENDBR();
memcpy(prog, x86_nops[5], X86_PATCH_SIZE); // NOTE:第一条指令,5 个字节大小的 nop
prog += X86_PATCH_SIZE;
if (!ebpf_from_cbpf) {
if (tail_call_reachable && !is_subprog)
EMIT2(0x31, 0xC0); /* xor eax, eax */
else
EMIT2(0x66, 0x90); /* nop2 */ // NOTE: 因为没有 tailcall,所以第二条指令是 1 个字节大小的 nop
}
EMIT1(0x55); /* push rbp */
EMIT3(0x48, 0x89, 0xE5); /* mov rbp, rsp */
/* X86_TAIL_CALL_OFFSET is here */
EMIT_ENDBR();
/* sub rsp, rounded_stack_depth */
if (stack_depth)
EMIT3_off32(0x48, 0x81, 0xEC, round_up(stack_depth, 8));
if (tail_call_reachable)
EMIT1(0x50); /* push rax */
*pprog = prog;
}
每个 bpf prog 的 prologue
都包含有 5 个字节大小的 nop
指令。据观察,每个可 trace 的内核函数的第一条指令也是如此。
freplace
技术便是对 prologue
进行 poke
的简单应用,将 prologue
里的第一条 nop
指令替换成 jmp
指令。
不过,想要真正掌握 freplace
,需要理解一些前置知识:
Recommend
About Archive Link
everyday a lot of link has gone away.
archive.link will keep it forever.