美文网首页
从零开始的内核ebpf开发之旅

从零开始的内核ebpf开发之旅

作者: JackHCC | 来源:发表于2021-06-17 22:36 被阅读0次

    引言


    内核研究与开发是计算机底层处于与硬件打交道的部位,ebpf可以理解为是内核开发的一个模块。在研究ebpf开发之前需要对计算机的一些基础知识学习了解,懂得计算机的基本组成和操作系统的基本原理和运行机制,了解Linux内核设计的机制和相关源码的阅读与理解,再深入内核模块观察ebpf的设计思路,进而做到对ebpf的开发与实现。

    在此之前,首先需要储备一些基本的计算机知识。

    基础知识储备


    计算机组成原理

    学习计算机组成原理可以对计算机的基础架构有所理解,了解计算机中常见的术语和概念。

    计算机组成原理知识要点见:计算机组成原理学习笔记

    操作系统

    操作系统作为人和计算机交互的桥梁,理解其工作原理对后续内核开发有很好的帮助,对操作系统的术语了解知道其背后的道理是开发的基础。

    清华大学操作系统课程入口

    操作系统知识要点见:操作系统学习笔记

    C语言

    C语言是Linux内核开发主要使用的编程语言和开发工具,需要熟悉其基本语法和结构。

    C语言中文网入口

    C语言API入口

    Linux基础


    了解Linux基本组成和常用的shell命令,熟悉Linux的文件架构。

    FHS(Filesystem Hierarchy Standard):

    FHS依据文件系统使用的频繁与否与是否允许使用者随意更动, 而将目录定义成为四种交互作用的形态,用表格来说有点像底下这样:

    无法复制加载中的内容

    • 可分享的:可以分享给其他系统挂载使用的目录,所以包括执行文件与用户的邮件等数据, 是能够分享给网络上其他主机挂载用的目录;

    • 不可分享的:自己机器上面运作的装置文件或者是与程序有关的socket文件等, 由于仅与自身机器有关,所以当然就不适合分享给其他主机了.

    • 不变的:有些数据是不会经常变动的,跟随着distribution而不变动. 例如函式库、文件说明文件、系统管理员所管理的主机服务配置文件等等;

    • 可变动的:经常改变的数据,例如登录文件、一般用户可自行收受的新闻组等.

    事实上,FHS针对目录树架构仅定义出三层目录底下应该放置什么数据而已,分别是底下这三个目录的定义:

    1. / (root, 根目录):与开机系统有关;
    2. /usr (unix software resource):与软件安装/执行有关;
    3. /var (variable):与系统运作过程有关.

    根目录 (/) 的意义与内容:

    概要:

    1. 所有的目录都是由根目录衍生出来的(根目录是整个系统最重要的一个目录)

    2. 与开机/还原/系统修复等动作有关. (由于系统开机时需要特定的开机软件、核心文件、开机所需程序、 函式库等等文件数据,若系统出现错误时,根目录也必须要包含有能够修复文件系统的程序才行)

    3. FHS标准建议:根目录(/)所在分割槽应该越小越好, 且应用程序所安装的软件最好不要与根目录放在同一个分割槽内,保持根目录越小越好.(因为越大的分割槽妳会放入越多的数据,如此一来根目录所在分割槽就可能会有较多发生错误的机会,如此不但效能较佳,根目录所在的文件系统也较不容易发生问题.)

    根目录(/)底下目录FHS定义的说明:

    无法复制加载中的内容

    除上 FHS 中定义的目录说明外, 底下是几个在Linux当中非常重要的目录:

    无法复制加载中的内容

    不可与根目录分开的目录(与开机过程有关):

    根目录与开机有关,开机过程中仅有根目录会被挂载, 其他分割槽则是在开机完成之后才会持续的进行挂载的行为.就是因为如此,因此根目录下与开机过程有关的目录, 就不能够与根目录放到不同的分割槽去!

    • /etc:配置文件

    • /bin:重要执行档

    • /dev:所需要的装置文件

    • /lib:执行档所需的函式库与核心所需的模块

    • /sbin:重要的系统执行文件

    /usr 的意义与内容:

    概要:

    1. 依据FHS的基本定义,/usr里面放置的数据属于可分享的与不可变动的(shareable, static), 如果你知道如何透过网络进行分割槽的挂载,那么/usr确实可以分享给局域网络内的其他主机来使用!

    2. usr(Unix Software Resource 即Unix操作系统软件资源) FHS建议所有软件开发者,应该将他们的数据合理的分别放置到这个目录下的次目录,而不要自行建立该软件自己独立的目录.

    3. 所有系统默认的软件(distribution发布者提供的软件)都会放置到/usr底下,因此这个目录有点类似Windows 系统的『C:\Windows\ + C:\Program files\』这两个目录的综合体,系统刚安装完毕时,这个目录会占用最多的硬盘容量.

    一般来说,/usr的次目录建议有底下这些:

    无法复制加载中的内容

    /var 的意义与内容:

    概要:

    /var目录主要针对常态性变动的文件,包括缓存(cache)、登录档(log file)以及某些软件运作所产生的文件, 包括程序文件(lock file, run file),或者例如MySQL数据库的文件等等. 所以/var在系统运作后才会渐渐占用硬盘容量的目录

    常见的次目录有:

    无法复制加载中的内容

    针对FHS,各家distributions的异同:

    由于FHS仅是定义出最上层(/)及次层(/usr, /var)的目录内容应该要放置的文件或目录数据, 因此,在其他次目录层级内,就可以随开发者自行来配置了.举例来说,CentOS的网络设定数据放在 /etc/sysconfig/network-scripts/ 目录下,但是SuSE则是将网络放置在 /etc/sysconfig/network/ 目录下,目录名称可是不同的呢!不过只要记住大致的FHS标准,差异性其实有限啦!

    Linux 命令大全查询表

    无法复制加载中的内容

    Linux内核


    Linux内核学习策略

    Linux学习建议配套远古版本的Linux内核源码学习,有助于帮助理解内核设计的思路,下载并阅读Linux内核1.0版本的源码去学习,该版本基本包含了内核基本部件,后续的版本都是在此基础上扩充功能,但是基本的内在没有变化。

    内核源码不同版本间的阅读与对比可参考Bootlin,其中1.0源码目录结构如下,其中对主要文件目录进行解释:

    无法复制加载中的内容

    对照着内核设计的源代码进行学习,会从根源上思考这样设计的目的是什么。

    Linux内核开发环境配置

    内核开发环境和源码安装配置:Linux内核开发环境配置

    Linux内核简介

    Linux 内核的用途是什么?

    Linux 内核有 4 项工作:

    1. 内存管理:追踪记录有多少内存存储了什么以及存储在哪里
    2. 进程管理:确定哪些进程可以使用中央处理器(CPU)、何时使用以及持续多长时间
    3. 设备驱动程序:充当硬件与进程之间的调解程序/解释程序
    4. 系统调用和安全防护:从流程接受服务请求

    在正确实施的情况下,内核对于用户是不可见的,它在自己的小世界(称为内核空间)中工作,并从中分配内存和跟踪所有内容的存储位置。用户所看到的内容(例如 Web 浏览器和文件)则被称为用户空间。这些应用通过系统调用接口(SCI)与内核进行交互。

    举例来说,内核就像是一个为高管(硬件)服务的忙碌的个人助理。助理的工作就是将员工和公众(用户)的消息和请求(进程)转交给高管,记住存放的内容和位置(内存),并确定在任何特定的时间谁可以拜访高管、会面时间有多长。

    Linux内核学习路线和框架图

    image

    Linux Security Coaching

    image

    Linux内核基础学习资料

    Linux内核与系统驱动保护入口

    该视频资料详细介绍了Linux内核的知识点及其在内核中的实现进行比对,很有参考价值。

    MakeFile详解


    Makefile 可以简单的认为是一个工程文件的编译规则,描述了整个工程的编译和链接等规则。详细介绍如下:

    MakeFile详解

    Makefile文件负责编写程序的编译与运行规则,免去命令行使用Clang去逐步编译分析。

    GDB详解


    GDB是一个强大的调试工具,通过它可以实现C程序代码bug的调试。

    GDB详解

    Linux崩溃调试


    Linux内核Crash下的问题解决方案:

    Linux调试之崩溃

    EBPF基础


    什么是ebpf?

    Linux 内核一直是实现监控/可观测性、网络和安全功能的理想地方。 不过很多情况下这并非易事,因为这些工作需要修改内核源码或加载内核模块, 最终实现形式是在已有的层层抽象之上叠加新的抽象。 eBPF 是一项革命性技术,它能在内核中运行沙箱程序(sandbox programs), 而无需修改内核源码或者加载内核模块。

    image

    eBPF 催生了一种全新的软件开发方式。基于这种方式,我们不仅能对内核行为进行 编程,甚至还能编写跨多个子系统的处理逻辑,而传统上这些子系统是完全独立、 无法用一套逻辑来处理的。

    安全:

    观测和理解所有的系统调用的能力,以及在 packet 层和 socket 层审视所有的网络操作的能力, 这两者相结合,为系统安全提供了革命性的新方法。 以前,系统调用过滤、网络层过滤和进程上下文跟踪是在完全独立的系统中完成的; eBPF 的出现统一了可观测性和各层面的控制能力,使我们有更加丰富的上下文和更精细的控制能力, 因而能创建更加安全的系统。

    网络:

    eBPF 的两大特色 —— 可编程和高性能 —— 使它能满足所有的网络包处理需求。 可编程意味着无需离开内核中的包处理上下文,就能添加额外的协议解析器或任何转发逻辑, 以满足不断变化的需求。高性能的 JIT 编译器使 eBPF 程序能达到几乎与原生编译的内核态代码一样的执行性能。

    跟踪 & 性能分析:

    eBPF 程序能够加载到 trace points、内核及用户空间应用程序中的 probe points, 这种能力使我们对应用程序的运行时行为(runtime behavior)和系统本身 (system itself)提供了史无前例的可观测性。应用端和系统端的这种观测能力相结合, 能在排查系统性能问题时提供强大的能力和独特的信息。BPF 使用了很多高级数据结构, 因此能非常高效地导出有意义的可观测数据,而不是像很多同类系统一样导出海量的原始采样数据。

    观测 & 监控:

    相比于操作系统提供的静态计数器(counters、gauges),eBPF 能在内核中收集和聚合自定义 metric, 并能从不同数据源来生成可观测数据。这既扩展了可观测性的深度,也显著减少了整体系统开销, 因为现在可以选择只收集需要的数据,并且后者是直方图或类似的格式,而非原始采样数据。

    Linux驱动模块开发

    简介

    Linux 内核的整体结构已经非常庞大,而其包含的组件也非常多。这会导致两个问题,一是生成的内核会很大,二是如果我们要在现有的内核中新增或删除功能,将不得不重新编译内核。Linux 提供了这样的一种机制,这种机制被称为模块(Module)。使得编译出的内核本身并不需要包含所有功能,而在这些功能需要被使用的时候,其对应的代码被动态地加载到内核中。

    举例

    先来看一个最简单的内核模块“Hello World”,代码如下:

    #include <linux/init.h>
    #include <linux/module.h>
    
    static int hello_init(void) /初始化函数/
    {
     printk(KERN_INFO " Hello World enter\n");
     return 0;
    }
    
    static void hello_exit(void) /卸载函数/
    {
     printk(KERN_INFO " Hello World exit\n ");
    }
    
    module_init(hello_init); /模块初始化/
    module_exit(hello_exit); /卸载模块/
    
    MODULE_LICENSE("Dual BSD/GPL"); /许可声明/
    
    MODULE_AUTHOR("Linux");
    MODULE_DESCRIPTION("A simple Hello World Module");
    MODULE_ALIAS("a simplest module");
    
    

    这个模块定义了两个函数, 一个在模块加载到内核时被调用( hello_init )以及一个在模块被去除时被调用( hello_exit ). moudle_init 和 module_exit 这几行使用了特别的内核宏来指出这两个函数的角色. 另一个特别的宏 (MODULE_LICENSE) 是用来告知内核, 该模块带有一个自由的许可证.

    注:内核模块中用于输出的函数是内核空间的 printk()而非用户空间的 printf(),具体用法参考附件 printk函数介绍。

    几个常用命令

    加载模块

    通过“insmod ./hello.ko”命令可以加载,加载时输出“Hello World enter”。

    卸载模块

    通过“rmmod hello”命令可以卸载,卸载时输出“Hello World exit”。

    查看系统中已经加载的模块列表

    在Linux中,使用lsmod命令可以获得系统中加载了的所有模块以及模块间的依赖关系,例如:

    root@imx6:~$ lsmod
    Module      Size    Used by
    hello      1568    0 
    ohci1394     32716   0 
    ide_scsi     16708   0 
    ide_cd      39392   0 
    cdrom      36960   1 ide_cd
    
    
    查看某个具体模块的详细信息

    使用modinfo <模块名>命令可以获得模块的信息,包括模块作者、模块的说明、模块所支持 的参数以及 vermagic:

    root@imx6:~$ modinfo hello.ko
    filename:   hello.ko
    license:   Dual BSD/GPL
    author:    Song Baohua
    description: A simple Hello World Module
    alias:    a simplest module
    vermagic:   2.6.15.5 686 gcc-3.2
    depends: 
    
    

    Linux 内核模块程序的结构

    一个Linux内核模块主要由如下几个部分组成:

    1、模块加载函数(一般需要) 当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。

    2、模块卸载函数(一般需要) 当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功能。

    3、模块许可证声明(必须) 许可证(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到内核被污染 (kernel tainted)的警告。在Linux 2.6内核中,可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”。大多数情况下,内核模块应遵循GPL兼容许可权。Linux 2.6内核模块最常见的是以MODULE_LICENSE( “Dual BSD/GPL” )语句声明模块采BSD/GPL双LICENSE。

    4、模块参数(可选) 模块参数是模块被加载的时候可以被传递给它的值,它本身对应模块内部的全局变量。

    5、模块导出符号(可选) 内核模块可以导出符号(symbol,对应于函数或变量),这样其它模块可以使用本模块中的变量或函数。

    6、模块作者等信息声明(可选) 用于申明模块作者的相关信息,一般用于备注作者姓名、邮箱等。

    模块加载函数

    Linux 内核模块加载函数一般以_ _init 标识声明,典型的模块加载函数如下:

    static int _ _init initialization_function(void)
    {
    /* 初始化代码 */
    }
    module_init(initialization_function);
    
    

    模块加载函数必须以“module_init(函数名)”的形式被指定。它返回整型值,若初始化成功,应返回 0。而在初始化失败时,应该返回错误编码。在 Linux 内核里,错误编码是一个负值。

    在 Linux 2.6 内核中,可以使用 request_module(const char *fmt, …)函数加载内核模块,驱动开发人员可以通过调用。

    request_module(module_name);
    /**** 或者 ****/
    request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev));
    
    

    注意:在 Linux 中,所有标识为_ init 的函数在连接的时候都放在.init.text 这个区段内,此外,所有的 init 函数在区段.initcall.init 中还保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些 _init 函数,并在初始化完成后,释放 init 区段(包括.init.text、.initcall.init 等)。

    模块卸载函数
    static void _ _exit cleanup_function(void)
    {
    /* 释放代码 */
    }
    module_exit(cleanup_function);
    
    

    模块卸载函数在模块卸载的时候执行,不返回任何值,必须以“module_exit(函数名)”的形式来指定。通常来说,模块卸载函数要完成与模块加载函数相反的功能,如下所示。

    若模块加载函数注册了 XXX,则模块卸载函数应该注销 XXX。

    若模块加载函数动态申请了内存,则模块卸载函数应释放该内存。

    若模块加载函数申请了硬件资源(中断、DMA 通道、I/O 端口和 I/O 内存等)的占用,则模块卸载函数应释放这些硬件资源。

    若模块加载函数开启了硬件,则卸载函数中一般要关闭之。

    模块参数

    用“module_param(参数名,参数类型,参数读/写权限)”为模块定义一个参数,例如下列代码定义了 1 个整型参数和 1 个字符指针参数:

    static char *book_name = " dissecting Linux Device Driver ";
    static int num = 4 000;
    module_param(num, int, S_IRUGO);
    module_param(book_name, charp, S_IRUGO);
    
    

    参数类型可以是 byte、short、ushort、int、uint、long、ulong、charp(字符指针)、bool 或 invbool(布尔的反),在模块被编译时会将 module_param 中声明的类型与变量定义的类型进行比较,判断是否一致。

    在装载内核模块时,用户可以向模块传递参数,形式为“insmode(或 modprobe)模块名 参数名=参数值”,如果不传递,参数将使用模块内定义的缺省值。

    内核模块的符号导出

    模块可以使用如下宏导出符号到内核符号表:

    EXPORT_SYMBOL(符号名);
    EXPORT_SYMBOL_GPL(符号名);
    
    

    导出的符号将可以被其他模块使用,使用前声明一下即可。EXPORT_SYMBOL_GPL()只适用于包含 GPL 许可权的模块。

    模块声明与描述

    在Linux内核模块中,我们可以用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分别声明模块的作者、描述、版本、设备表和别名,例如:

    MODULE_AUTHOR(author);
    MODULE_DESCRIPTION(description);
    MODULE_VERSION(version_string);
    MODULE_DEVICE_TABLE(table_info);
    MODULE_ALIAS(alternate_name);
    
    

    对于USB、PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE。

    MakeFile

    Kernel modules
    obj-m += hello.o
    
    
    Specify flags for the module compilation.
    #EXTRA_CFLAGS=-g -O0
    
    build: kernel_modules
    kernel_modules:
    make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules #modules表示编译成模块的意思
    #CURDIR是make的内嵌变量,自动设置为当前目录
    
    clean:
    make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
    该 Makefile 文件应该与源代码 hello.c 位于同一目录,开启其中的 EXTRA_CFLAGS=-g -O0可以得到包含调试信息的 hello.ko 模块。运行 make 命令得到的模块可直接在 PC 上运行。
    
    

    注:uname 的更多用法详见附件

    如果一个模块包括多个.c 文件(如 file1.c、file2.c),则应该以如下方式编写 Makefile:

    obj-m := modulename.o
    modulename-objs := file1.o file2.o
    
    

    obj-m是个makefile变量,它的值可以是一串.o文件的表列

    EBPF详解

    ebpf详细学习笔记和记录:

    EBPF(Berkeley Packet Filter)学习记录

    在这里不赘述ebpf的历史等没有太多学习意义的信息,主要从实际开发角度需要去展开必要介绍。

    Seccross项目理解


    开发记录及相关源码分析记录:

    SECCROSS项目解读

    开发记录

    相关文章

      网友评论

          本文标题:从零开始的内核ebpf开发之旅

          本文链接:https://www.haomeiwen.com/subject/eglqyltx.html