バイナリ解析ツールの使い方とか雑多なことについての個人的なメモ.

不定期更新.

ツールについて

objdump

実行ファイルの逆アセンブルができる汎用ツール. コンパイル後に各関数がどんな機械語に落ちているのかを調べるのに便利.

# アセンブリのみの出力を得る.
$ objdump -d binary_file

# 元の C 言語ソース付きの出力を得る.
$ objdump -S binary_file

# デフォルトの AT&T シンタックスではなく、(nasm などの) Intel シンタックスを使用して表示する.
$ objdump -d -M intel binary_file

# 大体出力結果が多すぎるので、head で先頭だけ取り出したりする.
$ objdump -d binary_file | head

# でも出力結果を細かく調べたいときには vim が便利.
$ objdump -d binary_file | vim -

なお、バイナリファイルのフォーマットを objdump が自動判定できない場合には --target オプションを与えることでファイルの読み込みフォーマットを手動で設定することができる. (--target=coff-i386 など.)

readelf

readelf コマンドは ELF 形式の実行ファイルの解析ができる. 関数のシンボルテーブル情報が含まれているため、各関数がどのアドレスに割り当てられているのか確認できる. 複数の関数が同じアドレスに割り当てられているかを調べることで関数同士が互いにエイリアスになっているかを調べられる.

# -a は全ての情報を出力する. --all でも同様.
$ readelf -a binary_file
$ readelf --all binary_file

# -S はセクション情報を出力する. --sections や --section-headers でも同様.
$ readelf -S binary_file
$ readelf --sections binary_file
$ readelf --section-headers binary_file

# -l はセグメント情報を出力する. --segments や --program-headers でも同様.
$ readelf -l binary_file
$ readelf --segments binary_file
$ readelf --program-headers binary_file

strace

特定の実行ファイルが呼び出しているシステムコールの一覧を調べられる.

$ strace binary_file

hexedit

コマンドライン上で動くバイナリエディタ.

$ hexedit binary_file

カーソルの移動は Ctrl + f/b/n/p. Emacs と同じ. Tab キーを押すことでカーソル位置を Hex 文字列 - ASCII 文字列間で切り替えることができる. / キーで検索も可能. カーソル位置が Hex 文字列と ASCII 文字列のどちらにあるかによって検索対象が異なるので注意. 保存は Ctrl + x. 終了は Ctrl + c.

ndisasm

逆アセンブラ.

# 32 ビットモードのバイナリファイルを逆アセンブルする.
$ ndisasm -b 32 binary_file

# 先頭から特定のバイト数分を無視して逆アセンブルする. (この場合は先頭 0x24 バイトを無視.)
$ ndisasm -b 32 -e 0x24 binary_file

ldd

実行ファイルにリンクしている共有ライブラリを解析する.

$ ldd binary_file

gcc

いつもお馴染みの gcc. オプションだけちょっとまとめておく.

# gdb でのデバッグの際に便利なようにソースコードのシンボル情報を付加して静的リンク.
$ gcc hello.c -o hello -Wall -g -O0 -static

# プリプロセス、コンパイルまでを行い、アセンブリコードを出力. (アセンブル以降の処理を打ち切り)
$ gcc -S hello.c -o hello.s

# アセンブルを行い、オブジェクトファイルを出力. (リンク以降の処理を打ち切り)
$ gcc -c hello.s -o hello.o

# リンクを行い、実行ファイルを出力.
$ gcc hello.o -o hello

# 標準出力にログを出力する.
$ gcc hello.c -o hello -Wall -g -O0 -static -v

# 実行ファイルが生成されるまでの過程で生成される中間ファイルを削除せずに残す.
$ gcc -save-temps -o hello hello.c -Wall -g -O0 -static

ld

リンカ. 使いそうなオプションをメモ.

# gcc が実行ファイルを生成する際に使用している標準のリンカスクリプトを表示.
$ ld -verbose

make

ビルドツール. -j オプションでコア数を指定できる.

# 4 コアに分散してビルドを行う.
$ make -j 4

configure スクリプト

Makefile 生成ツール. --prefixmake install でのインストール先指定ができる. また、--disable-werrormake でワーニングが発生した際にエラー扱いしなくなる.

# glibc の configure スクリプトをビルド用ディレクトリから実行.
# make install でのインストール先を /usr/local/glibc-2.21 に指定.
$ ../glibc-2.21/configure --prefix=/usr/local/glibc-2.21 --disable-werror

strings

バイナリファイル中に埋め込まれている文字列を列挙する.

# 実行ファイル中に含まれている文字列の中から "Hoge" を含むものを表示する.
$ strings binary_file | grep Hoge

シェル上でのファイル検索

バイナリ解析全然関係ないけどよく忘れるので… (恥)

find コマンドでファイル名についての絞り込み、grep コマンドでファイルの内容についての検索ができる. find はデフォルトでディレクトリを再帰的に検索していくが、grep は通常 1 つのファイル内の検索しか行わず、 -r オプションを渡さないと再帰的には検索しないので注意.

検索ヒット数を調べたいときには出力中の改行文字の個数を調べればよいので、出力に対してさらに wc -l をかませばよい. (-l がないと全体の文字列数をカウントするので注意.)

wc などの UNIX コマンドは標準入力を直接処理対象にすることができる (単に $ wc として wc を起動すると、EOF - つまり Ctrl + D が入力されるまでのキーボード入力を対象に文字数をカウントできる) ので単にパイプ | でつなぐことで wcfindgrep の出力結果のカウントに利用できる.

find からの出力結果を grep に渡す場合、単に find . -name "*.txt" | grep hoge とすると “拡張子が .txt のファイルのうち、名前に hoge を含むファイル名 (hoge.txthoge2.txt など)” を検索するという処理となる.

そうではなく、find から返された結果中に含まれる各ファイルの中身に対して grep で検索をしたいという場合には xargs コマンドを間に挟むことによって標準入力からコマンドラインを作って grep に処理させる必要がある. その場合は find . -name "*.txt" | xargs grep hoge と入力すれば良い. これにより、“拡張子が .txt の各ファイルに対して grephoge という文字列を検索する” という処理となった.

Wikipedia 曰く、find . -name "foo" | xargs grep bargrep bar `find . -name "foo"` に相当するらしい.

# カレントディレクトリ以下に存在する拡張子が ".S" のファイルの個数を調べる.
$ find . -name "*.S" | wc -l

# カレントディレクトリ以下のファイルに対し "syscall" という文字列を検索し、ヒットした箇所の数を調べる.
$ grep -r syscall . | wc -l

# カレントディレクトリ以下に存在する拡張子が ".S" のファイルに対して "syscall" という文字列を検索し、ヒットした箇所の数を調べる.
$ find . -name "*.S" | xargs grep syscall | wc -l

また、grep に対して -v オプションを渡すと “指定したパターンを 含まない行” にヒットするので、一度 grep した結果から特定のパターンを含まない行だけを抽出する二段目の grep に利用できる.

# カレントディレクトリ以下のファイルに対して "PSEUDO" という文字列を検索し、ヒットした行の中から "PSEUDO_" を含まない行だけを表示する.
# 関数やマクロの参照を調べたい時に、"PSEUDO_" は除外するが "PSEUDO" を含む参照だけを調べるような場合に便利.
$ grep -r PSEUDO . | grep -v PSEUDO_

なお、検索結果に対してさらにフィルタリングできるとより便利になる. (まだ使いこなせず.) 例えば grep -r hoge . を実行して以下のような結果が得られたとする.

$ grep -r hoge .
./sample.txt:This is a hoge1 line.
./sample.txt:This is a hoge2 line.
./sample2.txt:This is a hoge1 line.
./sample2.txt:This is a hoge2 line.

このように、grep -r の出力結果は “ファイル名:検索がヒットした行” というフォーマットになっている. このとき、":" から後ろを削除して出力することができれば、検索がヒットしたファイル名だけを出力することができ、結果が見通しやすくなる.

このような出力結果のフィルタリングには正規表現が使える. この例の場合には、sedperl を使って次のように書ける.

# sed を使う場合.
$ grep -r hoge . | sed 's/:.*//'

# perl を使う場合.
$ grep -r hoge . | perl -pe 's/:.*//'

これによって以下のように検索がヒットしたファイル名だけを得ることができる.

$ grep -r hoge . | sed 's/:.*//'
./sample.txt
./sample.txt
./sample2.txt
./sample2.txt

さらに、一つのファイル内で複数の行が検索にヒットしてしまって出力が大量になってしまっている場合には、 このフィルタリングした結果をさらに uniq コマンドでフィルタリングすることができる.

$ grep -r hoge . | sed 's/:.*//' | uniq
./sample.txt
./sample2.txt

後は大量の検索結果中から特定の文字列を探すような場合には最後に less を繋げばよい. まぁ、僕は vim に慣れすぎてしまってしっくりこないからいつも vim に繋いでしまうんだけれども.

# less に繋ぐ場合.
$ grep -r hoge . | less

# vim に繋ぐ場合.
$ grep -r hoge . | vim -

システムコールについて

ptrace

デバッガなどが使うシステムコール. gdbgdbserver なども使用している. デバッグ対象プログラムが動いているプロセスのメモリやレジスタの状態を確認したり、その中身を書き換える独自トレーサを作成できる. fork() によって独自トレーサプロセスが親プロセスとなり、デバッグ対象プログラムが子プロセスとなる場合に、子プロセス側が execve() の実行前に PTRACE_TRACEME というリクエストを発行しておくと親プロセス側で子プロセスの動作を制御できるようになる. これによって親プロセス側で子プロセスのレジスタ状態を確認 (PTRACE_GETREGS) したり、子プロセスのステップ実行 (PTRACE_SINGLESTEP) などができる.

その他

x86 スタックフレーム

ハロー“Hello,World"OSと標準ライブラリのシゴトとしくみ で扱う C 言語の Hello World を実行する際のスタックフレームメモ.

  • hello.c
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Hello World! %d %s\n", argc, argv[0]);
    return 0;
}
  • $ objdump -d hello
080482bc <main>:
 80482bc:       55                      push   %ebp
 80482bd:       89 e5                   mov    %esp,%ebp
 80482bf:       83 e4 f0                and    $0xfffffff0,%esp
 80482c2:       83 ec 10                sub    $0x10,%esp
 80482c5:       8b 45 0c                mov    0xc(%ebp),%eax
 80482c8:       8b 10                   mov    (%eax),%edx
 80482ca:       b8 0c 36 0b 08          mov    $0x80b360c,%eax
 80482cf:       89 54 24 08             mov    %edx,0x8(%esp)
 80482d3:       8b 55 08                mov    0x8(%ebp),%edx
 80482d6:       89 54 24 04             mov    %edx,0x4(%esp)
 80482da:       89 04 24                mov    %eax,(%esp)
 80482dd:       e8 7e 10 00 00          call   8049360 <_IO_printf>
 80482e2:       b8 00 00 00 00          mov    $0x0,%eax
 80482e7:       c9                      leave
 80482e8:       c3                      ret
 80482e9:       90                      nop
 80482ea:       90                      nop
 80482eb:       90                      nop
 80482ec:       90                      nop
 80482ed:       90                      nop
 80482ee:       90                      nop
 80482ef:       90                      nop
low  ^  0. start             1. push              2./3. mov/and
addr.|  +-----------+        +-----------+        +-----------+
     |  |           |        |           |        |           |
     |  |           |        |           |        |           |
     |  |           |        |           |        |           |
     |  |           |        |           |        |           |
     |  |           |        |           |        |           |
     |  |           |        |           |        |           |
     |  |           |        |           |        |           | <-esp (16 byte aligned)
     |  |           |        |           |        |           |
     |  |           |        | old ebp   | <-esp  | old ebp   | <-ebp
     |  |-Ret.addr--| <-esp  |-Ret.addr--|        |-Ret.addr--|
high |  | argc      |        | argc      |        | argc      |
addr.|  | argv addr |        | argv addr |        | argv addr |


low  ^  4. sub               12. call (直後に Ret.addr が push される)
addr.|  +-----------+        +--------------+
     |  |           |        |              |
     |  |           |        |              |
     |  |           | <-esp  | 0x80b360c    | <-esp (esp+0x0 〜 esp+0x3)
     |  |           |        | argc         | esp+0x4 〜 esp+0x7
     |  |           |        | argv[0] addr | esp+0x8 〜 esp+0xb
     |  |           |        |              |
     |  |           |        |              |
     |  |           |        |              |
     |  | old ebp   | <-ebp  | old ebp      | <-ebp (ebp+0x0 〜 ebp+0x3)
     |  |-Ret.addr--|        |-Ret.addr-----| ebp+0x4 〜 ebp+0x7
high |  | argc      |        | argc         | ebp+0x8 〜 ebp+0xb
addr.|  | argv addr |        | argv addr    | ebp+0xc 〜 ebp+0xf


low  ^  printf にジャンプした直後
addr.|  +--------------+
     |  |              |
     |  |-Ret.addr-----| <-esp
     |  | 0x80b360c    |
     |  | argc         |
     |  | argv[0] addr |
     |  |              |
     |  |              |
     |  |              |
     |  | old ebp      | <-ebp
     |  |-Ret.addr-----|
high |  | argc         |
addr.|  | argv addr    |

leave の処理は mov %ebp,%esp の後に pop %ebp をして状態を 0. start と同じ状態に戻すこと. 直後の ret によってスタック先頭の Ret.addr が pop されてプログラムカウンタにセットされジャンプ.

x86 Linux での int $0x80 によるシステムコール呼び出し

eax にシステムコール番号、ebx、ecx、edx … にシステムコールへの引数を順にセットして int $0x80 を呼び出すことでシステムコールを実行することができる. システムコールからの戻り値は eax にセットされる.

Linux カーネル v2.6.39 の場合、arch/x86/kernel/entry_32.S#194 からの SAVE_ALL マクロの中でスタックにレジスタを順にプッシュすることでシステムコールハンドラへの引数のセットを行っている. したがって、この順序を参考にしておけばよい. 最後に push されるのは #215 の ebx なので、ebx がシステムコールハンドラの 1st arg となる.

x86-64 での関数呼び出し ABI

x86 とは異なり、関数呼び出しの際の引数は 6 つ目まではレジスタ経由、7 つ目以降はスタック経由で渡される.

引数 レジスタ
1st arg rdi
2nd arg rsi
3rd arg rdx
4th arg rcx
5th arg r8
6th arg r9

なお 7 つ目以降の引数をスタック経由で渡す際は、call の前にスタックポインタが 16 バイト境界にアライメントされていることが ABI で要求されているので注意.

これを守っていないと関数呼び出しの直後にバスエラーでプログラムが死ぬ.

なお、関数からの戻り値は相変わらず rax で返される.

システムコールラッパー

標準 C ライブラリ (glibc など) が提供している、システムコール呼び出しのためのラッパー関数. int $0x80sysentersyscall といったアセンブリ命令を実行する必要があるため、標準 C ライブラリのアーキテクチャ依存ディレクトリ中に収められている (glibc-2.21/sysdeps など).

システムコール呼び出しのため、システムコールラッパー自体はアーキテクチャごとにカーネルが定めている ABI を満たすためにアセンブリコードで記述され、システムコール番号やシステムコール実行に必要となるパラメータを各レジスタに設定したりしている. このため、標準 C ライブラリの利用者となるプログラマはアーキテクチャごとのアセンブリコードを記述することなくカーネルのシステムコール呼び出しを行うことができる.

なお、システムコールラッパーは利用者に対してシステムコール呼び出しのための API を提供しているとも捉えられる. Linux は POSIX で定めているシステムコール API に準拠していると言われているが、実際には Linux が POSIX に準拠できているのは glibc が POSIX に準拠した API を提供しているためであり、システムコールラッパー内で適切な Linux カーネルのシステムコール命令の呼び出しを行っている.

例えば POSIX では atexit(3) 関数などで登録されたハンドラ関数を一切実行せずに直ちにプロセスを終了させる _exit(2) というシステムコール呼び出しのための API (システムコールラッパー) が規定されているが、Linux ではこの API は glibc によってアセンブリコードで書かれた関数として提供されている. (したがって通常の C 言語のプログラムから呼び出すことができる).

実際にプロセスを終了させるためにはシステムコールラッパー _exit(2) の中から Linux カーネルに対してプロセスを終了させるためのシステムコール呼び出しを行わなければならないが、Linux カーネル v2.6 ではこの機能を提供するシステムコールとして exit_group というシステムコール手続きが呼ばれるようになっている.

なお、atexit(3) 関数で登録したハンドラ関数を実行してからプロセスを終了する exit(3) という関数も標準 C ライブラリによって提供されている.

glibc ではシステムコールラッパーはシェルスクリプトとマクロ展開によって自動生成されるようになっているらしく (テンプレートとなるコードは glibc-2.21/sysdeps/unix/syscall-template.S あたり. シェルスクリプトは glibc-2.21/sysdeps/unix/make-syscalls.sh あたり. このシェルスクリプトは Makefile から呼ばれるっぽい)、どのコードがどの処理に対応しているのかを調べる場合には glibc を自前でビルドし、その glibc と自分のプログラムをリンクさせて gdb 上でデバッグすることで標準 C ライブラリ内の処理をシンボリックデバッグできるようにするのが有効.

Linux におけるプログラム実行の際のスタートアップ処理と終了処理

UNIX 系のシステムでは、プロセスは親プロセスから fork(2) システムコール呼び出しが行われることで子プロセスとして生成され、exec() 系の関数 (execlp()execvp() など) から execve(2) システムコール呼び出しが行われる (あるいは直接 execve(2) システムコール呼び出しを行う) ことで新たなプログラムがメモリに書き込まれる. ユーザがシェルからプログラムを実行する場合、各プログラムのプロセスの親プロセスは多くの場合シェルとなる.

execve(2) システムコール呼び出しが行われると、OS カーネルによって実行ファイル読み込み、仮想メモリへのマッピング、argc/argv の初期化などの処理が行われた後、実行ファイルのエントリポイント情報に従ってエントリポイントのアドレスからプログラムの実行が開始される.

x86 Linux では実行ファイルのフォーマットとして ELF フォーマットが採用されているため、このとき Linux カーネルは ELF フォーマットの実行ファイルを解釈し、メモリ上にロードしている. Linux v2.6 では fs/binfmt_elf.c 内で定義されている load_elf_binary() 関数によって ELF ファイルのロードができるようになっている. この load_elf_binary() 関数の末尾では start_thread() というアーキテクチャ依存の関数 (arch/x86/kernel/process_32.c などで定義されている) が呼び出されており、この関数によって子プロセスのためのプログラムカウンタやスタックポインタの値が設定されている.

readelf を使用したり、ELF ヘッダのフォーマット を知った上で ELF ファイルをバイナリエディタで開いたりすればその実行ファイルのエントリポイントのアドレスを知ることができる.

main() 実行前のスタートアップ処理は glibc などの標準 C ライブラリによって提供される. すなわち、作成した C 言語プログラムをコンパイル・アセンブルして得たオブジェクトファイル中にはスタートアップ処理は含まれておらず、glibc などの標準 C ライブラリをそのオブジェクトファイルをリンクさせた際に標準 C ライブラリからスタートアップ処理が提供されることとなる.

スタートアップ処理は標準 C ライブラリによって提供されるため、スタートアップ処理のソースコードは標準 C ライブラリのソースコード中に含まれている. スタートアップ処理は各種レジスタの設定などが必要となることから、アセンブリコードで記述される必要がある. したがって、標準 C ライブラリのアーキテクチャ依存ディレクトリ内で管理されている. (x86 の場合、glibc-2.21/sysdeps/i386/start.S)

gdb などで調べると、glibc をリンクすることで得た実行ファイルのエントリポイントアドレスには _start というシンボルが配置されていることがわかり、プログラムの実行は _start() から開始されることがわかる. _start() からは __libc_start_main()call 命令によって呼び出される. その直後には hlt 命令が存在しているが、実際には __libc_start_main() の先から _start() には戻ってこないため、この hlt 命令は全く実行されない命令となっている.

_start() から呼び出される __libc_start_main() は C 言語で書かれたアーキテクチャ非依存の処理となっており、glibc-2.21/csu/libc-start.c 内で記述されている. __libc_start_main() からは main()call 命令によって呼び出されることになる. これによって main() の処理が開始されるため、C 言語で記述したプログラムの本体が開始される. なお、main() の呼び出しの直前には argcargvenvp といった引数の準備処理が __libc_start_main() 中で行われている.

main() の処理が終了して return すると再び __libc_start_main() に戻るが、その直後には exit(3) の呼び出しが行われる. この時、main() から return した際のステータスコードがそのまま exit(3) の引数として渡されるため、これらの関数は exit(main(argc, argv, envp)); のような形で実行されるようになっている.

exit(3)__run_exit_handlers() を呼び出すだけとなっており、実際の処理はこの関数で行われる. この関数は主に atexit(3) で登録したハンドラ関数の実行処理を行い、最後に _exit(2) を呼び出すようになっている.

_exit(2) の先ではシステムコール exit_group が呼ばれることとなり、プログラムはその時点で終了する. このため、main() の末尾では return 0; を行っても exit(0); を行ってもその振る舞いは全く変化しないこととなる.

Buffered I/O のバッファリングモード

標準出力などに文字列を出力したい場合にはユーザプログラムから OS カーネルに対して write システムコール呼び出しをしなければならないが、一般的にシステムコール呼び出しはオーバーヘッドが大きい. したがってパフォーマンスを向上させるために、逐一システムコールを呼び出すのではなく一定量までバッファを貯めてからまとめて出力する Buffered I/O が使われている.

プログラムの標準出力がターミナルなどに直接結びついている場合 (TTY) には printf() などの Buffered I/O 関数はラインバッファモードとなり、改行コードを認識するたびにバッファに貯められた文字列が標準出力に flush (write システムコールを用いて実際に書き出し) されるが、標準出力がパイプに結びついている場合や cron などで実行されていてターミナルなどに直接結びついていない場合にはバッファリングモードが切り替わり、フルバッファモードとなる. このため、実際に標準出力に対して flush されるタイミングがプログラムの実行方法によって変化する場合がある.

特に最後に終了メッセージを表示して終了するようなプログラムの場合、ラインバッファモードでは改行文字を発見次第即座に標準出力に flush するが、フルバッファモードではバッファが満タンにならない限りは flush されないため、プログラムが exit(3) によって終了する際に終了メッセージなどがバッファ中に残ってしまって標準出力に書き出されないままとなる場合がある. その場合、exit(3) 中からバッファの flush が行われることとなり、プログラムの終了前に全ての文字列がバッファから標準出力に実際に書き出されることとなる.

Buffered I/O のバッファ本体についての情報は FILE 構造体中のメンバ *_IO_read_ptr*_IO_read_end*_IO_read_base*_IO_write_base*_IO_write_ptr*_IO_write_end にアクセスすることで得ることができる. glibc-2.21 では FILEstruct _IO_FILE の別名として libio/stdio.h で定義されており、struct _IO_FILE の定義は libio/libio.h に定義されている. stdiostdoutstderr はいずれも struct _IO_FILE 型へのポインタ型 struct _IO_FILE* として定義されている. Buffered I/O の場合、実際の write システムコールによる flush 処理はこの FILE 構造体へのポインタ中の *_IO_write_ptr などを参照することで行っている. (glibc-2.21/libio/fileops.c_IO_new_file_overflow() など参照).

なお、標準 C ライブラリでは scanf() 関数などで標準入力を待ち受ける際、直前に標準出力のバッファを flush するようになっているらしい. これにより、入力を促すプロンプトを確実に表示してから入力を待ち受けることができるようになっている.

ELF フォーマットについて

ELF フォーマットは先頭に ELF ヘッダがあり、内部はセクションとセグメントという 2 種類の単位で管理されている. それぞれ、セクションはリンクの単位、セグメントは実行時のロードの単位として使用されており、各セクションについてはセクションヘッダ、各セグメントについてはプログラムヘッダにまとめて記述されている.

ELF ヘッダ、セクションヘッダ、セグメントヘッダのそれぞれの情報は readelf コマンドを用いて表示することができる.

Linux カーネル v2.6 では fs/binfmt_elf.c で ELF ファイルのロード処理を行う load_elf_binary() 関数を定義しており、このファイルからインクルードされている include/linux/elf.h 中に ELF ヘッダやプログラムヘッダ、セクションヘッダといった各種ヘッダを解析するための構造体が定義されている.

  • ELF ヘッダ
#define EI_NIDENT      16

typedef struct elf32_hdr{
  unsigned char e_ident[EI_NIDENT];
  Elf32_Half    e_type;
  Elf32_Half    e_machine;
  Elf32_Word    e_version;
  Elf32_Addr    e_entry;  /* Entry point */
  Elf32_Off     e_phoff;
  Elf32_Off     e_shoff;
  Elf32_Word    e_flags;
  Elf32_Half    e_ehsize;
  Elf32_Half    e_phentsize;
  Elf32_Half    e_phnum;
  Elf32_Half    e_shentsize;
  Elf32_Half    e_shnum;
  Elf32_Half    e_shstrndx;
} Elf32_Ehdr;
  • セクションヘッダ (セクション情報管理)
typedef struct {
  Elf32_Word    sh_name;
  Elf32_Word    sh_type;
  Elf32_Word    sh_flags;
  Elf32_Addr    sh_addr;
  Elf32_Off     sh_offset;
  Elf32_Word    sh_size;
  Elf32_Word    sh_link;
  Elf32_Word    sh_info;
  Elf32_Word    sh_addralign;
  Elf32_Word    sh_entsize;
} Elf32_Shdr;
  • プログラムヘッダ (セグメント情報管理)
typedef struct elf32_phdr{
  Elf32_Word    p_type;
  Elf32_Off     p_offset;
  Elf32_Addr    p_vaddr;
  Elf32_Addr    p_paddr;
  Elf32_Word    p_filesz;
  Elf32_Word    p_memsz;
  Elf32_Word    p_flags;
  Elf32_Word    p_align;
} Elf32_Phdr;

セクションヘッダ中には .text.rodata のようなセクション名やそのセクションが配置されるアドレス、ファイル中でのセクションの位置のオフセット値、セクションのサイズといった情報がまとめられている.

また、プログラムヘッダ中には各セグメントの種類やオフセット、仮想アドレスと物理アドレスの対応などがまとめられており、readelf で確認すると .text.rodata といった各セクションがどのセグメントに格納されているかが確認できるようになっている.

セクションは主にリンカのために存在する管理単位となっている. 複数のオブジェクトファイル中にはそれぞれの .text セクションや .rodata セクションが存在するが、リンカによってこれらのオブジェクトファイルがリンクされると、同じ名前のセクションが一つにまとめられて実行ファイルが生成される. すなわち、それぞれのオブジェクトファイル中の .text セクションや .rodata セクションは、生成される新たな実行ファイル中の .text セクションや .rodata セクションへとまとめられる. このリンカによるセクションのまとめかたは、リンカスクリプトと呼ばれる設定ファイルで制御できる.

またセグメントは主にプログラムの実行の際のローダのために存在する管理単位となっている. execve(2) システムコールによって子プロセスに ELF フォーマットの実行ファイルがロードされる際、Linux カーネルはプログラムヘッダの情報を元にして実行ファイルをロードするようになっている.

基本的にセクションヘッダのセクション情報はリンクの際に必要となる情報であり、実行時には必ずしも必須とはならない情報となっている (デバッグ情報などが含まれることがあるが). したがって実行ファイルのサイズ削減のために削除することができるようになっており、削除する際にセクションヘッダ領域以降のオフセットが狂うようなことが発生しないように実行ファイルの終端に存在している.

逆に、プログラムヘッダのセグメント情報は ELF ファイルの先頭付近に配置されているため、予めセグメント情報を参照しておき、その情報に合わせてロード先のメモリ上に直接ロードすることができるようになっている. このため、最初に実行ファイル全体をロードしてから再度メモリ上にコピーし直すような手間が不要となる.

動的リンクと共有ライブラリ

動的リンク (dynamic linking) は 実行時リンク を指し、共有ライブラリ (shared object; .so) は 仮想メモリ機構を用いてメモリ上で共有されるライブラリ を指す.

動的リンクは実行ファイルのサイズ削減、共有ライブラリはメモリ消費量の削減に貢献する.

PLT (Procedure Linkage Table)、GOT (Global Offset Table)、PIC (Position Independent Code)、遅延リンクなどは後日もう少し調べる.

参考文献

  • ハロー“Hello,World"OSと標準ライブラリのシゴトとしくみ
    • C 言語の Hello World のプログラムの動的解析から始まり、Linux カーネルや glibc のソースコードを参照しながら低レイヤーの幅広いトピックに踏み込んでいく珍しい本. シェルの操作に引っかかっているとなかなか読み進められず…