概要 #
本記事では、プログラムで利用している文字列リテラルがプログラム内のどこに配置しているのかを確かめるための手法について解説する。
本記事の実験環境 #
- OS: Ubuntu 22.04
- コンパイラ: clang 17.0.6
- アーキテクチャ: x86_64
- GNU readelf: GNU Binutils 2.38
文字列 #
定番のHello, World!プログラムを使って、文字列がどこにあるかみてみよう。
#include <stdio.h>
int main(void) {
puts("Hello, World!\n");
}
まずは-cオプションをつかってリンク前のオブジェクトファイルでみてみよう。
$ clang -c helloworld.c
バイナリのセクション情報をみるために、ここではGNU readelfプログラムを利用している。
$ readelf -WS helloworld.o
There are 11 section headers, starting at offset 0x260:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .strtab STRTAB 0000000000000000 0001e1 00007f 00 0 0 1
[ 2] .text PROGBITS 0000000000000000 000040 000014 00 AX 0 0 16
[ 3] .rela.text RELA 0000000000000000 000198 000030 18 I 10 2 8
[ 4] .rodata.str1.1 PROGBITS 0000000000000000 000054 00000f 01 AMS 0 0 1
[ 5] .comment PROGBITS 0000000000000000 000063 000066 01 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 0000c9 000000 00 0 0 1
[ 7] .eh_frame X86_64_UNWIND 0000000000000000 0000d0 000038 00 A 0 0 8
[ 8] .rela.eh_frame RELA 0000000000000000 0001c8 000018 18 I 10 7 8
[ 9] .llvm_addrsig LOOS+0xfff4c03 0000000000000000 0001e0 000001 00 E 10 0 1
[10] .symtab SYMTAB 0000000000000000 000108 000090 18 1 4 8
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
ここで各セクションのフラグに注目してみると、.rodata.str1.1と.commentにはS(strings)というフラグがついていることがわかる。これはヌル終端な文字列データを含んでいることを示すフラグであり、つまり文字列データが内包されている箇所である。ここでついでに.rodata.str1.1にM(merge)フラグがついていることも覚えておいてほしい。
readelfの-xオプションを使うと、そのセクションの中身のデータをみることができる。
$ readelf -x .rodata.str1.1 helloworld.o
Hex dump of section '.rodata.str1.1':
0x00000000 48656c6c 6f2c2057 6f726c64 210a00 Hello, World!..
ここで文字列Hello, World!は.rodata.str1.1にあることがわかった。 .rodataは不変なデータを置くグローバルなデータ領域で、文字列はこのセクションに置かれる。
ちなみに.commentセクションはclangの場合はコンパイルに利用したコンパイラの情報が含まれる。
$ readelf -x .comment helloworld.o
Hex dump of section '.comment':
0x00000000 00636c61 6e672076 65727369 6f6e2031 .clang version 1
0x00000010 372e302e 36202868 74747073 3a2f2f67 7.0.6 (https://g
0x00000020 69746875 622e636f 6d2f6c6c 766d2f6c ithub.com/llvm/l
0x00000030 6c766d2d 70726f6a 65637420 36303039 lvm-project 6009
0x00000040 37303862 34333637 31373163 63646266 708b4367171ccdbf
0x00000050 34623539 30356362 36613830 33373533 4b5905cb6a803753
0x00000060 66653138 2900 fe18).
さて、ここまではリンク前のオブジェクトファイルでバイナリの中身をみてきたが、実行ファイルだとどうだろうか。
$ clang -o helloworld helloworld.c
同様にreadelf -WSでセクション情報をみてみる。
$ readelf -WS helloworld
(長いので割愛)
[14] .text PROGBITS 0000000000001050 001050 000104 00 AX 0 0 16
[15] .fini PROGBITS 0000000000001154 001154 00000d 00 AX 0 0 4
[16] .rodata PROGBITS 0000000000002000 002000 000013 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 0000000000002014 002014 00002c 00 A 0 0 4
[18] .eh_frame PROGBITS 0000000000002040 002040 000094 00 A 0 0 8
[19] .init_array INIT_ARRAY 0000000000003de8 002de8 000008 08 WA 0 0 8
[20] .fini_array FINI_ARRAY 0000000000003df0 002df0 000008 08 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000003df8 002df8 0001e0 10 WA 6 0 8
[22] .got PROGBITS 0000000000003fd8 002fd8 000028 08 WA 0 0 8
[23] .got.plt PROGBITS 0000000000004000 003000 000020 08 WA 0 0 8
[24] .data PROGBITS 0000000000004020 003020 000010 00 WA 0 0 8
[25] .bss NOBITS 0000000000004030 003030 000008 00 WA 0 0 1
上記で少しだけ触れたのだが、.rodata.str1.1はM(merge)フラグがついていた。これはそのセクションが他のものと結合されるかもしれないという意味を持つ。その結果、実行ファイル内の.rodataセクションにはすべての.rodata.*な情報がまとめられてしまった。
ここで特定の文字列がどこに配置されているかを知りたい場合は、readelf -pを利用するとよい。
$ readelf -p .rodata helloworld
String dump of section '.rodata':
[ 4] Hello, World!\n
-pオプションは指定したセクションの文字列っぽいものを探索し(stringsコマンドのような動きをする)、それをみつけたときにオフセット情報とともに出力してくれる。
今回のプログラムの例では文字列Hello, World!は.rodataセクションの先頭からオフセット4の位置に配置されていることがわかる。
$ readelf -x .rodata helloworld
Hex dump of section '.rodata':
0x00002000 01000200 48656c6c 6f2c2057 6f726c64 ....Hello, World
0x00002010 210a00 !..