本篇文章通过一个案例,对systemtap、BPF/BCC、bpftrace三种不同类型的内核探测工具进行剖析和对比。这个程序就是简单对icmp_rcv函数,收到icmp报文,打印出对应的源IP地址和目的IP地址。

  1. 使用BPF/BCC
    1.1在centos8操作系统上安装对应的软件二进制包
1)    安装kernel-devel包;
2)    安装dnf -y install bcc-tools

1.2 源码包安装

dnf install -y bison cmake ethtool flex git iperf3 libstdc+±devel python3-netaddr python3-pip gcc gcc-c++ make zlib-devel elfutils-libelf-devel
dnf install -y clang clang-devel llvm llvm-devel llvm-static ncurses-devel
dnf -y install netperf
pip3 install pyroute2
ln -s /usr/bin/python3 /usr/bin/python
dnf -y install openssl
git clone https://github.com/iovisor/bcc.git
mkdir bcc_build
cmake …/bcc -DCMAKE_INSTALL_PREFIX=/usr -DENABLE_LLVM_SHARED=1
cd …/&& make -j10
make install
1.3 程序示例

使用bpf/bcc需要的内核版本最少是4.10以上。
使用下面的bcc代码,

#!/usr/bin/env python3.6

from __future__ import print_function
from bcc import BPF
from bcc.utils import printb

bpf_text = """
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <bcc/proto.h>
#include <uapi/linux/icmp.h>
#include <linux/icmp.h>
#include <uapi/linux/ip.h>
#include <linux/ip.h>


static inline struct iphdr *skb_to_iphdr(const struct sk_buff *skb)
{
    // unstable API. verify logic in ip_hdr() -> skb_network_header().
    return (struct iphdr *)(skb->head + skb->network_header);
}

int icmp_rcv_cb(struct pt_regs *ctx, struct sk_buff *skb)
{
            struct icmphdr *icmph ;
            struct iphdr *iph = skb_to_iphdr(skb);
            bpf_trace_printk("ipsrc:%pI4  ipdst:%pI4 \\n",&iph->saddr, &iph->daddr);
            icmph = (struct icmphdr *)skb->data;
            bpf_trace_printk("devname:%s ----- icmp_type:%d  \\n",skb->dev->name, icmph->type);
            return 0;
};
"""
# initialize BPF
b = BPF(text=bpf_text)
b.attach_kprobe(event="icmp_rcv", fn_name="icmp_rcv_cb")
#end format output
while 1:
    # Read messages from kernel pipe
    (task, pid, cpu, flags, ts, msg) = b.trace_fields()
    print("task:%s pid: %d %s " % (task, pid, msg))
#b.trace_print()

BPF9.png

  1. Systemtap
    2.1安装 systemtap

在centos8 上直接使用yum安装 yum install systemtap systemtap-runtime
2.2 Stap-prep

BPF10.png

通过在http://debuginfo.centos.org/8/x86_64/Packages/下载安装完debuginfo包后,执行stap-prep命令

BPF11.png

简单测试可以运行成功

BPF12.png

2.3 程序示例

下面是systemtap的方式对icmp_rcv函数的探测,对本机收到的ICMP报文打印出,对应的源IP和目的IP地址。

stap -g icmp_systemtap.stp
#!/usr/bin/stap -g
%{
#include <linux/kernel.h>
#include <linux/net.h>
#include <linux/skbuff.h>
#include <net/ip.h>
#include <linux/module.h>
#include <uapi/linux/if_packet.h>
#include <linux/fdtable.h>
#include <net/icmp.h>

        static inline void ip2str(char *to,unsigned int from)
        {
                int size = snprintf(to,16,"%pI4",&from);
                to[size] = '\0';
         }
%}
function get_icmp_packet_info:string(skb:long)
%{
        int ret = -1;
        struct sk_buff *skb = (struct sk_buff *)STAP_ARG_skb;
        struct iphdr *ip_header;
        unsigned int src_ip_1 = 0;
        unsigned int  dst_ip_1 = 0;
        char src_ip[16],dst_ip[16];
        struct icmphdr *icmph;

        if(!skb)
        {
                goto EXIT_F;
        }

        ip_header = (struct iphdr *)skb_network_header(skb);


        if(!ip_header)
        {
                goto EXIT_F;
        }

        src_ip_1 = (unsigned int)ip_header->saddr;
        dst_ip_1 = (unsigned int)ip_header->daddr;
        ip2str(src_ip,src_ip_1);
        ip2str(dst_ip,dst_ip_1);

        icmph = icmp_hdr(skb);
        if(icmph->type == 0)
        {

                goto ECHO_ICMP;
        }
        if(icmph->type == 8)
        {
                goto REPLY_ICMP;
        }

EXIT_F:
        snprintf(STAP_RETVALUE,MAXSTRINGLEN,"ERROR:src_ip:%s dst_ip:%s",src_ip,dst_ip);
ECHO_ICMP:
        snprintf(STAP_RETVALUE,MAXSTRINGLEN,"ECHO_ICMP:src_ip:%s dst_ip:%s",src_ip,dst_ip);
REPLY_ICMP:
        snprintf(STAP_RETVALUE,MAXSTRINGLEN,"REPLY_ICMP:src_ip:%s dst_ip:%s",src_ip,dst_ip);
%}

global locations

probe begin { printf("Monitoring for recv icmp packets\n") }
probe end { printf("Stropping monitoring  packets\n") }

probe kernel.function("icmp_rcv").return
{
        printf("%s\n",get_icmp_packet_info($skb))
        iphdr = __get_skb_iphdr($skb)
        saddr = format_ipaddr(__ip_skb_saddr(iphdr), @const("AF_INET"))
        daddr = format_ipaddr(__ip_skb_daddr(iphdr), @const("AF_INET"))
        printf("src_ip:%s  dst_ip:=%s\n",saddr,daddr);

}

probe timer.sec(5)
{
        exit ()
}

下面是运行后的测试结果:

BPF13.png

  1. bpftrace
    3.1 安装软件

yum -y install bpftrace
3.2 程序示例

bpftrace是使用自定义单行代码和简短脚本的临时工具的不错的选择,而BCC是复杂工具和守护程序的理想选择、bpftrace和BCC都是BPF的前端工具。

在这里插入代码片#!/usr/bin/bpftrace

#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/socket.h>

BEGIN
{
        printf("Tracing icmp rev.Hit  Ctrl-C end.\n");
}

kprobe:icmp_rcv
{
        $skb = (struct sk_buff *)arg0;

        $iph = (struct iphdr*)($skb->head + $skb->network_header);
        $src_ip = ntop(AF_INET,$iph->saddr);
        $dst_ip = ntop(AF_INET,$iph->daddr);

        printf("src_ip:%s  ----> dst_ip:%s\n",$src_ip,$dst_ip);
}

END
{
        printf("OVER  bye!!")
}

运行结果如下:

BPF14.png

4 总结

使用systemtap工具跟踪内核需要安装和内核对应版本的debuginfo包,systemtap作为老牌的内核跟踪工具,可以支持比较老的内核版本,对于现有存量的内核定位跟踪有明显的优势。
BPF/BCC作为新的内核跟踪工具,需要较新的内核版本,最少是4.10版本,最好是4.19版本的内核。
通过运行对比发现,编译和运行BPF/BCC的代码比systemtap的代码要快的多。
BPF有各类安全检查,避免在内核跟踪过程中产生panic,systemtap没有此类的安全检查,需要开发者在开发systemtap程序时,保证代码的安全性。
Bpftrace作为内核跟踪的一种工具,特别适合简单的内核跟踪,适合一条命令搞定的内核跟踪,bpftrace也有自己的一套语法体系可用。
各种不同类型的内核探测跟踪技术,适合不同类型的场景,在实际使用中可选择适合自己的方式。

参考文献:
https://lwn.net/Articles/852112/