目次


はじめに

以下のようなシンプルなCプログラムを例にとってみましょう:

#include <stdio.h>

int main(void) {
    printf("Hello, world!\n");
    return 0;
}

このプログラムを gcc hello.c -o hello のようにコンパイルすると、表面上は main() 関数がプロセスの最初に実行されるように思えます。しかし実際には、プログラムが起動して環境を整える段階で、main() の前後に実行される特別なコード群が存在します。これらのコードはCランタイム(CRT)のスタートアップオブジェクトに含まれており、crt0.ocrt1.ocrti.ocrtn.o といったファイル名で見かけることがあります。本記事では、これらのファイルがどのような役割を持ち、なぜ存在するのか、そしてC(およびC++)プログラムがスムーズに動作するためにどのように連携しているのかを解説します。

Cランタイムとは何か

Cランタイム(CRT)は、プログラムが実行されるために必要な初期化ルーチン、ライブラリのサポートコード、システムコールのラッパなどをまとめた仕組みです。これらの多くはユーザーが書くソースコードの外側にあり、コンパイラ(たとえば gccclang)が自動的にリンクしてくれます。

以下のコマンドのように、プログラムをコンパイルした場合:

gcc main.c -o main

または

clang main.c -o main

コンパイラドライバとリンカは、CRTのオブジェクトファイルやライブラリを暗黙的に含めます。これらのファイルには、アセンブリレベルのエントリポイントや以下のようなルーチンが含まれています。

  1. レジスタやスタックの初期化
  2. プログラム引数(argcargvenvp)のセットアップ
  3. (C++ の場合)グローバルコンストラクタの呼び出し
  4. ユーザーが書いた main() 関数の呼び出し
  5. main() 関数の返り値をOSに渡して終了させる処理

crt0.o(あるいはモダンツールチェーンにおける crt1.o)の役割

従来、crt0.o(Cランタイム“ゼロ”)はプログラムの実際のエントリポイント(しばしば _start と呼ばれるルーチン)を含む小さなオブジェクトファイルとして存在していました。その主な役割は次のとおりです。

  1. プログラムの初期化

    • (必要に応じて)スタックの初期化
    • メモリセグメント(データセクションやBSS領域など)のセットアップ
    • カーネルから受け取る argcargv、環境変数などの受け渡し準備
    • グローバル・スタティックオブジェクト(特にC++で重要)のコンストラクタ呼び出し
    • 標準ライブラリの初期化(標準I/Oなど)
  2. main() への制御移譲

    • 環境が整った後、crt0.omain(argc, argv, envp) を呼び出します。
  3. 終了処理

    • main() がリターンすると、crt0.o あるいは最後の終了ルーチンがOS特有のシステムコール(例:_exit など)を呼び出して、戻り値をプロセスの終了コードとして渡します。

crt0.o は一枚岩的なファイルであったため、近年のツールチェーンではよりモジュール化が進み、crt1.o という別の名前のファイルが使われることが多くなりました。名前は違っても、本質的な役割は同じで、リンカによってデフォルトのエントリポイント _start が組み込まれます。

crt0.o / crt1.o の典型的な内容

  • ランタイム初期化のための低レベルアセンブリコード
  • リンカによって参照される _start(または __start)シンボル
  • main() (または _main)を呼び出すルーチン

リンクフェーズ

リンク時には、Cライブラリ(例:glibc、musl)やコンパイラツールチェーンから crt0.o(または crt1.o)が自動的にリンクされます。-nostartfiles のような特殊なフラグを使わない限り、ユーザーが意識しなくても常に含まれます。

追加のランタイムファイル:crti.ocrtn.o など

近年のツールチェーンでは、Cランタイムが以下のように複数のオブジェクトファイルに分割されることが一般的です。

  • crti.o(Cランタイム初期化)
  • crtn.o(Cランタイム終了処理)
  • crt1.o(Cランタイムのエントリポイント)

crti.o: Cランタイム初期化

crti.o には、ランタイム初期化における“プロローグ”部分が含まれます。主なタスクは次のとおりです。

  • プラットフォーム固有の初期化
    特殊レジスタやCPU機能の初期化、あるいはアーキテクチャ依存の設定など
  • 環境の準備
    C++ であれば .ctors セクションのコンストラクタ呼び出しを行うための下準備
  • 早期セットアップのフック
    OSやプラットフォームに特有の初期ルーチン(例:スレッドローカルストレージ(TLS)の準備)

crti.o は「これからランタイムを始動するので、まずはプロローグを実行する」というイメージです。最終的には main() など実行時の本体へ制御が移ります。

crtn.o: Cランタイム終了処理

crtn.o には、初期化の“エピローグ”部分と終了時の後処理が含まれます。具体的には次のような作業を行います。

  • 初期化シーケンスの完了
    crti.o が開始した初期化を完結させ、すべてのグローバルコンストラクタが呼び出されたことを保証
  • デストラクタの管理
    C++ の場合、グローバルデストラクタ(.dtors セクション)がプログラム終了時に正しく呼ばれるようにする

プログラムが終了するとき、グローバルオブジェクトのデストラクタを呼び出し、必要なリソースがクリーンアップされてから実際に終了(OSに制御が戻る)します。

全体的な流れ

これらのファイルがどのように連携してプログラムを開始・終了させるのかを、簡単なフローチャートに示します。

        ┌─────────────────────┐
        │ Program Entry Point │  (Defined in crt1.o or crt0.o)
        │     _start()        │
        └──────────┬──────────┘
                   │
                   │ (1) Initialize environment, memory, etc.
                   │
        ┌──────────┴──────────┐
        │   crti.o (Prologue) │
        │  Calls constructors │
        └──────────┬──────────┘
                   │
                   │ (2) Jump to main()
                   │
        ┌──────────┴──────────┐
        │        main()       │
        └──────────┬──────────┘
                   │
                   │ (3) main returns
                   │
        ┌──────────┴──────────┐
        │   crtn.o (Epilogue) │
        │  Calls destructors  │
        └──────────┬──────────┘
                   │
                   │ (4) exit syscall
                   │
             ┌─────┴──────┐
             │   OS Exit  │
             └────────────┘

ポイント:

  1. _startcrt0.o または crt1.o に含まれる)が低レベルの初期化を行い、その後 crti.o のプロローグコードを呼び出します。
  2. crti.o で初期化が終了すると、main() へジャンプします。
  3. main() が終了すると、crtn.o のエピローグが呼び出され、グローバルデストラクタなどが処理されます。
  4. 最後に、システムコールによってプロセスを終了させ、main() のリターン値をOSに返却します。

アセンブリコードの例

以下は、Linux x86-64 における _start ルーチンの最簡易的な例です(実際の crt1.ocrt0.o はもっと複雑です)。本番環境では、環境変数の管理やスレッドローカルストレージなど、より多くの仕組みが含まれます。

    .global _start
_start:
    ; The stack pointer is already set by the OS.
    ; Registers RDI, RSI, and RDX might have pointers to argc, argv, and envp.

    ; Save argc, argv, and envp to the stack, or
    ; pass them to main() directly (depending on calling convention).
    mov rdi, [rsp]               ; argc is at top of stack
    lea rsi, [rsp+8]             ; argv pointer just after argc
    ; envp would be after argv, etc.

    call main                    ; Call main(argc, argv, envp implicitly)

    ; Make an exit system call
    mov rax, 60                  ; sys_exit on Linux x86-64
    syscall

続いて、非常にシンプルな crti.o の例(C++向け擬似コード/アセンブリ)を示します:

    .section .init
    _init:
        ; グローバルコンストラクタを初期化するなど、
        ; ランタイムが main を呼び出す前に必要な処理を実行。
        ; 例えば __libc_init_array を呼び出す場合もある。
        ret

さらに、crtn.o は対になる以下の例を持ちます:

    .section .fini
    _fini:
        ; 終了時のクリーンアップとグローバルデストラクタの呼び出しを行う。
        ; 例えば __libc_fini_array を呼び出す場合もある。
        ret

実際のツールチェーンでは、GNUリンカスクリプトや .init_array / .fini_array、あるいは .ctors / .dtors を利用し、main() の前後でこれらのコードを自動的に実行します。

モダンな使用法における注意点

  • 静的リンクと動的リンク
    -static フラグを用いて静的リンクを行うと、CRTのファイルは最終的なバイナリにすべて含まれます。動的リンクする場合は、これらのCRTオブジェクトが動的ローダとの連携を担当し、必要なライブラリをロードした後に main() を呼び出します。

  • OSによる実装差
    Linux(glibc や musl)では crt1.ocrti.ocrtn.o などがよく見られますが、BSD系やmacOSなどでは名称やプロセスが異なる場合があります。

  • C++ におけるコンストラクタ/デストラクタ
    .ctors.dtors、あるいは .init_array.fini_array は、グローバルオブジェクトのコンストラクタとデストラクタを自動的に呼び出す仕組みです。crti.ocrtn.o はこれらの呼び出しをラップして、プログラム開始前と終了時に正しく処理させます。

  • カスタムエントリポイント
    -nostdlib-nostartfiles などを使って標準CRTをリンクせずに独自のエントリポイントを用意することも可能です(組込みシステムやブートローダなど)。

結論

Cランタイム(CRT)は、CやC++プログラムを起動するうえで非常に重要でありながら、普段はあまり意識されない部分です。crt0.o(あるいは crt1.ocrti.ocrtn.o)は、main() が実行される前にスタックやグローバルコンストラクタ、ライブラリなどのセットアップを行い、実行が終了したあともデストラクタなどを呼び出して最後の後始末をします。コンパイラが自動的にリンクしてくれるためユーザーは通常意識しませんが、この仕組みを知ることで、C/C++アプリケーションがどのように動作を開始し、どのように終了へ至るのかをより深く理解することができます。

組込み開発やコンパイラの開発を行う場合はもちろん、Cの仕組みについて学びたいという方にとっても、Cランタイムの概要を把握しておくことは大きな意味を持ちます。main() 関数の“見えない裏側”を理解することで、プログラムの起動プロセスや終了プロセスの本質を明らかにできるでしょう。

参考文献


このブログ記事の議論に参加してください:

x
discord
telegram

この記事は AI 搭載の翻訳ツールによって翻訳されました。翻訳に誤りがある場合はご容赦ください。すぐに校正し、考えられる誤りを修正します。誤りを見つけた場合は、GitHub で問題を作成してください。