分享

构建第一个Linux驱动模块

 汉无为 2023-08-03 发布于湖北

上一篇文章介绍了 Linux 驱动编程需要了解的基础知识:

Linux驱动编程必备基础知识

这篇文章来介绍一下,如何构建一个驱动模块。构建一个模块,可以在两个地方完成:

  • 内核树内部
  • 内核树外部

除此之外,还需要编写相应的构建脚本文件 makefile。

接下来,我们逐步进行介绍。

模块构建的makefile

makefile 是用来执行一组操作的特殊文件,其中最重要的操作是程序的编译。专用工具 make 用于解析makefile。

在说明整个make文件之前,先介绍一下 obj-<X>    kbuild 变量。<X> 可以取值为 y、m、n或者空白。

例如:

obj-y += mymodule.o

告诉 kbuild 在当前目录中有一个名为 mymodule.o 的对象。mymodule.o 将从 mymodule.c 或 mymodule.S 构建。<X> 的值决定了如何构建以及是否构建或链接 mymodule.o

  • 如果 <X> 设置为 m,则使用变量 obj-m,并将 mymodule.o 构建为模块
  • 如果 <X> 设置为 y,则使用变量 obj-y,mymodule.o将构建为内核的一部分。也可以说它是一个内置模块。
  • 如果 <X> 设置为 n,则使用变量 obj-n,不会构建 mymodule.o

如果后边跟着是某个目录,例如

obj-<X> += onedir/

kbuild 应该进入 onedir 目录,查找其中所有的 makefile 并处理它们,从而决定应该构建哪些对象。

一份完整的模块构建Makefile 示例:

KERNELDIR ?= /lib/modules/$(shell uname -r)/build

obj-m := helloworld.o

all:
    $(MAKE) -C $(KERNELDIR) M=$(shell pwd) modules;

clean:
    $(MAKE) -C $(KERNELDIR) M=$(shell pwd) clean;
  • obj-m := hellowolrd.o 解析,obj-m 列出要构建的模块。对于每一个 <filename> .o,进行系统构建时会查找<filename> .c

  • KERNELDIR := /lib/modules/$(shell uname -r)/build 解析,KERNELDIR 是预构建的内核源码的位置。构建任何模块都需要预构建内核。 -C 要求 make 在读取 makefile 或执行其他任何操作之前先更改到指定的目录。

  • M=$(shell pwd) 解析,这与内核构建系统相关。内核 makefile 使用这个变量来定位要构建的外部模块的目录,.c 文件应该被放置在这。

  • (MAKE) -C $(KERNELDIR ) M=$(shell pwd) modules; 构建驱动模块的规则

内核树内的模块

在内核树中构建驱动程序,需要把驱动程序的代码文件放在特定的目录中。驱动程序中的每个子目录都有 makefile 和 kconfig。

例如,驱动文件 mychardev.c 为字符驱动程序源码,则应该把他放在内核源码的 drivers/char 目录中。

一个 kconfig 的示例文件如下:

config PACKT_MYCDEV
    tristate 'Our packtpub special Characterdriver'
    default m
    help
        Say Y here if you want to support the/dev/mycdev device.
        The /dev/mycdev device is used to access packtpub.

同时,在这个目录下的 makefile 文件中添加一下语句:

obj-$(CONFIG_PACKT_MYCDEV) += mychardev.o

注意,.o 文件名称必须与 .c 文件名完全一致。

配置完成后,可以分别使用 make 和 make modules 构建内核和模块。

内核源码树中包含的模块安装在 /lib/modules/$(KERNELRELEASE)/kernel/ 中。在Linux系统上,它是/lib/modules/$(uname -r)/kernel/

内核树外的模块

在构建外部模块之前,需要有一个完整的、预编译的内核源代码树。内核源码树版本必须与将加载和使用模块的内核相同。

有两种方法可以获得预构建的内核版本:

  • 自己构建

  • 从发行版本库安装linux-headers- *包
安装指令
sudo apt-get update
sudo apt-get install linux-headers-$(uname -r)

这将只安装头文件,而不是整个源代码树。

头文件将被安装在 /usr/src/linux-headers-$(uname -r) 目录下 。

有一个符号链接 /lib/modules/$(uname-r)/build,指向前面安装的头文件,这应该是在 makefile 中指定为内核目录的路径。

以上内容准备完成之后,就可以进行构建驱动模块。

构建模块

处理完 makefile 后,只需要切换到源码目录并运行 make 命令,即可开始构建模块。

一个简单模块程序 helloworld.c

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

/* 模块入口点函数 */
static int helloworld_init(void)
{
pr_info('Hello world!\n');
return 0;
}

/* 模块出口点函数 */
static void helloworld_exit(void)
{
pr_info('End of the world\n');
}

/* 指定函数用途 */
module_init(helloworld_init);
module_exit(helloworld_exit);

MODULE_AUTHOR('zsky');
MODULE_LICENSE('GPL');

构建脚本文件 Makefile

KERNELDIR ?= /lib/modules/`uname -r`/build

obj-m:=   helloworld.o

all :
    $(MAKE) -C $(KERNELDIR) M=$(shell pwd) modules;

clean:
    $(MAKE) -C $(KERNELDIR) M=$(shell pwd) clean;
    rm -f *.ko;

开始构建:

$ make
make -C /lib/modules/`uname -r`/build M=/home/user/learn/drivers/chap2 modules;
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-73-generic'
CC [M] /home/user/learn/drivers/chap2/helloworld.o
MODPOST /home/user/learn/drivers/chap2/Module.symvers
CC [M] /home/user/learn/drivers/chap2/helloworld.mod.o
LD [M] /home/user/learn/drivers/chap2/helloworld.ko
BTF [M] /home/user/learn/drivers/chap2/helloworld.ko
Skipping BTF generation for /home/user/learn/drivers/chap2/helloworld.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-73-generic'

构建完成后,在当先目录下会生成一些文件,其中 helloworld.ko,为最终生成的内核模块。

交叉编译

上面的例子使用的是本地构建,在 x86 机器上为x86 机器编译。交叉编译怎么实现?

这个过程是在机器 A(称为宿主机)上编译,该代码要运行在机器 B(称为目标机)上;宿主机和目标机具有不同的体系结构。

常见的交叉编译是在 x86 机器上构建的代码要运行在 ARM 架构上。交叉编译内核模块时,构建 makefile 需要指定两个变量:ARCH 和 CROSS_COMPILE,它们分别表示目标体系结构和编译器的前缀名称。

因此,内核模块本地编译和交叉编译之间的差别在于 makefile 构建文件。

另外,还需要一份目标机器正在使用的内核源码,编译模块的时候需要用到。

模块装载和卸载

模块构建完成后,可以通过 insmod 指令进行装载。注意装载和卸载需要 root 访问权限,可以在模块加载指令之前加上 sudo

$ sudo insmod helloworld.ko

指令执行完,看不到任何信息。

加载内核模块,模块的打印信息需要通过 dmsg 指令查看。可以看到入口函数 helloworld_init()打印的 Hello world!

卸载模块,通过 rmmod 指令

sudo rmmod helloworld

同样出口函数打印的信息,dmesg 指令查看。

另外,用 modinfo 查看模块信息如下:

$ modinfo helloworld.ko
filename: /home/user/learn/drivers/chap2/helloworld.ko
license: GPL
author: zsky
srcversion: 6EFA6AC1502C67E96C09216
depends:
retpoline: Y
name: helloworld
vermagic: 5.15.0-73-generic SMP mod_unload modversions

好了,感谢阅读,加油~

图片

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多