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

时间:2021-11-12 18:22:13

模块的编译和装载

更多内容请参考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设备驱动程序学习----目录

Linux设备驱动程序学习----3.模块的编译和装载的更多相关文章

  1. Linux设备驱动程序学习之分配内存

    内核为设备驱动提供了一个统一的内存管理接口,所以模块无需涉及分段和分页等问题. 我已经在第一个scull模块中使用了 kmalloc 和 kfree 来分配和释放内存空间. kmalloc 函数内幕 ...

  2. Linux设备驱动程序学习----1&period;设备驱动程序简介

    设备驱动程序简介 更多内容请参考Linux设备驱动程序学习----目录 1. 简介   Linux系统的优点是,系统内部实现细节对所有人都是公开的.Linux内核由大量复杂的代码组成,设备驱动程序可以 ...

  3. Linux设备驱动程序学习----目录

    目录 设备驱动程序简介 1.设备驱动程序简介 构造和运行模块 2.内核模块和应用程序的对比 3.模块编译和装载 4.模块的内核符号表  5.模块初始化和关闭  6.模块参数  7.用户空间编写驱动程序 ...

  4. Linux设备驱动程序学习----2&period;内核模块与应用程序的对比

    内核模块与应用程序的对比 更多内容请参考Linux设备驱动程序学习----目录 1. 内核模块与应用程序的对比 内核模块和应用程序之间的不同之处: 大多数中小规模的应用程序是从头到尾执行单个任务,而模 ...

  5. linux设备驱动程序&lowbar;hello word 模块编译各种问题集锦

    在看楼经典书籍<linux设备驱动程序>后,第一个程序就是编写一个hello word 模块. 原以为非常easy,真正弄起来,发现问题不少啊.前两天编过一次,因为没有记录,今天看的时候又 ...

  6. Linux设备驱动程序学习 高级字符驱动程序操作[阻塞型I&sol;O和非阻塞I&sol;O&rsqb;【转】

    转自:http://blog.csdn.net/jacobywu/article/details/7475432 阻塞型I/O和非阻塞I/O 阻塞:休眠 非阻塞:异步通知 一 休眠 安全地进入休眠的两 ...

  7. linux设备驱动程序该添加哪些头文件以及驱动常用头文件介绍(转)

    原文链接:http://blog.chinaunix.net/uid-22609852-id-3506475.html 驱动常用头文件介绍 #include <linux/***.h> 是 ...

  8. Linux设备驱动程序 第三版 读书笔记(一)

    Linux设备驱动程序 第三版 读书笔记(一) Bob Zhang 2017.08.25 编写基本的Hello World模块 #include <linux/init.h> #inclu ...

  9. linux设备驱动程序--在用户空间注册文件接口

    linux字符设备驱动程序--创建设备节点 基于4.14内核,运行在beagleBone green 在上一讲中,我们写了第一个linux设备驱动程序--hello_world,在驱动程序中,我们什么 ...

随机推荐

  1. AM335x kernel 4&period;4&period;12 i2c eeprom AT24c02驱动移植

    kernel 4.4.12 i2c eeprom AT24c02驱动移植 在kernel make menuconfig ARCH=ARM 中打开: Device Drivers ---> Mi ...

  2. opencv&lowbar;haar分类器的训练

    本文为作者原创,未经允许不得转载:原文由作者发表在博客园: http://www.cnblogs.com/panxiaochun/p/5345412.html 因为工作的原因,本人需要用到分类器来检测 ...

  3. redis-windows执行redis-cli查询

    1.无密码时的访问 redis-cli -h redis > redis > keys *1) "myset"2) "mysortset" redi ...

  4. python 代码片段19

    #coding=utf-8 # 函数 def foo(x): print x foo(123) # import httplib def check_web_server(host,port,path ...

  5. DEDECMS中,会员中心的常用知识

    会员中心 引入了member/config.php,即可用$cfg_ml->fields['face'].$cfg_ml->fields['spacesta']等

  6. ubuntu14&period;04下交叉编译器的安装

    今天打算换个工作环境,在ubuntu下装交叉编译器,可谓一波三折.最后总算是装好了. 首先参照一下这位仁兄的博客http://blog.csdn.net/silleyj/article/details ...

  7. mysql数据库的连接

    public TJb checkjbByschool(long id)throws ClassNotFoundException,SQLException { Class.forName(" ...

  8. js-学习方法之3

    熟悉JavaScript每一个方法的作用 这一要求听起来似乎有点不太实际,我想这个要求对于像C#.JAVA这些大型语言来说确实是,因为这些语言类库实在太庞大了,相信没有人可以全面记住它,而且也是没有必 ...

  9. 第六次meeting会议

    [Beta] 第六次Daily Scrum Meeting 一.本次会议为第六次meeting会议 二.时间:10:00AM-10:20AM 地点:禹州楼 三.会议站立式照片 四.今日任务安排 成员 ...

  10. python中时间日期格式化符号:

    %y 两位数的年份表示(00-99) %Y 四位数的年份表示(000-9999) %m 月份(01-12) %d 月内中的一天(0-31) %H 24小时制小时数(0-23) %I 12小时制小时数( ...