1. Cの文字列がCPUへ届くまで

1.1 ストレージ、メモリ、CPU

要素 このプログラムでの役割
ストレージ 実行ファイルを長期保存する
メモリ 実行中の命令とデータをアドレス付きで置く
CPU メモリから命令を取得し、解釈し、実行する
レジスタ CPU内部の小さく高速な状態保存場所

ストレージにあるバイナリは、そのままCPUが読むわけではない。OSがプロセスを作り、実行に必要な部分を仮想メモリに対応付ける。CPUはその仮想アドレスを使って命令とデータにアクセスする。

1.2 コンパイルから実行まで

Cソース、コンパイル、ELF実行ファイル、OSロード、CPU実行の流れ

図: ソースの関数名は、最終的にはアドレス付きの命令列になる。

  1. プリプロセッサが#includeなどを処理する。
  2. コンパイラがCをアセンブリ相当の命令へ変換する。
  3. アセンブラが命令をオブジェクトファイルへ符号化する。
  4. リンカが必要なコードとデータをまとめ、ELF実行ファイルを作る。
  5. OSのローダがELFのプログラムヘッダを読み、命令やデータを仮想メモリへ対応付ける。
  6. OSが初期スタックとレジスタ状態を準備し、エントリポイントから実行を始める。

mainは実行ファイルの先頭命令ではない。ランタイムの初期化後、最終的にmainが呼ばれる。本編はその後から追う。

1.3 デモのビルド条件

cc -std=c11 -O0 -g3 -Wall -Wextra \
  -fno-omit-frame-pointer -fno-stack-protector -fno-pie \
  ret2win.c -o ret2win -no-pie -Wl,-z,noexecstack
指定 目的
-O0 最適化による関数の削除や変形を抑える
-fno-omit-frame-pointer RBPでフレームを追いやすくする
-fno-stack-protector カナリを無効化し、原因の連鎖を観察する
-fno-pie -no-pie デモで本体コードのアドレスを固定する
-z noexecstack スタックは実行不可にし、既存のwinへの遷移だけを見る

コンパイラは16バイトの配列へ128バイトを書き込み得ることを警告する。それでもビルドするのは、警告対象の原因を自作プログラムで観察するためである。