Binutilsとは何か:実行ファイルを組み立て、仕上げる職人集団
連載ナビ
この連載は「Linuxを使えるようになる」ではなく、「Linuxがどう動いているのか」を理解するための実践シリーズです。
今回は Linuxを理解する 〜 LFSで学ぶLinux内部構造 〜 の Part 3 / 第5回として、仕組みのつながりを意識しながら学びます。
- 前回: glibcとは何か
- 次回: ./configureとは何か
—
Binutilsは、アセンブラ・リンカ・バイナリ解析ツールの詰め合わせ
結論から言うと、Binutils(GNU Binary Utilities)とは、バイナリファイル(機械語のファイル)を生成・結合・解析・加工するためのコマンド群をまとめたパッケージです。ツールチェーン3点セットの最後の1つで、主なコマンドは次のとおりです。
| コマンド | 役割 |
as |
アセンブラ。アセンブリコードを機械語のオブジェクトファイルに変換する |
ld |
リンカ。オブジェクトファイルとライブラリを結合し、実行可能ファイルを作る |
nm |
オブジェクトファイルや実行ファイルのシンボル(関数名・変数名)を一覧表示する |
objdump |
バイナリを逆アセンブルするなど、内容を人間が読める形で表示する |
readelf |
ELFファイルのヘッダやセクション構造を表示する |
strip |
デバッグ情報やシンボルテーブルを削除してファイルを小さくする |
ar |
複数のオブジェクトファイルを1つの静的ライブラリ(.a)にまとめる |
as と ld は普段GCCの裏で自動的に呼ばれるため目立ちませんが、これらがなければ実行ファイルは1つも完成しません。
コンパイラだけでは、実行できるファイルは1つも作れないから
なぜ独立したパッケージとしてBinutilsが必要なのか。理由は、GCC(cc1)の最終出力がアセンブリコードというテキストファイルであり、そこから先の工程がまるごとBinutilsの担当だからです。
- アセンブル(
as): アセンブリコードを機械語に変換し、ELF形式のオブジェクトファイル(.o)を作ります。ただしこの時点では、関数や変数のアドレスは未確定です。 - リンク(
ld): 複数のオブジェクトファイルとライブラリを受け取り、(1) 同じ種類のセクション(機械語本体の.text、初期化済みデータの.dataなど)を統合し、(2) 「名前は分かるが場所が分からない」シンボル参照を実際のアドレスに解決し、(3) プログラムの開始地点(エントリポイント)や、実行時に使う動的リンカのパスを書き込んで、カーネルがexecveでロードできる完成品に仕上げます。
つまり、C言語の世界(GCC)と、カーネルがロードして実行するELFバイナリの世界との間を埋めているのがBinutilsです。この「シンボル解決」の仕組みを知っていると、undefined reference エラーがなぜ起き、どう直すのかが原理から理解できます。
2つのファイルをリンクして「シンボル解決」を目で見る
リンカの中心的な仕事であるシンボル解決を、nm コマンドで観察してみます。関数 add を定義するファイルと、それを呼び出すファイルを分けて作ります。
cat > /tmp/helper.c <<'EOF'
int add(int a, int b) { return a + b; }
EOF
cat > /tmp/main.c <<'EOF'
#include <stdio.h>
int add(int a, int b);
int main(void) { printf("%d\n", add(2, 3)); return 0; }
EOF
# それぞれをオブジェクトファイルまでコンパイル(リンクはまだ)
gcc -c -o /tmp/main.o /tmp/main.c
gcc -c -o /tmp/helper.o /tmp/helper.c
# main.o のシンボルを確認
nm /tmp/main.o
nm /tmp/main.o の出力はおおよそ次のようになります。
U add
0000000000000000 T main
U printf
U(Undefined)は「使っているが実体がここにない」印です。main.o は add と printf の実体を知りません。ここでリンクを実行します。
gcc /tmp/main.o /tmp/helper.o -o /tmp/calc
/tmp/calc # → 5 と表示される
nm /tmp/calc | grep -w -e add -e main
今度は add にも T(Text = 機械語本体がある)が付き、実際のアドレスが割り当てられています。リンカ ld が helper.o の中に add の実体を見つけ、main.o 側の「宛先不明」の呼び出しを解決したのです。なお printf の実体はGlibc(libc.so.6)にあるため、実行時に動的リンカが解決する形で記録されます。
試しに helper.o を渡さずリンクすると、前々回見たエラーがそのまま再現します。
gcc /tmp/main.o -o /tmp/calc
# /usr/bin/ld: /tmp/main.o: in function `main':
# main.c:(.text+0x...): undefined reference to `add'
# collect2: error: ld returned 1 exit status
undefined reference とは「U のまま最後まで解決できなかったシンボルがある」という、リンカからの報告だったわけです。
readelf・objdump・strip で完成品を解析・加工する
Binutilsには、できあがったバイナリを調べる・削るためのツールも含まれています。
# ELFヘッダを表示する。先頭のMagic: 7f 45 4c 46 は「ELFファイルの目印」
# (45 4c 46 はASCIIで 'E' 'L' 'F')
readelf -h /tmp/calc
# add関数の機械語を逆アセンブルして表示する
objdump -d /tmp/calc | grep -A 8 '<add>:'
# シンボルテーブルを削除してファイルを小さくする
ls -l /tmp/calc
strip /tmp/calc
ls -l /tmp/calc # サイズが減っている
nm /tmp/calc # → no symbols (シンボル情報が消えた)
readelf -h はファイルの素性(64bitか、対象アーキテクチャはx86-64かAArch64か、実行形式か共有ライブラリか)を教えてくれます。objdump -d を使えば、自分の書いた return a + b; がどんなCPU命令になったかまで確認できます。strip は配布用バイナリの軽量化によく使われますが、シンボルが消えるとデバッガで関数名が見えなくなるため、「本番用はstrip、デバッグ用は残す」という使い分けが一般的です。障害調査で「このバイナリ、stripされててスタックトレースに関数名が出ない」という場面に出会ったとき、この知識がそのまま効きます。
リンクを制するBinutilsが、ツールチェーンの最初に来る
Binutilsは、アセンブラ as・リンカ ld と、nm・readelf・objdump・strip などの解析・加工ツールをまとめたパッケージです。リンカの本質は「セクションの統合とシンボル解決」であり、nm で U が T に変わる瞬間を見れば、undefined reference エラーの正体まで原理から説明できます。
そしてPart 3全体を振り返ると、LFSがツールチェーンを Binutils → GCC → Glibc の順にビルドする理由も見えてきます。GCCのビルドには as と ld が必要で、Glibcのビルドには完成したコンパイラが必要——依存の根っこにあるのがBinutilsなのです。次回からはPart 4に入り、LFSで何十回も実行することになる ./configure && make && make install の1行目、「configureとは何か」を解剖します。
コマンドでBinutilsの仕事を確認する
# 簡単なCプログラムをオブジェクトファイルとしてコンパイル
cat > /tmp/hello.c <<'EOF'
#include <stdio.h>
int main(void){ puts("hello, world"); return 0; }
EOF
gcc -c -o /tmp/hello.o /tmp/hello.c
# 1. アセンブラとリンカの場所とバージョンを確認(どちらもBinutils)
which as ld
ld --version | head -n 1
# 2. オブジェクトファイルのシンボルを確認(U puts に注目)
nm /tmp/hello.o
# 3. /bin/ls のELFヘッダを表示
readelf -h /bin/ls | head -n 10
# 4. オブジェクトファイルの概要と逆アセンブル結果を表示
objdump -f /tmp/hello.o
objdump -d /tmp/hello.o | head -n 20
チェックポイント
- Binutilsが、
as・ldと各種バイナリ解析ツールをまとめたパッケージであることを説明できる - リンカ
ldの仕事を「セクションの統合とシンボル解決」として説明できる nmの出力で、リンク前のU addがリンク後にT addへ変わることを確認したreadelf -hでELFファイルの素性を、objdump -dで機械語の中身を確認できる
今回理解できたこと
- Binutilsは、アセンブル・リンクというビルドの最終工程と、バイナリの解析・加工を担うツール群であること
- リンカ
ldは未解決シンボル(U)を実体のアドレスに解決し、カーネルがロードできるELF実行ファイルを完成させること undefined referenceエラーは「最後まで解決できなかったシンボルがある」というリンカからの報告であることreadelf/objdump/stripを使えば、実行ファイルの内部構造の確認や軽量化ができること- LFSのビルド順(Binutils → GCC → Glibc)は、依存関係の根にBinutilsがあるためであること