迫于Linux eBPF文档过少,我边学习边把对其的理解记录下来,供后来者参考。
本文是eBPF系列的第二篇:例子——openat2。
- 若对Linux tracing技术不清晰,可参考前置篇the Overview of Linux Tracing Tools
- 若对eBPF的工作流程不清晰,可参考eBPF系列一:Hello eBPF
Introduction
在计算机中运行程序、读写文件,都会涉及到文件的打开操作,Linux v5.10与文件打开相关的系统调用有open() / creat() / openat() / openat2
这四类,在使用glibc v2.32时,几乎所有的文件打开操作使用的都是openat2()
这个系统调用。
openat2()
是POSIX标准定义的系统调用之一,用于文件的创建或打开,它有4个参数,其中第一个参数dirfd
为文件夹的描述符,第二个参数pathname
为文件路径。
这里实现一个eBPF程序,他能获取系统调用openat()
的前两个参数信息。
Instance
这些源码在这里。
Example 1
在v5.10版本的内核上,系统调用入口SYSCALL_DEFINE4(openat2...)
对参数做了一些简单的检查后,调用的是do_sys_openat2()
进行进一步处理,其因此可以使用kprobe hook do_sys_openat2()
间接地打印openat2()
的参数信息。它的第一、二个参数含义等同于openat2()
,因此打印前两个参数信息即可。相关源码主要如下:
1 | SEC("kprobe/do_sys_openat2") |
运行:
1 | $ make hello openat1_kern.o |
Example 2
参数pathname
在do_sys_openat2()
是个指向用户态程序空间的char
类型的指针,若想把文件名复制到eBPF程序中,则需要借助bpf_probe_read_user_str()
了:
1 | char msg[256]; |
Internal
Linux区分了不同特权等级下程序可访问的虚拟内存空间范围,它是通过access_ok()
检查struct thread_info
中的addr_limit
来实现的。有一组API {get,set}_fs()
可用于在kernel运行时中控制可访问的内存空间范围。
Note:
Example 3
这里写一写怎么直接hook系统调用的入口,即SYSCALL_DEFINE4(openat2...)
。
SYSCALL_DEFINE4
一步步展开如下:
1 | SYSCALL_DEFINE4 |
拼接起来,amd64架构,系统调用openat2()
的入口函数名为__x64_sys_openat2()
,参数类型是struct pt_regs *
。因此eBPF程序这么写:
1 | SEC("kprobe/sys_openat") |
代码中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...)
所示的参数信息。
Example 4
Linux内部API经常变更,使用kprobe hook特定的函数名不具有普适性。Linux为系统调用提供了tracepoint,若用tracepoint例子则这么写:
1 | struct syscalls_enter_openat_args { |
struct syscalls_enter_openat_args
成员信息来自tracefs中的文件events/syscalls/sys_enter_openat2/format
。