x86_64 Linuxでの仮想アドレス/物理アドレス

Linux Device Driver Chapter 15では Linuxで利用されるアドレスを以下の5種類に分類しています.

  • User virtual address
  • Physical address
  • Bus address
  • Kernel logical address
  • Kenerl virtual address

User virtual addressはその名の通りユーザプロセスが利用する仮想アドレス, Physical addressは物理アドレス,Bus addressはデバイスが使用するアドレス (アーキテクチャ依存で,Physical addressと同じことも多い)です.残りの Kernel logical addressとKenel virtual addressの違いを理解するためには, Linuxでのメモリマップ(仮想ドレスの使い方)を知る必要があります.これはアー キテクチャ依存の話になりますが,ここではx86_64に関して見ていきます.

Linuxでのx86_64のメモリマップは Documentation/x86/x86_64/mm.txt に書いてあります.

0000000000000000 - 00007fffffffffff (=47 bits) user space, different per mm
hole caused by [48:63] sign extension
ffff800000000000 - ffff87ffffffffff (=43 bits) guard hole, reserved for hypervisor
ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory
ffffc80000000000 - ffffc8ffffffffff (=40 bits) hole
ffffc90000000000 - ffffe8ffffffffff (=45 bits) vmalloc/ioremap space
ffffe90000000000 - ffffe9ffffffffff (=40 bits) hole
ffffea0000000000 - ffffeaffffffffff (=40 bits) virtual memory map (1TB)
... unused hole ...
ffffec0000000000 - fffffbffffffffff (=44 bits) kasan shadow memory (16TB)
... unused hole ...
ffffff0000000000 - ffffff7fffffffff (=39 bits) %esp fixup stacks
... unused hole ...
ffffffef00000000 - fffffffeffffffff (=64 GB) EFI region mapping space
... unused hole ...
ffffffff80000000 - ffffffff9fffffff (=512 MB)  kernel text mapping, from phys 0
ffffffffa0000000 - ffffffffff5fffff (=1526 MB) module mapping space
ffffffffff600000 - ffffffffffdfffff (=8 MB) vsyscalls
ffffffffffe00000 - ffffffffffffffff (=2 MB) unused hole

0000000000000000 - 00007fffffffffffの領域がUser virtual addressに相当するも ので,プロセスごとに独自のメモリ空間になります(プロセスごとにpage tableが 切り替わる).それ以外がKernel Virtual Address( カーネルが利用する仮想アドレス,プロセス間で共通)になります. User virtual address以外の領域をカーネルが全部利用しているかというとそうで はなくて,一部の領域は使用されていません.例えばffff800000000000 - ffff87ffffffffffの領域はハイパーバイザ用に予約されています.また, “unused hole"と書いてある場所は何にも利用されていないようです.

さて,それではKernel logical addressが何かと言うと,ffff880000000000 - ffffc7ffffffffffの64TBの領域がそれに相当します.Kernel logical addressは Kernel virtual addressの一部です.この領域は “direct mapping of all phys” と書いてある通り,物理メモリ全体を連続した仮想アドレス空間にマッピン グしている領域です.MMUを利用して仮想アドレス経由で物理メモリにアクセスすると はいえ,カーネルが物理メモリ領域にアクセスできないことがあると困ります.そこ で,この領域に物理メモリ領域全体をマッピングします.こうすることでカーネルが常 に物理メモリ領域にアクセスできるようにします.x86 (32bit) の場合は仮想アドレス 空間が32bitであったため,32bitの物理アドレス全体を仮想アドレス空間マッピング することができず,物理メモリの先頭の1GB(カーネル領域のみ)をマッピングしてお り,さらにそれがhigh memory,low memory領域と別れていましたが,x86_64の場合は そのような問題はとりあえずはありません.(x86_64のアーキテクチャ的には256TBのメ モリ空間をサポートしていますが,今のところ64TBの空間があれば十分でしょう.ただ し,近々出現すると噂されるストレージクラスのメモリ (nvdimm) が登場してくるとも しかすると事情は変わるのかもしれません.)

また,ffffffff80000000 - ffffffff9fffffffアドレス空間が"kernel text mapping"として利用されており,この領域は物理メモリの先頭から512MBがマッピングさ れています.

kmalllc()を利用してカーネル内でメモリを取得した場合,Kernel logical address の範囲のメモリアドレスが得られます.そのため,kmalloc()で取得した仮想アドレ スは,物理メモリ的にも連続しています.vmalloc()で取得したアドレスは,上のメ モリーマップでの"vmalloc/ioremap space"の範囲のアドレスが得られます.要する に,この範囲のアドレスで適切にpage tableが設定されるわけで,必ずしもその仮想ア ドレスが物理的に連続しているとは限りません.

仮想アドレス <=> 物理アドレス 変換

カーネル内にはvirt_to_phys()という関数がありますが,この関数は任意の仮想アド レスを物理アドレスに変換するのではなく,Kernel logical addressあるいはkernel text mappingのアドレスを物理アドレスに変換します. その逆がphys_to_virt()です.virt_to_phys()の実装は以下のように なっています.

https://github.com/torvalds/linux/blob/v4.10/arch/x86/include/asm/io.h#L118

static inline phys_addr_t virt_to_phys(volatile void *address)
{
    return __pa(address);
}

https://github.com/torvalds/linux/blob/v4.10/arch/x86/include/asm/page.h#L41

#define __pa(x)     __phys_addr((unsigned long)(x))

https://github.com/torvalds/linux/blob/v4.10/arch/x86/mm/physaddr.c#L13

unsigned long __phys_addr(unsigned long x)
{
    unsigned long y = x - __START_KERNEL_map;

    /* use the carry flag to determine if x was < __START_KERNEL_map */
    if (unlikely(x > y)) {
        x = y + phys_base;

        VIRTUAL_BUG_ON(y >= KERNEL_IMAGE_SIZE);
    } else {
        x = y + (__START_KERNEL_map - PAGE_OFFSET);

        /* carry flag will be set if starting x was >= PAGE_OFFSET */
        VIRTUAL_BUG_ON((x > y) || !phys_addr_valid(x));
    }

    return x;
}

virt_to_phys()から__phys_addr()が呼ばれ,そこから物理アドレスを計算するた めにオフセットが引かれます.__phys_addr()ではif文で処理が別れていますが,if の最初のブロックがkernel text mappingのアドレスに対する処理,else部分のブロッ クがkernel logical addressのアドレスに対する処理です.__START_KERNEL_mapPAGE_OFFSETのアドレスは以下で定義されています.

https://github.com/torvalds/linux/blob/v4.10/arch/x86/include/asm/page_64_types.h#L39

#define __PAGE_OFFSET_BASE      _AC(0xffff880000000000, UL)
#ifdef CONFIG_RANDOMIZE_MEMORY
#define __PAGE_OFFSET           page_offset_base
#else
#define __PAGE_OFFSET           __PAGE_OFFSET_BASE

#define __START_KERNEL_map      _AC(0xffffffff80000000, UL)

上のメモリマップのアドレスと比較すれば,一致していることが分かります.

ちなみに,vmalloc()で得られた仮想アドレスから物理アドレスを求める場合は, PFN_PHYS( vmalloc_to_pfn( address )) を使えば良いようです.pfnとはpage frame numberの略で,要するに各ページに連番で振られた番号のことです.linuxは物理メモ リをpage単位で管理しており,pfnから物理アドレスを求めることが可能です.

https://github.com/torvalds/linux/blob/v4.10/mm/vmalloc.c#L269

unsigned long vmalloc_to_pfn(const void *vmalloc_addr)
{
    return page_to_pfn(vmalloc_to_page(vmalloc_addr));
}

https://github.com/torvalds/linux/blob/v4.10/mm/vmalloc.c#L235

struct page *vmalloc_to_page(const void *vmalloc_addr)
{
    unsigned long addr = (unsigned long) vmalloc_addr;
    struct page *page = NULL;
    pgd_t *pgd = pgd_offset_k(addr);

    /*
    * XXX we might need to change this if we add VIRTUAL_BUG_ON for
    * architectures that do not vmalloc module space
    */
    VIRTUAL_BUG_ON(!is_vmalloc_or_module_addr(vmalloc_addr));

    if (!pgd_none(*pgd)) {
        pud_t *pud = pud_offset(pgd, addr);
        if (!pud_none(*pud)) {
            pmd_t *pmd = pmd_offset(pud, addr);
            if (!pmd_none(*pmd)) {
                pte_t *ptep, pte;

                ptep = pte_offset_map(pmd, addr);
                pte = *ptep;
                if (pte_present(pte))
                    page = pte_page(pte);
                pte_unmap(ptep);
            }
        }
    }
    return page;
}

vmallod_to_page()ではこのようにpage tableを動的に参照して物理アドレスを求め ます.もしpageがメモリ常にない場合はNULLが返ります.

PFN_PHYS()はただ単にpage tableのオフセット分だけシフトするだけです. https://github.com/torvalds/linux/blob/v4.10/include/linux/pfn.h#L20

#define PFN_PHYS(x) ((phys_addr_t)(x) << PAGE_SHIFT)

あとはこれで得られたアドレスにオフセット(つまり x & ((1 << PAGE_SHIFT) -1) を足せば,物理アドレスが得られます.

また,あまりないと思いますがmalloc()等で取得したユーザ空間のアドレスを物理アドレスに変換したい場合 は,/proc/self/pagemapを見る方法があるようです(参考: http://stackoverflow.com/a/28987409).

実際のアドレスの確認

簡単なカーネルモジュールを書いてアドレスを確認してみると以下のように なりました.

#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>

static int hello_init(void)
{
    void *kaddr = kmalloc(1024, GFP_KERNEL);
    void *vaddr = vmalloc(1024);
    pr_info("kmalloc addr: %p (%lx), %d, %d\n", kaddr, (unsigned long)virt_to_phys(kaddr), __virt_addr_valid((unsigned long)kaddr), is_vmalloc_addr(kaddr));
    pr_info("vmalloc addr: %p, %d, %d\n", vaddr, __virt_addr_valid((unsigned long)vaddr), is_vmalloc_addr(vaddr));
    kfree(kaddr);
    vfree(vaddr);
    return 0;
}

static void hello_exit(void)
{
}

module_init(hello_init);
module_exit(hello_exit);
% make
% sudo insmod hello.ko
% dmesg | tail -n2
[1123700.799923] kmalloc addr: ffff9e5d971f6000 (1571f6000), 1, 0
[1123700.799925] vmalloc addr: ffffb5ee419a5000, 0, 1

…あれ…なんかvmallocのアドレスがメモリマップのドキュメントの"vmalloc/ioremap space"の範囲にない.. でもis_vmalloc_addr()の結果は正しい..

おかしい..と少し悩みましたがよくよくソースを見てみると__PAGE_OFFSETを設定し ている箇所でCONFIG_RANDOMIZE_MEMORYとかいう明らかに怪しいものがあることに 気づきました (https://github.com/torvalds/linux/blob/v4.10/arch/x86/include/asm/page_64_types.h#L40). このオプションですが,Linux 4.8から導入されたもので,その名の通 りこれが設定されているとメモリセクションのアドレスがセキュリティを高めるために ランダム化されるようです (http://lkml.iu.edu/hypermail/linux/kernel/1607.3/00404.html).自分のカーネルのconfig確認したら確かにCONFIG_RANDOMIZE_MEMORY=yとなっていました.

カーネルモジュールで__PAGE_OFFSETのアドレスを確認してみると,確かにランダム化 されていました.

    pr_info("__PAGE_OFFSET: %lx\n", __PAGE_OFFSET);
    pr_info("VMALLOC_START: %lx\n", VMALLOC_START);
    pr_info("VMALLOC_END:   %lx\n", VMALLOC_END);
[1123873.411375] __PAGE_OFFSET: ffff9e5c40000000
[1123873.411377] VMALLOC_START: ffffb5ee40000000
[1123873.411378] VMALLOC_END:   ffffd5ee3fffffff

このランダム化処理ですが,arch/x86/mm/kaslr.cでやっているようです. https://github.com/torvalds/linux/blob/v4.10/arch/x86/mm/kaslr.c#L147