ELFファイルを作る part6
part5からの続き
目標
ELFファイルから各セクションを抽出し、それらのセクションの情報からELFファイル全体を再構築する。
環境
- opensuse12.3
- ld-2.23.1
実装
元になるELFファイルや、各セクションの抽出についてはpart5を参照。やることはこの、part5で抽出してきたセクションの情報のみ(ELFヘッダーの変更無いところなどは流用)を元にELFファイル全体を再構築するということ。
今回作成したコードについてはそこそこファイル数があったのでこちらのgithubにまとめてUPしておいた。挙動は以降に記すとおり。test.asmをコンパイルしたtest.outがmain.pyに置いてあるという前提で
$ python main.py
で実行。
Restructor.py
- 21~24行目
se = SectionExtractor(self.byteList) for name, body, sh in se.extract(): secm.append(name, body, sh)
で、part5で取得したようなセクションのリストをtest.outのbyteListから抽出してseへ格納し、それらをelf.components.SectionManagerへそれぞれ渡している。
- 26行目
上でセクションを格納したSectionManagerのインスタンスをSegmentManagerのmappingへ渡す。このmappingで行っている事は、セクションとセグメントの対応part2で得た関係を元に、セクションがどこのセグメントへ当てはめられるべきなのかをマッピングしている。その辺の対応はelf.Relation.pyに記述してあるので、それを元に。ちなみにSegmentManagerの86,87行目はちょっとズルしてます。とりあえずはこのtest.outを再構築するってことで、ここを書いていた時点では算出方法がわかっていなかったので。
- 28行目
上でセグメントの数はわかるので、ELFヘッダーとプログラムヘッダーに使われるサイズを計算
- 29行目
SectionManagerのmakeBodyで本体部分のデータを作成。alignを調整したりして。ここは本来、リンカスクリプトを元にしてセクションの順番やら開始アドレス等々が決定されるが、これまたリンカスクリプトを使うようにしていると今回は不要なものも多いので、対応をRelation.pyに書いて逃げておく。また、このmakeBodyでも49行目辺りでズルをしているが、これについては後述。
- 30行目
プログラムヘッダーの作成
- 31行目
アドレス位置の調整
- 33行目
shstrtblの作成
- 35行目
ここまでのELFヘッダー、プログラムヘッダー、body部分、shstrtblを足したサイズを求め、これ以降に続くセクションヘッダーへのオフセットを取得しておく
- 37行目~
ELFヘッダーの作成や、ここまで作成していたもののbyteListをくっつける。55行目のはセクションヘッダー先頭のNULL要素を作成して追加している。
- 59行目~
ファイルへの出力部分
ズルの釈明
何があったのか?
さて、先に述べたズルしていた部分。これについて。
まずは今回の大元、test.asmからgccを使って作成したtest.outの中身をreadelfでのぞいてみると
$ readelf -l test.out Elf file type is EXEC (Executable file) Entry point 0x400340 There are 7 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x0000000000000188 0x0000000000000188 R E 8 INTERP 0x00000000000001c8 0x00000000004001c8 0x00000000004001c8 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000358 0x0000000000000358 R E 200000 LOAD 0x0000000000000eb0 0x0000000000600eb0 0x0000000000600eb0 0x0000000000000178 0x0000000000000178 RW 200000 DYNAMIC 0x0000000000000eb0 0x0000000000600eb0 0x0000000000600eb0 0x0000000000000150 0x0000000000000150 RW 8 NOTE 0x00000000000001e4 0x00000000004001e4 0x00000000004001e4 0x0000000000000024 0x0000000000000024 R 4 GNU_RELRO 0x0000000000000eb0 0x0000000000600eb0 0x0000000000600eb0 0x0000000000000150 0x0000000000000150 R 1
というように、LOADセグメントについて、flagがREの方はオフセットが0でサイズ0x358となっている。順序的に続く事になるflagがRWのLOADセグメントはオフセットが0xeb0から始まっている。この箇所について、セクション単位ではどうなっているかを見ると
readelf -S test.out There are 16 section headers, starting at offset 0x10b0: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 00000000004001c8 000001c8 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.gnu.build-i NOTE 00000000004001e4 000001e4 0000000000000024 0000000000000000 A 0 0 4 [ 3] .hash HASH 0000000000400208 00000208 0000000000000018 0000000000000004 A 5 0 8 [ 4] .gnu.hash GNU_HASH 0000000000400220 00000220 000000000000001c 0000000000000000 A 5 0 8 [ 5] .dynsym DYNSYM 0000000000400240 00000240 0000000000000048 0000000000000018 A 6 1 8 [ 6] .dynstr STRTAB 0000000000400288 00000288 0000000000000024 0000000000000000 A 0 0 1 [ 7] .gnu.version VERSYM 00000000004002ac 000002ac 0000000000000006 0000000000000002 A 5 0 2 [ 8] .gnu.version_r VERNEED 00000000004002b8 000002b8 0000000000000020 0000000000000000 A 6 1 8 [ 9] .rela.plt RELA 00000000004002d8 000002d8 0000000000000030 0000000000000018 A 5 10 8 [10] .plt PROGBITS 0000000000400310 00000310 0000000000000030 0000000000000010 AX 0 0 16 [11] .text PROGBITS 0000000000400340 00000340 0000000000000014 0000000000000000 AX 0 0 16 [12] .eh_frame PROGBITS 0000000000400358 00000358 0000000000000000 0000000000000000 A 0 0 8 [13] .dynamic DYNAMIC 0000000000600eb0 00000eb0 0000000000000150 0000000000000010 WA 6 0 8 [14] .got.plt PROGBITS 0000000000601000 00001000 0000000000000028 0000000000000008 WA 0 0 8 [15] .shstrtab STRTAB 0000000000000000 00001028 0000000000000088 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), l (large) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
となっている。先のオフセットとなるのは.dynamicの箇所だが、アドレスが0x600000台になるのはわかるけど、このオフセットがよくわからない。
原因は?
リンク時の指示と言えばリンカスクリプトということで、リンカスクリプトをのぞいてみる。
問題の箇所は.eh_frameセクションと.dynamicセクションの間ということで、その間を調べてみると
/* Adjust the address for the data segment. We want to adjust up to the same address within the page on the next page up. */ . = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .) & (CONSTANT (MAXPAGESIZE) - 1)); . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
というのがある。
また、リンク時にアドレスの指定がどのような挙動になっているかを調べるため、gccを以下のようなオプション付きで実行する
$ gcc -Wl,-M -Wl,"-T/usr/lib64/ldscripts/elf_x86_64.x" -s -nostartfiles -o test.out test.o
この結果を見ると
0x0000000000400358 . = (ALIGN (0x200000) - ((0x200000 - .) & 0x1fffff)) 0x0000000000600eb0 . = DATA_SEGMENT_ALIGN (0x200000, 0x1000)
となっているので、どうもこのDATA_SEGMENT_ALIGNってのが一枚噛んでいるのでは。
DATA_SEGMENT_ALIGN
これについてはbinutilsのコードを調べてみると、ld/ld.infoにて
`DATA_SEGMENT_ALIGN(MAXPAGESIZE, COMMONPAGESIZE)' This is equivalent to either (ALIGN(MAXPAGESIZE) + (. & (MAXPAGESIZE - 1))) or (ALIGN(MAXPAGESIZE) + (. & (MAXPAGESIZE - COMMONPAGESIZE))) depending on whether the latter uses fewer COMMONPAGESIZE sized pages for the data segment (area between the result of this expression and `DATA_SEGMENT_END') than the former or not. If the latter form is used, it means COMMONPAGESIZE bytes of runtime memory will be saved at the expense of up to COMMONPAGESIZE wasted bytes in the on-disk file.
との記述が。DATA_SEGMENT_ALIGNはこの2式のどっちかと等価ということの様で。
このDATA_SEGMENT_ALIGNの実装はld/ldexp.cにいくつかあるcase文で、DATA_SEGMENT_ALIGNとなっている所。
それで?
ここまで追ってみたが、計算してみてもどうにも先の様にオフセットに余計?なスペースが出来る計算にはなってくれない。なぜか。
何の気なしに、ソースコードを見るために手元にあったbinutilsをインストールしてみて、ldはそっちを使うようにしてtest.outを作ってみた。そちらをreadelfしてみると・・・
$ readelf -l test.out Elf file type is EXEC (Executable file) Entry point 0x4002e0 There are 6 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x0000000000000150 0x0000000000000150 R E 8 INTERP 0x0000000000000190 0x0000000000400190 0x0000000000400190 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000002f8 0x00000000000002f8 R E 200000 LOAD 0x00000000000002f8 0x00000000006002f8 0x00000000006002f8 0x0000000000000168 0x0000000000000168 RW 200000 DYNAMIC 0x00000000000002f8 0x00000000006002f8 0x00000000006002f8 0x0000000000000140 0x0000000000000140 RW 8 NOTE 0x00000000000001ac 0x00000000004001ac 0x00000000004001ac 0x0000000000000024 0x0000000000000024 R 4
readelf -S test.out There are 15 section headers, starting at offset 0x4e8: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000400190 00000190 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.gnu.build-i NOTE 00000000004001ac 000001ac 0000000000000024 0000000000000000 A 0 0 4 [ 3] .hash HASH 00000000004001d0 000001d0 0000000000000018 0000000000000004 A 4 0 8 [ 4] .dynsym DYNSYM 00000000004001e8 000001e8 0000000000000048 0000000000000018 A 5 1 8 [ 5] .dynstr STRTAB 0000000000400230 00000230 0000000000000024 0000000000000000 A 0 0 1 [ 6] .gnu.version VERSYM 0000000000400254 00000254 0000000000000006 0000000000000002 A 4 0 2 [ 7] .gnu.version_r VERNEED 0000000000400260 00000260 0000000000000020 0000000000000000 A 5 1 8 [ 8] .rela.plt RELA 0000000000400280 00000280 0000000000000030 0000000000000018 A 4 9 8 [ 9] .plt PROGBITS 00000000004002b0 000002b0 0000000000000030 0000000000000010 AX 0 0 16 [10] .text PROGBITS 00000000004002e0 000002e0 0000000000000014 0000000000000000 AX 0 0 16 [11] .eh_frame PROGBITS 00000000004002f8 000002f8 0000000000000000 0000000000000000 A 0 0 8 [12] .dynamic DYNAMIC 00000000006002f8 000002f8 0000000000000140 0000000000000010 WA 5 0 8 [13] .got.plt PROGBITS 0000000000600438 00000438 0000000000000028 0000000000000008 WA 0 0 8 [14] .shstrtab STRTAB 0000000000000000 00000460 0000000000000084 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), l (large) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
さっきのような、余計?なオフセットが無い・・・。
ちなみにどちらもbinutilsのバージョンは同じ。
- 元からあった方
GNU ld (GNU Binutils; openSUSE 12.3) 2.23.1
- コードからインストールした方
GNU ld (GNU Binutils) 2.23.1
(追記)
出来上がった実行ファイルのサイズは
- コードからインストールしたldを使った場合:2216byte
- パッケージで入ってたldを使った場合:5296byte
と、結構な差があった
というわけで
結論。あのオフセットは今のところ謎のまま。
この謎がある状態のバイナリに再構築しようとしていたので、あのオフセットの算出方法が分からずにズルをしてたと。
実行
一応動くようにはなったものの、こういったズルの値は状況に因って異なってしまうので、おそらく自分の手元の環境でしか動きません。(あのズルの値を調整すれば動くはず)
$ ./elf.out T
つぎはその辺りをちゃんとどんな環境でも動くように、各セクションの中身を調整する、もしくは作り上げるようにするのが良さそう。