编写一个 Linux 内核模块

作者:解琛
时间:2020 年 8 月 16 日

  • 编写一个 Linux 内核模块
  • 一、实验环境
  • 二、Linux 内核模块相关命令
  • 三、程序架构
  • 四、编写一个内核模块
    • 4.1 头文件
      • 4.1.1 init.h
      • 4.1.2 module.h
    • 4.2 模块加载
    • 4.3 模块卸载
    • 4.4 参数
    • 4.5 导出符号
    • 4.6 许可证
    • 4.7 作者
    • 4.8 模块描述信息
    • 4.9 模块别名
    • 4.10 实验源码
  • 五、Makefile
  • 六、编译模块
  • 七、加载内核模块
  • 八、卸载内核模块
  • 九、终端输出调试信息

内核模块

一、实验环境

jerome@jerome:~$ uname -a
Linux jerome 5.4.0-42-generic #46~18.04.1-Ubuntu SMP Fri Jul 10 07:21:24 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
jerome@jerome:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.4 LTS
Release:    18.04
Codename:   bionic

二、Linux 内核模块相关命令

命令 作用
lsmod 用于显示所有已载入系统的内核模块。
insmod 用于加载内核模块,通常可载入的模块一般是设备驱动程序
rmmod 用于卸载不需要的模块。
modinfo 用于显示内核模块的相关信息。
depmod 用于分析检测内核模块之间的依赖关系。
modprobe 同样用于加载内核模块,与insmod不同,modprobe会根据depmod产生的依赖关系,加载依赖的的其他模块。

三、程序架构

内核模块程序的基本结构包括了以下几个部分:

  1. 头文件;
  2. 内核模块加载/卸载函数;
  3. 内核模块的参数;
  4. 内核模块导出符号;
  5. 内核模块的许可证;
  6. 内核模块的其他信息,如作者,模块的描述信息,模块的别名等;

四、编写一个内核模块

4.1 头文件

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

4.1.1 init.h

位于 lib/modules/5.4.0-42-generic/build/include/linux/init.h。

/* These are for everybody (although not all archs will actually
discard it in modules) */#define __init __section(.init.text) __cold notrace
#define __initdata __section(.init.data)
#define __initconst __constsection(.init.rodata)
#define __exitdata __section(.exit.data)
#define __exit_call __used __section(.exitcall.exit)/*** module_init() - driver initialization entry point* @x: function to be run at kernel boot time or module insertion** module_init() will either be called during do_initcalls() (if* builtin) or at module insertion time (if a module).* There can only* be one per module.*/#define module_init(x) __initcall(x);/*** module_exit() - driver exit entry point* @x: function to be run when driver is removed** module_exit() will wrap the driver clean-up code* with cleanup_module() when used with rmmod when* the driver is a module.* the driver is statically* compiled into the kernel, module_exit() has no effect.* There can only be one per module.*/#define module_exit(x) __exitcall(x);

init.h 头文件主要包含了内核模块的加载、卸载函数的声明,还有一些宏定义,因此,只要涉及内核模块的编程,就需要加上该头文件。

4.1.2 module.h

位于 lib/modules/5.4.0-42-generic/build/include/linux/module.h。

/* Generic info of form tag = "info" */#define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)/* For userspace: you can also call me...
*/#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)/** Author(s), use "Name <email>" or just "Name", for multiple* authors use multiple MODULE_AUTHOR() statements/lines.*/#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)

MODULE_LICENSE 是指定该内核模块的许可证,是必须要有的,编写内核模块时需要添加该头文件。

4.2 模块加载

static int __init hello_init(void)
{if (debug_on)printk("[ DEBUG ] debug info output.\n");printk("Hello World Jerome's Module Init.\n");return 0;
}module_init(hello_init);

hello_init 函数类型为 int,该函数返回 0,表示模块初始化成功,并会在 /sys/module 下新建一个以模块名为名的目录。返回非0值,表示模块初始化失败。

static 关键字的作用如下:

  1. static 修饰的静态局部变量直到程序运行结束以后才释放,延长了局部变量的生命周期;
  2. static 的修饰全局变量只能在本文件中访问,不能在其它文件中访问;
  3. static 修饰的函数只能在本文件中调用,不能被其他文件调用;

内核模块的代码,实际上是内核代码的一部分,假如内核模块定义的函数和内核源代码中的某个函数重复了,编译器就会报错,导致编译失败,因此我们给内核模块的代码加上 static 修饰符就可以避免这种错误的发生。

/* 定义在 init.h 中; */#define __init __section(.init.text) __cold notrace
#define __initdata __section(.init.data)

Linux 内核的栈资源有限,应该动态分配。

__init 用于修 饰函数,__initdata 用于修饰变量。

带有 __init 的修饰符,表示将该函数放到可执行文件的 __init 节区中,该节区的内容只能用于模块的初始化阶段,初始化阶段执行完毕之后,这部分的内容则会被释放掉。

module_init 用于通知内核初始化模块的时候,要使用哪个函数进行初始化。它会将函数地址加入到相应的节区 section 中。因此,开机的时候就可以自动加载该模块了。

4.3 模块卸载

static void __exit hello_exit(void)
{printk("Hello World Jerome's Module Exit.\n");
}module_exit(hello_exit);

_exit 主要是用于释放初始化阶段分配的内存,分配的设备号等,是初始化过程的逆过程,该函数的返回值是 void 类型。

#define __exit __section(.exit.text) __exitused __cold notrace
#define __exitdata __section(.exit.data)

__exit 用于修饰函数,__exitdata 用于修饰变量。__exit,表示将该函数放在可执行文件的 __exit 节区,当执行完模块卸载阶段之后,就会自动释放该区域的空间。

宏定义 module_exit 用于告诉内核,当卸载模块时,需要调用 hello_exit 函数。

4.4 参数

bool debug_on = 0;module_param(debug_on, bool, S_IRUSR);

在调试内核模块的时候,可以使用 module_param 函数来定义一个变量,控制调试信息的输出。

/* 位于内核源码 /linux/moduleparam.h; */#define module_param(name, type, perm) module_param_named(name, name, type, perm)

如果定义了一个模块参数,则会在 /sys/module/模块名/parameters 下会存在以模块参数为名的文件。

可以通过指令 insmod debug_on=1 来输出调试信息。参数 perm 表示的是该文件的权限。

文件权限名称 说明
S_IRUSR 用户拥有读权限
S_IWUSR 用户拥有写权限
S_IRGRP 当前用户组的其他用户拥有读权限
S_IWGRP 当前用户组的其他用户拥有写权限
S_IROTH 其他用户拥有读权限
S_IWOTH 其他用户拥有写权限

4.5 导出符号

符号指的就是函数和变量。

当模块被装入内核后,它所导出的符号都会记录在内核符号表中。在使用命令 insmod 加载模块后,模块就被连接到了内核,因此可以访问内核的共用符号。

#define EXPORT_SYMBOL(sym) __EXPORT_SYMBOL(sym, "")

EXPORT_SYMBOL 宏用于向内核导出符号,这样的话,其他模块也可以使用这个已经被导出的符号了。类似一般写程序时使用的 extern。

4.6 许可证

Linux 是一款免费的操作系统,采用了 GPL 协议,允许用户可以任意修改其源代码。

GPL 协议的主要内容是软件产品中即使使用了某个 GPL 协议产品提供的库,衍生出一个新产品,该软件产品都必须采用 GPL 协议,即必须是开源和免费使用的,可见 GPL 协议具有传染性。

在 Linux 内核版本 2.4.10 之后,模块必须通过 MODULE_LICENSE 宏声明此模块的许可证,否则在加载此模块时,会提示内核被污染。

MODULE_LICENSE("GPL");

内核模块许可证有 “GPL”,“GPL v2”,“GPL and additional rights”,“Dual SD/GPL”,“Dual MPL/GPL”,“Proprietar”。

4.7 作者

MODULE_AUTHOR("xiechen");

使用 modinfo 中打印出的模块信息中 author 信息便是来自于宏定义 MODULE_AUTHOR。

该宏定义用于声明该模块的作者,可有可无。

4.8 模块描述信息

MODULE_DESCRIPTION("xiechen's hello world module.");

模块信息中 description 信息则来自宏 MODULE_DESCRIPTION,该宏用于描述该模块的功能作用。

4.9 模块别名

MODULE_ALIAS("hello_module");

模块信息中 alias 信息来自于宏定义 MODULE_ALIAS。该宏定义用于给内核 模块起别名。

在使用该模块的别名时,需要将该模块复制到 /lib/modules/ 目录下,使用命令 depmod 更新模块的依赖关系,否则的话,Linux 内核不知道这个模块还有另一个名字。

4.10 实验源码

#include <linux/init.h>
#include <linux/module.h>bool debug_on = 0;
module_param(debug_on, bool, S_IRUSR);static int __init hello_init(void)
{if (debug_on)printk("[ DEBUG ] debug info output\n");printk("Jerome's Hello World Module Init\n");return 0;
}
module_init(hello_init);static void __exit hello_exit(void)
{printk("Jerome's Hello World Module Exit\n");
}
module_exit(hello_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("xiechen");
MODULE_DESCRIPTION("xiehcne's hello world module");
MODULE_ALIAS("hello_module");

五、Makefile

Ubuntu下的第一个内核模块编译

obj-m := hello.o    KERNELBUILD :=/lib/modules/$(shell uname -r)/build  # 编译内核模块需要的Makefile的路径,Ubuntu下是 /lib/modules/5.4.0-42-generic-generic/build;default:  make -C $(KERNELBUILD) M=$(shell pwd) modules   # 编译内核模块。-C 将工作目录转到 KERNELBUILD,调用该目录下的 Makefile,并向这个 Makefile 传递参数 M 的值是 $(shell pwd) modules;clean:  rm -rf *.o *.ko *.mod.c .*.cmd *.markers *.order *.symvers .tmp_versions

六、编译模块

文件结构如下。

jerome@jerome:~/文档/2.训练场/1.linux内核/0.编写一个内核模块$ tree
.
├── hello.c
└── Makefile0 directories, 2 files

对程序进行编译。

jerome@jerome:~/文档/2.训练场/1.linux内核/0.编写一个内核模块$ sudo make
[sudo] jerome 的密码:
make -C /lib/modules/5.4.0-42-generic/build   M=/home/jerome/文档/2.训练场/1.linux内核/0.编写一个内核模块 modules     # 编译内核模块。-C 将工作目录转到 KERNELBUILD,调用该目录下的 Makefile,并向这个 Makefile 传递参数 M 的值是 /home/jerome/文档/2.训练场/1.linux内核/0.编写一个内核模块 modules;
make[1]: 进入目录“/usr/src/linux-headers-5.4.0-42-generic”CC [M]  /home/jerome/文档/2.训练场/1.linux内核/0.编写一个内核模块/hello.oBuilding modules, stage 2.MODPOST 1 modulesCC [M]  /home/jerome/文档/2.训练场/1.linux内核/0.编写一个内核模块/hello.mod.oLD [M]  /home/jerome/文档/2.训练场/1.linux内核/0.编写一个内核模块/hello.ko
make[1]: 离开目录“/usr/src/linux-headers-5.4.0-42-generic”
jerome@jerome:~/文档/2.训练场/1.linux内核/0.编写一个内核模块$ tree
.
├── hello.c
├── hello.ko
├── hello.mod
├── hello.mod.c
├── hello.mod.o
├── hello.o
├── Makefile
├── modules.order
└── Module.symvers0 directories, 9 files

这时,在hello.c 所在文件夹就会有 hello.ko ,这个就是编译好的内核模块。

七、加载内核模块

sudo insmod ./hello.ko

使用 dmesg 查看产生的内核信息。

jerome@jerome:~/文档/2.训练场/1.linux内核/0.编写一个内核模块$ dmesg
[ 6992.056350] Jerome's Hello World Module Init

八、卸载内核模块

sudo rmmod hello

使用 dmesg 查看内核的打印信息。

jerome@jerome:~/文档/2.训练场/1.linux内核/0.编写一个内核模块$ dmesg
[ 7222.074148] Jerome's Hello World Module Exit

九、终端输出调试信息

加载内核模块时通过传入参数来选择输出调试信息。

sudo insmod hello.ko debug_on=1

使用 dmesg 查看内核打印出来的信息。

jerome@jerome:~/文档/2.训练场/1.linux内核/0.编写一个内核模块$ dmesg
[ 7371.072662] [ DEBUG ] debug info output
[ 7371.072664] Jerome's Hello World Module Init

编写一个 Linux 内核模块相关推荐

  1. 编写函数实现员工信息录入和输出_编写我的第一个Linux 内核模块“hello_module”...

    前言: Linux 内 核 模 块 全 称 为 " 动 态 可 加 载 内 核 模 块 (Loadable Kernel Module,LKM)",是系统内核向外部提供的功能插口. ...

  2. 用C语言编写一个Linux下的简单shell程序

    这是一个简单的C程序,展示了如何进行系统调用执行logout cd ls pwd pid rm mkdir mv cp等命令,这是一个简单的命令解释程序shell,其源代码如下: #include & ...

  3. linux的静态编译elf无法调试,[翻译]自己动手编写一个Linux调试器系列之4 ELF文件格式与DWARF调试格式 by lantie@15PB...

    自己动手编写一个Linux调试器系列之4 ELF文件格式与DWARF调试格式 by lantie@15PB 在上一节中,你已经听说了DWARF调试格式,它是程序的调试信息,是一种可以更好理解源码的方式 ...

  4. 实战!手把手教你如何编写一个Linux驱动并写一个支持物联网的LED演示demo

    目录 一.开发环境 二. 准备工作: 1. 创建一个项目工程目录 2. 创建输出与目标目录 3.头文件目录 4. 建立源代码src目录 5. 使用git管理你的项目 三.编写LED驱动 三.一 准备工 ...

  5. 操作系统课程设计——Shell编程(用c编写一个Linux的外壳Shell)

    文章目录 前言 功能与展示 功能列表 功能展示 依赖库安装 具体实现 Shell工作流程 外部命令工作流程 内置命令工作流程 管道功能与I/O重定向的实现 alias功能的一些思考 Shell的编译与 ...

  6. linux 内核模块 编写例子,Linux内核模块实例

    一个简单的内核模块来读取 timespec 数据结构的数据. "read_kernel_time.c": #include #include #include #include s ...

  7. 内核实验(三):编写简单Linux内核模块,使用Qemu加载ko做测试

    文章目录 一.篇头 二.QEMU:挂载虚拟分区 2.1 创建 sd.ext4.img 虚拟分区 2.2 启动 Qemu 2.3 手动挂载 sd.ext4.img 三.实现一个简单的KO 3.1 目录文 ...

  8. linux 内核模块 编写例子,LINUX内核模块编程8

    Chapter 8. System Calls 系统调用 到目前为止,我们所做的只是使用完善的内核机制注册/proc文件和处理设备的对象.如果只是想写一个设备驱动, 这些内核程序员设定的方式已经足够了 ...

  9. c语言logout_用C语言编写一个Linux下的简单shell程序

    (作者) 2011-11-26 01:05 3 The shell sets some environment variables according to the command line argu ...

最新文章

  1. 【每日一算法】什么是二分图?
  2. python处理excel大数据-当Excel遇到大数据问题,是时候用Python来拯救了
  3. docker 安装git_docker随手笔记第十二节 jenkins+docker+nginx+纯静态页面配置
  4. 使用FiddlerCore来测试WebAPI
  5. Kaggle | 用 YoloV5 将物体检测的性能翻倍的心路历程
  6. 并联机器人市场呈现快速增长之势
  7. firefox驱动_零适配 + 全兼容!龙芯电脑推出办公外设利旧通用解决方案:运行原生 Windows 驱动程序...
  8. 贝壳找房的深度学习模型迭代及算法优化
  9. Android学习记录(一) 重拾Activity
  10. 寻找互联网创业的时间点规律
  11. Visual Studio中View页面与Js页面用快捷键互相跳转
  12. 怎么看其他人系统连接的服务器,别人如何连接云服务器
  13. 献给攻击者,请放弃攻击吧,这样只会浪费自己的青春+金钱
  14. java IO 测试题
  15. udp测试工具linux系统,网络测试工具下载_Packet Sender(UDP/TCP网络测试工具)
  16. linux 查看链接文件,Linux下的链接文件详解
  17. CameraBag Photo 2020 for Mac(Mac滤镜软件)
  18. 【吐血推荐】技术人员的发展之路
  19. Unity学习笔记:Rule Tile、Advance Rule Overide Tile、Rule Override Tile的用法【By Chutianto】
  20. 在代码里设置view的android:layout_marginTop

热门文章

  1. 【tkinter组件专栏】Frame:承载规划布局的框架
  2. HDU 1846 Brave Game 【巴什博奕】
  3. 锐捷telnet登录配置
  4. 教师利用计算机教室培训的简报,校园全体计算机教师教研会议简报范文
  5. sm2格式数字信封加解密详解
  6. 五分钟了解--指纹浏览器背后的一切
  7. androidstudio自动生成变量_为数不多的人知道的AndroidStudio快捷键(一)
  8. 机构已经入场!美国养老基金4000万美元要投数字资产
  9. 【MM32F5270开发板试用】移植Google Chrome小恐龙游戏到MM32F5270
  10. 工业机器人(8)-- UART和RS-232、RS-422、RS-485通信接口