[インデックス 1279] ファイルの概要
このコミットは、Go言語のリンカである6l
がDarwin(macOS)上で生成する実行ファイル(6.out
)のリンク方式を、デフォルトで静的リンクから動的リンクに変更するものです。ただし、-d
フラグが指定された場合は静的リンクのままとなります。この変更の主な目的は、生成されたバイナリがmacOSの動的トレースツールであるdtrace
やdtruss
で解析可能になるようにすることです。
コミット
Go言語のリンカ6l
がDarwin環境で生成するバイナリ(6.out
)について、デフォルトのリンク方式を静的リンクから動的リンクに変更しました。これにより、バイナリは動的リンカへの参照を持つようになりますが、実際に動的ライブラリを使用することはありません。この変更は、dtrace
やdtruss
といったmacOSの動的トレースツールがGoバイナリを認識し、トレースできるようにするために行われました。-d
フラグがリンカに渡された場合は、引き続き静的リンクされたバイナリが生成されます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cfb94254608e8aa83ca589d5cc30dd3d3c64ff26
元コミット内容
emit dynamically linked 6.out on darwin instead of static,
unless given -d flag.
the resulting binary doesn't *use* any dynamic libraries,
it just has a reference to the dynamic linker and an
empty list of needed symbols and libraries.
this is enough to make 6.out binaries that can be run
under dtrace / dtruss.
R=r
DELTA=39 (34 added, 0 deleted, 5 changed)
OCL=20476
CL=20482
---
src/cmd/6l/asm.c | 43 ++++++++++++++++++++++++++++++++++++++-----
src/cmd/6l/l.h | 1 +
2 files changed, 39 insertions(+), 5 deletions(-)
diff --git a/src/cmd/6l/asm.c b/src/cmd/6l/asm.c
index 5bb6cc4149..fec8fd0e16 100644
--- a/src/cmd/6l/asm.c
+++ b/src/cmd/6l/asm.c
@@ -121,7 +121,7 @@ asmb(void)
{\n \tProg *p;\n \tint32 v, magic;\n-\tint a, np;\n+\tint a, nl, np;\n \tuchar *op1;\n \tvlong vl, va, fo, w, symo;\n \tint strtabsize;\n@@ -357,10 +357,12 @@ asmb(void)\n \t\tlputl((1<<24)|7);\t\t/* cputype - x86/ABI64 */\n \t\tlputl(3);\t\t\t/* subtype - x86 */\n \t\tlputl(2);\t\t\t/* file type - mach executable */\n-\t\tif (debug[\'s\'])\n-\t\t\tlputl(4);\t\t\t/* number of loads */\n-\t\telse\n-\t\t\tlputl(7);\t\t\t/* /* number of loads */\n+\t\tnl = 4;\n+\t\tif (!debug[\'s\'])\n+\t\t\tnl += 3;\n+\t\tif (!debug[\'d\'])\t// -d = turn off \"dynamic loader\"\n+\t\t\tnl += 2;\n+\t\tlputl(nl);\t\t\t/* number of loads */\n \t\tlputl(machheadr()-32);\t\t/* size of loads */\n \t\tlputl(1);\t\t\t/* flags - no undefines */\n \t\tlputl(0);\t\t\t/* reserved */\n@@ -399,6 +401,7 @@ asmb(void)\n \t\t\t0,0,0,0,\t\t/* offset align reloc nreloc */\n \t\t\t1);\t\t\t/* flag - zero fill */\n \n+\t\tmachdylink();\n \t\tmachstack(va+HEADR);\n \n \t\tif (!debug[\'s\']) {\n@@ -824,6 +827,32 @@ machsect(char *name, char *seg, vlong addr, vlong size, uint32 off,\n \tlputl(0);\t/* reserved */\n }\n \n+// Emit a section requesting the dynamic loader\n+// but giving it no work to do (an empty dynamic symbol table).\n+// This is enough to make the Apple tracing programs (like dtrace)\n+// accept the binary, so that one can run dtruss on a 6.out.\n+// The dynamic linker loads at 0x8fe00000, so if we want to\n+// be able to build >2GB binaries, we\'re going to need to move\n+// the text segment to 4G like Apple does.\n+void\n+machdylink(void)\n+{\n+\tint i;\n+\n+\tif(debug[\'d\'])\n+\t\treturn;\n+\n+\tlputl(11);\t/* LC_DYSYMTAB */\n+\tlputl(80);\t/* byte count */\n+\tfor(i=0; i<18; i++)\n+\t\tlputl(0);\n+\n+\tlputl(14);\t/* LC_LOAD_DYLINKER */\tlputl(28);\t/* byte count */\n+\tlputl(12);\t/* offset to string */\n+\tstrnput(\"/usr/lib/dyld\", 16);\n+}\n+\n void\n machstack(vlong e)\n {\n@@ -855,6 +884,10 @@ machheadr(void)\n \ta += 20;\t/* data sect */\n \ta += 20;\t/* bss sect */\n \ta += 46;\t/* stack sect */\n+\tif (!debug[\'d\']) {\n+\t\ta += 20;\t/* dysymtab */\n+\t\ta += 7;\t/* load dylinker */\n+\t}\n \tif (!debug[\'s\']) {\n \t\ta += 18;\t/* symdat seg */\n \t\ta += 4;\t/* symtab seg */\ndiff --git a/src/cmd/6l/l.h b/src/cmd/6l/l.h\nindex ce3ff8eaf6..7367bfec2a 100644\n--- a/src/cmd/6l/l.h\+++ b/src/cmd/6l/l.h\n@@ -438,6 +438,7 @@ void\tmachseg(char*, vlong, vlong, vlong, vlong, uint32, uint32, uint32, uint32);\n void\tmachsymseg(uint32, uint32);\n void\tmachsect(char*, char*, vlong, vlong, uint32, uint32, uint32, uint32, uint32);\n void\tmachstack(vlong);\n+void\tmachdylink(void);\n uint32\tmachheadr(void);\n \n uint32\tlinuxheadr(void);\n```
## 変更の背景
この変更が行われた背景には、macOS(Darwin)におけるシステムトレースツールの利用可能性が関係しています。Go言語の初期のリンカは、生成するバイナリを完全に静的にリンクしていました。これは、Goの設計思想の一つである「単一のバイナリで依存関係を完結させる」という考え方に基づいています。しかし、この静的リンクされたバイナリは、macOSに標準で搭載されている強力な動的トレースツールである`dtrace`やそのフロントエンドである`dtruss`によって、適切に解析されないという問題がありました。
`dtrace`や`dtruss`は、実行中のプロセスやシステムコール、ファイルI/Oなどを詳細に監視・分析するためのツールです。これらのツールは、通常、バイナリが動的リンカ(macOSでは`/usr/lib/dyld`)への参照を持っていることを前提として動作します。静的にリンクされたバイナリは、この参照を持たないため、`dtrace`がバイナリを認識できず、トレースが開始できない、あるいは期待通りの情報が得られないという状況が発生していました。
このコミットは、Goで書かれたアプリケーションのデバッグやパフォーマンス分析をmacOS環境でより容易にするために、`dtrace`/`dtruss`との互換性を確保することを目的としています。
## 前提知識の解説
### Go言語のツールチェーンと`6l`
Go言語は、コンパイラ、アセンブラ、リンカなどを含む独自のツールチェーンを持っています。
* **`6l`**: Go言語のリンカの一つで、64ビットアーキテクチャ(x86-64)向けのバイナリを生成します。Goのビルドプロセスにおいて、コンパイルされたオブジェクトファイルを結合し、最終的な実行可能ファイルを生成する役割を担います。
### 静的リンクと動的リンク
プログラムが外部のライブラリを使用する際、そのライブラリをプログラムに結合する方法には大きく分けて静的リンクと動的リンクの2種類があります。
* **静的リンク (Static Linking)**:
* ライブラリのコードが、コンパイル時に実行可能ファイルに直接組み込まれます。
* 利点: 実行可能ファイルが自己完結型となり、他のシステムに配布する際にライブラリの有無を気にする必要がありません。依存関係の問題が少ないです。
* 欠点: 実行可能ファイルのサイズが大きくなる傾向があります。複数のプログラムが同じライブラリを使用する場合でも、それぞれのプログラムがライブラリのコピーを持つため、メモリの無駄が生じる可能性があります。ライブラリの更新があった場合、そのライブラリを使用するすべてのプログラムを再コンパイル・再リンクする必要があります。
* **動的リンク (Dynamic Linking)**:
* ライブラリのコードは、実行可能ファイルには組み込まれず、実行時に動的リンカ(macOSでは`dyld`)によってメモリにロードされます。実行可能ファイルには、どのライブラリが必要か、そしてそれらのライブラリのどこに目的の関数があるかを示す情報(シンボル情報)のみが含まれます。
* 利点: 実行可能ファイルのサイズが小さくなります。複数のプログラムが同じライブラリを使用する場合、ライブラリのコードはメモリ上に一度だけロードされ、共有されるため、メモリ効率が良いです。ライブラリが更新されても、プログラムを再コンパイル・再リンクする必要がない場合が多いです。
* 欠点: 実行時に必要なライブラリがシステムに存在しない場合、プログラムは実行できません(「DLL地獄」や「依存関係の地獄」と呼ばれる問題)。
このコミットでは、Goのバイナリが実際に動的ライブラリを使用しないにもかかわらず、動的リンカへの参照を持つようにすることで、動的リンクの「形式」だけを導入しています。
### Mach-Oフォーマット
Mach-O (Mach Object) は、macOS、iOS、watchOS、tvOSなどのAppleのオペレーティングシステムで使用される実行可能ファイル、オブジェクトコード、共有ライブラリ、およびコアダンプのファイル形式です。Mach-Oファイルは、ヘッダ、ロードコマンド、セグメント、セクションなどで構成されます。
* **ロードコマンド (Load Commands)**: Mach-Oファイルのヘッダの後に続くデータ構造で、カーネルがプログラムをロードして実行するために必要な情報を提供します。これには、セグメントの定義、シンボルテーブルの場所、動的リンカのパスなどが含まれます。
* **`LC_DYSYMTAB` (Dynamic Symbol Table Command)**: 動的シンボルテーブルに関する情報を提供します。動的リンクされたバイナリが、実行時に解決する必要があるシンボルや、他のライブラリからエクスポートされるシンボルに関する情報を含みます。
* **`LC_LOAD_DYLINKER` (Load Dynamic Linker Command)**: 動的リンカのパスを指定します。macOSでは通常`/usr/lib/dyld`が指定されます。このコマンドが存在することで、OSはバイナリが動的リンクを必要とすると認識し、指定された動的リンカをロードします。
### `dtrace`と`dtruss`
* **`dtrace`**: Sun Microsystems(現Oracle)が開発した、動的なシステムトレースフレームワークです。macOSに標準で搭載されており、カーネルやアプリケーションの動作をリアルタイムで詳細に監視・分析できます。特定のイベント(システムコール、関数呼び出し、ファイルI/Oなど)が発生した際に、カスタムスクリプトを実行して情報を収集できます。
* **`dtruss`**: `dtrace`をベースにしたツールで、特定のプロセスが実行するシステムコールをトレースし、その引数や戻り値を表示します。Linuxの`strace`やBSDの`ktrace`に似ています。
これらのツールは、バイナリが動的リンカへの参照を持っていることを前提としているため、静的リンクされたGoバイナリではうまく機能しませんでした。
## 技術的詳細
このコミットの核心は、Goのリンカ`6l`がMach-O形式の実行ファイルを生成する際に、動的リンカへの参照を意図的に埋め込む点にあります。これは、バイナリが実際に動的ライブラリに依存しているわけではないにもかかわらず、`dtrace`などのツールが「動的リンクされたバイナリ」として認識できるようにするためのハックです。
具体的には、以下のMach-Oロードコマンドが追加されます。
1. **`LC_DYSYMTAB` (Dynamic Symbol Table Command)**:
* このコマンドは、動的シンボルテーブルに関する情報を提供します。このコミットでは、このシンボルテーブルを「空」として設定します。つまり、バイナリは動的リンカに「動的シンボルテーブルがあるよ」と伝えますが、その中身は空なので、実際に解決すべき動的シンボルは存在しません。これにより、`dtrace`は動的リンクされたバイナリとして認識しつつも、余計なシンボル解決のオーバーヘッドは発生しません。
* コードでは、`lputl(11); /* LC_DYSYMTAB */` と `lputl(80); /* byte count */` の後に、18個のゼロ(`for(i=0; i<18; i++) lputl(0);`)を書き込むことで、空の動的シンボルテーブルを表現しています。
2. **`LC_LOAD_DYLINKER` (Load Dynamic Linker Command)**:
* このコマンドは、システムがバイナリをロードする際に使用する動的リンカのパスを指定します。macOSでは、このパスは常に`/usr/lib/dyld`です。このコマンドが存在することで、OSはバイナリが動的リンクを必要とすると判断し、`dyld`をロードします。
* コードでは、`lputl(14); /* LC_LOAD_DYLINKER */` と `lputl(28); /* byte count */` の後に、動的リンカのパス文字列`/usr/lib/dyld`を書き込んでいます(`strnput("/usr/lib/dyld", 16);`)。
これらのロードコマンドは、リンカに`-d`フラグが渡されない限り、デフォルトでMach-Oヘッダに組み込まれます。`-d`フラグは、この動的リンカの参照を「オフにする」ためのデバッグフラグとして機能します。
コミットメッセージにある「The dynamic linker loads at 0x8fe00000, so if we want to be able to build >2GB binaries, we're going to need to move the text segment to 4G like Apple does.」というコメントは、当時のMach-Oの設計上の制約と将来的な課題を示唆しています。動的リンカが特定のメモリアドレス(0x8fe00000)にロードされるため、2GBを超えるような巨大なバイナリをビルドする際には、テキストセグメントの配置を調整する必要があるかもしれないという考察です。これは、Goのバイナリサイズが大きくなる可能性を考慮した、将来を見据えたコメントと言えます。
## コアとなるコードの変更箇所
変更は主に`src/cmd/6l/asm.c`と`src/cmd/6l/l.h`の2つのファイルで行われています。
### `src/cmd/6l/l.h`
* `machdylink`関数のプロトタイプ宣言が追加されました。
```diff
--- a/src/cmd/6l/l.h
+++ b/src/cmd/6l/l.h
@@ -438,6 +438,7 @@ void machseg(char*, vlong, vlong, vlong, vlong, uint32, uint32, uint32, uint32);
void machsymseg(uint32, uint32);
void machsect(char*, char*, vlong, vlong, uint32, uint32, uint32, uint32, uint32);
void machstack(vlong);
+void machdylink(void);
uint32 machheadr(void);
uint32 linuxheadr(void);
```
### `src/cmd/6l/asm.c`
* `asmb`関数内で、Mach-Oヘッダのロードコマンド数(`nl`)の計算ロジックが変更されました。
```diff
--- a/src/cmd/6l/asm.c
+++ b/src/cmd/6l/asm.c
@@ -357,10 +357,12 @@ asmb(void)
lputl((1<<24)|7); /* cputype - x86/ABI64 */
lputl(3); /* subtype - x86 */
lputl(2); /* file type - mach executable */
- if (debug['s'])
- lputl(4); /* number of loads */
- else
- lputl(7); /* number of loads */
+ nl = 4;
+ if (!debug['s'])
+ nl += 3;
+ if (!debug['d']) // -d = turn off "dynamic loader"
+ nl += 2;
+ lputl(nl); /* number of loads */
lputl(machheadr()-32); /* size of loads */
lputl(1); /* flags - no undefines */
lputl(0); /* reserved */
```
* `asmb`関数内で、`machdylink()`関数が呼び出されるようになりました。
```diff
--- a/src/cmd/6l/asm.c
+++ b/src/cmd/6l/asm.c
@@ -399,6 +401,7 @@ asmb(void)
0,0,0,0, /* offset align reloc nreloc */
1); /* flag - zero fill */
+\tmachdylink();
machstack(va+HEADR);
if (!debug['s']) {
```
* `machdylink`関数が新規に追加されました。この関数が`LC_DYSYMTAB`と`LC_LOAD_DYLINKER`ロードコマンドをMach-Oヘッダに書き込みます。
```diff
--- a/src/cmd/6l/asm.c
+++ b/src/cmd/6l/asm.c
@@ -824,6 +827,32 @@ machsect(char *name, char *seg, vlong addr, vlong size, uint32 off,
lputl(0); /* reserved */
}
+// Emit a section requesting the dynamic loader
+// but giving it no work to do (an empty dynamic symbol table).\n+// This is enough to make the Apple tracing programs (like dtrace)\n+// accept the binary, so that one can run dtruss on a 6.out.\n+// The dynamic linker loads at 0x8fe00000, so if we want to\n+// be able to build >2GB binaries, we're going to need to move\n+// the text segment to 4G like Apple does.\n+void
+machdylink(void)
+{\n+\tint i;\n+\n+\tif(debug['d'])\n+\t\treturn;\n+\n+\tlputl(11);\t/* LC_DYSYMTAB */\n+\tlputl(80);\t/* byte count */\n+\tfor(i=0; i<18; i++)\n+\t\tlputl(0);\n+\n+\tlputl(14);\t/* LC_LOAD_DYLINKER */\n+\tlputl(28);\t/* byte count */\n+\tlputl(12);\t/* offset to string */\n+\tstrnput("/usr/lib/dyld", 16);\n+}\n+\n void
machstack(vlong e)
{
```
* `machheadr`関数内で、Mach-Oヘッダのサイズ計算ロジックが変更されました。`LC_DYSYMTAB`と`LC_LOAD_DYLINKER`のサイズが追加されるようになりました。
```diff
--- a/src/cmd/6l/asm.c
+++ b/src/cmd/6l/asm.c
@@ -855,6 +884,10 @@ machheadr(void)
a += 20; /* data sect */
a += 20; /* bss sect */
a += 46; /* stack sect */
+\tif (!debug['d']) {\n+\t\ta += 20; /* dysymtab */\n+\t\ta += 7; /* load dylinker */\n+\t}\n if (!debug['s']) {\n a += 18; /* symdat seg */
a += 4; /* symtab seg */
```
## コアとなるコードの解説
### `src/cmd/6l/l.h`
* `void machdylink(void);` の追加:
* これは、`machdylink`という新しい関数が定義され、Mach-Oヘッダの生成に関連する処理を行うことをリンカの他の部分に知らせるための前方宣言です。
### `src/cmd/6l/asm.c`
* **`asmb`関数内の`nl`計算の変更**:
* `nl`はMach-Oヘッダ内のロードコマンドの数を表します。
* 元のコードでは、デバッグフラグ`debug['s']`の有無によって`nl`が4または7に設定されていました。
* 変更後、`nl`はまず4で初期化されます。
* `if (!debug['s']) nl += 3;` は、シンボル関連のロードコマンド(シンボルテーブル、文字列テーブルなど)が追加される場合の数を調整しています。
* `if (!debug['d']) nl += 2;` が今回のコミットの主要な変更点です。
* `debug['d']`フラグが設定されていない場合(つまり、デフォルトの動作)、`nl`に2が追加されます。この2は、新しく追加される`LC_DYSYMTAB`と`LC_LOAD_DYLINKER`の2つのロードコマンドに対応します。
* `-d`フラグは「動的ローダーをオフにする」という意味合いで使われており、このフラグが設定されている場合は、これらの動的リンカ関連のロードコマンドは追加されません。
* **`asmb`関数内での`machdylink()`の呼び出し**:
* `machdylink();` の行が追加されました。これは、Mach-Oヘッダの生成プロセス中に、新しく定義された`machdylink`関数を呼び出し、動的リンカ関連のロードコマンドを実際に書き込むようにします。
* **`machdylink`関数の実装**:
* この関数は、`debug['d']`フラグが設定されている場合はすぐに`return`し、動的リンカ関連のロードコマンドの生成をスキップします。
* **`LC_DYSYMTAB`の書き込み**:
* `lputl(11); /* LC_DYSYMTAB */`: ロードコマンドタイプとして`LC_DYSYMTAB`(値11)を書き込みます。
* `lputl(80); /* byte count */`: このロードコマンドのバイト数を書き込みます。
* `for(i=0; i<18; i++) lputl(0);`: 18個のゼロを書き込みます。これは、動的シンボルテーブルが「空」であることを示します。つまり、実際に解決すべき動的シンボルは存在しないが、形式上は動的シンボルテーブルが存在すると宣言します。
* **`LC_LOAD_DYLINKER`の書き込み**:
* `lputl(14); /* LC_LOAD_DYLINKER */`: ロードコマンドタイプとして`LC_LOAD_DYLINKER`(値14)を書き込みます。
* `lputl(28); /* byte count */`: このロードコマンドのバイト数を書き込みます。
* `lputl(12); /* offset to string */`: 動的リンカのパス文字列へのオフセットを書き込みます。
* `strnput("/usr/lib/dyld", 16);`: 動的リンカのパスである`/usr/lib/dyld`を書き込みます。これは、macOSの標準的な動的リンカのパスです。
* **`machheadr`関数内のヘッダサイズ計算の変更**:
* `machheadr`関数は、Mach-Oヘッダ全体のサイズを計算します。
* `if (!debug['d']) { a += 20; /* dysymtab */ a += 7; /* load dylinker */ }` の行が追加されました。
* `-d`フラグが設定されていない場合、`LC_DYSYMTAB`(20バイト)と`LC_LOAD_DYLINKER`(7バイト)のサイズがヘッダの合計サイズに追加されます。これにより、Mach-Oヘッダのサイズが正しく報告され、OSがバイナリを正しく解析できるようになります。
これらの変更により、Goのリンカは、デフォルトでMach-Oバイナリに動的リンカへの参照と空の動的シンボルテーブルを埋め込むようになり、`dtrace`や`dtruss`といったツールがGoバイナリを動的リンクされたものとして認識し、トレースが可能になります。
## 関連リンク
* **Go言語公式サイト**: [https://go.dev/](https://go.dev/)
* **dtraceについて (Wikipedia)**: [https://ja.wikipedia.org/wiki/DTrace](https://ja.wikipedia.org/wiki/DTrace)
* **Mach-Oファイルフォーマット (Wikipedia)**: [https://ja.wikipedia.org/wiki/Mach-O](https://ja.wikipedia.org/wiki/Mach-O)
* **dyld (Dynamic Linker) について**: macOSの動的リンカに関する公式ドキュメントやApple Developerドキュメントを参照すると良いでしょう。
## 参考にした情報源リンク
* Go言語のソースコード(特に`src/cmd/6l`ディレクトリ)
* Mach-Oファイルフォーマットに関する公開ドキュメントや仕様
* `dtrace`および`dtruss`に関するドキュメントやチュートリアル
* 静的リンクと動的リンクに関する一般的なプログラミングの概念説明
* Go言語の初期のリンカの挙動に関するコミュニティの議論やメーリングリストのアーカイブ(もしあれば)
* [https://github.com/golang/go/commit/cfb94254608e8aa83ca589d5cc30dd3d3c64ff26](https://github.com/golang/go/commit/cfb94254608e8aa83ca589d5cc30dd3d3c64ff26) (コミット自体)
* [https://dtrace.org/](https://dtrace.org/) (DTrace公式サイト)
* [https://developer.apple.com/documentation/](https://developer.apple.com/documentation/) (Apple Developer Documentation - Mach-O, dyld, dtrace関連)