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)
- .gnu.version
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周りは.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
- test.out
となっている。赤い箇所が.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が出てきてくれたので、まだ他の調整は必要だが。