Glibcとは何か:Linuxアプリとカーネルを繋ぐ、縁の下の力持ち
連載ナビ
この連載は「Linuxを使えるようになる」ではなく、「Linuxがどう動いているのか」を理解するための実践シリーズです。
今回は Linuxを理解する 〜 LFSで学ぶLinux内部構造 〜 の Part 3 / 第4回として、仕組みのつながりを意識しながら学びます。
- 前回: gccとは何か
- 次回: binutilsとは何か
—
Glibcは「標準C関数の実装」であり「カーネルへの窓口」
Glibc(The GNU C Library)は、Linux上のほぼすべてのプログラムが実行時に依存しているライブラリです。実体はおもに libc.so.6 という共有ライブラリファイルで、Ubuntuなら /lib/x86_64-linux-gnu/libc.so.6(Arm64環境なら /lib/aarch64-linux-gnu/libc.so.6)に置かれています。
結論から言うと、Glibcの役割は2つです。
1. 標準C関数の実装を提供する — printf、malloc、strcpy といったC標準の関数や、POSIXで定められたAPIの実体はすべてGlibcの中にあります。前回、gcc がリンク段階で puts の実体を結合していましたが、その実体の提供元がGlibcです。
2. カーネルへの窓口(システムコールラッパー)になる — ファイル操作やメモリ確保など、カーネルにしかできない処理を、プログラマが扱いやすいC関数の形に包んで提供します。
さらにGlibcパッケージには、Part 1で登場した動的リンカ(/lib64/ld-linux-x86-64.so.2 など)も含まれています。プログラムの起動時に必要な共有ライブラリを探して読み込むのもGlibcの仕事の一部、ということです。
生のシステムコールを直接扱うのは現実的ではないから
なぜ「窓口」が必要なのでしょうか。Part 2で学んだとおり、ユーザー空間のプロセスはカーネルにシステムコールで依頼しないと、ファイルにもネットワークにも触れません。しかし生のシステムコールは、
- 番号で指定する(たとえばx86-64では
openatは257番、といった具合にアーキテクチャごとに異なる) - 引数をCPUのレジスタに規定どおり積んで、専用の命令でカーネルに切り替える
- 失敗時は負の値が返るだけで、
errnoへの変換も自分でやる必要がある
という、アセンブリ言語レベルの低水準なインターフェースです。これを毎回書くのは非現実的ですし、アーキテクチャを変えたら書き直しになります。
そこでGlibcが、システムコールを普通のC関数として包んで提供します。プログラマは open() や read() を呼ぶだけでよく、レジスタの作法もアーキテクチャの違いもGlibcが吸収します。さらに fopen() のような高水準の関数は、システムコールの回数を減らすためのバッファリングまで面倒を見てくれます。アプリケーションが安定して動き、別のマシンでも同じソースが動くのは、この安定したインターフェースがあるからです。
fopen 1回の裏側を strace で観察する
「GlibcのC関数が、裏でシステムコールに変換されている」ことは、Part 1でも使った strace で実際に観察できます。
# /etc/hostname を読んで表示するだけのプログラム
cat > /tmp/readfile.c <<'EOF'
#include <stdio.h>
int main(void) {
char buf[256];
FILE *fp = fopen("/etc/hostname", "r");
if (fp == NULL) { perror("fopen"); return 1; }
if (fgets(buf, sizeof(buf), fp) != NULL) printf("%s", buf);
fclose(fp);
return 0;
}
EOF
gcc -o /tmp/readfile /tmp/readfile.c
# ファイル操作系のシステムコールだけを追跡する
strace -e trace=openat,read,write,close /tmp/readfile
出力の終盤に、おおよそ次のような行が並びます。
openat(AT_FDCWD, "/etc/hostname", O_RDONLY) = 3
read(3, "myhost\n", 4096) = 7
write(1, "myhost\n", 7) = 7
close(3) = 0
ソースコードと見比べてみてください。
fopen("/etc/hostname", "r")→openatシステムコールに変換され、ファイルディスクリプタ3が返っているfgets(buf, 256, fp)→readに変換。しかも256バイトではなく4096バイト読もうとしている。これがGlibcのバッファリングで、「まとめて読んでおいて、以降のfgetsはメモリ上のバッファから返す」ことでシステムコールの回数を減らしていますprintf→ 最終的にwrite(第1引数の1は標準出力)に変換されている
C関数とシステムコールは1対1ではなく、Glibcが間で最適化や変換を行っている——それがこの数行から読み取れます。
GLIBC_2.34' not found — すべてが依存しているがゆえのエラー
Glibcが「ほぼすべての依存の要」であることは、トラブルの形にも現れます。DockerやCI/CDを触る人が実際に遭遇しやすいのが、次のエラーです。
./myapp: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by ./myapp)
これは、新しいGlibcを持つ環境でビルドしたバイナリを、古いGlibcしかない環境に持ち込んで実行したときに出ます。Glibcは後方互換(古いバイナリを新しいGlibcで動かす)は維持しますが、その逆は保証されません。バイナリには「必要なGlibcのバージョン」が記録されており、ldd --version で環境側のバージョンと突き合わせれば原因を特定できます。
もう1つの典型例がAlpine Linuxです。Part 1で見たとおり、AlpineのCライブラリはGlibcではなく musl です。そのため、Glibc前提でビルドしたバイナリを FROM alpine のイメージに COPY しても、そのままでは動きません(ldd で見ると要求する libc.so.6 が存在しない)。「コンテナを軽くしたくてAlpineにしたら動かない」問題の正体は、多くの場合このCライブラリの違いです。
Glibcを制する者は、Linuxの互換性を制する
Glibcは、標準C関数の実装とシステムコールラッパーという2つの役割で、アプリケーションとカーネルの間に立つライブラリです。strace を使えばC関数がシステムコールに変換される瞬間を観察でき、ldd を使えばプログラムのGlibc依存を確認できます。そして「どの環境でビルドしたバイナリが、どの環境で動くか」というポータビリティ問題の多くは、Glibcのバージョンと種類(Glibcかmuslか)に帰着します。
LFSでは、このGlibcをソースからビルドして配置します。ビルド直後に動的リンカの動作確認を行う手順があるのは、Glibcが壊れているとその後の全プログラムが動かないからです。次回はツールチェーンの最後の1つ、リンクと解析を担う「Binutils」を見ていきます。
コマンドでGlibcの存在を確認する
あなたのシステムで、ほぼすべてのコマンドがGlibcに依存していることを確認してみましょう。
# Glibcのバージョンを確認
ldd --version
# lsコマンドが依存する共有ライブラリを表示
# libc.so.6 がGlibc本体。動的リンカ(ld-linux)も表示される
ldd /bin/ls
# bashシェル自身も同じくGlibcに依存している
ldd /bin/bash
# libc.so.6 の実体ファイルを確認
ls -l /lib/*/libc.so.6 2>/dev/null || ls -l /lib64/libc.so.6
チェックポイント
- Glibcの2つの役割(標準C関数の実装、システムコールラッパー)を説明できる
straceの出力で、fopenがopenatに、printfがwriteに変換されていることを確認したfgets(buf, 256, fp)なのにreadが4096バイト要求する理由(バッファリング)を説明できるversion GLIBC_x.xx not foundエラーの原因(ビルド環境と実行環境のGlibcバージョン差)を説明できる
今回理解できたこと
- Glibcは標準C関数の実装であると同時に、ユーザー空間とカーネル空間を繋ぐシステムコールラッパーであること
- C関数とシステムコールは1対1ではなく、Glibcがバッファリングなどの最適化を挟んでいること
- Linux上のほとんどのプログラムが
libc.so.6に依存しており、Glibcが互換性の要になっていること - バイナリのポータビリティ問題(GLIBCバージョンエラー、Alpine/muslで動かない問題)はCライブラリの違いに帰着すること