Mach-O
概要
ファイルフォーマット
Mach-O ファイルは、 3 つの主要な領域から構成されている。 Header の後には、複数の Load Command が続く。Load Command には主に自身が格納しているプログラムを実行するのに必要な情報が格納され、そのサイズや構造は各 Load Command 毎に異なる。Mach-O ファイルの構造 (どんな Segment や Section があって、それらがファイル内のどこに位置しているか) や、それらを 仮想メモリ 上にどのように展開するか、プログラム内で最初に実行されるべき場所の情報や、実行に必要な外部の Dynamic Library をロードするのに必要な情報等が保持されているようだ。 Load Commands の後には複数の Segment が続く。Segment はそのまま 仮想メモリ にマッピングされる都合上、ページサイズ単位で区切られている。仮想メモリ のページサイズは CPUアーキテクチャ によって異なるため、各 Segment のサイズも同様に CPUアーキテクチャ によって異なる。例えば 64bit CPU であれば 4KB であり、arm64 であれば 16KB となる。 各 Segment はさらに 0 以上の Section から構成される。Segment 内のデータは、この Section として保持される。Segment のサイズはページサイズ単位で丸められるが、Section のサイズは自由である。Segment のサイズは、Section の合計サイズをページサイズに丸めたもの、とも捉えられる。dyld 等のプロセスによって 仮想メモリ にマッピングされる場合、そのレイアウトは Load Command による定義 (及びファイルの種類) に従うことになる。 Segment も Section も命名規則がある。どちらも先頭に2つのアンダースコア (__) をもち、Segment は全て大文字、Section は全て小文字が続く。主な Segment には __TEXT, __DATA, __LINKEDIT がある。
https://gyazo.com/a65742f0ee72fbe4426a94e3f8196f3a
参考:
Segment - __TEXT
先頭に位置する Segment。Read-only であり、実行可能コードと定数値を格納している。慣例として、コンパイラは最低1つの __TEXT Segment を生成する。Read-only であるため、カーネルが一度 Mach-O ファイルから __TEXT を 仮想メモリ へマッピングしたら、その後再度マッピングする必要はない。例えば展開している Mach-O ファイルが Framework や共有ライブラリであった場合、一度 仮想メモリ にマッピングされたら、それを利用する全てのプロセスは 仮想メモリ を参照すればよく、I/O を発生させずに済む。 table:__TEXT Segment の主な Section
Section 概要
__text コンパイル後のマシンコード
__const 定数データ
__cstring 定数文字列 (ソースコード内で " で囲んで定義された文字列など)
Segment - __DATA
読み書き可能な Segment。__TEXT は静的な定数データを保持しているのに対して、__DATA は動的なデータを保持する。読み書き可能であるため、CoW により利用するプロセス毎にメモリ空間上にコピーが生成される。 table:__DATA Segment の主な Section
Section 概要
__data 初期化されたグローバル変数 (int a = 1; とか static int a = 1; とか)
__bss 初期化されていない静的変数 ( static int a; とか)
__const 再割り当てが必要となる定数データ (char * const p = "foo"; とか)
Segment - __LINKEDIT
Mach-O バイナリの末尾の Segment。基本的には、どのようにプログラムをロードするか?についてのメタデータを含んでいる。これは、例えば利用する関数や変数の名前とそのアドレスなどになる。 参考
ファイルの構造がわかったところで、実際に Mach-O ファイルの中身を覗いてみる。そのためには覗く対象となる Mach-O ファイルが必要となるけれど、これを得る方法は簡単で、適当なソースコードをコンパイルすれば良い。 以下のようなファイルを用意して、
code:c
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
code:shell
$ xcrun clang hello.c
$ ./a.out
Hello World!
Header の内容を確認する
code:c
struct mach_header
{
unsigned long magic;
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
unsigned long filetype;
unsigned long ncmds;
unsigned long sizeofcmds;
unsigned long flags;
};
code:shell
$ otool -h -v a.out
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL LIB64 EXECUTE 16 1368 NOUNDEFS DYLDLINK TWOLEVEL PIE
Segment と Section を一覧する
size コマンドを利用すると、Segment 及び Section を確認できる。5 つの Segment が存在し、そのいくつかは Section を保持していることがわかる。
code:shell
$ xcrun size -x -l -m a.out
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
Section __text: 0x31 (addr 0x100000f50 offset 3920)
Section __stubs: 0x6 (addr 0x100000f82 offset 3970)
Section __stub_helper: 0x1a (addr 0x100000f88 offset 3976)
Section __cstring: 0xe (addr 0x100000fa2 offset 4002)
Section __unwind_info: 0x48 (addr 0x100000fb0 offset 4016)
total 0xa7
Segment __DATA_CONST: 0x1000 (vmaddr 0x100001000 fileoff 4096)
Section __got: 0x8 (addr 0x100001000 offset 4096)
total 0x8
Segment __DATA: 0x1000 (vmaddr 0x100002000 fileoff 8192)
Section __la_symbol_ptr: 0x8 (addr 0x100002000 offset 8192)
Section __data: 0x8 (addr 0x100002008 offset 8200)
total 0x10
Segment __LINKEDIT: 0x1000 (vmaddr 0x100003000 fileoff 12288)
total 0x100004000
このファイルが実行されプロセスが生成された時、各 Segment が、カーネルによって 仮想メモリ 上にマッピングされる。この時、__TEXT Segment は read-only かつ実行可能、__DATA Segment は read/write かつ実行不可なパーミッションでマッピングされる。 Section
Section の内容は otool を利用すると確認できる。例えば、__TEXT Segment の __text Section には、機械語コードが格納されているはず。 code:shell
$ xcrun otool -s __TEXT __text a.out
a.out:
Contents of (__TEXT,__text) section
0000000100000f50 55 48 89 e5 48 83 ec 20 c7 45 fc 00 00 00 00 89
0000000100000f60 7d f8 48 89 75 f0 48 8d 3d 35 00 00 00 b0 00 e8
0000000100000f70 0e 00 00 00 31 c9 89 45 ec 89 c8 48 83 c4 20 5d
0000000100000f80 c3
これだと見にくいので、もうちょっと見やすくしたい。そのような場合、以下のようにして disassembled したコードを確認することができる。
code:shell
$ xcrun otool -v -t a.out
a.out:
(__TEXT,__text) section
_main:
0000000100000f50 pushq %rbp
0000000100000f51 movq %rsp, %rbp
0000000100000f54 subq $0x20, %rsp
0000000100000f58 movl $0x0, -0x4(%rbp)
0000000100000f5f movl %edi, -0x8(%rbp)
0000000100000f62 movq %rsi, -0x10(%rbp)
0000000100000f66 leaq 0x35(%rip), %rdi
0000000100000f6d movb $0x0, %al
0000000100000f6f callq 0x100000f82
0000000100000f74 xorl %ecx, %ecx
0000000100000f76 movl %eax, -0x14(%rbp)
0000000100000f79 movl %ecx, %eax
0000000100000f7b addq $0x20, %rsp
0000000100000f7f popq %rbp
0000000100000f80 retq
他の Section も見てみる。__cstring には文字列定数が含まれているはず。確認すると、確かにソースコード上で記述した文字列定数 "Hello World!" が存在していることがわかる。
code:shell
$ xcrun otool -v -s __TEXT __cstring a.out
a.out:
Contents of (__TEXT,__cstring) section
0000000100000fa2 Hello World!\n
参考
=== 下記は WIP ===
Mach-O 実行ファイルのアプリケーションコードが実行されるまでには、カーネル、CPU 等によって様々な事前処理が行われる。その部分がどうなっているか?について、WWDC 2016 のセッションで行われていた話をまとめてみる。 まず、__TEXT Segment から Mach ヘッダーを メモリ上で 読み取ろうとする。しかし該当アドレスは空なのでページフォールトが発生する。すると(おそらくカーネルのページフォールトハンドラーによって)実ファイルの最初のページがアドレス空間上に展開される。これで dyld が Mach ヘッダーを読めるようになる。 Mach ヘッダーをみると __LINKEDIT のどの情報を読み取るべきかがわかるので、次は __LINKEDIT Segment を見にいく。ここでもページフォールトが発生した後、仮想メモリ上に Segment の内容がマップされる。 __LINKEDIT をみると、この Dynamic Library を実行可能にするために、どのような fix-up (後述) が必要か?がわかる。この必要な修正を加えるため、今度は __DATA Segment を読み出そうとしてページフォールトとアドレス空間へのマップが発生する。さらに __DATA Segment が他と異なるのは、__DATA Segment のみ 書き込み可能である という点。書き込みを行うと CoW が発生し、dirty page になる。 https://gyazo.com/ee7b3ad66b0c1a9217ad35622427dde5
手順は最初のプロセスと同様だが、今度は __TEXT Segment は既にメモリ上に乗っている。そのため、I/O を発生させる必要はなく、仮想メモリ アドレスに既存の物理メモリアドレスをマッピングするだけで良い。これで、最初と今回の仮想メモリアドレス空間上の最初のページは、どちらも同一の物理メモリアドレスを参照することになるはず。そしてこれは __LINKEDIT Segment でも同様となる。 ところが、__DATA Segment はプロセス毎に dyld によって内容が書き換えられているので、dirty page であり、プロセス毎に内容が異なる。従って、別々のメモリアドレスがマッピングされる。 最後に、__LINKEDIT Segment の情報はdyldが処理を行っている間のみ必要になる。処理が終えたことをカーネルに伝えると、カーネルによってアドレスが解放される。 https://gyazo.com/8cd0ceeb7885830186d77c35a0a7e7bd
3ページの Dynamic Library を 2 プロセスから利用した場合、最終的に 1 つの clean page と 2 つの dirty page が残ることがわかる。 アプリケーションのロード
Mach-O イメージ単体ではなく、もっと広い視点、アプリケーション自体のロード時の処理を見ていく。 アプリケーションの実行は、大抵 exec システムコールによって開始される。exec システムコールは、それをコールしたプロセス自体を、新しいプロセスで置き換え、実行ファイルの内容をメモリアドレス空間上に展開する。この実行ファイルは、今回の場合 Mach-O ファイルになる。 exec システムコールを実行すると、ASLR によってアドレス空間上のランダムな位置に Mach-O ファイルの内容がマッピングされ、NX によってアクセス不可領域ができる。 その後、カーネルは dyld をロードする。このロード時も ASLR によってランダムな位置に dyld がロードされる。そして、dyld によって実行ファイルが開始される。 dyld にはいくつかの実行手順があり、各々見ていく。大きく 5 つのステップがある。 2. Fix-up: 各 Mach-O ファイルが実行可能になるように修正を加える 3. Initializers: 初期化を実行し、main 関数を呼び出す
1. Load dylibs
2. Fix-up
仮想メモリ 上に必要な Mach-O ファイルが全て展開されても、各々は独立している。プログラムを走らせるためにはそれらを bind する必要があり、そのための一連の処理が fix-up と呼ばれる。 この時に利用される技術が Dynamic PIC (Position Independent Code) というものらしい。PIC はコード生成技術であり、dyld が実行毎に異なる 仮想メモリ アドレスに存在するコード領域をロードできるようにする。具体的には、__DATA 内のポインタを書き換えて、実際に利用したい機能が存在する場所へポインタを向けるようなことをするらしい。これは、Linux の ELF における GOT (Global Offset Table) と似たような概念とのこと。 ここで、そのイメージ内にポインタを向ける操作を rebase, イメージ外にポインタを向ける操作を bind という。xcrun dyldinfo を利用すると、rebase と bind で書き換えられる対象の一覧が表示できるようだ。 2-1. Rebase
昔は preferred load address を指定でき、その指定されたアドレスにロードされた場合は修正は必要なかった。しかし現在は ASLR によってランダムなアドレスにロードされるため、イメージ内部の内部向けのポインタの値が、ランダムにロードされたアドレス分ずれてしまう。このずれた分、各々のポインタ値をスライドさせる必要がある。ひたすらに読み書きをしていくだけなので、することはシンプル。 ただ、書き込みを行うたびにページフォールトが発生してしまい、I/O が頻発して効率が悪くなってしまう。そこで、dyld はこれを手前から順番に行うことを保証している。これによって、カーネルは手前から順番に読み出される、ということを知っているので、先読みして I/O のコストを削減できるようになっているらしい。 2-2. Bind
Bind はイメージの外を指すポインターを修正する操作。外部へのポインターの指示は対象のシンボルの文字列として表現されている。そのため、dyld はそのシンボルの実装を見つける必要があり、そのためにシンボルテーブルを調べる必要がある。Rebase よりも複雑だが、すでに Rebase が終了した後なので I/O は発生しない。 2-3. ObjC
3. Initializers
依存ライブラリ毎の Initializer の依存グラフを生成し、ボトムアップで実行していく。最終的に main 関数を呼び出す。
参考