- プログラムが実行される前に、カーネルが
execve システムコールを通じてプロセスを生成・初期化する過程を探る技術分析
- この呼び出しは実行ファイルのパス、引数、環境変数を渡し、カーネルはそれに基づいて ELF 形式の実行ファイルをロードする
- ELF ファイルにはコード、データ、シンボル、動的リンク情報などが含まれ、カーネルはこれを解釈して メモリマッピングとスタック初期化を行う
- その後カーネルは
_start エントリポイントに制御を渡し、言語ごとのランタイムが初期化された後にようやく ユーザー定義の main 関数が呼び出される
- この過程は OS、コンパイラ、ランタイムの協調構造を示しており、システムレベルでプログラム実行がどのように行われるかを理解するうえで重要である
プログラム実行の開始点: execve 呼び出し
- Linux でのプログラム実行は
execve システムコールによって始まる
execve(const char *filename, char *const argv[], char *const envp[]) の形で、実行ファイル名、引数リスト、環境変数リストを渡す
- カーネルはこれによって、どのプログラムをどの環境で実行するかを決定する
- 高水準言語では、この呼び出しは 標準ライブラリのプロセス実行 API でラップされている
- 例: Rust の
std::process::Command は内部的に execve を呼び出す
- シェルの PATH 探索と似た形で、コマンド名を完全パスに変換する処理を行う
- Shebang(
#!) があるスクリプトの場合、カーネルは指定されたインタープリタを使ってプログラムを実行する
- 例:
#!/usr/bin/python3 → Python インタープリタで実行
ELF: 実行ファイルの構造
- Linux の実行ファイルは ELF(Executable and Linkable Format) 形式に従う
- ELF はコード、データ、シンボル、再配置情報などを含む 標準的な実行ファイル形式
- 他の OS では Mach-O(macOS)、PE(Windows) など別の形式が使われる
- ELF ヘッダにはファイルの構造やメモリ配置情報が含まれる
- 例:
ELF Magic, Class, Entry point address, Program headers, Section headers
Entry point address はプログラムが最初に実行する命令のアドレスである
- 例示された ELF ヘッダでは、RISC-V アーキテクチャ向けの ELF32 実行ファイルで、
0x10358 アドレスがエントリポイントに指定されている
ELF の内部構成要素
- ELF ファイルはいくつもの セクション(section) で構成されている
.text: 実行コード
.data: 初期化済みグローバル変数
.bss: 未初期化グローバル変数
.plt: 共有ライブラリ呼び出し用テーブル
.symtab, .strtab: シンボルおよび文字列テーブル
- PLT(Procedure Linkage Table) は共有ライブラリ関数の呼び出しを支える
- 例:
libc の printf, malloc など
- ELF の
PT_INTERP セクションは動的リンカ(interpreter)を指定する
- カーネルは ELF を読み取り、ロード可能なセクションをメモリに配置し、必要に応じて ASLR, NX bit などのセキュリティ機能を適用する
シンボルテーブルとランタイムリンク
- ELF の シンボルテーブル(symtab) には関数や変数のアドレス情報が含まれる
- 例:
_start, main, __libc_start_main などのエントリが存在する
- 単純な「Hello, World!」プログラムであっても 2300 個以上のシンボルを含むことがある
- これは大半が 標準ライブラリとランタイム初期化コードに由来する
musl や glibc といった libc 実装がリンクされているためである
- カーネルは ELF の各セクションをロードした後、インタープリタ(動的リンカ) に制御を渡す
- インタープリタは再配置(relocation)、アドレス空間配置のランダム化(ASLR)、実行権限の設定(NX bit)などを処理する
スタック初期化の過程
- カーネルはプログラム実行前に スタック(stack) を直接構成しなければならない
- スタックはローカル変数、関数呼び出しフレーム、引数の受け渡しなどに使われる
execve 呼び出し時に渡された argv, envp はスタックに格納される
- プログラムはこれを通じてコマンドライン引数や環境変数にアクセスする
- カーネルはさらに ELF 補助ベクトル(auxv) もスタックに含める
- ページサイズ、ELF メタデータ、システム情報など約 30 項目を含む
- 例:
AT_PAGESZ はメモリページサイズ(例: 4KiB)を指定する
- RISC-V エミュレータの例では、スタックポインタ(
sp)を高位アドレスから開始し、引数、環境変数、補助ベクトルを逆順に積んでいく
エントリポイントと _start 関数
- ELF の エントリポイントは
_start 関数のアドレスに指定される
_start はカーネルが制御を渡す最初のユーザー空間コードである
- ほとんどの言語は
_start で ランタイム初期化を行った後に main を呼び出す
- 例: Rust の
std::rt::lang_start, C の __libc_start_main
- Rust の例では、
#![no_std], #![no_main] 属性を使うことで、ランタイムなしに _start を直接定義できる
_start 内でスタックから argc, argv, envp を読み取り、main ポインタを呼び出す
- 言語ごとのランタイムは、グローバルコンストラクタ、スレッドローカルストレージ、例外処理など 言語特有の初期化処理を担う
main() 呼び出し前までの全体の流れ
- 全体の流れは次のように要約できる
execve 呼び出し → カーネルが ELF ファイルをロード
- ELF の解釈 → コード/データセクションをマッピングし、インタープリタを指定
- スタックの構成 → 引数、環境変数、補助ベクトルを格納
- エントリポイント
_start を実行
- ランタイム初期化後に
main() を呼び出す
- この一連の流れは OS カーネル、ELF フォーマット、言語ランタイムの協力構造を示している
- 実際の Linux カーネルにはアドレス空間、プロセステーブル、グループ管理など追加の内部ロジックも含まれるが、本稿はその前段階にある中核的な流れを説明している
結論と訂正
main() 以前の実行過程は カーネルレベルの初期化とランタイム設定の結合体である
- 単純な「Hello, World!」プログラムでさえ、複雑な ELF 構造とランタイム初期化を経て実行される
- 記事の初期版では一部のセクション読み込みロジックをカーネルの役割としていたが、実際には ELF インタープリタの役割であることが訂正された
- この分析は システムプログラミング、コンパイラ、OS アーキテクチャの理解に役立つ基礎資料となる
まだコメントはありません。