わらばんし仄聞記

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

sh_addralign

ELFファイルを作る part5で取得した、セクション毎に分解されたデータを元にELFファイルを再構築していた際、出来上がったファイルをreadelf -aしたら

readelf: Error: Unable to read in 0x10 bytes of version need aux (2)
readelf: Error: Unable to read in 0x10 bytes of version need

なんてのが大量に出まくった。

ということで、これの原因と共に近辺を調べてみる。

目標

readelf: Error: Unable to read in 0x10 bytes of version need aux (2)

の原因調査と解消

調査

前提

分解する元となったファイル(test.out)のreadlelf全体像はこちら
問題のエラーはこのファイルのセクションを抽出、分解し、自分で再構築してみたファイル(elf.out)に対して発生したもの。

どこがおかしいのか?

先のエラーが出た際、readelfのオプションは -a で行っていた。部分毎に出力してみたら問題の箇所がわかるかな?と。
いくつかのオプションで試してみたところ正常に出力されるところもあるようで、問題のエラーが出てきたのは -V オプションで実行したとき。これらが表示するセクションはバージョン情報。正常に動作する分解元のファイルで試してみると、こんな感じ。

$ readelf -V test.out

Version symbols section '.gnu.version' contains 3 entries:
 Addr: 00000000004002ac  Offset: 0x0002ac  Link: 5 (.dynsym)
  000:   0 (*local*)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)

Version needs section '.gnu.version_r' contains 1 entries:
 Addr: 0x00000000004002b8  Offset: 0x0002b8  Link: 6 (.dynstr)
  000000: Version: 1  File: libc.so.6  Cnt: 1
  0x0010:   Name: GLIBC_2.2.5  Flags: none  Version: 2

これらのセクションについてのセクションヘッダ情報は

$ readelf -S test.out
...略...
  [ 7] .gnu.version      VERSYM           00000000004002ac  000002ac
       0000000000000006  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          00000000004002b2  000002b2
       0000000000000020  0000000000000000   A       6     1     8
...略...

なにこれ?

.gnu.version

まずはググってみる。たどり着いたページがこちら。11.7.2より、.gnu.versionはSymbol version tableというものらしい。そして、このセクションは.dynsymセクションと同じ数の要素を持つとのこと。.gnu.versionセクションの中身はElf64_Half型の配列で、定義済みのバージョンか、.dynsymで定義されているシンボル番号と一致するものが入っている。
平たく言うと、.dynsymで定義されるシンボルに対応するバージョンの一覧は.gnu.versionに格納されている。

  • .dynsym
Symbol table '.dynsym' contains 3 entries:                                                                                                           
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND                                         
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND putchar@GLIBC_2.2.5 (2) 
Version symbols section '.gnu.version' contains 3 entries:
 Addr: 00000000004002ac  Offset: 0x0002ac  Link: 5 (.dynsym)
  000:   0 (*local*)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)

.dynsym各要素のNameパラメーター末尾にある(2)とか。この括弧内の数値が.gnu.versionの対応するインデックスが持つ値である。参考URLにもあるとおり、0と1は特殊な値。

参考として、他の実行ファイルでも確認してみると

$ readelf -a `which ld`

Symbol table '.dynsym' contains 312 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
...略...
    40: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND getopt_long_only@GLIBC_2.2.5 (2)
    41: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND fseek@GLIBC_2.2.5 (2)
    42: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND bfd_elf_set_dyn_lib_class
    43: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND mkstemps@GLIBC_2.11 (4)
...略...

Version symbols section '.gnu.version' contains 312 entries:
 Addr: 0000000000403eb0  Offset: 0x003eb0  Link: 7 (.dynsym)
...略...
  028:   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   0 (*local*)       4 (GLIBC_2.11)

という具合になる。.gnu.versionの方の見出しの値は16進なので、0x28 = 40。つまり、.dynsumのnum40から順に、2,2,0,4が対応する。ということで、各バージョンの値が対応する箇所の値になっていることが読み取れる。

.gnu.version_r

このセクションが持つ値の構造体はElf64_Verneed構造体の配列となる。また、オプションとして、Elf64_Vernaux構造体が続く。

typedef struct {
	Elfxx_Half    vn_version;
	Elfxx_Half    vn_cnt;
	Elfxx_Word    vn_file;
	Elfxx_Word    vn_aux;
	Elfxx_Word    vn_next;
} Elf64_Verneed;
typedef struct {
	Elfxx_Word    vna_hash;
	Elfxx_Half    vna_flags;
	Elfxx_Half    vna_other;
	Elfxx_Word    vna_name;
	Elfxx_Word    vna_next;
} Elf64_Vernaux;

ここで先の.gnu.versionが指すversionの値についての情報が示される。readelfより、

Version needs section '.gnu.version_r' contains 1 entries:
 Addr: 0x00000000004002b8  Offset: 0x0002b8  Link: 6 (.dynstr)
  000000: Version: 1  File: libc.so.6  Cnt: 1                                                       
  0x0010:   Name: GLIBC_2.2.5  Flags: none  Version: 2

となり、最終行の"Version: 2"というのが.gnu.versionで示されていた値と対応する。つまり、.gnu.versionで値が2となっているなら、.gnu.version_rのVersionが2となっているものと対応する。
先ほどと同様に、ldコマンドについてみてみると

$ readelf -V `which ld`
...略...
Version needs section '.gnu.version_r' contains 2 entries:
 Addr: 0x0000000000404120  Offset: 0x004120  Link: 8 (.dynstr)
  000000: Version: 1  File: libdl.so.2  Cnt: 1
  0x0010:   Name: GLIBC_2.2.5  Flags: none  Version: 5
  0x0020: Version: 1  File: libc.so.6  Cnt: 4
  0x0030:   Name: GLIBC_2.4  Flags: none  Version: 6
  0x0040:   Name: GLIBC_2.11  Flags: none  Version: 4
  0x0050:   Name: GLIBC_2.3.4  Flags: none  Version: 3
  0x0060:   Name: GLIBC_2.2.5  Flags: none  Version: 2

ということで、.gnu.version_rのVersionと先に提示したldコマンドの.gnu.versionの値は対応している事が読み取れる。

セクションの関連

このように、セクション間で関連を持つ場合はセクションヘッダーのsh_link要素にその関連が示される。

  [ 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

.gnu.versionは.dynsymに。.gnu.version_rは.dynstrに関連してることが読み取れる。

問題のエラーの原因は?

さて、.gnu.version周りは.dymsymが保持するダイナミックリンクのシンボルについて、バージョン情報を保持していることはわかった。では、件のエラーメッセージは何なのか?
今一度見直してみると

readelf: Error: Unable to read in 0x10 bytes of version need aux (2)

この文章から、先に述べているElf64_Verneedのvn_aux要素が示す0x10が読めないとかなんとか。
このvn_auxについてはよく世話になってるこちらのoracleの資料より、「Elf64_Verneedエントリの先頭から、関連付けられているファイル依存性から要求されるバージョン定義の Elf64_Vernaux 配列までのバイトオフセット」だそうだ。回りくどいけど、vn_auxが含まれる構造体の先頭から、関連するElf64_Vernaux構造体までのオフセットということかな。

諸悪の根源

それは一体何かというと、自分の勘違いな訳で。

さて、最初に述べました再構築をしている際、セクションのaddress alignについて自分はこう捉えていた。
「offsetの示す場所からセクションが保持する情報(=本体)を配置し、その終了地点のアドレスがaddress alignの示す値で割り切れる値で無いなら0x00でパディングして割り切れる様に調整する」

正常に動作する方と異常な方の違いを見る

上で述べたような考えを元に再構築していた所、セクション情報は以下のようになっていた

$ readelf -S elf.out
...略...
  [ 7] .gnu.version      VERSYM           00000000004002ac  000002ac
       0000000000000006  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          00000000004002b2  000002b2
       0000000000000020  0000000000000000   A       6     1     8

対して、正常に動作する方は以下のように。

$ readelf -S test.out
...略...
  [ 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
...略...

これらの該当箇所を実際にバイナリエディタで見ると

  • elf.out

f:id:warabanshi21:20130411032938j:plain

  • test.out

f:id:warabanshi21:20130411032942j:plain
となっている。赤い箇所が.gnu.versionに相当し、青い箇所が.gnu.version_rに相当する。赤い箇所の開始位置はどちらも同様で0x2ac開始でサイズ6となっている。ここで、elf.outの方、つまり自分で再構築した方はalignを先に述べたように計算するものと考えていたために、直後に.gnu.version_rが続く形になっている。その為、あとでtest.outと比較した際、test.out側にあるこの.gnu.versionと.gnu.version_rの間にある空白の意味が分からなかった。

改めてaddress alignについての説明を読んでみる

「セクションがメモリにロードされる際の、バイトアラインメントのサイズ。sh_addrはsh_addralignの倍数値になっている必要がある。」

つまり、alignはセクションの末尾を整えるものではなくて、セクションの開始位置を決めるものだったわけで・・・。
それなら合点がいく。先のバイナリ値にある空白は.gnu.versionセクションが設けたalignではなく、.gnu.version_rが持つaddress align = 8に合わせて8で割りきれるアドレスから開始するべく設けた空白だった。そしてめでたく.gnu.version_rは8で割りきれるアドレス、0x2b8から開始していると。

結果

ということで、上のalignについて修正したところ、問題のエラーは出なくなった。.gnu.version_rの持つsh_addralignには一致しないバイナリの配置となっていたため、vn_auxが示すオフセットについて齟齬が出ていたようだ。

実行したらsegmentation faultが出てきてくれたので、まだ他の調整は必要だが。