NaCl のメモリレイアウトについて

前から図でも書こうかなぁと思ってたのですが、機会があったのでとりあえずスプレッドシートを作ってみました。

https://docs.google.com/spreadsheet/ccc?key=0AolcvzoWgN21dG5PVldnM0VJUzByU2hqcXFQaGVVdWc&usp=drive_web

ひとつ前で書いた通り、 NaCl では text は最初の 256MB に位置していて、 data はその後の 768MB を使うことになっています (x86-64 では data は 3840MB)。普通の linux バイナリでは text の直後に data が配置されるようになってるため、大雑把に言うと

main binary の text
main binary の data
隙間
libc.so の text
libc.so の data
隙間
...

というように配置されるわけですけど、 NaCl (glibc) では

main binary の text
libc.so の text
...
隙間
main binary の data
libc.so の data
...

というように配置されます。こういうことを実現するために、 NaCl では ELF binary を作った時点で text と data の距離が 256MB 離れているバイナリが生成されます。例えば適当に作った hello は、通常の linux バイナリでは

$ readelf -l a.out | grep LOAD
  LOAD           0x000000 0x08048000 0x08048000 0x0059c 0x0059c R E 0x1000
  LOAD           0x00059c 0x0804959c 0x0804959c 0x00118 0x0011c RW  0x1000

という感じで、 PT_LOAD が連続して配置されているんですけど、 NaCl バイナリでは

$ readelf -l a.out | grep LOAD
  LOAD           0x010000 0x01000000 0x01000000 0x10000 0x10000 R E 0x10000
  LOAD           0x020000 0x11000000 0x11000000 0x00230 0x00230 R   0x10000
  LOAD           0x020230 0x11010230 0x11010230 0x0010c 0x0010c RW  0x10000
  LOAD           0x030000 0x11020000 0x11020000 0x00000 0x00020 RW  0x10000

のように、最初の PT_LOAD (text) とその次 (rodata) の間に 256MB==0x10000000 bytes の隙間があります。このため、一つ前で mmap された場所によって、 text が入っても data が入らない場合や、 data が入っても text が入らない場合があるので、 text と data の大きい方のサイズ分だけ両方の領域が消費されます。例えば一つ目のバイナリの text が 1MB で data が 2MB の場合、次のバイナリは text も data も 2MB 進んだところに配置されるし、逆に text が 2MB で data が 1MB の場合も同様です。

このため、空間効率の良い NaCl バイナリは text と data のサイズが近いバイナリってことになるんで、データだけが入ってる so とかは text がムダになるのであまり作りたくない感じになっています。

現状、 so は dlclose されても munmap はするけど消費された領域は再利用しない実装になってるため (直す予定はあるみたいですけど) dlopen と dlclose を繰り返すと領域を使い果たします。そのあたりのコードは https://chromium.googlesource.com/native_client/nacl-glibc/+/master/sysdeps/nacl/nacl_dyncode_alloc.c にあります。余談ですけどこのコードはなんでこんなややこしい書き方するんだろ、ってコードになってて読みにくいです…

また、固定で置き場所が決まっているバイナリなどがいくつかあります

  • 0x10000 から 0x20000 は verify されない、 service runtime が NaCl syscall の entry point への jmp 命令を置く空間になってます。直接 service runtime のコードへの jmp は verifier が許さないので、 Pepper API を使う場合などは、ここを経由して NaCl syscall することになっています
  • 0x20000 から 0x1000000 までの空間はダイナミックローダ (runnable-ld.so) の text が置かれます。同様に 0x10020000 から 0x11000000 は runnable-ld.so の data が配置されます。残った空間は nacl_dyncode_create や mmap で使うことはできると思いますけど。
  • 0x1000000 から 0x0fa00000 までの空間は、 so が順次置かれていきます。 nacl_dyncode_create で JIT したコードを置きたい場合は、後続の dlopen で使う領域と競合されないように、 0x0fa00000 より少し小さいアドレスを適当に使うと良いみたいです。まぁこのへんは service runtime 側でちゃんと allocation して欲しい感じで、まぁ直す予定はあるみたいです…
  • 0x0fa00000 から先は IRT が使います。 IRT は text と data の gap が 256MB ではなくて、 untrusted code が使える data 領域の最後の部分、具体的には 0x3ef00000 から先が使われるようになってるみたいです。
  • so が配置された後、 IRT の前の data 領域は、 NaCl syscall の mmap が後ろから順に適当に使っていくみたいです。たくさん allocation すると so のための領域とぶつかるので、 dlopen やら mmap が失敗しはじめるはずです。
  • 今回調べてて気付いたのですが、 IRT の data 領域の後の領域は main thread の stack になるみたいです。ここに stack guard 無いのは、あった方がいい気しかしないです…こわい

というようなことをまとめたのが最初の spread sheet になっています。

newlib や static link した glibc のバイナリは so が無いのでシンプルで、 newlib の場合 runnable-ld.so が置かれる場所に、 static link した glibc のバイナリは dynamic link された場合の main binary の場所に main binary が置かれます。

残念ながら、これも NaCl のメモリレイアウトについての世界一詳しい記述だと思いますね…

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h