リンカによるシンボル解決(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のオフセットと文字列リテラルの対応関係は

  1. 0x00 → "123456"
  2. 0x07 → "ABCDEF"
  3. 0x0e → "string = %p"

となります。

実行形式での.rodataの先頭アドレスは、0x080484e8なので、

  [15] .rodata           PROGBITS        080484e8 0004e8 000023 00   A  0   0  4

文字列リテラルのアドレスは以下のようになると思うのですが、

  1. "123456" → 080484e8 + 0x00 = 080484e8
  2. "ABCDEF" → 080484e8 + 0x07 = 080484ef
  3. "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バイトのデータが追加されています。なので、文字列リテラルのアドレスは、正しくは

  1. "123456" → 080484e8 + 0x08 = 080484f0
  2. "ABCDEF" → 080484e8 + 0x0f = 080484f7
  3. "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()にジャンプします。