现在的位置: 首页 > 综合 > 正文

The Linux Kernel Module Programming Guide(1)

2018年04月13日 ⁄ 综合 ⁄ 共 15402字 ⁄ 字号 评论关闭

Chapter 1. Introduction

1.1. 什么是内核模块?

现在,你是不是想编写内核模块。你应该懂得C语言,写过一些用户程序, 那么现在你将要见识一些真实的东西。在这里,你会看到一个野蛮的指针是如何 毁掉你的文件系统的,一次内核崩溃意味着重启动。

什么是内核模块?内核模块是一些可以让操作系统内核在需要时载入和执 行的代码,这同样意味着它可以在不需要时有操作系统卸载它们扩展了操作系 统内核的功能却不需要重新启动系统。举例子来说,其中一种内核模块时设备驱 动程序模块,它们用来让操作系统正确识别,使用安装在系统上的硬件设备。如 果没有内核模块,我们不得不一次又一次重新编译生成单内核操作系统的内核镜 像来加入新的功能。这还意味着一个臃肿的内核。


1.2. 内核模块是如何被调入内核工作的?

你可以通过执行lsmod命令来查看内核已经加载了哪 些内核模块, 该命令通过读取/proc/modules文件的内容 来获得所需信息。

这些内核模块是如何被调入内核的?当操作系统内核需要的扩展功能不存 在时,内核模块管理守护进程kmod[1]执行modprobe去加载内核模 块。两种类型的参数被传递给modprobe:

 

  • 一个内核模块的名字像softdog或是ppp

  • 通用识别符像char-major-10-30

当传递给modprobe是通用识别符时,modprobe首先在文件 /etc/modules.conf查找该字符串。如果它发现的一行别名像:

alias char-major-10-30 softdog
	

它就明白通用识别符是指向内核模块softdog.o

然后,modprobe遍历文件/lib/modules/version/modules.dep 来判断是否有其它内核模块需要在该模块加载前被加载。该文件是由命令depmod -a 建立,保存着内核模块的依赖关系。举例来说,msdos.o依赖于模块fat.o 内核模块已经被内核载入。当要加载的内核模块需要使用别的模块提供的符号链接时(多半是变量或函数), 那么那些提供这些所需符号链接的内核模块就被该模块所依赖。

最终,modprobe调用insmod先加载被依赖的模块,然后加载该被内核要求的模块modprobe将insmod指向 /lib/modules/version/[2]目录,该目录为默认标准存放内核模块的目录。insmod对内核模块存放位置 的处理相当呆板,所以modprobe应该很清楚的知道默认标准的内核模块存放的位置。所以,当你想要载入一个内 核模块时,你可以执行:

insmod /lib/modules/2.5.1/kernel/fs/fat/fat.o
insmod /lib/modules/2.5.1/kernel/fs/msdos/msdos.o
	

或只是执行"modprobe -a msdos"。

Linux提供modprobe, insmod and depmod在一个名为modutils 或 mod-utils的工具包内。

在结束本章前,让我们来看一个 /etc/modules.conf文件:

#This file is automatically generated by update-modules
path[misc]=/lib/modules/2.4.?/local
keep
path[net]=~p/mymodules
options mydriver irq=10
alias eth0 eepro
	

用'#'起始的行为注释。空白行被忽略。

path[misc]起始的行告诉modprobe用 /lib/modules/2.4.?/local替代搜寻 misc内核模块的路径。正如你看到的,命令解释器shell的元字符也可以使用。

path[net]起始的行告诉modprobe 在目录 ~p/mymodules搜索网络方面的内核模块。但是,在path[net] 指令之前使用的"keep" 指令告诉modprobe只是将该路径添加到标准搜索路径中,而不是像对待 misc前面那样进行替换。

以alias 起始的的行使modprobe加载eepro.o当kmod 以通用识别符'eth0' 要求加载相应内核模块时。

你不会发现像"alias block-major-2 floppy"这样的别名行在文件/etc/modules.conf 因为modprobe已经知道在绝大多数系统上安装的标准的设备的驱动模块

现在你已经知道内核模块是如何被调入的了。当你想写你自己的依赖于其它模块的内核模块时, 还有一些内容没有提供。这个相对高级的问题将在以后的章节中介绍,当我们已经完成前面的学习后。


1.2.1. 在开始前

在我们介绍源代码前,有一些事需要注意。系统彼此之间的不同会导致许多困难。 顺利的编译并且加载你的第一个"hello world"模块有时就会比较困难。但是当你跨过 这道坎时,后面会顺利的多。


1.2.1.1. 内核模块和内核的版本问题

为某个版本编译的模块将不能被另一个版本的内核加载如果内核中打开了 CONFIG_MODVERSIONS选项。我们暂时不会讨论与此相关的 内容。在我们进入相关内容前,本文档中的范例可能在该选项打开的情况下无法 工作。但是,目前绝大多数的发行版是将该选项打开的。所以如果你遇到和版本 相关的错误时,最好,重新编译一个关闭该选项的内核。


1.2.1.2. 使用 X带来的问题

强烈建议你在控制台下输入文档中的范例代码,编译然后加载模块,而不是在X下。

模块不能像printf()那样输出到屏幕,但它们可以 记录信息和警告,当且仅当你在使用控制台时这些信息才能最终显示在屏幕上。 如果你从xterm中insmod一个模块,这些日志信息只会记录在你的日志文件中。 除了查看日志文件你将无法 得到输出信息。想要及时的获得这些日志信息,建议 所有的工作都在控制台下进行。


1.2.1.3. 编译相关和内核版本相关的问题

Linux的发行版经常给内核打一些非标准的补丁,这种情况回导致一些问题的发生。

一个更普遍的问题是一些Linux发行版提供的头文件不完整。编译模块时你将需要非常多 的内核头文件。墨菲法则之一就是那些缺少的头文件恰恰是你最需要的。

我强烈建议从Linux镜像站点下载源代码包,编译新内核并用新内核启动系统来避免以上 的问题。参阅"Linux Kernel HOWTO"获得详细内容。

具有讽刺意味的是,这也会导致一些问题。gcc倾向于在缺省的内核源文件路径(通常是/usr/src/)下寻找源代码文件。这可以通过gcc的-I 选项来切换。


Chapter 2. Hello World

2.1. Hello, World (part 1): 最简单的内核模块

当第一个洞穴程序员在第一台洞穴计算机的墙上上凿写第一个程序时, 这是一个在羚羊皮上输出`Hello, world'的字符串。罗马的编程书籍上是以 `Salut, Mundi'这样的程序开始的。 我不明白人们为什么要破坏这个传统, 但我认为还是不明白为好。我们将从编写一系列的`Hello, world'模块开始, 一步步展示编写内核模块的基础的方方面面。

这可能是一个最简单的模块了。先别急着编译它。我们将在下章模块编译的章节介绍相关内容。

Example 2-1. hello-1.c

/*  
 *  hello-1.c - The simplest kernel module.
 */
#include <linux/module.h>	/* Needed by all modules */
#include <linux/kernel.h>	/* Needed for KERN_ALERT */

int init_module(void)
{
	printk("<1>Hello world 1./n");

	/* 
	 * A non 0 return means init_module failed; module can't be loaded. 
	 */
	return 0;
}

void cleanup_module(void)
{
	printk(KERN_ALERT "Goodbye world 1./n");
}

一个内核模块应该至少包含两个函数。一个“开始”(初始化)的函数被称为init_module() 还有一个“结束” (干一些收尾清理的工作)的函数被称为cleanup_module() ,当内核模块被rmmod卸载时被执行。实际上,从内核版本2.3.13开始这种情况有些改变。 你可以为你的开始和结束函数起任意的名字。 你将在以后学习如何实现这一点Section 2.3。 实际上,这个新方法时推荐的实现方法。但是,许多人仍然使init_module()cleanup_module()作为他们的开始和结束函数。

一般,init_module()要么向内核注册它可以处理的事物,要么用自己的代码 替代某个内核函数(代码通常这样做然后再去调用原先的函数代码)。函数 cleanup_module()应该撤消任何init_module()做的事,从而 内核模块可以被安全的卸载

最后,任一个内核模块需要包含linux/module.h。 我们仅仅需要包含 linux/kernel.h当需要使用 printk()记录级别的宏扩展时KERN_ALERT,相关内容将在Section 2.1.1中介绍。


2.1.1. 介绍printk()

不管你可能怎么想,printk()并不是设计用来同用户交互的,虽然我们在 hello-1就是出于这样的目的使用它!它实际上是为内核提供日志功能, 记录内核信息或用来给出警告。因此,每个printk() 声明都会带一个优先级,就像你看到的<1>KERN_ALERT 那样。内核总共定义了八个优先级的宏, 所以你不必使用晦涩的数字代码,并且你可以从文件 linux/kernel.h查看这些宏和它们的意义。如果你 不指明优先级,默认的优先级DEFAULT_MESSAGE_LOGLEVEL将被采用。

阅读一下这些优先级的宏。头文件同时也描述了每个优先级的意义。在实际中, 使用宏而不要使用数字,就像<4>。总是使用宏,就像 KERN_WARNING

当优先级低于int console_loglevel,信息将直接打印在你的终端上。如果同时 syslogdklogd都在运行,信息也同时添加在文件 /var/log/messages,而不管是否显示在控制台上与否。我们使用像 KERN_ALERT这样的高优先级,来确保printk()将信息输出到 控制台而不是只是添加到日志文件中。 当你编写真正的实用的模块时,你应该针对可能遇到的情况使用合 适的优先级。


2.2. 编译内核模块

内核模块在用gcc编译时需要使用特定的参数。另外,一些宏同样需要定义。 这是因为在编译成可执行文件和内核模块时, 内核头文件起的作用是不同的。 以往的内核版本需要我们去在Makefile中手动设置这些设定。尽管这些Makefile是按目录分层次 安排的,但是这其中有许多多余的重复并导致代码树大而难以维护。 幸运的是,一种称为kbuild的新方法被引入,现在外部的可加载内核模块的编译的方法已经同内核编译统一起来。想了解更多的编 译非内核代码树中的模块(就像我们将要编写的)请参考帮助文件linux/Documentation/kbuild/modules.txt

现在让我们看一个编译名字叫做hello-1.c的模块的简单的Makefile文件:

Example 2-2. 一个基本的Makefile

obj-m += hello-1.o

现在你可以通过执行命令 make -C /usr/src/linux-`uname -r` SUBDIRS=$PWD modules 编译模块。 你应该得到同下面类似的屏幕输出:

[root@pcsenonsrv test_module]# make -C /usr/src/linux-`uname -r` SUBDIRS=$PWD modules
make: Entering directory `/usr/src/linux-2.6.x
  CC [M]  /root/test_module/hello-1.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test_module/hello-1.mod.o
  LD [M]  /root/test_module/hello-1.ko
make: Leaving directory `/usr/src/linux-2.6.x
	

请注意2.6的内核现在引入一种新的内核模块命名规范:内核模块现在使用.ko的文件后缀(代替 以往的.o后缀),这样内核模块就可以同普通的目标文件区别开。更详细的文档请参考 linux/Documentation/kbuild/makefiles.txt。在研究Makefile之前请确认你已经参考了这些文档。

现在是使用insmod ./hello-1.ko命令加载该模块的时候了(忽略任何你看到的关于内核污染的输出 显示,我们将在以后介绍相关内容)。

所有已经被加载的内核模块都罗列在文件/proc/modules中。cat一下这个文件看一下你的模块是否真的 成为内核的一部分了。如果是,祝贺你!你现在已经是内核模块的作者了。当你的新鲜劲过去后,使用命令 rmmod hello-1.卸载模块。再看一下/var/log/messages文件的内容是否有相关的日志内容。

这儿是另一个练习。看到了在声明 init_module()上的注释吗? 改变返回值非零,重新编译再加载,发生了什么?


2.3. Hello World (part 2)

在内核Linux 2.4中,你可以为你的模块的“开始”和“结束”函数起任意的名字。它们不再必须使用 init_module()cleanup_module()的名字。这可以通过宏 module_init()module_exit()实现。这些宏在头文件linux/init.h定义。唯一需要注意的地方是函数必须在宏的使用前定义,否则会有编译 错误。下面就是一个例子。

Example 2-3. hello-2.c

/*  
 *  hello-2.c - Demonstrating the module_init() and module_exit() macros.
 *  This is preferred over using init_module() and cleanup_module().
 */
#include <linux/module.h>	/* Needed by all modules */
#include <linux/kernel.h>	/* Needed for KERN_ALERT */
#include <linux/init.h>		/* Needed for the macros */

static int __init hello_2_init(void)
{
	printk(KERN_ALERT "Hello, world 2/n");
	return 0;
}

static void __exit hello_2_exit(void)
{
	printk(KERN_ALERT "Goodbye, world 2/n");
}

module_init(hello_2_init);
module_exit(hello_2_exit);

现在我们已经写过两个真正的模块了。添加编译另一个模块的选项十分简单,如下:

Example 2-4. 两个内核模块使用的Makefile

obj-m += hello-1.o
obj-m += hello-2.o

现在让我们来研究一下linux/drivers/char/Makefile这个实际中的例子。就如同你看到的, 一些被编译进内核 (obj-y),但是这些obj-m哪里去了呢?对于熟悉shell脚本的人这不难理解。这些在Makefile中随处可见 的obj-$(CONFIG_FOO)的指令将会在CONFIG_FOO被设置后扩展为你熟悉的obj-y或obj-m。这其实就是你在使用 make menuconfig编译内核时生成的linux/.config中设置的东西。


2.4. Hello World (part 3): 关于__init__exit

这里展示了内核2.2以后引入的一个新特性。注意在负责“初始化”和“清理收尾”的函数定义处的变化。宏 __init的使用会在初始化完成后丢弃该函数并收回所占内存,如果该模块被编译进内核,而不是动态加载。

__initdata__init 类似,只不过对变量有效。

__exit将忽略“清理收尾”的函数如果该模块被编译进内核。同宏 __exit一样,对动态加载模块是无效的。这很容易理解。编译进内核的模块 是没有清理收尾工作的, 而动态加载的却需要自己完成这些工作。

这些宏在头文件linux/init.h定义,用来释放内核占用的内存。 当你在启动时看到这样的Freeing unused kernel memory: 236k freed内核输出,上面的 那些正是内核所释放的。

Example 2-5. hello-3.c

/*  
 *  hello-3.c - Illustrating the __init, __initdata and __exit macros.
 */
#include <linux/module.h>	/* Needed by all modules */
#include <linux/kernel.h>	/* Needed for KERN_ALERT */
#include <linux/init.h>		/* Needed for the macros */

static int hello3_data __initdata = 3;

static int __init hello_3_init(void)
{
	printk(KERN_ALERT "Hello, world %d/n", hello3_data);
	return 0;
}

static void __exit hello_3_exit(void)
{
	printk(KERN_ALERT "Goodbye, world 3/n");
}

module_init(hello_3_init);
module_exit(hello_3_exit);


2.5. Hello World (part 4): 内核模块证书和内核模块文档说明

如果你在使用2.4或更新的内核,当你加载你的模块时,你也许注意到了这些输出信息:

# insmod hello-3.o
Warning: loading hello-3.o will taint the kernel: no license
  See http://www.tux.org/lkml/#export-tainted for information about tainted modules
Hello, world 3
Module hello-3 loaded, with warnings
	

在2.4或更新的内核中,一种识别代码是否在GPL许可下发布的机制被引入, 因此人们可以在使用非公开的源代码产品时得到警告。这通过在下一章展示的宏 MODULE_LICENSE()当你设置在GPL证书下发布你的代码时, 你可以取消这些警告。这种证书机制在头文件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 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
 */

类似的,宏MODULE_DESCRIPTION()用来描述模块的用途。 宏MODULE_AUTHOR()用来声明模块的作者。宏MODULE_SUPPORTED_DEVICE() 声明模块支持的设备。

这些宏都在头文件linux/module.h定义, 并且内核本身并不使用这些宏。它们只是用来提供识别信息,可用工具程序像objdump查看。 作为一个练习,使用grep从目录linux/drivers看一看这些模块的作者是如何 为他们的模块提供识别信息和档案的。

Example 2-6. hello-4.c

/*  
 *  hello-4.c - Demonstrates module documentation.
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#define DRIVER_AUTHOR "Peter Jay Salzman <p@dirac.org>"
#define DRIVER_DESC   "A sample driver"

static int __init init_hello_4(void)
{
	printk(KERN_ALERT "Hello, world 4/n");
	return 0;
}

static void __exit cleanup_hello_4(void)
{
	printk(KERN_ALERT "Goodbye, world 4/n");
}

module_init(init_hello_4);
module_exit(cleanup_hello_4);

/*  
 *  You can use strings, like this:
 */

/* 
 * Get rid of taint message by declaring code as GPL. 
 */
MODULE_LICENSE("GPL");

/*
 * Or with defines, like this:
 */
MODULE_AUTHOR(DRIVER_AUTHOR);	/* Who wrote this module? */
MODULE_DESCRIPTION(DRIVER_DESC);	/* What does this module do */

/*  
 *  This module uses /dev/testdevice.  The MODULE_SUPPORTED_DEVICE macro might
 *  be used in the future to help automatic configuration of modules, but is 
 *  currently unused other than for documentation purposes.
 */
MODULE_SUPPORTED_DEVICE("testdevice");


2.6. 从命令行传递参数给内核模块

模块也可以从命令行获取参数。但不是通过以前你习惯的argc/argv

要传递参数给模块,首先将获取参数值的变量声明为全局变量。然后使用宏MODULE_PARM()(在头文件linux/module.h)。运行时,insmod将给变量赋予命令行的参数,如同 ./insmod mymodule.o myvariable=5。为使代码清晰,变量的声明和宏都应该放在 模块代码的开始部分。以下的代码范例也许将比我公认差劲的解说更好。

MODULE_PARM()需要两个参数,变量的名字和其类型。支持的类型有" b": 比特型,"h": 短整型, "i": 整数型," l: 长整型和 "s": 字符串型,其中正数型既可为signed也可为unsigned。 字符串类型应该声明为"char *"这样insmod就可以为它们分配内存空间。你应该总是为你的变量赋初值。 这是内核编程,代码要编写的十分谨慎。举个例子:

int myint = 3;
char *mystr;

MODULE_PARM(myint, "i");
MODULE_PARM(mystr, "s");
	

数组同样被支持。在宏MODULE_PARM中在类型符号前面的整型值意味着一个指定了最大长度的数组。 用'-'隔开的两个数字则分别意味着最小和最大长度。下面的例子中,就声明了一个最小长度为2,最大长度为4的整形数组。

int myshortArray[4];
MODULE_PARM (myintArray, "3-9i");
	

将初始值设为缺省使用的IO端口或IO内存是一个不错的作法。如果这些变量有缺省值,则可以进行自动设备检测, 否则保持当前设置的值。我们将在后续章节解释清楚相关内容。在这里我只是演示如何向一个模块传递参数。

最后,还有这样一个宏,MODULE_PARM_DESC()被用来注解该模块可以接收的参数。该宏 两个参数:变量名和一个格式自由的对该变量的描述。

Example 2-7. hello-5.c

/*
 *  hello-5.c - Demonstrates command line argument passing to a module.
 */
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/stat.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Peter Jay Salzman");

static short int myshort = 1;
static int myint = 420;
static long int mylong = 9999;
static char *mystring = "blah";

/* 
 * module_param(foo, int, 0000)
 * The first param is the parameters name
 * The second param is it's data type
 * The final argument is the permissions bits, 
 * for exposing parameters in sysfs (if non-zero) at a later stage.
 */

module_param(myshort, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
MODULE_PARM_DESC(myshort, "A short integer");
module_param(myint, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
MODULE_PARM_DESC(myint, "An integer");
module_param(mylong, long, S_IRUSR);
MODULE_PARM_DESC(mylong, "A long integer");
module_param(mystring, charp, 0000);
MODULE_PARM_DESC(mystring, "A character string");

static int __init hello_5_init(void)
{
	printk(KERN_ALERT "Hello, world 5/n=============/n");
	printk(KERN_ALERT "myshort is a short integer: %hd/n", myshort);
	printk(KERN_ALERT "myint is an integer: %d/n", myint);
	printk(KERN_ALERT "mylong is a long integer: %ld/n", mylong);
	printk(KERN_ALERT "mystring is a string: %s/n", mystring);
	return 0;
}

static void __exit hello_5_exit(void)
{
	printk(KERN_ALERT "Goodbye, world 5/n");
}

module_init(hello_5_init);
module_exit(hello_5_exit);

我建议用下面的方法实验你的模块:

satan# insmod hello-5.o mystring="bebop" mybyte=255 myintArray=-1
mybyte is an 8 bit integer: 255
myshort is a short integer: 1
myint is an integer: 20
mylong is a long integer: 9999
mystring is a string: bebop
myintArray is -1 and 420

satan# rmmod hello-5
Goodbye, world 5

satan# insmod hello-5.o mystring="supercalifragilisticexpialidocious" /
> mybyte=256 myintArray=-1,-1
mybyte is an 8 bit integer: 0
myshort is a short integer: 1
myint is an integer: 20
mylong is a long integer: 9999
mystring is a string: supercalifragilisticexpialidocious
myintArray is -1 and -1

satan# rmmod hello-5
Goodbye, world 5

satan# insmod hello-5.o mylong=hello
hello-5.o: invalid argument syntax for mylong: 'h'


2.7. 由多个文件构成的内核模块

有时将模块的源代码分为几个文件是一个明智的选择。在这种情况下,你需要:

 

  1. 只要在一个源文件中添加#define __NO_VERSION__预处理命令。 这很重要因为module.h通常包含 kernel_version的定义,此时一个存储着内核版本的全局变量将会被编译。但如果此时你又要包含头文件 version.h,你必须手动包含它,因为 module.h不会再包含它如果打开预处理选项__NO_VERSION__

  2. 像通常一样编译。

  3. 将所有的目标文件连接为一个文件。在x86平台下,使用命令ld -m elf_i386 -r -o <module name.o> <1st src file.o> <2nd src file.o>

此时Makefile一如既往会帮我们完成编译和连接的脏活。

这里是这样的一个模块范例。

Example 2-8. start.c

/*
 *  start.c - Illustration of multi filed modules
 */

#include <linux/kernel.h>	/* We're doing kernel work */
#include <linux/module.h>	/* Specifically, a module */

int init_module(void)
{
	printk("Hello, world - this is the kernel speaking/n");
	return 0;
}

另一个文件:

Example 2-9. stop.c

/*
 *  stop.c - Illustration of multi filed modules
 */

#include <linux/kernel.h>	/* We're doing kernel work */
#include <linux/module.h>	/* Specifically, a module  */

void cleanup_module()
{
	printk("<1>Short is the life of a kernel module/n");
}

最后是该模块的Makefile:

Example 2-10. Makefile

obj-m += hello-1.o
obj-m += hello-2.o
obj-m += hello-3.o
obj-m += hello-4.o
obj-m += hello-5.o
obj-m += startstop.o
startstop-objs := start.o stop.o


2.8. 为已编译的内核编译模块

很显然,我们强烈推荐你编译一个新的内核,这样你就可以打开内核中一些有用的排错功能,像强制卸载模块(MODULE_FORCE_UNLOAD): 当该选项被打开时,你可以rmmod -f module强制内核卸载一个模块,即使内核认为这是不安全的。该选项可以为你节省不少开发时间。

但是,你仍然有许多使用一个正在运行中的已编译的内核的理由。例如,你没有编译和安装新内核的权限,或者你不希望重启你的机器来运行新内核。 如果你可以毫无阻碍的编译和使用一个新的内核,你可以跳过剩下的内容,权当是一个脚注。

如果你仅仅是安装了一个新的内核代码树并用它来编译你的模块,当你加载你的模块时,你很可能会得到下面的错误提示:

insmod: error inserting 'poet_atkm.ko': -1 Invalid module format
	

一些不那么神秘的信息被纪录在文件/var/log/messages中;

Jun  4 22:07:54 localhost kernel: poet_atkm: version magic '2.6.5-1.358custom 686 
REGPARM 4KSTACKS gcc-3.3' should be '2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3'
	

换句话说,内核拒绝加载你的模块因为记载版本号的字符串不符(更确切的说是版本印戳)。版本印戳作为一个静态的字符串存在于内核模块中,以 vermagic:。 版本信息是在连接阶段从文件init/vermagic.o中获得的。 查看版本印戳和其它在模块中的一些字符信息,可以使用下面的命令 modinfo module.ko

[root@pcsenonsrv 02-HelloWorld]# modinfo hello-4.ko 
license:        GPL
author:         Peter Jay Salzman <p@dirac.org>
description:    A sample driver
vermagic:       2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3
depends:        
	

我们可以借助选项--force-vermagic解决该问题,但这种方法有潜在的危险,所以在成熟的模块中也是不可接受的。 解决方法是我们构建一个同我们预先编译好的内核完全相同的编译环境。如何具体实现将是该章后面的内容。

首先,准备同你目前的内核版本完全一致的内核代码树。然后,找到你的当前内核的编译配置文件。通常它可以在路径 /boot下找到,使用像config-2.6.x的文件名。你可以直接将它拷贝到内核代码树的路径下: cp /boot/config-`uname -r` /usr/src/linux-`uname -r`/.config

让我们再次注意一下先前的错误信息:仔细看的话你会发现,即使使用完全相同的配置文件,版本印戳还是有细小的差异的,但这足以导致 模块加载的失败。这其中的差异就是在模块中出现却不在内核中出现的custom字符串,是由某些发行版提供的修改过的 makefile导致的。检查/usr/src/linux/Makefile,确保下面这些特定的版本信息同你使用的内核完全一致:

VERSION = 2
PATCHLEVEL = 6
SUBLEVEL = 5
EXTRAVERSION = -1.358custom
...
	

像上面的情况你就需要将EXTRAVERSION一项改为-1.358。我们的建议是将原始的makefile备份在 /lib/modules/2.6.5-1.358/build下。 一个简单的命令cp /lib/modules/`uname -r`/build/Makefile /usr/src/linux-`uname -r`即可。 另外,如果你已经在运行一个由上面的错误的Makefile编译的内核,你应该重新执行 make,或直接对应/lib/modules/2.6.x/build/include/linux/version.h从文件 /usr/src/linux-2.6.x/include/linux/version.h修改UTS_RELEASE,或用前者覆盖后者的。

现在,请执行make来更新设置和版本相关的头文件,目标文件:

[root@pcsenonsrv linux-2.6.x]# make
CHK     include/linux/version.h
UPD     include/linux/version.h
SYMLINK include/asm -> include/asm-i386
SPLIT   include/linux/autoconf.h -> include/config/*
HOSTCC  scripts/basic/fixdep
HOSTCC  scripts/basic/split-include
HOSTCC  scripts/basic/docproc
HOSTCC  scripts/conmakehash
HOSTCC  scripts/kallsyms
CC      scripts/empty.o
...
	

如果你不是确实想编译一个内核,你可以在SPLIT后通过按下CTRL-C中止编译过程。因为此时你需要的文件 已经就绪了。现在你可以返回你的模块目录然后编译加载它:此时模块将完全针对你的当前内核编译,加载时也不会由任何错误提示。


 

抱歉!评论已关闭.