title | date | tags | |||
---|---|---|---|---|---|
eBPF系列二:例子——openat2 |
2021-02-12 05:24:10 -0800 |
|
Blog Post: eBPF系列二: 例子openat2
迫于Linux eBPF文档过少,我边学习边把对其的理解记录下来,供后来者参考。 本文是eBPF系列的第二篇:例子——openat2。
- 若对Linux tracing技术不清晰,可参考前置篇the Overview of Linux Tracing Tools
- 若对eBPF的工作流程不清晰,可参考eBPF系列一:Hello eBPF
在计算机中运行程序、读写文件,都会涉及到文件的打开操作,Linux v5.10与文件打开相关的系统调用有open() / creat() / openat() / openat2
这四类,在使用glibc v2.32时,几乎所有的文件打开操作使用的都是openat2()
这个系统调用。
openat2()
是POSIX标准定义的系统调用之一,用于文件的创建或打开,它有4个参数,其中第一个参数dirfd
为文件夹的描述符,第二个参数pathname
为文件路径。
这里实现一个eBPF程序,他能获取系统调用openat()
的前两个参数信息。
这些源码在这里。
在v5.10版本的内核上,系统调用入口SYSCALL_DEFINE4(openat2...)
对参数做了一些简单的检查后,调用的是do_sys_openat2()
进行进一步处理,其因此可以使用kprobe hook do_sys_openat2()
间接地打印openat2()
的参数信息。它的第一、二个参数含义等同于openat2()
,因此打印前两个参数信息即可。相关源码主要如下:
SEC("kprobe/do_sys_openat2")
int hello(struct pt_regs *ctx) {
const int dirfd = PT_REGS_PARM1(ctx);
const char *pathname = (char *)PT_REGS_PARM2(ctx);
char fmt[] = "@dirfd='%d' @pathname='%s'";
bpf_trace_printk(fmt, sizeof(fmt), dirfd, pathname);
return 0;
}
运行:
$ make hello openat1_kern.o
$ sudo ./hello openat1_kern.o
参数pathname
在do_sys_openat2()
是个指向用户态程序空间的char
类型的指针,若想把文件名复制到eBPF程序中,则需要借助bpf_probe_read_user_str()
了:
char msg[256];
bpf_probe_read_user_str(msg, sizeof(msg), pathname);
Linux区分了不同特权等级下程序可访问的虚拟内存空间范围,它是通过access_ok()
检查struct thread_info
中的addr_limit
来实现的。有一组API {get,set}_fs()
可用于在kernel运行时中控制可访问的内存空间范围。
Note:
这里写一写怎么直接hook系统调用的入口,即SYSCALL_DEFINE4(openat2...)
。
SYSCALL_DEFINE4
一步步展开如下:
SYSCALL_DEFINE4
--> SYSCALL_DEFINEx
--> SYSCALL_METADATA // syscall tracepoint的封装
__SYSCALL_DEFINEx
// for x86
__SYSCALL_DEFINEx
--> __X64_SYS_STUBx // amd64使用
__IA32_SYS_STUBx // ia32使用
// for amd64
__X64_SYS_STUBx
--> __SYS_STUBx(x64, sys##name, SC_X86_64_REGS_TO_ARGS(x, __VA_ARGS__)))
--> long __##abi##_##name(const struct pt_regs *regs)
拼接起来,amd64架构,系统调用openat2()
的入口函数名为__x64_sys_openat2()
,参数类型是struct pt_regs *
。因此eBPF程序这么写:
SEC("kprobe/sys_openat")
int hello(struct pt_regs *ctx) {
char fmt[] = "@dirfd='%d' @pathname='%s'";
struct pt_regs *real_regs = (struct pt_regs *)PT_REGS_PARM1(ctx);
int dirfd = PT_REGS_PARM1_CORE(real_regs);
char *pathname = (char *)PT_REGS_PARM2_CORE(real_regs);
bpf_trace_printk(fmt, sizeof(fmt), dirfd, pathname);
return 0;
}
代码中SEC("kprobe/sys_openat")
表示kprobe的hook point为sys_openat
,实际上用户态程序hello在调用load_and_attach()
时候会检查kprobe的hook point前缀是否是sys_
,若是对amd64则自动添加__x64_
前缀。
macro PT_REGS_PARMx_CORE
对bpf_probe_read_kernel()
做了封装,可以简单地认为用于获取hook func的第x个参数。因hook func的参数是struct pt_regs *
,所以需要使用bpf_probe_read_kernel()
取得struct pt_regs
,进而获取得到系统调用SYSCALL_DEFINE4(openat2...)
所示的参数信息。
Linux内部API经常变更,使用kprobe hook特定的函数名不具有普适性。Linux为系统调用提供了tracepoint,若用tracepoint例子则这么写:
struct syscalls_enter_openat_args {
unsigned short common_type;
unsigned char common_flags;
unsigned char common_preempt_count;
int common_pid;
long syscall_nr;
long dfd;
long filename_ptr;
long flags;
long mode;
};
SEC("tracepoint/syscalls/sys_enter_openat")
int hello(struct syscalls_enter_openat_args *ctx) {
char fmt[] = "@dirfd='%d' @pathname='%s'";
bpf_trace_printk(fmt, sizeof(fmt), ctx->dfd, (char *)ctx->filename_ptr);
return 0;
}
struct syscalls_enter_openat_args
成员信息来自tracefs中的文件events/syscalls/sys_enter_openat2/format
。