🧩

Mach-O フォーマットを調べがてら otool 風出力をしてみる

2024/12/09に公開

KeyVisual

はじめに

最近、低レイヤー周りを調べたり実装するのが楽しいので 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 にアップしているので実際の動作を見たい方は参照ください。

https://github.com/edom18/Mach-O-Analyze

Mach-O フォーマットとは

Wikipedia から引用すると以下のように説明されています。

Mach-O(まーく・おー)はコンパイラが生成するオブジェクトファイルおよび実行ファイルのファイルフォーマットである。NEXTSTEP に由来し、macOS で標準のバイナリフォーマットとして採用されている。

macOS だけでなく、iOS や visionOS でも採用されているので基本的には Apple 製品向けのアプリ開発全般で使われていると思っていいと思います。

Mach-O フォーマットの構造

こちらの記事から画像を引用させてもらうと Mach-O フォーマットの構造は以下のようになっています。

Mach-O フォーマットの構造1

まず最初に 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_64symtab_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 やその下に続くセグメントとセクションの情報は以下のように格納されています。

Mach-O フォーマットの構造2

上で説明した通り 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 という定義があります。

Mach-O フォーマットのマジックナンバー
#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 コマンドを実行してみましょう。以下の結果を得ました。

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 構造体は対象データへのファイル先頭からのオフセットが格納されています。

Mach-O ファイル構造
ファイル先頭 (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 の定義を見てみると以下のようになっています。

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_DATASEG_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