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