Mach-Oのヘッダとロードコマンドを読み込む

動的ライブラリの仕組みを知りたかったので、その準備としてMach-Oの構造について調べてみた。環境はx86_64。

Mach-Oとは

Mac OS Xでプログラムやライブラリをディスクに格納するために標準で使用されるフォーマット。中間形式(リンク前)にも最終形式(リンク後)にも用いられる。コード、データの両方を格納できる。
Mach-Oは一つのファイルが一つのアーキテクチャ(CPU)に対応する。複数のアーキテクチャに対応するものはMach-Oファイルではなく、複数のMach-Oファイルをまとめてあるだけ。例えば Universal Binary がそれ。

構造

Mach-Oは3つの部分から成っている。

  1. ヘッダ
  2. ロードコマンド
  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 -

実行に必要そうな雰囲気のロードコマンドが増えてる。セグメントも増えてる。実は中間形式では容量節約のために全てのセクション(セグメントの下の階層構造)が一つの仮のセグメントに詰め込まれているからこうなっている。そのあたりは次回詳しく調べる。