Skip to content

Latest commit

 

History

History
1733 lines (1261 loc) · 107 KB

File metadata and controls

1733 lines (1261 loc) · 107 KB

五、编写你的第一个内核模块——LKMs 第二部分

本章是我们关于可加载内核模块 ( LKM )框架以及如何使用它编写内核模块的后半部分。为了最大限度地利用它,我希望你先完成前一章,并在处理这一章之前在那里尝试代码和问题。

在这一章中,我们从上一章中中断的地方继续。在这里,我们将介绍如何为 LKMs 使用一个“更好”的 Makefile,如何为 ARM 平台交叉编译一个内核模块(作为一个典型的例子),什么是模块堆叠以及如何进行,以及如何设置和使用模块参数。在此过程中,除了其他几件事,您将了解内核 API/ABI 稳定性(或者说,缺乏稳定性!),编写用户空间和内核代码之间的主要区别,在系统启动时自动加载内核模块,以及安全问题和如何解决这些问题。我们以关于内核文档(包括编码风格)的信息结束,并为主线做贡献。

简而言之,我们将在本章中讨论以下主题:

  • 内核模块的“更好”的 Makefile 模板
  • 交叉编译内核模块
  • 收集最少的系统信息
  • 授权内核模块
  • 模拟内核模块的“类库”特性
  • 将参数传递给内核模块
  • 内核中不允许浮点运算
  • 系统启动时自动加载模块
  • 内核模块和安全性-概述
  • 内核开发人员的编码风格指南
  • 为主线内核做贡献

技术要求

本章的技术要求–所需的软件包–与第 4 章编写您的第一个内核模块–LKMs 第 1 部分中的技术要求部分所示的内容相同;请参考它。和往常一样,您可以在本书的 GitHub 存储库中找到本章的源代码。用以下内容克隆它:

git clone https://github.com/PacktPublishing/Linux-Kernel-Programming

书中显示的代码通常只是一个相关的片段。从存储库中获取完整的源代码。对于本章(以及随后的章节),有关技术要求的更多信息可在下一节中找到。

内核模块的“更好”的 Makefile 模板

前一章向您介绍了用于从源代码生成内核模块的 Makefile,以安装和清理它。然而,正如我们在那里简要提到的,我现在将介绍在我看来什么是上级,什么是“更好的”Makefile,并解释它是如何变得更好的。

最终,我们都必须编写更好、更安全的代码——包括用户和内核空间。好消息是,有几个工具有助于提高代码的健壮性和安全性,静态和动态分析器就是其中之一(正如在第 1 章、*内核工作区设置、*中已经提到的几个工具,我在此不再赘述)。

我已经为内核模块设计了一个简单但有用的 Makefile“模板”,它包括几个帮助您运行这些工具的目标。这些目标允许您非常容易地执行有价值的检查和分析;*你可能会忘记、忽略或永远搁置的东西!*这些目标包括以下内容:

  • “通常的”目标——目标buildinstallclean

  • 内核编码风格生成和检查(分别通过indent(1)和内核的checkpatch.pl脚本)。

  • 内核静态分析目标(sparsegccflawfinder,提到球菌

  • 两个“虚拟”内核动态分析目标(KASANLOCKDEP / CONFIG_PROVE_LOCKING),鼓励您为所有测试用例配置、构建和使用“调试”内核。

  • 一个简单的tarxz-pkg目标是将源文件定位并压缩到前面的目录中。这使您能够将压缩的tar-xz文件传输到任何其他 Linux 系统,并在那里提取和构建 LKM。

  • 一个“虚拟”的动态分析目标,指出你应该如何投入时间来配置和构建一个“调试”内核,并使用它来捕捉 bug!(接下来会有更多相关内容。)

您可以在ch5/lkm_template目录中找到代码(还有一个README文件)。为了帮助您了解它的用途和功能,并帮助您入门,下图简单显示了代码在其help目标下运行时产生的输出截图:

Figure 5.1 – The output of the helptarget from our "better" Makefile

图 5.1 中,我们先做make,然后按 Tab 键两次,让它显示所有可用的目标。一定要仔细研究并使用它!例如,运行make sa会导致它在你的代码上运行它所有的静态分析 ( sa)目标!

还需要注意的是,使用这个 Makefile 将需要您在系统上安装一些包/应用;这些包括(对于基础 Ubuntu 系统)indent(1)linux-headers-$(uname -r)sparse(1)flawfinder(1)cppcheck(1)tar(1)。(第 1 章内核工作空间设置,已经指定应该安装这些。)

另外,注意 Makefile 中提到的所谓动态分析 ( da)目标仅仅是虚拟目标,除了打印消息之外什么也不做。它们在那里 提醒你通过在适当配置的“调试”内核上运行来彻底测试你的代码!

说到“调试”内核,下一节将向您展示如何配置一个。

配置“调试”内核

(有关配置和构建内核的详细信息,请参考第 2 章从源代码构建 5.x Linux 内核-第 1 部分第 3 章从源代码构建 5.x Linux 内核-第 2 部分)。

调试内核上运行您的代码可以帮助您发现难以发现的错误和问题。我强烈建议这样做,特别是在开发和测试期间!在这里,我最低限度地期望您配置您的定制 5.4 内核,以打开以下内核调试配置选项(在make menuconfig用户界面中,您将在Kernel Hacking子菜单下找到大多数选项;以下列表与 Linux 5.4.0 相关):

  • CONFIG_DEBUG_INFO

  • CONFIG_DEBUG_FS(伪文件系统debugfs

  • CONFIG_MAGIC_SYSRQ(魔法系统热键功能)

  • CONFIG_DEBUG_KERNEL

  • CONFIG_DEBUG_MISC

  • 内存调试:

    • CONFIG_SLUB_DEBUG
    • CONFIG_DEBUG_MEMORY_INIT
    • CONFIG_KASAN:这里是内核地址杀毒软件端口;然而,截至本文撰写之时,它仅适用于 64 位系统。
  • CONFIG_DEBUG_SHIRQ

  • CONFIG_SCHED_STACK_END_CHECK

  • 锁定调试:

  • CONFIG_PROVE_LOCKING:非常强大的lockdep功能,捕捉锁定 bug!这也打开了其他几个锁调试配置,在第 13 章内核同步-第 2 部分中进行了解释。

  • CONFIG_LOCK_STAT

  • CONFIG_DEBUG_ATOMIC_SLEEP

  • CONFIG_STACKTRACE

  • CONFIG_DEBUG_BUGVERBOSE

  • CONFIG_FTRACE ( ftrace:在其子菜单中,至少打开几个“追踪器”)

  • CONFIG_BUG_ON_DATA_CORRUPTION

  • CONFIG_KGDB(仁 GDB;可选)

  • CONFIG_UBSAN

  • CONFIG_EARLY_PRINTK

  • CONFIG_DEBUG_BOOT_PARAMS

  • CONFIG_UNWINDER_FRAME_POINTER(选择FRAME_POINTERCONFIG_STACK_VALIDATION

A couple of things to note: a) Don't worry too much right now if you don't get what all the previously mentioned kernel debug config options do; by the time you're done with this book, most of them will be clear. b) Turning on some Ftrace tracers (or plugins), such as CONFIG_IRQSOFF_TRACER, would be useful as we actually make use of it in our Linux Kernel Programming (Part 2) book in the Handling Hardware Interrupts chapter; (note that though Ftrace itself may be enabled by default, all its tracers aren't).

请注意,打开这些配置选项确实会导致性能下降,但这没关系。我们运行这种“调试”内核的明确目的是捕捉错误和 bug*(尤其是难以发现的那种!).它确实可以拯救生命!在您的项目中,您的工作流程应该包括您的代码在以下两个上进行测试和运行:*

** 调试内核系统,其中所有必需的内核调试配置选项都已打开(如前所述)

  • 生产内核系统(其中所有或大部分前面的内核调试选项将被关闭)

不用说,我们将在本书的所有后续 LKM 代码中使用前面的 Makefile 风格。

好了,现在你都准备好了,让我们进入一个有趣而实用的场景——为另一个目标(通常是 ARM)编译你的内核模块。

交叉编译内核模块

第 3 章从源代码构建 5.x Linux 内核-第 2 部分中,在树莓皮的内核构建部分,中,我们展示了如何为“外来”目标架构(如 ARM、PowerPC、MIPS 等)交叉编译 Linux 内核。本质上,对于内核模块也可以这样做;通过适当设置“特殊”ARCHCROSS_COMPILE环境变量,可以轻松地交叉编译内核模块。

例如,让我们假设我们正在开发一个嵌入式 Linux 产品;我们的代码将在其上运行的目标设备有一个 AArch32 (ARM-32) CPU。为什么不举一个实际的例子。让我们为树莓 Pi 3 单板计算机 ( SBC )交叉编译我们的你好,世界内核模块!

这很有趣。你会发现,虽然它看起来简单明了,但我们最终要经历四次迭代才能成功。为什么呢?请继续阅读了解详情。

建立交叉编译系统

交叉编译内核模块的先决条件非常清楚:

  • 我们需要将目标系统的内核源树作为工作空间的一部分安装在我们的主机系统上,通常是 x86_64 桌面(对于我们的示例,使用树莓 Pi 作为目标,请参考这里的官方树莓 Pi 文档:https://www . raspberrpi . org/documents/Linux/kernel/building . MD)。
  • 我们现在需要一个交叉工具链。通常,主机系统是 x86_64,这里,由于目标是 ARM-32,我们将需要一个 x86_64 到 ARM32 的交叉工具链。同样,正如第 3 章从源代码构建 5.x Linux 内核-第 2 部分为树莓 Pi 构建内核中明确提到的,您必须下载并安装树莓 Pi 专用的 x86_64 到 ARM 工具链,作为主机系统工作空间的一部分(请参考第 3 章从源代码构建 5.x Linux 内核-第 2 部分,了解如何安装工具链)。

好的,从这一点开始,我将假设您安装了 x86_64 到 ARM 的交叉工具链。我也会假设工具链前缀arm-linux-gnueabihf-;我们可以通过尝试调用gcc交叉编译器来快速检查工具链是否已安装,其二进制文件是否已添加到路径中:

$ arm-linux-gnueabihf-gcc
arm-linux-gnueabihf-gcc: fatal error: no input files
compilation terminated.
$ 

它起作用了——只是我们没有传递任何 C 程序作为编译的参数,因此它会抱怨。

You can certainly look up the compiler version as well with the arm-linux-gnueabihf-gcc --version command.

尝试 1–设置“特殊”环境变量

实际上,交叉编译内核模块是非常容易的(或者我们这样认为!).只需确保适当设置“特殊”ARCHCROSS_COMPILE环境变量。遵循以下步骤:

  1. 让我们为树莓 Pi 目标重新构建我们的第一个 Hello,world 内核模块。下面是如何构建它:

To do so without corrupting the original code, we make a new folder called cross with a copy of the (helloworld_lkm) code from Chapter 4, Writing your First Kernel Module - LKMs Part 1, to begin with.

cd <dest-dir>/ch5/cross

这里,<dest-dir>是本书 GitHub 源树的根。

  1. 现在,运行以下命令:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-

但是它不起作用(或者它可能起作用;请立即查看以下信息框)。我们会遇到编译失败,如下所示:

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make -C /lib/modules/5.4.0-llkd01/build/ M=/home/llkd/book_llkd/Linux-Kernel-Programming/ch5/cross modules
make[1]: Entering directory '/home/llkd/kernels/linux-5.4'
  CC [M]  /home/llkd/book_llkd/Linux-Kernel-Programming/ch5/cross/helloworld_lkm.o
arm-linux-gnueabihf-gcc: error: unrecognized command line option ‘-fstack-protector-strong’
scripts/Makefile.build:265: recipe for target '/home/llkd/book_llkd/Linux-Kernel-Programming/ch5/cross/helloworld_lkm.o' failed
[...]
make: *** [all] Error 2
$ 

为什么会失败?

Assuming all tools are set up as per the technical requirements discussed earlier, the cross-compile should work. This is because the Makefile provided in the book's repository is a proper working one, the Raspberry Pi kernel has been correctly configured and built, the device is booted off this kernel, and the kernel module is compiled against it. The purpose here, in this book, is to explain the details; thus, we begin with no assumptions, and guide you through the process of correctly performing the cross-compilation.

为什么前面的交叉编译尝试失败的线索在于,它试图使用–来构建当前主机系统的内核源*,而不是目标的内核源树。因此,我们需要修改 Makefile,使其指向目标的正确内核源树。这样做真的很容易。在下面的代码中,我们看到了(已更正的)Makefile 代码的典型编写方式:*

# ch5/cross/Makefile:
# To support cross-compiling for kernel modules:
# For architecture (cpu) 'arch', invoke make as:
# make ARCH=<arch> CROSS_COMPILE=<cross-compiler-prefix> 
ifeq ($(ARCH),arm)
  # *UPDATE* 'KDIR' below to point to the ARM Linux kernel source tree on 
  # your box
  KDIR ?= ~/rpi_work/kernel_rpi/linux
else ifeq ($(ARCH),arm64)
  # *UPDATE* 'KDIR' below to point to the ARM64 (Aarch64) Linux kernel 
  # source tree on your box
  KDIR ?= ~/kernel/linux-4.14
else ifeq ($(ARCH),powerpc)
  # *UPDATE* 'KDIR' below to point to the PPC64 Linux kernel source tree  
  # on your box
  KDIR ?= ~/kernel/linux-4.9.1
else
  # 'KDIR' is the Linux 'kernel headers' package on your host system; this 
  # is usually an x86_64, but could be anything, really (f.e. building 
  # directly on a Raspberry Pi implies that it's the host)
  KDIR ?= /lib/modules/$(shell uname -r)/build
endif

PWD          := $(shell pwd)
obj-m        += helloworld_lkm.o
EXTRA_CFLAGS += -DDEBUG

all:
    @echo
    @echo '--- Building : KDIR=${KDIR} ARCH=${ARCH} CROSS_COMPILE=${CROSS_COMPILE} EXTRA_CFLAGS=${EXTRA_CFLAGS} ---'
    @echo
    make -C $(KDIR) M=$(PWD) modules
[...]

仔细查看(新的和“更好的”,如前一节所述)Makefile,您会看到它是如何工作的:

  • 最重要的是,我们有条件地设置KDIR变量指向正确的内核源树,这取决于ARCH环境变量的值(当然,我已经使用了 ARM[64]和 PowerPC 的内核源树的一些路径名作为例子;请用内核源代码树的实际路径替换路径名)
  • 像往常一样,我们设置obj-m += <module-name>.o
  • 我们还设置CFLAGS_EXTRA来添加DEBUG符号(以便在我们的 LKM 甚至pr_debug()/pr_devel()宏工作中定义DEBUG符号)。
  • @echo '<...>'线相当于炮弹的echo命令;它只是在构建时发出一些有用的信息(前缀@隐藏了 echo 语句本身不显示)。
  • 最后,我们有“通常”的 Makefile 目标:allinstallclean–这些与早期的相同,除了这个重要的变化:我们使其将目录(通过-C开关)更改为KDIR的值!
  • 虽然在前面的代码中没有显示,但是这个“更好的”Makefile 有几个额外的有用目标。您肯定应该花时间去探索和使用它们(如前一节所述;开始时,只需输入make help,研究输出并尝试)。

完成所有这些之后,让我们用这个版本重试交叉编译,看看它是如何进行的。

尝试 2–将 Makefile 指向目标的正确内核源代码树

所以现在,有了上一节描述的增强的 Makefile,它应该可以工作了。在我们的新目录中,我们将尝试这一点–cross(因为我们在交叉编译,而不是我们生气!)–遵循以下步骤:

  1. 使用适合交叉编译的make命令尝试构建(第二次):
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- 
--- Building : KDIR=~/rpi_work/kernel_rpi/linux ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- EXTRA_CFLAGS=-DDEBUG ---

make -C ~/rpi_work/kernel_rpi/linux M=/home/llkd/booksrc/ch5/cross modules
make[1]: Entering directory '/home/llkd/rpi_work/kernel_rpi/linux'

ERROR: Kernel configuration is invalid.
 include/generated/autoconf.h or include/config/auto.conf are missing.
 Run 'make oldconfig && make prepare' on kernel src to fix it.

 WARNING: Symbol version dump ./Module.symvers
 is missing; modules will have no dependencies and modversions.
[...]
make: *** [all] Error 2
$ 

它失败的真正原因是我们编译内核模块所针对的树莓皮内核仍然处于“原始”状态。它的根目录中甚至没有.config文件(前面的输出告诉我们,还有其他必需的头),它需要(至少)对其进行配置。

  1. 要解决此问题,请切换到树莓皮内核源树的根,并按照以下步骤操作:
$ cd ~/rpi-work/kernel_rpi/linux $ make ARCH=arm bcmrpi_defconfig
#
# configuration written to .config
#
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- oldconfig
scripts/kconfig/conf --oldconfig Kconfig
#
# configuration written to .config
#
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- prepare
scripts/kconfig/conf --silentoldconfig Kconfig
 CHK include/config/kernel.release
 UPD include/config/kernel.release
 WRAP arch/arm/include/generated/asm/bitsperlong.h
 WRAP arch/arm/include/generated/asm/clkdev.h
 [...]
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
 CHK include/config/kernel.release
 CHK include/generated/uapi/linux/version.h
 CHK include/generated/utsrelease.h
 [...]
 HOSTCC scripts/recordmcount
 HOSTCC scripts/sortextable
 [...]
$

请注意,这些步骤实际上相当于执行树莓皮内核的部分构建!事实上,如果您已经构建(交叉编译)了这个内核,正如前面在第 3 章中从源代码构建 5.x Linux 内核-第 2 部分中所解释的,那么内核模块交叉编译应该只工作,没有这里看到的中间步骤。

尝试 3–交叉编译我们的内核模块

现在我们已经配置了树莓 Pi 内核源树(在主机系统上)和增强的 Makefile(参见尝试 2–将 Makefile 指向目标的正确内核源树部分),它应该会工作。让我们重试:

  1. 我们(再次)尝试构建(交叉编译)内核。发出make命令,照常传递ARCHCROSS_COMPILE环境变量:
$ ls -l
total 12
-rw-rw-r-- 1 llkd llkd 1456 Mar 18 17:48 helloworld_lkm.c
-rw-rw-r-- 1 llkd llkd 6470 Jul  6 17:30 Makefile
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- --- Building : KDIR=~/rpi_work/kernel_rpi/linux ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- EXTRA_CFLAGS=-DDEBUG ---

make -C ~/rpi_work/kernel_rpi/linux M=/home/llkd/booksrc/ch5/cross modules
make[1]: Entering directory '/home/llkd/rpi_work/kernel_rpi/linux' 
 WARNING: Symbol version dump ./Module.symvers
 is missing; modules will have no dependencies and modversions.

Building for: ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- EXTRA_CFLAGS= -DDEBUG
 CC [M] /home/llkd/book_llkd/Linux-Kernel-Programming/ch5/cross/helloworld_lkm.o
​  Building modules, stage 2.
  MODPOST 1 modules
  CC /home/llkd/booksrc/ch5/cross/helloworld_lkm.mod.o
  LD [M] /home/llkd/booksrc/ch5/cross/helloworld_lkm.ko
make[1]: Leaving directory '/home/llkd/rpi_work/kernel_rpi/linux'
$ file ./helloworld_lkm.ko 
./helloworld_lkm.ko: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), BuildID[sha1]=17...e, not stripped
$

构建成功!helloworld_lkm.ko内核模块确实已经针对 ARM 架构进行了交叉编译(使用树莓 Pi 交叉工具链和内核源代码树)。

We can ignore the preceding warning regarding the Module.symvers file for now. It isn't present as (here) the entire Raspberry Pi kernel hasn't been built.

Also, FYI, on recent hosts running GCC 9.x or later and kernel versions 4.9 or later, there are some compiler attribute warnings emitted. When I tried cross-compiling this kernel module using arm-linux-gnueabihf-gcc version 9.3.0 and the Raspberry Pi kernel version 4.14.114, warnings such as this were emitted:

./include/linux/module.h:131:6: warning: ‘init_module’ specifies less restrictive attribute than its target ‘helloworld_lkm_init’: ‘cold’ [-Wmissing-attributes]

Miguel Ojeda points this out (https://lore.kernel.org/lkml/CANiq72=T8nH3HHkYvWF+vPMscgwXki1Ugiq6C9PhVHJUHAwDYw@mail.gmail.com/) and has even generated a patch to handle this issue (https://github.com/ojeda/linux/commits/compiler-attributes-backport). As of the time of writing, the patch is applied in the kernel mainline and in recent Raspberry Pi kernels (so, the rpi-5.4.y branch works fine but earlier ones such as the rpi-4.9.y branch don't seem to have it)! Hence the compiler warnings... effectively, if you do see these warnings, update the Raspberry Pi branch to rpi-5.4.y or later (or, for now, just ignore them).

  1. 布丁好不好的证据在于吃。因此,我们在交叉编译的内核模块对象文件中启动我们的树莓 Pi,scp(1),如下所示(在树莓 Pi 上的ssh(1)会话中),尝试一下(以下输出直接来自设备):
$ sudo insmod ./helloworld_lkm.ko insmod: ERROR: could not insert module ./helloworld_lkm.ko: Invalid module format $ 

显然,前面代码中的insmod(8)失败了!了解原因很重要。

这实际上与我们试图加载模块的内核版本中的不匹配以及模块编译所针对的内核版本有关。

  1. 登录树莓 Pi 时,打印出我们正在运行的当前树莓 Pi 内核版本,并使用modinfo(8)实用程序打印出内核模块本身的详细信息:
rpi ~ $ cat /proc/version 
Linux version 4.19.75-v7+ (dom@buildbot) (gcc version 4.9.3 (crosstool-NG crosstool-ng-1.22.0-88-g8460611)) #1270 SMP Tue Sep 24 18:45:11 BST 2019
rpi ~ $ modinfo ./helloworld_lkm.ko 
filename: /home/pi/./helloworld_lkm.ko
version: 0.1
license: Dual MIT/GPL
description: LLKD book:ch5/cross: hello, world, our first Raspberry Pi LKM
author: Kaiwan N Billimoria
srcversion: 7DDCE78A55CF6EDEEE783FF
depends: 
name: helloworld_lkm
vermagic: 5.4.51-v7+ SMP mod_unload modversions ARMv7 p2v8 
rpi ~ $ 

从前面的输出中,很明显,我们在树莓皮上运行4.19.75-v7+内核。事实上,这是我在设备的 microSD 卡上安装默认 Raspbian OS 时继承的内核(这里介绍的是一个经过深思熟虑的场景,最初不是使用的是我们之前为树莓 Pi 构建的 5.4 内核)。另一方面,内核模块显示它是针对5.4.51-v7+ Linux 内核编译的(来自modinfo(8)vermagic字符串显示了这一点)。*很明显,有错配。*嗯,那又怎样?

Linux 内核有一个规则,内核的一部分 应用二进制接口 ( ABI ): 只有在内核模块已经针对其构建的情况下,它才会将该内核模块插入内核内存——精确的内核版本、构建标志,甚至内核配置选项都很重要!

The built against kernel is the kernel whose source location you specified in the Makefile (we did so via the KDIR variable previously).

换句话说,内核模块与内核不二进制兼容,除了它们针对 构建的内核。例如,如果我们在一个 Ubuntu 18.04 LTS 盒子上构建一个内核模块,那么它将只在运行这个精确环境(库、内核或工具链)的系统上工作!它不会在软呢帽 29 或 RHEL 7.x、树莓皮等上面工作。现在——再一次,想想这个——这并不意味着内核模块是完全不兼容的。不,它们是跨不同架构的源代码兼容的(至少它们可以或者应该是这样写的)。所以,假设你有源代码,你总是可以在给定的系统上重建一个内核模块,然后它会在那个系统上工作。只是二进制图像(文件.ko)与内核不兼容,除了它所针对的精确内核。

别紧张,这个问题其实很容易发现。查找内核日志:

$ dmesg |tail -n2 [ 296.130074] helloworld_lkm: no symbol version for module_layout
[ 296.130093] helloworld_lkm: version magic '5.4.51-v7+ mod_unload modversions ARMv6 p2v8 ' should be '4.19.75-v7+ SMP mod_unload modversions ARMv7 p2v8 ' $ 

在设备上,当前运行的内核是这样的:4.19.75-v7+。内核实际上告诉我们,我们的内核模块是根据5.4.51-v7+内核版本构建的(它还显示了一些预期的内核配置)以及它应该是什么。有错配!因此无法插入内核模块。

虽然我们在这里不使用这种方法,但是有一种方法可以通过名为 DKMS ( 动态内核模块支持 ) *的框架来确保成功构建和部署第三方树外内核模块(只要它们的源代码可用)。*以下是直接引自它的话:

动态内核模块支持(DKMS)是一个能够生成 Linux 内核模块 的程序/框架,其源代码通常位于内核源代码树之外。其概念是让 DKMS 模块 在安装新内核时自动重建。

作为 DKMS 用法的一个例子,Oracle VirtualBox 虚拟机管理程序(在 Linux 主机上运行时)使用 DKMS 来自动构建和更新其内核模块。

尝试 4–交叉编译我们的内核模块

因此,现在我们理解了这个问题,有两种可能的解决方案:

  • 我们必须为产品使用所需的定制配置内核,并根据它构建我们所有的内核模块。
  • 或者,我们可以重建内核模块,以匹配设备运行的当前内核。

现在,在典型的嵌入式 Linux 项目中,您几乎肯定会有一个为目标设备定制配置的内核,您必须使用它。该产品的所有内核模块都将/必须根据它来构建。因此,我们遵循第一种方法–我们必须使用定制配置和构建的(5.4!)内核,由于我们的内核模块是针对它构建的,所以它现在应该可以工作了。

We (briefly) covered the kernel build for the Raspberry Pi in Chapter 3, Building the 5.x Linux Kernel from Source - Part 2. Refer back there for the details if required.

好的,我将不得不假设您已经遵循了步骤(在第 3 章中介绍了从源代码构建 5.x Linux 内核-第 2 部分)并且已经为树莓 Pi 配置和构建了 5.4 内核。关于如何将我们的自定义zImage复制到设备的 microSD 卡等的细节在此不做介绍。我在这里向您推荐正式的树莓 Pi 文档:https://www . raspberrypi . org/documents/Linux/kernel/building . MD

然而,我们将指出一种在设备上内核之间切换的方便方法(这里,我假设设备是运行 32 位内核的树莓皮 3B+):

  1. 将您定制的zImage内核二进制文件复制到设备的 microSD 卡的/boot分区中。将原始树莓皮核图像保存为kernel7.img.orig

  2. 将刚刚交叉编译的内核模块(ARM 的“T1”,在上一节中完成)从您的主机系统复制到 microSD 卡上(通常是复制到“T2”)。

  3. 接下来,再次在设备的 microSD 卡上,编辑/boot/config.txt文件,设置内核通过kernel=xxx线启动。设备上该文件的一个片段显示了这一点:

rpi $ cat /boot/config.txt
[...]
# KNB: enable the UART (for the adapter cable: USB To RS232 TTL UART 
# PL2303HX Converter USB to COM)
enable_uart=1
# KNB: select the kernel to boot from via kernel=xxx
#kernel=kernel7.img.orig
kernel=zImage
rpi $ 
  1. 保存并重新启动后,我们登录到设备并重试我们的内核模块。图 5.2 是在树莓 Pi 设备上使用的刚刚交叉编译的helloworld_lkm.ko LKM 的截图:

Figure 5.2 – The cross-compiled LKM being used on a Raspberry Pi

啊,成功了!请注意,这一次,当前内核版本(5.4.51-v7+)与构建模块的内核版本精确匹配——在modinfo(8)输出中,我们可以看到vermagic字符串显示它是5.4.51-v7+

If you do see an issue with rmmod(8) throwing a non-fatal error (though the cleanup hook is still called), the reason is that you haven't yet fully set up the newly built kernel on the device. You will have to copy in all the kernel modules (under /lib/modules/<kernel-ver>) and run the depmod(8) utility there. Here, we will not delve further into these details – as mentioned before, the official documentation for the Raspberry Pi covers all these steps.

Of course, the Raspberry Pi is a pretty powerful system; you can install the (default) Raspbian OS along with development tools and kernel headers and thus compile kernel modules on the board itself! (No cross-compile required.) Here, though, we have followed the cross-compile approach as this is typical when working on embedded Linux projects.

LKM 框架是一项相当大的工作。还有很多事情有待探索。我们开始吧。在下一节中,我们将研究如何从内核模块中获取一些最少的系统信息。

收集最少的系统信息

在我们上一节(ch5/cross/helloworld_lkm.c)的简单演示中,我们硬编码了一个printk()来发出一个"Hello/Goodbye, Raspberry Pi world\n"字符串,不管内核模块是否真的运行在树莓 Pi 设备上。为了更好地“检测”一些系统细节(如中央处理器或操作系统),我们建议您参考我们的示例ch5/min_sysinfo/min_sysinfo.c内核模块。在下面的代码片段中,我们只显示了相关的函数:

// ch5/min_sysinfo/min_sysinfo.c
[ ... ]
void llkd_sysinfo(void)
{
    char msg[128];

    memset(msg, 0, strlen(msg));
    snprintf(msg, 47, "%s(): minimal Platform Info:\nCPU: ", __func__);

    /* Strictly speaking, all this #if... is considered ugly and should be
     * isolated as far as is possible */
#ifdef CONFIG_X86
#if(BITS_PER_LONG == 32)
    strncat(msg, "x86-32, ", 9);
#else
    strncat(msg, "x86_64, ", 9);
#endif
#endif
#ifdef CONFIG_ARM
    strncat(msg, "ARM-32, ", 9);
#endif
#ifdef CONFIG_ARM64
    strncat(msg, "Aarch64, ", 10);
#endif
#ifdef CONFIG_MIPS
    strncat(msg, "MIPS, ", 7);
#endif
#ifdef CONFIG_PPC
    strncat(msg, "PowerPC, ", 10);
#endif
#ifdef CONFIG_S390
    strncat(msg, "IBM S390, ", 11);
#endif

#ifdef __BIG_ENDIAN
    strncat(msg, "big-endian; ", 13);
#else
    strncat(msg, "little-endian; ", 16);
#endif

#if(BITS_PER_LONG == 32)
    strncat(msg, "32-bit OS.\n", 12);
#elif(BITS_PER_LONG == 64)
    strncat(msg, "64-bit OS.\n", 12);
#endif
    pr_info("%s", msg);

  show_sizeof();
 /* Word ranges: min & max: defines are in include/linux/limits.h */
 [ ... ]
}
EXPORT_SYMBOL(lkdc_sysinfo);

(这个 LKM 向您展示的其他细节,如各种原始数据类型加上单词范围的大小,在这里没有显示;请务必参考我们 GitHub 存储库中的源代码,并亲自尝试一下。)前面的内核模块代码很有启发性,因为它有助于演示如何编写可移植代码。请记住,内核模块本身是一个二进制不可移植的对象文件,但是它的源代码可以(也许,应该,取决于您的项目)以这样的方式编写,以便它可以跨各种体系结构移植。然后,在目标架构上(或为目标架构)进行简单的构建,就可以进行部署了。

For now, please ignore the EXPORT_SYMBOL() macro used here. We will cover its usage shortly.

在我们现在熟悉的 x86_64 Ubuntu 18.04 LTS 客户机上构建和运行它,我们得到了以下输出:

$ cd ch5/min_sysinfo
$ make
[...]
$ sudo insmod ./min_sysinfo.ko 
$ dmesg
[...]
[29626.257341] min_sysinfo: inserted
[29626.257352] llkd_sysinfo(): minimal Platform Info:
              CPU: x86_64, little-endian; 64-bit OS.
$ 

太好了。类似地(如前所述),我们可以为 ARM-32(树莓 Pi)交叉编译这个内核模块,然后将交叉编译的内核模块转移(scp(1))到我们的树莓 Pi 目标并在那里运行(以下输出来自运行 32 位树莓 Pi 操作系统的树莓 Pi 3B+):

$ sudo insmod ./min_sysinfo.ko
$ dmesg
[...]
[    80.428363] min_sysinfo: inserted
[    80.428370] llkd_sysinfo(): minimal Platform Info:
               CPU: ARM-32, little-endian; 32-bit OS.
$

事实上,这揭示了一些有趣的事情;树莓皮 3B+有一个原生的 64 位中央处理器,但是默认情况下(在撰写本文时)运行一个 32 位操作系统,因此有前面的输出。我们将让您在树莓皮(或其他)设备上安装 64 位 Linux 操作系统,并重新运行该内核模块。

The powerful Yocto Project (https://www.yoctoproject.org/) is one (industry-standard) way to generate a 64-bit OS for the Raspberry Pi. Alternatively (and much easier to quickly try), Ubuntu provides a custom Ubuntu 64-bit kernel and root filesystem for the device (https://wiki.ubuntu.com/ARM/RaspberryPi).

更加注重安全性

当然,安全是目前的一个关键问题。专业开发人员应该编写安全的代码。近年来,已经有许多针对 Linux 内核的已知攻击(更多信息请参见进一步阅读部分)。与此同时,许多提高 Linux 内核安全性的努力也在进行中。

在我们前面的内核模块(ch5/min_sysinfo/min_sysinfo.c)中,要警惕使用老式的例程(比如sprintfstrlen等等;是的,它们存在于内核中)!静态分析器可以极大地帮助捕捉潜在的安全相关和其他错误;我们强烈建议您使用它们。第 1 章内核 工作空间设置中,提到了几个对内核有用的静态分析工具。在下面的代码中,我们使用“更好的”Makefile 中的一个sa目标来运行一个相对简单的静态分析器:flawfinder(1)(由大卫·惠勒编写):

$ make [tab][tab] all        clean      help       install     sa_cppcheck    sa_gcc    
tarxz-pkg  checkpatch code-style indent      sa             sa_flawfinder sa_sparse $ make sa_flawfinder 
make clean
make[1]: Entering directory '/home/llkd/llkd_book/Linux-Kernel-Programming/ch5/min_sysinfo'

--- cleaning ---

[...]

--- static analysis with flawfinder ---

flawfinder *.c
Flawfinder version 1.31, (C) 2001-2014 David A. Wheeler.
Number of rules (primarily dangerous function names) in C/C++ ruleset: 169
Examining min_sysinfo.c

FINAL RESULTS:

min_sysinfo.c:60: [2] (buffer) char:
  Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119:CWE-120). Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length.

[...]

min_sysinfo.c:138: [1] (buffer) strlen:
  Does not handle strings that are not \0-terminated; if given one it may
  perform an over-read (it could cause a crash if unprotected) (CWE-126).
[...]

仔细看flawfinder(1)发出的关于strlen()功能的警告(在它产生的众多警告中!).这确实是我们在这里面临的情况!请记住,未初始化的局部变量(如我们的msg缓冲区)在声明时具有随机内容。因此,strlen()函数可能会也可能不会产生我们期望的值。

The output of flawfinder even mentions the CWE number (here, CWE-126) of the generalized class of security issue that is being seen here; (do google it and you will see the details. In this instance, CWE-126 represents the buffer over-read issue: https://cwe.mitre.org/data/definitions/126.html).

同样,我们避免使用strncat()并用strlcat()功能代替。因此,考虑到安全问题,我们将llkd_sysinfo()函数的代码重写为llkd_sysinfo2()

我们还添加了几行代码来显示平台上无符号和有符号变量的范围(最小值,最大值)(基数为 10 和 16)。我们让你通读。作为一个简单的任务,在你的 Linux 盒子上运行这个内核模块并验证输出。

现在,让我们继续讨论一下关于 Linux 内核和内核模块代码的许可。

授权内核模块

众所周知,Linux 内核代码库本身是在 GNU GPL v2(又名 GPL-2.0; GPL 代表通用公共许可证,就大多数人而言,仍将如此。如前所述,在第 4 章编写您的第一个内核模块–LKMs 第 1 部分中,许可您的内核代码是必需且重要的。本质上,讨论的内容,至少对于我们的目的来说,归结为:如果你的意图是直接使用内核代码和/或将你的代码上游贡献到主线内核中(下面是一些注释),你必须在与发布 Linux 内核相同的许可下发布代码:GNU GPU-2.0。对于一个内核模块来说,情况仍然有点“不稳定”。无论如何,要让内核社区参与进来并得到他们的帮助(这是一个巨大的优势),你应该,或者被期望在 GNU GPU-2.0 许可下发布代码(尽管双重许可当然是可能的,也是可以接受的)。

使用MODULE_LICENSE()宏指定许可证。以下评论转载自include/linux/module.h内核头,清楚地显示了什么许可“标识”是可接受的(注意双重许可)。显然,内核社区强烈建议在 GPL-2.0 (GPL v2)和/或其他版本下发布您的内核模块,例如 BSD/MIT/MPL。如果你打算向内核主线上游贡献代码,不言而喻,仅 GPL-2.0就是发布许可:

// include/linux/module.h
[...]
/*
 * The following license idents are currently accepted as indicating free
 * software modules
 *
 * "GPL"                       [GNU Public License v2 or later]
 * "GPL v2"                    [GNU Public License v2]
 * "GPL and additional rights" [GNU Public License v2 rights and more]
 * "Dual BSD/GPL"              [GNU Public License v2
 *                              or BSD license choice]
 * "Dual MIT/GPL"              [GNU Public License v2
 *                              or MIT license choice]
 * "Dual MPL/GPL"              [GNU Public License v2
 *                              or Mozilla license choice]
 *
 * The following other idents are available
 *
 * "Proprietary" [Non free products]
 *
 * There are dual licensed components, but when running with Linux it is the GPL that is relevant so this is a non issue. Similarly LGPL linked with GPL is a GPL combined work.
 *
 * This exists for several reasons
 * 1\. So modinfo can show license info for users wanting to vet their setup is free
 * 2\. So the community can ignore bug reports including proprietary modules
 * 3\. So vendors can do likewise based on their own policies
 */
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)
[...]

仅供参考,内核源代码树有一个LICENSES/目录,在这个目录下你可以找到关于许可证的详细信息;这个文件夹上的快速ls显示其中的子文件夹:

$ ls <...>/linux-5.4/LICENSES/
deprecated/ dual/ exceptions/ preferred/

我们将让您看一看,这样,关于许可的讨论就到此为止了;现实是,这是一个需要法律知识的复杂话题。建议您咨询公司内部的专业法律人员(律师)(或雇佣他们),以便为您的产品或服务找到合适的法律角度。

在这个话题上,为了保持一致,最近的内核有一个规则:每个单独的源文件的第一行必须是一个 SPDX 许可证标识符(详情见https://spdx.org/)。当然,脚本需要第一行来指定解释器。此外,GPL 许可证常见问题的一些答案也在此处给出:https://www.gnu.org/licenses/gpl-faq.html

更多关于许可模式,不滥用MODULE_LICENSE宏,特别是多许可/双许可模式,可以在本章进一步阅读部分提供的链接中找到。现在,让我们回到技术上来。下一节将解释如何在内核空间中有效地模拟类似库的特性。

模拟内核模块的“类库”特性

用户模式和内核模式编程的一个主要区别是后者中完全没有熟悉的“库”概念。库本质上是 API 的集合或归档,方便开发者满足重要目标,典型的有:不要重新发明轮子、软件复用、模块化等等。但是在 Linux 内核中,库是不存在的。

不过,好消息是,从广义上讲,有两种技术可以在内核空间中为我们的内核模块实现“类似库”的功能:

  • 第一种技术:显式地“链接”多个源文件——包括“库”代码——到你的内核模块对象。
  • 第二种叫做模块堆叠。

当我们更详细地讨论这些技术时,请务必继续阅读。一个搅局者,也许,但是马上知道是有用的:前面的技术中的第一个通常优于第二个。话说回来,这确实取决于项目。一定要阅读下一节的细节;我们边走边列出一些利弊。

通过多个源文件执行库仿真

到目前为止,我们已经处理了只有一个 C 源文件的非常简单的内核模块。一个内核模块有不止一个 C 源文件的(相当典型的)现实情况如何?所有的源文件都必须被编译,然后作为一个单一的.ko二进制对象链接在一起。

例如,假设我们正在构建一个名为projx的内核模块项目。它由三个 C 源文件组成:prj1.c, prj2.cprj3.c。我们希望最终的内核模块被称为projx.ko。Makefile 是您指定这些关系的地方,如图所示:

obj-m      := projx.o
projx-objs := prj1.o prj2.o prj3.o

在前面的代码中,注意projx标签是如何在obj-m指令之后用作下一行 -objs指令的前缀的。当然,你可以使用任何标签。我们前面的例子将让内核构建系统将三个单独的 C 源文件编译成单独的对象(.o)文件,然后将将它们链接在一起,形成最终的二进制内核模块对象文件, projx.ko,正如我们所期望的那样。

我们可以利用这种机制在我们的书的源代码树中构建一个小的例程“库”(这个“内核库”的源文件在源代码树的根中:klib_llkd.hklib_llkd.c)。这个想法是,其他内核模块可以通过链接到它们来使用这里的功能!例如,在即将到来的第 7 章 *【内存管理内部构件-要点】*中,我们让我们的ch7/lowlevel_mem/lowlevel_mem.c内核模块代码调用驻留在我们的库代码../../klib_llkd.c中的函数。“链接到”我们所谓的“库”代码是通过将以下内容放入lowlevel_mem内核模块的 Makefile 来实现的:

obj-m                 += lowlevel_mem_lib.o
lowlevel_mem_lib-objs := lowlevel_mem.o ../../klib_llkd.o

第二行指定要构建的源文件(到目标文件中);它们是lowlevel_mem.c内核模块的代码和../../klib_llkd库代码。然后,它将和lowlevel_mem_lib.ko连接成一个二进制内核模块,实现了我们的目标。(为什么不做本章末尾问题部分规定的作业 5.1。)

理解内核模块中的函数和变量范围

在深入研究之前,快速地重新审视一些基础知识是个好主意。用 C 语言编程时,您应该了解以下内容:

  • 在函数中局部声明的变量显然是它的局部变量,并且只在该函数中有作用域。
  • 前缀为static限定符的变量和函数只有在当前“单位”内才有作用域;实际上,他们声明的文件。这很好,因为它有助于减少名称空间污染。静态(和全局)数据变量在该函数中保留它们的值。

在 2.6 Linux(即<= 2.4.x, ancient history now), kernel module static and global variables, as well as all functions, were automatically visible throughout the kernel. This was, in retrospect, obviously not a great idea. The decision was reversed from 2.5 (and thus 2.6 onward, modern Linux): 之前,所有内核模块变量(静态和全局数据)和函数在默认情况下只限于它们的内核模块私有,因此在它之外是不可见的。所以,如果两个内核模块lkmAlkmB有一个全局名为maya,那么它对它们每个都是唯一的;没有冲突。

为了改变范围,LKM 框架提供了EXPORT_SYMBOL()宏。使用它,您可以声明一个数据项或函数在范围上是全局的,实际上,对所有其他内核模块以及内核核心都是可见的。

我们举一个简单的例子。我们有一个名为prj_core的内核模块,它包含一个全局和一个函数:

static int my_glob = 5;
static long my_foo(int key)
{ [...]
}

虽然两者都可以在这个内核模块中使用,但是在它之外看不到。这是故意的。为了使它们在这个内核模块之外可见,我们可以导出它们:

int my_glob = 5;
EXPORT_SYMBOL(my_glob);

long my_foo(int key)
{ [...]
}
EXPORT_SYMBOL(my_foo);

现在,两者都有这个内核模块之外的范围(注意,在前面的代码块中,static关键字是如何被故意移除的)。其他内核模块(以及核心内核)现在可以“看到”并使用它们。准确地说,这一想法通过两种广泛的方式得到利用:

  • 首先,内核导出一个精心设计的全局变量和函数的子集,这些变量和函数构成了内核功能的一部分,也是其他子系统的一部分。现在,这些全局变量和函数是可见的,因此可以从内核模块中使用!我们将很快看到一些示例用途。

  • 第二,内核模块作者(通常是设备驱动程序)使用这个概念来导出特定的数据和/或功能,这样其他的内核模块,在更高的抽象层次上,也许可以利用这个设计并使用这个数据和/或功能——这个概念被称为模块堆叠,我们将很快通过一个例子来深入研究它。

例如,对于第一个用例,设备驱动程序作者可能想要处理来自外围设备的硬件中断。一种常见的方法是通过request_irq() API,事实上,它只不过是这个 API 的一个精简(内联)包装器:

// kernel/irq/manage.c
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
                         irq_handler_t thread_fn, unsigned long irqflags,
                         const char *devname, void *dev_id)
{
    struct irqaction *action;
[...]
    return retval;
}
EXPORT_SYMBOL(request_threaded_irq);

正是因为request_threaded_irq()函数是导出的,所以它可以从设备驱动程序内部调用,而设备驱动程序通常被写成内核模块。类似地,开发人员经常需要一些“方便”的例程——例如,字符串处理例程。在lib/string.c中,Linux 内核提供了几个常见字符串处理函数的实现(您期望出现):str[n]casecmpstr[n|l|s]cpystr[n|l]catstr[n]cmpstrchr[nul]str[n|r]chrstr[n]len等等。当然,这些都是通过EXPORT_SYMBOL()宏导出的*,以使它们可见,从而可供模块作者使用。*

*Here, we used the str[n|l|s]cpy notation to imply that the kernel provides the four functions: strcpy, strncpy, strlcpy, and strscpy.

另一方面,让我们来看一下内核的(微小的)一点 CFS ( 完全公平调度器)在内核内核深处调度代码。这里pick_next_task_fair()函数是调度代码在我们需要找到另一个任务进行上下文切换时调用的函数:

// kernel/sched/fair.c
static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
        struct cfs_rq *cfs_rq = &rq->cfs;
[...]
        if (new_tasks > 0)
                goto again;
        return NULL;
}

我们并不是真的想在这里研究调度(第 10 章CPU 调度器-第 1 部分第 11 章CPU 调度器-第 2 部分,小心点)这里的重点是:由于前面的函数是而不是标记了EXPORT_SYMBOL()宏,所以它永远不能被内核模块调用。它对核心内核保持私有

您也可以将数据结构标记为使用同一宏导出。此外,很明显,只有全局范围的数据(而不是局部变量)可以标记为导出。

If you want to see how the EXPORT_SYMBOL() macro works, please refer to the Further reading section of this chapter, which links to the book's GitHub repository.

回想一下我们关于内核模块许可的简短讨论。Linux 内核有一个,我们可以说,有趣的,命题:还有一个宏叫做EXPORT_SYMBOL_GPL()。就像它的表亲EXPORT_SYMBOL()宏一样,除了,是的,导出的数据项或函数只对那些在其MODULE_LICENSE()宏中包含GPL一词的内核模块可见!啊,内核社区的甜蜜复仇。它确实被用在内核代码库中的几个地方。(我将把它作为一个练习留给您,让您在代码中查找这个宏的出现;在 5.4.0 内核上,通过cscope(1)快速搜索发现了“仅仅”14000 多个使用实例!)

To view all exported symbols, navigate to the root of your kernel source tree and issue the make export_report command. Note though that this works only upon a kernel tree that has been configured and built.

现在让我们看看实现类似于库的内核特性的另一种关键方法:模块堆叠。

了解模块堆叠

这里的第二个重要想法——模块堆叠——是我们现在要深入研究的。

模块堆叠是一个概念,它在一定程度上为内核模块作者提供了“类似库”的特性。在这里,我们通常以这样一种方式设计我们的项目或产品设计,即我们有一个或多个“核心”内核模块,其工作是充当一个种类库。它将包括数据结构和功能(函数/应用编程接口),这些数据结构和功能将被导出到其他内核模块(上一节讨论了符号的导出)。

为了更好地理解这一点,让我们看几个真实的例子。首先,在我的主机系统(Ubuntu 18.04.3 LTS 本地 Linux 系统)上,我通过 Oracle VirtualBox 6.1 虚拟机管理程序应用*运行了一个来宾虚拟机。*好的,在过滤字符串vbox的同时,在主机系统上执行快速lsmod(8)会显示以下内容:

$ lsmod | grep vbox
vboxnetadp             28672  0
vboxnetflt             28672  1
vboxdrv               479232  3 vboxnetadp,vboxnetflt
$ 

回想一下我们之前的讨论,第三列是使用计数。第一行是0,但第三行的值是3。不仅如此,vboxdrv内核模块的右边还列出了两个内核模块(在使用计数栏之后)。如果任何内核模块出现在第三列之后,则表示依赖关系;这样看:右边显示的内核模块依赖于左边的内核模块。

因此,在前面的例子中,vboxnetadpvboxnetflt内核模块依赖于vboxdrv内核模块。靠它用什么方式?他们使用vboxdrv核心内核模块内的数据结构和/或函数(API),当然!一般来说,出现在第三列右边的内核模块意味着它们正在使用左边内核模块的一个或多个数据结构和/或函数(导致使用计数增加;这个用法计数是引用计数器的一个很好的例子(这里,它实际上是一个 32 位原子变量)*,*这是我们在上一章深入研究的东西)。实际上,vboxdrv内核模块类似于一个“库”(在有限的意义上,除了提供模块化功能之外,没有与用户模式库相关的通常的用户空间内涵)。你可以看到,在这个快照中,它的使用次数是3并且依赖它的内核模块堆叠在它上面——字面上!(可以在lsmod(1)输出的前两行看到。)此外,请注意vboxnetflt内核模块有一个正的使用计数(1),但是在其右侧没有内核模块出现;这仍然意味着某个东西正在使用它,通常是一个进程或线程。

FYI, the Oracle VirtualBox kernel modules we see in this example are actually the implementation of the VirtualBox Guest Additions. They are essentially a para-virtualization construct, helping to accelerate the working of the guest VM. Oracle VirtualBox provides similar functionality for Windows and macOS hosts as well (as do all the major virtualization vendors).

模块堆叠的另一个例子,就像承诺的那样:运行强大的LTTng(Linux Tracing Toolkit 下一代)框架使您能够执行详细的系统分析。LTTng 项目安装并使用了相当多的内核模块(通常为 40 个或更多)。这些内核模块中有几个是“堆叠的”,允许项目精确地利用我们在这里讨论的“类似库”的特性。

在下图中(已经在 Ubuntu 18.04.4 LTS 系统上安装了 LTTng),查看与其内核模块相关的lsmod | grep --color=auto "^lttng"输出的部分截图:

Figure 5.3 – Heavy module stacking within the LTTng product

可以看到,lttng_tracer内核模块右侧有 35 个内核模块,表示它们“堆叠”在上面,使用它提供的功能(类似地,lttng_lib_ring_buffer内核模块有 23 个内核模块“依赖”它)。

这里有一些快速的脚本魔法来查看所有使用计数为非零的内核模块(它们经常——但不总是——在它们的右侧显示一些相关的内核模块):

lsmod | awk '$3 > 0 {print $0}'

模块堆叠的一个含义:只有当内核模块的使用次数为0时,才能成功rmmod(8);也就是说,它没有被使用。因此,对于前面的第一个示例,我们只能在移除堆叠在其上的两个相关内核模块之后移除vboxdrv内核模块(从而使使用计数下降到0)。

尝试模块堆叠

让我们为模块堆叠设计一个非常简单的概念验证代码。为此,我们将构建两个内核模块:

  • 第一种我们称之为core_lkm;它的工作是充当某种“库”,为内核和其他模块提供一些函数(API)。
  • 我们的第二个内核模块user_lkm,是‘库’的‘用户’(或消费者);它将简单地调用驻留在第一个。

为此,我们的一对内核模块需要执行以下操作:

  • 核心内核模块必须使用EXPORT_SYMBOL()宏将一些数据和功能标记为导出
  • 用户内核模块必须通过 C extern关键字声明它期望在外部使用的数据和/或功能(记住,导出数据或功能只是建立适当的链接;编译器仍然需要知道被调用的数据和/或函数)。
  • 对于最近的工具链,允许将导出的功能和数据项标记为static。不过,结果是一个警告;我们不会对导出的符号使用static关键字。
  • 编辑自定义 Makefile 来构建两个内核模块。

代码如下;首先,核心或库内核模块。为了(希望)让这变得更有趣,我们将把前面模块的一个函数的代码复制到这个内核模块中,并导出它,从而使它对我们的第二个“用户”LKM 可见,他将调用这个函数:

Here, we do not show the full code; you can refer to the book's GitHub repo for it.

// ch5/modstacking/core_lkm.c
#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__
#include <linux/init.h>
#include <linux/module.h>

#define MODNAME     "core_lkm"
#define THE_ONE     0xfedface
MODULE_LICENSE("Dual MIT/GPL");

int exp_int = 200;
EXPORT_SYMBOL_GPL(exp_int);

/* Functions to be called from other LKMs */
void llkd_sysinfo2(void)
{
[...]
}
EXPORT_SYMBOL(llkd_sysinfo2);

#if(BITS_PER_LONG == 32)
u32 get_skey(int p)
#else // 64-bit
u64 get_skey(int p)
#endif
{
#if(BITS_PER_LONG == 32)
    u32 secret = 0x567def;
#else // 64-bit
    u64 secret = 0x123abc567def;
#endif
    if (p == THE_ONE)
        return secret;
    return 0;
}
EXPORT_SYMBOL(get_skey);
[...]

接下来是user_lkm内核模块,一个“堆叠”在core_lkm内核模块之上的模块:

// ch5/modstacking/user_lkm.c
#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__
#define MODNAME "user_lkm"

#if 1
MODULE_LICENSE("Dual MIT/GPL");
#else
MODULE_LICENSE("MIT");
#endif

extern void llkd_sysinfo2(void);
extern long get_skey(int);
extern int exp_int;

/* Call some functions within the 'core' module */
static int __init user_lkm_init(void)
{
#define THE_ONE 0xfedface
     pr_info("%s: inserted\n", MODNAME);
     u64 sk = get_skey(THE_ONE);
     pr_debug("%s: Called get_skey(), ret = 0x%llx = %llu\n",
             MODNAME, sk, sk);
     pr_debug("%s: exp_int = %d\n", MODNAME, exp_int);
 llkd_sysinfo2();
     return 0;
}

static void __exit user_lkm_exit(void)
{
    pr_info("%s: bids you adieu\n", MODNAME);
}
module_init(user_lkm_init);
module_exit(user_lkm_exit);

Makefile 与我们早期的内核模块基本相同,只是这次我们需要构建两个内核模块对象,如下所示:

obj-m     := core_lkm.o
obj-m     += user_lkm.o

好吧,让我们试试:

  1. 首先,构建内核模块:
$ make

--- Building : KDIR=/lib/modules/5.4.0-llkd02-kasan/build ARCH= CROSS_COMPILE= EXTRA_CFLAGS=-DDEBUG ---

make -C /lib/modules/5.4.0-llkd02-kasan/build M=/home/llkd/booksrc/ch5/modstacking modules
make[1]: Entering directory '/home/llkd/kernels/linux-5.4'
  CC [M] /home/llkd/booksrc/ch5/modstacking/core_lkm.o
  CC [M] /home/llkd/booksrc/ch5/modstacking/user_lkm.o
  [...]
  Building modules, stage 2.
  MODPOST 2 modules
  CC [M] /home/llkd/booksrc/ch5/modstacking/core_lkm.mod.o
  LD [M] /home/llkd/booksrc/ch5/modstacking/core_lkm.ko
  CC [M] /home/llkd/booksrc/ch5/modstacking/user_lkm.mod.o
  LD [M] /home/llkd/booksrc/ch5/modstacking/user_lkm.ko
make[1]: Leaving directory '/home/llkd/kernels/linux-5.4'
$ ls *.ko
core_lkm.ko  user_lkm.ko
$ 

Note that we're building our kernel modules against our custom 5.4.0 kernel. Do notice its full version is 5.4.0-llkd02-kasan; this is deliberate. This is the "debug kernel" that I have built and am using as a test-bed!

  1. 现在,让我们执行一系列快速测试来演示模块堆叠概念验证。先做:先尝试插入user_lkm 内核模块,再插入core_lkm 模块。

这将失败——为什么?您将意识到user_lkm内核模块所依赖的导出功能(和数据)在内核中不可用。更严格地说,符号不会位于内核的符号表中,因为还没有插入包含它们的core_lkm 内核模块:

$ sudo dmesg -C
$ sudo insmod ./user_lkm.ko 
insmod: ERROR: could not insert module ./user_lkm.ko: Unknown symbol in module
$ dmesg 
[13204.476455] user_lkm: Unknown symbol exp_int (err -2)
[13204.476493] user_lkm: Unknown symbol get_skey (err -2)
[13204.476531] user_lkm: Unknown symbol llkd_sysinfo2 (err -2)
$ 

不出所料,由于所需的(要导出的)符号不可用,insmod(8)失败(您在内核日志中看到的确切错误消息可能会因内核版本和调试配置选项集而略有不同)。

  1. 现在,让我们做对:
$ sudo insmod ./core_lkm.ko 
$ dmesg 
[...]
[19221.183494] core_lkm: inserted
$ sudo insmod ./user_lkm.ko 
$ dmesg 
[...]
[19221.183494] core_lkm:core_lkm_init(): inserted
[19242.669208] core_lkm:core_lkm_init(): /home/llkd/book_llkd/Linux-Kernel-Programming/ch5/modstacking/core_lkm.c:get_skey():100: I've been called
[19242.669212] user_lkm:user_lkm_init(): inserted
[19242.669217] user_lkm:user_lkm:user_lkm_init(): Called get_skey(), ret = 0x123abc567def = 20043477188079
[19242.669219] user_lkm:user_lkm_init(): exp_int = 200
[19242.669223] core_lkm:llkd_sysinfo2(): minimal Platform Info:
 CPU: x86_64, little-endian; 64-bit OS.
$ 
  1. 果然管用!使用lsmod(8)查看模块列表:
$ lsmod | egrep "core_lkm|user_lkm"
user_lkm               20480  0
core_lkm               16384  1 user_lkm
$ 

注意,对于core_lkm内核模块,使用计数列已经增加到了1 我们现在可以看到user_lkm内核模块依赖于core_lkm内核模块。回想一下lsmod输出的最右列中显示的内核模块依赖于最左列中的内核模块。

  1. 现在,让我们移除内核模块。移除内核模块也有一个排序依赖(就像插入一样)。试图移除core_lkm 首先会失败,因为很明显,内核内存中还有另一个模块依赖于它的代码/数据;换句话说,它仍然在使用:
$ sudo rmmod core_lkm 
rmmod: ERROR: Module core_lkm is in use by: user_lkm
$ 

Note that if the modules are installed onto the system, then you could use the modprobe -r <modules...> command to remove all related modules; we cover this topic in the Auto-loading modules on system boot section.

  1. 前面的rmmod(8)失败消息不言自明。所以,让我们做对:
$ sudo rmmod user_lkm core_lkm 
$ dmesg 
[...]
 CPU: x86_64, little-endian; 64-bit OS.
[19489.717265] user_lkm:user_lkm_exit(): bids you adieu
[19489.732018] core_lkm:core_lkm_exit(): bids you adieu
$ 

好了,完成了!

你会注意到在user_lkm 内核模块的代码中,我们发布它的许可证是在一个有条件的#if语句中:

#if 1
MODULE_LICENSE("Dual MIT/GPL");
#else
MODULE_LICENSE("MIT");
#endif

我们可以看到它是在双 MIT/GPL 许可下发布的(默认);那又怎样?想想看:在core_lkm 内核模块的代码中,我们有以下内容:

int exp_int = 200;
EXPORT_SYMBOL_GPL(exp_int);

exp_int整数是*只对那些在 GPL 许可下运行的内核模块可见。*所以,试试这个:将core_lkm中的#if 1语句改为#if 0,这样现在就可以在麻省理工学院专用许可下发布了。现在,重建并重试。它在构建阶段本身失败了:

$ make
[...]
Building for: kver=5.4.0-llkd01 ARCH=x86 CROSS_COMPILE= EXTRA_CFLAGS=-DDEBUG
  Building modules, stage 2.
  MODPOST 2 modules
FATAL: modpost: GPL-incompatible module user_lkm.ko uses GPL-only symbol 'exp_int'
[...]
$ 

执照确实很重要!在我们结束这一部分之前,这里有一个模块堆叠可能出错的快速列表;也就是要检查的东西:

  • 插入/移除时指定的内核模块顺序错误
  • 试图插入已经在内核内存中的导出例程–命名空间冲突问题:
$ sudo insmod ./min_sysinfo.ko
[...]
$ cd ../modstacking ; sudo insmod ./core_lkm.ko
insmod: ERROR: could not insert module ./core_lkm.ko: Invalid module format
$ dmesg
[...]
[32077.823472] core_lkm: exports duplicate symbol llkd_sysinfo2 (owned by min_sysinfo)
$ sudo rmmod min_sysinfo
$ sudo insmod ./core_lkm.ko * # now it's ok*
  • 使用EXPORT_SYMBOL_GPL()宏导致的许可证问题

Always look up the kernel log (with dmesg(1) or journalctl(1)). It often helps to show what actually went awry.

因此,让我们总结一下:为了在内核模块空间中模拟一个类似库的特性,我们探索了两种技术:

  • 我们使用的第一种技术是将多个源文件链接到一个内核模块中。
  • 这与模块堆叠技术相反,在这种技术中,我们实际上构建了多个内核模块,并将它们“堆叠”在彼此之上。

第一种技术不仅运行良好,还具有以下优点:

  • 我们做而不是必须明确标记(通过EXPORT_SYMBOL())我们用作导出的每个数据/功能符号。
  • 这些功能只对它实际链接到的内核模块可用(而不是整个**内核,包括其他模块*)。这是好事!所有这些都是以稍微调整 Makefile 为代价的——非常值得。*

*“链接”方法的缺点是:当链接多个文件时,内核模块的大小可能会变大。

这就是你学习内核编程的一个强大特性——将多个源文件链接在一起形成一个内核模块的能力,和/或利用模块堆叠设计,这两者都允许你开发更复杂的内核项目。

在下一节中,我们将深入探讨如何将参数传递给内核模块的细节。

将参数传递给内核模块

一种常见的调试技术是仪器你的代码;也就是说,在适当的点插入打印,这样您就可以遵循代码的路径。当然,在内核模块中,我们会为此使用通用的printk 函数。那么,假设我们做了如下的事情(伪代码):

#define pr_fmt(fmt) "%s:%s():%d: " fmt, KBUILD_MODNAME, __func__, __LINE__
[ ... ]
func_x() { 
    pr_debug("At 1\n");
    [...]
    while (<cond>) {
        pr_debug("At 2: j=0x%x\n", j); 
        [...] 
 }
 [...]
}

好极了。但是我们不希望调试打印出现在生产(或发布)版本中。这正是我们使用pr_debug()的原因:只有当符号DEBUG被定义时,它才会发出一个 printk!的确如此,但有趣的是,如果我们的客户是工程客户,并且希望动态打开或关闭这些调试打印件会怎样?你可以采取几种方法;一个是如下伪代码:

static int debug_level;     /* will be init to zero */
func_x() { 
    if (debug_level >= 1) pr_debug("At 1\n");
    [...]
    while (<cond>) {
        if (debug_level >= 2) 
            pr_debug("At 2: j=0x%x\n", j); 
        [...] 
    }
 [...]
}

啊,太好了。所以,我们真正得到的是这样的:如果我们能让 debug_level 模块变量 *成为我们内核模块的一个参数呢?*然后,一个强大的东西,你的内核模块的用户可以控制调试消息是否出现。

声明和使用模块参数

模块参数在模块插入(insmod)时作为名称=值对传递给内核模块。例如,假设我们有一个名为mp_debug_level模块参数;然后,我们可以在insmod(8)时间传递它的值,像这样:

sudo insmod modparams1.ko mp_debug_level=2

Here, the mp prefix stands for module parameter. It's not required to name it that way, of course, it is pedantic, but might just makes it a bit more intuitive.

那将是强大的。现在,最终用户可以决定他们想要什么样的详细程度的调试级别的消息。我们甚至可以很容易地安排默认值为0

你可能会想:内核模块没有main()功能,因此没有常规的(argc, argv)参数列表,那么,你到底是如何传递参数的呢?事实是,这有点链接器的诡计;只需这样做:将预期的模块参数声明为全局(static)变量,然后使用module_param()宏向构建系统指定将其视为模块参数。

这很容易从我们第一个模块参数的演示内核模块中看到(像往常一样,完整的源代码和 Makefile 可以在书中的 GitHub repo 中找到):

// ch5/modparams/modparams1/modparams1.c
[ ... ]
/* Module parameters */
static int mp_debug_level;
module_param(mp_debug_level, int, 0660);
MODULE_PARM_DESC(mp_debug_level,
"Debug level [0-2]; 0 => no debug messages, 2 => high verbosity");

static char *mp_strparam = "My string param";
module_param(mp_strparam, charp, 0660);
MODULE_PARM_DESC(mp_strparam, "A demo string parameter");

In the static int mp_debug_level; statement, there is no harm in changing it to static int mp_debug_level = 0; , thus explicitly initializing the variable to 0, right? Well, no: the kernel's scripts/checkpatch.pl script output reveals that this is not considered good coding style by the kernel community:

ERROR: do not initialise statics to 0 #28: FILE: modparams1.c:28: +static int mp_debug_level = 0;

在前面的代码块中,我们已经通过module_param()宏将两个变量声明为模块参数。module_param()宏取三个参数:

  • 第一个参数:变量名(我们希望将其视为模块参数)。这应该使用static限定符来声明。
  • 第二个参数:它的数据类型。
  • 第三个参数:权限(真的,它的可见性通过sysfs;这解释如下)。

MODULE_PARM_DESC()宏允许我们“描述”参数所代表的内容。想想看,这就是你如何通知最终用户内核模块(或驱动)以及哪些参数是实际可用的。通过modinfo(8)实用程序执行查找。此外,通过使用-p选项开关,您可以专门将参数信息打印到模块,如图所示:

cd <booksrc>/ch5/modparams/modparams1
make
$ modinfo -p ./modparams1.ko 
parm:          mp_debug_level:Debug level [0-2]; 0 => no debug messages, 2 => high verbosity (int)
parm:          mp_strparam:A demo string parameter (charp)
$ 

modinfo(8)输出显示可用的模块参数(如果有)。在这里,我们可以看到我们的modparams1.ko内核模块有两个参数,它们的名称、描述和数据类型(括号内;charp是字符指针,显示一个字符串)。好了,现在让我们快速演示一下我们的内核模块:

sudo dmesg -C
sudo insmod ./modparams1.ko 
dmesg 
[42724.936349] modparams1: inserted
[42724.936354] module parameters passed: mp_debug_level=0 mp_strparam=My string param

这里,我们从dmesg(1)输出中看到,由于我们没有显式传递任何内核模块参数,模块变量显然保留了它们的缺省(原始)值。让我们重复一遍,这次将显式值传递给模块参数:

sudo rmmod modparams1 
sudo insmod ./modparams1.ko mp_debug_level=2 mp_strparam=\"Hello modparams1\"
$ dmesg 
[...]
[42734.162840] modparams1: removed
[42766.146876] modparams1: inserted
[42766.146880] module parameters passed: mp_debug_level=2 mp_strparam=Hello modparams1
$ 

它像预期的那样工作。既然我们已经看到了如何声明一些参数并将其传递给内核模块,那么让我们看看如何在运行时检索甚至修改它们。

插入后获取/设置模块参数

让我们再仔细看看前面modparams1.c源文件中module_param()宏的用法:

module_param(mp_debug_level, int, 0660);

注意第三个参数,权限(或者模式):是0660(当然是八进制号,暗示所有者和组有读写权限,其他人没有权限)。直到您意识到如果权限参数被指定为非零,伪文件会在sysfs文件系统下创建,表示内核模块参数,这里:/sys/module/<module-name>/parameters/:

sysfs is usually mounted under /sys. Also, by default, all pseudo-files will have the owner and group as root.

  1. 因此,对于我们的modparams1内核模块(假设它被加载到内核内存中),让我们来查找它们:
$ ls /sys/module/modparams1/
coresize   holders/    initsize  initstate  notes/  parameters/  refcnt sections/  srcversion  taint     uevent     version
$ ls -l /sys/module/modparams1/parameters/
total 0
-rw-rw---- 1 root root 4096 Jan  1 17:39 mp_debug_level
-rw-rw---- 1 root root 4096 Jan  1 17:39 mp_strparam
$ 

的确,他们在那里!不仅如此,它真正的妙处在于,这些“参数”现在可以随时随意读写(当然,只是需要 root 权限)!

  1. 看看吧:
$ cat /sys/module/modparams1/parameters/mp_debug_level 
cat: /sys/module/modparams1/parameters/mp_debug_level: Permission denied
$ sudo cat /sys/module/modparams1/parameters/mp_debug_level
[sudo] password for llkd: 
2

是的,我们mp_debug_level内核模块参数的当前值确实是2

  1. 让我们将其动态更改为0,这意味着modparams1内核模块不会发出任何“调试”消息:
$ sudo bash -c "echo 0 > /sys/module/modparams1/parameters/mp_debug_level"
$ sudo cat /sys/module/modparams1/parameters/mp_debug_level 
0

瞧,完成了。您可以类似地获取和/或设置mp_strparam参数;我们将把它留给你作为一个简单的练习来尝试。这是很强大的东西:你可以通过内核模块参数编写简单的脚本来控制设备(或任何东西)的行为,获取(或切断)调试信息,等等;可能性是无穷的。

实际上,将第三个参数module_param()编码为文字八进制数(如0660)在某些圈子里并不被认为是最佳编程实践。通过适当的宏(在include/uapi/linux/stat.h中指定)指定sysfs伪文件的权限,例如:

module_param(mp_debug_level, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP);

然而,说到这里,我们的“更好”Makefile 的 checkpatch 目标(当然,它调用内核的scripts/checkpatch.pl“编码风格”Perl 脚本检查器)礼貌地告诉我们,简单地使用八进制权限更好:

$ make checkpatch
[ ... ]
checkpatch.pl: /lib/modules/<ver>/build//scripts/checkpatch.pl --no-tree -f *.[ch]
[ ... ]
WARNING: Symbolic permissions 'S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP' are not preferred. Consider using octal permissions '0660'.
 #29: FILE: modparams1.c:29:
 +module_param(mp_debug_level, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP);

所以,内核社区不同意。因此,我们将只使用0660的“常用”八进制数符号。

模块参数数据类型和验证

在前面的简单内核模块中,我们设置了整数和字符串数据类型的两个参数(charp)。可以使用哪些其他数据类型?几个,事实证明:moduleparam.h包含文件揭示了所有(在一个注释内,复制如下):

// include/linux/moduleparam.h
[...]
 * Standard types are:
 * byte, short, ushort, int, uint, long, ulong
 * charp: a character pointer
 * bool: a bool, values 0/1, y/n, Y/N.
 * invbool: the above, only sense-reversed (N = true).

如果需要,您甚至可以定义自己的数据类型。不过,通常标准类型已经足够了。

验证内核模块参数

所有内核模块参数默认为可选;用户可以或不可以显式地传递它们。但是如果我们的项目要求用户必须为给定的内核模块参数显式地传递一个值呢?我们在这里解决这个问题:让我们增强我们之前的内核模块,创建另一个(ch5/modparams/modparams2),关键区别在于我们设置了一个名为control_freak的附加参数。现在,我们要求用户在模块插入时必须传递该参数:

  1. 让我们用代码设置新的模块参数:
static int control_freak;
module_param(control_freak, int, 0660);
MODULE_PARM_DESC(control_freak, "Set to the project's control level [1-5]. MANDATORY");
  1. 如何才能做到这种“强制通过”?嗯,真的有点黑:只要在插入时检查值是否是默认值(这里是0)。如果是这样,那么用适当的消息中止(我们还做了一个简单的有效性检查,以确保传递的整数在给定的范围内)。以下是ch5/modparams/modparams2/modparams2.c的初始化代码:
static int __init modparams2_init(void)
{
    pr_info("%s: inserted\n", OUR_MODNAME);
    if (mp_debug_level > 0)
        pr_info("module parameters passed: "
                "mp_debug_level=%d mp_strparam=%s\n control_freak=%d\n",
                mp_debug_level, mp_strparam, control_freak);

    /* param 'control_freak': if it hasn't been passed (implicit guess), 
     * or is the same old value, or isn't within the right range,
     * it's Unacceptable!  :-)
     */
    if ((control_freak < 1) || (control_freak > 5)) {
        pr_warn("%s: Must pass along module parameter"
              " 'control_freak', value in the range [1-5]; aborting...\n",
              OUR_MODNAME);
        return -EINVAL;
    }
    return 0; /* success */
}
  1. 此外,作为一个快速演示,请注意我们如何发出 printk,仅当mp_debug_level为正时才显示模块参数值。
  2. 最后,在这个主题上,内核框架提供了一种更严格的方法来“获取/设置”内核(模块)参数,并通过module_parm_cb()宏(cb用于回调)对其执行有效性检查。我们在这里就不深究了;我建议你参考一篇在进一步阅读文档中提到的博客文章,了解使用它的详细信息。

现在,让我们继续讨论如何(以及为什么)覆盖模块参数的名称。

重写模块参数的名称

为了解释这个特性,让我们从(5.4.0)内核源代码树中举一个例子:直接映射缓冲 I/O 库驱动程序drivers/md/dm-bufio.c,需要使用dm_bufio_current_allocated变量作为模块参数。然而,这个名字实际上是一个内部变量的名字,对于这个司机的用户来说不是很直观。该驱动程序的作者更愿意使用另一个名称–current_allocated_bytes–作为别名或*名称覆盖。*准确地说,这可以通过module_param_named()宏实现,覆盖并完全等同于内部变量名,如下所示:

// drivers/md/dm-bufio.c
[...]
module_param_named(current_allocated_bytes, dm_bufio_current_allocated, ulong, S_IRUGO);
MODULE_PARM_DESC(current_allocated_bytes, "Memory currently used by the cache");

因此,当用户在该驱动程序上执行insmod时,他们可以执行如下操作:

sudo insmod <path/to/>dm-bufio.ko current_allocated_bytes=4096 ...

在内部,实际变量dm_bufio_current_allocated将被赋值4096

硬件相关的内核参数

出于安全原因,指定硬件特定值的模块或内核参数有一个单独的宏–module_param_hw[_named|array]()。David Howells 于 2016 年 12 月 1 日提交了这些新硬件参数内核支持的补丁系列。补丁邮件[https://lwn.net/Articles/708274/]提到了以下内容:

Provided an annotation for module parameters that specify hardware
parameters (such as io ports, iomem addresses, irqs, dma channels, fixed
dma buffers and other types).

This will enable such parameters to be locked down in the core parameter
parser for secure boot support.  [...]

关于内核模块参数的讨论到此结束。让我们继续讨论一个特殊的方面——内核中的浮点用法。

内核中不允许浮点运算

几年前,在从事温度传感器设备驱动程序的工作时,我有过一次有趣的经历(尽管当时并不那么有趣)。试图将以毫摄氏度为单位的温度值表示为以摄氏度为单位的“常规”温度值,我做了如下工作:

double temp;
[... processing ...]
temp = temp / 1000.0;
printk(KERN_INFO "temperature is %.3f degrees C\n", temp);

从那以后一切都变坏了!

德高望重的 LDD ( Linux 设备驱动程序,由科尔贝特、鲁比尼、G-K-哈特曼所著)一书指出了我的错误——内核空间不允许浮点 (FP)运算!这是一个有意识的设计决定——保存处理器(FP)状态,打开 FP 单元,工作,然后关闭并恢复 FP 状态,只是在内核中不被认为是一件值得做的事情。内核(或驱动程序)开发人员最好不要试图在内核空间中执行 FP 工作。

那么,你会问,(在我的例子中)如何进行温度转换?简单:将整数毫摄氏度值传递给用户空间,在那里进行 FP 工作!

说到这里,显然有一种方法可以强制内核执行 FP:将您的浮点代码放在kernel_fpu_begin()kernel_fpu_end()宏之间。在内核代码库中,有一些地方精确地使用了这种技术(通常,一些代码路径覆盖了加密/AES、循环冗余校验等)。无论如何,建议典型的模块(或驱动程序)开发者只在内核中执行整数运算。

然而,为了测试这整个场景(永远记住,*经验方法——实际上尝试事情——是唯一现实的前进方式! ) *,我们编写了一个简单的内核模块,试图执行一些 FP 工作。代码的关键部分如下所示:

// ch5/fp_in_kernel/fp_in_kernel.c
static double num = 22.0, den = 7.0, mypi;
static int __init fp_in_lkm_init(void)
{
    [...]
    kernel_fpu_begin();
    mypi = num/den;
    kernel_fpu_end();
#if 1
    pr_info("%s: PI = %.4f = %.4f\n", OURMODNAME, mypi, num/den);
#endif
    return 0;     /* success */
}

它实际上是起作用的,直到 我们尝试通过 printk()显示 FP 值!在这一点上,它变得相当疯狂。请看下面的截图:

Figure 5.4 – The output of WARN_ONCE() when we try and print an FP number in kernel space

关键线路是Please remove unsupported %f in format string

这告诉我们这个故事。系统实际上并没有崩溃或恐慌,因为这只是一个WARNING,通过WARN_ONCE()宏向内核日志抛出。不过,请务必意识到,在生产系统中,/proc/sys/kernel/panic_on_warn伪文件很可能会被设置为值1,从而导致内核(相当正确地)恐慌。

The section in the preceding screenshot (Figure 5.3) beginning with Call Trace: is, of course, a peek into the current state of the kernel-mode stack of the process or thread that was "caught" in the preceding WARN_ONCE() code path (hang on, you will learn key details regarding the user- and kernel-mode stacks and so on in Chapter 6, Kernel Internals Essentials – Processes and Threads). Interpret the kernel stack by reading it in a bottom-up fashion; so here, the do_one_initcall function called fp_in_lkm_init (which belongs to the kernel module in square brackets, [fp_in_lkm_init]), which then calls printk(), which then ends up causing all kinds of trouble as it attempts to print a FP (floating point) quantity!

寓意很明确:避免在内核空间内使用浮点数学。现在让我们继续讨论如何在系统启动时安装和自动加载内核模块。

系统启动时自动加载模块

到目前为止,我们已经编写了简单的“树外”内核模块,它们驻留在自己的私有目录中,必须手动加载,通常是通过insmod(8)modprobe(8)实用程序。在大多数现实世界的项目和产品中,您将要求您的树外内核模块在启动时 自动加载。本节介绍如何实现这一点。

假设我们有一个名为foo.ko的内核模块。我们假设我们可以访问源代码和 Makefile。为了让它在系统启动时自动加载,您需要首先内核模块安装到系统上的已知位置。为此,我们期望模块的 Makefile 包含一个install目标,通常是:

install:
 make -C $(KDIR) M=$(PWD) modules_install

这不是什么新鲜事;我们已经将install 目标放置在我们演示内核模块的Makefile中。

为了演示这个“自动加载”过程,我们展示了一组步骤,以便在引导时安装并自动加载我们的ch5/min_sysinfo内核模块:

  1. 首先,将目录更改为模块的源目录:
cd <...>/ch5/min_sysinfo
  1. 接下来,重要的是首先构建内核模块(用make),成功后安装它(正如您很快会看到的,我们的‘更好’Makefile 通过保证首先完成构建,然后是安装和depmod)使过程变得更简单:
make && sudo make install   

假设它已经构建好了,sudo make install命令然后按照预期在这里/lib/modules/<kernel-ver>/extra/安装内核模块(也可以看到下面的信息框和提示):

$ cd <...>/ch5/min_sysinfo
$ make                *<-- ensure it's first built 'locally'   
               generating the min_sysinfo.ko kernel module object*
[...]
$ sudo make install Building for: KREL= ARCH= CROSS_COMPILE= EXTRA_CFLAGS=-DDEBUG
make -C /lib/modules/5.4.0-llkd01/build M=<...>/ch5/min_sysinfo modules_install
make[1]: Entering directory '/home/llkd/kernels/linux-5.4'
 INSTALL <...>/ch5/min_sysinfo/min_sysinfo.ko
 DEPMOD  5.4.0-llkd01
make[1]: Leaving directory '/home/llkd/kernels/linux-5.4'
$ ls -l /lib/modules/5.4.0-llkd01/extra/
total 228
-rw-r--r-- 1 root root 232513 Dec 30 16:23 min_sysinfo.ko
$ 

During sudo make install, it's possible you might see (non-fatal) errors regarding SSL; they can be safely ignored. They indicate that the system failed to "sign" the kernel module. More on this in the note on security coming up. Also, just in case you find that sudo make install fails, try the following approaches: a) Switch to a root shell (sudo -s) and within it, run the make ; make install commands. b) A useful reference: Makefile: installing external Linux kernel module, StackOverflow, June 2016 (https://unix.stackexchange.com/questions/288540/makefile-installing-external-linux-kernel-module).

  1. 另一个名为depmod(8)的模块实用程序通常在sudo make install中被默认调用(从前面的输出可以看出)。以防(不管什么原因)这种情况没有发生,您总是可以手动调用depmod:它的工作本质上是解决模块依赖关系(详见其手册页):sudo depmod。安装内核模块后,可以看到depmod(8)--dry-run选项开关的效果:
$ sudo depmod --dry-run | grep min_sysinfo
extra/min_sysinfo.ko:
alias symbol:lkdc_sysinfo2 min_sysinfo
alias symbol:lkdc_sysinfo min_sysinfo
$ 
  1. 引导时自动加载内核模块:一种方法是创建/etc/modules-load.d/<foo>.conf配置文件(当然,创建该文件需要 root 访问权限);最简单的情况:只要把内核模块的foo名字放在里面,就这样。任何以#字符开头的行都被视为注释并被忽略。对于我们的min_sysinfo示例,我们有以下内容:
$ cat /etc/modules-load.d/min_sysinfo.conf 
# Auto load kernel module for LLKD book: ch5/min_sysinfo
min_sysinfo
$

FYI, another (even simpler) way to inform systemd to load up our kernel module is to enter the name of the module into the (preexisting) /etc/modules-load.d/modules.conf file.

  1. sync; sudo reboot重启系统。

一旦系统启动,使用lsmod(8)并查找内核日志(也许是dmesg(1))。您应该会看到与内核模块加载相关的信息(在我们的示例中为min_sysinfo):

[... system boots up ...]

$ lsmod | grep min_sysinfo
min_sysinfo         16384  0
$ dmesg | grep -C2 min_sysinfo
[...]
[ 2.395649] min_sysinfo: loading out-of-tree module taints kernel.
[ 2.395667] min_sysinfo: module verification failed: signature and/or required key missing - tainting kernel
[ 2.395814] min_sysinfo: inserted
[ 2.395815] lkdc_sysinfo(): minimal Platform Info:
               CPU: x86_64, little-endian; 64-bit OS.
$

在那里,它完成了:我们的min_sysinfo内核模块确实已经在启动时自动加载到内核空间中了!

正如您刚刚学到的,您必须首先构建您的内核模块,然后执行安装;为了帮助实现自动化,我们的“更好”Makefile 在其模块安装install目标中有以下内容:

// ch5/min_sysinfo/Makefile
[ ... ]
install:
    @echo
    @echo "--- installing ---"
    @echo " [First, invoke the 'make' ]"
    make
    @echo
    @echo " [Now for the 'sudo make install' ]"
    sudo make -C $(KDIR) M=$(PWD) modules_install
 sudo depmod

它确保,首先,构建完成,然后是安装和(明确地)第depmod(8)

如果自动加载的内核模块需要在加载时传递一些(模块)参数,该怎么办?有两种方法可以确保这一点:通过所谓的 modprobe 配置文件(在/etc/modprobe.d/下),或者,如果模块内置于内核,通过内核命令行。

这里我们展示第一种方法:简单地设置您的 modprobe 配置文件(作为一个例子,我们使用名称mykmod作为我们的 LKM 的名称;同样,您需要 root 访问权限来创建此文件):/etc/modprobe.d/mykmod.conf;在其中,您可以传递如下参数:

options <module-name> <parameter-name>=<value>

例如,我的 x86_64 Ubuntu 20.04 LTS 系统上的/etc/modprobe.d/alsa-base.conf modprobe 配置文件包含以下几行(以及其他几行):

# Ubuntu #62691, enable MPU for snd-cmipci
options snd-cmipci mpu_port=0x330 fm_port=0x388

关于内核模块自动加载相关项目的更多要点如下。

模块自动加载-其他详细信息

一旦系统上安装了内核模块(如前所示,通过sudo make install,您也可以通过交互方式(或通过脚本)将其插入内核,只需使用insmod(8)实用程序的“更智能”版本,称为modprobe(8)。例如,我们可以首先rmmod(8)模块,然后执行以下操作:

sudo modprobe min_sysinfo

有趣的是,请考虑以下内容。在有多个内核模块对象需要加载的情况下(例如模块堆叠设计),modprobe如何知道顺序加载内核模块?在本地执行构建时,构建过程会生成一个名为modules.order的文件。它告诉诸如modprobe这样的实用程序加载内核模块的顺序,以便解决所有的依赖关系。当内核模块安装到内核中时(即安装到/lib/modules/$(uname -r)/extra/或类似的位置),depmod(8)实用程序会生成一个/lib/modules/$(uname -r)/modules.dep文件。这包含依赖信息——它指定一个内核模块是否依赖于另一个。使用这些信息,modprobe 然后按照所需的顺序加载它们。为了充实这一点,让我们安装我们的模块堆叠示例:

$ cd <...>/ch5/modstacking
$ make && sudo make install
[...]
$ ls -l /lib/modules/5.4.0-llkd01/extra/
total 668K
-rw-r--r-- 1 root root 218K Jan 31 08:41 core_lkm.ko
-rw-r--r-- 1 root root 228K Dec 30 16:23 min_sysinfo.ko
-rw-r--r-- 1 root root 217K Jan 31 08:41 user_lkm.ko
$ 

显然,我们的模块堆叠示例中的两个内核模块(core_lkm.kouser_lkm.ko)现在安装在预期位置/lib/modules/$(uname -r)/extra/下。现在,看看这个:

$ grep user_lkm /lib/modules/5.4.0-llkd01/* 2>/dev/null
/lib/modules/5.4.0-llkd01/modules.dep:extra/user_lkm.ko: extra/core_lkm.ko
Binary file /lib/modules/5.4.0-llkd01/modules.dep.bin matches
$

grep后的第一行输出是相关的:depmod已经安排modules.dep文件显示extra/user_lkm.ko内核模块依赖于extra/core_lkm.ko内核模块(通过<k1.ko>: <k2.ko>...符号,暗示k1.ko模块依赖于k2.ko模块)。因此,modprobe 看到这一点后,按照所需的顺序加载它们,从而避免了任何问题。

(仅供参考,在本主题中,生成的Module.symvers文件包含所有导出符号的信息。)

接下来,回想一下 Linux 上新的(ish) init框架, systemd 。事实是,在现代的 Linux 系统上,实际上是 systemd 负责在系统启动时自动加载内核模块,通过解析文件的内容,比如/etc/modules-load.d/*(负责这个的 systemd 服务是systemd-modules-load.service(8))。详见modules-load.d(5)手册页。

相反,有时您可能会发现某个自动加载的内核模块运行不正常——导致锁定或延迟,或者根本不起作用——因此您肯定想禁用它的加载。这可以通过 模块列入黑名单来实现。您可以在内核命令行中指定这一点(当所有其他操作都失败时很方便!)或在(前面提到的)/etc/modules-load.d/<foo>.conf配置文件中。在内核命令行上,通过module_blacklist=mod1,mod2,...,内核文档向我们展示了语法/解释:

module_blacklist=  [KNL] Do not load a comma-separated list of
                        modules.  Useful for debugging problem modules.

You can look up the current kernel command line by doing cat /proc/cmdline.

在内核命令行的主题上,还有其他几个有用的选项,使我们能够使用内核的帮助来调试与内核初始化相关的问题。例如,在其他几个参数中,内核在这方面提供了以下参数(来源:https://www . kernel . org/doc/html/latest/admin-guide/kernel-parameters . html):

debug           [KNL] Enable kernel debugging (events log level).
[...]
initcall_debug  [KNL] Trace initcalls as they are executed. Useful
                      for working out where the kernel is dying during
                      startup.
[...]
ignore_loglevel [KNL] Ignore loglevel setting - this will print /all/
                      kernel messages to the console. Useful for  
                      debugging. We also add it as printk module 
                      parameter, so users could change it dynamically, 
                      usually by /sys/module/printk/parameters/ignore_loglevel.

仅供参考,正如本章前面提到的,还有一个第三方内核模块自动重建的替代框架,称为动态内核模块支持 ( DKMS )。

本章的进一步阅读文档也提供了一些有用的链接。总之,在系统启动时将内核模块自动加载到内存中是产品中一项有用且经常需要的功能。打造高品质产品需要对安全有敏锐的理解和知识;这是下一节的主题。

内核模块和安全性—概述

一个具有讽刺意味的现实是,在改善用户空间安全考虑上花费的巨大努力,在最近几年已经带来了相当大的回报。几十年前,一个恶意用户执行可行的缓冲区溢出 ( BoF )攻击是完全有可能的,但今天真的很难做到。为什么呢?因为有许多层增强的安全机制来防止这些攻击类别。

To quickly name a few countermeasures: compiler protections (-fstack-protector[...], -Wformat-security, -D_FORTIFY_SOURCE=2, partial/full RELRO, better sanity and security checker tools (checksec.sh, the address sanitizers, paxtest, static analysis tools, and so on), secure libraries, hardware-level protection mechanisms (NX, SMEP, SMAP, and so on), [K]ASLR, better testing (fuzzing), and so on.

具有讽刺意味的是内核空间攻击在过去几年变得越来越普遍!已经证明,即使向聪明的攻击者透露一个有效的内核(虚拟)地址(及其对应的符号),也可以让她知道一些关键的内部内核结构的位置,从而为执行各种权限升级 ( 权限)攻击铺平道路。因此,即使泄露一条看起来很简单的内核信息(如内核地址及其关联的符号),也是潜在的信息泄露(或信息泄露),必须在生产系统中加以防止。接下来,我们将列举并简要描述 Linux 内核提供的一些安全特性。然而,最终,内核开发者——你!–发挥重要作用:首先编写安全代码!使用我们的“更好的”Makefile 是一个很好的开始方式——其中的几个目标与安全性有关(例如,所有的静态分析)。

影响系统日志的 Proc 文件系统可调参数

我们直接让您参考proc(5)上的手册页–非常有价值!–收集这两个安全相关可调参数的信息:

  • dmesg_restrict
  • kptr_restrict

一、dmesg_restrict:

dmesg_restrict
/proc/sys/kernel/dmesg_restrict (since Linux 2.6.37)
 The value in this file determines who can see kernel syslog contents. A  value of 0 in this file imposes no restrictions. If the value is 1, only privileged users can read the kernel syslog. (See syslog(2) for more details.) Since Linux 3.4, only users with the CAP_SYS_ADMIN capability may change the value in this file.

默认值(在我们的 Ubuntu 和 Fedora 平台上)是0:

$ cat /proc/sys/kernel/dmesg_restrict
0

Linux 内核使用强大的细粒度 POSIX 功能模型。CAP_SYS_ADMIN功能本质上是对传统的*根(超级用户/系统管理员)*访问的全面控制。CAP_SYSLOG功能赋予进程(或线程)执行特权syslog(2)操作的能力。

如前所述,“泄露”内核地址及其相关符号可能会导致基于信息泄露的攻击。为了帮助防止这些情况,建议内核和模块作者始终使用新的printf样式格式打印内核地址:打印地址时,应该使用较新的 %pK 格式说明符,而不是熟悉的%p%px。(使用%px格式说明符确保打印实际地址;你会希望在生产中避免这种情况)。这有什么帮助?继续读...

打印内核地址时,kptr_restrict 可调(2.6.38 向前)影响printk()输出;做printk("&var = **%pK**\n", &var); 而不做老好人printk("&var = %p\n", &var);被认为是安全的最佳做法。理解kptr_restrict可调滤波器的工作原理是关键:

kptr_restrict
/proc/sys/kernel/kptr_restrict (since Linux 2.6.38)
 The value in this file determines whether kernel addresses are exposed via /proc files and other interfaces. A value of 0 in this file imposes no restrictions. If the value is 1, kernel pointers printed using the %pK format specifier will be replaced with zeros unless the user has the CAP_SYSLOG capability. If the value is 2, kernel pointers printed using the %pK format specifier will be replaced with zeros regardless of the user's capabilities. The initial default value for this file was 1, but the default was changed to 0 in Linux 2.6.39\. Since Linux 3.4, only users with the CAP_SYS_ADMIN capability can change the value in this file.

默认值(在我们最新的 Ubuntu 和 Fedora 平台上)是1:

$ cat /proc/sys/kernel/kptr_restrict 
1

为了安全起见,您可以(更确切地说,必须)将生产系统上的这些可调参数更改为安全值(1 或 2)。当然,安全措施只有在开发人员利用它们时才起作用;截至 5.4.0 Linux 内核,共有(刚刚!)整个 Linux 内核代码库中%pK格式说明符的 14 种用法(总共约 5200 多种使用%p的 printk 用法中,约 230 种明确使用%px格式说明符)。

a) As procfs is, of course, a volatile filesystem, you can always make the changes permanent by using the sysctl(8) utility with the -w option switch (or by directly updating the /etc/sysctl.conf file). b) For the purpose of debugging, if you must print an actual kernel (unmodified) address, you're advised to use the %px format specifier; do remove these prints on production systems! c) Detailed kernel documentation on printk format specifiers can be found at https://www.kernel.org/doc/html/latest/core-api/printk-formats.html#how-to-get-printk-format-specifiers-right; do browse through it.

随着 2018 年初硬件级缺陷的出现(现在众所周知的*熔毁、Spectre、和其他处理器推测安全问题),人们对检测信息泄露再次产生了紧迫感,*因此使开发人员和管理员能够阻止它们。

A useful Perl script, scripts/leaking_addresses.pl, was released in mainline in 4.14 (in November 2017; I am happy to have lent a hand in this important work: https://github.com/torvalds/linux/commit/1410fe4eea22959bd31c05e4c1846f1718300bde), with more checks being made for detecting leaking kernel addresses.

内核模块的加密签名

一旦恶意攻击者在系统上站稳脚跟,他们通常会尝试某种私有向量来获得根访问权限。一旦实现了这一点,典型的下一步就是安装一个 rootkit :本质上是一个脚本和内核模块的集合,它们将几乎接管系统(通过“劫持”系统调用、设置后门和键盘记录器等等)。

当然,这并不容易——充满了 Linux 安全模块 ( LSMs )等等的现代生产质量 Linux 系统的安全姿态意味着这根本不是一件微不足道的事情,但是对于一个熟练且有动力的攻击者来说,一切皆有可能。假设他们安装了足够复杂的 rootkit,系统现在就被认为受到了威胁。

一个有趣的想法是这样的:即使有根访问,也不要允许insmod(8)(或modprobe(8),甚至底层的[f]init_module(2)系统调用)将内核模块插入内核地址空间**,除非它们是用内核密钥环中的安全密钥**加密签名的。这个强大的安全特性是 3.7 内核引入的(相关提交在这里:https://git . kernel . org/pub/SCM/Linux/kernel/git/Torvalds/Linux . git/commit/?id = 106 a4 ee 258d 14818467829 bf0e 12 EAE 14 c 16 CD 7

The details on performing cryptographic signing of kernel modules is beyond the scope of this book; you can refer to the official kernel documentation here: https://www.kernel.org/doc/html/latest/admin-guide/module-signing.html.

与该特性相关的一些内核配置选项有CONFIG_MODULE_SIGCONFIG_MODULE_SIG_FORCECONFIG_MODULE_SIG_ALL等。为了帮助理解这到底意味着什么,请参见第一部分的Kconfig 'help'部分,如下所示(来自init/Kconfig):

config MODULE_SIG
 bool "Module signature verification"
 depends on MODULES
 select SYSTEM_DATA_VERIFICATION
 help
  Check modules for valid signatures upon load: the signature is simply  
  appended to the module. For more information see  
  <file:Documentation/admin-guide/module-signing.rst>. Note that this  
  option adds the OpenSSL development packages as a kernel build   
  dependency so that the signing tool can use its crypto library.

 !!!WARNING!!! If you enable this option, you MUST make sure that the  
 module DOES NOT get stripped after being signed. This includes the
 debuginfo strip done by some packagers (such as rpmbuild) and
 inclusion into an initramfs that wants the module size reduced

MODULE_SIG_FORCE内核配置是一个布尔值(默认为n)。只有打开MODULE_SIG时,它才会起作用。如果MODULE_SIG_FORCE设置为y,那么内核模块必须具有有效的签名才能被加载。否则,加载将失败。如果它的值保留为n,这意味着即使没有签名的内核模块也会被加载到内核中,但是内核会被标记为被污染。这往往是典型的现代 Linux 发行版的默认设置。在下面的代码块中,我们在 x86_64 Ubuntu 20.04.1 LTS 来宾虚拟机上查找这些内核配置:

$ grep MODULE_SIG /boot/config-5.4.0-58-generic 
CONFIG_MODULE_SIG_FORMAT=y
CONFIG_MODULE_SIG=y
# CONFIG_MODULE_SIG_FORCE is not set
CONFIG_MODULE_SIG_ALL=y
[ ... ] 

生产系统鼓励内核模块的加密签名(近年来,随着(I)物联网边缘设备变得越来越普遍,安全性是一个关键问题)。

完全禁用内核模块

偏执的人可能想完全禁用内核模块的加载(和卸载)。相当激烈,但是,嘿,这种方式可以完全锁定系统的内核空间(以及使任何 rootkits 变得几乎无害)。这可以通过两种广泛的方式实现:

  • 首先,通过在构建之前的内核配置期间将CONFIG_MODULES内核配置设置为关闭(当然,默认情况下是打开的)。这样做是相当激烈的——它使这个决定成为一个永久的决定!
  • 第二,假设CONFIG_MODULES开启,可以通过modules_disabled sysctl 可调,在运行时动态关闭模块加载;看看这个:
$ cat /proc/sys/kernel/modules_disabled
0 

当然是默认关闭 ( 0)。像往常一样,proc(5)上的手册页告诉我们这个故事:

/proc/sys/kernel/modules_disabled (since Linux 2.6.31)
 A toggle value indicating if modules are allowed to be loaded in an otherwise modular kernel. This toggle defaults to off (0), but can be set true (1). Once true, modules can be neither loaded nor unloaded, and the toggle cannot be set back to false. The file is present only if the kernel is built with the CONFIG_MODULES option enabled.

总之,内核安全强化和恶意攻击当然是猫捉老鼠的游戏。例如,(K)ASLR(我们在接下来关于 Linux 内存管理的章节中讨论(K)ASLR 的意思)经常被击败。另外,参见本文–有效绕过安卓上的 kptr _ restrict:http://bits-请. blogspot . com/2015/08/有效绕过-kptrrestrict-on.html 。安全不易;这总是一项正在进行的工作。(几乎)不言而喻:开发人员——无论是在用户空间还是内核空间——都必须编写具有安全意识的代码,并持续使用工具和测试。**

让我们用关于 Linux 内核的编码风格指南、访问内核文档以及如何为主线内核做贡献的主题来完成这一章。

内核开发人员的编码风格指南

许多大型项目指定了他们自己的一套编码指南;Linux 内核社区也是如此。遵循 Linux 内核编码风格准则确实是个好主意。你可以在这里找到它们的官方文档:https://www . kernel . org/doc/html/latest/process/coding-style . html(请务必阅读!).

此外,作为(相当详尽的)代码提交清单的一部分,对于像您这样想要上游代码的开发人员,您需要通过一个 Perl 脚本来运行您的补丁,该脚本检查您的代码是否与 Linux 内核编码风格一致:scripts/checkpatch.pl

默认情况下,该脚本仅在格式良好的git补丁上运行。可以针对独立的 C 代码运行它(就像您的树外内核模块代码一样),如下所示(就像我们的“更好的”Makefile 所做的那样):

<kernel-src>/scripts/checkpatch.pl --no-tree -f <filename>.c

在你的内核代码中养成这样的习惯是有帮助的,可以让你抓住那些烦人的小问题——还有更严重的问题!–否则可能会阻碍你的修补。我们再次提醒您:我们的“更好”Makefile 的indentcheckpatch目标就是针对这一点。

除了编码风格指南之外,你会发现时不时地,你需要深入研究复杂而有用的内核文档。提醒一下:我们在定位和使用 Linux 内核文档部分的第 1 章内核工作区设置中介绍了定位和使用内核文档。

我们现在将通过简单介绍如何开始一个崇高的目标来完成这一章:为主线 Linux 内核项目贡献代码。

为主线内核做贡献

在本书中,我们通常通过 LKM 框架在内核源代码树之外执行内核开发。如果你在内核树中编写代码*,明确的目标是将你的代码向上流到内核主线,会怎么样?这确实是一个值得称赞的目标——开源的整个基础源于社区愿意投入工作并将其贡献给项目的上游。*

开始为内核做贡献

最常被问到的问题当然是如何入门?为了帮助解决这个问题,内核文档中有一个很长很详细的答案:如何进行 linux 内核开发:https://www . kernel . org/doc/html/latest/process/how To . html #如何进行 Linux 内核开发

事实上,您可以生成完整的 Linux 内核文档(通过make pdfdocs命令,在内核源代码树的根中);一旦成功,你会在这里找到这个 PDF 文档:<root-of-kernel-source-tree>/Documentation/output/latex/development-process.pdf

这是一个非常详细的 Linux 内核开发过程指南,包括代码提交指南。此处显示了此文档的裁剪截图:

Figure 5.5 – (Partial) screenshot of the kernel development docs just generated

作为这个内核开发过程的一部分,为了保持质量标准,一个严格且必须遵守的清单——一个长长的配方!–是内核补丁提交过程的一部分。官方核对表在这里: Linux 内核补丁提交核对表:https://www . Kernel . org/doc/html/latest/process/submit-核对表. html # Linux-内核-补丁-提交-核对表

尽管对于一个内核新手来说,这看起来是一项繁重的任务,但是仔细遵循这个清单可以让你的工作更加严谨和可信,并最终得到更好的代码。我强烈建议您通读内核补丁提交清单,并尝试其中提到的过程。

Is there a really practical hands-on tip, an almost guaranteed way to become a kernel hacker? Of course, keep reading this book! Ha ha, yes, besides, do partake in the simply awesome Eudyptula Challenge (http://www.eudyptula-challenge.org/) Oh, hang on, it's – very unfortunately, and as of the time of writing – closed down.

Fear not; here's a site with all the challenges (and solutions, but don't cheat!) posted. Do check it out and try the challenges. This will greatly accelerate your kernel hacking skills: https://github.com/agelastic/eudyptula.

摘要

在这一章,关于使用 LKM 框架编写内核模块的第二章,我们涵盖了与这个重要主题相关的几个(剩余的)领域:其中,为内核模块使用“更好的”Makefile,配置调试内核的技巧(这非常重要!),交叉编译内核模块,从内核模块中收集一些最基本的平台信息,甚至还有一点内核模块的许可。我们还研究了用两种不同的方法模拟类似库的特性(一种是首选的链接方法,另一种是模块堆叠方法),使用模块参数,避免浮点运算,自动加载内核模块,等等。安全问题以及如何解决这些问题非常重要。最后,我们通过介绍内核编码风格指南、内核文档以及如何开始为主线内核做贡献来结束这一章。所以,恭喜你!您现在知道如何开发内核模块,甚至可以开始内核上游贡献之旅。

在下一章,我们将深入探讨一个有趣且必要的话题。我们将开始深入探索 Linux 内核及其内存管理子系统的内部。

问题

作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions。你会在这本书的 GitHub repo 中找到一些问题的答案:https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn

进一步阅读

为了帮助你用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中提供了一个更详细的在线参考资料和链接列表(有时甚至是书籍)。进一步阅读文档可在此获得:https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md 。****