わらばんし仄聞記

南の国で引きこもってるWeb屋さん

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

つぎはその辺りをちゃんとどんな環境でも動くように、各セクションの中身を調整する、もしくは作り上げるようにするのが良さそう。