前言:

在 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

  1. 编写模块程序

    参考代码如下,代码文件命名为“vma_test.c”

//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 行释放读者信号量。

  1. 编译内核模块

    编写 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
  2. 编译

    使用 make 命令编译即可。

  3. 插入模块

    先通过 top 命令查看进程,任意获取一个进程 pid,如图所示, 本例中获取apache2 进程的 pid 3509。

    image-20200608163648953

    使 用 insmod 插 入 模 块 , 并 传 参 。 如 图 所 示 , 本 例 中 模 块 名 为“vma_test.ko”,pid 为 3509,则插入模块命令为:

insmod vma_test.ko pid=3509
  1. 查看程序打印信息

    通过 dmesg 命令查看 VMA 信息。如下图所示:

image-20200608164250039

从上图可看到 apache2 进程包含许多 VMA 区域,以第一个 VMA 区域为例,其起始地址为 0x564af2e3a000,结束地址为 564af2ed7000,长度为 643072 字节。

如下图所示为从 proc 虚拟文件系统中查看相应进程第一个 VMA 的完整信息。  

image-20200608164609038

从以上两图中内容对比可知,本程序输出信息正确。