Linux x86_64のメモリアドレッシング

  • 23
    いいね
  • 0
    コメント

概要

物理アドレスとリニアアドレス(仮想アドレス)、論理アドレスの違い、ややこしいですよね。

Linuxカーネルを読むのに比較的理解が難しい(と勝手に思っている)メモリアドレッシングについて、まとめてみました。
詳解Linuxカーネルだと概要の次の章にあるので簡単かと思いきや、半分くらいx86の機能の話なのでソースコードを読んでもよくわからない部分です。

結論から言うと、Linuxではセグメンテーションを使わないので、リニアアドレスは論理アドレスと一緒です。ページングによって物理アドレスにマッピングされます。

参考文献

無料とは思えない情報量です。さすがIntelとAMD。普段はOSに隠蔽されてるので気にしませんが、こんなビットごとの意味が書いてある表をみていると秋葉原で買えるマイコンの仕様書みたいで、そういえばCPUも電子部品なんだなぁという実感がありますね。

物理メモリとアドレス

プログラムが使うメモリアドレスから物理メモリまでは次のように変換されます。

論理アドレス --[セグメンテーション機構]--> リニアアドレス --[ページング機構]--> 物理アドレス

ここでの用語はこんな感じです。

用語 意味 別名
Logical address 論理アドレス セグメントセレクタ:オフセットで表される far pointer
Effective address 論理アドレスのオフセットのこと near pointer
Liner address リニアアドレス セグメントのベースアドレス+ Effective address virtual address
Pysical address 物理アドレス 物理的なメモリにアクセスするときのアドレス、Linerアドレスから変換する(後述)

前述のように、Linuxはセグメンテーションを使いません、x86ではIntelのマニュアルで言うところの「3.2.1 Basic Flat Model」を利用します。また、そもそもx86_64の64bitモードではセグメンテーションが使えません。セグメンテーション機構はLegacy-modeと書かれているので、実行時に指定できるアドレスの範囲より物理メモリのサイズが大きかった8086の名残みたいな機能なのでしょう、きっと。

しかしながら、どのユーザプログラムも他のプロセスがどの部分の物理メモリを使っているか意識したくないので、各プロセスはそれぞれ仮想的なアドレス空間で動作します。ページング機構によって物理アドレスとリニアアドレスのマッピングを管理することで、それを実現しています。

ページングの仕組み

メモリをページフレームという一定サイズの領域に分割します。そして、ページフレームとそこに格納するページの対応を管理することで、効率的に(本当に使うところだけ)物理メモリを利用できます。しかし、単なる配列で対応を管理してしまうと、利用ページフレーム数分の配列が必要となるため、スパースに利用されるメモリでは無駄が多くなります。そこで、木構造で対応を管理して使っていない枝をアロケートしないことで無駄を省きます。

単なる配列の例.png
図 単なる配列の例(網掛けは確保済みの領域)

木構造の例.png
図 木構造の例(白矢印はポインタ、黒はオフセットの指定)

管理構造のルート(ページディレクトリ)をポイントするのがcr3レジスタです。このレジスタを切り替えることで、仮想的なメモリ空間の切り替えを実現します。
ところで、このメモリアクセスごとに動きそうなページングの処理自体はカーネルのコードには書かれていません。これはCPUの機能なので、カーネルにあるのはこの構造を作るコードだけです。

カーネルのメモリマッピング

アドレス空間をページングで切り替えできるのはいいですが、カーネルは全体のメモリが見れないと困ります。
そこで、次のように低位のメモリはプロセスごとに切り替え、カーネルが使う部分のページは先に作って使いまわします。

"direct mapping of all phys. memory"の箇所にすべてのメモリがマッピングされます。64bitなのに64TBで済んでいるのはLinuxでは46bitの物理メモリを上限としているからで、"hole caused by [48:63] sign extension"といって真ん中のアドレスがスパッと空いているのはCPUの仕様(48bitまでしかリニアアドレスを使えない)から来る制限です。

Documentation/x86/x86_64/mm.txt
Virtual memory map with 4 level page tables:

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

プロセスから見たメモリマッピング

procfsから、こんな風に見ることができます。
"0000000000000000 - 00007fffffffffff"の中で、最初の方にコード、次にヒープがあって、高位の方にスタックがありますね。共有ライブラリはスタックの下にマップされるようです。

juntaki@baredev ~> cat /proc/12642/maps
00400000-00407000 r-xp 00000000 fc:00 2361965                            /bin/sleep
00606000-00607000 r--p 00006000 fc:00 2361965                            /bin/sleep
00607000-00608000 rw-p 00007000 fc:00 2361965                            /bin/sleep
010bd000-010de000 rw-p 00000000 00:00 0                                  [heap]
7f105e03a000-7f105e40f000 r--p 00000000 fc:00 917629                     /usr/lib/locale/locale-archive
7f105e40f000-7f105e5ce000 r-xp 00000000 fc:00 2231903                    /lib/x86_64-linux-gnu/libc-2.23.so
7f105e5ce000-7f105e7ce000 ---p 001bf000 fc:00 2231903                    /lib/x86_64-linux-gnu/libc-2.23.so
7f105e7ce000-7f105e7d2000 r--p 001bf000 fc:00 2231903                    /lib/x86_64-linux-gnu/libc-2.23.so
7f105e7d2000-7f105e7d4000 rw-p 001c3000 fc:00 2231903                    /lib/x86_64-linux-gnu/libc-2.23.so
7f105e7d4000-7f105e7d8000 rw-p 00000000 00:00 0
7f105e7d8000-7f105e7fe000 r-xp 00000000 fc:00 2228256                    /lib/x86_64-linux-gnu/ld-2.23.so
7f105e9ce000-7f105e9d1000 rw-p 00000000 00:00 0
7f105e9fb000-7f105e9fd000 rw-p 00000000 00:00 0
7f105e9fd000-7f105e9fe000 r--p 00025000 fc:00 2228256                    /lib/x86_64-linux-gnu/ld-2.23.so
7f105e9fe000-7f105e9ff000 rw-p 00026000 fc:00 2228256                    /lib/x86_64-linux-gnu/ld-2.23.so
7f105e9ff000-7f105ea00000 rw-p 00000000 00:00 0
7ffca191c000-7ffca193d000 rw-p 00000000 00:00 0                          [stack]
7ffca1999000-7ffca199b000 r--p 00000000 00:00 0                          [vvar]
7ffca199b000-7ffca199d000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
この投稿は Linux Advent Calendar 201619日目の記事です。