如何编写一个简单的 Linux 内核模块

12-05 20:06

抢占 Gloden Ring-0

Linux为应用程序提供了强大且可扩展的API,但有时那并不够。在系统中和硬件交互或者执行需要访问保密信息的操作时需要一个核心模块。

Linux内核是一组编译后的二进制码,它可以直接嵌入 Linux内核,在Ring0上运行,是在x86 - 64处理器中执行的最低和最小保护环。这里的代码完全不受控制,但运行速度惊人而且能够访问系统中的所有内容。

不仅仅是为了人类

编写Linux内核不适合胆小鬼。更改内核,您将可能面临数据丢失和系统损坏的风险。内核编码没有往常的Linux应用程序所习惯的安全保障。如果你有一个错误,他将锁定整个系统。

更糟糕的是,你的问题可能不会显现出来。运行失败的最佳的状况是模块在加载是立刻被锁定。当您向模块中添加更多代码时,您就面临这循环时空和内存泄漏的风险。如果你不仔细,这些风险将随着机器运行而一直增长。最终,重要的内存结构甚至缓冲区都可能被覆盖。

传统的应用程序开发模式将要被大量丢弃。除了模块的加载和卸载之外,您还将编写系统 响应 事件而不是执行顺序模式运行的代码。随着内核的发展,你将要编写的是APIs,而不是应用程序本身。

您还没有访问标准库的权限。而内核提供的一些功能如printk(作为一种printf的 替代 )和kmalloc(其工作方式类似于malloc),大部分都由你自己决定。另外,当你的模块被卸载时,您要负责将您自己的模块清理干净。这没有垃圾回收机制。

前提

在我们开始之前,要确保用的是正确的工作工具。更重要的是,你需要一台Linux系统的机器。我知道那完全是一个意外!虽然任意的Linux的版本都可以,在例子中我将要使用 Ubuntu 16.04 LTS,如果您使用其他的版本,可能需要稍微调整安装命令。

其次,您需要一个独立的运行环境或者虚拟机。我更偏向在虚拟机中工作,但这完全取决于您。我不建议您使用您当前的机器,因为当你出现错误时数据可能会丢失。我是说什么时候,而不是如果,因为在这个过程中你肯定会锁定您的机器几次。当内核极度繁忙的时候,你的最新代码会存放在缓冲区,因此,您的源文件可能会损坏。而在虚拟机中进行这种测试可以消除这种风险。

最后,你需要知道一点C的知识。C++运行库内核太大,所以写裸机的C是必不可少的。为了与硬件进行交互。了解一些程序集可能会有所帮助。

安装开发环境

在 Ubuntu 中我们需要运行:

apt-get install build-essential linux-headers-`uname -r`

这个命令会安装本示例所需要的开发工具和核心头文件。

下面的示例假定你是一个普通用户而不是 root 用户,但是你有 sudo 权限。sudo 是用于加载核心模型的,但我们希望尽可能地不使用 root 用户。

我们开始写代码之前,先准备环境:

mkdir ~/src/lkm_example
cd ~/src/lkm_example

打开你喜欢的编辑器(我使用 VIM),创建文件 lkm_example.c,输入下面的内容:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“Robert W. Oliver II”);
MODULE_DESCRIPTION(“A simple example Linux module.”);
MODULE_VERSION(“0.01”);
static int __init lkm_example_init(void) {
 printk(KERN_INFO “Hello, World!\n”);
 return 0;
}
static void __exit lkm_example_exit(void) {
 printk(KERN_INFO “Goodbye, World!\n”);
}
module_init(lkm_example_init);
module_exit(lkm_example_exit);

我们已经构造了一个极尽简单的模块,现在我们举例来详细说明各个重要部分:

  • “includes” 涵盖了 Linux 内核开发所需要的头文件。

  • MODULE_LICENSE 可用于设置表示模块许可的值。如果想知道完整的许可列表,可以运行:  grep “MODULE_LICENSE” -B 27 /usr/src/linux-headers-`uname -r`/include/linux/module.h

  • 我们定义了静态的初始化(加载)和退出(卸载)函数,它们都返回整数。

  • 注意我们用的 printk 而不是 printf。printk 与 printf 的参数有所不同。比如 KERN_INFO 声明了当前日志行的优先级,声明中没有使用逗号。内核在 printk 内部整理这些值以节省栈空间。

  • 在文件的最后,我们调用 module_init 和 module_exit 来告诉内核哪个是加载函数,哪个是卸载函数。这样我们可以按自己喜好对函数自由命名。

但是我们还不能编译这个文件。我们需要制造一个文件。这个基础实例现在要起作用了。注意,make对于空格键和 tab 键是非常苛刻的,所以要保证的适当的地方用 tabs 键代替 Tab 键。

obj-m += lkm_example.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"时,它应该可以成功的编译你的模块。这个文件的编译结果是“lkm_example.ko”。如果你发现任何错误,请检查你的示例文件中的引用是否是正确的,并且检查有没有不小心粘贴了UTF-8的字符。

现在我们来添加这个模块进行测试。运行操作:

sudo insmod lkm_example.ko

如果运行成功,你还看不到任何结果。printk 函数不会输出在控制台而是输出在内核日志。为了看到日志,我们运行一下命令:

sudo dmesg

你将会看到“Hello,World!”前边有一个时间戳。这意味着内核模块加载并成功的将结果打印在内核日志。我们还可以检查模块是否仍然被加载:

lsmod | grep “lkm_example”

要想移除该模块,运行一下命令:

sudo rmmod lkm_example

如果你再一次运行dmesg,你将会在日志中看到“Goodbye,World!”。你可以再一次运行lsmod来确认模块是否已卸载。

正如你所看到,这个测试工作流程有点乏味,为了让其进行自动化测试,在Makefile结尾添加以下代码:

test:
 sudo dmesg -C
 sudo insmod lkm_example.ko
 sudo rmmod lkm_example.ko
 dmesg

然后运行以下命令:

make test

不必再运行单行命令来测试我们的模块和在内核日志看输出。

现在我们有了一个功能全面但又非常琐碎的内核模块!

原文链接:https://www.oschina.net/translate/writing-a-simple-linux-kernel-module?utm_source=tuicool&utm_medium=referral
标签: Linux
© 2014 TuiCode, Inc.