リンカによるシンボル解決(4)文字列リテラル
文字列リテラルのシンボル解決について見ていきます。
1 #include <stdio.h> 2 3 const char *dummy_string; 4 const char *string; 5 6 int main() 7 { 8 dummy_string = "123456"; 9 string = "ABCDEF"; 10 11 printf("string = %p\n", string); 12 13 return 0; 14 }
ソースコード中に出てくる文字列リテラルは、"123456"、"ABCDEF"、"string = %p\n"の3つです。
再配置テーブルを見てみると、
Relocation section '.rel.text' at offset 0x454 contains 7 entries: Offset Info Type Sym.Value Sym. Name 0000000b 00000901 R_386_32 00000004 dummy_string 0000000f 00000501 R_386_32 00000000 .rodata 00000015 00000a01 R_386_32 00000004 string 00000019 00000501 R_386_32 00000000 .rodata 0000001f 00000a01 R_386_32 00000004 string 00000024 00000501 R_386_32 00000000 .rodata 00000030 00000c02 R_386_PC32 00000000 printf
.text上の、文字列リテラルの参照箇所を示すエントリは、Offsetの0x0f、0x19、0x24の3つです。オブジェクトファイル中の実行コードを見てみると、
00000000: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 e4 f0 and $0xfffffff0,%esp 6: 83 ec 10 sub $0x10,%esp 9: c7 05 00 00 00 00 00 movl $0x0,0x0 10: 00 00 00 13: c7 05 00 00 00 00 07 movl $0x7,0x0 1a: 00 00 00 1d: 8b 15 00 00 00 00 mov 0x0,%edx 23: b8 0e 00 00 00 mov $0xe,%eax 28: 89 54 24 04 mov %edx,0x4(%esp) 2c: 89 04 24 mov %eax,(%esp) 2f: e8 fc ff ff ff call 30 34: b8 00 00 00 00 mov $0x0,%eax 39: c9 leave 3a: c3 ret
3箇所に埋め込まれている「仮の値」は、それぞれ「0x00」@0x0f「0x07」@0x19、「0x0e」@0x24となります。また、関連するシンボルテーブル・エントリの番号は、すべて5番(再配置テーブルのInfoの上位24ビット)ですが、
Symbol table '.symtab' contains 13 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS sample.c 2: 00000000 0 SECTION LOCAL DEFAULT 1 3: 00000000 0 SECTION LOCAL DEFAULT 3 4: 00000000 0 SECTION LOCAL DEFAULT 4 ★5: 00000000 0 SECTION LOCAL DEFAULT 5 6: 00000000 0 SECTION LOCAL DEFAULT 7 7: 00000000 0 SECTION LOCAL DEFAULT 8 8: 00000000 0 SECTION LOCAL DEFAULT 6 9: 00000004 4 OBJECT GLOBAL DEFAULT COM dummy_string 10: 00000004 4 OBJECT GLOBAL DEFAULT COM string 11: 00000000 59 FUNC GLOBAL DEFAULT 1 main 12: 00000000 0 NOTYPE GLOBAL DEFAULT UND printf
シンボルテーブルの5番エントリ(★)は、5番セクション(Ndxの値)に関連します。5番セクションは、以下の通り、.rodataセクションです。
Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 00003b 00 AX 0 0 4 [ 2] .rel.text REL 00000000 000454 000038 08 11 1 4 [ 3] .data PROGBITS 00000000 000070 000000 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 000070 000000 00 WA 0 0 4 [ 5] .rodata PROGBITS 00000000 000070 00001b 00 A 0 0 1 [ 6] .comment PROGBITS 00000000 00008b 00002b 01 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 00000000 0000b6 000000 00 0 0 1 [ 8] .eh_frame PROGBITS 00000000 0000b8 000038 00 A 0 0 4 [ 9] .rel.eh_frame REL 00000000 00048c 000008 08 11 8 4 [10] .shstrtab STRTAB 00000000 0000f0 00005f 00 0 0 1 [11] .symtab SYMTAB 00000000 000358 0000d0 10 12 9 4 [12] .strtab STRTAB 00000000 000428 00002a 00 0 0 1
つまり、前述の3つの「仮の値」は、.rodataセクションの先頭からのオフセットを示すものと推測されます。
そこで、.rodataセクションをダンプしてみます。
$ readelf -x 5 sample.o Hex dump of section '.rodata': 0x00000000 31323334 35360041 42434445 46007374 123456.ABCDEF.st 0x00000010 72696e67 203d2025 700a00 ring = %p..
先の「仮の値」は、「0x00」@0x0f「0x07」@0x19、「0x0e」@0x24でしたが、上記のダンプ結果を見てみると、.rodataのオフセットと文字列リテラルの対応関係は
- 0x00 → "123456"
- 0x07 → "ABCDEF"
- 0x0e → "string = %p"
となります。
実行形式での.rodataの先頭アドレスは、0x080484e8なので、
[15] .rodata PROGBITS 080484e8 0004e8 000023 00 A 0 0 4
文字列リテラルのアドレスは以下のようになると思うのですが、
- "123456" → 080484e8 + 0x00 = 080484e8
- "ABCDEF" → 080484e8 + 0x07 = 080484ef
- "string = %p" → 080484e8 + 0x0e = 080484f6
実行形式の.rodataを見てみると、
$ readelf -x 15 sample Hex dump of section '.rodata': 0x080484e8 03000000 01000200 31323334 35360041 ........123456.A 0x080484f8 42434445 46007374 72696e67 203d2025 BCDEF.string = % 0x08048508 700a00 p..
と、おそらくリンク工程でのセクション集約時に、「03000000 01000200」という8バイトのデータが追加されています。なので、文字列リテラルのアドレスは、正しくは
- "123456" → 080484e8 + 0x08 = 080484f0
- "ABCDEF" → 080484e8 + 0x0f = 080484f7
- "string = %p" → 080484e8 + 0x16 = 08048fe
となると推測されます。
実行コードを確認すると、上記の計算結果は、以下★1〜3のアドレスと一致しています。
080483e4: 80483e4: 55 push ebp 80483e5: 89 e5 mov ebp,esp 80483e7: 83 e4 f0 and esp,0xfffffff0 80483ea: 83 ec 10 sub esp,0x10 80483ed: c7 05 20 a0 04 08 f0 mov DWORD PTR ds:0x804a020,0x80484f0(★1) 80483f4: 84 04 08 80483f7: c7 05 1c a0 04 08 f7 mov DWORD PTR ds:0x804a01c,0x80484f7(★2) 80483fe: 84 04 08 8048401: 8b 15 1c a0 04 08 mov edx,DWORD PTR ds:0x804a01c 8048407: b8 fe 84 04 08 mov eax,0x80484fe(★3) 804840c: 89 54 24 04 mov DWORD PTR [esp+0x4],edx 8048410: 89 04 24 mov DWORD PTR [esp],eax 8048413: e8 e8 fe ff ff call 8048300 8048418: b8 00 00 00 00 mov eax,0x0 804841d: c9 leave 804841e: c3 ret 804841f: 90 nop
ついでなので、実行コードを簡単に追ってみます。
★2で"ABCDEF"へのポインタをstringに代入し、printf()の引数として(edxレジスタ経由で)スタックに積み、★3で"string = %p\n"へのポインタをeaxレジスタ経由でスタックに積んでいるのが分かります。引数の準備をした後で、call命令でprintf()にジャンプします。