いまさら理解する「スタティックリンク」と「ダイナミックリンク」- Webアプリケーションエンジニアのための低レイヤー入門シリーズ その1

最近業務で、ひさびさにCコンパイラ使いました。納品されたモジュールが .so だったためです。みなさん、そんなときどうしますか?Golangでcgoとか使います?

ということで、昔はプログラマーとしては当たり前の情報でしたが、昨今はWebアプリケーションエンジニアは目にすることも少なくなった情報を、改めて書いておきたいと思います!

リンク???

みなさん、「リンク」って聞いたことありますか?いえ、HTMLの<a>タグの話ではありません。もちろん、ゼルダの伝説でおなじみのあの方でもなければ、イナバウワーのステージでもありません。今回は、プログラムが実行ファイルになるまでの「リンク」という工程について、C言語を例にお話しします。文章だとしても。

なぜこの話が必要なのか

「リンクとかよく知らないけど、Go言語でgo buildしたら実行ファイルができる。それで十分じゃない?」

もうおっしゃるとおり!でも、たとえば:

  • Dockerイメージのサイズを小さくしたい
  • ライブラリの更新でアプリを再ビルドせずに済ませたい
  • 「shared library not found」的なエラーに遭遇してもう無理ってなった

こんなとき、リンクの仕組みを知っていると解決の糸口が見えてきます。


まずは全体像:プログラムができるまで

C言語のプログラムが実行ファイルになるまでには、実は複数のステップがあります。

hello.c (ソースコード)
    ↓ コンパイラによるコンパイル
hello.o (オブジェクトファイル)
    ↓ リンカによるリンク
hello (実行ファイル)

Go言語との違い:Goの場合、go buildで一気に実行ファイルになります。内部的には似たようなプロセスを経ていますが、中間ファイル(.oファイル)は通常見えません。Goは開発者体験を重視して、こうした複雑さを隠してくれているんですね。たぶん!


コンパイラとリンカ:役割分担

コンパイラの仕事

コンパイラは、人間が読めるソースコード(.c)を、機械が理解できる機械語に翻訳します。

// hello.c
#include <stdio.h>

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

これをコンパイルすると:

$ gcc -c hello.c -o hello.o

できるもの:hello.o(オブジェクトファイル)

このファイルは機械語になっていますが、まだ実行できません。なぜなら、printfという関数の本体がどこにあるかわからないからです。

#include <stdio.h>

で、どっかしらに printf という関数が、こういう引数でこういう戻り値でこういう型で、というのがわかります。そして、コンパイラはそれをもとに構文が合っているかがチェックできます。しかし、その呼び先の関数の実態がどこなのかはまだ謎です!

Reactでの例え:Reactコンポーネントでimport { useState } from 'react'と書いたけど、まだReactライブラリ本体とつながっていない状態、みたいなイメージです。

リンカの仕事

リンカは、複数のオブジェクトファイルやライブラリを結合して、実行可能なファイルを作ります。

gcc hello.o -o hello

このとき、リンカは「printfの本体はこのライブラリにあるよ」と教えてもらい、それらを全部つなぎ合わせます。

今回は1つの .o ファイルですが、複数の .o ファイルを結合することができます。また、 printf に関しては、いわゆる標準ライブラリ(たまに目にする glibc だと思ってください)に入っているので、Cコンパイラは明示しなくてもどこにリンクすれば良いのか知っているため、引数にそれっぽいものをつけなくても大丈夫です。逆に、標準じゃない系のものは明示する必要があります。


.oファイルとは何か

.o(オブジェクトファイル) は、コンパイルされた機械語のかたまりです。

特徴

  • まだ実行できない中間形式
  • 複数の.cファイルをコンパイルすると、それぞれに対応する.oファイルができる
  • これらを後でリンカが組み合わせる

実例

// math_utils.c
int add(int a, int b) {
    return a + b;
}
// main.c
#include <stdio.h>
int add(int, int); // 関数の宣言

int main() {
    printf("2 + 3 = %d\n", add(2, 3));
    return 0;
}

コンパイル:

gcc -c math_utils.c -o math_utils.o
gcc -c main.c -o main.o

この時点で、main.oには「addっていう関数を使いたい」という情報はあるけど、その実体はまだつながっていません。

リンク:

gcc main.o math_utils.o -o myapp

これで初めて実行ファイルmyappが完成します。

Goとの比較:Goでも複数の.goファイルを書きますが、go buildが自動的にすべてを処理してくれます。中間の.oファイルにあたるファイルは一時的なディレクトリに作られるため普段意識する必要はありません。


スタティックリンクとは

スタティックリンク(静的リンク)は、リンク時にライブラリのコードをすべて実行ファイルに埋め込む方式です。

仕組み

main.o + libmymath.a (静的ライブラリ) 
    ↓ スタティックリンク
myapp (実行ファイル:全部入り)

.a ファイル:複数の.oファイルをまとめたアーカイブ。Windowsでは.libという拡張子になります。

メリット

  1. 配布が簡単:実行ファイル1つで完結するので、他のファイルが不要
  2. 依存関係の心配なし:ライブラリがシステムにインストールされてなくても動く

つまり、Goと同じようにバイナリ1つで動くイメージ!

デメリット

  1. ファイルサイズが大きい:すべてのコードが含まれるため
  2. 更新が大変:ライブラリにバグ修正があっても、アプリを再ビルドしないと反映されない
  3. メモリ効率が悪い:同じライブラリを使う複数のアプリが起動すると、それぞれがコードのコピーをメモリに持つ

Goとの比較:Goのgo buildは基本的にスタティックリンクです!だからGoの実行ファイルは大きめですが、1つのバイナリで完結するので、Dockerイメージに入れるときもFROM scratch (何にも入っていないコンテナから作る) が使えて便利なんです。

FROM scratch
COPY myapp /
CMD ["/myapp"]

※Goでも、バージョンや環境によっては内部でダイナミックリンクすることもありますが、このブログではわかりやすくするために、スタティックリンクしているということにしています


ダイナミックリンクとは

ダイナミックリンク(動的リンク)は、実行時にライブラリを読み込む方式です。

仕組み

main.o + libmymath.so への参照
    ↓ ダイナミックリンク
myapp (実行ファイル:参照情報のみ)

実行時に...
myapp → libmymath.so を探して読み込む

.so ファイル(Shared Object):動的に読み込まれる共有ライブラリ。Windowsでは.dll、macOSでは.dylibという拡張子になります。

メリット

  1. ファイルサイズが小さい:実行ファイルには参照情報だけ
  2. メモリ効率が良い:複数のアプリが同じライブラリを共有できる
  3. 更新が容易:ライブラリだけ差し替えれば、すべてのアプリに反映される

デメリット

  1. 配布が複雑:実行ファイルと一緒に.soファイルも配布が必要
  2. 依存関係の管理:「ライブラリが見つからない」エラーが実行時に初めて起きてしまう
  3. バージョン地獄:ライブラリのバージョン違いで動かないこともあったりする

よく見るエラー

./myapp: error while loading shared libraries: libmymath.so: 
cannot open shared object file: No such file or directory

これは「動的ライブラリが見つからないよ」というエラーです。


実際に試してみよう

スタティックリンクの例

# 静的ライブラリを作る
gcc -c math_utils.c -o math_utils.o
ar rcs libmymath.a math_utils.o

# スタティックリンク
gcc main.c -L. -lmymath -static -o myapp_static

# ファイルサイズを確認
ls -lh myapp_static
# 例:800KB

ダイナミックリンクの例

# 共有ライブラリを作る
gcc -fPIC -shared math_utils.c -o libmymath.so

# ダイナミックリンク
gcc main.c -L. -lmymath -o myapp_dynamic

# ファイルサイズを確認
ls -lh myapp_dynamic
# 例:16KB

# 依存関係を確認
ldd myapp_dynamic
# libmymath.so => ./libmymath.so

サイズの違いに注目してください!


実務でよくあるシーン

Dockerイメージが大きすぎる問題

スタティックリンクされたGoアプリは大きいですが、動的ライブラリ不要なのでFROM scratchが使えます。

一方、動的リンクされたアプリは小さいですが、必要なライブラリをすべてイメージに含める必要があります。

# よくある解決策:Alpine Linuxを使う
FROM alpine:latest
RUN apk add --no-cache libc6-compat
COPY myapp_dynamic /
CMD ["/myapp_dynamic"]

ライブラリの脆弱性対応

スタティックリンク:OpenSSLに脆弱性が見つかった場合、すべてのアプリを再ビルド・再デプロイ

ダイナミックリンク:OpenSSLの.soファイルだけ更新すれば、すべてのアプリに反映(再起動は必要)

システムライブラリとの関係

LinuxやmacOSには、標準Cライブラリ(libc/glibc)など、OSが提供する共有ライブラリがあります。多くのプログラムはこれらを動的にリンクして使います。

# MacでNode.jsの依存関係を見る
otool -L $(which node)

# 複数の.dylibが並ぶはず

まとめ

項目 スタティックリンク ダイナミックリンク
ファイル拡張子 .a (.lib) .so (.dll / .dylib)
実行ファイルサイズ 大きい 小さい
配布の簡単さ ○ 1ファイルで完結 △ ライブラリも配布
メモリ効率 △ 各アプリが個別にコピー ○ 複数アプリで共有
更新の容易さ △ 再ビルドが必要 ○ ライブラリだけ更新
Goでの対応 デフォルト オプション

どちらを選ぶべきか

スタティックリンクが向いている場合

  • シンプルな配布を優先したい
  • Dockerコンテナでscratchベースとかで使う
  • セキュリティのために依存を最小化したい

ダイナミックリンクが向いている場合

  • 複数のアプリで同じライブラリを使う
  • ライブラリの頻繁な更新が必要
  • メモリ使用量を抑えたい

ものすごくざっくり言えば、プログラミングで共有の関数を作るか、自分しか使わない関数を作るか、のようなイメージです!(ちょっと違うけど)


おわりに

Web開発では意識しない「リンク」の世界を楽しんでいただけましたでしょうか?!

普段GoやReactで開発していると、こうした複雑なことを意識しないでも開発できます。でも、その裏側を知ることで、コンピュータの気持ちがわかるようになり、例えばDockerイメージの最適化や、ライブラリエラーのトラブルシューティングがぐっと楽になったりもします。

次に「shared library not found」的なエラーに出会ったら、「ああ、動的リンクで.soファイルが見つからないんだな」と余裕を持って対応できるはずです!

こんな感じで、昨今は学ばなくても特にプログラミングで困らないが、知ってると楽になる系のお話を書いていきたいと思います!最近は、生成AIでプログラム書いてもらったけどどうしても動かない場面、なんかにも有用かもしれません!

こちらからは以上です。