当前位置: 首页 > Linux

Datenlord-利用BPF实现用户态追踪

时间:2023-04-06 18:33:22 Linux

BPF是最近Linux内核领域的一个热门技术。传统的BPF是指tcpdump命令用来过滤网络数据包的工具。现在BPF已经有了很大的扩展,不再是一个简单的网络包过滤工具,对应BerkeleyPacketFilter的缩写。从Kernel4.9开始,BPF成为了一个完整的内核扩展工具。BPF在内核中运行一个沙箱来执行BPF字节码。在执行BPF程序之前,BPF检查器对BPF程序的字节码进行安全检查(例如,访问前必须确定指针不为空,代码中不能有循环等),确保BPF程序不会导致系统崩溃,因为BPF程序是在内核模式下执行的。因此BPF可以安全的在内核态执行用户编写的程序,有安全保障,比编写内核模块要安全得多。正是因为BPF可以保证安全,运行在内核态,所以可以大大简化之前很多复杂的事情。目前BPF已经应用于性能分析、网络、安全、驱动、区块链等领域。已经有很多文章介绍了BPF在内核跟踪中的应用。内核有各种定义的跟踪点用于静态跟踪,动态跟踪也可以用于跟踪内核函数调用。使用BPF进行内核跟踪具有低开销和良好的性能。因为BPF程序运行在内核态,不需要将采集到的数据传回用户态进行处理,而是直接在内核态完成数据采集和处理,然后将处理结果传回用户态进行处理展示。我不会详细介绍使用BPF进行内核跟踪。本文重点介绍使用BPF进行用户空间跟踪。用户状态跟踪在执行用户状态跟踪之前,必须在程序中定义跟踪点。这里主要介绍UserlandStaticallyDefinedTracepoints(USDT)。因为USDT依赖systemtap-sdt-dev包,所以需要先安装依赖包。以Ubuntu为例,运行sudoaptinstallsystemtap-sdt-dev进行安装。以下示例test-server.c显示了如何使用宏DTRACE_PROBE1在用户程序中定义跟踪点:#include#includeintmain(intargc,char**argv){intidx=0;while(1){idx++;//自定义跟踪点DTRACE_PROBE1(test_grp,test_idx,idx);睡觉(1);}return0;}在上面的例子中,一个名为test_grp的组是用宏DTRACE_PROBE1定义的,用户模式跟踪点名为test_idx。跟踪点只有一个参数,它是一个递增的整数变量。如果要定义具有两个或更多参数的跟踪点,请使用DTRACE_PROBE2、DTRACE_PROBE3等。如果tracepoint没有参数,则用DTRACE_PROBE定义。使用gcc命令编译上述程序gcc-g-fno-omit-frame-pointer-O0test-server.c-otest-server得到可执行二进制程序test-server。USDT的原理是利用宏DTRACE_PROBE1定义的tracepoint对应编译二进制文件中的CPU指令nop操作。可以用gdb查看二进制文件test-server对应的程序集:$gdbtest-server......Readingsymbolsfromtest-server...(gdb)disasmain<<==gdb命令是用于查看二进制文件的汇编函数main的汇编程序代码转储:0x0000000000001149<+0>:endbr640x000000000000114d<+4>:push%rbp0x000000000000114e<+5>:mov%rsp,%rbp0x0000000010000114e<+8>15:sub$0x20,%rsp0x0000000000001155<+12>:mov%edi,-0x14(%rbp)0x0000000000001158<+15>:mov%20(0%rbp)0x0000000000000115c<+19>:movl$0x0,-0x4(%rbp)0x0000000000001163<+26>:addl$0x1,-0x4(%rbp)0x0000000000001167<+30>:nop<<=tracepoint指令对应0x000000000001168<+31>:mov$0x1,%edi0x00000016168<+31>:mov$0x1,%edi0x0000001616>d6000x10500x0000000000001172<+41>:jmp0x1163组装结束,nop操作只是清空一个CPU周期,成本很低,所以程序中定义USDT对性能的影响可以忽略在运行时,如果使用BPF去tracetest-server(具体的tracing细节后面会讲),然后用sudogdb查看runtime程序对应的assembly,会发现nop操作已经被替换了int3指令:$sudogdb-p$(pidoftest-server)......(gdb)disasmainDump函数main的汇编代码:0x00005583ee599149<+0>:endbr640x00005583ee59914d<+4>:push%rbp0x00005583ee59914e<+5>:mov%rsp,%rbp0x00005583ee599151<+8>:sub$0x20,%rsp0x00005583ee599155<+12>:mov%edi,-0x14(%rbp)0x00005583ee599158<+%rbp>:mov<+15>-0x209150(%rbp)<+19>:movl$0x0,-0x4(%rbp)0x00005583ee599163<+26>:addl$0x1,-0x4(%rbp)0x00005583ee599167<+30>:int3<<==跟踪点对应到int3指令0x00005583ee599168<+31>:mov$0x1,%edi0x00005583ee59916d<+36>:callq0x5583ee5990500x00005583ee599172<+41>:jmp0x5583ee5990500x00005583ee599172<+41>:jmp0x5583ee599050汇编中发现nop指令被int3指令代替,用来设置断点转而执行BPF等追踪工具的指令,如采集tracepoint数据等。因此,USDT从指令层面进行追踪,对程序运行时的性能影响不大。使用bpftrace命令进行跟踪。这里先介绍bpftrace命令。该命令无需编写BPF程序,仅使用脚本实现跟踪。在Ubuntu上运行sudoapt-getinstall-ybpftrace安装bpftrace。要运行以下bpftrace命令,您必须使用sudo权限:$sudobpftrace\-e'usdt:/home/pwang/usdt_test/test-server:test_grp:test_idx\{printf("%d\n",arg0);}'\-p$(pidoftest-server)Attaching1probe...136513661367136813691370^C上面的bpftrace命令收集了test-server名为test_idx的tracepoint,并打印出tracepoint第一个参数的值。bpftrace命令包括以下组成部分:-e表示运行下面的脚本,该脚本由两部分组成:1.第一部分usdt:/home/pwang/usdt_test/test-server:test_grp:test_idx是一个冒号,用四个隔开sections:usdt是跟踪类型;/home/pwang/usdt_test/test-server为被跟踪程序的绝对路径;test_grp是tracepoint的组名,也可以省略;test_idx是跟踪点的名称。2.第二部分{printf("%d\n",arg0);}是打印tracepoint第一个参数的值;-p是指定被跟踪的进程ID。使用BCC工具编写BPF程序进行跟踪BCC是一个用于编写BPF程序的开发框架和编译器。目前BPF程序主要是用C语言编写的,但并不支持C的所有语法,比如不支持循环。BCC调用LLVM将BPFC代码转换成BPF字节码,然后通过BPFchecker的检查。检查通过的BPF字节码成功加载到沙箱中运行。BCC还支持使用Python代码与沙箱中运行的BPF程序进行交互,例如获取和显示BPF跟踪结果。在Ubuntu上运行sudoapt-getinstall-ybpfcc-tools以安装BCC。以下是嵌入了BPFC代码的BCCPython脚本:#!/usr/bin/python3frombccimportBPF,USDTimportsys//嵌入式BPFC代码bpf_src="""inttrace_udst(structpt_regs*ctx){u32idx;bpf_usdt_readarg(1,ctx,&idx);bpf_trace_printk("test_idx=%dtracepointcached\\n",idx);return0;};"""//程序中指定USDT的tracepoint并关联BPF函数procid=int(sys.argv[1])u=USDT(pid=procid)u.enable_probe(probe="test_idx",fn_name="trace_udst")//加载BPF程序并打印结果b=BPF(text=bpf_src,usdt_contexts=[u])print("StartUSDTtracing")b.trace_print()上面的BCC代码分为几个部分:1.第一部分是嵌入式BPFC代码,以string的方式定义到bpf_src变量。bpf_src中的C代码定义了trace_udst函数。此函数的输入参数是捕获的跟踪点上下文。这个函数做了两件事:bpf_usdt_readarg(1,ctx,&idx);用于从tracepoint上下文中读取第一个tracepoint参数的值;bpf_trace_printk("test_idx=%dtracepointcached\n",idx);打印出结果,打印出来的结果会被送到一个pipeline,pipeline的路径是/sys/kernel/debug/tracing/trace_pipe;2.第二部分指定USDT的tracepoint,关联BPF程序实现tracing:u=USDT(pid=procid)生成USDT对象;u.enable_probe(probe="test_idx",fn_name="trace_udst")指定tracepoint为text_idx,指定BPF程序中的trace_udst函数作为trace到该点的执行操作;3.第三部分是加载BPF程序并从管道中取出输出并打印到STDOUT:b=BPF(text=bpf_src,usdt_contexts=[u])生成一个BPF对象并将其与一个USDT对象相关联,将BPF程序加载到沙箱中进行检查和运行;b.trace_print()将每个BPF程序跟踪的输出结果打印到text_idx。下面是BCC码的执行结果。请注意,该脚本是使用python3运行的:$sudo./ebpf_hello_usdt.py$(pidoftest-server)StartUSDTtracingb'test-server-27125[000]....26587.633948:0:test_idx=2958tracepointcachted'b'test-server-27125[000]....26588.634090:0:test_idx=2959跟踪点缓存'b'test-server-27125[000]....26589.634237:0:test_idx=2960跟踪点缓存'b'测试-server-27125[000]....26590.634386:0:test_idx=2961跟踪点缓存'b'test-server-27125[000]....26591.634563:0:test_idx=2962跟踪点缓存'b'测试服务器-27125[000]....26592.634714:0:test_idx=2963tracepointcachted'b'test-server-27125[000]....26593.634870:0:test_idx=2964tracepointcached'你可以看到BPF程序成功追踪到递增的text_idx值。BPF是一个非常强大的内核扩展工具。使用BPF,你可以做很多以前很复杂或不可能的事情。另外,有了BCC,大大降低了编写BPF程序的复杂度,可以用BCC编写非常强大的内核扩展应用程序,安全性和性能都有保障。业内人士普遍认为,有了BPF,Linux内核会越来越微内核化,内核中的很多东西或者内核扩展都可以由用户自己编写BPF程序来完成。作者|王璞