Mach-Oの構造メモ

Mach-Oの情報をどこかに書かないと忘れてしまいそうだったため。

Mach-OのリファレンスはAppleの開発者用サイトから削除されてしまったようだが,githubにミラーがある。 Mirror of OS X ABI Mach-O File Format Reference

Mach-Oのリファレンスは誤記がいくつかあるようなので,注意する。

Mach-Oには大きく分けて,Header, Load Commands, Data という三つの領域がある。

Header

Headerは,ELFやPEなどと同じように,マジックナンバーがあり,そのあとにCPUの種類,ファイルの種類などが列挙されている。 以下はHeaderの構造体だ。

/*
 * The 32-bit mach header appears at the very beginning of the object file for
 * 32-bit architectures.
 */
struct mach_header {
    uint32_t   magic;      /* mach magic number identifier */
    cpu_type_t  cputype;    /* cpu specifier */
    cpu_subtype_t   cpusubtype; /* machine specifier */
    uint32_t   filetype;   /* type of file */
    uint32_t   ncmds;      /* number of load commands */
    uint32_t   sizeofcmds; /* the size of all the load commands */
    uint32_t   flags;      /* flags */
};

/* Constant for the magic field of the mach_header (32-bit architectures) */
#define    MH_MAGIC    0xfeedface /* the mach magic number */
#define MH_CIGAM   0xcefaedfe /* NXSwapInt(MH_MAGIC) */

/*
 * The 64-bit mach header appears at the very beginning of object files for
 * 64-bit architectures.
 */
struct mach_header_64 {
    uint32_t   magic;      /* mach magic number identifier */
    cpu_type_t  cputype;    /* cpu specifier */
    cpu_subtype_t   cpusubtype; /* machine specifier */
    uint32_t   filetype;   /* type of file */
    uint32_t   ncmds;      /* number of load commands */
    uint32_t   sizeofcmds; /* the size of all the load commands */
    uint32_t   flags;      /* flags */
    uint32_t   reserved;   /* reserved */
};

mach_headerが32bit,mach_header_64が64bit用だ。 これらのMach-Oの構造体などは,自分の環境(MacBook Pro 2018, macOS Mojave 10.14.5)の場合,/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/mach-o以下のloader.hにあった。

Load Commands

Load CommandsはHeaderのすぐ後ろにある。以下,Load Commandsのことを"LC"とする。 HeaderのncmdsがLCの数,sizeofcmdsがLC全体のサイズである。

Mach-Oでは,各LCの先頭はコマンドの種類とサイズになる。load_command構造体で,それらを読み込むことができる。

struct load_command {
    uint32_t cmd;      /* type of load command */
    uint32_t cmdsize;  /* total size of command in bytes */
};

Segment Commandは,プログラムを読み込むためのコマンドだ。 以下にSegment Commandの構造体を示す。

struct segment_command { /* for 32-bit architectures */
    uint32_t   cmd;        /* LC_SEGMENT */
    uint32_t   cmdsize;    /* includes sizeof section structs */
    char       segname[16];   /* segment name */
    uint32_t   vmaddr;     /* memory address of this segment */
    uint32_t   vmsize;     /* memory size of this segment */
    uint32_t   fileoff;    /* file offset of this segment */
    uint32_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 */
};

/*
 * The 64-bit segment load command indicates that a part of this file is to be
 * mapped into a 64-bit task's address space.  If the 64-bit segment has
 * sections then section_64 structures directly follow the 64-bit segment
 * command and their size is reflected in cmdsize.
 */
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 */
};

Segment Commandの後にはSectionが存在する。 Sectionにはプログラムなどのデータの情報が書かれている。 以下にSectionの構造体を示す。

struct section { /* for 32-bit architectures */
    char       sectname[16];  /* name of this section */
    char       segname[16];   /* segment this section goes in */
    uint32_t   addr;       /* memory address of this section */
    uint32_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) */
};

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 */
};

Mach-OのSegment Commandの情報を表示するCプログラムを以下に示す。

readmacho.c:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include <mach-o/loader.h>

static const char *usage = "Usage: readmacho ¥"filename¥"¥n";

void *read(FILE *fp, size_t size) {
    void *data = calloc(1, size);
    fread(data, size, 1, fp);
    return data;
}

void *load(FILE *fp, int offset, size_t size) {
    void *data = calloc(1, size);
    fseek(fp, offset, SEEK_SET);
    fread(data, size, 1, fp);
    return data;
}

void print_sections(FILE *fp, int nsects, int *current) {
    for (int i = 0; i < nsects; i++) {
        size_t sect_size = sizeof(struct section_64);
        struct section_64 *sect = read(fp, sect_size);
        *current += sect_size;
        printf("  section: %s¥n", sect->sectname);
        printf("    offset: %x¥n", sect->offset);
        free(sect);
    }
}

int main(int argc, char **argv) {
    if (argc < 2) {
        fprintf(stderr, "%s¥n", usage);
        return 1;
    }
    char *name = argv[1];
    FILE *fp;
    if ((fp = fopen(name, "rb")) == NULL) {
        fprintf(stderr, "Can't open the file: %s¥n", name);
        return 1;
    }

    uint32_t *magic = load(fp, 0, sizeof(uint32_t));
    if (*magic != MH_MAGIC_64) {
        fprintf(stderr, "%s is not a mach-o file¥n", name);
        free(magic);
        return 1;
    }
    free(magic);
    
    size_t header_size = sizeof(struct mach_header_64);
    struct mach_header_64 *header = load(fp, 0, header_size);
    int current = header_size;
    
    for (int i = 0; i < header->ncmds; i++) {
        size_t lc_size = sizeof(struct load_command);
        struct load_command *lc = load(fp, current, lc_size);

        if (lc->cmd == LC_SEGMENT_64) {
            size_t seg_size = sizeof(struct segment_command_64);
            struct segment_command_64 *seg = load(fp, current, seg_size);
            current += seg_size;
            printf("segment: %s¥n", seg->segname);
            print_sections(fp, seg->nsects, &current);
            free(seg);
        } else {
            current += lc->cmdsize;
        }
        free(lc);
    }

    free(header);
    fclose(fp);
    return 0;
}

試しに自分自身の情報を表示してみる。

./readmacho readmacho

出力は以下のようになる。

segment: __PAGEZERO
segment: __TEXT
  section: __text
    offset: a82
  section: __stubs
    offset: df4
  section: __stub_helper
    offset: e24
  section: __cstring
    offset: e84
  section: __eh_frame
    offset: f08
segment: __DATA
  section: __nl_symbol_ptr
    offset: 1000
  section: __got
    offset: 1010
  section: __la_symbol_ptr
    offset: 1018
  section: __data
    offset: 1058
segment: __LINKEDIT

Section以下のoffsetは,そのSectionのデータが書かれている位置だ。

Data

Load Commandsの後ろにあるData領域は,その名の通りプログラムやシンボルテーブル,リロケーション情報などが書かれている。 試しに以下のCプログラムをコンパイルして,先ほどのreadmachoで情報を表示してみる。

main.c:

#include <stdio.h>

int main(void) {
    printf("hello, world¥n");
    return 0;
}
gcc -c main.c
./readmacho main.o

すると出力は以下のようになる。

segment:
  section: __text
    offset: 1d8
  section: __cstring
    offset: 1ef
  section: __eh_frame
    offset: 200

__textSectionにはプログラムデータ,__cstringSectionには文字列データ,__eh_franeは,調査不足だが,恐らく例外処理に使われる。

何らかのバイナリエディタでmain.oを開き,__cstringのoffset(ここでは1efだが,環境によって異なるだろう)に飛んでみる。 そこには,hello, worldのasciiコードが書かれているはずだ。 データの横に文字列が表示されるタイプのバイナリエディタであれば,そこにはhello, worldと表示されているはずだ。

編集履歴

Mach-Oに関してはまだ調査不足なので,この記事は随時更新となる。

2019/07/01: 最初のバージョンを公開。

2019/07/02: 誤字の修正。

2019/07/06: リファレンスに関する注意書きを追加。