Mach-Oのリロケーション情報を読み込む

引き続き、共有ライブラリの仕組を知るための準備でMach-Oの構造を調べる。今回はリロケーション情報を読み込んでみる。環境はx86_64

リロケーションとは

最終形式にリンクされる前の中間形式のMach-Oファイルにはコードやデータが含まれているが、コードにデータへの参照(データのアドレス)が含まれている場合は、データが実際に配置されるアドレスに応じて、コード中に含まれる参照を書き換えなければならない。callやjmp等の分岐命令も実際に分岐先が配置されるアドレスに応じて参照を書き換えなければならない。例えば、中間形式では1234に有るデータを参照していても、そのデータがリンク後は4321に配置されたならば、コード中の「1234」と言うアドレスを「4321」に書き換えなければならない。これをリロケーションと言う。

実際のMach-Oファイルで見てみる

ソース

terux:blog_macho_struct teru$ cat hoge2.s
.file "hoge2.s"
.text
.globl start
start:
    movq $0x2000001, %rax
    movq hoge(%rip), %rdi
    syscall
.data
hoge:
   .byte 0x5


コンパイル

$ as -o hoge2.o hoge2.s && ld -o hoge2 hoge2.o


gobjdumpでマシン語を確認

中間形式

$ gobjdump -D hoge2.o
(略)
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 
(略)
0000000000000010 :
  10:   05                      .byte 0x5
(略)

左の "0:","1:" は配置される仮想メモリアドレス。続く "48 c7 ..." はマシン語。それ以降は逆アセンブルして得たアセンブリ

実行形式

$ gobjdump -d hoge2
(略)
0000000000001ff0 :
    1ff0:       48 c7 c0 01 00 00 02    mov    $0x2000001,%rax
    1ff7:       48 8b 3d 02 00 00 00    mov    0x2(%rip),%rdi        # 2000 
    1ffe:       0f 05                   syscall 

0000000000002000 :
    2000:       05                      .byte 0x5


"hoge"の位置に有るデータを参照しているmov命令に注目

48 8b 3d 00 00 00 00    mov    0x0(%rip),%rdi (中間形式ではこう)
↓
48 8b 3d 02 00 00 00    mov    0x2(%rip),%rdi (実行形式ではこう)

中間形式では仮の値として 0x00000000 となっているが、実行形式ではリンカによってリロケーションされて 0x00000002 (x86はリトルエンディアン) に書き換えられていることがわかる。

リロケーション情報を読み込む

どこを書き換えれば良いのか、どのシンボルの値に合わせて書き換えれば良いのかと言う情報は mach-o/reloc.h で relocation_info構造体としてMach-Oファイルに格納されている。r_typeの値については mach-o/x86_64/reloc.h で詳しく例付きで述べられている。

struct relocation_info {
   int32_t   r_address;	      /* offset in the section to what is being
                                 relocated */
   uint32_t  r_symbolnum:24,  /* symbol index if r_extern == 1 or section
                                 ordinal if r_extern == 0 */
             r_pcrel:1,       /* was relocated pc relative already */
             r_length:2,      /* 0=byte, 1=word, 2=long, 3=quad */
             r_extern:1,      /* does not include value of sym referenced */
             r_type:4;        /* if not 0, machine specific relocation type */
};

section_64構造体の、reloff と nreloc メンバがそのセクションに関するリロケーション情報テーブルの開始位置と含まれるrelocation_info構造体の数を示している。


それぞれのセグメントに含まれるセクション、それぞれからリロケーション情報を読み込む。参照しているシンボルの情報も表示する。
前々回のml3.cにリロケーション情報を読み込むコードを追加する。

ソース(ml4.c)

/* getbits: get n bits from position p */
unsigned getbits(unsigned x, int p, int n)
{
    return (x >> (p+1-n)) & ~(~0 << n);
}

int load_relocation_info(FILE *f, struct relocation_info *reloc_info) {
    uint32_t value;

    fread(&reloc_info->r_address, sizeof(uint32_t), 1, f);
    fread(&value, sizeof(uint32_t), 1, f);
    reloc_info->r_symbolnum = getbits(value, 23, 24);    
    reloc_info->r_pcrel = getbits(value, 24, 1);
    reloc_info->r_length = getbits(value, 26, 2);
    reloc_info->r_extern = getbits(value, 27, 1);
    reloc_info->r_type = getbits(value, 31, 4);

    printf("r_address = 0x%08x\n", reloc_info->r_address);
    printf("r_type = 0x%x\n", reloc_info->r_type);
    printf("r_extern = 0x%x\n", reloc_info->r_extern);
    printf("r_length = 0x%x\n", reloc_info->r_length);
    printf("r_pcrel = 0x%x\n", reloc_info->r_pcrel);
    printf("r_symbolnum = 0x%06x\n", reloc_info->r_symbolnum);

    return 0;
}

int main(int argc, char **argv) {
(略)
    // load relocation info
    relocation_infos = (struct relocation_info *)malloc(sizeof(struct relocation_info) * nrelocs);
    
    p_reloc = relocation_infos;
    for (i=0; i < nsects; i++) {
        fseek(f, sections[i].reloff, SEEK_SET);
        for (j=0; j < sections[i].nreloc; j++) {
            printf("=== section[%d], relocation_info[%d] ===\n", i, j);
            load_relocation_info(f, p_reloc);
            if (p_reloc->r_extern) {
                printf("    symbol_value = 0x%016llx\n", nlists[p_reloc->r_symbolnum].n_value);
                printf("    string_table_index = 0x%0x\n", nlists[p_reloc->r_symbolnum].n_un.n_strx);
                printf("    string = %s\n", string_data + nlists[p_reloc->r_symbolnum].n_un.n_strx);
            }
            p_reloc++;
        }
    }
(略)
}


実行結果

$ ml4 hoge2.o
(略)
=== LC_SYMTAB ===
symoff = 0x0000018c
nsyms = 0x00000002
stroff = 0x000001ac
strsize = 0x0000000c
=== string table ===
str[0] = 
str[1] = start
str[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
=== section[0], relocation_info[0] ===
r_address = 0x0000000a
r_type = 0x1
r_extern = 0x1
r_length = 0x2
r_pcrel = 0x1
r_symbolnum = 0x000000
    symbol_value=0x0000000000000010
    string_table_index=0x7
    string = hoge
(略)

リロケーション情報は一つ有り、

  • r_address = 0x0000000a → セクション先頭から オフセット 0x0000000a の位置を書き換えるべし
  • r_type = 0x01 → 符号付き32bitディスプレースメント
  • r_extern = 0x01 → r_symbolnumはシンボルテーブル内のエントリ番号
  • r_length = 0x2 → 書き換えるバイト数は 4バイト(∵ 0=byte, 1=word, 2=long, 3=quad)
  • r_pcrel = 0x01 → プログラムカウンタに対する相対アドレスだよ
  • r_symbolnum = 0x00000000 → シンボルテーブルの 0 番目(=nlist_64[0])、 つまり hoge を参照

と言う内容。0x0000000a は先のgobjdumpの結果を見ると hoge を参照する mov 命令のアドレス指定の部分。

先程も見たようにリンカがこの情報をもとにリロケーションを実行して、実行形式ファイル中では 0x00000002 に書き換えられている。

リロケーション情報は otool でも確認することができる。

$ otool -r hoge2.o
hoge2.o:
Relocation information (__TEXT,__text) 1 entries
address  pcrel length extern type    scattered symbolnum/value
0000000a 1     2      1      1       0         0

Mac OS Xx86_64はscatteredリロケーションをサポートしないとのことなので、scatteredについては気にしない。
他の値は自作コードで読み取った値と一致している。


Mach-Oファイル内での位置を図示するとこうなる。

__textセクションの最後、syscall命令(0x0F 0x05)に続いて、__dataセクション(0x05)が有り、謎の3byteが有り、それに続いてrelocation_infoが格納されていることがわかる。
謎の3byteはrelocation_infoを4byteアラインメントするためなのかなあという気もするけど、確証はない。

外部のデータを参照している場合

グローバルデータを定義しているコード(hoge3_2.s)

.file "hoge3_2.s"
.data
.globl hoge 
hoge:
   .byte 0x5

グローバルデータを参照しているコード(hoge3_1.s)

.file "hoge3_1.s"
.text
.globl start
start:
    movq $0x2000001, %rax
    movq hoge(%rip), %rdi
    syscall


コンパイル

$ as -o hoge3_1.o hoge3_1.s && as -o hoge3_2.o hoge3_2.s && ld -o hoge3 hoge3_1.o hoge3_2.o

グローバルデータを参照しているコード hoge3_1.o の情報

=== string table ===
str[0] = 
str[1] = start
str[2] = hoge
=== nlist_64[0] ===
n_un.n_strx = 0x00000001
n_type = 0x0f
n_sect = 0x01
n_desc = 0x0000
n_value = 0x0000000000000000
string = start
=== nlist_64[1] ===
n_un.n_strx = 0x00000007
n_type = 0x01
n_sect = 0x00
n_desc = 0x0000
n_value = 0x0000000000000000
string = hoge
=== section[0], relocation_info[0] ===
r_address = 0x0000000a
r_type = 0x1
r_extern = 0x1
r_length = 0x2
r_pcrel = 0x1
r_symbolnum = 0x000001
    symbol_value=0x0000000000000000
    string_table_index=0x7
    string = hoge

シンボルテーブルの1番目(hoge)を参照するリロケーション情報が含まれている。しかし、hoge2.oの時のhogeとはシンボルの内容が異なっている。

hoge2.o hoge3_1.o
n_un.n_strx 0x00000007 0x00000007
n_type 0x0e 0x01
n_sect 0x02 0x00
n_desc 0x0000 0x0000
n_value 0x0000000000000010 0x0000000000000000
string hoge hoge

まず、n_tyep が異なっている。n_typeはビットごとに意味がある。詳細は mach-o/nlist.h で定義されている。hoge2.o の 0x0eはこのファイル(hoge2.o)内のセクション番号n_sectでシンボルが定義されていることを示している。
hoge3_1.o の 0x01は、外部ファイルで定義されたシンボルであるか、このファイルで定義された、外部ファイルから参照可能なシンボルであることを示すが、n_sect は 0 となっているので、このシンボルは外部ファイルで定義されたシンボルへの参照ということ。

グローバルデータを定義しているコード(hoge3_2.o)の情報

=== string table ===
str[0] = 
str[1] = hoge
=== nlist_64[0] ===
n_un.n_strx = 0x00000001
n_type = 0x0f
n_sect = 0x02
n_desc = 0x0000
n_value = 0x0000000000000000
string = hoge

リロケーション情報は無し。n_type の 0x0f は nlist.h を見ながら解釈すると (N_SECT | N_EXT) で有あるから、このファイルのセクション番号 n_sect で定義された外部ファイルから参照可能なシンボルであることを示している。


実行形式ファイルの中身

$ gobjdump -D hoge3
(略)
0000000000001ff0 :
    1ff0:       48 c7 c0 01 00 00 02    mov    $0x2000001,%rax
    1ff7:       48 8b 3d 02 00 00 00    mov    0x2(%rip),%rdi        # 2000 
    1ffe:       0f 05                   syscall 

0000000000002000 :
    2000:       05                      .byte 0x5
(略)


リンカはこの二つのファイルのシンボルテーブルとリロケーション情報を元に、実行形式での仮想メモリアドレスに合わせてリロケーションを行い、hogeへの参照が 0x00000002 へと解決されている。

まとめ

  • ロードアドレスを変更したときに、オブジェクトコードのどこをどう書き換えれば良いかを定義しているものはリロケーション情報。
  • リロケーション情報はシンボル情報とも密接な関係がある。
  • 外部ファイルで定義されたシンボルを参照した場合は、自ファイルと外部ファイル共にシンボル情報が含まれ、片方は参照、片方は定義という意味となる。