DrumatoのBlog

CS/Network/Infraが好きな桃です.

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製です)。

上手く行ったときの様子です。
本記事ではこのツイートより少し前、正常にリンク出来ないバグを踏んでいたときに知ったことを取り上げています。

以下のようなアセンブリ言語がパース、機械語に変換できたのでオブジェクトファイルに機械語を組み込むことにしました。

main:
    mov rax, 2
    ret

ここで、GNU ldがリンク可能なオブジェクトファイルに必要であるセクションは、

  • .text
  • .strtab
  • .symtab
  • .shstrtab

です(リンカスクリプトを書いたりすればもっと少なくて済むかも)。
なのでこれらセクションを用意し、いざgccコマンドを実行してみました。

それがこちらになります。
リンク出来たので嬉しい!でもセグフォ発生する。
この理由を当時の私は "スタートアップルーチンをリンクしていないから" だと思っていました。
でもよく見ると、バイナリに _start シンボルはある。
gdbで動的解析してみても スタートアップルーチンが実行できている…うーん、うーん…

と、結構困っていたのです。
いろいろ調べてみると、どうやら mainシンボルのシンボル解決が上手く行っていない 事がわかりました。
生成されたオブジェクトファイルのシンボルテーブルを見てみても、正しくアドレス補填されていないのです。

この原因を色々調べていると、
どうやら main シンボルが .text セクション内ではないとリンカに判断されている ようでした。
でもおかしいなあ、 .symtab に格納しているmain関数に対応したシンボルでは、
ちゃんと st_shndx.textを指す 1 を指している
これは gccの吐くオブジェクトファイルと同じなんだけど…


正解は…

感の鋭い方はおわかりになったかもしれません。
技術分野では 配列構造の先頭を1で表すことは珍しいよね? と。
そう、つまり
私の生成したオブジェクトファイル中のmainシンボルは 1 、つまり .text の次の .symtab を指していたのです。
そうかそうか、じゃあ Elf64_Sym->st_shndx0 を入れておけばいいんだな?
こう思うのは自然の摂理です。むしろそうなって当然です。

そうして生成されたオブジェクトファイルに対し、 readelf -s c.o を実行しました。
NDX を確認するとなんとそこには…

f:id:orangebladdy:20190910203302j:plain

そう、これが nullヘッダの存在する理由 です。
nullセクションを 仮定 し、 それに対応するヘッダを作ることで、
セクション番号が0を使わないようにしていたのです。

一般に、シンボルは何らかのセクションに含まれます。
.data であったり、 .rodata であったり、 .text であったり…
このとき Elf64_Sym 等のシンボルを表す構造体では、
自身が含まれるセクションの番号を格納する st_shndx というメンバを持っています。
しかしこれは unsigned-integer 型なので 負の数=UNDEFINED みたいなことも出来ず、
0をそのような意味付けで用いているようですね。