概要
- Linuxでプログラムが実行される流れの詳細解説
- execveシステムコールからELFファイルの構造説明
- スタックやELF補助ベクタ(auxv)の初期化手順
- エントリポイントと_start関数の役割
- 各言語ランタイムの初期化の違い
Linuxカーネルによるプログラム実行の流れ
- プログラム実行時、Linuxでは execveシステムコール が呼ばれる
- int execve(const char *filename, char *const argv[], char *const envp[])形式
- 引数は実行ファイル名・引数リスト・環境変数リスト
- 多くのプログラミング言語は標準ライブラリでexecveをラップ
- 例: Rustのstd::process::Command
- シェルと同様にPATH解決も行う
- カーネルは 絶対パスの実行ファイル を期待
- シェバン(#!)が先頭にある場合は、指定インタプリタで実行
- 例: #!/usr/bin/python3, #!/bin/bash
ELFファイルの基本構造
- Linuxの実行ファイル形式は ELF (Executable and Linkable Format)
- 他OSはMach-O(MacOS), PE(Windows)など
- ELFファイルのヘッダには マジックバイト(7f 45 4c 46) や エントリポイントアドレス などが記載
- readelfコマンドでヘッダ情報を確認可能
- ELFはa.out形式から発展し、ほぼ全てのプログラムに対応
ELFの各セクションと役割
- ELFファイルには複数のセクションが含まれる
- .text: プログラム本体のコード
- .data: 初期化済みデータ
- .bss: 未初期化グローバル変数用領域
- .plt: 共有ライブラリ関数呼び出し用
- .rodata: 読み取り専用データ
- .symtab, .strtab: シンボル・文字列テーブル
- 動的リンク用の情報も格納
- PT_INTERPセクションでELFインタプリタを指定
- libc(C標準ライブラリ)などの共有ライブラリのロード指示
シンボルテーブルと実態
- シンボルテーブルには多数のエントリが存在
- 例: Hello Worldプログラムでも2000以上のシンボル
- main関数や_start、__libc_start_mainなどが含まれる
- 多くのシンボルはリンクやデバッグ、動的リンク用
- muslやglibcなどlibcの種類で内容が異なる
カーネルによるロード処理
- カーネルはELF内の ロード可能セクション をメモリに配置
- PT_INTERP指定があればインタプリタ経由で処理
- ASLR(アドレス空間配置ランダム化)やNXビット(非実行属性)などのセキュリティ機能も適用
- スタックを初期化し、エントリポイントへジャンプ
スタックの初期化
- スタックは 高アドレス側から低アドレス側へ成長
- ヒープや共有ライブラリ、mmap領域との間の空間管理
- execveで渡されたargv, envpはスタック上に配置
- ELF補助ベクタ(auxv) もスタックに格納
- ページサイズやエントリポイントなどのシステム情報
- スタック初期化の擬似コード例(RISC-Vエミュレータより)
エントリポイントと_start関数
- ELFヘッダのエントリポイントアドレスから実行開始
- 通常は _start関数 が最初に呼ばれる
- glibcやmuslが提供、独自実装も可能
- _startから各言語のランタイム初期化処理へ
- 例: Rustならstd::rt::lang_start、C/C++も独自ランタイムあり
言語ごとの初期化の違い
-
_startからmainまでの間にグローバルコンストラクタやスレッドローカルストレージなどの処理
-
Rustの例
- main関数はユーザー定義
- _start関数でargc, argv, envp取得後、lang_startに渡す
-
C/C++も同様に最小限の初期化後mainを呼び出す
- 主要言語は独自のランタイムを持ち、main関数実行前に各種セットアップを行う仕組み
この一連の流れにより、カーネルからmain関数実行までの複雑な処理が抽象化・自動化されている仕組み。