Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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のソースコードから生成されたアセンブリコードを機械語に変換する役割を担います。この変換の過程で、ソースファイルのパス情報なども処理されます。

GOROOTGOPATH

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関数は、コンパイルされたファイルの履歴情報を出力する役割を担っています。

  1. GOROOTGOROOT_FINALの取得: outhist関数内で、firstという静的変数を導入し、初回呼び出し時に一度だけGOROOTGOROOT_FINALの環境変数を取得します。

    • getenv("GOROOT")でビルド時のGOROOTを取得します。
    • getenv("GOROOT_FINAL")で最終的なデプロイ先のGOROOTを取得します。
    • もしGOROOTが設定されていない場合は空文字列に、GOROOT_FINALが設定されていない場合はGOROOTの値にフォールバックします。
    • GOROOTGOROOT_FINALが同じ値である場合、パスの書き換えは不要と判断し、両方の変数をnilに設定して最適化します。
  2. パスの書き換えロジック: for(h = hist; h != H; h = h->link)ループ内で、各履歴エントリのファイルパスh->nameを処理します。

    • h->namenilでなく、かつgorootnilでない(つまり、GOROOTGOROOT_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がこの新しいパスを指すように更新されます。これにより、後続の処理でこの新しいパスが使用されます。
  3. メモリ解放: ループの最後に、tofreenilでない場合(つまり、パスが書き換えられた場合)、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のアセンブラがソースファイルパスを処理する際のロジックを示しています。

  1. 静的変数の初期化:

    • static int first = 1;: outhist関数が最初に呼び出されたときに一度だけ実行されるブロックを制御するためのフラグです。
    • static char *goroot, *goroot_final;: GOROOTGOROOT_FINAL環境変数の値を保持するためのポインタです。これらは静的変数なので、関数の呼び出し間で値が保持されます。
    • char *tofree = nil;: 動的に割り当てられたメモリを追跡し、後で解放するためのポインタです。
  2. 環境変数の取得と初期設定: 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;と設定して以降の処理をスキップし、パフォーマンスを最適化します。
  3. パスの書き換えロジック: for(h = hist; h != H; h = h->link)ループは、コンパイル対象のソースファイルの履歴(Hist構造体)を一つずつ処理します。

    • p = h->name;: 現在の履歴エントリのファイルパスを取得します。
    • if(p != nil && goroot != nil): ファイルパスが存在し、かつGOROOTGOROOT_FINALが異なる可能性がある場合にのみ、以下のパス書き換えロジックを実行します。
    • n = strlen(goroot);: GOROOTの長さを取得します。
    • strncmp(p, goroot, strlen(goroot)) == 0 && p[n] == '/': ファイルパスpgorootで始まり、その直後にディレクトリセパレータ(/)が続くかをチェックします。これにより、Goの標準ライブラリやツールチェイン内のファイルパスであることが確認されます。
    • tofree = smprint("%s%s", goroot_final, p+n);: smprint関数(おそらくGoの内部ユーティリティ関数で、文字列をフォーマットして新しい文字列を動的に割り当てる)を使って、gorootの部分をgoroot_finalに置き換えた新しいパスを生成します。p+nは、元のパスからgorootの長さを除いた残りの部分です。
    • p = tofree;: h->nameが指すポインタを、新しく生成されたパスtofreeに置き換えます。これにより、バイナリに埋め込まれるパスが更新されます。
  4. メモリ解放: ループの最後にあるif(tofree)ブロックは、smprintによって動的に割り当てられたメモリを解放します。

    • free(tofree);: tofreenilでない場合(つまり、パスが書き換えられ、新しいメモリが割り当てられた場合)、そのメモリを解放します。
    • tofree = nil;: tofreenilにリセットし、次のループイテレーションで誤って解放しないようにします。

この一連の処理により、Goのビルドシステムは、ビルド環境と異なるデプロイ環境においても、ソースファイルパスの正確な解決を保証できるようになります。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特にsrc/cmd/5a/lex.c, src/cmd/6a/lex.c, src/cmd/8a/lex.cの履歴)
  • Go言語の環境変数に関する一般的な情報源 (Goの公式ドキュメントやブログ記事など)
  • getenvstrlenstrncmpstrcmpfreeなどのC言語標準ライブラリ関数に関するドキュメント
  • smprintのようなGo内部のユーティリティ関数に関する情報 (Goのソースコードを直接読むことで理解を深めることができます)
  • Goのビルドプロセスに関する技術記事や解説

注記: smprintはGoのツールチェイン内部で使用される関数であり、標準のCライブラリには含まれません。これは、GoのツールチェインがC言語で書かれている部分があるためです。