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_map
や
PAGE_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