【Kernel】Linux Huge Pages

在HITCON CTF 2023的一道QEMU escape题目中,利用到了Huge Pages这项技术,借此复现题目的机会,来学习了解下这部分知识。

简介

当进程使用 RAM 时,CPU 会将其标记为已被该进程使用。为了提高效率,CPU 以块的形式分配 RAM——4K 字节是许多平台上的默认值。这些块被称为页面。页面可以交换到磁盘等。

由于进程地址空间是虚拟的,CPU和操作系统需要记住哪些页属于哪个进程,以及每个页存储在哪里。页面越多,查找内存映射位置所需的时间就越多。因而大多数当前的 CPU 架构都支持larger-than-default pages,这使得 CPU/OS 需要查找的条目更少。不同的操作系统对它有不同命名方式,在Linux上即为Huge Pages.

内存映射

首先回顾下Linux中的内存映射,下图展示了64位Linux系统中,虚拟内存与物理内存的转换方式,也体现了Linux的4级页表结构

偷的图,侵删😭

img

有图可知,Linux只使用了64位地址的前48位,并将其虚拟地址分为5个部分:

  • PGD索引,为虚拟地址的39-47位(9bit),代表其在Page Global Directory上的索引
  • PUD索引,为虚拟地址的30-38位(9bit),代表其在Page Upper Directory上的索引
  • PMD索引,为虚拟地址的21-29位(9bit),代表其在Page Middle Diectory上的索引
  • PTE索引,为虚拟地址的12-20位(9bit),代表其在Page Table上的索引
  • 偏移量,为虚拟地址的0-11位(12bit),代表其在对应Physical Page上的偏移

此外,CPU有几个控制寄存器,如下图CR0-CR4,各自有不同的作用。其中CR3的寄存器主要用于存放Page Global Directory的物理地址。详细的的讲解可以看这篇文章:控制寄存器 (CR0 , CR1 , CR2 , CR3)

img

基于上述的知识我们可以分析下,虚拟地址转换物理地址流程。

  1. 从CR3寄存器中取得Page Global Directory的物理地址
  2. 利用虚拟地址中的PGD索引部分,在Page Global Directory中取出Page Upper Directory的物理地址
  3. 利用虚拟地址中的PUD索引部分,在Page Upper Directory中取出Page Middle Diectory的物理地址
  4. 利用虚拟地址中的PMD索引部分,在Page Middle Diectory中取出Page Table的物理地址
  5. 利用虚拟地址终端PTE索引部分,在Page Table中取出对应物理内存页地址
  6. 此时虚拟地址所对应的物理地址就可以得到了,计算公式:物理地址 = 虚拟地址低12位offset + 物理内存页地址

Huge Pages原理

在明白了上述内存地址的划分后,Huge Page的原理就比较好理解了,即扩大物理页大小,减少页表层级。在Linux中,主要的HugePage的大小为2MB(即12bit + 9bit)和1GB(即12bit + 9bit + 9bit),下面以2MB的HugePage举例

img

可以很明显的看到由于页大小扩大至2MB,其虚拟地址中,表示偏移的部分扩大到了(12+9)bit。原先的4级页表也变为3级页表,同理,1G大小的页对应2级页表。所以此时 Page Middle Directory 直接指向映射的物理内存页地址。

这样我们也能够理解,为什么HugePage一般为2MB和1GB这样固定的大小。从另一个角度理解,我们也能看到这种地址划分方式与IP地址中A类,B类,C类地址的划分有异曲同工之妙。

如何设置 HugePages

root权限下,我们可以使用如下命令,设置HugePages的数量

sysctl vm.nr_hugepages=32 #其中32代表个数

我们可以使用如下命令,查看是否设置成功

cat /proc/meminfo | grep -i huge

效果如图,能够显示出总共的Hugepages的数量,以及处于free状态的HugePages的数量

image-20230911143756972