[インデックス 17628] ファイルの概要
このコミットは、Go言語のツールチェインの一部である cmd/nm コマンドの出力を改善するものです。具体的には、-S フラグ(シンボルのサイズを表示するオプション)を使用した際に、text や etext といったコンテナシンボルが、その内部に含まれる個別のシンボルを隠してしまう問題を修正します。これにより、より正確で有用なシンボルリストが生成されるようになります。
コミット
commit 00061219f03d666e93947a3cb326256062a7a92c
Author: Russ Cox <rsc@golang.org>
Date: Mon Sep 16 20:27:57 2013 -0400
cmd/nm: make -S listing a bit more accurate
Hide container symbols like text and etext so that
the individual pieces inside are shown instead.
For example, if text and main.init have the same
address, it was a toss-up which name was printed.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/13722046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/00061219f03d666e93947a3cb326256062a7a92c
元コミット内容
cmd/nm: make -S listing a bit more accurate
このコミットは、cmd/nm コマンドの -S オプションによるシンボルリストの表示をより正確にするものです。text や etext のようなコンテナシンボルを非表示にすることで、それらの内部に含まれる個々のシンボルが表示されるようにします。例えば、text と main.init が同じアドレスを持つ場合、以前はどちらの名前が表示されるか不確定でした。
変更の背景
Go言語の nm コマンドは、コンパイルされたGoプログラムのシンボルテーブルを検査するために使用されます。シンボルテーブルには、関数、変数、セクションなどの情報が含まれており、デバッグやパフォーマンス分析に役立ちます。
-S フラグは、各シンボルのサイズを表示するために使用されます。しかし、これまでの実装では、text (コードセクション全体) や data (初期化済みデータセクション全体) といった、より大きな「コンテナ」シンボルが、その中に含まれる個々の関数や変数のシンボルと同じアドレスを持つ場合がありました。この状況では、nm コマンドはどちらのシンボルを表示すべきか判断に迷い、結果として個々のシンボルが隠れてしまうことがありました。
例えば、text シンボルがプログラムのコードセクションの開始アドレスを指し、main.init 関数も同じアドレスから始まる場合、nm -S の出力では text シンボルが表示され、main.init のような重要な個別のシンボルがリストから漏れてしまう可能性がありました。これは、開発者が特定の関数や変数のサイズを正確に把握したい場合に問題となります。
このコミットは、このような「コンテナ」シンボルを意図的にスキップすることで、より詳細で正確なシンボル情報を提供し、開発者がプログラムの構造をより深く理解できるようにすることを目的としています。
前提知識の解説
nm コマンド
nm は "name mangler" または "name list" の略で、Unix系システムで実行可能ファイル、オブジェクトファイル、ライブラリファイルなどのシンボルテーブルをリスト表示するためのコマンドラインユーティリティです。Go言語のツールチェインにも cmd/nm として同様の機能が実装されています。
nm コマンドは、主に以下の情報を表示します。
- アドレス: シンボルがメモリ上で配置されているアドレス。
- 型: シンボルの種類(例:
Tはテキストセクションのグローバル関数、Dは初期化済みデータセクションのグローバル変数、Bは初期化されていないデータセクションのグローバル変数など)。 - 名前: シンボルの名前。
シンボルテーブル
シンボルテーブルは、コンパイルされたプログラム内のシンボル(関数名、変数名、セクション名など)と、それらがメモリ上でどこに配置されているか(アドレス)をマッピングするデータ構造です。リンカはシンボルテーブルを使用して、異なるオブジェクトファイル間で参照されるシンボルを解決します。デバッガやプロファイラは、シンボルテーブルを利用して、実行中のプログラムのメモリ上の位置をソースコードのエンティティにマッピングします。
コンテナシンボルと内部シンボル
実行可能ファイルやオブジェクトファイルには、プログラム全体のセクションを表すシンボルが存在します。例えば、.text セクション全体を表す text シンボルや、.data セクション全体を表す data シンボルなどです。これらは「コンテナシンボル」と呼ぶことができます。
一方、個々の関数(例: main.init, fmt.Println)やグローバル変数(例: main.myVar)は、これらのコンテナセクションの内部に配置されます。これらを「内部シンボル」と呼びます。
問題は、コンテナシンボルと内部シンボルが同じ開始アドレスを持つ場合があることです。例えば、.text セクションがアドレス 0x1000 から始まり、プログラムのエントリポイントである main.main 関数もアドレス 0x1000 から始まる場合、nm は text と main.main のどちらを表示すべきかという曖昧さが発生します。このコミット以前は、この曖昧さにより、より詳細な内部シンボルが隠れてしまうことがありました。
nelem マクロ
nelem は、C言語で配列の要素数を計算するためによく使われるマクロです。
#define nelem(x) (sizeof(x)/sizeof((x)[0]))
これは、配列 x の全体のサイズを、配列の最初の要素のサイズで割ることで、要素の数を導き出します。
技術的詳細
このコミットの核心は、cmd/nm/nm.c ファイルに skipnames という文字列配列と skipsize という関数を追加し、既存の printsyms 関数を修正することです。
-
skipnames配列の導入:text,data,bss,rodataなど、Goのコンパイラやリンカが生成するセクション全体を表すシンボル名がこの配列にリストアップされます。これらは、個々のシンボルを隠してしまう可能性のある「コンテナシンボル」と見なされます。const char *skipnames[] = { "bss", "data", "ebss", "edata", "egcbss", "egcdata", "enoptrbss", "enoptrdata", "epclntab", "erodata", "esymtab", "etext", "etypelink", "noptrbss", "noptrdata", "rodata", "text", }; -
skipsize関数の追加: この関数は、与えられたシンボル名がskipnames配列に含まれているかどうかをチェックします。含まれていれば1(true) を返し、そうでなければ0(false) を返します。int skipsize(char *name) { int i; for(i=0; i<nelem(skipnames); i++) if(strcmp(skipnames[i], name) == 0) return 1; return 0; }strcmpはC標準ライブラリの関数で、2つの文字列を比較します。 -
printsyms関数の修正:printsyms関数は、シンボルリストを実際に整形して出力する役割を担っています。この関数内で、シンボルのサイズを計算し表示するロジックが変更されました。変更前:
if(Sflag) { vlong siz; siz = 0; for(j=i+1; j<nsym; j++) { if(symptr[j]->type != 'a' && symptr[j]->type != 'p') { siz = symptr[j]->value - s->value; break; } }変更後:
if(Sflag && !skipsize(cp)) { // ここが追加 vlong siz; siz = 0; for(j=i+1; j<nsym; j++) { if(!skipsize(symptr[j]->name) && symptr[j]->type != 'a' && symptr[j]->type != 'p') { // ここが変更 siz = symptr[j]->value - s->value; break; } }-
if(Sflag && !skipsize(cp))の追加:-Sフラグが指定されており、かつ現在のシンボルcpがskipnamesリストに含まれていない場合にのみ、シンボルのサイズ計算と表示を行うように変更されました。これにより、textやdataといったコンテナシンボル自体のサイズは表示されなくなります。 -
if(!skipsize(symptr[j]->name) && symptr[j]->type != 'a' && symptr[j]->type != 'p')の変更: 次のシンボルsymptr[j]を探して現在のシンボルsとのサイズ差を計算する際にも、次のシンボルがskipnamesリストに含まれていないことを条件に追加しました。これは、サイズ計算の基準となる次のシンボルがコンテナシンボルである場合に、不正確なサイズが計算されるのを防ぐためです。'a'と'p'はそれぞれアセンブラシンボルと擬似シンボルを表し、これらは通常、サイズ計算の対象外とされます。
-
これらの変更により、nm -S コマンドは、text や data のようなセクション全体を表すシンボルではなく、その内部に含まれる個々の関数や変数のシンボルとそのサイズを優先的に表示するようになります。
コアとなるコードの変更箇所
src/cmd/nm/nm.c ファイルが変更されています。
--- a/src/cmd/nm/nm.c
+++ b/src/cmd/nm/nm.c
@@ -299,6 +299,37 @@ psym(Sym *s, void* p)
symptr[nsym++] = s;
}
+const char *skipnames[] = {
+ "bss",
+ "data",
+ "ebss",
+ "edata",
+ "egcbss",
+ "egcdata",
+ "enoptrbss",
+ "enoptrdata",
+ "epclntab",
+ "erodata",
+ "esymtab",
+ "etext",
+ "etypelink",
+ "noptrbss",
+ "noptrdata",
+ "rodata",
+ "text",
+};
+
+int
+skipsize(char *name)
+{
+ int i;
+
+ for(i=0; i<nelem(skipnames); i++)
+ if(strcmp(skipnames[i], name) == 0)
+ return 1;
+ return 0;
+}
+
void
printsyms(Sym **symptr, long nsym)
{
@@ -332,12 +363,12 @@ printsyms(Sym **symptr, long nsym)
else
Bprint(&bout, "%*s ", wid, "");
if(Sflag) {
- vlong siz;
+ if(Sflag && !skipsize(cp)) { // 変更点1
siz = 0;
for(j=i+1; j<nsym; j++) {
- if(symptr[j]->type != 'a' && symptr[j]->type != 'p') {
+ if(!skipsize(symptr[j]->name) && symptr[j]->type != 'a' && symptr[j]->type != 'p') { // 変更点2
siz = symptr[j]->value - s->value;
break;
}
コアとなるコードの解説
-
skipnames配列: この配列は、nm -Sコマンドでサイズを表示する際にスキップすべきシンボル名を定義しています。これらは通常、プログラムの特定のセクション全体(例:.text、.data、.bss、.rodata)や、Goランタイム内部の特定の領域(例:pclntab、symtab、typelink)を表すシンボルです。これらのシンボルは、個々の関数や変数のシンボルとアドレスが重複しやすく、詳細なシンボル情報を隠してしまう可能性があるため、明示的に除外されます。 -
skipsize関数: このヘルパー関数は、与えられたシンボル名がskipnames配列に含まれているかを効率的にチェックします。strcmpを使用して文字列比較を行い、一致するものがあれば1を返します。これにより、printsyms関数内でシンボルをフィルタリングするロジックが簡潔になります。 -
printsyms関数の変更点:if(Sflag && !skipsize(cp))の追加: これは、現在のシンボルcpがskipnamesリストに含まれていない場合にのみ、シンボルのサイズを計算し表示する主要な変更です。これにより、textやdataといったコンテナシンボル自体のサイズは出力されなくなります。if(!skipsize(symptr[j]->name) && symptr[j]->type != 'a' && symptr[j]->type != 'p')の変更: シンボルのサイズは、現在のシンボルから次の有効なシンボルまでのアドレス差として計算されます。この変更は、次のシンボルsymptr[j]がskipnamesリストに含まれていないことを確認することで、サイズ計算の精度を向上させます。これにより、次のシンボルがコンテナシンボルであるために不正確なサイズが計算されるのを防ぎます。'a'と'p'はそれぞれアセンブラシンボルと擬似シンボルを表し、これらは通常、サイズ計算の対象外とされます。
これらの変更により、nm -S の出力は、より粒度の細かい、個々の関数や変数のサイズ情報に焦点を当てるようになり、デバッグや最適化の際に開発者にとってより有用な情報を提供します。
関連リンク
- Go言語の
cmd/nmのソースコード: https://github.com/golang/go/tree/master/src/cmd/nm - Go言語の
nmコマンドに関するドキュメント (もしあれば):go doc cmd/nmまたはgo help nmで確認できます。
参考にした情報源リンク
- Go言語のコミット履歴: https://github.com/golang/go/commits/master
- Unix
nmコマンドのmanページ (一般的な情報):man nm - C言語の
strcmp関数に関する情報: https://www.cplusplus.com/reference/cstring/strcmp/ - C言語の
sizeof演算子に関する情報: https://www.cplusplus.com/reference/cstddef/sizeof/ - Go言語のツールチェインに関する一般的な情報 (Goの公式ドキュメントなど)
- Go言語のランタイムとメモリレイアウトに関する情報 (Goの内部構造に関する記事やドキュメント)
- GoのIssueトラッカーやCL (Change List) の情報 (CL 13722046): https://golang.org/cl/13722046 (これはコミットメッセージに記載されているリンクです)