Mach-Oのシンボルテーブルを読み込む

前回に引き続き共有ライブラリの仕組を知るための準備としてMach-Oの構造を調べる。今回はシンボルテーブルを読み込んでみる。環境はx86_64。

シンボルとは

私達がソースを書く時には関数や変数に

int i;
i = foo();

の様に文字列の名前を付ける。しかし、CPUが関数や変数を参照するときはアドレスを使う。この間を取り持つオブジェクトをシンボルと言う。具体的にはこのようなもの。/usr/include/mach-o/nlist.hで定義されている。

struct nlist_64
{
    union {
        uint32_t n_strx;
    } n_un;
    uint8_t n_type;
    uint8_t n_sect;
    uint16_t n_desc;
    uint64_t n_value;
};

n_strxが対応する文字列を指し、n_valueが値(アドレス)を示す。例えばこのシンボルが関数foo()を指すものだとするとn_strxからfooと言う文字列が、n_valueからfoo()のアドレスを知ることができる。必ずしもそうではないが、今日のところはこれで良しとする(n_typeの値によってn_valueの意味は異なる)。

シンボルテーブルとストリングテーブル

Mach-Oファイルにはファイル内で定義したり参照したシンボルのリストが含まれていて、これをシンボルテーブルと言う。シンボルテーブルの位置や、要素の数は LC_SYMTAB というロードコマンドに記述されている。シンボルテーブルの要素は先に触れた nlist_64 構造体。シンボルに対応する文字列はシンボルテーブルとは別にリストされておりこちらはストリングテーブルと言う。構造は NULL 文字(\0)終端の文字列を連結したもの。n_strx はストリングテーブル先頭からのオフセットで文字列を示す。ストリングテーブルの先頭は必ず NULL 文字なので、n_strx=0 とすると空文字列""を表すことになる。


シンボルテーブルの構造は mach-o/loder.h で定義されている。

struct symtab_command
   {
   uint_32 cmd;
   uint_32 cmdsize;
   uint_32 symoff;
   uint_32 nsyms;
   uint_32 stroff;
   uint_32 strsize;
};

ファイルの先頭からのオフセットsymoffの位置に、nsyms個のnlist_64構造体が並んでいる。この構造体にはストリングテーブルの位置も含まれていて、ファイル先頭からのオフセットstroffの位置にstrsize Byteの文字列テーブルが有る。


図にするとこんな感じ。

シンボルテーブルを読み込んでみる

前回使用したhoge1.sで単純なMach-Oを作ってそこから読み込む。

.file "hoge1.s"
.text
.globl start
start:
    movq $0x2000001, %rax  // exit
    movq hoge(%rip), %rdi
    syscall
.data
hoge:
   .byte 0x5
$ as -o hoge1.o hoge1.s && ld -o hoge1 hoge1.o


前回のロードコマンドを読込むコードにシンボルテーブルとストリングテーブルを読み込むコードを追加する。
ソースコード抜粋(ml2.c)

・
・
#include <string.h>
#include <mach-o/nlist.h>
・
・
static struct symtab_command symtab_command;
static struct nlist_64 *nlists;
static char *string_table;
・
・
int load_symtab_command(FILE *f, struct symtab_command *symtab_cmd) {
    fread(&symtab_cmd->symoff, sizeof(uint32_t), 1, f);
    fread(&symtab_cmd->nsyms, sizeof(uint32_t), 1, f);
    fread(&symtab_cmd->stroff, sizeof(uint32_t), 1, f);
    fread(&symtab_cmd->strsize, sizeof(uint32_t), 1, f);
    
    printf("symoff = 0x%08x\n", symtab_cmd->symoff);
    printf("nsyms = 0x%08x\n", symtab_cmd->nsyms);
    printf("stroff = 0x%08x\n", symtab_cmd->stroff);
    printf("strsize = 0x%08x\n", symtab_cmd->strsize);

    return 0;
}

int load_nlist_64(FILE *f, struct nlist_64 *nl_64) {    
    fread(&nl_64->n_un.n_strx, sizeof(uint32_t), 1, f);
    fread(&nl_64->n_type, sizeof(uint8_t), 1, f);
    fread(&nl_64->n_sect, sizeof(uint8_t), 1, f);
    fread(&nl_64->n_desc, sizeof(uint16_t), 1, f);
    fread(&nl_64->n_value, sizeof(uint64_t), 1, f);
    
    printf("n_un.n_strx = 0x%08x\n", nl_64->n_un.n_strx);
    printf("n_type = 0x%02x\n", nl_64->n_type);
    printf("n_sect = 0x%02x\n", nl_64->n_sect);
    printf("n_desc = 0x%04x\n", nl_64->n_desc);
    printf("n_value = 0x%016llx\n", nl_64->n_value);

    return 0;
}


int main(int argc, char** argv) {
    ・
    ・
    load_commands = (struct load_command *) malloc(sizeof(struct load_command) * mach_header.ncmds);
    
    // load load commands
    for (i=0; i < mach_header.ncmds; i++) {
        fread(&load_commands[i].cmd, sizeof(uint32_t), 1, f);
        fread(&load_commands[i].cmdsize, sizeof(uint32_t), 1, f);

        printf("=== cmd = 0x%08x, cmdsize=0x%08x ===\n",
               load_commands[i].cmd, load_commands[i].cmdsize);

        // next load command location
        next_pos = ftell(f) + load_commands[i].cmdsize - (sizeof(uint32_t) * 2);

        if (load_commands[i].cmd == LC_SYMTAB) {
            // symtab_command
            printf("=== LC_SYMTAB ===\n");
            symtab_command.cmd = load_commands[i].cmd;
            symtab_command.cmdsize = load_commands[i].cmdsize;
            load_symtab_command(f, &symtab_command);
        }

        fseek(f, next_pos, SEEK_SET);
    }

    // load string_table
    string_table = (char *) malloc(symtab_command.strsize);
    
    fseek(f, symtab_command.stroff, SEEK_SET);
    fread(string_table, symtab_command.strsize, 1, f);

    // dump string table
    printf("=== string table ===\n");
    p = string_table;
    for (i = 0; i <= symtab_command.nsyms; i++) {
        printf("offset = 0x%08x, string_table[%d] = %s\n", (unsigned) (p - string_table), i, p);
        p = strchr(p, 0) + 1;
    }

    // load nlist_64
    nlists = (struct nlist_64 *) malloc(sizeof(struct nlist_64) * symtab_command.nsyms);
    fseek(f, symtab_command.symoff, SEEK_SET);
    for (i = 0; i < symtab_command.nsyms; i++) {
        printf("=== nlist_64[%d] ===\n", i);
        load_nlist_64(f, &nlists[i]);
        printf("(string = %s)\n", string_table + nlists[i].n_un.n_strx);
    }
    ・
    ・
}


実行結果(抜粋)

$ ./ml2 hoge1.o
=== cmd = 0x00000002, cmdsize=0x00000018 ===
=== LC_SYMTAB ===
symoff = 0x0000018c
nsyms = 0x00000002
stroff = 0x000001ac
strsize = 0x0000000c
=== string table ===
offset = 0x00000000, string_table[0] = 
offset = 0x00000001, string_table[1] = start
offset = 0x00000007, string_table[2] = hoge
=== nlist_64[0] ===
n_un.n_strx = 0x00000007
n_type = 0x0e
n_sect = 0x02
n_desc = 0x0000
n_value = 0x0000000000000010
(string = hoge)
=== nlist_64[1] ===
n_un.n_strx = 0x00000001
n_type = 0x0f
n_sect = 0x01
n_desc = 0x0000
n_value = 0x0000000000000000
(string = start)

nlist_64にstringと言うメンバは無いけど、読みやすさのためにストリングテーブルから引っ張ってきた文字列を()内に記述した。答え合わせをするには otool と nm コマンドを使用する。


symtab_commandの内容

$ otool -l hoge1.o
hoge1.o:
(略)
Load command 1
     cmd LC_SYMTAB
 cmdsize 24
  symoff 396
   nsyms 2
  stroff 428
 strsize 12
(略)


シンボルテーブル

$ nm -x hoge1.o
0000000000000010 0e 02 0000 0000000000000007 hoge
0000000000000000 0f 01 0000 0000000000000001 start

(nm -x で n_value, n_type, n_sect, n_desc, n_un.strx, 文字列 という順で表示される)


ml2.cで読み込んだ値と一致していることがわかる。フラグ等の細かい意味は今は気にしない。ここではMach-Oの中でシンボルが具体的にどのように格納されているのかが概ねわかれば十分。


Mach-Oの中での位置を図示するとこんな感じ。


MachPortsでbinutilsをインストールしておくとgobjdumpコマンドが使える。

$ sudo port install binutils


このコマンドを使用してシンボルの値がどこを指しているのか確かめてみる。

$ gobjdump -d hoge1.o

hoge1.o:     file format mach-o-le


Disassembly of section .text:

0000000000000000 :
   0:   48 c7 c0 01 00 00 02    mov    $0x2000001,%rax
   7:   48 8b 3d 00 00 00 00    mov    0x0(%rip),%rdi        # e 
   e:   0f 05                   syscall 

Disassembly of section .data:

0000000000000010 :
  10:   05                      .byte 0x5

hoge1.s と見比べてみると、ラベル start 部分の命令は仮想メモリアドレス 0x0 へ、ラベル hoge 部分のデータは仮想メモリアドレス 0x10 へ配置されている。これらはそれぞれ、nlist_64[1]、nlist_64[0]の値と一致している。


hoge1.s で hoge(%rip) と記述した部分が 0x2(%rip)ではなく、0x0(%rip) となっている。これはいったい・・・。(hoge と 0x7のmov命令とのrip相対アドレスは、ripは次の命令のアドレスを保持するので 0x10 - 0xe = 0x2)
これにはリロケーションと言うものが絡んでいる。リロケーションに関する事は次回調べよう。

まとめ

  • 関数名や変数名はMach-Oではシンボルとして扱われる
  • シンボルに関与するデータ構造にはシンボルテーブルとストリングテーブルが有る
  • それらの構造のMach-Oファイルでの具体的な格納のされ方を確認した

参考文献

Linkers & Loaders

Linkers & Loaders