Linux内核模块编程小例子之查看虚拟内存VMA(virtual memory areas)信息
前言:
在 32 位的系统上,线性地址空间为 4GB,其中用户进程占有 3GB 线性地址空间,内核占有 1GB 线性地址空间。由于虚拟内存的引入,使的每个进程都可拥有 3GB 的虚拟内存。
用户进程的虚拟地址空间包含若干区域,这些区域的分布方式因体系结构的差异而不同,但所有的方式都包含下列成分:
(1) 代码段:可执行文件的二进制代码
(2) 数据段:存储全局变量
(3) 栈:用于保存局部变量和实现函数调用
(4) 环境变量和命令行参数
(5) 程序使用的动态库的代码
(6) 用于映射文件内容的区域为便于描述,系统中进程的虚拟内存空间被划分为若干不同的区域,每个区域都有其相关的属性和用途,一个合法的地址总是落在某个区域当中的,这些区域也不会重叠。在 Linux 内核中,这样的区域被称为虚拟内存区域(virtual memory areas,VMA)。一个 VMA 是一块连续的线性地址空间的抽象,它拥有自身的权限(可读,可写,可执行等) ,对进程而言,VMA 其实是虚拟空间的内存块,一个进程的所有资源由多个内存块组成。
每一个虚拟内存区域都由一个相关的 struct vm_area_struct 结构来描述。
本文将编写一个内核模块,通过此内核模块遍历一个用户进程中所有的 VMA,并且打印这些 VMA 的属性信息,如 VMA 的大小、起始地址等,并通过与“/proc/pid/maps”中显示的信息进行对比验证 VMA 信息是否正确。
本文所使用的环境:
操作系统:4.15.0-96-generic #97-Ubuntu SMP Wed Apr 1 03:25:46 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
gcc: 7.5.0
make:GNU Make 4.1
//vma_test.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/sched.h>
static int pid;
module_param(pid, int, 0644);
static void printit(struct task_struct *tsk)
{
struct mm_struct *mm;
struct vm_area_struct *vma;
int j = 0;
unsigned long start, end, length;
mm = tsk->mm;
pr_info("mm_struct addr = 0x%p\n", mm);
vma = mm->mmap;
/* 使用 mmap_sem 读写信号量进行保护 */
down_read(&mm->mmap_sem);
pr_info("vmas: vma start end length\n");
while (vma) {
j++;
start = vma->vm_start;
end = vma->vm_end;
length = end - start;
pr_info("%6d: %16p %12lx %12lx %8ld\n",
j, vma, start, end, length);
vma = vma->vm_next;
}
up_read(&mm->mmap_sem);
}
static int __init vma_init(void)
{
struct task_struct *tsk;
/* 如果插入模块时未定义 pid 号,则使用当前 pid */
if (pid == 0) {
tsk = current;
pid = current->pid;
pr_info("using current process\n");
} else {
tsk = pid_task(find_vpid(pid), PIDTYPE_PID);
}
if (!tsk)
return -1;
pr_info(" Examining vma's for pid=%d, command=%s\n", pid, tsk->comm);
printit(tsk);
return 0;
}
static void __exit vma_exit(void)
{
pr_info("Module exit\n");
}
module_init(vma_init);
module_exit(vma_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mr Yu");
MODULE_DESCRIPTION("vma test");
以上代码中:
38-52 行是内核模块初始化函数 vma_init;
40-46 行目的是获取 pid,可在加载模块时可传递相关参数(即进程 pid);如果没有传递参数,则使用当前进程,即执行 insmod 命令的进程;
45 行 pid_task()函数为获取任务的任务描述符信息,其返回值是 struct task_struct 结构体类型的变量;
50 行调用自定义的 printit()函数打印相关信息;
9-34 行是本实验核心函数;
16 行获取待检查进程的内存描述符 struct mm_struct 数据结构,该结构由struct task_struct 中的*mm 指向;
18 行获取 VMA 链表头,即 mm->mmap;
21 行开始遍历 VMA 链表,down_read()函数用于申请读信号量,因本程序只是读取 VMA 链表,所以申请读者类型即可,若需丢 VMA 链表进行修改,则需申请写者类型信号量;
24-32 行遍历 VMA 链表,并对每个 VMA 打印其起始地址、终止地址和长度信
息;33 行释放读者信号量。
编译内核模块
编写 Makefile 文件,文件名必须为“Makefile”
obj-m := vma_test.o KERNELBUILD := /lib/modules/$(shell uname -r)/build CURRENT_PATH := $(shell pwd) all: make -C $(KERNELBUILD) M=$(CURRENT_PATH) modules clean: make -C $(KERNELBUILD) M=$(CURRENT_PATH) clean
编译
使用 make 命令编译即可。
插入模块
先通过 top 命令查看进程,任意获取一个进程 pid,如图所示, 本例中获取apache2 进程的 pid 3509。
使 用 insmod 插 入 模 块 , 并 传 参 。 如 图 所 示 , 本 例 中 模 块 名 为“vma_test.ko”,pid 为 3509,则插入模块命令为:
insmod vma_test.ko pid=3509
从上图可看到 apache2 进程包含许多 VMA 区域,以第一个 VMA 区域为例,其起始地址为 0x564af2e3a000,结束地址为 564af2ed7000,长度为 643072 字节。
如下图所示为从 proc 虚拟文件系统中查看相应进程第一个 VMA 的完整信息。
从以上两图中内容对比可知,本程序输出信息正确。