Compile-kernel-module

1. 内核模块编程
1.1 简介
1.2 加载内核模块
1.3 最简单的模块
1.4 模块必要信息
1.4.1 内核模块必须至少包含的头文件:
1.4.2 内核模块必须至少有两个功能:
1.4.3 printk()日志记录
1.4.4 优先级
1.4.5 许可和模块文档
1.5 编译内核模块
1.6 实战
1.6.1 源代码文件: hello.c
1.6.2 Makefile文件: Makefile
1.6.3 使用make编译
1.6.4 使用modinfo查看新的模块文件
1,6.5 使用insmod加载内核模块
1.6.6 使用lsmod查看已加载的模块
1.6.7 使用rmmod卸载模块
1.6.8 使用dmesg查看日志
1.6.9 练习
1.6.10 其他信息
1.7 更多基础示例链接
1.7.1 将命令行参数传递给模块
1.7.2 跨越多个文件的模块
1.7.3 构建预编译内核的模块
1.7.4 与应用程序交互
2 make工具
2.1 DESCRIPTION
2.2 退出状态
2.3 选项
3. 其他内核编译方法及相关连接
3.1 编译Arch的内核模块
3.2 Arch构建系统
3.3 内核/传统编译
3.4 修补包
3.5 模块与程序

1. 内核模块编程

http://www.tldp.org/LDP/lkmpg/2.6/html/
Linux内核模块编程指南 2007-05-18见2.6.4

1.1 简介

什么是内核模块?模块是可以根据需要加载和卸载到内核中的代码片段。它们扩展了内核的功能,而无需重启系统。
例如,一种类型的模块是设备驱动程序,它允许内核访问连接到系统的硬件。还有很多的文件系统等。

1.2 加载内核模块

您可以通过运行lsmod来查看已经加载到内核中的模块,lsmod通过读取文件/proc/modules来获取其信息。
执行modprobe来加载模块.modprobe以两种形式之一传递一个字符串:
  > 模块名称,如softdog或ppp。
  > 一个更通用的标识符,如char-major-10-30。
如果modprobe被赋予通用标识符,它首先在文件/etc/modprobe.conf中查找该字符串。如果找到如下的别名行:alias char-major-10-30 softdog
它知道通用标识符引用模块softdog.ko。
然后,modprobe查看文件/lib/modules/version/modules.dep,查看是否必须加载其他模块才能加载所请求的模块。该文件由depmod -a创建,包含模块依赖项。
      例如,msdos.ko要求fat.ko模块已经加载到内核中。
最后,modprobe使用insmod首先将任何必备模块加载到内核中,然后加载所请求的模块。
modprobe将insmod指向/lib/modules/'uname -r'/, 模块的标准目录。

insmod对于模块的位置是相当愚蠢的,而modprobe知道模块的默认位置,知道如何找出依赖关系并以正确的顺序加载模块。
因此,例如,如果要加载msdos模块,则必须运行:
$ insmod /lib/modules/2.6.11/kernel/fs/fat/fat.ko
$ insmod /lib/modules/2.6.11/kernel/fs/msdos/msdos.ko

要么:
$ modprobe msdos
所以,insmod要求你传递完整的路径名并以正确的顺序插入模块,而modprobe只取名字,没有任何扩展名,并通过解析/lib找出它需要知道的所有内容/modules/version/modules.dep。

1.3 最简单的模块

http://www.tldp.org/LDP/lkmpg/2.6/html/x121.html
Example 2-1. hello-1.c
#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_INFO */
int init_module(void)
{
printk(KERN_INFO "Hello world 1.\n");
return 0;
}
void cleanup_module(void)
{
printk(KERN_INFO "Goodbye world 1.\n");
}

1.4 模块必要信息

1.4.1 内核模块必须至少包含的头文件:

#include <linux/module.h> /* module_init() module_exit() 函数来注册模块入口和退出处理。
#include <linux/kernel.h> /* Needed for KERN_INFO ,仅用于printk()日志级别的KERN_ALERT的宏扩展 */
#include <linux/init.h> /* Needed for the macros */

1.4.2 内核模块必须至少有两个功能:

  • 一个“开始”(初始化)功能调用 的init_module()当模块insmoded到内核被称为,
  • 以及“结束”(清理)函数调用在cleanup_module()这是刚刚称为在它被破坏之前。
从Linux 2.4开始,您可以重命名模块的init和cleanup功能; 它们不再需要分别被称为 init_module()和cleanup_module()。
这是通过 module_init()和module_exit()宏完成的。这些宏在linux / init.h中定义。
唯一需要注意的是,必须在调用宏之前定义init和cleanup函数,否则会出现编译错误。

1.4.3 printk()日志记录

由于代码运行在内核空间里面,不能直接使用用户空间的 print 函数,而要使用内核中的 printk 函数.
作为日志记录机制,用于记录信息或发出警告。
每个printk() 语句都带有一个优先级,即您看到的<1>和KERN_ALERT。
有8个优先级,内核有宏,所以你不必使用神秘的数字,你可以在linux/kernel.h中查看它们(及其含义)。
如果未指定优先级,则将使用默认优先级DEFAULT_MESSAGE_LOGLEVEL。

花点时间阅读优先级宏。头文件还描述了每个优先级的含义。
在实践中,不要使用数字,如<4>。始终使用宏,如 KERN_WARNING。

如果优先级低于int console_loglevel,则会在当前终端上打印消息。
如果syslogd和klogd都在运行,那么该消息也将附加到/var/log/messages,无论它是否打印到控制台。
我们使用高优先级(如KERN_ALERT)来确保将printk()消息打印到控制台而不是仅记录到日志文件中。
编写实际模块时,您需要使用对当前情况有意义的优先级。

1.4.4 优先级

https://szosoft.blogspot.com/2019/06/linux-journal.html#1021
cat  /usr/lib/modules/5.1.15-arch1-1-ARCH/build/include/linux/kernel.h
cat  /usr/lib/modules/5.1.15-arch1-1-ARCH/build/include/linux/printk.h
内核模块printk no (Key)journal
KERN_EMERG 0 Emergency 紧急
KERN_ALERT 1 Alert 警报
KERN_CRIT 2 Critical 危急
KERN_ERR 3 Error 错误
KERN_WARNING 4 Warning 警告
KERN_NOTICE 5 Notice 注意
KERN_INFO 6 Informational 信息
KERN_DEBUG 7 Debug 调试

1.4.5 许可和模块文档

MODULE_LICENSE("GPL");  /* Get rid of taint message by declaring code as GPL.  */
MODULE_AUTHOR("Tom"); /* Who wrote this module? */
MODULE_DESCRIPTION("Test"); /* What does this module do */
MODULE_VERSION("0.0.1");
MODULE_SUPPORTED_DEVICE("testdevice");   /* 声明模块支持哪些类型的设备。 */
在内核2.4及更高版本中,设计了一种机制来识别在GPL(和朋友)下许可的代码,以便可以警告人们代码是非开源的。
这是通过MODULE_LICENSE()宏实现的。通过将许可证设置为GPL,可以防止打印警告。
此许可证机制在linux/module.h中定义并记录:
$ cat  /usr/lib/modules/5.1.15-arch1-1-ARCH/build/include/linux/module.h |grep MODULE_

/*
 * 目前接受以下许可证标识为免费软件模块
 * "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]
 *
 * 以下其他标识可供选择
 * "Proprietary" [Non free products]
 */

1.5 编译内核模块

http://www.tldp.org/LDP/lkmpg/2.6/html/x181.html
内核模块的编译需要与常规用户空间应用程序略有不同。
以前的内核版本要求我们关注这些设置,这些设置通常存储在Makefile中。虽然按层次结构组织,但许多冗余设置在次级Makefile中累积并使它们变大并且难以维护。
幸运的是,有一种新方法可以做这些事情,称为kbuild,外部可加载模块的构建过程现在完全集成到标准内核构建机制中。
要了解有关如何编译不属于官方内核的模块的更多信息(例如本指南中的所有示例),请参阅文件 linux/Documentation/kbuild/modules.txt.

编译时通过一个 Makefile 文件进行,把这个 Makefile 文件置于 hello.c 同一目录下.
Makefile对格式有要求。每一行文本除非顶头开始,如果需要格式编排,不能使用空格键来控制文本行缩进,必须使用Tab键

1.6 实战

1.6.1 源代码文件: hello.c

/*  hello.c - Demonstrates module documentation. */
#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_INFO */
#include <linux/init.h> /* Needed for the macros */
#define DRIVER_AUTHOR "Peter Jay Salzman <p@dirac.org>"
#define DRIVER_DESC   "A sample driver"

static int __init init_hello(void)
{
printk(KERN_INFO "HelloWorld\n");
return 0;
}

static void __exit cleanup_hello(void)
{
printk(KERN_INFO "GoodbyeWorld\n");
}

module_init(init_hello);
module_exit(cleanup_hello);

MODULE_LICENSE("GPL");
MODULE_AUTHOR(DRIVER_AUTHOR); /* Who wrote this module? */
MODULE_DESCRIPTION(DRIVER_DESC); /* What does this module do */
MODULE_SUPPORTED_DEVICE("testdevice");

1.6.2 Makefile文件: Makefile

obj-m += hello.o

all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

从技术角度来看,第一行确实是必要的,为了方便起见,添加了“全部”和“清洁”目标。
现在您可以通过发出命令make来编译模块。您应该获得类似于以下内容的输出:

1.6.3 使用make编译

$ make
make -C /lib/modules/5.1.15-arch1-1-ARCH/build M=/home/toma/ko modules
make[1]: Entering directory '/usr/lib/modules/5.1.15-arch1-1-ARCH/build'
  CC [M]  /home/toma/ko/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/toma/ko/hello.mod.o
  LD [M]  /home/toma/ko/hello.ko
make[1]: Leaving directory '/usr/lib/modules/5.1.15-arch1-1-ARCH/build'

请注意,内核2.6引入了一种新的文件命名约定:内核模块现在具有.ko 扩展名(代替旧的.o扩展名),可以轻松地将它们与传统的目标文件区分开来。
原因是它们包含一个额外的.modinfo部分,其中保留了有关该模块的其他信息。我们很快就会看到这些信息有什么用处。
有关内核模块的Makefile的更多详细信息,请参见 linux/Documentation/kbuild/makefiles.txt .

$ ls -l   //编译后查看文件列表
name size
Makefile 154   //make文件
hello.c 786      //源文件
hello.ko 4528   //内核文件
hello.mod.c 646
hello.mod.o 2712
hello.o 2664   //目标文件
modules.order 30
Module.symvers 0

1.6.4 使用modinfo查看新的模块文件

$ modinfo hello.ko
filename:       /home/toma/ko/hello.ko
version:        0.0.1
description:    Test
author:         Tom
license:        GPL
srcversion:     BC3C3A49026E0297D738AE7
depends:      
retpoline:      Y
name:           hello
vermagic:       5.1.15-arch1-1-ARCH SMP preempt mod_unload

1,6.5 使用insmod加载内核模块

$ sudo insmod ./hello.ko

1.6.6 使用lsmod查看已加载的模块

$ lsmod |grep hello
Module                  Size  Used by
hello                  16384  0

1.6.7 使用rmmod卸载模块

$ sudo rmmod hello
$ lsmod |grep hello

1.6.8 使用dmesg查看日志

$ dmesg |tail
...
[562050.818179] HelloWorld
[562458.113633] GoodbyeWorld

1.6.9 练习

练习1
请参阅init_module()中 return语句上方的注释 ?将返回值更改为负值,重新编译并再次加载模块。怎么了?
$ sudo insmod ./hello-2.ko
insmod: ERROR: could not insert module ./hello-2.ko: Operation not permitted
$ dmesg |tail
...
[546135.110381] HelloWorld2.

练习2
将代码中许可的部分删除,再编译看看。
$ make
make -C /lib/modules/5.1.15-arch1-1-ARCH/build M=/home/toma/ko modules
make[1]: Entering directory '/usr/lib/modules/5.1.15-arch1-1-ARCH/build'
  CC [M]  /home/toma/ko/hello-1.o
  Building modules, stage 2.
  MODPOST 1 modules
style="color: red; font-size: xx-small;">WARNING: modpost: missing MODULE_LICENSE() in /home/toma/ko/hello-1.o
see include/linux/module.h for more information
  CC      /home/toma/ko/hello-1.mod.o
  LD [M]  /home/toma/ko/hello-1.ko
make[1]: Leaving directory '/usr/lib/modules/5.1.15-arch1-1-ARCH/build'

1.6.10 其他信息

现在看一下linux/drivers/char/Makefile的真实示例。
正如你所看到的,有些东西被硬件连接到内核(obj-y)但是那些obj-m去了哪里?
那些熟悉shell脚本的人很容易发现它们。
对于那些没有的,你看到的obj - $(CONFIG_FOO)条目会扩展为obj-y或obj-m,具体取决于CONFIG_FOO变量是否已设置为y或m。
虽然我们在这里,但那些正是你在linux/.config文件中设置的那种变量,最后一次你说make menuconfig 或类似的东西。

1.7 更多基础示例链接

1.7.1 将命令行参数传递给模块

http://www.tldp.org/LDP/lkmpg/2.6/html/x323.html

1.7.2 跨越多个文件的模块

http://www.tldp.org/LDP/lkmpg/2.6/html/x351.html

1.7.3 构建预编译内核的模块

http://www.tldp.org/LDP/lkmpg/2.6/html/x380.html

1.7.4 与应用程序交互

https://www.oschina.net/translate/writing-a-simple-linux-kernel-module

2 make工具

2.1 DESCRIPTION

make实用程序将自动确定需要重新编译大型程序的哪些部分,并发出命令以重新编译它们。
我们的示例显示了C程序,因为它们非常常见,但您可以使用make与任何编译语言,其编译器可以使用shell命令运行。
实际上,make并不仅限于程序。
您可以使用它来描述任何一些任务,其中某些文件必须在其他文件更改时自动从其他文件更新。

要准备使用make,您必须编写一个名为makefile的文件,该文件描述程序中文件之间的关系,以及用于更新每个文件的命令的状态。
在程序中,通常从目标文件更新可执行文件,而目标文件又通过编译源文件来完成。

一旦存在合适的makefile,每次更改一些源文件时,这个简单的shell命令:
make
足以执行所有必要的重新编译。
make程序使用makefile描述和文件的最后修改时间来决定需要更新哪些文件。
对于每个文件,它会发出makefile中记录的命令。

make执行makefile中的命令以更新一个或多个目标名称,其中name通常是程序。
如果不存在-f选项,make将按顺序查找 makefiles GNUmakefile, makefile, and Makefile, in that order.

(我们建议使用Makefile,因为它突出显示在目录列表的开头附近,紧邻其他重要文件,如README。)
检查的第一个名称,建议不要将GNUmakefile用于大多数makefile。
如果您具有特定于GNU make的makefile,则应使用此名称,并且其他版本的make不会理解该名称。
如果makefile为' - ',则读取标准输入。

2.2 退出状态

如果所有的makefile都被成功解析并且没有构建的目标失败,则GNU make退出状态为零。
如果使用-q标志并且make确定需要重建目标,则将返回状态1。
如果遇到任何错误,将返回状态2。

2.3 选项

-b, -m Ignored for compatibility. 忽略兼容性.
-B, --always-make Unconditionally make all targets. 无条件地制定所有目标.
-C DIRECTORY, --directory=DIRECTORY Change to DIRECTORY before doing anything. 在做任何事之前改为DIRECTORY.
-d Print lots of debugging information. 打印大量调试信息.
--debug[=FLAGS] Print various types of debugging information.  FLAGS可以用于所有调试输出(与使用-d相同),
b用于基本调试,v用于更详细的基本调试,i用于显示隐式规则,
j用于调用命令的详细信息,m用于在重新生成makefile时进行调试.
 使用n禁用所有先前的调试标志.
-e, --environment-overrides Environment variables override makefiles. 环境变量覆盖makefile.
--eval=STRING Evaluate STRING as a makefile statement. 将STRING评估为makefile语句.
-f FILE, --file=FILE, --makefile=FILE Read FILE as a makefile. 将FILE作为makefile读取.
-h, --help Print this message and exit. 打印此消息并退出.
-i, --ignore-errors Ignore errors from recipes.  忽略为重制文件而执行的命令中的所有错误.
-I DIRECTORY, --include-dir=DIRECTORY Search DIRECTORY for included makefiles. 搜索DIRECTORY以获取包含的makefile.
-j [N], --jobs[=N] Allow N jobs at once; infinite jobs with no arg. 一次允许N个工作;没有arg的无限工作.
-k, --keep-going Keep going when some targets can't be made. 当一些目标无法制作时继续前进.
-l [N], --load-average[=N],--max-load[=N] Don't start multiple jobs unless load is below N. 除非负载低于N,否则不要启动多个作业.
-L, --check-symlink-times Use the latest mtime between symlinks and target. 使用符号链接和目标之间的最新mtime.
-n, --just-print, --dry-run, --recon Don't actually run any recipe; just print them. 实际上不要运行任何配方;只需打印它们.
-o FILE, --old-file=FILE, --assume-old=FILE Consider FILE to be very old and don't remake it. 即使FILE很老,也不要重新生成它.
-O[TYPE], --output-sync[=TYPE] Synchronize output of parallel jobs by TYPE. 按TYPE同步并行作业的输出.
 当与-j并行运行多个作业时,请确保将每个作业的输出收集在一起,而不是穿插其他作业的输出.
 如果未指定type或是target,则将每个目标的整个配方的输出组合在一起.
 如果type为line,则配方中每个命令行的输出将组合在一起.
 如果type是recurse,则整个递归make的输出被组合在一起.
如果type为none,则禁用输出同步.
-p, --print-data-base Print make's internal database.  打印通过读取makefile产生的数据库(规则和变量值);
然后像往常一样或以其他方式指定执行.
要打印数据库而不尝试重新创建任何文件,请使用: make -p -f/dev/null.
-q, --question Run no recipe; exit status says if up to date. ``问题模式''.不要运行任何命令,也不要打印任何东西;如果指定的目标已经是最新的,则返回退出状态为零,否则返回非零值.
-r, --no-builtin-rules Disable the built-in implicit rules. 禁用内置隐式规则.
-R, --no-builtin-variables Disable the built-in variable settings. 禁用内置变量设置.
-s, --silent, --quiet Don't echo recipes.  无声操作;不要在执行时打印命令.
-S, --no-keep-going, --stop Turns off -k.  取消-k选项的效果.这是永远不必要的,
除了在递归make中,-k可能通过MAKEFLAGS从顶级make继承,
或者如果你在环境中的MAKEFLAGS中设置-k.
-t, --touch Touch targets instead of remaking them. 触摸目标而不是重新制作它们.
 这用于假装命令已完成,以欺骗未来的make调用.
--trace Print tracing information. 打印跟踪信息.
-v, --version Print the version number of make and exit. 打印make和exit的版本号.
-w, --print-directory Print the current directory. 打印当前目录.
--no-print-directory Turn off -w, even if it was turned on implicitly. 关闭-w,即使它是隐式打开的.
-W FILE, --what-if=FILE, --new-file=FILE, --assume-new=FILE Consider FILE to be infinitely new. 认为FILE是无限新的.
 没有-n,它几乎与在运行make之前在给定文件上运行touch命令相同,只是修改时间仅在make的想象中改变.
--warn-undefined-variables Warn when an undefined variable is referenced. 引用未定义的变量时发出警告.

3. 其他内核编译方法及相关连接

3.1 编译Arch的内核模块

https://wiki.archlinux.org/index.php/Compile_kernel_module
首先,您需要安装构建依赖项,例如compiler(base-devel)和linux-headers。

3.2 Arch构建系统

https://wiki.archlinux.org/index.php/Kernel/Arch_Build_System
https://www.kernel.org/doc/Documentation/kbuild/kconfig.txt
内核/Arch构建系统
该拱门构建系统可以用来构建基于官方的自定义内核的Linux软件包。
这种编译方法可以自动化整个过程,并且基于经过良好测试的软件包。
您可以编辑PKGBUILD以使用自定义内核配置或添加其他修补程序。
$ pacman -Ss asp
extra/asp 5-1    Arch Linux build source file management tool
安装的ASP封装和基devel的包组。
您需要一个干净的内核来开始自定义。通过运行以下命令从ABS获取最新的内核包文件到您的构建目录:
$ asp update linux
$ asp checkout linux
然后,从各自的源获取您需要的任何其他文件(例如,自定义配置文件,修补程序等)。

3.3 内核/传统编译

https://wiki.archlinux.org/index.php/Kernel/Traditional_compilation#Download_the_kernel_source
安装核心包
安装base-devel软件包组,其中包含必要的软件包,例如make和gcc。
还建议安装以下软件包,如默认的Arch内核PKGBUILD中所列:xmlto,kmod,inetutils,bc,libelf,git

3.4 修补包

https://wiki.archlinux.org/index.php/Patching_packages#Applying_patches
本文介绍如何创建以及如何在Arch Build System(ABS)中将补丁应用于包。
一个补丁描述了一组针对一个或多个文件线路的变化。补丁通常用于自动更改源代码。

3.5 模块与程序

http://www.tldp.org/LDP/lkmpg/2.6/html/x427.html
程序 模块
用户空间      内核空间
printf() printk()
main()开始 init_module或 通过module_init调用指定的函数 开始
  cleanup_module或 使用module_exit调用指定的函数 结束