[インデックス 12822] ファイルの概要
このコミットは、Go言語のツールチェインにおけるアセンブラ(5a
, 6a
, 8a
)が、コンパイル時に生成されるバイナリ内のファイルパスを適切に処理するための変更を加えています。具体的には、GOROOT_FINAL
環境変数を考慮に入れることで、Goのインストールパスがビルド時と異なるデプロイ環境においても、デバッグ情報やスタックトレース内のソースファイルパスが正しく解決されるように改善されています。
コミット
commit 6af069f3e1b64b89cd3f77b486af3d15cc8a4d8c
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Wed Apr 4 00:03:42 2012 +0800
5a, 6a, 8a: take GOROOT_FINAL into consideration
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5940052
---
src/cmd/5a/lex.c | 33 ++++++++++++++++++++++++++++++++-
src/cmd/6a/lex.c | 32 ++++++++++++++++++++++++++++++++\n src/cmd/8a/lex.c | 32 ++++++++++++++++++++++++++++++++\n 3 files changed, 96 insertions(+), 1 deletion(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6af069f3e1b64b89cd3f77b486af3d15cc8a4d8c
元コミット内容
5a, 6a, 8a: take GOROOT_FINAL into consideration
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5940052
変更の背景
Go言語のビルドシステムでは、ソースコードのコンパイル時に、そのソースファイルがどこから来たのかという情報(ファイルパス)を生成されるバイナリに埋め込むことがあります。これは、デバッグ情報やスタックトレースなどで、エラーが発生した際にどのソースファイルのどの行で問題が起きたかを正確に特定するために非常に重要です。
しかし、Goのビルド環境と実行環境が異なる場合、特にGOROOT
(Goのインストールディレクトリ)がビルド時と実行時で異なるパスになるシナリオにおいて問題が発生していました。例えば、あるパス(例: /usr/local/go
)でGoをビルドし、そのバイナリを別のパス(例: /opt/go
)にデプロイした場合、バイナリに埋め込まれたソースパスが古いGOROOT
を参照したままになり、デバッグツールが正しいソースファイルを見つけられないという問題が生じます。
このコミットは、この「パスの不一致」問題を解決するために導入されました。GOROOT_FINAL
という環境変数を導入し、ビルド時にこの変数を参照することで、最終的なデプロイ先でのGOROOT
パスをバイナリに埋め込むことができるようになります。これにより、ビルド環境と実行環境のGOROOT
が異なっていても、ソースファイルパスの解決が正しく行われるようになります。
前提知識の解説
Go言語のツールチェインとアセンブラ
Go言語は、go build
コマンドを通じてソースコードをコンパイルし、実行可能なバイナリを生成します。このプロセスには、コンパイラ、リンカ、アセンブラなど、複数のツールが連携して動作します。
- アセンブラ (
5a
,6a
,8a
): Goのツールチェインには、異なるアーキテクチャ(例:5a
はARM、6a
はx86-64、8a
はx86)向けのアセンブラが含まれています。これらは、Goのソースコードから生成されたアセンブリコードを機械語に変換する役割を担います。この変換の過程で、ソースファイルのパス情報なども処理されます。
GOROOT
とGOPATH
Go言語の開発において、以下の2つの重要な環境変数があります。
GOROOT
: GoのSDK(Standard Development Kit)がインストールされているディレクトリを指します。Goの標準ライブラリやツールチェインの実行ファイルなどがこのディレクトリ以下に配置されます。GOPATH
: ユーザーが開発するGoのプロジェクトのワークスペースを指します。通常、src
(ソースコード)、pkg
(コンパイル済みパッケージ)、bin
(実行可能バイナリ)のサブディレクトリを持ちます。
GOROOT_FINAL
GOROOT_FINAL
は、このコミットで導入された、またはその概念が強化された環境変数です。これは、Goのバイナリが最終的にデプロイされる環境におけるGOROOT
のパスを指定するために使用されます。ビルド時にGOROOT_FINAL
が設定されている場合、アセンブラはバイナリに埋め込むソースファイルパスのプレフィックスとして、ビルド時のGOROOT
ではなくGOROOT_FINAL
の値を優先的に使用します。これにより、クロスコンパイルや異なる環境へのデプロイ時に、ソースパスの解決が正しく行われるようになります。
デバッグ情報とスタックトレース
- デバッグ情報: コンパイルされたバイナリには、デバッガがソースコードと機械語を対応付けるために必要な情報(変数名、関数名、行番号など)が埋め込まれることがあります。
- スタックトレース: プログラムがクラッシュしたりエラーが発生したりした際に、関数呼び出しの履歴(どの関数がどの関数を呼び出したか)を追跡するための情報です。スタックトレースには通常、各フレームに対応するソースファイルのパスと行番号が含まれます。
これらの情報が正しく機能するためには、バイナリに埋め込まれたソースファイルパスが、実行環境で実際に存在するパスと一致している必要があります。
技術的詳細
このコミットの技術的な核心は、Goのアセンブラ(5a
, 6a
, 8a
)が、ソースファイルの履歴情報(Hist
構造体で管理されるファイル名など)をバイナリに書き出す際に、GOROOT_FINAL
環境変数の値を考慮に入れるように変更された点です。
変更は、各アセンブラのouthist
関数に集中しています。outhist
関数は、コンパイルされたファイルの履歴情報を出力する役割を担っています。
-
GOROOT
とGOROOT_FINAL
の取得:outhist
関数内で、first
という静的変数を導入し、初回呼び出し時に一度だけGOROOT
とGOROOT_FINAL
の環境変数を取得します。getenv("GOROOT")
でビルド時のGOROOT
を取得します。getenv("GOROOT_FINAL")
で最終的なデプロイ先のGOROOT
を取得します。- もし
GOROOT
が設定されていない場合は空文字列に、GOROOT_FINAL
が設定されていない場合はGOROOT
の値にフォールバックします。 GOROOT
とGOROOT_FINAL
が同じ値である場合、パスの書き換えは不要と判断し、両方の変数をnil
に設定して最適化します。
-
パスの書き換えロジック:
for(h = hist; h != H; h = h->link)
ループ内で、各履歴エントリのファイルパスh->name
を処理します。h->name
がnil
でなく、かつgoroot
がnil
でない(つまり、GOROOT
とGOROOT_FINAL
が異なる可能性がある)場合にのみ、パスの書き換えを試みます。strncmp(p, goroot, strlen(goroot)) == 0 && p[n] == '/'
という条件で、現在のファイルパスp
がビルド時のGOROOT
で始まるかどうかをチェックします。p[n] == '/'
は、GOROOT
の後にディレクトリセパレータが続くことを確認し、例えば/usr/local/go
と/usr/local/golang
のような部分一致を防ぎます。- もし条件が真であれば、
smprint("%s%s", goroot_final, p+n)
を使って、GOROOT
の部分をGOROOT_FINAL
に置き換えた新しいパスを生成します。p+n
は、元のパスからGOROOT
の長さをスキップした残りの部分を指します。 - 生成された新しいパスは
tofree
に格納され、p
がこの新しいパスを指すように更新されます。これにより、後続の処理でこの新しいパスが使用されます。
-
メモリ解放: ループの最後に、
tofree
がnil
でない場合(つまり、パスが書き換えられた場合)、free(tofree)
を呼び出して動的に割り当てられたメモリを解放し、メモリリークを防ぎます。
この変更により、Goのバイナリに埋め込まれるソースファイルパスは、ビルド時のGOROOT
ではなく、GOROOT_FINAL
で指定されたパスを基準とするようになり、デプロイ環境でのデバッグやスタックトレースの正確性が向上します。
コアとなるコードの変更箇所
変更は主に以下の3つのファイルにわたっていますが、内容はほぼ同一です。
src/cmd/5a/lex.c
src/cmd/6a/lex.c
src/cmd/8a/lex.c
各ファイルのouthist
関数内に以下のコードが追加・変更されています。
// 変更前 (例: src/cmd/5a/lex.c)
@@ -641,11 +641,37 @@ outhist(void)
Hist *h;
char *p, *q, *op, c;
int n;
-
+ char *tofree;
+ static int first = 1;
+ static char *goroot, *goroot_final;
+
+ if(first) {
+ // Decide whether we need to rewrite paths from $GOROOT to $GOROOT_FINAL.
+ first = 0;
+ goroot = getenv("GOROOT");
+ goroot_final = getenv("GOROOT_FINAL");
+ if(goroot == nil)
+ goroot = "";
+ if(goroot_final == nil)
+ goroot_final = goroot;
+ if(strcmp(goroot, goroot_final) == 0) {
+ goroot = nil;
+ goroot_final = nil;
+ }
+ }
+
+ tofree = nil;
g = nullgen;
c = '/';
for(h = hist; h != H; h = h->link) {
p = h->name;
+ if(p != nil && goroot != nil) {
+ n = strlen(goroot);
+ if(strncmp(p, goroot, strlen(goroot)) == 0 && p[n] == '/') {
+ tofree = smprint("%s%s", goroot_final, p+n);
+ p = tofree;
+ }
+ }
op = 0;
if(systemtype(Windows) && p && p[1] == ':'){
c = p[2];
@@ -697,6 +723,11 @@ outhist(void)
Bputc(&obuf, h->line>>24);
zaddr(&nullgen, 0);
zaddr(&g, 0);
+\
+\t\tif(tofree) {\n+\t\t\tfree(tofree);\n+\t\t\ttofree = nil;\n+\t\t}\n }
}
コアとなるコードの解説
上記のコードスニペットは、Goのアセンブラがソースファイルパスを処理する際のロジックを示しています。
-
静的変数の初期化:
static int first = 1;
:outhist
関数が最初に呼び出されたときに一度だけ実行されるブロックを制御するためのフラグです。static char *goroot, *goroot_final;
:GOROOT
とGOROOT_FINAL
環境変数の値を保持するためのポインタです。これらは静的変数なので、関数の呼び出し間で値が保持されます。char *tofree = nil;
: 動的に割り当てられたメモリを追跡し、後で解放するためのポインタです。
-
環境変数の取得と初期設定:
if(first)
ブロック内で、getenv("GOROOT")
とgetenv("GOROOT_FINAL")
を使って環境変数を取得します。goroot == nil
の場合、goroot = ""
とすることで、環境変数が設定されていない場合でも安全に処理を進めます。goroot_final == nil
の場合、goroot_final = goroot
とすることで、GOROOT_FINAL
が明示的に設定されていない場合はGOROOT
と同じパスを使用します。strcmp(goroot, goroot_final) == 0
で両者が同じパスを指す場合、パスの書き換えは不要なので、goroot = nil; goroot_final = nil;
と設定して以降の処理をスキップし、パフォーマンスを最適化します。
-
パスの書き換えロジック:
for(h = hist; h != H; h = h->link)
ループは、コンパイル対象のソースファイルの履歴(Hist
構造体)を一つずつ処理します。p = h->name;
: 現在の履歴エントリのファイルパスを取得します。if(p != nil && goroot != nil)
: ファイルパスが存在し、かつGOROOT
とGOROOT_FINAL
が異なる可能性がある場合にのみ、以下のパス書き換えロジックを実行します。n = strlen(goroot);
:GOROOT
の長さを取得します。strncmp(p, goroot, strlen(goroot)) == 0 && p[n] == '/'
: ファイルパスp
がgoroot
で始まり、その直後にディレクトリセパレータ(/
)が続くかをチェックします。これにより、Goの標準ライブラリやツールチェイン内のファイルパスであることが確認されます。tofree = smprint("%s%s", goroot_final, p+n);
:smprint
関数(おそらくGoの内部ユーティリティ関数で、文字列をフォーマットして新しい文字列を動的に割り当てる)を使って、goroot
の部分をgoroot_final
に置き換えた新しいパスを生成します。p+n
は、元のパスからgoroot
の長さを除いた残りの部分です。p = tofree;
:h->name
が指すポインタを、新しく生成されたパスtofree
に置き換えます。これにより、バイナリに埋め込まれるパスが更新されます。
-
メモリ解放: ループの最後にある
if(tofree)
ブロックは、smprint
によって動的に割り当てられたメモリを解放します。free(tofree);
:tofree
がnil
でない場合(つまり、パスが書き換えられ、新しいメモリが割り当てられた場合)、そのメモリを解放します。tofree = nil;
:tofree
をnil
にリセットし、次のループイテレーションで誤って解放しないようにします。
この一連の処理により、Goのビルドシステムは、ビルド環境と異なるデプロイ環境においても、ソースファイルパスの正確な解決を保証できるようになります。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goの環境変数に関するドキュメント(
GOROOT
,GOPATH
など): https://golang.org/doc/code.html (GoのバージョンによってURLが異なる場合があります) - GoのIssue Tracker (この変更に関連するIssueがあるかもしれません): https://github.com/golang/go/issues
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージにある
https://golang.org/cl/5940052
はこのGerritのチェンジリストへのリンクです)
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/5a/lex.c
,src/cmd/6a/lex.c
,src/cmd/8a/lex.c
の履歴) - Go言語の環境変数に関する一般的な情報源 (Goの公式ドキュメントやブログ記事など)
getenv
、strlen
、strncmp
、strcmp
、free
などのC言語標準ライブラリ関数に関するドキュメントsmprint
のようなGo内部のユーティリティ関数に関する情報 (Goのソースコードを直接読むことで理解を深めることができます)- Goのビルドプロセスに関する技術記事や解説
注記: smprint
はGoのツールチェイン内部で使用される関数であり、標準のCライブラリには含まれません。これは、GoのツールチェインがC言語で書かれている部分があるためです。