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

[インデックス 12823] ファイルの概要

このコミットは、Goコンパイラ(具体的には5c6c8c)が生成するバイナリ内のデバッグ情報や履歴情報に含まれるソースファイルパスを、GOROOT_FINAL環境変数の値に基づいて適切に書き換えるように修正するものです。これにより、Goのビルド環境と最終的なデプロイ環境でGOROOTのパスが異なる場合に、デバッグ情報が正しく参照されるようになります。

コミット

commit bcdafaa582017ad7cd32739e564cedd6d0a5a83b
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Wed Apr 4 00:04:36 2012 +0800

    5c, 6c, 8c: take GOROOT_FINAL into consideration
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5936050

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/bcdafaa582017ad7cd32739e564cedd6d0a5a83b

元コミット内容

5c, 6c, 8c の各コンパイラが、GOROOT_FINAL 環境変数を考慮に入れるように変更されました。

変更の背景

Go言語のビルドシステムでは、GOROOTという環境変数がGoのインストールディレクトリを指します。しかし、クロスコンパイル環境や、ビルド時とデプロイ時でGoのインストールパスが異なるような特殊な環境では、GOROOTのパスが最終的な実行環境と一致しない場合があります。

例えば、/usr/local/goにGoをインストールしてビルドを行ったとしても、最終的にバイナリがデプロイされるシステムではGoが/opt/goにインストールされているかもしれません。この場合、コンパイラが生成するデバッグ情報やスタックトレースに含まれるソースファイルパスが、ビルド時のGOROOT(例: /usr/local/go/src/runtime/proc.c)をそのまま記録してしまうと、デプロイ先でデバッガが正しいソースファイルを見つけられなくなるという問題が発生します。

この問題を解決するため、GoのビルドシステムにはGOROOT_FINALという概念が導入されました。GOROOT_FINALは、ビルドされたGoバイナリが最終的に配置されるGOROOTのパスを指定するための環境変数です。このコミットは、コンパイラが生成するオブジェクトファイル内の履歴情報(AHISTORYレコード)に記録されるソースファイルパスを、GOROOT_FINALの値に基づいて適切に書き換えることで、このパスの不一致問題を解消することを目的としています。

前提知識の解説

Go言語のビルドシステムと環境変数

  • GOROOT: Goの標準ライブラリやツールチェーンがインストールされているルートディレクトリを指す環境変数です。Goのビルド時にソースファイルの場所を特定するために使用されます。
  • GOROOT_FINAL: Goのビルドシステムで使用される特殊な環境変数です。これは、ビルドされたGoバイナリが最終的にデプロイされるシステム上でのGOROOTのパスを指定するために使用されます。例えば、コンテナイメージをビルドする際に、ホストのGOROOTとは異なるコンテナ内のパスをGOROOT_FINALとして指定することができます。
  • GOOS / GOARCH: Goのクロスコンパイルを制御するための環境変数です。GOOSはターゲットオペレーティングシステム(例: linux, windows)、GOARCHはターゲットアーキテクチャ(例: amd64, arm, 386)を指定します。

Goコンパイラ (5c, 6c, 8c)

Go言語の初期のコンパイラは、ターゲットアーキテクチャごとに異なる名前を持っていました。これらはPlan 9 Cコンパイラをベースにしていました。

  • 5c: ARMアーキテクチャ向けのGoコンパイラ。
  • 6c: x86 (32-bit) アーキテクチャ向けのGoコンパイラ。
  • 8c: x86-64 (64-bit) アーキテクチャ向けのGoコンパイラ。

現在では、これらのコンパイラは統合され、go tool compileコマンドを通じて利用されますが、このコミットが作成された2012年当時は、これらの個別のコンパイラがGoのビルドプロセスにおいて重要な役割を担っていました。これらのコンパイラは、ソースコードをアセンブリコードに変換し、オブジェクトファイルを生成します。オブジェクトファイルには、デバッグ情報や、元のソースファイルへのパスなどのメタデータが含まれることがあります。

AHISTORY レコード

Goコンパイラが生成するオブジェクトファイルには、コンパイルされたソースファイルの履歴情報が埋め込まれることがあります。これはAHISTORYというアセンブリ命令として表現され、元のソースファイル名と行番号を記録します。この情報は、デバッガがソースコードと実行中のバイナリを関連付けるために利用されます。

技術的詳細

このコミットは、Goコンパイラのバックエンド部分、具体的にはオブジェクトファイルに履歴情報を書き出すouthist関数に変更を加えています。outhist関数は、コンパイルされたソースファイルのパスをオブジェクトファイルに埋め込む役割を担っています。

変更の核心は、GOROOTGOROOT_FINALという2つの環境変数を比較し、もしこれらが異なる場合に、履歴情報に記録されるパスをGOROOT_FINALに基づいて書き換えるというロジックを追加した点にあります。

  1. 環境変数の取得と初期化:

    • firstという静的変数を導入し、outhist関数が最初に呼び出されたときに一度だけ環境変数のチェックを行うようにします。
    • getenv("GOROOT")getenv("GOROOT_FINAL")を使って、それぞれの環境変数の値を取得します。
    • もしGOROOTが設定されていない場合は空文字列に、GOROOT_FINALが設定されていない場合はGOROOTの値にフォールバックします。
    • strcmp(goroot, goroot_final) == 0で両者が同じであれば、パスの書き換えは不要と判断し、gorootgoroot_finalnilに設定して以降の処理をスキップします。
  2. パスの書き換えロジック:

    • histリストをイテレートし、各履歴エントリのファイルパスh->nameをチェックします。
    • p != nil && goroot != nilの条件で、パスが存在し、かつGOROOTGOROOT_FINALが異なる場合にのみ書き換え処理に進みます。
    • strncmp(p, goroot, strlen(goroot)) == 0 && p[n] == '/'という条件で、現在のファイルパスpGOROOTで始まり、かつその後にパスセパレータ(/)が続く場合に、Goの標準ライブラリ内のファイルであると判断します。
    • 条件が満たされた場合、smprint("%s%s", goroot_final, p+n)を使って新しいパスを生成します。ここでp+nは、元のパスからGOROOT部分を除いた残りの部分(例: /src/runtime/proc.c)を指します。smprintは、フォーマットされた文字列を新しく割り当てられたメモリに書き込む関数です。
    • 生成された新しいパスはtofreeに格納され、pがこの新しいパスを指すように更新されます。これにより、AHISTORYレコードにはGOROOT_FINALに基づいたパスが記録されます。
  3. メモリ管理:

    • smprintによって動的に割り当てられたメモリは、tofreeポインタを通じて管理されます。
    • 各履歴エントリの処理の最後に、if(tofree) { free(tofree); tofree = nil; }というコードが追加され、割り当てられたメモリが適切に解放されるようにしています。これは、メモリリークを防ぐために非常に重要です。

この変更により、Goのコンパイラは、ビルド環境と異なるデプロイ環境においても、デバッグ情報が正確なソースファイルパスを指すように調整できるようになりました。

コアとなるコードの変更箇所

src/cmd/5c/swt.csrc/cmd/6c/swt.csrc/cmd/8c/swt.c の3つのファイルで、outhist関数内に以下の変更が加えられています。変更内容は3つのファイルで同一です。

--- a/src/cmd/5c/swt.c
+++ b/src/cmd/5c/swt.c
@@ -472,12 +472,38 @@ outhist(Biobuf *b)
 	char *p, *q, *op, c;
 	Prog pg;
 	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;
 	pg = zprog;
 	pg.as = AHISTORY;
 	c = pathchar();
 	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];
@@ -525,6 +551,11 @@ outhist(Biobuf *b)
 			pg.to.type = D_CONST;
 
 		zwrite(b, &pg, 0, 0);
+
+ 		if(tofree) {
+ 			free(tofree);
+ 			tofree = nil;
+ 		}
 	}
 }

コアとなるコードの解説

追加されたコードは、outhist関数内で履歴エントリのパスを処理する部分に挿入されています。

	char *tofree; // 動的に割り当てられたメモリを解放するためのポインタ
	static int first = 1; // outhist関数が最初に呼び出されたかどうかを追跡するフラグ
	static char *goroot, *goroot_final; // GOROOTとGOROOT_FINALの値を保持する静的ポインタ

	if(first) {
		// GOROOTからGOROOT_FINALへのパス書き換えが必要かどうかを判断する
		first = 0; // 初回呼び出しフラグをリセット
		goroot = getenv("GOROOT"); // GOROOT環境変数の値を取得
		goroot_final = getenv("GOROOT_FINAL"); // GOROOT_FINAL環境変数の値を取得
		if(goroot == nil) // GOROOTが設定されていない場合
			goroot = ""; // 空文字列として扱う
		if(goroot_final == nil) // GOROOT_FINALが設定されていない場合
			goroot_final = goroot; // GOROOTの値をデフォルトとして使用
		if(strcmp(goroot, goroot_final) == 0) { // GOROOTとGOROOT_FINALが同じ場合
			goroot = nil; // 書き換え不要なのでnilに設定
			goroot_final = nil; // 書き換え不要なのでnilに設定
		}
	}

	tofree = nil; // tofreeポインタを初期化

	// ... (既存のコード) ...

	for(h = hist; h != H; h = h->link) {
		p = h->name; // 現在の履歴エントリのファイルパス
		if(p != nil && goroot != nil) { // パスが存在し、かつGOROOTとGOROOT_FINALが異なる場合
			n = strlen(goroot); // GOROOTの長さを取得
			// パスがGOROOTで始まり、かつその後にパスセパレータが続く場合
			if(strncmp(p, goroot, strlen(goroot)) == 0 && p[n] == '/') {
				// GOROOT_FINALに基づいて新しいパスを生成
				tofree = smprint("%s%s", goroot_final, p+n);
				p = tofree; // 新しいパスを使用するようにポインタを更新
			}
		}
		// ... (既存のコード) ...

		zwrite(b, &pg, 0, 0); // 履歴情報をオブジェクトファイルに書き込む

 		if(tofree) { // 動的に割り当てられたメモリがある場合
 			free(tofree); // メモリを解放
 			tofree = nil; // ポインタをリセット
 		}
	}
  • static int first = 1;: このフラグは、outhist関数がプロセス内で最初に呼び出されたときにのみ、環境変数の取得と初期化を行うためのものです。これにより、不要なgetenv呼び出しを避けてパフォーマンスを向上させています。
  • getenv("GOROOT"), getenv("GOROOT_FINAL"): これらのC標準ライブラリ関数は、指定された環境変数の値を取得します。
  • strcmp(goroot, goroot_final) == 0: 2つの文字列が等しいかどうかを比較します。等しい場合は0を返します。
  • strlen(goroot): 文字列の長さを返します。
  • strncmp(p, goroot, strlen(goroot)): pの先頭がgorootと一致するかどうかを、gorootの長さ分だけ比較します。
  • p[n] == '/': GOROOTの直後にパスセパレータがあることを確認し、GOROOTがより長いパスのプレフィックスではないことを保証します(例: /usr/local/go/usr/local/golangの区別)。
  • smprint("%s%s", goroot_final, p+n): これはGoコンパイラの内部関数で、sprintfに似ていますが、結果の文字列を動的に割り当てられたメモリに書き込み、そのポインタを返します。p+nは、元のパスからGOROOT部分を除いた残りの部分(例: /src/pkg/runtime/proc.c)を指します。
  • free(tofree): smprintによって割り当てられたメモリを解放します。これはC言語における重要なメモリ管理のプラクティスです。

この変更により、Goのコンパイラは、ビルド環境と異なるデプロイ環境においても、デバッグ情報が正確なソースファイルパスを指すように調整できるようになりました。

関連リンク

参考にした情報源リンク