複数の仮想ページに同じ物理ページをマッピングする方法 (Linux)

小ネタ.

Linuxで複数の仮想ページを一つの物理ページにマッピングする方法です.

連続した仮想ページを全て同じ物理ページにマッピングしたいということがあって,原理的にはページテーブルで同じ領域を指すだけです.でもユーザ空間からはどうするんだっけ?と思ったらそういえばspectreのPOCでそんなことやっていたなと思い出したのでそれを参考に作ってみました.

#define _GNU_SOURCE

#include <assert.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

void *map(int num_pages) {
    int fd, pagesize = getpagesize();
    void *area;
    char *start, *end, *p, *q;
    size_t length = pagesize * num_pages;

    // reserve virtual address space
    area = mmap(NULL, length, PROT_NONE,
                MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
    assert(area != MAP_FAILED);
    assert(munmap(area, length) == 0);

    // create temp file whose size is a pagesize
    fd = open("/tmp", O_TMPFILE | O_RDWR, 0600);
    assert(fd != -1);
    assert(ftruncate(fd, pagesize) == 0);

    // mmap each virtual page to the same physical page
    start = area;
    end = start + length;
    for (p = start; p < end; p += pagesize) {
        q = mmap(p, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC,
                 MAP_SHARED | MAP_FIXED | MAP_POPULATE | MAP_NORESERVE, fd, 0);
        assert(p == q);
    }
    assert(close(fd) == 0);

    return area;
}

仕組みとしてはサイズがページサイズのファイルをopen(ここではtempfileを作成)し,そのfdに対してmmapするだけです.mmapの第一引数で仮想アドレスが指定できます.

ここでは連続した仮想アドレスを同じページにmmapするということをしています. ここで問題となるのは,空いている仮想アドレス空間が確保できるかどうかという点です. ユーザが適当に仮想アドレスを決めた場合,すでにそのアドレスが使用されている可能性があります(mmapは失敗する).

そこでここでは最初に必要な仮想アドレス空間の領域をmmapしています. mmapの第一引数をNULLにすればOSが適当な領域を見つけてくれます. また,MAP_NORESERVE指定することで,物理ページの割り当てなしに領域の確保が可能です. 使用可能なアドレスが分かったら,その領域にmmapするために一旦unmapしてあとは先述の方法でmmapしていきます.

本当に同じアドレスにマッピングされているかは/proc/self/pagemapから確認できます(ちなみにproc/self/pagemapの参照にはCAP_SYS_ADMIN(root権限)が必要です).

static uintptr_t virt_to_phys(void *virt) {
    long pagesize = getpagesize();
    int fd = open("/proc/self/pagemap", O_RDONLY);
    assert(fd != -1);
    off_t ret =
        lseek(fd, (uintptr_t)virt / pagesize * sizeof(uintptr_t), SEEK_SET);
    assert(ret != -1);
    uintptr_t entry = 0;
    ssize_t rc = read(fd, &entry, sizeof(entry));
    assert(rc > 0);
    assert(entry != 0);
    assert(close(fd) == 0);

    return (entry & 0x7fffffffffffffULL) * pagesize +
           ((uintptr_t)virt) % pagesize;
}

int main() {
    int pagesize = getpagesize();
    int i, num_pages = 100;
    char *p = map(num_pages);

    for (i = 0; i < num_pages; i++, p += pagesize) {
        printf("%016llx, %016llx\n", (unsigned long long)p,
               (unsigned long long)virt_to_phys(p));
    }

    return 0;
}

実行結果

% sudo ./a.out
00007f6e950f7000, 0000000fe7773000
00007f6e950f8000, 0000000fe7773000
00007f6e950f9000, 0000000fe7773000
00007f6e950fa000, 0000000fe7773000
00007f6e950fb000, 0000000fe7773000
00007f6e950fc000, 0000000fe7773000
00007f6e950fd000, 0000000fe7773000
00007f6e950fe000, 0000000fe7773000
...

上記コードはここにあります.