Mach-Oのヘッダとロードコマンドを読み込む
動的ライブラリの仕組みを知りたかったので、その準備としてMach-Oの構造について調べてみた。環境はx86_64。
Mach-Oとは
Mac OS Xでプログラムやライブラリをディスクに格納するために標準で使用されるフォーマット。中間形式(リンク前)にも最終形式(リンク後)にも用いられる。コード、データの両方を格納できる。
Mach-Oは一つのファイルが一つのアーキテクチャ(CPU)に対応する。複数のアーキテクチャに対応するものはMach-Oファイルではなく、複数のMach-Oファイルをまとめてあるだけ。例えば Universal Binary がそれ。
構造
Mach-Oは3つの部分から成っている。
- ヘッダ
- ロードコマンド
- データ
■ヘッダ
Mach-Oであることを示すマジックナンバー、対象アーキテクチャ、ロードコマンドの数、残りの部分を読み取るためのフラグ等が記述されている。
■ロードコマンド
ヘッダに続いて複数並んでいて、データ部の構造を記述している。セグメント用のロードコマンド、シンボルテーブル用のロードコマンドなどデータの種類に応じたロードコマンドが有る。コマンドと言っても実体は構造体。
■データ
ロードコマンドに対応するデータが格納されている。
ヘッダを読み込んでみる
まず処理対象となる、単純なMach-Oを作る。hogeにある1バイトを終了コードとして返すだけのプログラム。
.file "hoge1.s" .text .globl start start: movq $0x2000001, %rax movq hoge(%rip), %rdi syscall .data hoge: .byte 0x5
一応コンパイル&実行。
$ as -g -o hoge1.o hoge1.s && ld -o hoge1 hoge1.o $ ./hoge1 $ echo $? 5
ここで作成された、hoge1.oからMachヘッダを読み込んでみる。ヘッダはこういう構造をしている。/usr/include/mach-o/loader.h で定義されている。
struct mach_header_64 { uint32_t magic; cpu_type_t cputype; cpu_subtype_t cpusubtype; uint32_t filetype; uint32_t ncmds; uint32_t sizeofcmds; uint32_t flags; uint32_t reserved; };
読み込むプログラムはこんな感じ
01: #include <stdio.h> 02: #include <mach-o/loader.h> 03: 04: static struct mach_header_64 mach_header; 05: 06: int load_macho_header_64(FILE *f, struct mach_header_64 *h) { 07: printf("== load 64bit mach-o header ==\n"); 08: fread(&h->cputype, sizeof(cpu_type_t), 1, f); 09: fread(&h->cpusubtype, sizeof(cpu_subtype_t), 1, f); 10: fread(&h->filetype, sizeof(uint32_t), 1, f); 11: fread(&h->ncmds, sizeof(uint32_t), 1, f); 12: fread(&h->sizeofcmds, sizeof(uint32_t), 1, f); 13: fread(&h->flags, sizeof(uint32_t), 1, f); 14 fread(&h->reserved, sizeof(uint32_t), 1, f); 15: 16: printf("magic = 0x%08x\n", h->magic); 17: printf("cputype = 0x%08x\n", h->cputype); 18: printf("cpusubtype = 0x%08x\n", h->cpusubtype); 19: printf("filetype = 0x%08x\n", h->filetype); 20: printf("ncmds = 0x%08x\n", h->ncmds); 21: printf("sizeofcmds = 0x%08x\n", h->sizeofcmds); 22: printf("flags = 0x%08x\n", h->flags); 23: printf("reserved = 0x%08x\n", h->reserved); 24: 25: return 0; 26: } 27: 28: int main(int argc, char** argv) { 29: int retval, magic; 30: FILE *f = 0; 31: 32: retval = 0; 33: 34: if (argc != 2) { 35: printf("usage: ml1 <macho file>\n"); 36: retval = 1; 37: goto end; 38: } 39: 40: if((f = fopen(argv[1], "r")) == NULL) { 41: printf("fileopen error: %s\n", argv[1]); 42: retval = 1; 43: goto end; 44: } 45: 46: // load magic 47: fread(&magic, sizeof(magic), 1, f); 48: 49: if (magic == MH_MAGIC) { 50: printf("i386 mach-o not supported.\n"); 51: retval = 1; 52: goto end; 53: } else if (magic == MH_MAGIC_64) { 54: mach_header.magic = magic; 55: load_macho_header_64(f, &mach_header); 56: } else { 57: printf("unknown magic: 0x%08x\n", magic); 58: retval = 1; 59: goto end; 60: } 61: 62: end: 63: if (f != 0) { 64: fclose(f); 65: } 66: 67: return retval; 68: }
実行結果
$ ./ml1 hoge1.o == load 64bit mach-o header == magic = 0xfeedfacf cputype = 0x01000007 cpusubtype = 0x00000003 filetype = 0x00000001 ncmds = 0x00000003 sizeofcmds = 0x00000150 flags = 0x00000000 reserved = 0x00000000
答え合わせ。バイナリファイルを眺めて答え合わせをしても良いが、otoolと言うコマンドを使うと楽。
$ otool -h hoge1.o hoge1.o: Mach header magic cputype cpusubtype caps filetype ncmds sizeofcmds flags 0xfeedfacf 16777223 3 0x00 1 3 336 0x00000000
10進数と16進数が入り交じり・・・。以下のコマンドでテキトーに変換しつつ見比べる。
$ perl -e 'printf("0x%x\n", 16777223)'
ツールが有るならわざわざ自分でコード書かなくても・・・と言う声も聞こえてきそうだが、Mach-Oの生な構造を理解することが目的なので自分で解析コードを書き、失敗し、バイナリエディタや仕様書をながめつつ進めることに意味が有る。
各項目の値がどのような意味を持つかはloader.hに様々な定数が#defineされていて、その値と比較することでわかる。例えば magic の 0xfeedfacf は 64bit Mach-Oのマジックナンバーである MH_MAGIC_64 に一致しているし、 cputype の取りうる値は loader.h からさらにincludeされている mach/machine.h で定義されていて、0x01000007 はx86_64を表す CPU_TYPE_X86_64 に一致している。マニュアルにも書いてある。(マニュアルでは CPU_TYPE_x86_64 となっていたが、これじゃコンパイル通らない。正しくは X は大文字)
ロードコマンドを読み込む
ロードコマンドには色々な種類があるけど、最初の2つのメンバは共通しているので、共通部分のデータ構造としてこのような構造体が定義されている。
struct load_command { uint32_t cmd; uint32_t cmdsize; };
この後に各ロードコマンド依存のデータが続くのだけれど、今日のところはぜーんぶスキップする。
ソース抜粋(ヘッダ読み込みに続けて読み込む感じで書く。)
#include <stdlib.h> (略) static struct load_command *load_commands; int main(int argc, char **argv) { (略) load_commands = (struct load_command *) malloc(sizeof(struct load_command) * mach_header.ncmds); for (i=0; i < mach_header.ncmds; i++) { fread(&load_commands[i].cmd, sizeof(uint32_t), 1, f); fread(&load_commands[i].cmdsize, sizeof(uint32_t), 1, f); printf("=== cmd = 0x%08x, cmdsize=0x%08x ===\n", load_commands[i].cmd, load_commands[i].cmdsize); // ロードコマンドの残りの部分はスキップ fseek(f, load_commands[i].cmdsize - (sizeof(uint32_t) * 2), SEEK_CUR); } (略) free(load_commands) (略) }
実行結果(中間形式)
$ ./ml1 hoge1.o == load 64bit mach-o header == (略: machヘッダ部) === cmd = 0x00000019, cmdsize=0x000000e8 === === cmd = 0x00000002, cmdsize=0x00000018 === === cmd = 0x0000000b, cmdsize=0x00000050 ===
これを元に上の図のロードコマンド部分をロードコマンドごとに線で区切るとこんな感じ。
どのようなコマンドが有るのかをloader.hを元に以下の表にまとめてみた。
#define | 値 | 説明 |
---|---|---|
LC_SEGMENT | 0x1 | segment of this file to be mapped |
LC_SYMTAB | 0x2 | link-edit stab symbol table info |
LC_SYMSEG | 0x3 | link-edit gdb symbol table info (obsolete) |
LC_THREAD | 0x4 | thread |
LC_UNIXTHREAD | 0x5 | unix thread (includes a stack) |
LC_LOADFVMLIB | 0x6 | load a specified fixed VM shared library |
LC_IDFVMLIB | 0x7 | fixed VM shared library identification |
LC_IDENT | 0x8 | object identification info (obsolete) |
LC_FVMFILE | 0x9 | fixed VM file inclusion (internal use) |
LC_PREPAGE | 0xa | prepage command (internal use) |
LC_DYSYMTAB | 0xb | dynamic link-edit symbol table info |
LC_LOAD_DYLIB | 0xc | load a dynamically linked shared library |
LC_ID_DYLIB | 0xd | dynamically linked shared lib ident |
LC_LOAD_DYLINKER | 0xe | load a dynamic linker |
LC_ID_DYLINKER | 0xf | dynamic linker identification |
LC_PREBOUND_DYLIB | 0x10 | modules prebound for a dynamically |
LC_ROUTINES | 0x11 | image routines |
LC_SUB_FRAMEWORK | 0x12 | sub framework |
LC_SUB_UMBRELLA | 0x13 | sub umbrella |
LC_SUB_CLIENT | 0x14 | sub client |
LC_SUB_LIBRARY | 0x15 | sub library |
LC_TWOLEVEL_HINTS | 0x16 | two-level namespace lookup hints |
LC_PREBIND_CKSUM | 0x17 | prebind checksum |
LC_LOAD_WEAK_DYLIB | (0x18|LC_REQ_DYLD) | |
LC_SEGMENT_64 | 0x19 | 64-bit segment of this file to be mapped |
LC_ROUTINES_64 | 0x1a | 64-bit image routines |
LC_UUID | 0x1b | the uuid |
LC_RPATH | (0x1c|LC_REQ_DYLD) | runpath additions |
LC_CODE_SIGNATURE | 0x1d | local of code signature |
LC_SEGMENT_SPLIT_INFO | 0x1e | local of info to split segments |
LC_REEXPORT_DYLIB | (0x1f|LC_REQ_DYLD) | load and re-export dylib |
LC_LAZY_LOAD_DYLIB | 0x20 | delay load of dylib until first use |
LC_ENCRYPTION_INFO | 0x21 | encrypted segment information |
LC_DYLD_INFO | 0x22 | compressed dyld information |
LC_DYLD_INFO_ONLY | (0x22|LC_REQ_DYLD) | compressed dyld information only |
さっきの実行結果のコマンドは、0x19, 0x02, 0x0b だったので、表と照らし合わせるとそれぞれ LC_SEGMENT_64, LC_SYMTAB, LC_DYSYMTAB で有ることがわかる。
実行形式のファイルからもロードコマンドを読み込んでみる
$ ./ml1 hoge1 == load 64bit mach-o header == (略: machヘッダ部) === cmd = 0x00000019, cmdsize=0x00000048 === === cmd = 0x00000019, cmdsize=0x00000098 === === cmd = 0x00000019, cmdsize=0x00000098 === === cmd = 0x00000019, cmdsize=0x00000048 === === cmd = 0x00000002, cmdsize=0x00000018 === === cmd = 0x0000000b, cmdsize=0x00000050 === === cmd = 0x0000000e, cmdsize=0x00000020 === === cmd = 0x0000001b, cmdsize=0x00000018 === === cmd = 0x00000005, cmdsize=0x000000b8 ===
何か増えてる。
違いを表にしてみる。
hoge1.o(中間形式) | hoge1(実行形式) | |
---|---|---|
LC_SEGMENT_64 | ◯ | ◯ |
LC_SYMTAB | ◯ | ◯ |
LC_DYNSYMTAB | ◯ | ◯ |
LC_LOAD_DYNLINKER | - | ◯ |
LC_UUID | - | ◯ |
LC_UNIXTHREAD | - | ◯ |
実行に必要そうな雰囲気のロードコマンドが増えてる。セグメントも増えてる。実は中間形式では容量節約のために全てのセクション(セグメントの下の階層構造)が一つの仮のセグメントに詰め込まれているからこうなっている。そのあたりは次回詳しく調べる。