ABI・呼び出し規約とリンカの仕組み
ソースが実行ファイルになるまでの最後の一段が見えると、リンクエラーの正体が腑に落ちる。呼び出し規約・名前修飾・シンボル解決・再配置を原理から押さえる。
- 1.呼び出し規約はABIの一部で、引数をどのレジスタ/スタックに置き、戻り値の場所や呼び出し前後で保存すべきレジスタを定める。System V AMD64では整数引数を rdi,rsi,rdx,rcx,r8,r9 の順に渡す。
- 2.C++は関数シグネチャをシンボル名に符号化(名前修飾)するためオーバーロードを区別でき、Cは修飾しないため extern "C" でリンクを橋渡しする。
- 3.リンカはシンボル解決(未定義参照を定義に結びつける)と再配置(仮アドレスを確定値に書き換える)を行う。静的リンクは複製を埋め込み、動的リンクは実行時にGOT/PLT経由で解決する。
ABIと呼び出し規約は何を約束するのか
ソースコードが機械語へ翻訳されても、それだけでは実行できません。複数のオブジェクトファイルやライブラリを1つの実行像に組み上げ、関数同士が正しく値を受け渡せるよう取り決めを揃える必要があります。この取り決めの総体が ABI(Application Binary Interface)です。ABIはバイナリ同士の契約であり、int が何バイトか、構造体のメンバをどう詰めるか、関数をどう呼ぶか、までを規定します。ソースレベルの API(関数の名前や型)に対する、機械語レベルの対応物だと考えてください。翻訳全般の流れはコンパイルとインタプリタで扱っています。
ABIの中核が 呼び出し規約(calling convention)です。これは関数呼び出しに関する次の取り決めを指します。
- 引数を どこに置くか(レジスタかスタックか、その順序)
- 戻り値を どこで返すか
- 誰がスタックを片付けるか(呼び出し側か被呼び出し側か)
- 呼び出しをまたいで どのレジスタを保存すべきか
ABIは命令セットとは別物です。同じ x86-64 でも、Linux/macOS の System V AMD64 ABIと、Windows x64 ABIは引数レジスタが異なります。だから Linux 向けにビルドした共有ライブラリを Windows のバイナリへそのまま渡しても呼べません。「同じCPU=同じABI」ではない点が要注意です。
レジスタ渡しとスタック渡し
現代の64ビットABIは、速度のため引数の先頭いくつかをレジスタで渡します。System V AMD64 ABI(Linux/macOS)では、整数・ポインタ型の引数を rdi, rsi, rdx, rcx, r8, r9 の順に割り当て、7個目以降をスタックへ積みます。浮動小数点は別系統で xmm0〜xmm7 を使います。
long f(long a, long b, long c) { return a + b + c; }
の呼び出し(System V AMD64):
a -> rdi, b -> rsi, c -> rdx
戻り値 -> rax
呼び出し規約はレジスタを2種に分けます。呼び出し側保存(caller-saved/volatile、例 rax,rcx,rdx,rsi,rdi,r8-r11)は被呼び出し側が自由に壊してよく、値を残したい呼び出し側が事前退避します。被呼び出し側保存(callee-saved/non-volatile、例 rbx,rbp,r12-r15)は使うなら被呼び出し側が復元責任を負います。この線引きがあるからこそ、別々にコンパイルされた関数が互いの内部を知らずに安全に呼び合えます。引数受け渡しの言語側の見え方は関数も参照してください。
| 観点 | レジスタ渡し | スタック渡し |
|---|---|---|
| 速度 | 速い(メモリアクセス不要) | 遅い(push/popを伴う) |
| 個数の上限 | レジスタ数に制限あり | 事実上無制限 |
| 主な用途 | 先頭数個の引数 | あふれた引数・可変長引数 |
| 大きな構造体 | ポインタを渡すことが多い | 値ごとコピーして積む |
スタックには引数のほか、戻り番地・退避レジスタ・ローカル変数がフレーム単位で積まれます。System V AMD64は call 直前にスタックを16バイト境界へ整える規律も課します(SIMD命令のアライメント要件のため)。スタックとアドレスの関係はポインタと参照、フレームのメモリ配置はメモリレイアウトとデータ局所性で補強できます。
名前修飾とextern "C"
リンカが関数を識別する手がかりは シンボル(識別子の文字列)です。ここで言語差が表面化します。C++はオーバーロード(同名で引数違いの関数)を許すため、関数の引数型や所属名前空間をシンボル名へ符号化します。これが 名前修飾(name mangling)です。
C: int add(int,int) -> シンボル "add"
C++: int add(int,int) -> "_Z3addii" (ii = int,int)
double add(double) -> "_Z3addd" (別シンボルになる)
Cは修飾しないため、同名関数は1つしか持てません。C++コードからCの関数を呼ぶ(あるいはCへ公開する)には extern "C" を付け、コンパイラに「このシンボルは修飾するな」と指示します。これを忘れると、宣言と定義でシンボル名が食い違い「undefined reference」になります。
リンクエラーの多くは「シンボル名の不一致」です。extern "C" の付け忘れ、ライブラリのリンク順誤り、修飾規則の異なるコンパイラ混在などで、参照側が探すシンボル名と定義側が提供するシンボル名がずれます。エラーに出る生のシンボル名(_Z…)を c++filt で復元すると、どの関数が見つからないか即座に判明します。
リンカの二大仕事——シンボル解決と再配置
リンカの役割は2つに集約できます。第一が シンボル解決で、各オブジェクトファイルの未定義シンボル(外部参照)を、どこかの定義シンボルへ結びつけます。第二が 再配置(relocation)です。コンパイル時点では各関数・変数の最終アドレスが未確定なので、コンパイラは「ここはアドレスを後で埋めよ」という再配置エントリを残します。リンカはセクションを連結して各シンボルの最終アドレスを決め、再配置エントリが指す箇所へ確定値を書き込みます。
オブジェクトA: call <printf> ← 飛び先アドレス未定(再配置エントリ)
↓ リンク(解決+再配置)
実行像: call 0x401050 ← printfの確定アドレスを書き込み
静的リンクと動的リンク
最終的な結合をいつ行うかで方式が分かれます。静的リンク はビルド時にライブラリの必要部分を実行ファイルへ取り込みます。実行時依存がなく単体で動く反面、同じライブラリが各プロセスに複製され、更新時は再リンクが要ります。動的リンク は共有ライブラリ(.so/.dll/.dylib)への参照だけを残し、実行時にローダ(動的リンカ)が解決します。
動的リンクの肝が、位置独立コードのための間接参照表です。外部関数は PLT(Procedure Linkage Table)経由で呼ばれ、実アドレスは GOT(Global Offset Table)に格納されます。多くの実装は 遅延束縛 を行い、関数を初めて呼んだ瞬間にだけ解決してGOTへ書き戻します。以後はGOT経由の直接ジャンプで済み、起動コストを最初の呼び出しまで先送りできます。
| 観点 | 静的リンク | 動的リンク |
|---|---|---|
| 結合の時点 | ビルド時 | 実行時(ロード/初回呼び出し) |
| 実行ファイルサイズ | 大きい(コードを内包) | 小さい(参照のみ) |
| 共有とメモリ | プロセスごとに複製 | コードを複数プロセスで共有 |
| ライブラリ更新 | 再リンクが必要 | 差し替えのみで反映 |
| 起動・配布 | 依存なしで確実 | 依存解決に失敗しうる |
「DLL hell」「.so のバージョン不一致」は、実行時に期待するABI/シンボルを満たす共有ライブラリが見つからない(または非互換版が見つかる)動的リンク固有の問題です。soname(libfoo.so.2 のメジャー番号)はABI互換性の単位で、メジャーが変われば非互換を意味します。静的リンクはこの問題を回避する代わりに、セキュリティ修正の一括反映を失います。
まとめ
実行ファイル生成の最終段は、ABIという契約の上に成り立ちます。呼び出し規約が引数・戻り値・レジスタ保存責任を定め、別々にコンパイルされた関数が互いの中身を知らずに呼び合えるようにします。名前修飾はオーバーロードを区別するための符号化で、extern "C" がC/C++間の橋渡しになります。リンカはシンボル解決と再配置の2仕事で断片を1つの像に統合し、静的リンクは複製を埋め込み、動的リンクはGOT/PLTと遅延束縛で実行時に解決します。リンクエラーやバージョン不整合は、この「機械語レベルの約束ごと」のどこがずれたかを問う問題です。仕組みを押さえれば、エラーメッセージは原因を指し示す手がかりへと変わります。
プログラミング Article
ABI・呼び出し規約とリンカの仕組みを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ABI
比較で見る軸
難易度: advanced / カテゴリ: プログラミング / タグ数: 5
導入後に効く点
C++は関数シグネチャをシンボル名に符号化(名前修飾)するためオーバーロードを区別でき、Cは修飾しないため extern "C" でリンクを橋渡しする。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- プログラミング
- タグ数
- 5
判断チェックリスト
- 自社の用途が「ABI / 呼び出し規約」に近いか確認する。
- 強みである「呼び出し規約はABIの一部で、引数をどのレジスタ/スタックに置き、戻り値の場所や呼び出し前後で保存すべきレジスタを定める。System V AMD64では整数引数を rdi,rsi,rdx,rcx,r8,r9 の順に渡す。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。