Linux设备驱动程序学习----3.模块的编译和装载

模块的编译和装载

更多内容请参考Linux设备驱动程序学习----目录

1. 设置测试系统

第1步,要先从kernel.org的镜像网站上获取一个主线内核,并安装到自己的系统中,因为学习驱动程序的编写,最好使用标准内核。

第2步,必须在自己的系统中配置并构造好内核树,这样可以得到一个更加健壮的模块装载器,可以使内核的模块要和内核源码树中的目标文件连接。同时也需要这些目标文件存在于内核目录树中。这样,准备一个内核源代码树,构造一个新内核,并安装到自己的系统中,有利于开发工作的进行。

第3步,要决定在什么地方完成模块的开发、调试,内核代码中的错误可能导致用户进程甚至整个系统崩溃,这些错误通常不会制造更加严重的问题,但建议开发者应该在一个不包含任何敏感数据或者不执行重要服务的系统上完成内核的调试实验。

2. Hello World模块

  几乎所有编程学习都是以“Hello world”示例程序开始的,在这里的模块中也可以使用这个经典历程。如下代码段:

#include <linux/init.h>
#include <linux/module.h>

static int __init hello_init(void)
{
    printk(KERN_ALERT "Hello, world\n");
    
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_ALERT "Goodbye, cruel world\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");

module_init() 宏表示模块被装载到内核时调用hello_init函数
module_exit() 宏表示模块被移除内核时调用hello_exit函数
MODULE_LICENSE() 宏用来告诉内核,该模块采用自由许可证;如果没有该声明,内核在装载该模块时会产生抱怨。

  printk() 函数在Linux内核中定义,功能类似标准C库中的printf() 函数。是内核独有的打印输出函数,因为内核载运行的时不能依赖C库。模块中能够调用printk()函数,是因为在insmod插入模块之后,模块连接到内核,可以访问内核的公共符号。KERN_ALERT定义了消息的优先级,需要在模块中显式地指定高优先级的原因是:具有默认优先级的消息可能不会输出在控制台上。

模块编译过程如下:

# make
make -C /lib/modules/4.15.0-55-generic/build M=/home/mcy/code/ldd3-demo/1_module modules
make[1]: Entering directory '/usr/src/linux-headers-4.15.0-55-generic'
Makefile:976: "Cannot use CONFIG_STACK_VALIDATION=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel"
  CC [M]  /home/mcy/code/ldd3-demo/1_module/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/mcy/code/ldd3-demo/1_module/hello.mod.o
  LD [M]  /home/mcy/code/ldd3-demo/1_module/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-4.15.0-55-generic'

  可以通过insmod和rmmod命令来加载和卸载模块,注意:只有超级用户才有权加载和卸载模块。

# insmod hello.ko
Hello, world

# lsmod 
Module                  Size  Used by
hello                  16384  0

# rmmod hello
Goodbye, cruel world

  能够编译模块的前提条件是,必须在模块的Makefile能够找到的路径,正确配置和构造了内核树。模块的编译参考下文中编译和装载部分。

  编写设备驱动程序并不困难,真正的困难在于理解设备并最大化其性能。

3. 编译和装载

  本节将详细介绍如何将源代码编译成能够装载到内核中的可执行模块。

3.1 编译模块

在构造内核模块之前,需要先满足一下条件:

  1. 应确保具备了正确版本的编译器、模块工具和其他必要的工具;使用太旧、太新的工具都有可能出现未知问题;
  2. 应该先配置并构造内核;最好运行和模块对应的内核;

之后,为模块创建Makefile很简单,只需要添加就可以了:

obj-m := hello.o

  由内核构造系统处理其余问题,上面的赋值语句说明有一个模块要从目标文件hello.o构造,而由该目标文件构造的模块名称为hello.ko。

  如果要构造的模块名称为module.ko,该模块由两个源文件file1.c和file2.c生成,则Makefile应该如下书写:

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

  为了上述类型的Makefile文件正常工作,必须在大的内核构造系统中调用它,即在包含模块源代码和Makefile文件的目录中,用下面的命令:

make -C kernel_path M=`pwd` modules

  kernel_path为已经构造好的内核源代码的路径。该命令先改变目录到-C选项指定的目录(内核源代码目录),其中保存有内核的顶层Makefile文件。
M=选项让该Makefile在构造modules目标之前返回到模块源代码目录。然后,modules目标指向obj-m变量中设定的模块,本例中为module.o。

  一般情况下,编译模块的文件为module.ko,在加载该模块文件时,会出现:

# insmod my_module.ko
module: module is already loaded

  这是因为module.ko这个模块名字和系统中的模块名称冲突造成的,可以在Makefile中修改目标文件,避免使用module作为模块的命名。

  为了使内核树之外的模块构造更加方便,可以使用一下Makefile方法:

// Makefile

# 如果定义了KERNELRELEASE,则说明是从内核构造系统调用的
ifneq ($(KERNELRELEASE), )
    obj-m := hello.o

# 否则,是直接从命令行调用的,这时需要调用内核构造系统
else
    KERNELDIR ?= ......
    PWD := $(shell pwd)
default:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) clean
endif

  在一个典型的构造过程中,Makefile被读取两次。当Makefile从命令行调用时,KERNELRELEASE变量还未设置,已安装的模块目录中存在一个符号链接,指向内核的构造树,以找到内核的源代码目录。在找到内核源代码树之后,该Makefile会调用default目标,这个目录使用之前描述过的方法,第二次运行make命令($MAKE),以运行内核构造系统。在第二次读取该Makefile文件时,设置了obj-m,而内核的Makefile负责真正构造该模块。

  如上述Makefile所示,所有的Makefile都应该包含通常用来清除无用文件的目标、安装模块的目标等。

编译清理过程如下所示:

# make clean
make -C /lib/modules/4.15.0-55-generic/build M=/home/mcy/code/ldd3-demo/1_module clean
make[1]: Entering directory '/usr/src/linux-headers-4.15.0-55-generic'
  CLEAN   /home/mcy/code/ldd3-demo/1_module/.tmp_versions
  CLEAN   /home/mcy/code/ldd3-demo/1_module/Module.symvers
make[1]: Leaving directory '/usr/src/linux-headers-4.15.0-55-generic'

3.2 装载和卸载模块

  模块构造完成之后,下一步就是将模块装入内核,insmod和ld类似,insmod将模块的代码和数据装入内核,然后使用内核的符号表解析模块中任何未解析的符号。内核不会修改模块在磁盘的文件,而是修改内存中的副本。insmod可以接受一些命令行选项,并且可以在模块链接到内核之前给模块中的变量赋值(即模块传参,参考模块参数),模块可以在装载时进行配置,比编译时的配置更加灵活。

  insmod程序加载模块的过程解析,insmod依赖于定义在kernel/module.c中的系统调用,sys_init_module() 函数给模块分配内核内存以便装载模块,然后将模块正文复制到内存区域,并通过内核符号表解析模块中的内核引用,最后调用模块的初始化函数。sys_前缀的函数用于系统调用,其他函数不能使用,方便使用grep搜索系统调用。

  modprobe工具和insmod类似,也用来将模块装载到内核中。modprobe和insmod的区别在于,modprobe会考虑要装载的模块是否引用了一些当前内核不存在的符号。如果有,modprobe会在当前模块搜索路径中查找定义了这些符号的其他模块。如果找到了这些模块,会同时将这些模块装载到内核。此时如果使用insmod将会失败,并在系统日志文件中记录“unresolved symbols”(未解析的符号)消息。

  rmmod工具从内核中移除模块。如果内核认为该模块正在使用状态,或者内核配置为禁止卸载模块,则无法移除该模块。

  lsmod工具列出当前装载到内核中的所有模块,还有其他模块是否在使用某个模块等信息。lsmod工具通过读取/proc/modules文件来获取这些信息。当前已装载模块信息也可以在/sys/module目录下找到。

# insmod hello.ko
Hello, world

# lsmod 
Module                  Size  Used by
hello                  16384  0

# rmmod hello
Goodbye, cruel world

3.3 版本依赖

  在缺少modversions的情况下,模块代码必须针对要链接的每个版本的内核重新编译。模块和特定内核版本定义的数据结构和函数原型紧密关联。

  在构造模块过程中,可以将模块和当前内核树中的vermagic.o文件链接,该目标文件包含了大量有关内核的信息,包括目标内核版本、编译器版本以及一些重要配置变量的设置。在试图装载模块时,这些信息用来检查模块和正在运行的内核的兼容性。如果有任何不匹配,就不会装载该模块,同时会有如下信息:

# insmod hello.ko
Error inserting './hello.ko': -1 Invalid module format

查看系统日志文件/var/log/messages,将看到导致模块装载失败的具体原因。

  如果要为某个特定的内核版本编译模块,则需要该特定版本对应的构造系统和源代码树。同时需要修改Makefile中的KERNELDIR变量来实现。如果打算编写一个能够和多个内核版本一起工作的模块,则必须使用宏以及#ifdef条件编译来构造并编译模块代码。

可以参考<linux/version.h>中的相关定义,该头文件已包含于<linux/module.h>头文件中,参考如下宏定义:

UTS_RELEASE
&emsp;&emsp;扩展为一个描述内核版本的字符串,如:2.6.10
LINUX_VERSION_CODE
&emsp;&emsp;扩展为内核版本的二进制表示,版本发行号中的每一部分对应一个字节
KERNEL_VERSION(major, minor, release)
&emsp;&emsp;该宏已组成版本号的三部分为参数,创建整数的版本号

  通过检查KERNEL_VERSION和LINUX_VERSION_CODE宏而使用预处理条件,能够解决大部分基于内核版本的依赖性问题。最好的处理方法是,将所有相关的预处理条件语句几种存放在一个特定的头文件里。一般而言,依赖于特定版本或平台的代码应该隐藏在低层宏或者函数之后,高层函数可直接调用这些函数,而无需关注低层细节。这样的代码便于阅读,更为健壮。

更多内容请参考Linux设备驱动程序学习----目录

posted @ 2019-08-25 22:20  micro虾米  阅读(1073)  评论(0编辑  收藏  举报