[インデックス 16091] ファイルの概要
コミット
このコミットは、Go言語のツールチェインの一部である cmd/nm (シンボル表示ツール) におけるバグ修正です。具体的には、新しいコンパイラが生成する追加の 'm' シンボルが誤ってファイル名要素として扱われ、結果としてメモリ不足エラーを引き起こす問題を解決します。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/272687ec24ca828f8c7ba22c64e0060436124051
元コミット内容
commit 272687ec24ca828f8c7ba22c64e0060436124051
Author: Anthony Martin <ality@pbrane.org>
Date: Wed Apr 3 18:23:43 2013 -0700
cmd/nm: don't add filename elements for m symbols
The compilers used to generate only one 'm' symbol
to record the stack frame size for each function.
In cmd/nm, the 'm' and 'f' symbols are handled in
the same switch case with a special exception for
the symbol described above called ".frame".
Now that the compilers emit additional 'm' symbols
for precise garbage collection of the stack, the
current logic is incorrect. cmd/nm will attempt to
interpret these new 'm' symbols as 'f' symbols and
add them to the file name index table.
This fails with an out-of-memory condition when
zenter encounters an 'm' symbol with a very large
value (usually the .args symbol indicating a
variadic NOSPLIT function).
R=iant
CC=dave, gobot, golang-dev, rsc
https://golang.org/cl/7962045
変更の背景
この変更は、Goコンパイラの進化、特にガベージコレクション (GC) の精度向上に伴うものです。
以前のGoコンパイラは、各関数のスタックフレームサイズを記録するために、通常1つの 'm' シンボルを生成していました。cmd/nm ツールは、この 'm' シンボルと 'f' シンボル (ファイル名要素に関連するシンボル) を同じコードパスで処理し、特に .frame というシンボルに対して特別な例外を設けていました。.frame シンボルは、スタックフレームに関する情報を示すために使用されていました。
しかし、Goコンパイラがスタックのより正確なガベージコレクション (Precise GC) をサポートするために進化し、追加の 'm' シンボル (スタックマップ情報など) を生成するようになりました。これらの新しい 'm' シンボルは、従来の 'f' シンボルとは異なる目的を持っていますが、cmd/nm の既存のロジックでは、これらを誤って 'f' シンボルとして解釈し、ファイル名インデックステーブルに追加しようとしました。
この誤った処理は、特に .args シンボル (可変引数を持つ NOSPLIT 関数を示すことが多い) のように非常に大きな値を持つ 'm' シンボルに遭遇した場合に、zenter 関数内でメモリ不足 (Out-of-Memory, OOM) の状態を引き起こしました。zenter 関数は、シンボルをファイル名インデックステーブルに登録する役割を担っていると考えられます。この問題は、cmd/nm がバイナリを解析する際にクラッシュする原因となっていました。
このコミットは、このような新しい 'm' シンボルがファイル名要素として誤って扱われることを防ぎ、cmd/nm の安定性と正確性を向上させることを目的としています。
前提知識の解説
1. cmd/nm とは
nm は、Unix系システムで広く使われているコマンドラインユーティリティで、オブジェクトファイル、静的ライブラリ、共有ライブラリなどのバイナリファイルからシンボル (関数名、変数名など) のリストを表示するために使用されます。Go言語の cmd/nm は、Goのバイナリファイル (実行可能ファイルやライブラリ) に特化して、その内部のシンボル情報を解析し表示するツールです。デバッグやバイナリの構造解析に役立ちます。
2. Goバイナリにおけるシンボル
Goのコンパイラは、生成するバイナリファイル内に様々な種類のシンボルを埋め込みます。これらのシンボルは、プログラムの構造、関数、変数、型情報、デバッグ情報などを表します。nm ツールはこれらのシンボルを読み取り、その種類 (データ、テキスト、未定義など) やアドレス、サイズなどを表示します。
3. 'm' シンボルと 'f' シンボル
Goの nm ツールが扱うシンボルには、特定の意味を持つタイプがあります。
- 'm' シンボル: 伝統的に、Goのコンパイラは各関数のスタックフレームサイズを記録するために 'm' シンボルを使用していました。しかし、このコミットの背景にあるように、Goのガベージコレクションの進化に伴い、スタック上のポインタを正確に識別するための「スタックマップ」情報も 'm' シンボルとして表現されるようになりました。これは、GCがスタックをスキャンする際に、どのメモリ位置がポインタであり、どのメモリ位置がポインタでないかを正確に判断するために不可欠です。
- 'f' シンボル: ファイル名要素 (filename elements) に関連するシンボルです。Goのバイナリには、デバッグ情報としてソースファイル名や行番号の情報が含まれることがあります。'f' シンボルは、これらのファイル名情報への参照や、特定のコードセクションがどのソースファイルに由来するかを示すために使用されることがあります。
4. Precise Garbage Collection (正確なガベージコレクション)
Goのガベージコレクタは、不要になったメモリを自動的に解放する役割を担っています。初期のGoのGCは、スタック上のポインタを「保守的」に扱っていました。これは、スタック上の値がポインタであるかどうかが不明な場合でも、それがポインタである可能性があれば、そのメモリを解放しないというアプローチです。これにより、誤って使用中のメモリを解放してしまうリスクは減りますが、実際にはポインタではない値がポインタとして扱われ、メモリリークやGCの効率低下につながる可能性がありました。
「正確なガベージコレクション」とは、GCがスタック上のすべての値について、それがポインタであるか否かを正確に識別できることを意味します。これを実現するために、コンパイラは各関数のスタックフレームのレイアウトに関するメタデータ (スタックマップ) を生成し、バイナリに埋め込みます。GCは実行時にこのスタックマップを参照することで、スタック上のどの位置にポインタが存在するかを正確に把握し、より効率的かつ正確にメモリを回収できるようになります。このスタックマップ情報が、追加の 'm' シンボルとして表現されるようになりました。
5. .frame シンボルと .args シンボル
.frameシンボル: 以前のGoコンパイラがスタックフレーム情報を表現するために使用していたシンボル名の一つです。このコミットの変更前は、cmd/nmが 'm' シンボルと 'f' シンボルを処理する際に、.frameという名前のシンボルに対して特別な処理を行っていました。.argsシンボル: 関数呼び出しの引数に関する情報を示すシンボルです。特に、可変引数 (variadic) を持つ関数や、スタックフレームのレイアウトが特殊な関数 (例:NOSPLIT関数) の場合に、その引数の情報が.argsシンボルとして表現されることがあります。コミットメッセージにあるように、このシンボルが非常に大きな値を持つことがあり、これがメモリ不足の原因となりました。
6. NOSPLIT 関数
Goの関数は、通常、必要に応じてスタックを自動的に拡張 (スプリット) します。しかし、一部の低レベルな関数や、スタックの拡張がパフォーマンスに影響を与える可能性がある関数 (例: ランタイムのクリティカルパスにある関数) では、コンパイラディレクティブ (//go:nosplit) を使用してスタックのスプリットを無効にすることができます。このような関数は NOSPLIT 関数と呼ばれ、スタックフレームの管理が通常とは異なる場合があります。
技術的詳細
このコミットの核心は、src/cmd/nm/nm.c ファイル内の psym 関数における 'm' シンボルと 'f' シンボルの処理ロジックの分離と修正です。
変更前のコードでは、case 'm': と case 'f': が連続しており、共通の処理ブロックを共有していました。このブロックでは、特定のフラグ (aflag, uflag, gflag) の状態に応じて処理をスキップし、その後 strcmp(s->name, ".frame") を使ってシンボル名が ".frame" でない場合にのみ zenter(s) を呼び出していました。zenter(s) は、シンボル s をファイル名インデックステーブルに追加する役割を持つ関数です。
新しいコンパイラがスタックの正確なGCのために追加の 'm' シンボル (スタックマップなど) を生成するようになった際、これらの新しい 'm' シンボルは .frame ではないため、従来のロジックでは zenter(s) が呼び出されてしまい、ファイル名インデックステーブルに誤って追加されることになりました。特に、.args シンボルのように非常に大きな値を持つ 'm' シンボルがファイル名インデックステーブルに追加されると、メモリを大量に消費し、最終的にメモリ不足エラーを引き起こしました。
このコミットによる修正は以下の通りです。
- 'm' シンボルと 'f' シンボルの処理の分離:
case 'm':ブロックがcase 'f':ブロックから完全に分離されました。 - 'm' シンボルに対する
zenter呼び出しの削除:case 'm':ブロック内では、フラグチェック (!aflag || uflag || gflag) の後、すぐにreturn;が実行されるようになりました。これにより、'm' シンボルに対してはzenter(s)が決して呼び出されなくなり、ファイル名インデックステーブルへの追加が完全に防止されます。 - 'f' シンボルに対する
.frame例外の削除:case 'f':ブロックでは、以前存在したif (strcmp(s->name, ".frame"))という条件が削除されました。これは、'm' シンボルがファイル名インデックステーブルに追加されなくなったため、.frameシンボルに対する特別な処理が不要になったことを意味します。'f' シンボルは、フラグチェックを通過すれば常にzenter(s)によってファイル名インデックステーブルに追加されるようになりました。
この修正により、cmd/nm は新しいコンパイラが生成する 'm' シンボルを正しく無視し、ファイル名インデックステーブルの破損やメモリ不足エラーを防ぐことができます。
コアとなるコードの変更箇所
変更は src/cmd/nm/nm.c ファイルの psym 関数内、switch(s->type) ステートメントの一部です。
--- a/src/cmd/nm/nm.c
+++ b/src/cmd/nm/nm.c
@@ -275,11 +275,13 @@ psym(Sym *s, void* p)
return;
break;
case 'm':
+ if(!aflag || uflag || gflag)
+ return;
+ break;
case 'f': /* we only see a 'z' when the following is true*/
if(!aflag || uflag || gflag)
return;
- if (strcmp(s->name, ".frame"))
- zenter(s);
+ zenter(s);
break;
case 'a':
case 'p':
コアとなるコードの解説
-
変更前:
case 'm': case 'f': /* we only see a 'z' when the following is true*/ if(!aflag || uflag || gflag) return; if (strcmp(s->name, ".frame")) zenter(s); break;このコードでは、
'm'と'f'の両方のシンボルタイプが同じ処理パスを共有していました。if(!aflag || uflag || gflag)は、特定の表示オプション (-a(all),-u(undefined),-g(global)) が設定されているかどうかに応じて、シンボルの処理をスキップするための条件です。 その後のif (strcmp(s->name, ".frame"))は、シンボル名が".frame"でない場合にのみzenter(s)を呼び出すという特殊なロジックでした。これは、".frame"シンボルがファイル名要素として扱われるべきではないという古い仮定に基づいています。 -
変更後:
case 'm': if(!aflag || uflag || gflag) return; break; case 'f': /* we only see a 'z' when the following is true*/ if(!aflag || uflag || gflag) return; zenter(s); break;case 'm':ブロックが独立しました。このブロックでは、フラグチェック (!aflag || uflag || gflag) を通過した場合でも、zenter(s)は呼び出されず、すぐにbreak;します。これにより、'm' シンボルはファイル名インデックステーブルに一切追加されなくなりました。case 'f':ブロックも独立しました。ここでは、フラグチェックを通過すれば、無条件にzenter(s)が呼び出されます。以前のif (strcmp(s->name, ".frame"))という条件は削除されました。これは、'm' シンボルが適切に処理されるようになったため、'f' シンボルが".frame"であるかどうかを区別する必要がなくなったためです。
この変更により、cmd/nm は、スタックの正確なGCのために導入された新しい 'm' シンボルを、ファイル名要素として誤って解釈し、メモリ不足を引き起こす問題を回避できるようになりました。
関連リンク
- Go言語の
nmコマンドに関する公式ドキュメント (もしあれば、Goのツールに関するページ) - Go言語のガベージコレクションの進化に関する記事や提案 (特に2013年頃のPrecise GC導入に関するもの)
- Goのシンボルテーブルの構造に関する情報
参考にした情報源リンク
- golang/go commit 272687ec24ca828f8c7ba22c64e0060436124051
- golang.org/cl/7962045 (Go Code Review - cmd/nm: don't add filename elements for m symbols)
- Go言語のガベージコレクションに関する一般的な知識 (例: Goの公式ブログ、Goのドキュメント、関連する論文など)
- Unix
nmコマンドに関する一般的な知識 - Goのコンパイラとランタイムに関する一般的な知識# [インデックス 16091] ファイルの概要
コミット
このコミットは、Go言語のツールチェインの一部である cmd/nm (シンボル表示ツール) におけるバグ修正です。具体的には、新しいコンパイラが生成する追加の 'm' シンボルが誤ってファイル名要素として扱われ、結果としてメモリ不足エラーを引き起こす問題を解決します。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/272687ec24ca828f8c7ba22c64e0060436124051
元コミット内容
commit 272687ec24ca828f8c7ba22c64e0060436124051
Author: Anthony Martin <ality@pbrane.org>
Date: Wed Apr 3 18:23:43 2013 -0700
cmd/nm: don't add filename elements for m symbols
The compilers used to generate only one 'm' symbol
to record the stack frame size for each function.
In cmd/nm, the 'm' and 'f' symbols are handled in
the same switch case with a special exception for
the symbol described above called ".frame".
Now that the compilers emit additional 'm' symbols
for precise garbage collection of the stack, the
current logic is incorrect. cmd/nm will attempt to
interpret these new 'm' symbols as 'f' symbols and
add them to the file name index table.
This fails with an out-of-memory condition when
zenter encounters an 'm' symbol with a very large
value (usually the .args symbol indicating a
variadic NOSPLIT function).
R=iant
CC=dave, gobot, golang-dev, rsc
https://golang.org/cl/7962045
変更の背景
この変更は、Goコンパイラの進化、特にガベージコレクション (GC) の精度向上に伴うものです。
以前のGoコンパイラは、各関数のスタックフレームサイズを記録するために、通常1つの 'm' シンボルを生成していました。cmd/nm ツールは、この 'm' シンボルと 'f' シンボル (ファイル名要素に関連するシンボル) を同じコードパスで処理し、特に .frame というシンボルに対して特別な例外を設けていました。.frame シンボルは、スタックフレームに関する情報を示すために使用されていました。
しかし、Goコンパイラがスタックのより正確なガベージコレクション (Precise GC) をサポートするために進化し、追加の 'm' シンボル (スタックマップ情報など) を生成するようになりました。これらの新しい 'm' シンボルは、従来の 'f' シンボルとは異なる目的を持っていますが、cmd/nm の既存のロジックでは、これらを誤って 'f' シンボルとして解釈し、ファイル名インデックステーブルに追加しようとしました。
この誤った処理は、特に .args シンボル (可変引数を持つ NOSPLIT 関数を示すことが多い) のように非常に大きな値を持つ 'm' シンボルに遭遇した場合に、zenter 関数内でメモリ不足 (Out-of-Memory, OOM) の状態を引き起こしました。zenter 関数は、シンボルをファイル名インデックステーブルに登録する役割を担っていると考えられます。この問題は、cmd/nm がバイナリを解析する際にクラッシュする原因となっていました。
このコミットは、このような新しい 'm' シンボルがファイル名要素として誤って扱われることを防ぎ、cmd/nm の安定性と正確性を向上させることを目的としています。
前提知識の解説
1. cmd/nm とは
nm は、Unix系システムで広く使われているコマンドラインユーティリティで、オブジェクトファイル、静的ライブラリ、共有ライブラリなどのバイナリファイルからシンボル (関数名、変数名など) のリストを表示するために使用されます。Go言語の cmd/nm は、Goのバイナリファイル (実行可能ファイルやライブラリ) に特化して、その内部のシンボル情報を解析し表示するツールです。デバッグやバイナリの構造解析に役立ちます。
2. Goバイナリにおけるシンボル
Goのコンパイラは、生成するバイナリファイル内に様々な種類のシンボルを埋め込みます。これらのシンボルは、プログラムの構造、関数、変数、型情報、デバッグ情報などを表します。nm ツールはこれらのシンボルを読み取り、その種類 (データ、テキスト、未定義など) やアドレス、サイズなどを表示します。
3. 'm' シンボルと 'f' シンボル
Goの nm ツールが扱うシンボルには、特定の意味を持つタイプがあります。
- 'm' シンボル: 伝統的に、Goのコンパイラは各関数のスタックフレームサイズを記録するために 'm' シンボルを使用していました。しかし、このコミットの背景にあるように、Goのガベージコレクションの進化に伴い、スタック上のポインタを正確に識別するための「スタックマップ」情報も 'm' シンボルとして表現されるようになりました。これは、GCがスタックをスキャンする際に、どのメモリ位置がポインタであり、どのメモリ位置がポインタでないかを正確に判断するために不可欠です。
- 'f' シンボル: ファイル名要素 (filename elements) に関連するシンボルです。Goのバイナリには、デバッグ情報としてソースファイル名や行番号の情報が含まれることがあります。'f' シンボルは、これらのファイル名情報への参照や、特定のコードセクションがどのソースファイルに由来するかを示すために使用されることがあります。
4. Precise Garbage Collection (正確なガベージコレクション)
Goのガベージコレクタは、不要になったメモリを自動的に解放する役割を担っています。初期のGoのGCは、スタック上のポインタを「保守的」に扱っていました。これは、スタック上の値がポインタであるかどうかが不明な場合でも、それがポインタである可能性があれば、そのメモリを解放しないというアプローチです。これにより、誤って使用中のメモリを解放してしまうリスクは減りますが、実際にはポインタではない値がポインタとして扱われ、メモリリークやGCの効率低下につながる可能性がありました。
「正確なガベージコレクション」とは、GCがスタック上のすべての値について、それがポインタであるか否かを正確に識別できることを意味します。これを実現するために、コンパイラは各関数のスタックフレームのレイアウトに関するメタデータ (スタックマップ) を生成し、バイナリに埋め込みます。GCは実行時にこのスタックマップを参照することで、スタック上のどの位置にポインタが存在するかを正確に把握し、より効率的かつ正確にメモリを回収できるようになります。このスタックマップ情報が、追加の 'm' シンボルとして表現されるようになりました。
5. .frame シンボルと .args シンボル
.frameシンボル: 以前のGoコンパイラがスタックフレーム情報を表現するために使用していたシンボル名の一つです。このコミットの変更前は、cmd/nmが 'm' シンボルと 'f' シンボルを処理する際に、.frameという名前のシンボルに対して特別な処理を行っていました。.argsシンボル: 関数呼び出しの引数に関する情報を示すシンボルです。特に、可変引数 (variadic) を持つ関数や、スタックフレームのレイアウトが特殊な関数 (例:NOSPLIT関数) の場合に、その引数の情報が.argsシンボルとして表現されることがあります。コミットメッセージにあるように、このシンボルが非常に大きな値を持つことがあり、これがメモリ不足の原因となりました。
6. NOSPLIT 関数
Goの関数は、通常、必要に応じてスタックを自動的に拡張 (スプリット) します。しかし、一部の低レベルな関数や、スタックの拡張がパフォーマンスに影響を与える可能性がある関数 (例: ランタイムのクリティカルパスにある関数) では、コンパイラディレクティブ (//go:nosplit) を使用してスタックのスプリットを無効にすることができます。このような関数は NOSPLIT 関数と呼ばれ、スタックフレームの管理が通常とは異なる場合があります。
技術的詳細
このコミットの核心は、src/cmd/nm/nm.c ファイル内の psym 関数における 'm' シンボルと 'f' シンボルの処理ロジックの分離と修正です。
変更前のコードでは、case 'm': と case 'f': が連続しており、共通の処理ブロックを共有していました。このブロックでは、特定のフラグ (aflag, uflag, gflag) の状態に応じて処理をスキップし、その後 strcmp(s->name, ".frame") を使ってシンボル名が ".frame" でない場合にのみ zenter(s) を呼び出していました。zenter(s) は、シンボル s をファイル名インデックステーブルに追加する役割を持つ関数です。
新しいコンパイラがスタックの正確なGCのために追加の 'm' シンボル (スタックマップなど) を生成するようになった際、これらの新しい 'm' シンボルは .frame ではないため、従来のロジックでは zenter(s) が呼び出されてしまい、ファイル名インデックステーブルに誤って追加されることになりました。特に、.args シンボルのように非常に大きな値を持つ 'm' シンボルがファイル名インデックステーブルに追加されると、メモリを大量に消費し、最終的にメモリ不足エラーを引き起こしました。
このコミットによる修正は以下の通りです。
- 'm' シンボルと 'f' シンボルの処理の分離:
case 'm':ブロックがcase 'f':ブロックから完全に分離されました。 - 'm' シンボルに対する
zenter呼び出しの削除:case 'm':ブロック内では、フラグチェック (!aflag || uflag || gflag) の後、すぐにreturn;が実行されるようになりました。これにより、'm' シンボルに対してはzenter(s)が決して呼び出されなくなり、ファイル名インデックステーブルへの追加が完全に防止されます。 - 'f' シンボルに対する
.frame例外の削除:case 'f':ブロックでは、以前存在したif (strcmp(s->name, ".frame"))という条件が削除されました。これは、'm' シンボルがファイル名インデックステーブルに追加されなくなったため、.frameシンボルに対する特別な処理が不要になったことを意味します。'f' シンボルは、フラグチェックを通過すれば常にzenter(s)によってファイル名インデックステーブルに追加されるようになりました。
この修正により、cmd/nm は新しいコンパイラが生成する 'm' シンボルを正しく無視し、ファイル名インデックステーブルの破損やメモリ不足エラーを防ぐことができます。
コアとなるコードの変更箇所
変更は src/cmd/nm/nm.c ファイルの psym 関数内、switch(s->type) ステートメントの一部です。
--- a/src/cmd/nm/nm.c
+++ b/src/cmd/nm/nm.c
@@ -275,11 +275,13 @@ psym(Sym *s, void* p)
return;
break;
case 'm':
+ if(!aflag || uflag || gflag)
+ return;
+ break;
case 'f': /* we only see a 'z' when the following is true*/
if(!aflag || uflag || gflag)
return;
- if (strcmp(s->name, ".frame"))
- zenter(s);
+ zenter(s);
break;
case 'a':
case 'p':
コアとなるコードの解説
-
変更前:
case 'm': case 'f': /* we only see a 'z' when the following is true*/ if(!aflag || uflag || gflag) return; if (strcmp(s->name, ".frame")) zenter(s); break;このコードでは、
'm'と'f'の両方のシンボルタイプが同じ処理パスを共有していました。if(!aflag || uflag || gflag)は、特定の表示オプション (-a(all),-u(undefined),-g(global)) が設定されているかどうかに応じて、シンボルの処理をスキップするための条件です。 その後のif (strcmp(s->name, ".frame"))は、シンボル名が".frame"でない場合にのみzenter(s)を呼び出すという特殊なロジックでした。これは、".frame"シンボルがファイル名要素として扱われるべきではないという古い仮定に基づいています。 -
変更後:
case 'm': if(!aflag || uflag || gflag) return; break; case 'f': /* we only see a 'z' when the following is true*/ if(!aflag || uflag || gflag) return; zenter(s); break;case 'm':ブロックが独立しました。このブロックでは、フラグチェック (!aflag || uflag || gflag) を通過した場合でも、zenter(s)は呼び出されず、すぐにbreak;します。これにより、'm' シンボルはファイル名インデックステーブルに一切追加されなくなりました。case 'f':ブロックも独立しました。ここでは、フラグチェックを通過すれば、無条件にzenter(s)が呼び出されます。以前のif (strcmp(s->name, ".frame"))という条件は削除されました。これは、'm' シンボルが適切に処理されるようになったため、'f' シンボルが".frame"であるかどうかを区別する必要がなくなったためです。
この変更により、cmd/nm は、スタックの正確なGCのために導入された新しい 'm' シンボルを、ファイル名要素として誤って解釈し、メモリ不足を引き起こす問題を回避できるようになりました。
関連リンク
- Go言語の
nmコマンドに関する公式ドキュメント (もしあれば、Goのツールに関するページ) - Go言語のガベージコレクションの進化に関する記事や提案 (特に2013年頃のPrecise GC導入に関するもの)
- Goのシンボルテーブルの構造に関する情報
参考にした情報源リンク
- golang/go commit 272687ec24ca828f8c7ba22c64e0060436124051
- golang.org/cl/7962045 (Go Code Review - cmd/nm: don't add filename elements for m symbols)
- Go言語のガベージコレクションに関する一般的な知識 (例: Goの公式ブログ、Goのドキュメント、関連する論文など)
- Unix
nmコマンドに関する一般的な知識 - Goのコンパイラとランタイムに関する一般的な知識