Mach-Oのセグメント&セクション定義を読み込む

今回も引き続き共有ライブラリの仕組を知るための準備としてMach-Oの構造を学習。今回はセグメントとセクションに関する情報を読み込んでみる。環境はx86_64

セグメントとセクション

図1のように、Mach-Oファイルにはロードコマンドに続いてセグメントと呼ばれるデータ領域が一つ以上有る。また、セグメントはセクションと呼ばれる領域に分かれている。セクションは1個も含まれないことも有る。



図1: セグメントとセクション


セグメント名はアンダースコア2個に続けて大文字で表す(例: __TEXT, __DATA) セクション名はアンダースコア2個に続けて小文字で表す(例: __text, __data)

Mach-Oファイルには様々なテーブルが含まれていて、その中でセクションが番号で参照される。セクション番号は 1 から始まる。セクション番号はセグメントを跨いだ通し番号となる(図1参照)。

セグメントを定義するロードコマンドはLC_SEGMENT_64で、ロードコマンドの構造は mach-o/loader.h で定義されている segment_command_64 構造体。

struct segment_command_64 { /* for 64-bit architectures */
    uint32_t  cmd;            /* LC_SEGMENT_64 */
    uint32_t  cmdsize;        /* includes sizeof section_64 structs */
    char      segname[16];    /* segment name */
    uint64_t  vmaddr;         /* memory address of this segment */
    uint64_t  vmsize;         /* memory size of this segment */
    uint64_t  fileoff;        /* file offset of this segment */
    uint64_t  filesize;       /* amount to map from the file */
    vm_prot_t maxprot;        /* maximum VM protection */
    vm_prot_t initprot;       /* initial VM protection */
    uint32_t  nsects;         /* number of sections in segment */
    uint32_t  flags;          /* flags */
};

Mach-Oファイルのどの部分をプロセス仮想メモリのどこにロードするかや、ロードされた先の仮想メモリの保護属性もセグメントコマンドで定義される。セグメントは常に仮想メモリのページサイズでアラインされる。セグメントの仮想メモリ上のサイズはMach-Oファイルのサイズよりも大きくなることが有る。例えば __PAGEZEROセグメントはMach-Oファイル上でのサイズは 0 バイトだが、仮想メモリ上でのサイズは 1ページ(= 4096 byte @ x86_64)となる。


Mach-Oでの標準的なセグメントの例。(詳しくはこちら)

__PAGEZERO
NULL(=0)ポインタ経由のアクセスに対して、直ちにプログラムを異常終了させるためのセグメント
__TEXT
コードとリードオンリーデータが格納されているセグメント
__DATA
ライタブルデータが格納されているセグメント
__LINKEDIT
ダイナミックリンカが使用する情報が格納されているセグメント

セクション

セグメントのロードコマンドに続いてセクションを定義するsection_64構造体がnsects個続いている。

struct section_64 { /* for 64-bit architectures */
    char     sectname[16];    /* name of this section */
    char     segname[16];     /* segment this section goes in */
    uint64_t addr;            /* memory address of this section */
    uint64_t size;            /* size in bytes of this section */
    uint32_t offset;          /* file offset of this section */
    uint32_t align;           /* section alignment (power of 2) */
    uint32_t reloff;          /* file offset of relocation entries */
    uint32_t nreloc;          /* number of relocation entries */
    uint32_t flags;           /* flags (section type and attributes)*/
    uint32_t reserved1;       /* reserved (for offset or index) */
    uint32_t reserved2;	      /* reserved (for count or sizeof) */
    uint32_t reserved3;	      /* reserved */
};

Mac OS X ABI Mach-O File Format Referenceにはreserved3が無かったが実際のMach-Oファイルを見てみると確かにreserved3に相当するデータは有った。


■セクションの例 (詳しくはこちら)

__text
実行可能なコードが格納されているセクション。__TEXTセグメントに含まれる。
__cstring
コンスタント文字列。非'\0'のバイトの並びが格納されているセクション。__TEXTセグメントに含まれる。
__data
初期化済み(int i=3; など)の可変なデータが格納されているセクション。__DATAセグメントに含まれる
__bss
未初期化のスタティック変数(static int i; など)の可変なデータが格納されているセクション。__DATAセグメントに含まれる。

セグメントとセクションの定義を読み込む

前回のソース(ml2.c)にセグメントとセクションの定義を読み込むコードを追加する。読み込むMach-Oファイルは前回作成のhoge1.o


ソース抜粋(ml3.c)

int load_segment_command_64(FILE *stream, 
                            struct segment_command_64 *seg_cmd_64) {
        fread(seg_cmd_64->segname, 16, 1, stream);
        fread(&seg_cmd_64->vmaddr, sizeof(uint64_t), 1, stream);
        fread(&seg_cmd_64->vmsize, sizeof(uint64_t), 1, stream);
        fread(&seg_cmd_64->fileoff, sizeof(uint64_t), 1, stream);
        fread(&seg_cmd_64->filesize, sizeof(uint64_t), 1, stream);
        fread(&seg_cmd_64->maxprot, sizeof(vm_prot_t), 1, stream);
        fread(&seg_cmd_64->initprot, sizeof(vm_prot_t), 1, stream);
        fread(&seg_cmd_64->nsects, sizeof(uint32_t), 1, stream);
        fread(&seg_cmd_64->flags, sizeof(uint32_t), 1, stream);

        printf("cmd = 0x%08x\n", seg_cmd_64->cmd);
        printf("cmdsize = 0x%08x\n", seg_cmd_64->cmdsize);
        printf("segname = %s\n", seg_cmd_64->segname);
        printf("vmaddr = %016llx\n", seg_cmd_64->vmaddr);
        printf("vmsize = %016llx\n", seg_cmd_64->vmsize);
        printf("fileoff = %016llx\n", seg_cmd_64->fileoff);
        printf("filesize = %016llx\n", seg_cmd_64->filesize);
        printf("maxprot = %016x\n", seg_cmd_64->maxprot);
        printf("initprot = %016x\n", seg_cmd_64->initprot);
        printf("nsects = %08x\n", seg_cmd_64->nsects);
        printf("flags = %08x\n", seg_cmd_64->flags);

        return 0;
}

int load_section_64(FILE *f, struct section_64 *sect64) {
    fread(sect64->sectname, 16, 1, f);
    fread(sect64->segname, 16, 1, f);
    fread(&sect64->addr, sizeof(uint64_t), 1, f);
    fread(&sect64->size, sizeof(uint64_t), 1, f);
    fread(&sect64->offset, sizeof(uint32_t), 1, f);
    fread(&sect64->align, sizeof(uint32_t), 1, f);
    fread(&sect64->reloff, sizeof(uint32_t), 1, f);
    fread(&sect64->nreloc, sizeof(uint32_t), 1, f);
    fread(&sect64->flags, sizeof(uint32_t), 1, f);
    fread(&sect64->reserved1, sizeof(uint32_t), 1, f);
    fread(&sect64->reserved2, sizeof(uint32_t), 1, f);
    fread(&sect64->reserved3, sizeof(uint32_t), 1, f);
    
    printf("sectname = %s\n", sect64->sectname);    
    printf("setgname = %s\n", sect64->segname);    
    printf("addr = 0x%016llx\n", sect64->addr);    
    printf("size = 0x%016llx\n", sect64->size);    
    printf("offset = 0x%08x\n", sect64->offset);    
    printf("align = 0x%08x\n", sect64->align);    
    printf("reloff = 0x%08x\n", sect64->reloff);    
    printf("nreloc = 0x%08x\n", sect64->nreloc);    
    printf("flags = 0x%08x\n", sect64->flags);    
    printf("reserved1 = 0x%08x\n", sect64->reserved1);    
    printf("reserved2 = 0x%08x\n", sect64->reserved2);    
    printf("reserved3 = 0x%08x\n", sect64->reserved3);    
}

int main(int argc, char **argv) {
  // テキトーにsegment_command_64とsection_64の配列を作ってそこに上記関数でロード。
}


実行結果(抜粋)

$ ml3 hoge1.o

(略)
=== LC_SEGMENT_64[0] ===
cmd = 0x00000019
cmdsize = 0x000000e8
segname = 
vmaddr = 0000000000000000
vmsize = 0000000000000011
fileoff = 0000000000000170
filesize = 0000000000000011
maxprot = 0000000000000007
initprot = 0000000000000007
nsects = 00000002
flags = 00000000
=== section[0] ===
sectname = __text
setgname = __TEXT
addr = 0x0000000000000000
size = 0x0000000000000010
offset = 0x00000170
align = 0x00000000
reloff = 0x00000184
nreloc = 0x00000001
flags = 0x80000400
reserved1 = 0x00000000
reserved2 = 0x00000000
reserved3 = 0x00000000
=== section[1] ===
sectname = __data
setgname = __DATA
addr = 0x0000000000000010
size = 0x0000000000000001
offset = 0x00000180
align = 0x00000000
reloff = 0x00000000
nreloc = 0x00000000
flags = 0x00000000
reserved1 = 0x00000000
reserved2 = 0x00000000
reserved3 = 0x00000000
(略)

中間形式のMach-Oでは容量削減のためにセグメントは一つしか無い。セクションには最終形式のMach-Oでの格納先となるセグメント名が記述されている。


otoolで答え合わせ。

$ otool -l hoge1.o

(略)
Load command 0
      cmd LC_SEGMENT_64
  cmdsize 232
  segname 
   vmaddr 0x0000000000000000
   vmsize 0x0000000000000011
  fileoff 368
 filesize 17
  maxprot 0x00000007
 initprot 0x00000007
   nsects 2
    flags 0x0
Section
  sectname __text
   segname __TEXT
      addr 0x0000000000000000
      size 0x0000000000000010
    offset 368
     align 2^0 (1)
    reloff 388
    nreloc 1
     flags 0x80000400
 reserved1 0
 reserved2 0
Section
  sectname __data
   segname __DATA
      addr 0x0000000000000010
      size 0x0000000000000001
    offset 384
     align 2^0 (1)
    reloff 0
    nreloc 0
     flags 0x00000000
 reserved1 0
 reserved2 0
(略)

よさげ。

まとめ

  • セグメントとセクションのMach-Oファイル内での構造を理解できた