Mach-O フォーマットを調べがてら otool 風出力をしてみる
はじめに
最近、低レイヤー周りを調べたり実装するのが楽しいので Mach-O フォーマットについて調べてみました。また、会社の方針としても visionOS をメインとしていくことになったので、のちのち役立つことを期待しています。
Mach-O フォーマットを調べつつ、今回の記事用に otool
コマンドを実行したときのような出力を得られるように自分で実装してみました。
実行すると以下のような結果が得られます。
出力結果
今回実装したものを、以下のコードをコンパイルしたファイルに対して実行した結果。
#include <stdio.h>
int main(int argc, char* argv[])
{
printf("DONE\n");
return 0;
}
-----------------------------------------------------------------------
Printing a mach-o header.
-----------------------------------------------------------------------
Magic: -17958193
CPU Type: 16777228
CPU Sub Type: 0
File Type: 2
Command Count: 17
Size of Commands: 1056
Flags: 2097285
-----------------------------------------------------------------------
Printing a symbol table and a dynamic symbol table metadata
-----------------------------------------------------------------------
[Symbol table]
cmd: 2
cmdsize: 24
symoff: 32928
nsyms: 3
stroff: 32984
strsize: 40
[Dynamic symbol table]
cmd: 11
cmdsize: 80
ilocalsym: 0
nlocalsym: 0
iextdefsym: 0
nextdefsym: 2
tocoff: 0
ntoc: 0
modtaboff: 0
nmodtab: 0
extrefsymoff: 0
nextrefsyms: 0
indirectsymoff: 32976
nindirectsyms: 2
extreloff: 0
nextrel: 0
locreloff: 0
nlocrel: 0
-----------------------------------------------------------------------
Printing a symbol table and a dynamic symbol table
-----------------------------------------------------------------------
Symbol name: __mh_execute_header
Symbol name: _main
Symbol name: _printf
-------------------------------------
Printing mach-o segments.
-------------------------------------
cmd: LC_SEGMENT_64
cmdsize: 72
segname: __PAGEZERO
vmaddr: 0x0000000000000000
vmsize: 0x0000000100000000
fileoff: 0
filesize: 0
maxprot: 0x00000000
initprot: 0x00000000
nsects: 0
flags: 0x0
==================================
cmd: LC_SEGMENT_64
cmdsize: 392
segname: __TEXT
vmaddr: 0x0000000100000000
vmsize: 0x0000000000004000
fileoff: 0
filesize: 16384
maxprot: 0x00000005
initprot: 0x00000005
nsects: 4
flags: 0x0
------------------- Sections
sectname: __text
segname: __TEXT
addr: 0x0000000100003f58
size: 0x000000000000003c
offset: 16216
align: 2
reloff: 0
nreloc: 0
flags: 0x80000400
reserved1: 0
reserved2: 0
reserved3: 0
- - - - - - - - - - - - - - - - - - - - - -
sectname: __stubs
segname: __TEXT
addr: 0x0000000100003f94
size: 0x000000000000000c
offset: 16276
align: 2
reloff: 0
nreloc: 0
flags: 0x80000408
reserved1: 0
reserved2: 12
reserved3: 0
- - - - - - - - - - - - - - - - - - - - - -
sectname: __cstring
segname: __TEXT
addr: 0x0000000100003fa0
size: 0x0000000000000006
offset: 16288
align: 0
reloff: 0
nreloc: 0
flags: 0x00000002
reserved1: 0
reserved2: 0
reserved3: 0
- - - - - - - - - - - - - - - - - - - - - -
sectname: __unwind_info
segname: __TEXT
addr: 0x0000000100003fa8
size: 0x0000000000000058
offset: 16296
align: 2
reloff: 0
nreloc: 0
flags: 0x00000000
reserved1: 0
reserved2: 0
reserved3: 0
- - - - - - - - - - - - - - - - - - - - - -
cmd: LC_SEGMENT_64
cmdsize: 152
segname: __DATA_CONST
vmaddr: 0x0000000100004000
vmsize: 0x0000000000004000
fileoff: 16384
filesize: 16384
maxprot: 0x00000003
initprot: 0x00000003
nsects: 1
flags: 0x16
------------------- Sections
sectname: __got
segname: __DATA_CONST
addr: 0x0000000100004000
size: 0x0000000000000008
offset: 16384
align: 3
reloff: 0
nreloc: 0
flags: 0x00000006
reserved1: 1
reserved2: 0
reserved3: 0
- - - - - - - - - - - - - - - - - - - - - -
--------- Dynamic symbols
- _printf
----------------------------------
cmd: LC_SEGMENT_64
cmdsize: 72
segname: __LINKEDIT
vmaddr: 0x0000000100008000
vmsize: 0x0000000000004000
fileoff: 32768
filesize: 664
maxprot: 0x00000001
initprot: 0x00000001
nsects: 0
flags: 0x0
==================================
上記出力は Mach-O フォーマットに記載されているメタデータを読み取って出力しています。これを読み取るための実装について解説していきたいと思います。
この実装については GitHub にアップしているので実際の動作を見たい方は参照ください。
Mach-O フォーマットとは
Wikipedia から引用すると以下のように説明されています。
Mach-O(まーく・おー)はコンパイラが生成するオブジェクトファイルおよび実行ファイルのファイルフォーマットである。NEXTSTEP に由来し、macOS で標準のバイナリフォーマットとして採用されている。
macOS だけでなく、iOS や visionOS でも採用されているので基本的には Apple 製品向けのアプリ開発全般で使われていると思っていいと思います。
Mach-O フォーマットの構造
こちらの記事から画像を引用させてもらうと Mach-O フォーマットの構造は以下のようになっています。
まず最初に Header が配置され、その後に Load Commands と呼ばれる、セグメントやセクションなどファイル全体の情報を示すメタデータが格納されています。
Header のフォーマット
ファイル先頭に配置されている Header のフォーマットは以下のようになっています。
Offset | Description | Value (example) | 備考 |
---|---|---|---|
00000000 | Magic Number | MH_MAGIC_64 | バイナリファイルの先頭によくあるマジックナンバー。判定するには mach-o/loader.h で定義されている MH_MAGIC_64 を利用する。 |
00000004 | CPU Type | CPU_TYPE_X86_64 | |
00000008 | CPU SubType | CPU_SUBTYPE_x86_64_ALL | |
0000000C | File Type | MH_EXECUTE | |
00000010 | Number of Load Commands | 11 | |
00000014 | Size of Load Commands | 2432 | |
00000018 | Flags | ||
0000001C | Reserved | 0 |
ロードコマンド
ヘッダに続いて配置されているデータです。ロードコマンドのあとにデータ部が続きますが、そのデータ部についての構造を記述しているメタデータに相当します。セグメント用のロードコマンドやシンボルテーブル用のロードコマンドなど様々なデータの種類に応じたロードコマンドが定義されています。実態は構造体として定義されており、 segment_command_64
や symtab_command
などがあります。
生成されたバイナリをバイナリエディタで見てみると以下のようになっています。
青の部分が Header で緑の部分が Load Commands、そして赤以降がデータ部になります。
ロードコマンドの種類
今回の解説ではセグメントとシンボルテーブル・ダイナミックシンボルテーブル周りのみの話になりますが、それ以外にも多数のロードコマンドがあります。
以下は mach-o/loader.h
に定義されているロードコマンドを抜き出したものです。
定義名 | 値 | 説明 |
---|---|---|
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 linked shared library |
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 | load a dynamically linked shared library that is allowed to be missing (weak import) |
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 |
LC_LOAD_UPWARD_DYLIB | 0x23 | LC_REQ_DYLD | load upward dylib |
LC_VERSION_MIN_MACOSX | 0x24 | build for MacOSX min OS version |
LC_VERSION_MIN_IPHONEOS | 0x25 | build for iPhoneOS min OS version |
LC_FUNCTION_STARTS | 0x26 | compressed table of function start addresses |
LC_DYLD_ENVIRONMENT | 0x27 | string for dyld to treat like environment variable |
LC_MAIN | 0x28 | LC_REQ_DYLD | replacement for LC_UNIXTHREAD |
LC_DATA_IN_CODE | 0x29 | table of non-instructions in __text |
LC_SOURCE_VERSION | 0x2A | source version used to build binary |
LC_DYLIB_CODE_SIGN_DRS | 0x2B | Code signing DRs copied from linked dylibs |
LC_ENCRYPTION_INFO_64 | 0x2C | 64-bit encrypted segment information |
LC_LINKER_OPTION | 0x2D | linker options in MH_OBJECT files |
LC_LINKER_OPTIMIZATION_HINT | 0x2E | optimization hints in MH_OBJECT files |
LC_VERSION_MIN_TVOS | 0x2F | build for AppleTV min OS version |
LC_VERSION_MIN_WATCHOS | 0x30 | build for Watch min OS version |
LC_NOTE | 0x31 | arbitrary data included within a Mach-O file |
LC_BUILD_VERSION | 0x32 | build for platform min OS version |
LC_DYLD_EXPORTS_TRIE | 0x33 | LC_REQ_DYLD | used with linkedit_data_command, payload is trie |
LC_DYLD_CHAINED_FIXUPS | 0x34 | LC_REQ_DYLD | used with linkedit_data_command |
LC_FILESET_ENTRY | 0x35 | LC_REQ_DYLD | used with fileset_entry_command |
LC_ATOM_INFO | 0x36 | used with linkedit_data_command |
セグメントとセクション
こちらの記事から別の画像を引用させてもらうと、Load Commands やその下に続くセグメントとセクションの情報は以下のように格納されています。
上で説明した通り Load Commands には種類があり、それぞれが担当するデータの情報を保持しています。そして Load Commands 領域の下には実データが続きます。
今回はその中でもセグメントとシンボルテーブル・ダイナミックシンボルテーブルに焦点を当てています。そしてさらにセグメントは細かいセクションに分けられており、セクションは 0 個以上存在します。
図を見ると分かるように Mach-O ファイルには様々なデータ(セグメントやテーブルなど)が含まれています。その中でセクションは番号で参照され、通し番号で管理されています。全体の構造を把握する際のヒントになると思います。
セクションのメタデータ
ひとつ注意点として、Load Command の中のセグメントコマンドにはセクションの「メタデータ」も含まれます。
以下の図はセグメントコマンドのメタデータの構造を示しています。
+--------------------------------------+
| Mach Header |
+--------------------------------------+ <- Machヘッダ終了
| Load Commands |
| +----------------------------------+
| | segment_command_64 | <- 1つのセグメント
| +----------------------------------+
| | section_64 (1) | \
| | section_64 (2) | } セクションメタデータ
| | section_64 (3) | /
| | ... |
| | section_64 (n) |
| +----------------------------------+
| ... 他のLoad Commands ...|
+--------------------------------------+ <- Load Command領域終了
| |
| セクション実データ領域 | <- 上記section_64が指すoffset/sizeで参照
| (コードや静的データ等) |
| |
+--------------------------------------+
Load Command の中の segment_command_64
構造体がセグメントのメタデータを表し、その直下に、そのセグメントが持つセクションのメタデータである section_64
構造体が、セクションの数だけ続きます。
命名規則
セグメントとセクションには命名規則があります。どちらもアンダースコアを 2 つ付けることになっています。
例) __TEXT
セグメント、__text
セクション
Mach-O 以外でも似たような命名規則をしているので見覚えがある人もいるでしょう。
Mach-O で標準的なセグメント
Mach-O では以下のセグメントが標準的に使われています。
セグメント名 | 説明 |
---|---|
__PAGEZERO |
NULL(=0)ポインタ経由のアクセスに対して、直ちにプログラムを異常終了させるためのセグメント |
__TEXT |
コードと Read Only データが格納されているセグメント |
__DATA |
Writable データが格納されているセグメント |
__LINKEDIT |
ダイナミックリンカが使用する情報が格納されているセグメント |
セクション例
セクション名 | 説明 |
---|---|
__text |
実行可能なコードが格納されているセクション。 __TEXT セグメントに含まれる |
__cstring |
コンスタント文字列。非 \0 のバイトの並びが格納されているセクション。 __TEXT セグメントに含まれる |
__data |
初期化済み( int i = 3; など)の可変なデータが格納されているセクション。 __DATA セグメントに含まれる |
__bss |
未初期化のスタティック変数( static int i; など)の可変データが格納されているセクション。 __DATA セグメントに含まれる |
フォーマットの概要について触れてきました。ここからは、実際に各データを読み取り、出力するための実装について解説していきます。
ファイルを読み込む
まずはファイルを読み込みメモリにマップするところから始めます。
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// -----------------------
const char* file_path = "<PATH/TO/BINARY/FILE>";
int fd = open(file_path, O_RDONLY);
struct stat st;
if (fstat(fd, &st) < 0)
{
perror("fstat");
close(fd);
return;
}
void* buffer = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (buffer == MAP_FAILED)
{
perror("mmap");
close(fd);
return;
}
close(fd);
mmap
システムコールを利用してバイナリファイルの内容をそのままメモリにマップします。
ヘッダを出力する
最初に見ていくのはヘッダ部分です。ここにも様々な情報が格納されています。フォーマットについては冒頭で説明した通りです。それを出力してみましょう。
const mach_header_t* header = (const mach_header_t*)buffer;
if (header->magic != MH_MAGIC_64)
{
fprintf(stderr, "The file [%s] is not a Mach-O file.\n", file_path);
munmap(buffer, st.st_size);
return;
}
printf("-----------------------------------------------------------------------\n");
printf("Printing a mach-o header.\n");
printf("-----------------------------------------------------------------------\n");
printf("Magic: %d\n", header->magic);
printf("CPU Type: %d\n", header->cputype);
printf("CPU Sub Type: %d\n", header->cpusubtype);
printf("File Type: %d\n", header->filetype);
printf("Command Count: %d\n", header->ncmds);
printf("Size of Commands: %d\n", header->sizeofcmds);
printf("Flags: %d\n", header->flags);
printf("\n\n");
まずヘッダの最初にはマジックナンバーがあります。これを見て対象のファイルが Mach-O ファイルであるかを判断しているわけですね。
マジックナンバーなので数値が保存されています。 <mach-o/loader.h>
にこの値を表す MH_MAGIC_64
という定義があります。
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
最初の部分ではこの値がファイル先頭にあるかを判定しているわけですね。そしてもし違った場合は処理対象ではないので処理をスキップしています。
上記を実行すると以下のような結果が得られます。
-----------------------------------------------------------------------
Printing a mach-o header.
-----------------------------------------------------------------------
Magic: -17958193
CPU Type: 16777228
CPU Sub Type: 0
File Type: 2
Command Count: 17
Size of Commands: 1056
Flags: 2097285
セグメント情報を出力する
次はセグメント情報を出力しましょう。
セグメント情報はロードコマンドの segment_command_64
構造体が持っています。
ロードコマンドはヘッダの直下に存在しているため、まずはポインタをその位置に進めます。
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
header
ポインタの位置は実質ファイルの先頭のため、その位置からヘッダサイズ分ポインタを進めます。
そしてロードコマンドの数分ループ処理します。ロードコマンドがいくつあるかはヘッダに情報があります。( header->ncmds
)
for (unsigned int i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize)
{
cur_seg_cmd = (segment_command_64*)cur;
if (cur_seg_cmd->cmd != LC_SEGMENT_64)
{
continue;
}
printf(" cmd: LC_SEGMENT_64\n");
printf(" cmdsize: %d\n", cur_seg_cmd->cmdsize);
printf(" segname: %s\n", cur_seg_cmd->segname);
printf(" vmaddr: 0x%016llx\n", cur_seg_cmd->vmaddr);
printf(" vmsize: 0x%016llx\n", cur_seg_cmd->vmsize);
printf(" fileoff: %llu\n", cur_seg_cmd->fileoff);
printf("filesize: %llu\n", cur_seg_cmd->filesize);
printf(" maxprot: 0x%08x\n", cur_seg_cmd->maxprot);
printf("initprot: 0x%08x\n", cur_seg_cmd->initprot);
printf(" nsects: %d\n", cur_seg_cmd->nsects);
printf(" flags: 0x%d\n", cur_seg_cmd->flags);
}
ループの更新処理では i
のインクリメントとは別にポインタを進める処理が書かれています。
cur += cur_seg_cmd->cmdsize;
そして対象のロードコマンドがセグメントコマンド( == LC_SEGMENT_64
)だった場合にその内容を出力するようにしています。
セクション情報を出力する
セグメントとセクションのところで説明した通り、セグメントコマンドの場合はセグメント情報のすぐ下にセクション情報が並んでいます。
次はそのセクション情報を出力しましょう。
セグメントコマンドには対象セグメントにいくつのセクションがあるかを示す nsects
フィールドがあります。この数の分だけループを回すことでセクション情報を列挙することができます。
void print_section(segment_command_64* seg_cmd)
{
// section 情報は segment 情報の直後に配置されている
// そのため section_t 構造体は segment_command_64 ひとつ分の
// アドレスを進めた先(= cur_seg_cmd + 1)の位置から開始する
section_64* section = (section_64*)(seg_cmd + 1);
for (uint32_t j = 0; j < seg_cmd->nsects; j++)
{
printf(" sectname: %s\n", section[j].sectname);
printf(" segname: %s\n", section[j].segname);
printf(" addr: 0x%016llx\n", section[j].addr);
printf(" size: 0x%016llx\n", section[j].size);
printf(" offset: %d\n", section[j].offset);
printf(" align: %d\n", section[j].align);
printf(" reloff: %d\n", section[j].reloff);
printf(" nreloc: %d\n", section[j].nreloc);
printf(" flags: 0x%08x\n", section[j].flags);
printf("reserved1: %d\n", section[j].reserved1);
printf("reserved2: %d\n", section[j].reserved2);
printf("reserved3: %d\n", section[j].reserved3);
printf("- - - - - - - - - - - - - - - - - - - - - - \n");
}
}
コメントにも書いてある通り、seg_cmd
ポインタが示す位置はセグメントの先頭位置です。そのため、 + 1
することで segment_command_64
構造体のサイズ分ポインタが進むため、結果的に最初のセクション情報の先頭位置にポインタが進みます。
そしてそこから、先ほどの nsects
フィールドの数分ループすることですべてのセクション情報を列挙することができます。
シンボルテーブルコマンド・ダイナミックシンボルテーブルコマンドを出力する
次に見ていくのは別のロードコマンドである「シンボルテーブルコマンド」と「ダイナミックシンボルテーブルコマンド」です。
それぞれ symtab_command
, dysymtab_command
という構造体で定義されています。
前回のセグメントコマンドをループ処理した際に、対象が上記コマンドだったら分岐して出力してみます。
struct load_command* cur_cmd;
for (unsigned int i = 0; i < header->ncmds; i++, cur += cur_cmd->cmdsize)
{
cur_cmd = (struct load_command*)cur;
if (cur_cmd->cmd == LC_SYMTAB)
{
struct symtab_command* symtab_cmd = (struct symtab_command*)cur_cmd;
print_symtable(symtab_cmd);
}
else if (cur_cmd->cmd == LC_DYSYMTAB)
{
struct dysymtab_command* dysymtab_cmd = (struct dysymtab_command*)cur_cmd;
print_dysymtable(dysymtab_cmd);
}
}
void print_dysymtable(struct dysymtab_command* dysymtab_cmd)
{
printf("[Dynamic symbol table]\n");
printf(" cmd: %d\n", dysymtab_cmd->cmd);
printf(" cmdsize: %d\n", dysymtab_cmd->cmdsize);
printf(" ilocalsym: %d\n", dysymtab_cmd->ilocalsym);
printf(" nlocalsym: %d\n", dysymtab_cmd->nlocalsym);
printf(" iextdefsym: %d\n", dysymtab_cmd->iextdefsym);
printf(" nextdefsym: %d\n", dysymtab_cmd->nextdefsym);
printf(" tocoff: %d\n", dysymtab_cmd->tocoff);
printf(" ntoc: %d\n", dysymtab_cmd->ntoc);
printf(" modtaboff: %d\n", dysymtab_cmd->modtaboff);
printf(" nmodtab: %d\n", dysymtab_cmd->nmodtab);
printf(" extrefsymoff: %d\n", dysymtab_cmd->extrefsymoff);
printf(" nextrefsyms: %d\n", dysymtab_cmd->nextrefsyms);
printf("indirectsymoff: %d\n", dysymtab_cmd->indirectsymoff);
printf(" nindirectsyms: %d\n", dysymtab_cmd->nindirectsyms);
printf(" extreloff: %d\n", dysymtab_cmd->extreloff);
printf(" nextrel: %d\n", dysymtab_cmd->nextrel);
printf(" locreloff: %d\n", dysymtab_cmd->locreloff);
printf(" nlocrel: %d\n", dysymtab_cmd->nlocrel);
printf("\n\n");
}
void print_symtable(struct symtab_command* symtab_cmd)
{
printf("[Symbol table]\n");
printf(" cmd: %d\n", symtab_cmd->cmd);
printf("cmdsize: %d\n", symtab_cmd->cmdsize);
printf(" symoff: %d\n", symtab_cmd->symoff);
printf(" nsyms: %d\n", symtab_cmd->nsyms);
printf(" stroff: %d\n", symtab_cmd->stroff);
printf("strsize: %d\n", symtab_cmd->strsize);
printf("\n\n");
}
上記を実行すると以下のような内容が出力されます。
[Symbol table]
cmd: 2
cmdsize: 24
symoff: 32928
nsyms: 3
stroff: 32984
strsize: 40
[Dynamic symbol table]
cmd: 11
cmdsize: 80
ilocalsym: 0
nlocalsym: 0
iextdefsym: 0
nextdefsym: 2
tocoff: 0
ntoc: 0
modtaboff: 0
nmodtab: 0
extrefsymoff: 0
nextrefsyms: 0
indirectsymoff: 32976
nindirectsyms: 2
extreloff: 0
nextrel: 0
locreloff: 0
シンボルテーブルからシンボル名を出力する
実はこの記事の中で一番書きたかったのはここと、次のダイナミックシンボルテーブルの処理でした。
Linux では nm
コマンドでバイナリに含まれるシンボル名を列挙することができます。このコマンドが参照しているのもまさにシンボルテーブルです。
ここではシンボルテーブルがどんな構造をしていて、それをどう取得するのかについて書いていきたいと思います。
試しに、今回テスト用にコンパイルしたバイナリに対して nm
コマンドを実行してみましょう。以下の結果を得ました。
0000000100000000 T __mh_execute_header
0000000100003f58 T _main
U _printf
3 つのシンボルが含まれていることが分かります。冒頭の、今回の実装の出力結果を見てもらうと、しっかりとこの 3 つのシンボルが出力されているのが確認できます。
-----------------------------------------------------------------------
Printing a symbol table and a dynamic symbol table
-----------------------------------------------------------------------
Symbol name: __mh_execute_header
Symbol name: _main
Symbol name: _printf
ここではこの実装部分を見ていきます。
ファイル構造
まず最初にファイルに格納されている情報を整理します。Mach-O ファイルにはヘッダとロードコマンドのあとにデータ部が続きます。そこにシンボルテーブルなど様々な実データが存在しています。
symtab_command
/ dysymtab_command
構造体は対象データへのファイル先頭からのオフセットが格納されています。
ファイル先頭 (offset 0)
|
|---- symtab_command
| \
| +--- symoff で指定されるシンボルテーブル相対位置
| +--- stroff で指定される文字列表相対位置
|
+---- ファイル終端
この symoff
はシンボルテーブルのデータ位置がファイル先頭からどれだけオフセットしているかの情報です。また stroff
は文字列データがファイル先頭からどれだけオフセットしているかの情報です。
シンボルテーブルからシンボル名を求める
上で書いた通り、シンボルテーブルにはシンボル名が直接格納されてるわけではありません。シンボルテーブルからシンボル名を得る方法を見ていきましょう。
前段のシンボルテーブルコマンドの出力のタイミングで対象コマンドの参照を得ている前提で説明していきます。
// stroff はファイル先頭からのオフセット
uint32_t stroff = symtab_cmd->stroff;
上記のオフセットを使って実際のデータへの参照を得るには以下のようにします。
char* strtab = (char*)base_addr + stroff;
nlist_64* symtab = (nlist_64*)(base_addr + symtab_cmd->symoff);
ここの base_addr
は冒頭で説明した、ファイルを mmap
によってマップしたメモリの先頭アドレスです。オフセットの値が「ファイル先頭からのオフセット」であったことを思い出してください。
現在マップされているメモリアドレスの先頭からオフセットを足すことで目的のデータへのポインタを得ることができます。
シンボルテーブル内のシンボルをすべて列挙する
シンボルテーブルに何個のシンボルが含まれているかはシンボルテーブルコマンドの nsyms
フィールドに保存されています。つまりこの回数分ループを回すことでシンボル名を列挙することができます。
for (uint32_t i = 0; i < symtab_cmd->nsyms; i++)
{
uint32_t strtab_offset = symtab[i].n_un.n_strx;
char* symbol_name = strtab + strtab_offset;
printf("Symbol name: %s\n", symbol_name);
}
n_strx
は文字列データ先頭からのオフセットです。先頭アドレスは strtab
ポインタの位置なので、そのポインタに対して足すだけで対象シンボル名の開始位置にポインタを移動させることができます。
前述したように、文字列データは \0
区切りとなっているため取得したポインタ位置をそのまま printf
で出力することでシンボル名が出力されるというわけです。
これを実行すると以下の結果を得ます。
Symbol name: __mh_execute_header
Symbol name: _main
Symbol name: _printf
nm
コマンドと同じシンボルが出力されました。
ダイナミックシンボルテーブルから必要なシンボルを検索する
最後に見ていくのはダイナミックシンボルテーブルで定義されているシンボル名を検索する方法です。
ダイナミックシンボルテーブルの一部は動的に解決されるシンボルなどが定義されています。
dysymtab_command
の定義を見てみると以下のようになっています。
struct dysymtab_command {
uint32_t cmd; // コマンドの種類 (LC_DYSYMTAB)
uint32_t cmdsize; // コマンドのサイズ
uint32_t ilocalsym; // ローカルシンボルのインデックス
uint32_t nlocalsym; // ローカルシンボルの数
uint32_t iextdefsym; // 外部シンボルのインデックス
uint32_t nextdefsym; // 外部シンボルの数
uint32_t iundefsym; // 未定義シンボルのインデックス
uint32_t nundefsym; // 未定義シンボルの数
uint32_t tocoff; // TOC (Table of Contents) のオフセット
uint32_t ntoc; // TOC のエントリ数
uint32_t modtaboff; // モジュールテーブルのオフセット
uint32_t nmodtab; // モジュールテーブルのエントリ数
uint32_t extrefsymoff; // 外部参照シンボルのオフセット
uint32_t nextrefsyms; // 外部参照シンボルの数
uint32_t indirectsymoff; // 間接シンボルテーブルのオフセット
uint32_t nindirectsyms; // 間接シンボルの数
uint32_t extreloff; // 外部再配置情報のオフセット
uint32_t nextrel; // 外部再配置情報の数
uint32_t locreloff; // ローカル再配置情報のオフセット
uint32_t nlocrel; // ローカル再配置情報の数
};
この中で今回解説するのは「間接シンボルテーブル」になります。これは、外部に定義が存在し、リンカが解決するべきシンボルが定義されているテーブルになります。
例えば外部のライブラリに依存している printf
などが該当します。
ちなみに今回のこのあたりを調査している理由として、Meta が公開している fishhook というライブラリの動作を解析する中で使われているというのがあります。
ちなみにこの fishhook
は Mach-O ファイルを解析して、dyld
(リンカ)がリンクを行う際の処理をフックして、独自の関数に差し替える機能を提供してくれるものです。
その中でまさに dysymtab_command
の内容を利用して処理していたので調べました。
今回の対象のシンボルが存在するのは、 __DATA
セグメントと __DATA_CONST
セグメントなのでそのセグメントだけ処理を行います。
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0)
{
continue;
}
セグメント名が SEG_DATA
と SEG_DATA_CONST
のときのみに絞ります。
対象セクション内のシンボルを列挙する
対象セグメントは前述の通りです。さらにその中の特定のセクションのみ処理を行います。
今回の実装では以下のセクションタイプのときのみ処理していします。
section_t* section = (section_t*)(cur + sizeof(segment_command_t)) + j;
if ((section->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS ||
(section->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) { }
シンボル名を検索・出力している処理は以下の通りです。
printf("--------- Dynamic symbols\n");
for (uint32_t j = 0; j < cur_seg_cmd->nsects; j++)
{
section_t* section = (section_t*)(cur + sizeof(segment_command_t)) + j;
if ((section->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS ||
(section->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS)
{
uint32_t* indirect_symtab = (uint32_t*)(base_addr + dysymtab_cmd->indirectsymoff);
uint32_t* indirect_symbol_indices = indirect_symtab + section->reserved1;
for (uint32_t k = 0; k < section->size / sizeof(void*); k++)
{
uint32_t symtab_index = indirect_symbol_indices[k];
if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS))
{
continue;
}
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
char* symbol_name = strtab + strtab_offset;
printf(" - %s\n", symbol_name);
}
}
}
前述したように、セグメントコマンドには対象セグメントが有するセクションの数を示す nsects
フィールドがあります。その数分ループを回し、さらにそのセクションが調査対象である場合にシンボルの検索を行っています。
間接シンボルテーブルの位置は indirectsymoff
フィールドが持っているオフセットを元に求めます。
uint32_t* indirect_symtab = (uint32_t*)(base_addr + dysymtab_cmd->indirectsymoff);
また、間接シンボルのシンボル名がシンボルテーブルのどの位置かを示すインデックスリストが存在するためそれの位置も求めます。
uint32_t* indirect_symbol_indices = indirect_symtab + section->reserved1;
上記ふたつの情報を使って間接シンボル名を出力しましょう。
for (uint32_t k = 0; k < section->size / sizeof(void*); k++)
{
uint32_t symtab_index = indirect_symbol_indices[k];
if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS))
{
continue;
}
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
char* symbol_name = strtab + strtab_offset;
printf(" - %s\n", symbol_name);
}
対象のセクションは置き換えるべきシンボルのリストを持っているためその分のループを行います。
ループの回数はセクションのサイズを関数ポインタのサイズで割ったものを利用します。これはセクションが関数ポインタのリストになっているためです。具体的には以下の部分ですね。
for (uint32_t k = 0; k < section->size / sizeof(void*); k++) { ... }
また、前段で関数ポインタのインデックスリストを求めてるため、それを添え字でアクセスすることでそれぞれのシンボルテーブルのインデックスを求めることができます。
ここまでの処理を簡単にまとめると以下のようになります。
- 間接シンボルテーブルからシンボルテーブルへのインデックスリストを得る
- シンボルテーブルへのインデックスリストを用いてシンボルテーブルからシンボル名を求める
ということを行っています。
実際にシンボル名を求めているところは以下の部分になります。
uint32_t* indirect_symtab = (uint32_t*)(base_addr + dysymtab_cmd->indirectsymoff);
uint32_t* indirect_symbol_indices = indirect_symtab + section->reserved1;
for (uint32_t k = 0; k < section->size / sizeof(void*); k++)
{
uint32_t symtab_index = indirect_symbol_indices[k];
if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS))
{
continue;
}
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
char* symbol_name = strtab + strtab_offset;
printf(" - %s\n", symbol_name);
}
最初の行で間接シンボルテーブルへのポインタを取得しています。そしてその次の行で、そのテーブルからインデックスリストへのオフセットを用いてリストの先頭のポインタを求めています。
求めたインデックリストをシンボルの数分ループして処理を行います。
シンボル名の求め方はシンボルテーブルのシンボル名すべてを列挙したのとほぼ同じです。違いは、シンボルテーブルの内容を列挙したときは全部のシンボル名をループで処理しましたが、こちらでは間接シンボルテーブルが保持しているシンボルテーブルのインデックス分ループ処理している点が異なります。
言い換えると、間接シンボルテーブルはリンカが置き換えるべきシンボルのリストをインデックスのリストという形で保持している、というわけです。
これを実行すると最終的に以下の結果を得ます。
--------- Dynamic symbols
- _printf
今回は printf
という外部のシンボルに依存しているということが分かりました。
冒頭で紹介した、今回コンパイルしたサンプルプログラムを再掲すると以下のようになっています。
#include <stdio.h>
int main(int argc, char* argv[])
{
printf("DONE\n");
return 0;
}
依存しているのが printf
だけなので納得の結果となりました。
さいごに
今回は Mach-O ファイルフォーマットの、特にシンボル周りについてまとめました。
fishhook
を見ると共有ライブラリのリンクについては比較的簡単にフック処理ができることが分かります。やはりこうした基礎を知ることで、なぜそれができるのか、どういう処理が行われているのかが明白になるのでとてもおもしろいですね。
次回は fishhook
が行っている処理について考察したいと思います。
Discussion