[转帖]调试你的BPF程序

调试,bpf,程序 · 浏览次数 : 0

小编点评

## 摘要 这篇文章介绍了bpf_trace_printk函数,以及它如何被使用来打印内核中的信息。文章还讨论了BPF的限制,以及如何可以使用inline关键字提升性能。 ## 核心问题 * bpf_trace_printk函数如何打印内核中的信息? * 怎样可以使用inline关键字提升性能? * BPF的哪些限制? * 如何可以使用inline关键字提升性能? ## 关键点 * bpf_trace_printk函数的调用背景 * 不同版本的内核支持 * 如何在用户空间移到内核空间 * 性能优化方法 ## 文章结论 * bpf_trace_printk函数是一个用于打印内核中的信息的函数。 * 优化性能的方法包括使用inline关键字提升性能。 * BPF的限制包括无法使用普通共享库的。 * 用户空间移到内核空间可以发挥各种瑞士军刀的作用。

正文

https://cloud.tencent.com/developer/user/2577825

 

TL;DR

文章涉及的实验环境和代码可以到这个git repo获取: https://github.com/nevermosby/linux-bpf-learning

问题

当停止了上篇文章实验中的XDP ingress hook,只保留TC egress hook时,使用命令curl localhost也是无法访问Nginx容器服务的?这是为什么呢?

解题思路

  1. 添加调试日志,打印通过目标网卡网络包的源地址(source address)和目标地址(destination address),观察是否符合现实情况;
  2. 单步调试,在加载到内核的BPF程序加断点(breakpoint),一旦被触发时,观察上下文的内容。

添加调试日志

第一种思路理论上是比较容易实现的,就是在适当的位置添加printf函数,但由于这个函数需要在内核运行,而BPF中没有实现它,因此无法使用。事实上,BPF程序能的使用的C语言库数量有限,并且不支持调用外部库。

使用辅助函数(helper function)

为了克服这个限制,最常用的一种方法是定义和使用BPF辅助函数,即helper function。比如可以使用bpf_trace_printk()辅助函数,这个函数可以根据用户定义的输出,将BPF程序产生的对应日志消息保存在用来跟踪内核的文件夹(/sys/kernel/debug/tracing/),这样,我们就可以通过这些日志信息,分析和发现BPF程序执行过程中可能出现的错误。

BPF默认定义的辅助函数有很多,它们都是非常有用的,可谓是「能玩转辅助函数,就能玩转BPF编程」。可以在这里找到全量的辅助函数清单

使用bpf_trace_printk()辅助函数添加日志

这个函数的入门使用方法和输出说明可以在这篇文章中找到,现在我们把它加到BPF程序里。老规矩,直接上代码:

#include <stdbool.h>
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <linux/pkt_cls.h>
#include <stdio.h>

#include "bpf_endian.h"
#include "bpf_helpers.h"

typedef unsigned int		u32;
#define bpfprint(fmt, ...)                        \
    ({                                             \
        char ____fmt[] = fmt;                      \
        bpf_trace_printk(____fmt, sizeof(____fmt), \
                         ##__VA_ARGS__);           \
    })

/*
  check whether the packet is of TCP protocol
*/
static __inline bool is_TCP(void *data_begin, void *data_end){
  bpfprint("Entering is_TCP\n");
  struct ethhdr *eth = data_begin;

  // Check packet's size
  // the pointer arithmetic is based on the size of data type, current_address plus int(1) means:
  // new_address= current_address + size_of(data type)
  if ((void *)(eth + 1) > data_end) //
    return false;

  // Check if Ethernet frame has IP packet
  if (eth->h_proto == bpf_htons(ETH_P_IP))
  {
    struct iphdr *iph = (struct iphdr *)(eth + 1); // or (struct iphdr *)( ((void*)eth) + ETH_HLEN );
    if ((void *)(iph + 1) > data_end)
      return false;

    // extract src ip and destination ip
    u32 ip_src = iph->saddr;
    u32 ip_dst = iph->daddr;
    
    // 
    bpfprint("src ip addr1: %d.%d.%d\n",(ip_src) & 0xFF,(ip_src >> 8) & 0xFF,(ip_src >> 16) & 0xFF);
    bpfprint("src ip addr2:.%d\n",(ip_src >> 24) & 0xFF);

    bpfprint("dest ip addr1: %d.%d.%d\n",(ip_dst) & 0xFF,(ip_dst >> 8) & 0xFF,(ip_dst >> 16) & 0xFF);
    bpfprint("dest ip addr2: .%d\n",(ip_dst >> 24) & 0xFF);

    // Check if IP packet contains a TCP segment
    if (iph->protocol == IPPROTO_TCP)
      return true;
  }
  return false;
}

SEC("xdp")
int xdp_drop_tcp(struct xdp_md *ctx)
{

  void *data_end = (void *)(long)ctx->data_end;
  void *data = (void *)(long)ctx->data;

  if (is_TCP(data, data_end))
    return XDP_DROP;

  return XDP_PASS;
}

SEC("tc")
int tc_drop_tcp(struct __sk_buff *skb)
{

  bpfprint("Entering tc section\n");
  void *data = (void *)(long)skb->data;
  void *data_end = (void *)(long)skb->data_end;


  if (is_TCP(data, data_end))
    return TC_ACT_SHOT;
  else
    return TC_ACT_OK;
}

char _license[] SEC("license") = "GPL";

我们来一起看下更新的地方:

  1. 在代码开头,给bpf_trace_printk定义了一个「替身」——定义了一个名为bpfprint的宏(如下所示),因为原始函数名称和参数有点冗长,这样做,便于后续使用简单。另外,函数体内使用了「可变参数宏」,这是C99标准才有的语法,想要深入了解的同学看这篇文章
  2. 这个bpfprint宏在代码里总共使用了6次。其中2处是字符串常量,用来标识程序运行到了相关的函数体内。另外4次是带有变量的: 这是非常有趣的4行代码,里面涉及到几个关键点:
    • 位运算和与运算,目的是把从网络上下文中拿到的32位integer类型的IP地址(源地址和目的地址)转换成可读的字符串类型(类似192.168.1.1)。如果在用户空间,这种转换可以通过inet_ntoa这样的系统库进行操作,但是在内核空间下,由于没有这样的库函数,因此无法这样操作。那么,我们可以利用这个integer类型的存储本质,把代表IP地址每个段的数字「稀释」(这个词是本人自己想的,觉得挺贴切,如果有更好的,欢迎提出)出来。由于涉及的概念有点多,这其中的具体过程,将来独立写篇文章跟大家分享。
    • 为什么需要用两行bpfprint函数来打印一个IP地址?因为bpfprint函数只能接受4个参数,它背后的bpf_trace_printk函数只能接受5个参数,为什么呢?答案是跟BPF的底层架构有关,这里先按下不表。
  3. 眼尖的同学已经发现我们为内部函数is_TCP添加了__inline关键词,这个是出于什么用意呢?大家不妨先猜测下,我们下文揭开谜团。

观察bpf_trace_printk()辅助函数打印的日志

代码侧已经添加好日志打印函数,那如何观察到日志输出呢?上文提到了一个专门记录日志的文件夹,里面的文件就是保持不同trace日志的。我们的bpf程序日志可以通过读取这个文件/sys/kernel/debug/tracing/trace_pipe

# 它是一个流,会不停读取信息
cat /sys/kernel/debug/tracing/trace_pipe
# 另一个种等价方式
tail -f /sys/kernel/debug/tracing/trace

解开egress网络谜团

我们只给主机侧的容器veth网卡加载tc egress的BPF程序(上面的代码),来看实际测试运行效果。

视频里的测试结果如下表格所示:

从上表格可以看出,前3条命令都会以源地址为172.17.0.1经过目标veth网卡,去向目的地址172.17.0.2(这个Nginx容器的私网地址),进而match到了BPF程序,然后符合期待地丢包了。

熟悉Docker容器网络同学看到了上面两个IP地址,可能就明白怎么一回事了。下面是我画的简版容器网络流向图,其中的关键点就是docker0网桥和主机侧的veth网卡其实是「一体」的,只要是访问容器内服务的,都会经过某个veth网卡,再访问具体的容器服务私网IP,这样就形成了veth网卡的egress流量。

考考大家,上面表格中的最后一行,你能填上正确的结果么?

解开新的谜团

上篇文章留下的疑团已经解开了,但这次的文章又出现了新的谜团:

  • bpf_trace_printk究竟能打印多少个参数?
  • __inline关键字的作用是什么?

我们来一一分析。

深入分析辅助函数bpf_trace_printk

深入分析就必须看源代码了,先来看看这个函数的调用方式:

bpf_trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__);

可以看出这个函数的前两个参数是相对固定的:

  • 第一个是计划输出的字符串模板
  • 第二个是这个字符串模板的长度
  • 那么根据上文的代码,后面的可变参数宏就是最多可容纳3个的参数列表了

上文提到了看容纳的参数个数跟BPF底层架构有关,那是因为BPF底层是由11个64位寄存器、1个计数器和1个512字节BPF stack组成。寄存器命名规则是r0-r10,每个寄存器都有专属的作用:

  • r0保存了调用一次辅助函数后的返回值
  • r1r5 保存了从BPF程序到辅助函数的参数列表
  • r6r9 是用来保存中间值的寄存器,它可以被多个辅助函数调用
  • r10 是唯一的只读寄存器,包含访问BPF stack的指针

发现了么?BPF用来保存调用辅助函数的参数列表就是存放在r1r5这5个寄存器中,超过5个参数的调用目前是不支持的,因此只能workround下,多调用几次辅助函数,如同上文示例代码中所示。

第一个谜团已经解开了,在看第二谜团之前,我们来想一个问题,既然BPF辅助函数对于参数个数是有限制的,那一个BPF程序中调用BPF辅助函数会不会有限制?

答案是可能会有的。 这是什么意思呢?

这里就要说到BPF程序更多的限制了。BPF程序目前是无法使用普通共享库的,通常的做法是把BPF程序的常用库代码放在头文件中,然后在主程序中引用。

如果你确实想在主程序中使用函数调用(BPF to BPF function call),就像上文示例代码中的is_TCP,最佳实践是添加inline关键字,使这个函数成为内联函数,这样做的本质是,使得整个BPF程序编译后是一组连续的BPF指令,而不是非连续的,因为非连续的指令会导致BPF程序无法成功加载到内核。

可以到下面两个gist对比编译后的BPF指令:

这样可能还不够,由于一些版本的编译器仍然可以「智能」地帮你决定是否取消内联大型的函数(这里就呼应了上文给出「可能会有」的答案),因此推荐使用always_inline关键词,保证编译器能严格按照我们的期待进行内联编译,上文示例代码的__inline关键字背后就是这样的一个宏定义:

#ifndef __inline
# define __inline                         \
   inline __attribute__((always_inline))
#endif

特别说明下,内核高于4.16配合LLVM 6.0以上,就可以天然支持主程序中使用函数调用(BPF to BPF function call)了,无需各种显式内联了。

如果你再细看bpf_trace_printk函数的源代码,其实还能看到更多信息(或者说是限制),比如字符串版本中只允许1个%s,详细代码看这里,我简单梳理了这个函数源代码的调用背景,有兴趣的同学可以深入看看。

除了bpf_trace_printk函数可以添加日志之外,还可以使用bpf_perf_event_output函数(如果你使用BCC,它的入口函数是BPF_PERF_OUTPUT),据说性能更好。

暂无通用的单步调试方案

很可惜,BPF目前没有通用的单步调试方案,你可能在互联网上发现一个bpf_dbg.c方案,它是cBPF时代诞生的工具,分析pcap文件格式更友好(对,就是那个tcpdump的生成文件)。

下篇预告

既然在内核空间调试BPF有这个那个的限制,那么我们可不可以移到用户空间?这样就可以发挥各种瑞士军刀的作用了。

当然可以。

下一篇我们讲BPF map和bpftool。

与[转帖]调试你的BPF程序相似的内容:

[转帖]调试你的BPF程序

https://cloud.tencent.com/developer/user/2577825 TL;DR 文章涉及的实验环境和代码可以到这个git repo获取: https://github.com/nevermosby/linux-bpf-learning 问题 当停止了上篇文章实验中的XD

[转帖]kubelet 原理解析五: exec的背后

https://segmentfault.com/a/1190000022163850 概述 线上排查pod 问题一般有两种方式,kubectl log或者kubectl exec调试。如果你的 log 写不够优雅,或者需要排除网络问题必须进容器,就只能 exec 了。 # 在pod 123456-

【转帖】10个Linux 系统性能监控命令行工具

引言: 系统一旦跑起来,我们就希望它能够稳定运行,不要宕机,不出现速度变慢。因此,对于Linux 系统管理员来说每天监控和调试 Linux 系统的性能问题是一项繁重却又重要的工作。监控和保持系统启动并运行是很不容易的一件事。 下面是小编总结的十个实用的 Linux 系统监控命令,让你轻松保持系统的实

[转帖]《Linux性能优化实战》笔记(24)—— 动态追踪 DTrace

使用 perf 对系统内核线程进行分析时,内核线程依然还在正常运行中,所以这种方法也被称为动态追踪技术。动态追踪技术通过探针机制来采集内核或者应用程序的运行信息,从而可以不用修改内核和应用程序的代码就获得丰富的信息,帮你分析、定位想要排查的问题。 以往,在排查和调试性能问题时,我们往往需要先为应用程

[转帖]对扩展开放,对修改关闭

https://www.cnblogs.com/light-train-union/p/12674000.html 1、轻易不要去修改别人的方法 可能很多地方调用 有的方法抛出去后 被其他外部的调用(你都不知道 被谁调用了) 做兼容 做扩展 2、自己的设计 保证好的扩展性 对扩展开放,对修改关闭 经

[转帖]调优"四剑客"的实战演练,福尔摩斯•K带你轻松优化性能

前言 天下武功,唯快不破。在侦探的世界中,破案效率永远是衡量一名侦探能力的不二法门。作为推理界冉冉升起的新星,大侦探福尔摩斯·K凭借着冷静的头脑、严谨的思维,为我们展现了一场场华丽而热血的推理盛宴。 接下来,我们不仅仅是看客,还将追随福尔摩斯·K的脚步,体验一场身临其境的冒险。一起寻访产生数据库性能

[转帖]k8spacket 和 Grafana 对 kubernetes 的 TCP 数据包流量可视化

https://devpress.csdn.net/k8s/62ff4fe47e66823466193b95.html 你知道你不看的时候你的k8s集群在做什么吗?谁与他建立 TCP 通信?他调用了谁,例如,来自第三方库? 使用k8spacket和Grafana,您可以可视化集群中的 TCP 流量。

[转帖]一文看尽 JVM GC 调优

https://zhuanlan.zhihu.com/p/428731068 首先看一个著名的学习方法论 向橡皮鸭求助学会提问,提问也是一门艺术提问前,先投入自己的时间做好功课发生了什么事情问题的基本情况你投入的研究和发现能正确提出你的问题,你的问题差不多已经解决一半深入的思考你的问题,大多情况下,

[转帖]Linux文件分发脚本,只需一条命令将你的文件分发到各个服务器上

https://zhuanlan.zhihu.com/p/438457921 背景 在运维或在日常工作生活中,我们经常会把一个文件拷贝到其它服务器上,或同时分发到多个服务器上,甚至要求目标机将文件放在相同的路径下,方便程序进一步调用。 遇到这种问题,我们通常的做法是使用scp或rsync命令把文件拷

[转帖]性能调优要根据自己的情况逐渐调整,往往结合系统监控和性能压力测试一起进行。不可不调,不可乱调。

你可以在https://www.kernel.org/doc/html/latest/admin-guide/sysctl/index.html 官方去看也有文档 性能调优有两个原则:遵从由上之下,木板补板的原则。 性能调优遵循由上至下的原则。 业务逻辑->缓存服务器->调度器->网络容器->中间件