Mach-Oオブジェクト形式を生成してみる

2024-06-15

Linuxなどの実行ファイルやコンパイラから出力されるオブジェクトファイルはELFという形式だが、 macOSではMach-Oという別のフォーマットが使われている。 このフォーマットを学んでみる、 要するにELFのオブジェクトファイル形式を生成するをMach-O/aarch64上でやってみる。

環境:Apple M1, macOS Sonoma 14.5, Apple clang version 15.0.0 (clang-1500.3.9.4)

Mach-Oの構成

Mach-Oフォーマットのファイルは、ヘッダとロードコマンド(複数)、他は任意という構成になっている。 コマンドにはいろいろな種類があり、オブジェクトファイルに重要な項目としてセグメントとセクション、シンボルテーブル、文字列テーブル、再配置情報がある。

Mach-Oファイル内容の確認方法

どのような内容のファイルを生成したらいいのか調べるために、gccなどの既存のコンパイラから出力したものの内容を確認する方法。 otooldsymutilobjdumpを使うとよい。

題材としてaarch64用の以下のアセンブリコードを使用する:

	.text
.globl _main
.p2align 2
_main:
stp fp, lr, [sp,#-16]!
mov x2, #14
adrp x1, msg@PAGE
add x1, x1, msg@PAGEOFF
mov w0, #1
bl _write
mov w0, wzr
ldp fp, lr, [sp],#16
ret

.section __DATA,__const
msg:
.ascii "Hello, world!\n\0"
$ gcc -c hello.s  # hello.oが出力される

machヘッダ

machヘッダはファイルの先頭32バイトで、内容はstruct mach_header_64

struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
int32_t cputype; /* cpu specifier */
int32_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 */
};

となる。オブジェクトファイルをダンプしてみると、

$ xxd hello.o | head -n2
00000000: cffa edfe 0c00 0001 0000 0000 0100 0000 ................
00000010: 0400 0000 6801 0000 0020 0000 0000 0000 ....h.... ......

ヘッダ内容を表示するにはotool -hvを使う:

$ otool -hv hello.o
hello.o:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 ARM64 ALL 0x00 OBJECT 4 360 SUBSECTIONS_VIA_SYMBOLS

ロードコマンド

machヘッダ内のncmdsに従ってロードコマンドという情報が続く。 ロードコマンドはstruct load_command

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

となっていて、コマンド番号に従ってデータが格納される。 コマンド内容によってサイズはまちまちになるが、cmdsize進めることで次のロードコマンドを辿ることができる。

ロードコマンドを表示するにはotool -lv

$ otool -lv hello.o
hello.o:
Load command 0
cmd LC_SEGMENT_64
cmdsize 232
...
Load command 1
cmd LC_BUILD_VERSION
cmdsize 24
...
Load command 2
cmd LC_SYMTAB
cmdsize 24
...
Load command 3
cmd LC_DYSYMTAB
cmdsize 80
...

セクション

ロードコマンドのセグメントコマンド内(上記のLC_SEGMENT_64)にセクション情報が含まれている。 セクションのみを一覧で見るにはobjdump --section-headers

$ objdump --section-headers hello.o

hello.o: file format mach-o arm64

Sections:
Idx Name Size VMA Type
0 __text 00000024 0000000000000000 TEXT
1 __const 0000000f 0000000000000024 DATA

シンボルテーブル

dsymutil -s

$ dsymutil -s hello.o
----------------------------------------------------------------------
Symbol table for: 'hello.o' (arm64)
----------------------------------------------------------------------
Index n_strx n_type n_sect n_desc n_value
======== -------- ------------------ ------ ------ ----------------
[ 0] 00000018 0e ( SECT ) 01 0000 0000000000000000 'ltmp0'
[ 1] 00000007 0e ( SECT ) 02 0000 0000000000000024 'msg'
[ 2] 00000012 0e ( SECT ) 02 0000 0000000000000024 'ltmp1'
[ 3] 00000001 0f ( SECT EXT) 01 0000 0000000000000000 '_main'
[ 4] 0000000b 01 ( UNDF EXT) 00 0000 0000000000000000 '_write'

またはobjdump --syms

$ objdump --syms hello.o

hello.o: file format mach-o arm64

SYMBOL TABLE:
0000000000000000 l F __TEXT,__text ltmp0
0000000000000024 l O __DATA,__const msg
0000000000000024 l O __DATA,__const ltmp1
0000000000000000 g F __TEXT,__text _main
0000000000000000 *UND* _write
  • シンボルにはどのセクションかの情報が含まれる
    • セクション番号は1オリジン
    • ただしn_valueはセクション先頭ではなくセグメント先頭から何バイト目かという風に指定される? (セクション2のaddr0x24だが、msgn_value0x24となっている)
  • シンボル名は文字列テーブルのバイト位置を指すことで指定
  • 外部公開シンボルはEXT(そうでなければローカル)
  • 外部参照(_write)はUNDFでセクション0となる

再配置情報

otool -rv

$ otool -rv hello.o
hello.o:
Relocation information (__TEXT,__text) 3 entries
address pcrel length extern type scattered symbolnum/value
00000014 True long True BR26 False _write
0000000c False long True PAGOF12 False msg
00000008 True long True PAGE21 False msg

またはobjdump --reloc

$ objdump --reloc hello.o

hello.o: file format mach-o arm64

RELOCATION RECORDS FOR [__text]:
OFFSET TYPE VALUE
0000000000000014 ARM64_RELOC_BRANCH26 _write
000000000000000c ARM64_RELOC_PAGEOFF12 msg
0000000000000008 ARM64_RELOC_PAGE21 msg
  • 再配置情報はセグメントに対応していて、そのセグメント内のオフセットと、再配置のタイプと計算に使用するシンボルが含まれる
  • _write呼び出しはbl1命令なので、再配置情報は1つ
  • msg参照は2命令に分かれているので、再配置情報も2つ

オブジェクトファイルでHello world

Mach-Oオブジェクトファイルを自分で生成してみる:

#include <mach-o/loader.h>
#include <mach-o/nlist.h>
#include <mach-o/reloc.h>
#include <mach-o/arm64/reloc.h>
#include <string.h>
#include <unistd.h>

int main(void) {
// マシンコード (aarch64)
static const uint32_t CODE[10] = {
0xa9bf7bfd, // stp fp, lr, [sp, #-16]!
// write(1, msg, 14);
0xd28001c2, // mov x2, #14
0x90000001, // adrp x1, msg@PAGE
0x91000021, // add x1, x1, msg@PAGEOFF
0x52800020, // mov w0, #1
0x94000000, // bl _write
// return 0;
0x2a1f03e0, // mov w0, wzr
0xa8c17bfd, // ldp fp, lr, [sp], #16
0xd65f03c0, // ret
};

// 読み取り専用データ
static const char RODATA[16] = "Hello, world!\n"; // msg

// 文字列テーブル
static const char strtab[] =
"\0"
"_main\0"
"msg\0"
"_write\0";
int null_nameofs = 0;
int main_nameofs = null_nameofs + strlen("") + 1;
int msg_nameofs = main_nameofs + strlen("_main") + 1;
int write_nameofs = msg_nameofs + strlen("msg") + 1;

// ヘッダ
struct mach_header_64 header;
// ロードコマンド
struct segment_command_64 segmentcmd;
struct build_version_command buildversioncmd;
struct symtab_command symtabcmd;
// セクション
struct section_64 sections[2];
// シンボル
struct nlist_64 symbols[3];
// 再配置情報
struct relocation_info relocs[3];

// ファイル内のオフセット位置を計算
uint32_t size_of_cmds = sizeof(segmentcmd) + sizeof(sections) + sizeof(buildversioncmd) + sizeof(symtabcmd);
uint64_t text_start_addr = 0;
uint64_t text_start_off = sizeof(header) + size_of_cmds;
uint64_t rodata_start_addr = sizeof(CODE);
uint64_t rodata_start_off = text_start_off + sizeof(CODE);
uint64_t reloc_start_off = rodata_start_off + sizeof(RODATA);
uint64_t symbol_start_off = reloc_start_off + sizeof(relocs);
uint64_t str_start_off = symbol_start_off + sizeof(symbols);

sections[0] = (struct section_64){
.sectname = "__text",
.segname = "__TEXT",
.addr = text_start_addr,
.size = sizeof(CODE),
.offset = text_start_off,
.align = 2, // 2^2
.reloff = reloc_start_off,
.nreloc = sizeof(relocs) / sizeof(*relocs),
.flags = S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS,
};
sections[1] = (struct section_64){
.sectname = "__const",
.segname = "__DATA",
.addr = rodata_start_addr,
.size = sizeof(RODATA),
.offset = rodata_start_off,
.align = 0, // 2^0
.reloff = 0,
.nreloc = 0,
.flags = 0,
};

// シンボル
symbols[0] = (struct nlist_64){
.n_un = { main_nameofs }, // _main
.n_type = N_SECT | N_EXT,
.n_sect = 1,
.n_desc = 0,
.n_value = text_start_addr,
};
symbols[1] = (struct nlist_64){
.n_un = { msg_nameofs }, // msg
.n_type = N_SECT,
.n_sect = 2,
.n_desc = 0,
.n_value = rodata_start_addr,
};
symbols[2] = (struct nlist_64){
.n_un = { write_nameofs }, // _write
.n_type = N_EXT,
.n_sect = 0,
.n_desc = 0,
.n_value = 0,
};

// 再配置情報
relocs[0] = (struct relocation_info){
.r_address = 0x000008,
.r_symbolnum = 1, // msg
.r_pcrel = 1,
.r_length = 2,
.r_extern = 1,
.r_type = ARM64_RELOC_PAGE21,
};
relocs[1] = (struct relocation_info){
.r_address = 0x00000c,
.r_symbolnum = 1, // msg
.r_pcrel = 0,
.r_length = 2,
.r_extern = 1,
.r_type = ARM64_RELOC_PAGEOFF12,
};
relocs[2] = (struct relocation_info){
.r_address = 0x000014,
.r_symbolnum = 2, // _write
.r_pcrel = 1,
.r_length = 2,
.r_extern = 1,
.r_type = ARM64_RELOC_BRANCH26,
};

// 内容構築

header = (struct mach_header_64){
.magic = MH_MAGIC_64,
.cputype = CPU_TYPE_ARM64,
.cpusubtype = 0,
.filetype = MH_OBJECT,
.ncmds = 3,
.sizeofcmds = size_of_cmds,
.flags = MH_SUBSECTIONS_VIA_SYMBOLS,
};
segmentcmd = (struct segment_command_64){
.cmd = LC_SEGMENT_64,
.cmdsize = sizeof(segmentcmd) + sizeof(sections),
.segname = "",
.vmaddr = 0,
.vmsize = sizeof(CODE) + sizeof(RODATA),
.fileoff = text_start_off,
.filesize = sizeof(CODE) + sizeof(RODATA),
.maxprot = 7, // rwx
.initprot = 7, // rwx
.nsects = sizeof(sections) / sizeof(*sections),
.flags = 0,
};
buildversioncmd = (struct build_version_command){
.cmd = LC_BUILD_VERSION,
.cmdsize = sizeof(buildversioncmd),
.platform = PLATFORM_MACOS,
.minos = 0x000e0000, // 14.0.0
.sdk = 0x000e0500, // 14.5.0
.ntools = 0,
};
symtabcmd = (struct symtab_command){
.cmd = LC_SYMTAB,
.cmdsize = sizeof(symtabcmd),
.symoff = symbol_start_off,
.nsyms = sizeof(symbols) / sizeof(*symbols),
.stroff = str_start_off,
.strsize = sizeof(strtab),
};

// 標準出力へ書き出し

int fd = STDOUT_FILENO;
write(fd, &header, sizeof(header));
write(fd, &segmentcmd, sizeof(segmentcmd));
write(fd, sections, sizeof(sections));
write(fd, &buildversioncmd, sizeof(buildversioncmd));
write(fd, &symtabcmd, sizeof(symtabcmd));
write(fd, &CODE, sizeof(CODE));
write(fd, &RODATA, sizeof(RODATA));
write(fd, &relocs, sizeof(relocs));
write(fd, &symbols, sizeof(symbols));
write(fd, &strtab, sizeof(strtab));

return 0;
}
  • 必要なロードコマンドはセグメント、ビルドバージョン、シンボルテーブルの3つ
    • ビルドバージョンを含めないとリンクに失敗する
  • セグメントコマンドに続いてセクション情報を列挙し、コマンドサイズにも含める
  • ~offというフィールドがファイル内のどこに配置されているかを示す
    • 8バイト程度でアライメントする必要があるかもしれない

実行する:

$ gcc -o hello_obj hello_obj.c
$ ./hello_obj > myhello.o # オブジェクトファイル出力
$ gcc -o myhello myhello.o # リンク
$ ./myhello # 実行
Hello, world!

出力されたmyhello.o の中身:

# machヘッダ (struct mach_header_64)
00000000: cffa edfe 0c00 0001 0000 0000 0100 0000 ................
00000010: 0300 0000 1801 0000 0020 0000 0000 0000 ......... ......
# ロードコマンド[0]:セグメント (struct segment_command_64)
00000020: 1900 0000 e800 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 3800 0000 0000 0000 3801 0000 0000 0000 8.......8.......
00000050: 3800 0000 0000 0000 0700 0000 0700 0000 8...............
00000060: 0200 0000 0000 0000 ........
# セクション[0] (struct section_64)
5f5f 7465 7874 0000 __text..
00000070: 0000 0000 0000 0000 5f5f 5445 5854 0000 ........__TEXT..
00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000090: 2800 0000 0000 0000 3801 0000 0200 0000 (.......8.......
000000a0: 7001 0000 0300 0000 0004 0080 0000 0000 p...............
000000b0: 0000 0000 0000 0000 ........
# セクション[1] (struct section_64)
5f5f 636f 6e73 7400 __const.
000000c0: 0000 0000 0000 0000 5f5f 4441 5441 0000 ........__DATA..
000000d0: 0000 0000 0000 0000 2800 0000 0000 0000 ........(.......
000000e0: 1000 0000 0000 0000 6001 0000 0000 0000 ........`.......
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000100: 0000 0000 0000 0000 ........
# ロードコマンド[1]:ビルドバージョン (struct build_version_command)
3200 0000 1800 0000 2.......
00000110: 0100 0000 0000 0e00 0005 0e00 0000 0000 ................
# ロードコマンド[2]:シンボルテーブル (struct symtab_command)
00000120: 0200 0000 1800 0000 8801 0000 0300 0000 ................
00000130: b801 0000 1800 0000 ........
# コード
fd7b bfa9 c201 80d2 .{......
00000140: 0100 0090 2100 0091 2000 8052 0000 0094 ....!... ..R....
00000150: e003 1f2a fd7b c1a8 c003 5fd6 0000 0000 ...*.{...._.....
# RODATA
00000160: 4865 6c6c 6f2c 2077 6f72 6c64 210a 0000 Hello, world!...
# 再配置情報 (struct relocation_info [3])
00000170: 0800 0000 0100 003d 0c00 0000 0100 004c .......=.......L
00000180: 1400 0000 0200 002d .......-
# シンボル (struct nlist_64 [3])
0100 0000 0f01 0000 ........
00000190: 0000 0000 0000 0000 0700 0000 0e02 0000 ................
000001a0: 2800 0000 0000 0000 0b00 0000 0100 0000 (...............
000001b0: 0000 0000 0000 0000 ........
# 文字列テーブル
005f 6d61 696e 006d ._main.m
000001c0: 7367 005f 7772 6974 6500 00 sg._write..

リンク