ELFバイナリに含まれるnullセクション/ヘッダの真実…?
概要
gccの吐くELFバイナリを見てみると、セクションヘッダテーブルの先頭に NULLヘッダ を見つけます。
これってなんだろう? ってずっと疑問だったのですが、今日理由がわかったのでそれについて述べたいと思います。
厳密には、 nullセクション も含まれています。
サイズが0のセクション と、それに対応する 全てのメンバが0のヘッダ が存在します。
また本記事で取り上げるnullセクション/ヘッダの意味は私がなんとなくそうじゃね?と思ったもので、
もっと歴史的な経緯、重要な意味が含まれているかも知れません。
その場合は教えて下さい。
本題
まずは実際に見てみましょう。
適当にC言語のプログラムを用意し、オブジェクトファイルに変換してみます。
#include <stdio.h> int main() { printf("Hello,World!\n"); }
生成された sample.o
に対し、readelfコマンドを実行します。
今回はセクションヘッダテーブルのみダンプしたいので、 -S
オプションを付けます。
~ 04:12:50 drumato $ readelf -S c.o There are 13 section headers, starting at offset 0x2b8: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 0000000000000017 0000000000000000 AX 0 0 1 [ 2] .rela.text RELA 0000000000000000 00000208 0000000000000030 0000000000000018 I 10 1 8 [ 3] .data PROGBITS 0000000000000000 00000057 0000000000000000 0000000000000000 WA 0 0 1 [ 4] .bss NOBITS 0000000000000000 00000057 0000000000000000 0000000000000000 WA 0 0 1 [ 5] .rodata PROGBITS 0000000000000000 00000057 000000000000000d 0000000000000000 A 0 0 1 [ 6] .comment PROGBITS 0000000000000000 00000064 0000000000000024 0000000000000001 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 0000000000000000 00000088 0000000000000000 0000000000000000 0 0 1 [ 8] .eh_frame PROGBITS 0000000000000000 00000088 0000000000000038 0000000000000000 A 0 0 8 [ 9] .rela.eh_frame RELA 0000000000000000 00000238 0000000000000018 0000000000000018 I 10 8 8 [10] .symtab SYMTAB 0000000000000000 000000c0 0000000000000120 0000000000000018 11 9 8 [11] .strtab STRTAB 0000000000000000 000001e0 0000000000000025 0000000000000000 0 0 1 [12] .shstrtab STRTAB 0000000000000000 00000250 0000000000000061 0000000000000000 0 0 1
結構多いですが、今回重要なのはセクション番号0の SHT_NULL
が設定されたヘッダです。
Elf64_Shdr
の全ての値が0になっているようですが、このヘッダは一体なんなのでしょうか。
これについてかなり前調べてみたのですが、あまり有益な情報は得られませんでした。
しかしこれ、結構重要な意味がありました(私にとっては)。
かなり回りくどいですが、順を追って解説します。
自作アセンブラについて
私は今x86-64アセンブラをフルスクラッチで書いています(Rust製です)。
うわあああああああああああああ出来たああああああああ
— Drumato🍑く54D (@drumato) September 10, 2019
人生で一番嬉しい(過言)
自作アセンブラで作ったオブジェクトファイルがGNU ldでリンクできて、正しく実行できた!!!!!!!!!!!
マジで嬉しい、ELFの理解が進んだ
うわあああああほんとに嬉しいなあ pic.twitter.com/PvtQ3DJrBP
上手く行ったときの様子です。
本記事ではこのツイートより少し前、正常にリンク出来ないバグを踏んでいたときに知ったことを取り上げています。
以下のようなアセンブリ言語がパース、機械語に変換できたのでオブジェクトファイルに機械語を組み込むことにしました。
main: mov rax, 2 ret
ここで、GNU ldがリンク可能なオブジェクトファイルに必要であるセクションは、
- .text
- .strtab
- .symtab
- .shstrtab
です(リンカスクリプトを書いたりすればもっと少なくて済むかも)。
なのでこれらセクションを用意し、いざgccコマンドを実行してみました。
うおー、リンク出来た!!!!!
— Drumato🍑く54D (@drumato) September 10, 2019
(セグフォ発生するのはスタートアップルーチンを埋め込めてないため)
めちゃくちゃ嬉しい
自作アセンブラでオブジェクトファイル生成という夢は叶った
(むしろ序章) pic.twitter.com/rpeUJ4crvy
それがこちらになります。
リンク出来たので嬉しい!でもセグフォ発生する。
この理由を当時の私は "スタートアップルーチンをリンクしていないから" だと思っていました。
でもよく見ると、バイナリに _start
シンボルはある。
gdbで動的解析してみても スタートアップルーチンが実行できている…うーん、うーん…
と、結構困っていたのです。
いろいろ調べてみると、どうやら main
シンボルのシンボル解決が上手く行っていない 事がわかりました。
生成されたオブジェクトファイルのシンボルテーブルを見てみても、正しくアドレス補填されていないのです。
この原因を色々調べていると、
どうやら main
シンボルが .text
セクション内ではないとリンカに判断されている ようでした。
でもおかしいなあ、 .symtab
に格納しているmain関数に対応したシンボルでは、
ちゃんと st_shndx
に .text
を指す 1
を指している。
これは gccの吐くオブジェクトファイルと同じなんだけど…
正解は…
感の鋭い方はおわかりになったかもしれません。
技術分野では 配列構造の先頭を1で表すことは珍しいよね? と。
そう、つまり
私の生成したオブジェクトファイル中のmainシンボルは 1
、つまり .text
の次の .symtab
を指していたのです。
そうかそうか、じゃあ Elf64_Sym->st_shndx
に 0
を入れておけばいいんだな?
こう思うのは自然の摂理です。むしろそうなって当然です。
そうして生成されたオブジェクトファイルに対し、 readelf -s c.o
を実行しました。
NDX
を確認するとなんとそこには…
そう、これが nullヘッダの存在する理由 です。
nullセクションを 仮定 し、 それに対応するヘッダを作ることで、
セクション番号が0を使わないようにしていたのです。
一般に、シンボルは何らかのセクションに含まれます。
.data
であったり、 .rodata
であったり、 .text
であったり…
このとき Elf64_Sym
等のシンボルを表す構造体では、
自身が含まれるセクションの番号を格納する st_shndx
というメンバを持っています。
しかしこれは unsigned-integer
型なので 負の数=UNDEFINED
みたいなことも出来ず、
0をそのような意味付けで用いているようですね。