[インデックス 10223] ファイルの概要
このコミットは、Go言語のツールチェインの一部である gopack コマンドにおけるバグ修正に関するものです。具体的には、gopack がGoオブジェクトではないファイルに対してもGoメタデータを探索しようとする問題を解決しています。
コミット
commit d6ff3c11774bf36c797dcc6cd946819959766dc1
Author: Russ Cox <rsc@golang.org>
Date: Thu Nov 3 12:07:47 2011 -0400
gopack: do not look for Go metadata in non-Go objects
Fixes #2333.
R=r
CC=golang-dev
https://golang.org/cl/5316075
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d6ff3c11774bf36c797dcc6cd946819959766dc1
元コミット内容
gopack: do not look for Go metadata in non-Go objects
Fixes #2333.
R=r
CC=golang-dev
https://golang.org/cl/5316075
変更の背景
このコミットは、Go言語のIssue #2333を修正するために行われました。gopack は、Goのパッケージをアーカイブ(通常は .a ファイル)にまとめるためのツールです。このツールは、アーカイブ内の各オブジェクトファイルがGoオブジェクトであるかどうかを判断し、Goオブジェクトであればその中に含まれるGo固有のメタデータ(例えば、エクスポートされたシンボル情報など)を読み取ります。
しかし、修正前の gopack は、Goオブジェクトではない通常のオブジェクトファイル(例えば、C言語で書かれたコードをコンパイルして生成されたオブジェクトファイル)に対しても、Goメタデータが存在することを期待して処理を進めてしまうバグがありました。これにより、gopack が非Goオブジェクトファイルを誤ってGoオブジェクトとして解釈し、不正なメモリアクセスやクラッシュ、あるいは誤ったパッケージ情報の生成といった問題を引き起こす可能性がありました。
このコミットの目的は、gopack がオブジェクトファイルを処理する際に、それが本当にGoオブジェクトであるかを正確に識別し、非Goオブジェクトに対してはGoメタデータの探索や解析を行わないようにすることです。
前提知識の解説
gopack: Go言語の初期のツールチェインの一部で、Goのパッケージ(コンパイルされたGoコード)をアーカイブファイル(通常は.a拡張子を持つ)にまとめるために使用されていました。これは、C言語のar(archiver) コマンドに似た機能を提供し、複数のオブジェクトファイルを一つのライブラリファイルに結合する役割を担っていました。現代のGoでは、go buildやgo installコマンドがこの役割を内部的に処理するため、gopackを直接使用することは稀です。- Goオブジェクトファイル: Go言語のソースコードがコンパイルされると、中間形式のオブジェクトファイルが生成されます。これらのファイルには、コンパイルされた機械語コードだけでなく、Goランタイムが必要とする型情報、シンボル情報、依存関係などの「Goメタデータ」が含まれています。このメタデータは、Goのリンカが最終的な実行可能ファイルを生成する際に非常に重要です。
ar(archiver) フォーマット: Unix系のシステムで広く使われているアーカイブファイルフォーマットです。複数のファイルを一つのアーカイブファイルにまとめるために使用されます。Goのツールチェインも、コンパイルされたGoパッケージをこのarフォーマットのファイル(.a)として出力していました。arファイルは、ヘッダとそれに続くメンバーファイル(オブジェクトファイルなど)で構成されます。src/cmd/gopack/ar.c: このファイルは、gopackコマンドがarフォーマットのアーカイブファイルを読み書きするためのC言語のコードを含んでいます。Goのツールチェインの一部ですが、初期のGoツールはC言語で書かれている部分も多くありました。このファイルは、アーカイブ内の個々のオブジェクトファイルを解析し、Goメタデータを抽出するロジックを扱っていました。- Goオブジェクトヘッダとメタデータ: Goオブジェクトファイルは、特定の形式で開始されます。コミットメッセージやコードの変更から推測すると、Goオブジェクトのヘッダの後にGoメタデータが続き、その後に
!という文字が単独の行で区切りとして現れるという構造を持っていたようです。非Goオブジェクトの場合、この!がすぐに現れることで、Goメタデータが存在しないことを示していました。
技術的詳細
このコミットの技術的な核心は、gopack がアーカイブファイル内の各メンバーを処理する scanobj 関数にあります。以前の実装では、scanobj はオブジェクトのヘッダを読み取った後、そのオブジェクトがGoオブジェクトであるかどうかを完全に確認することなく、Goメタデータの解析を行う scanpkg 関数を呼び出す可能性がありました。
修正は、この scanobj 関数内に goobject という新しいフラグを導入し、オブジェクトがGoオブジェクトであるかを明示的にチェックするロジックを追加することで行われました。
具体的には、以下の手順でGoオブジェクトであるかを判断します。
- オブジェクトの先頭から特定のオフセット(
"go object "という文字列の後に続く部分)を読み取ります。 - その直後に
!文字が続くかどうかをチェックします。- もし
!がすぐに続く場合、それはGoメタデータが存在しない非Goオブジェクトであると判断し、goobjectフラグを0(false) に設定します。 - そうでない場合、Goメタデータが存在するGoオブジェクトであると判断し、
goobjectフラグを1(true) に設定します。
- もし
この goobject フラグは、scanpkg 関数を呼び出す条件に追加されます。これにより、gopack は非Goオブジェクトに対して scanpkg を呼び出すことを避け、Goメタデータの誤った解析を防ぎます。
コアとなるコードの変更箇所
diff --git a/src/cmd/gopack/ar.c b/src/cmd/gopack/ar.c
index c02903bc7a..9125f2987e 100644
--- a/src/cmd/gopack/ar.c
+++ b/src/cmd/gopack/ar.c
@@ -654,7 +654,7 @@ matchhdr(char *p, char **lastp)
void
scanobj(Biobuf *b, Arfile *ap, long size)
{
- int obj;
+ int obj, goobject;
vlong offset, offset1;
Dir *d;
static int lastobj = -1;
@@ -695,9 +695,19 @@ scanobj(Biobuf *b, Arfile *ap, long size)
return;
}
+ goobject = 1;
offset1 = Boffset(b);
Bseek(b, offset, 0);
p = Brdstr(b, '\n', 1);
+
+ // After the go object header comes the Go metadata,
+ // followed by ! on a line by itself. If this is not a Go object,
+ // the ! comes immediately. Catch that so we can avoid
+ // the call to scanpkg below, since scanpkg assumes that the
+ // Go metadata is present.
+ if(Bgetc(b) == '!')
+ goobject = 0;
+
Bseek(b, offset1, 0);
if(p == nil || strncmp(p, "go object ", 10) != 0) {
fprint(2, "gopack: malformed object file %s\n", file);
@@ -734,7 +744,7 @@ scanobj(Biobuf *b, Arfile *ap, long size)
}
Bseek(b, offset, 0);
objtraverse(objsym, ap);
- if (gflag) {
+ if (gflag && goobject) {
scanpkg(b, size);
Bseek(b, offset, 0);
}
コアとなるコードの解説
-
int obj, goobject;の追加:scanobj関数のローカル変数としてgoobjectという整数型の変数が追加されました。この変数は、現在処理しているオブジェクトがGoオブジェクトであるかどうかを示すフラグとして機能します(1ならGoオブジェクト、0なら非Goオブジェクト)。
-
goobject = 1;の初期化:scanobj関数の冒頭で、goobjectはデフォルトで1(Goオブジェクトであると仮定) に初期化されます。これは、後続のチェックで非Goオブジェクトであることが判明した場合にのみ0に変更されるというアプローチを取っています。
-
Goオブジェクトの識別ロジックの追加:
offset1 = Boffset(b); Bseek(b, offset, 0); p = Brdstr(b, '\n', 1); // After the go object header comes the Go metadata, // followed by ! on a line by itself. If this is not a Go object, // the ! comes immediately. Catch that so we can avoid // the call to scanpkg below, since scanpkg assumes that the // Go metadata is present. if(Bgetc(b) == '!') goobject = 0; Bseek(b, offset1, 0);offset1 = Boffset(b);で現在のバッファのオフセットを保存します。これは、後で元の位置に戻るために必要です。Bseek(b, offset, 0);で、現在のオブジェクトのデータが始まるオフセットにシークします。p = Brdstr(b, '\n', 1);は、Goオブジェクトヘッダの後の最初の改行までの文字列を読み取ります。この文字列は、Goメタデータの開始を示すものと推測されます。if(Bgetc(b) == '!'): ここが最も重要な変更点です。Bgetc(b)は、現在のバッファ位置から1バイトを読み取ります。もしこのバイトが!文字であれば、それはGoメタデータが存在しない非Goオブジェクトであることを意味します。この場合、goobjectフラグは0に設定されます。Bseek(b, offset1, 0);で、バッファの読み取り位置を元のoffset1に戻します。これは、後続の処理が正しい位置から開始できるようにするためです。
-
scanpkg呼び出し条件の変更:if (gflag && goobject) { scanpkg(b, size); Bseek(b, offset, 0); }- 以前は
if (gflag)だけでscanpkgが呼び出されていましたが、この変更によりgflag && goobjectという条件になりました。 gflagは、gopackがGoパッケージの処理を行うべきかどうかを示すフラグです。goobjectは、上記で説明したように、現在のオブジェクトがGoオブジェクトであるかどうかを示します。- この変更により、
scanpkgはgflagが設定されており、かつ現在のオブジェクトがGoオブジェクトである場合にのみ呼び出されるようになります。これにより、非Goオブジェクトに対してGoメタデータの解析が誤って行われることがなくなります。
- 以前は
この修正は、gopack がより堅牢になり、Goオブジェクトと非Goオブジェクトを正しく区別して処理できるようになることを保証します。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/d6ff3c11774bf36c797dcc6cd946819959766dc1
- Go CL (Code Review): https://golang.org/cl/5316075
- Go Issue #2333: (直接のリンクは見つかりませんでしたが、コミットメッセージに記載されています)
参考にした情報源リンク
- Go言語の公式ドキュメント (Goツールチェインに関する一般的な情報)
arコマンドとアーカイブファイルフォーマットに関する一般的な情報- Go言語の初期のツールチェインの設計に関する情報 (必要に応じて)
- Go言語のソースコード (特に
src/cmd/gopackディレクトリ) - Go言語のIssueトラッカー (Issue #2333に関する詳細情報があれば)
(注: Issue #2333の直接のリンクは、古いIssueトラッカーのシステム変更などにより見つからない場合があります。しかし、コミットメッセージに明記されているため、このコミットがそのIssueを修正したことは確かです。)