Linux 模块编程
实验环境
本地开发操作系统:Windows 10
远程开发操作系统:CentOS Linux release 7.6.1810 (Core)
VSCode:1.56.1
实验环境配置方法见:使用 VSCode 配置远程开发
模块编程简介
内核模块
模块是可以根据需要来加载和卸载到内核中的代码片段,可以完成某种独立的功能;模块自身不是一个独立的进程,不能单独运行,可以动态的载入模块,使其成为内核代码的一部分,与内核其他代码的地位完全相同。
模块编程不同于添加系统调用,不需要重新编译内核。
模块编程示例
HelloWord
- 编写模块代码
helloword.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int hello_word_init(void)
{
// 打印hello word
printk(KERN_ALERT"hello word!");
return 0;
}
static void hello_word_exit(void)
{
// 清理时打印hello word exit
printk(KERN_ALERT "hello world exit\n");
}
// 初始化
module_init(hello_word_init);
// 清理函数
module_exit(hello_word_exit);
// 许可声明
MODULE_LICENSE("GPL");
- 头文件:
module.h
包含了加载模块需要的函数以及符号;init.h
包含了初始化以及清理时的定义;kernel.h
包含了 printk 等函数。module.h
和init.h
是模块变成必须包含的,如果编写带参数的模块,还必须包含moduleparam.h
- 许可声明:
MODULE_LICENSE
声明此模块的许可证,一般写在最后。如果不进行声明,则会收到内核被污染(kernel tainted)的警告。常见的有意义的声明:GPL,GPL v2 ,proprietary 等,其中 GPL 代表 GNU 通用公共许可证 - 初始化与清理:初始化主要完成资源申请以及模块注册,在
module_init
中定义并完成,返回 0 表示初始化成功,可以进行下一步。清理函数没有返回值,在module_exit
中定义并完成
- 编写模块编译规则文件
Makefile
obj-m :=hello.o
hello-objs:=helloword.o
KDIR:=/lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)
default:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
obj-m :=hello.o
:定义模块名,格式必须是obj-m :=模块名
hello-objs:=helloword.o
:定义目标文件,格式必须是:模块名-objs:=目标文件
KDIR:=/lib/modules/$(shell uname -r)/build
:指定内核编译器路径PWD:=$(shell pwd)
:模块源码所在目录default: make -C $(KDIR) M=$(PWD) module
和clean: make -C $(KDIR) M=$(PWD) clean
分别表示编译执行指令和清理执行指令
- 编译模块
执行 make
指令进行模块编译
$ make
编译成功后会在源码所在目录生成许多编译文件,其中 hello.ko
便是编译成功后生成的模块
若报错 Makefile:6: *** missing separator. Stop.
是因为 Makefile 文件中的编译执行指令和清理执行指令前的 Tab 不对,Windows 上开发的 Tab 与 Linux 不同,建议在终端中使用 vim 修改 Makefile 文件。
若需要重新编译,需要先执行
$ make clean
再重新编译
- 加载模块
执行指令
$ insmod hello.ko
若加载带参数的模块,执行指令
$ insmod 模块名 参数=值
加载成功不会有任何提示
查看打印消息
$ dmesg
若报错module verification failed: signature and/or required key missing - tainting kernel
且不输出hello word
是由于module签名导致的,有两种方式解决:
在当前 linux 系统的 kernel 源码下修改 config 文件,CONFIG_MODULE_SIG=n
在驱动的 Makefile 文件里面添加这行 CONFIG_MODULE_SIG=n
(这里选择第二种)
重新编译后,加载模块,便可看到输出
- 卸载模块
执行命令
$ rmmod hello.ko
查看打印消息
$ dmesg
简单的操作系统模块设计
要求:设计一个带参数的模块,其参数为指定进程标识符 PID,模块功能是输出指定进程的内存管理信息,如进程可执行代码的起始及结束地址、已初始化数据的起始及结束地址、用户态堆栈起始地址、堆起始地址等。提示:可能参考的内核函数:get_task_mm()
分析内核数据结构及函数
task_struct
:进程描述符,Linux 内核通过一个被称为进程描述符的 task_struct 结构体来管理进程,这个结构体包含了一个进程所需的所有信息。这里只例举部分,各个变量详细的解析可见:Linux进程描述符task_struct结构体详解// 系统进程状态 volatile long state; // 进程内存管理信息 struct mm_struct *mm; // 进程号 pid_t pid; // 进程组号 pid_t tpid; // 真正的父进程指针 struct task_struct *real_parent; // 父进程指针 struct task_struct *parent; // 孩子进程指针 struct list_head children; // 兄弟进程指针 struct list_head sibling; // 进程名 char comm.[TASK_COMM_LEN]; ......
mm_struct
:内存描述符,mm_struct 结构描述了一个进程的整个虚拟地址空间,每个进程只有一个 mm_struct 结构,在每个进程的 task_struct 结构中,有一个指向该进程的结构。详细解释详见:进程—内存描述符(mm_struct)// 指向虚拟区间(VMA)的链表 struct vm_area_struct * mmap; // 指向线性区对象红黑树的根 struct rb_root mm_rb; // 指向最近找到的虚拟区间 struct vm_area_struct * mmap_cache; // 用来在进程地址空间中搜索有效的进程地址空间的函数 unsigned long(*get_unmapped_area) (struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags); unsigned long(*get_unmapped_exec_area) (struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags); // 释放线性地址区间时调用的方法 void(*unmap_area) (struct mm_struct *mm, unsigned long addr); // 标识第一个分配文件内存映射的线性地址 unsigned long mmap_base; unsigned long task_size; unsigned long cached_hole_size; // 内核从这个地址开始搜索进程地址空间中线性地址的空闲区域 unsigned long free_area_cache; // 指向页全局目录 pgd_t * pgd; // 次使用计数器,使用这块空间的个数 atomic_t mm_users; // 主使用计数器 atomic_t mm_count; // 线性的个数 int map_count; // 线性区的读/写信号量 struct rw_semaphore mmap_sem; // 线性区的自旋锁和页表的自旋锁 spinlock_t page_table_lock; // 指向内存描述符链表中的相邻元素 struct list_head mmlist; ...... // 进程拥有的最大页表数目 unsigned long hiwater_rss; // 进程线性区的最大页表数目 unsigned long hiwater_vm; // 进程地址空间的大小,锁住无法换页的个数,共享文件内存映射的页数,可执行内存映射中的页数 unsigned long total_vm, locked_vm, shared_vm, exec_vm; // 用户态堆栈的页数 unsigned long stack_vm, reserved_vm, def_flags, nr_ptes; // 维护代码段和数据段 unsigned long start_code, end_code, start_data, end_data; // 维护堆和栈 unsigned long start_brk, brk, start_stack; // 维护命令行参数,命令行参数的起始地址和最后地址,以及环境变量的起始地址和最后地址 unsigned long arg_start, arg_end, env_start, env_end; ......
get_task_mm
:struct mm_struct *get_task_mm(struct task_struct *task)
此函数根据提供的任务描述符信息,获取其对应的内存信息,此内存信息保存在 mm_struct 结构体类型的变量中
编写模块代码
task.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>
#include <linux/sched.h>
static int pid;
// 定义模块参数 pid:参数名字,int:参数类型,0644:参数的读写权限
module_param(pid, int ,0644);
static int task_init(void)
{
struct pid *p;
struct task_struct *ts;
struct mm_struct *ms;
// 根据 PID 获取进程的描述符信息
p=find_get_pid(pid);
// 获取进程的任务描述符信息
ts=pid_task(p,PIDTYPE_PID);
// 获取任务的内存描述符
ms=get_task_mm(ts);
// 打印进程 PID 及 进程名称
printk("PID: %d\n", ts->pid);
printk("进程名: %20s\n", ts->comm);
// 打印进程可执行代码的起始及结束地址
printk("可执行代码起始地址: %ld\n", ms->start_code);
printk("可执行代码结束地址: %ld\n", ms->end_code);
// 打印已初始化数据的起始及结束地址
printk("数据起始地址: %ld\n", ms->start_data);
printk("数据结束地址: %ld\n", ms->end_data);
// 用户态堆栈起始地址、堆起始地址
printk("栈起始地址: %ld\n", ms->start_brk);
printk("堆起始地址: %ld\n", ms->start_stack);
// 打印其他内存管理信息
printk("进程数量(多线程): %d\n", ms->mm_users);
printk("引用计数 :%d\n", ms->mm_count);
return 0;
}
static void task_exit(void)
{
printk(KERN_ALERT "task_init() has exited\n");
}
module_init(task_init);
module_exit(task_exit);
MODULE_LICENSE("GPL");
- 编写模块编译规则文件
Makefile
obj-m :=sec.o
sec-objs:=task.o
KDIR:=/lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)
default:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
- 编译模块
与前一个实验相同,有 Warning 可以忽略,不影响程序的使用
- 加载模块
在终端执行指令获取进程列表,选择一个进程进行测试
$ ps -A
这里选择 bash
进程,PID 为4472
执行指令
$ insmod sec.ko pid=4472
查看打印消息
$ dmesg
- 卸载模块
执行命令
$ rmmod sec.ko
查看打印消息
$ dmesg
两个模块编程的示例均已完成