一 前言

对于第一次BPF的介绍,只是简单介绍了BPF,写了一个没啥用的例子,如何利用BCC写一个有点用的程序以及BCC中有哪些牛逼的工具,是本篇需要介绍的。

二 BPF程序开发一般流程

再来回忆下,BPF开发程序,加载和运行过程,如下图, 从用户程序角度看分为三个部分:

  1. BPF代码,即内核事件发生时候调用的代码,下图中的bpf_prog.c,通过LLVM和clang编译器,将代码转成BPF字节码。

  2. 加载BPF代码的用户代码,即负责进行系统调用将编写的BPF代码编译后的字节码,加载到内核中探针上(kprobe)。

  3. 用户程序通过读取映射表内容,来获取BPF代码的输出,即bpf_prog.c这个模块输出。内核的角度

  4. 字节码被传递到内核后,内核通过校验器进行校验;

  5. 校验通过后,事件被启动,BPF程序被挂载到事件上。

  6. 将数据输出到映射表中。

需要说明的是,内核会判断bpf_jit_enabled是否开启,这个可以在内核源码编译的时候开启,开启后,即时编译器(JIT)会将eBPF字节码编译成本地机器指令,执行起来效率更高。

我们知道BPF程序和事件关联的,这个里面的事件不止内核态的事件,还包括用户态的事件,最重要的是三类:

  1. 动态插桩,内核态(kprobes的BPF支持 )

  2. 动态插桩,用户态(uprobes的BPF支持)

  3. 静态跟踪,内核态(tracepoint)

  4. 时间采样事件(BPF,使用perf_event_open()) 还有很多,都不列出来了,根据挂载事件的不同,BPF的程序类型不同,每种eBPF程序挂载到不同的内核函数,内核跟踪点,用户态插桩点,性能事件上,当这些内核函数,内核跟踪点,用户态插桩点,性能事件被调用的时候,挂载在上面的BPF程序自动执行。

2.1 利用BCC编写C代码

2.1.1 BPF内核运行的代码

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include <linux/ptrace.h>// 内核的动态插桩位置:do_sys_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'";char msg[256];bpf_probe_read_user_str(msg, sizeof(msg), pathname);bpf_trace_printk(fmt, sizeof(fmt), dirfd, msg);return 0;
}char _license[] SEC("license") = "GPL";

说明:

  1. BPF的代码在内核的动态插桩位置为:openat2,此函数在glibc2.32时候,所有的打开文件的操作都是openat2,具体的参数声明如下:

int openat2(int dirfd, const char *pathname,const struct open_how *how, size_t size);
详情参考:[open(2) - Linux manual page (man7.org)](https://man7.org/linux/man-pages/man2/openat.2.html)
  1. 获取第一个参数和第二个参数值 PT_REGS_PARM1 和PT_REGS_PARM2 是获取openat2的前两个参数

bpf_probe_read_user_str(msg, sizeof(msg), pathname);

bpf的辅助函数,从用户空间读取字符串,这里面不复制下面也可以打印,只是通过这个调用后,就可以把用户空间的路径名复制到eBPF程序中。

  1. bpf_trace_printk 即向调试系统文件写入调试信息,输出位置是: /sys/kernel/debug/tracing/trace_pipe  不是标准输出。

  2. SEC("license") 宏,定义在头文件bpf/bpf_helpers.h 为:attribute((section(NAME), used)) 即把变量,函数,放在ELF文件中名为license的段中,_license 定义了eBPF程序的license类型,eBPF程序会校验此license是否为GPL兼容的。

2.1.1 编译BPF程序

我们用安装的clang编译器,结合llvm工具将c写的BPF程序编译成bpf字节码,指令如下:

clang -O2  -target bpf -c open_file.c  -o open_file.o

如果采用两步编译就是:

# 前端编译生成LLVM IR文件
clang -O2 -Wall -emit-llvm -S open_file.c
# 使用LLVM 后端生成BPF字节码
llc open_file.ll -march=bpf -filetype=obj -o open_file.o

过程:

clang 把 eBPF 程序翻译为中间语言(IR)是 LLVM 的 object (参数 -c -emit-llvm),再通过 llc 编译、链接成 target 为 bpf 的 ELF 程序(参数 -march=bpf -filetype=obj)

bpf  表示目标为bpf字节码格式,可以通过以下命令看下详细的bpf字节码:

[root@localhost hello-openat2]# llvm-objdump -d  open_file.oopen_file.o: file format elf64-bpfDisassembly of section kprobe/do_sys_openat2:0000000000000000 <hello>:0: bf 17 00 00 00 00 00 00 r7 = r11: 85 10 00 00 ff ff ff ff call -12: bf 06 00 00 00 00 00 00 r6 = r03: bf 71 00 00 00 00 00 00 r1 = r74: 85 10 00 00 ff ff ff ff call -15: b7 01 00 00 73 27 00 00 r1 = 100996: 6b 1a f8 ff 00 00 00 00 *(u16 *)(r10 - 8) = r17: 18 01 00 00 68 6e 61 6d 00 00 00 00 65 3d 27 25 r1 = 2677176009331142248 ll9: 7b 1a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r110: 18 01 00 00 25 64 27 20 00 00 00 00 40 70 61 74 r1 = 8386107401860244517 ll12: 7b 1a e8 ff 00 00 00 00 *(u64 *)(r10 - 24) = r113: 18 01 00 00 40 64 69 72 00 00 00 00 66 64 3d 27 r1 = 2827526532227490880 ll15: 7b 1a e0 ff 00 00 00 00 *(u64 *)(r10 - 32) = r116: b7 01 00 00 00 00 00 00 r1 = 017: 73 1a fa ff 00 00 00 00 *(u8 *)(r10 - 6) = r118: 67 00 00 00 20 00 00 00 r0 <<= 3219: c7 00 00 00 20 00 00 00 r0 s>>= 3220: bf a7 00 00 00 00 00 00 r7 = r1021: 07 07 00 00 e0 fe ff ff r7 += -28822: bf 71 00 00 00 00 00 00 r1 = r723: b7 02 00 00 00 01 00 00 r2 = 25624: bf 03 00 00 00 00 00 00 r3 = r025: 85 00 00 00 72 00 00 00 call 11426: bf a1 00 00 00 00 00 00 r1 = r1027: 07 01 00 00 e0 ff ff ff r1 += -3228: b7 02 00 00 1b 00 00 00 r2 = 2729: bf 63 00 00 00 00 00 00 r3 = r630: bf 74 00 00 00 00 00 00 r4 = r731: 85 00 00 00 06 00 00 00 call 632: b7 00 00 00 00 00 00 00 r0 = 033: 95 00 00 00 00 00 00 00 exit

2.1.1 编译用户端加载BPF程序

BPF程序需要用户端程序加载到内核上,并读取ebpf的输出,如下代码:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>#include "bpf_load.h"#define MAX_FILENAME_LEN 256void read_msg(void);int main(int argc, char *argv[]) {int ch;if (argc != 2) {fprintf(stdout, "Usage: %s <eBPF program>\n", argv[0]);return 0;}if (access(argv[1], R_OK) != 0) {fprintf(stderr, "ERROR: access('%s'): %s\n",argv[0], strerror(errno));return 1;}if (load_bpf_file(argv[1])) {fprintf(stdout, "%s", bpf_log_buf);return 1;}fprintf(stdout, ">>> begin to read trace_pipe...\n");read_msg();return 0;
}void read_msg(void) {FILE *fp;char filename[MAX_FILENAME_LEN];char *line = NULL;size_t len = 0;snprintf(filename, sizeof(filename), DEBUGFS "trace_pipe");fp = fopen(filename, "r");if (fp == NULL) {fprintf(stderr, "ERROR: fopen('%s'): %s\n",filename, strerror(errno));return;}while (getline(&line, &len, fp) != -1) {fprintf(stdout, "%s", line);continue;}return;
}

编译也比较简单:

cc -c -o bpf_load.o bpf_load.c
cc -c -o open_file_user.o open_file_user.c
cc -o bpfopen open_file_user.o  -lbpf  -lelf  bpf_load.o

都编译好了,那么来运行下:

./bpfopen open_file.o
[root@localhost hello-openat2]# ./bpfopen open_file.o
bpf_load: section 1:.strtab data 0x3462d50 size 102 link 0 flags 0
bpf_load: section 3:kprobe/do_sys_openat2 data 0x34631d0 size 224 link 0 flags 6
bpf_load: section 4:.rodata.str1.16 data 0x34632c0 size 27 link 0 flags 50
bpf_load: section 5:license data 0x34632f0 size 4 link 0 flags 3
bpf_load: section 6:.eh_frame data 0x3463310 size 48 link 0 flags 2
bpf_load: section 7:.rel.eh_frame data 0x3463350 size 16 link 8 flags 0
bpf_load: section 8:.symtab data 0x3463370 size 120 link 1 flags 0
>>> begin to read trace_pipe...<...>-231725  [006] d..31 219376.128743: bpf_trace_printk: @dirfd='-100' @pathname='/sys/kernel/debug/tracing/trace_pipe'irqbalance-928     [006] d..31 219376.546195: bpf_trace_printk: @dirfd='-100' @pathname='/proc/interrupts'irqbalance-928     [006] d..31 219376.546387: bpf_trace_printk: @dirfd='-100' @pathname='/proc/stat'pmdalinux-1326    [006] d..31 219378.265136: bpf_trace_printk: @dirfd='-100' @pathname='/proc/net/dev'pmdalinux-1326    [006] d..31 219378.265198: bpf_trace_printk: @dirfd='-100' @pathname='/proc/net/netstat'
....

输出的关键内容解释:irqbalance-928    为进程名和进程id,[006] 表示cpu编号,d..31表示一系列选项, 219376.546195 表示时间戳, bpf_trace_printk:表示函数名,@dirfd='-100' @pathname='/proc/interrupts' 表示输出内容。

三 查看BPF程序

  1. 利用bpftool工具查看正在运行的BPF程序

[root@localhost bpfstest]# bpftool prog show
[root@localhost bpfstest]# bpftool prog show
34: kprobe  tag b43a500aac124eef  gplloaded_at 2022-02-26T04:01:15-0500  uid 0xlated 224B  jited 139B  memlock 4096B
  1. 查看bpf字节码内容

root@localhost bpfstest]#  bpftool prog dump xlated id 340: (79) r6 = *(u64 *)(r1 +112)1: (79) r3 = *(u64 *)(r1 +104)2: (b7) r1 = 100993: (6b) *(u16 *)(r10 -8) = r14: (18) r1 = 0x25273d656d616e686: (7b) *(u64 *)(r10 -16) = r17: (18) r1 = 0x74617040202764259: (7b) *(u64 *)(r10 -24) = r110: (18) r1 = 0x273d64667269644012: (7b) *(u64 *)(r10 -32) = r113: (b7) r1 = 014: (73) *(u8 *)(r10 -6) = r115: (bf) r7 = r1016: (07) r7 += -28817: (bf) r1 = r718: (b7) r2 = 25619: (85) call bpf_probe_read_user_str#-6297620: (bf) r1 = r1021: (07) r1 += -3222: (b7) r2 = 2723: (bf) r3 = r624: (bf) r4 = r725: (85) call bpf_trace_printk#-5467226: (b7) r0 = 027: (95) exit

四 BCC中牛逼的工具

其实我们刚开发的工具在BCC中已经存在类似的,BCC不管有高级语言的接口还有一些牛逼的工具,非常方便,在解决一些麻烦问题的时候很有用:

4.1 execsnoop

这个工具通过追踪execve系统调用,来查看新创建的进程信息,如果我们遇到执行很短的进程,通过top命令看不到用它排查很有帮助,top命令是周期运行的,如果程序很快停止是无法查到的:

#execsnoop
PCOMM            PID    PPID   RET ARGS
ls               232501 230335   0 /usr/bin/ls --color=auto
ls               232502 230335   0 /usr/bin/ls --color=auto
make             232504 230335   0 /usr/bin/make
cc               232505 232504   0 /usr/bin/cc -print-file-name=include
sh               232506 232504   0 /bin/sh -c pkg-config --exists libbpf; echo $?
pkg-config       232507 232506   0 /usr/bin/pkg-config --exists libbpf
sh               232508 232504   0 /bin/sh -c pkg-config --exists libelf; echo $?
pkg-config       232509 232508   0 /usr/bin/pkg-config --exists libelf

4.2 opensnoop

这个类似我们上面开发的工具,在每次进行open调用的时候执行,打印打开文件的路径,程序名,进程号,成功还是失败(ERR 为2 表示失败)等信息,如下:

PID    COMM               FD ERR PATH
928    irqbalance          6   0 /proc/interrupts
928    irqbalance          6   0 /proc/stat
944    vmtoolsd           10   0 /proc/meminfo
944    vmtoolsd           10   0 /proc/vmstat
944    vmtoolsd           10   0 /proc/stat
944    vmtoolsd           10   0 /proc/zoneinfo

4.3 io相关

# 直方图的形式来跟踪磁盘IO延迟,会显示磁盘的IO延迟的分布情况不过在我机器上执行失败,估计是事件挂载点发生了变化不兼容导致
biolatency -m
#打印每次io请求延迟大小,磁盘,进程名等信息
biosnoop

4.3 cachestat

打印文件缓存的命中情况,用于排查缓存命中过低问题。

4.4 tcpconnect

每次调用tcp的connect时候打印一行信息,包括源地址,目标地址,端口,进程名,和进程id信息,用户发现非法的外连信息。打印信息是对外连接的信息。

4.5 tcpaccept

每次tcp监听接受被动连接的时候打印进程id,进程名,源地址,目标地址,端口信息。

4.6 tcpretrans

每次tcp重传时候会打印一条信息,包括源地址,目标地址,源端口,目标端口,以及连接状态,用于排查tcp网络慢问题。

4.7 runqlat

打印进程等待cpu运行时间的直方图,定位超出预期的cpu时间,可能是cpu包和调度等原因导致。

4.8 profile

cpu执行分析,用于判断cpu的时间主要耗费在哪些代码路径上了,不过我觉得用perf  top结合火焰图来的更好些。

透视Linux内核 神奇的BPF二相关推荐

  1. 透视Linux内核神奇的BPF 一

    一 前言 作为一个coder,时不时会遇到性能问题,有时候明明看资源,cpu,io都占用不高,程序的性能就是上不去,真有一种想进入到计算机里面看看到底发生什么的冲突:还有优化性能的时候不知道整个系统的 ...

  2. 首本深入讲解Linux内核观测技术BPF的书上市!

    新书速递 导读:BPF通过一种软件定义的方式,将内核的行为和数据暴露给用户空间,开发者可以通过在用户空间编写BPF程序,加载到内核空间执行,进而实现对内核行为的灵活管理和控制. 在计算机系统中,包过滤 ...

  3. Linux内核LED模块分析(二)

    Linux内核LED模块分析(二) 上次分析到那里后,还是有些同志说看不懂,那我就继续分析一把我认为不需要继续分析的东西吧.上回分析了 led_cdev和trigger的关系后就没有继续说了.有同志还 ...

  4. Linux 内核观测技术BPF

    BPF简介 BPF,全称是Berkeley Packet Filter(伯克利数据包过滤器)的缩写.其诞生于1992年,最初的目的是提升网络包过滤工具的性能.后面,随着这个工具重新实现BPF的内核补丁 ...

  5. Linux内核实验孟宁,《linux内核分析》实验二:时间片轮转多道程序运行原理

    一.概述 本文通过分析一个简单的时间片轮转多道程序的内核 mykernel,来理解操作系统是如何工作的. mykernel 是孟宁老师的一个开源项目,借助 Linux 内核部分源代码模拟存储程序计算机 ...

  6. Linux 内核链表剖析(二十)

    上节博客中,我们讲到了 Linux 中的宏定义 offsetof 与 container_of 宏.那么本节我们的课程目标就是一直 Linux 内核链表,使其适用于非 GNU 编译器,分析 Linux ...

  7. Linux内核list_head学习(二)

    前一篇文章讨论了list_head 结构的基本结构和实现原理,本文主要介绍一下实例代码. 自己如果想在应用程序中使用list_head 的相应操作(当然应该没人使用了,C++ STL提供了list 用 ...

  8. 《Linux内核分析》(二)——从一个简单Linux内核分析进程切换原理

    转载:https://blog.csdn.net/FIELDOFFIER/article/details/44280717 <Linux内核分析>MOOC课程http://mooc.stu ...

  9. 第3章 Linux内核调试手段之二

    =================== gdb 和 addr2line 调试内核模块 内核模块插入内核链表的时候,会调用 init 里面的程序,我们上面给的那个例程的程序因为是经过多年风吹雨打的,但是 ...

最新文章

  1. zabbixdocker里的mysql_Zabbix Docker
  2. NR 5G 运营发布
  3. ssl提高组周四备考赛【2018.11.1】
  4. php mian函数,电脑main什么意思
  5. Vue的调试工具(Chrome 浏览器)配置
  6. ssh client 报 algorithm negotiation failed的解决方法
  7. c语言线性拉伸0到255,数字图像处理作业题.doc
  8. 钟平老师的逻辑英语语法
  9. 记录This request has been blocked; the content must be served over HTTPS.
  10. 完备性的定义(ZZ)
  11. 计算机网络基础以及linux面试知识点总结
  12. 网站app被劫持怎么办?dns被劫持,域名被劫持,HTTPDNS阿里云域名防劫持, DNSPod移动解析防劫持服务D+...
  13. 关于人生的一些想法,和淘客SEO的思想。
  14. msf反弹shell成功,却无法建立会话处理办法
  15. 提高计算机网络可靠性的对策,提高计算机网络可靠性的方法研究
  16. 布拉格捷克理工大学研究团队:Prisma进化版
  17. 原生js实现扫雷游戏
  18. 天津高一学业水平测试计算机,2019年1月天津高中学业水平考试标准
  19. How to Haartraining.
  20. 基于单片机的家庭火灾警报系统设计

热门文章

  1. Nestlé Skin Health的医疗解决方案公司Galderma发布nemolizumab治疗中重度异位性皮炎2b期研究阳性结果
  2. 游戏最终排名预测--kaggle项目笔记
  3. linux安装svn使用解压包的方式
  4. 西安电子科技大学网络与信息安全学院专硕951考研回顾
  5. 幼儿园买玩具(暴力破解)
  6. AutoJS4.1.0实战教程 ---火火视频极速版
  7. python 全栈开发,Day91(Vue实例的生命周期,组件间通信之中央事件总线bus,Vue Router,vue-cli 工具)...
  8. Credit-based Flow Control的前世今生
  9. 类变量、成员变量、局部变量介绍说明
  10. 阿基米德优化算法AOA附Matlab代码