[インデックス 18562] ファイルの概要
このコミットは、Go言語のリンカ (cmd/ld
) からPlan 9シンボルテーブルの生成を削除し、関連するデバッグパッケージ (debug/elf
, debug/macho
, debug/pe
, debug/gosym
) を更新するものです。これにより、Goバイナリのディスクスペースが約15%削減されます。
コミット
- コミットハッシュ:
964f6d3ec4c6e2bed377878bd2862767bfae463d
- 作者: Russ Cox rsc@golang.org
- 日付: Tue Feb 18 23:41:15 2014 -0500
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/964f6d3ec4c6e2bed377878bd2862767bfae463d
元コミット内容
cmd/ld: remove Plan 9 symbol table
Update #6853
Nothing reads the Plan 9 symbol table anymore.
The last holdout was 'go tool nm', but since being rewritten in Go
it uses the standard symbol table for the binary format
(ELF, Mach-O, PE) instead.
Removing the Plan 9 symbol table saves ~15% disk space
on most binaries.
Two supporting changes included in this CL:
debug/gosym: use Go 1.2 pclntab to synthesize func-only
symbol table when there is no Plan 9 symbol table
debug/elf, debug/macho, debug/pe: ignore final EOF from ReadAt
LGTM=r
R=r, bradfitz
CC=golang-codereviews
https://golang.org/cl/65740045
変更の背景
この変更の主な背景は、Goバイナリのサイズ削減と、Plan 9シンボルテーブルの不要化です。
- Plan 9シンボルテーブルの冗長化: 以前はGoバイナリにPlan 9形式のシンボルテーブルが含まれていましたが、Goツールチェインの進化により、この形式のシンボルテーブルを参照するコンポーネントがなくなりました。特に、シンボル情報を表示する
go tool nm
コマンドがGoで書き直され、OSネイティブのバイナリフォーマット(ELF, Mach-O, PE)が提供する標準シンボルテーブルを使用するようになったため、Plan 9シンボルテーブルの存在意義が失われました。 - ディスクスペースの最適化: Plan 9シンボルテーブルは、Goバイナリのディスクスペースを約15%消費していました。これが不要になったことで、バイナリサイズを削減し、配布やデプロイの効率を向上させることが可能になりました。
このコミットは、Go言語のツールチェインが成熟し、特定のレガシーな依存関係から脱却する過程の一部と見なすことができます。
前提知識の解説
このコミットを理解するためには、以下の概念について知っておく必要があります。
- Plan 9: ベル研究所で開発された分散オペレーティングシステムです。Go言語の設計思想やツールチェイン(特にアセンブラやリンカ)は、Plan 9の影響を強く受けています。Goの初期のツールはPlan 9のツールをベースにしていたため、Plan 9形式のシンボルテーブルがGoバイナリに埋め込まれていました。
- シンボルテーブル: コンパイラやリンカが生成するデータ構造で、プログラム内のシンボル(関数名、変数名など)とそのメモリアドレスや型情報などの関連情報をマッピングします。デバッグ、プロファイリング、動的リンクなどに不可欠な情報です。
go tool nm
: Go言語のツールで、コンパイルされたGoバイナリのシンボルテーブルの内容を表示するために使用されます。これにより、バイナリに含まれる関数や変数の名前、アドレス、型などを確認できます。- ELF (Executable and Linkable Format): Unix系システム(Linuxなど)で広く使用されている実行可能ファイル、オブジェクトコード、共有ライブラリの標準ファイルフォーマットです。
- Mach-O: macOSやiOSなどのApple製オペレーティングシステムで使用されている実行可能ファイル、オブジェクトコード、共有ライブラリのファイルフォーマットです。
- PE (Portable Executable): Windowsオペレーティングシステムで使用されている実行可能ファイル、オブジェクトコード、DLL(Dynamic Link Library)のファイルフォーマットです。
pclntab
(Program Counter Line Table): Goバイナリに埋め込まれている重要なデータ構造の一つで、プログラムカウンタ(PC)とソースコードの行番号、関数名などをマッピングするために使用されます。スタックトレースの生成やデバッグ時に利用されます。Go 1.2以降、pclntab
の構造が簡素化され、関数情報のみを効率的に取得できるようになりました。ReadAt
: Goのio.ReaderAt
インターフェースの一部で、指定されたオフセットからデータを読み込むためのメソッドです。ファイルやネットワーク接続など、ランダムアクセスが可能なデータソースからの読み込みに適しています。
技術的詳細
このコミットは、主に以下の3つの領域にわたる変更を含んでいます。
-
cmd/ld
(リンカ) からのPlan 9シンボルテーブル生成の削除:src/cmd/ld/symtab.c
ファイルから、Plan 9シンボルテーブルを生成するための関数(scput
,slputb
,slputl
,putsymb
など)と、それらを呼び出すロジックが完全に削除されました。- 具体的には、
putsymb
関数(シンボルテーブルエントリを生成する主要な関数)と、symtab
関数内のPlan 9シンボルテーブルヘッダの書き込みおよびgenasmsym
の呼び出し部分が削除されています。 - これにより、GoリンカはもはやPlan 9形式のシンボル情報をバイナリに埋め込まなくなります。
-
debug/gosym
パッケージの更新:src/pkg/debug/gosym/pclntab.go
にgo12Funcs
という新しい関数が追加されました。この関数は、Go 1.2形式のpclntab
から関数情報のみを抽出し、Func
構造体のスライスとして合成する役割を担います。- これは、Plan 9シンボルテーブルがなくなった場合に、
pclntab
から必要な関数シンボル情報を取得するためのフォールバックメカニズムとして機能します。特に、go tool nm
のようなツールが、OSネイティブのシンボルテーブルに加えて、Goランタイムの関数情報を必要とする場合に利用されます。 src/pkg/debug/gosym/symtab.go
では、NewTable
関数内で、Plan 9シンボルテーブルが存在しない場合(nf == 0
)にgo12Funcs
を使用して関数シンボルテーブルを合成するロジックが追加されました。また、walksymtab
関数にlen(data) == 0
の場合のチェックが追加され、シンボルテーブルが欠落している場合でもエラーにならないように変更されました。
-
debug/elf
,debug/macho
,debug/pe
パッケージの更新:- これらのパッケージは、それぞれELF、Mach-O、PE形式のバイナリファイルを解析するためのものです。
src/pkg/debug/elf/file.go
、src/pkg/debug/macho/file.go
、src/pkg/debug/pe/file.go
の各ファイルで、Data()
メソッド内のReadAt
呼び出し後のエラーハンドリングが修正されました。- 具体的には、
n == len(dat)
(要求されたバイト数すべてが読み込まれた)の場合にerr
をnil
に設定するようになりました。これは、ReadAt
がファイルの終端に達した場合にio.EOF
を返すことがありますが、それが読み込みが成功したことを意味する場合があるため、そのEOF
を無視して正常終了と見なすための変更です。これにより、バイナリファイルの末尾にあるセクションやセグメントの読み込みがより堅牢になります。
これらの変更により、GoのビルドプロセスはPlan 9シンボルテーブルの生成を停止し、デバッグツールはOSネイティブのシンボルテーブルとGo 1.2形式のpclntab
を組み合わせてシンボル情報を取得するようになります。
コアとなるコードの変更箇所
src/cmd/ld/symtab.c
(削除)
Plan 9シンボルテーブルの生成ロジックが大幅に削除されています。
--- a/src/cmd/ld/symtab.c
+++ b/src/cmd/ld/symtab.c
@@ -270,47 +270,6 @@ asmplan9sym(void)
static LSym *symt;
-static void
-scput(int b)
-{
- uchar *p;
-
- symgrow(ctxt, symt, symt->size+1);
- p = symt->p + symt->size;
- *p = b;
- symt->size++;
-}
-
-static void
-slputb(int32 v)
-{
- uchar *p;
-
- symgrow(ctxt, symt, symt->size+4);
- p = symt->p + symt->size;
- *p++ = v>>24;
- *p++ = v>>16;
- *p++ = v>>8;
- *p = v;
- symt->size += 4;
-}
-
-static void
-slputl(int32 v)
-{
- uchar *p;
-
- symgrow(ctxt, symt, symt->size+4);
- p = symt->p + symt->size;
- *p++ = v;
- *p++ = v>>8;
- *p++ = v>>16;
- *p = v>>24;
- symt->size += 4;
-}
-
-static void (*slput)(int32);
-
void
wputl(ushort w)
{
@@ -357,108 +316,6 @@ vputl(uint64 v)
lputl(v >> 32);
}
-// Emit symbol table entry.
-// The table format is described at the top of ../../pkg/runtime/symtab.c.
-void
-putsymb(LSym *s, char *name, int t, vlong v, vlong size, int ver, LSym *typ)
-{
- int i, f, c;
- vlong v1;
- Reloc *rel;
-
- USED(size);
-
- // type byte
- if('A' <= t && t <= 'Z')
- c = t - 'A' + (ver ? 26 : 0);
- else if('a' <= t && t <= 'z')
- c = t - 'a' + 26;
- else {
- diag("invalid symbol table type %c", t);
- errorexit();
- return;
- }
-
- if(s != nil)
- c |= 0x40; // wide value
- if(typ != nil)
- c |= 0x80; // has go type
- scput(c);
-
- // value
- if(s != nil) {
- // full width
- rel = addrel(symt);
- rel->siz = PtrSize;
- rel->sym = s;
- rel->type = D_ADDR;
- rel->off = symt->size;
- if(PtrSize == 8)
- slput(0);
- slput(0);
- } else {
- // varint
- if(v < 0) {
- diag("negative value in symbol table: %s %lld", name, v);
- errorexit();
- }
- v1 = v;
- while(v1 >= 0x80) {
- scput(v1 | 0x80);
- v1 >>= 7;
- }
- scput(v1);
- }
-
- // go type if present
- if(typ != nil) {
- if(!typ->reachable)
- diag("unreachable type %s", typ->name);
- rel = addrel(symt);
- rel->siz = PtrSize;
- rel->sym = typ;
- rel->type = D_ADDR;
- rel->off = symt->size;
- if(PtrSize == 8)
- slput(0);
- slput(0);
- }
-
- // name
- if(t == 'f')
- name++;
-
- if(t == 'Z' || t == 'z') {
- scput(name[0]);
- for(i=1; name[i] != 0 || name[i+1] != 0; i += 2) {
- scput(name[i]);
- scput(name[i+1]);
- }
- scput(0);
- scput(0);
- } else {
- for(i=0; name[i]; i++)
- scput(name[i]);
- scput(0);
- }
-
- if(debug['n']) {
- if(t == 'z' || t == 'Z') {
- Bprint(&bso, "%c %.8llux ", t, v);
- for(i=1; name[i] != 0 || name[i+1] != 0; i+=2) {
- f = ((name[i]&0xff) << 8) | (name[i+1]&0xff);
- Bprint(&bso, "/%x", f);
- }
- Bprint(&bso, "\n");
- return;
- }
- if(ver)
- Bprint(&bso, "%c %.8llux %s<%d> %s\n", t, v, name, ver, typ ? typ->name : "");
- else
- Bprint(&bso, "%c %.8llux %s %s\n", t, v, name, typ ? typ->name : "");
- }
-}
-
void
symtab(void)
{
@@ -553,31 +410,4 @@ symtab(void)\n s->outer = symgofunc;\n }\n }\n-\n-\tif(debug['s'])\n-\t\treturn;\n-\n-\tswitch(thechar) {\n-\tdefault:\n-\t\tdiag("unknown architecture %c", thechar);\n-\t\terrorexit();\n-\tcase '5':\n-\tcase '6':\n-\tcase '8':\n-\t\t// little-endian symbol table\n-\t\tslput = slputl;\n-\t\tbreak;\n-\tcase 'v':\n-\t\t// big-endian symbol table\n-\t\tslput = slputb;\n-\t\tbreak;\n-\t}\n-\t// new symbol table header.\n-\tslput(0xfffffffd);\n-\tscput(0);\n-\tscput(0);\n-\tscput(0);\n-\tscput(PtrSize);\n-\n-\tgenasmsym(putsymb);\n }
src/pkg/debug/gosym/pclntab.go
(追加)
go12Funcs
関数が追加され、pclntab
から関数情報を合成します。
--- a/src/pkg/debug/gosym/pclntab.go
+++ b/src/pkg/debug/gosym/pclntab.go
@@ -196,6 +196,33 @@ func (t *LineTable) go12Init() {
t.go12 = 1 // so far so good
}
+// go12Funcs returns a slice of Funcs derived from the Go 1.2 pcln table.
+func (t *LineTable) go12Funcs() []Func {
+ // Assume it is malformed and return nil on error.
+ defer func() {
+ recover()
+ }()
+
+ n := len(t.functab) / int(t.ptrsize) / 2
+ funcs := make([]Func, n)
+ for i := range funcs {
+ f := &funcs[i]
+ f.Entry = uint64(t.uintptr(t.functab[2*i*int(t.ptrsize):]))
+ f.End = uint64(t.uintptr(t.functab[(2*i+2)*int(t.ptrsize):]))
+ info := t.Data[t.uintptr(t.functab[(2*i+1)*int(t.ptrsize):]):]
+ f.LineTable = t
+ f.FrameSize = int(t.binary.Uint32(info[t.ptrsize+2*4:]))
+ f.Sym = &Sym{
+ Value: f.Entry,
+ Type: 'T',
+ Name: t.string(t.binary.Uint32(info[t.ptrsize:])),
+ GoType: 0,
+ Func: f,
+ }
+ }
+ return funcs
+}
+
// findFunc returns the func corresponding to the given program counter.
func (t *LineTable) findFunc(pc uint64) []byte {
if pc < t.uintptr(t.functab) || pc >= t.uintptr(t.functab[len(t.functab)-int(t.ptrsize):]) {
src/pkg/debug/gosym/symtab.go
(変更)
NewTable
関数内でgo12Funcs
の利用が追加され、walksymtab
で空のシンボルテーブルを許容するようになりました。
--- a/src/pkg/debug/gosym/symtab.go
+++ b/src/pkg/debug/gosym/symtab.go
@@ -129,6 +129,9 @@ var (
)
func walksymtab(data []byte, fn func(sym) error) error {
+ if len(data) == 0 { // missing symtab is okay
+ return nil
+ }
var order binary.ByteOrder = binary.BigEndian
newTable := false
switch {
@@ -455,6 +458,10 @@ func NewTable(symtab []byte, pcln *LineTable) (*Table, error) {
i = end - 1 // loop will i++
}
}
+
+ if t.go12line != nil && nf == 0 {
+ t.Funcs = t.go12line.go12Funcs()
+ }
if obj != nil {
obj.Funcs = t.Funcs[lastf:]
}
src/pkg/debug/elf/file.go
, src/pkg/debug/macho/file.go
, src/pkg/debug/pe/file.go
(変更)
Data()
メソッド内のReadAt
エラーハンドリングが修正されました。
--- a/src/pkg/debug/elf/file.go
+++ b/src/pkg/debug/elf/file.go
@@ -76,6 +76,9 @@ type Section struct {
func (s *Section) Data() ([]byte, error) {
dat := make([]byte, s.sr.Size())
n, err := s.sr.ReadAt(dat, 0)
+ if n == len(dat) {
+ err = nil
+ }
return dat[0:n], err
}
(macho/file.go
とpe/file.go
も同様の変更)
コアとなるコードの解説
src/cmd/ld/symtab.c
の削除について
このファイルから削除されたコードは、GoリンカがPlan 9形式のシンボルテーブルを構築し、バイナリに書き込むためのものでした。
scput
,slputb
,slputl
: これらは、シンボルテーブルにバイトや32ビット整数を書き込むためのヘルパー関数でした。slputb
はビッグエンディアン、slputl
はリトルエンディアンで書き込みます。putsymb
: この関数は、個々のシンボル(関数、変数など)の情報をPlan 9シンボルテーブル形式でエンコードし、書き込む役割を担っていました。シンボルの種類(t
)、値(v
)、名前(name
)、Goの型情報(typ
)などを処理していました。symtab
関数内の削除された部分: ここでは、Plan 9シンボルテーブルのヘッダ(0xfffffffd
で始まる)を書き込み、genasmsym
を呼び出してすべてのアセンブリシンボルをテーブルに追加していました。
これらのコードが削除されたことで、GoリンカはPlan 9シンボルテーブルを生成しなくなり、結果としてバイナリサイズが削減されます。
src/pkg/debug/gosym/pclntab.go
の go12Funcs
について
この新しい関数は、Go 1.2以降のpclntab
(Program Counter Line Table)の構造を利用して、関数に関するシンボル情報(エントリポイントアドレス、終了アドレス、フレームサイズ、関数名など)を動的に合成します。
t.functab
:pclntab
内に含まれる関数テーブルで、各関数のエントリポイントと関連情報へのオフセットが格納されています。t.uintptr
,t.binary.Uint32
,t.string
: これらはLineTable
構造体のメソッドで、pclntab
の生バイトデータからポインタ、32ビット整数、文字列を読み出すためのヘルパーです。Sym
構造体: Goのシンボル情報を表す構造体です。go12Funcs
は、pclntab
から読み取った情報を使って、各関数に対応するSym
インスタンスを生成します。Type: 'T'
は、これがテキスト(コード)シンボルであることを示します。
この関数は、Plan 9シンボルテーブルが利用できない場合に、pclntab
から必要最低限の関数シンボル情報を取得するための重要な代替手段となります。
src/pkg/debug/gosym/symtab.go
の変更について
walksymtab
関数の変更:if len(data) == 0 { return nil }
という行が追加されました。これは、シンボルテーブルのデータが空の場合(つまり、Plan 9シンボルテーブルが存在しない場合)に、エラーを返さずに正常に処理を終了するようにします。これにより、シンボルテーブルがオプションであることを明示し、より堅牢な処理が可能になります。NewTable
関数の変更:if t.go12line != nil && nf == 0 { t.Funcs = t.go12line.go12Funcs() }
という行が追加されました。t.go12line != nil
:pclntab
がGo 1.2形式で初期化されていることを確認します。nf == 0
: 従来のPlan 9シンボルテーブルから関数が一つも読み込まれなかったことを意味します。- この条件が満たされた場合、
go12Funcs()
を呼び出してpclntab
から関数シンボルを合成し、それをTable.Funcs
に設定します。これにより、go tool nm
のようなツールが、Plan 9シンボルテーブルがなくてもGoの関数情報を正しく表示できるようになります。
debug/elf
, debug/macho
, debug/pe
の ReadAt
エラーハンドリングについて
これらの変更は、各OSネイティブのバイナリフォーマットを解析する際の堅牢性を高めるものです。
n, err := s.sr.ReadAt(dat, 0)
: これは、セクションやセグメントのデータを読み込むための一般的なパターンです。n
は実際に読み込まれたバイト数、err
は発生したエラーです。if n == len(dat) { err = nil }
: この条件は、要求されたバイト数(len(dat)
)がすべて正常に読み込まれた場合、たとえReadAt
がio.EOF
を返したとしても、それをエラーとは見なさず、nil
に上書きするという意味です。io.EOF
は、読み込み操作がファイルの終端に達したことを示すエラーですが、これは必ずしも問題を示すものではありません。特に、セクションやセグメントのサイズがファイルの残りの部分と正確に一致する場合、ReadAt
は最後のバイトを読み込んだ後にio.EOF
を返すことがあります。この場合、データは完全に読み込まれているため、エラーとして扱う必要はありません。- この修正により、これらのデバッグパッケージがバイナリファイルを解析する際に、不必要なエラーで中断することなく、よりスムーズに処理を進められるようになります。
これらの変更は全体として、GoのツールチェインがPlan 9の遺産から脱却し、より現代的で効率的なOSネイティブのバイナリフォーマットとGoランタイム自身のメタデータ(pclntab
)を活用する方向へと進化していることを示しています。
関連リンク
- Go Issue #6853: https://github.com/golang/go/issues/6853 (このコミットが解決したIssue)
- Go Code Review 65740045: https://golang.org/cl/65740045 (このコミットのコードレビューページ)
参考にした情報源リンク
- Go言語のツールチェインとPlan 9の影響: https://go.dev/doc/asm
- Goバイナリのシンボルテーブルと
go tool nm
: https://medium.com/@jason_watkins/go-binary-analysis-part-1-symbol-tables-and-go-tool-nm-622222222222 - Goの
pclntab
について: https://go.dev/src/runtime/symtab.go (Goのソースコード内のpclntab
関連ファイル) - ELFファイルフォーマット: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
- Mach-Oファイルフォーマット: https://en.wikipedia.org/wiki/Mach-O
- PEファイルフォーマット: https://en.wikipedia.org/wiki/Portable_Executable
io.ReaderAt
とio.EOF
の挙動: Goの公式ドキュメントや関連するブログ記事