閉じる

プログラムが動くまでの裏側:リンカとローダは何をしているの?

プログラムを書いてコンパイルボタンを押すと、当たり前のように実行ファイルができて動き始めます。

しかし、その裏側ではリンカとローダという2つの重要な「裏方」が、かなりの仕事量をこなしています。

この記事では、コンパイルの後ろ側で何が起きているのか、そしてリンカとローダが具体的に何をしているのかを、できるだけイメージしやすく整理して解説します。

リンカとローダとは何か

コンパイル後に必要になる「見えない裏方」

コンパイラは、ソースコード(.c, .cpp, .rs, .go など)を機械語に翻訳しますが、1回のコンパイルで「すぐに実行できる完成品」ができるとは限りません

多くの場合、コンパイラが出力するのはオブジェクトファイル(.o, .obj)と呼ばれる「部品」です。

この部品たちを組み立てて、OSが読み込める実行ファイルに仕上げるのがリンカ(linker)です。

一方で、その実行ファイルを実際にメモリへ配置し、CPUが動かせる状態にするのがローダ(loader)です。

コンパイラだけでは不十分で、その後ろでリンカとローダが動いて初めてプログラムは「実行」される、という構図を押さえておくことが大切です。

リンカとローダの違いをざっくり理解する

まず、ざっくりと役割を対比してみます。

  • リンカ: ファイルを「作る」側
  • ローダ: そのファイルを「使う」側

もう少し踏み込んで言うと、次のような違いがあります。

  • リンカはビルド時に動きます。コンパイル結果のオブジェクトファイルやライブラリをまとめて、OSが理解できる形式の実行ファイル共有ライブラリを生成します。
  • ローダは実行時に動きます。実行ファイルを開き、メモリ空間を用意して、中身を必要な場所へ読み込み、エントリポイント(main関数など)へジャンプします。

リンカは「完成品のバイナリを製造する人」、ローダは「完成品を倉庫(メモリ)に並べて動かす人」、というイメージでとらえると理解しやすくなります。

リンカの役割

オブジェクトファイルとライブラリをまとめる仕組み

コンパイルによって生成されるオブジェクトファイルは、それぞれがプログラムの一部分だけを持っています。

例えば:

  • main.c → main.o (main関数を含む)
  • util.c → util.o (ユーティリティ関数を含む)

これら単体では、他のファイルにある関数や変数の中身が欠けた状態になっていることが多く、まだ実行できません。

リンカは、これらのオブジェクトファイルや静的ライブラリ(.a, .lib)を集めて、次のような作業を行います。

  1. それぞれのオブジェクトファイルが持つコード・データを1つの実行ファイルにまとめる
  2. 各オブジェクトファイルが参照している関数や変数が、どのファイルのどの位置にあるかを対応付ける
  3. 実行ファイルフォーマット(ELF, PE など)に沿ってセクションやヘッダを組み立てる

[cst-strong]「バラバラなピースを組み立てて1つの完成品にする職人」がリンカ[/cst-strong]だと考えてください。

シンボル解決

リンカの中心となる仕事がシンボル解決(symbol resolution)です。

ここで言う「シンボル」とは、関数名やグローバル変数名などを指します。

例えば、main.c に次のようなコードがあったとします。

C言語
int main() {
    print_hello();
    return 0;
}

コンパイルの時点では、コンパイラは「print_hello という関数がどこかにあるはず」という前提で機械語を出力します。

しかし、main.o の中には print_hello の中身は含まれていません。

多くの場合、print_hello 関数は別のオブジェクトファイルやライブラリに定義されています。

リンカは次のような流れでシンボル解決を行います。

  1. 各オブジェクトファイルが定義しているシンボルの一覧を集める
  2. 各オブジェクトファイルが参照しているが自分では定義していないシンボルの一覧も集める
  3. 参照されているシンボルが、どのオブジェクトファイルやライブラリで定義されているかを照合する
  4. 見つかった場合、そのシンボルのアドレス情報を、参照している側のコードへ埋め込む

もし対応する定義が見つからなければ、undefined reference to print_hello のようなリンカエラーになります。

これは「宣言や使用だけあって定義が見つからない」という意味です。

再配置

シンボル解決と並んで重要なのが再配置(relocation)です。

コンパイル直後のオブジェクトファイル内のアドレスは、まだ仮のものであることが多く、最終的な配置はリンカが決めます。

リンカは、次のような流れで再配置を行います。

  1. 実行ファイル内の、コードセクションやデータセクションの最終的な並び順とサイズを決める
  2. 各シンボルに対して、実行ファイル内での実際のアドレスを割り当てる
  3. オブジェクトファイル中にある「ここには後でアドレスを入れてください」という再配置エントリを、実際のアドレスで書き換える

再配置によって、関数呼び出しのジャンプ先や、グローバル変数へのアクセス先などが、「本物の位置」に更新されます。

これによって、複数のファイルからなるコードが破綻なくつながり、実行可能な1つのプログラムに仕上がります。

静的リンクと動的リンクの違い

リンカの振る舞いで、実行ファイルの性質が大きく変わるのが静的リンク(static linking)動的リンク(dynamic linking)です。

静的リンク

静的リンクでは、必要なライブラリのコードを実行ファイルの中に丸ごと取り込んでしまいます。

  • メリット
    • 実行時に外部ライブラリファイルに依存しないため、配布が簡単
    • ライブラリのバージョン差異による問題が起きにくい
  • デメリット
    • 実行ファイルのサイズが大きくなりやすい
    • ライブラリを更新しても、再リンクし直さない限り反映されない

動的リンク

動的リンクでは、ライブラリを共有オブジェクト(.so, .dll など)として外側に保持し、実行時にロードして利用します。

  • メリット
    • 複数のプログラムでライブラリを共有できるため、システム全体で見たときのメモリ使用量を抑えられる
    • ライブラリのバグ修正やアップデートを、再リンクなしで反映しやすい
  • デメリット
    • 実行時にライブラリのロードやシンボル解決が必要になり、初回起動時のオーバーヘッドが発生する
    • 「ランタイムにライブラリが見つからない」「バージョン違い」など、実行時エラーの原因になりやすい

[cst-strong]静的リンクは「全部入りの弁当」、動的リンクは「必要なときに屋台からおかずを持ってくる」[/cst-strong]イメージと考えると、違いがわかりやすくなります。

ローダの役割

実行ファイルをメモリに読み込む仕組み

リンカが生成した実行ファイルは、ディスク上の単なるバイト列です。

このファイルを実際にメモリへ配置してCPUが実行できる状態にするのがローダ(loader)です。

多くのOSでは、ローダはカーネルの一部、またはカーネルが呼び出す専用のコンポーネントとして実装されています。

ローダは、おおまかに次のステップを踏みます。

  1. 実行ファイルを開き、ヘッダ(ELFヘッダ、PEヘッダなど)を読み取る
  2. ファイル中のどの部分を、メモリ空間のどこにマッピングするかを判断する
  3. コードセグメント、データセグメント、スタックなどをメモリ上に確保・マッピングする
  4. 必要に応じて、書き込み・実行権限などの保護属性を設定する
  5. エントリポイント(プログラムの開始アドレス)を取得し、そこへ制御を移す

[cst-strong]ローダは「ディスク上の設計図(実行ファイル)を、現実の建物(メモリレイアウト)に組み立てる作業」を担っている[/cst-strong]と言えます。

必要なライブラリをロードする流れ

実行ファイルが動的リンクを利用している場合、ローダは動的リンカ(dynamic linker, dynamic loader)と連携して、共有ライブラリを読み込み、シンボルを解決する必要があります。

動的リンク時の代表的な流れは次の通りです。

  1. 実行ファイルのヘッダから、「必要な共有ライブラリの一覧」を取得する
  2. それぞれの共有ライブラリファイル(.so や .dll)をディスクから探し出す
  3. 各ライブラリをメモリへマッピングする
  4. ライブラリ内のシンボル(関数・変数)を解決し、実行ファイル中の参照箇所に実アドレスを紐付ける
  5. ライブラリ側の初期化ルーチンがあれば、それを呼び出す

Unix系OSでは ld-linux.sold.so といったプログラムが動的リンカとして働き、WindowsではPEローダがDLLのロードとシンボル解決を行います。

[cst-strong]「動的リンク」はリンカだけでなくローダ側にも仕事を増やすので、そのぶん起動時の処理が複雑になることを理解しておくと、パフォーマンスチューニングやトラブルシュートに役立ちます。

アドレス空間の用意と初期化処理

ローダは単にファイルをメモリにコピーするだけでなく、実行プロセスのアドレス空間全体を整えなければなりません。

ここには次のような作業が含まれます。

  • スタック領域の確保と初期化
  • ヒープ領域(動的メモリ確保用)のための管理構造のセットアップ
  • 環境変数(PATH など)やコマンドライン引数(argv)の配置
  • セキュリティ機構(ASLR など)に応じたランダムな配置アドレスの決定
  • スレッドローカルストレージ(TLS)のための領域確保
  • ランタイム(言語処理系)の初期化コード呼び出し(C++ のグローバルコンストラクタ呼び出しなど)

プログラム本体の main() が呼ばれるより前に、ローダとランタイムがかなりの仕事を終えていることがわかります。

あなたが main() の中から printf() をすぐに使えるのは、この初期化処理のおかげです。

コンパイルから実行までの一連の流れ

コンパイル→リンカ→ローダの全体像

ここまでの内容を、コンパイルから実行までの時系列で整理してみます。

  1. コンパイル
    1. ソースコードをパースして構文解析
    2. 最適化やコード生成を通じて、オブジェクトファイル(.o)を出力
    3. この時点では、他ファイルのシンボルは未解決のまま
  2. リンカ
    1. 複数のオブジェクトファイルとライブラリを入力として受け取る
    2. シンボル解決と再配置を行い、1つの実行ファイル(あるいは共有ライブラリ)を生成
    3. 静的リンクか動的リンクかに応じて、バイナリの構造も変わる
  3. ローダ
    1. 実行ファイルを読み、メモリ空間を構成
    2. 動的リンクの場合は、共有ライブラリのロードとシンボル解決を実施
    3. 初期化処理を終えたあと、エントリポイント(main など)に制御を渡す

[cst-strong]コンパイラ・リンカ・ローダは、それぞれ独立した役割を持ちながらも、1本のパイプラインとして協調していると理解しておくと、トラブルの原因を切り分けやすくなります。

よくあるエラー(未定義参照など)とリンカの関係

ビルド時に遭遇するエラーの中には、コンパイルエラーではなくリンカエラーであるものが少なくありません。

代表的なのが次のようなメッセージです。

  • undefined reference to func_name
  • unresolved external symbol _func_name (Windows/MSVC 系)

これは、コンパイラはソースコードを機械語に翻訳できたが、リンカがシンボル解決に失敗したことを意味します。

具体的な原因としては、例えば次のようなものがあります。

  • 関数の宣言(extern など)はあるが、定義を含むオブジェクトファイルやライブラリがリンク対象に含まれていない
  • 関数名やシグネチャの不一致(オーバーロードや C と C++ の名前修飾の違いなど)
  • ライブラリのリンク順序の問題(Unix 系では gcc main.o -lmgcc -lm main.o で結果が変わることがある)

こうしたトラブルに直面したときは、「コンパイラの問題なのか、リンカの問題なのか」をまず切り分けることが重要です。

エラーメッセージに undefined referenceunresolved symbol といった単語が含まれていれば、多くの場合はリンカ側の問題です。

実行速度やファイルサイズに影響するポイント

リンカとローダは、プログラムの実行速度やファイルサイズにも直接影響します。

いくつか代表的なポイントを挙げます。

静的リンク vs 動的リンク

  • 静的リンク:
    • 実行ファイルサイズが大きくなりがち
    • 起動時に共有ライブラリを探す必要がないため、起動オーバーヘッドは比較的小さいことが多い
  • 動的リンク:
    • 実行ファイル自体は小さくなる
    • 起動時に共有ライブラリのロードとシンボル解決が必要で、初回起動がわずかに遅くなる可能性がある

使用していないコードの除去(デッドコード削除)

リンカは、設定によって使われていない関数やオブジェクトをバイナリから除外することができます。

これにより、ファイルサイズ削減やキャッシュ効率の向上につながります。

  • 例: GCC/Clang の -ffunction-sections-Wl,--gc-sections の組み合わせなど

ローダによるページング・メモリマッピング

ローダは通常、実行ファイルや共有ライブラリをメモリマップ方式で読み込みます。

これは、ファイルの内容をページ単位でメモリに対応付け、実際にアクセスされた部分から順に読み込む仕組みです。

  • メリット
    • 起動直後から、プログラム全体を読み込む必要がない
    • 未使用のコードやデータは最後までディスク上にとどまり、メモリを消費しない

どの関数がよく使われ、どのライブラリが頻繁にロードされるかといった観点は、実行速度やメモリ使用量の最適化では重要なテーマです。

その背後には、リンカとローダの挙動が密接に関係しています。

まとめ

この記事では、リンカとローダがプログラム実行の裏側でどのように働いているのかを、コンパイルから実行までの流れに沿って解説しました。

  • コンパイラはソースコードをオブジェクトファイルに翻訳するだけであり、リンカがシンボル解決と再配置を行って初めて実行ファイルが完成します。
  • リンカは、複数のオブジェクトファイルとライブラリを結合し、静的リンク・動的リンクのいずれかの方式で1つのバイナリを組み立てます。
  • ローダは、完成した実行ファイルをメモリにマッピングし、必要なら共有ライブラリのロードやシンボル解決、ランタイム初期化を行ったうえで、エントリポイントへ制御を渡します。
  • undefined reference などのリンカエラーは、シンボル解決の失敗に起因しており、どの段階で何が起きているのかを理解していると原因特定が容易になります。
  • 静的リンクか動的リンクか、どのようなリンカオプションを使うかによって、実行ファイルサイズや起動時間、メモリ使用量にも影響が出ます。

普段はあまり意識しないリンカとローダですが、ビルドの仕組みや実行時の挙動を深く理解するうえで避けて通れない存在です。

エラーの意味が読めるようになり、ビルドや実行性能をコントロールできるようになると、プログラムの世界の見え方が一段変わってきます。

ぜひ、自分が使っているコンパイラやツールチェインに合わせて、具体的な挙動やオプションも調べてみてください。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!