静的リンクと動的リンク
連載ナビ
この連載は「Linuxを使えるようになる」ではなく、「Linuxがどう動いているのか」を理解するための実践シリーズです。
今回は Linuxを理解する 〜 LFSで学ぶLinux内部構造 〜 の Part 4 / 第4回として、仕組みのつながりを意識しながら学びます。
- 前回: make installとは何か
- 次回: chrootを理解する
—
静的リンクは「埋め込む」、動的リンクは「実行時に借りる」
プログラムは printf のような機能を自分で実装せず、ライブラリ(Cライブラリなど)から借りて使います。この「借り方」が2通りあり、それが今回のテーマです。結論を先に言うと、
- 静的リンク: ビルド時に、ライブラリのコードを実行ファイルの中へコピーして埋め込む方式。できたバイナリは単体で完結する
- 動的リンク: 実行ファイルには「
libc.so.6が必要」という名前のメモだけを残し、実行のたびに動的リンカ(ld-linux)が共有ライブラリ(.soファイル)をメモリに読み込んで結合する方式
です。Ubuntuのコマンドはほぼすべて動的リンクで、静的リンクは特定の場面(後述)で選ばれます。どちらが優れているかではなく、何を優先するかのトレードオフとして理解するのが正解です。
サイズ・更新・可搬性のどれを取るかが、根本的に違うから
なぜ2方式が併存しているのでしょうか。それぞれの得失がはっきり分かれているからです。
動的リンクの利点(ディストリビューションの標準が動的である理由):
- ディスクとメモリの節約:
libc.so.6は数百のコマンドから共有されます。各バイナリに埋め込んだら、同じコードがディスクにもメモリにも数百部コピーされることになります - 更新が一括で済む: たとえばCライブラリに脆弱性が見つかった場合、動的リンクなら
.soファイルを1つ差し替えれば、それを使う全プログラムが次回起動時から修正版で動きます。静的リンクだと、そのライブラリを埋め込んだ全バイナリを再ビルド・再配布しなければなりません
静的リンクの利点:
- 自己完結: 実行環境に
.soがなくても動く。Part 1で見たerror while loading shared librariesは原理的に起きません - 可搬性: ビルドした1ファイルをコピーするだけで、ライブラリ構成の違う環境でも動かせます
この違いは実行の瞬間にも現れます。動的リンクされたバイナリを起動すると、カーネルはまずバイナリに記録されたインタープリタ(例: /lib64/ld-linux-x86-64.so.2)を起動し、それが必要な .so を検索・ロードしてから本体の処理が始まります。静的バイナリにはこの工程自体がありません。
gccの1オプションで、2つの世界を作り比べる
同じソースから両方式のバイナリを作って比較します。Linux環境(Part 1で作った仮想環境)で試してください。
cat > hello.c << 'EOF'
#include <stdio.h>
int main(void) { printf("hello, link\n"); return 0; }
EOF
gcc -o hello_dynamic hello.c # 既定 = 動的リンク
gcc -static -o hello_static hello.c # 静的リンク
ls -lh hello_dynamic hello_static
サイズの差は一目瞭然で、動的リンク版が十数KB程度なのに対し、静的リンク版はCライブラリを丸ごと抱え込むため数百KB以上になります。次に、それぞれが何に依存しているかを ldd(バイナリが必要とする共有ライブラリの一覧を表示するコマンド)で確認します。
ldd hello_dynamic
# linux-vdso.so.1 (...)
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (...)
# /lib64/ld-linux-x86-64.so.2 (...)
ldd hello_static
# not a dynamic executable
動的版には「必要なライブラリ名 => 実際に見つかった場所」の対応が並び、静的版は not a dynamic executable(動的実行ファイルではない)と返ってきます。file hello_dynamic / file hello_static を実行すると、それぞれ dynamically linked (uses shared libs) / statically linked と表示され、バイナリ自体に方式が刻まれていることも確認できます。
DockerとAlpineの「あのトラブル」は、リンク方式の話だった
この知識は実務のあちこちに顔を出します。代表例を2つ挙げます。
1. Goバイナリとscratchイメージ。
Goはcgo(C連携)を使わなければ静的リンクに近い自己完結バイナリを生成できるため、OSファイルを一切含まない FROM scratch のDockerイメージにバイナリ1つを置くだけでコンテナが動きます。「なぜGoのコンテナはあんなに小さくできるのか」の答えは、リンク方式にあったわけです。
2. Alpineで動かないglibcバイナリ。
Ubuntu上でビルドした動的リンクバイナリをAlpineコンテナに持ち込むと、起動に失敗することがあります。原因は、バイナリが要求する動的リンカ /lib64/ld-linux-x86-64.so.2(glibcの一部)がAlpine(Cライブラリはmusl)に存在しないためです。Part 1の連載で「glibc前提のバイナリはAlpineでそのまま動かないことがある」と書いた正体がこれで、ldd と file で依存を確認すれば原因まで自力でたどり着けます。
またLFSでも、リンク方式は主役級の登場をします。ビルド途中の隔離環境では「ホストのライブラリに依存しない道具」が必要になる場面があり、そこで自己完結な静的リンクの性質が効いてきます。
「埋め込むか、借りるか」を意識すると、バイナリの見え方が変わる
静的リンクはライブラリを埋め込んで自己完結、動的リンクは実行時に .so を借りてサイズと更新性を稼ぐ——これが今回の結論です。手元のバイナリがどちらか迷ったら file と ldd、この2つで即答できます。
そして次回はいよいよPart 4の締めくくり、chroot です。動的リンクの知識はそこで直ちに必要になります。chrootした先の世界に .so がなければ、シェルすら起動できない——その理由と対処を、実験しながら確かめます。
図解テキスト
静的リンク: binaryにライブラリを埋め込む
[hello_static] = 自分のコード + libcのコピー
→ 単体で動く / サイズ大 / 更新は再ビルド
動的リンク: 実行時に.soを読み込む
[hello_dynamic] = 自分のコード + 「libc.so.6が必要」というメモ
→ 起動時に ld-linux が .so を検索してロード
→ サイズ小 / .so差し替えで一括更新 / .soが無いと起動不能
この回で実行するコマンド
上から順番に実行すれば、この回の内容を体験できます。
まずはそのままコピペして実行し、次に1行ずつ意味を確認してください。
printf '#include <stdio.h>\nint main(void){ printf("hello, link\\n"); return 0; }\n' > hello.c
gcc -o hello_dynamic hello.c
gcc -static -o hello_static hello.c
ls -lh hello_dynamic hello_static
ldd hello_dynamic
ldd hello_static
file hello_dynamic hello_static
ldd /bin/ls
ハンズオン手順
1. ターミナルを開いて、上のコマンドを1行ずつ実行する
2. 2つのバイナリのサイズを比べ、差の分だけ何が埋め込まれたのか説明してみる
3. ldd /bin/ls の出力から、ls が依存するライブラリを2つ挙げる
4. gcc -static が cannot find -lc 系のエラーで失敗したら、静的リンク用ライブラリが未導入のサイン(Ubuntu系なら sudo apt install libc6-dev 済みか確認)。エラーメッセージをそのまま残す
チェックポイント
- コマンドが最後まで実行できた
lddの出力の=>の左右がそれぞれ何を意味するか説明できる- 「静的リンクの利点」と「動的リンクの利点」を1つずつ言える
- 次の回に進む前に、分からない単語を1つ調べた
今回理解できたこと
- 動的リンクは更新しやすい(.so差し替えで全プログラムに反映)が、実行環境に.soが必須
- 静的リンクは自己完結しやすいが、サイズが大きく更新は再ビルドが必要
- 方式の見分けは
file(statically/dynamically linked)とlddで即確認できる - Goのscratchイメージ、Alpineでのglibcバイナリ問題は、どちらもリンク方式の話