PK
パーネルカニック研究所
今日もどこかでkernel panic。
root@pk-lab:~#

Binutilsとは何か:実行ファイルを組み立て、仕上げる職人集団

連載ナビ

この連載は「Linuxを使えるようになる」ではなく、「Linuxがどう動いているのか」を理解するための実践シリーズです。

今回は Linuxを理解する 〜 LFSで学ぶLinux内部構造 〜 の Part 3 / 第5回として、仕組みのつながりを意識しながら学びます。

—

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があるためであること