[インデックス 1572] ファイルの概要
このコミットは、Go言語の初期のコンパイラおよびランタイムにおいて、go ステートメント(ゴルーチン生成)と defer ステートメント(遅延実行)に関連する潜在的なバグを修正するために、pragma textflag という新しいプラグマを導入するものです。このプラグマは、特定の関数のコード生成時に特殊なフラグを設定することで、これらの機能が正しく動作するようにします。
コミット
commit e90314d0242fe520f578ea256db1cb3800f1e7b9
Author: Ken Thompson <ken@golang.org>
Date: Tue Jan 27 14:12:35 2009 -0800
pragma textflag
fixes latent bugs in go and defer
R=r
OCL=23613
CL=23613
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e90314d0242fe520f578ea256db1cb3800f1e7b9
元コミット内容
pragma textflag の導入により、go および defer に関連する潜在的なバグを修正します。
変更の背景
Go言語の初期段階では、コンパイラとランタイムの連携がまだ発展途上にありました。特に、ゴルーチンの生成 (go ステートメント) や関数の終了時に実行される defer ステートメントは、スタック管理や関数呼び出し規約において特殊な処理を必要とします。
このコミット以前は、これらの特殊な関数(例えば、runtime パッケージ内の newproc や deferproc など、ゴルーチンや遅延実行の内部処理を担う関数)がコンパイルされる際に、通常の関数と同様のコード生成が行われていました。しかし、これらの関数はランタイムの非常に低レベルな部分で動作するため、特定の最適化やコード生成の挙動が問題を引き起こす可能性がありました。具体的には、プロファイリングのためのコード挿入や、特定のスタックフレームの扱いが、go や defer の内部ロジックと衝突し、潜在的なバグ(例えば、スタックの破損や不正な関数呼び出し)を引き起こす可能性があったと考えられます。
textflag は、これらのランタイムのコアとなる関数に対して、コンパイラが特別なコード生成を行うように指示するためのメカニズムとして導入されました。これにより、プロファイリングコードの挿入を抑制したり、特定のリンケージ情報を付与したりすることで、go および defer の安定性と正確性を確保することが目的でした。
前提知識の解説
Go言語の初期コンパイラ (6a, 6c, 8c, cc)
Go言語の初期のコンパイラは、現在の go tool compile とは異なり、ターゲットアーキテクチャごとに独立したツールとして存在していました。
6a: Plan 9a(アセンブラ) の Go バージョン。x86-64 (amd64) アーキテクチャ用のアセンブラ。6c: Plan 9c(Cコンパイラ) の Go バージョン。x86-64 (amd64) アーキテクチャ用のCコンパイラ。Go言語のソースコードを中間表現に変換し、最終的にアセンブリコードを生成する役割の一部を担っていました。8c: x86 (386) アーキテクチャ用のCコンパイラ。cc: 共通のCコンパイラフロントエンド。アーキテクチャ固有のバックエンド(6c,8cなど)と連携して動作します。
これらのツールは、Go言語のソースコードを機械語に変換するプロセスにおいて重要な役割を担っていました。
go ステートメントとゴルーチン
go ステートメントは、Go言語における並行処理の基本単位である「ゴルーチン」を生成するために使用されます。go キーワードの後に続く関数呼び出しは、新しいゴルーチンとして独立して実行されます。ゴルーチンは軽量なスレッドのようなもので、Goランタイムによって管理されます。
defer ステートメント
defer ステートメントは、そのステートメントを含む関数が終了する直前に、指定された関数呼び出しを遅延実行するために使用されます。これは、リソースの解放(ファイルのクローズ、ロックの解除など)を確実に行うためによく利用されます。defer された関数は、関数の正常終了時だけでなく、パニック発生時にも実行されます。
pragma (プラグマ)
pragma は、コンパイラに対する特別な指示を与えるためのディレクティブです。Go言語のコンパイラでは、//go:build や //go:noinline のようなコメント形式のプラグマが一般的ですが、初期のコンパイラではC言語の #pragma に似た形式も使用されていました。これらは、コンパイラの最適化の挙動を変更したり、特定のコード生成ルールを適用したりするために使われます。
textflag と profileflg
textflag: このコミットで導入された新しい概念です。関数のコードセクション(テキストセクション)に付与されるフラグで、コンパイラやリンカに対して特定の挙動を指示します。例えば、プロファイリングコードの挿入を抑制したり、特定のリンケージ属性を設定したりするために使用されます。このコミットでは、特にランタイムのコア関数に対して、プロファイリングを無効化し、特別なリンケージ属性(7という値が設定されていることから、おそらくNOPROFやRODATAなど、特定のセクションへの配置や最適化を指示するビットフラグの組み合わせ)を付与する目的で使われています。profileflg: 以前から存在したフラグで、プロファイリングの有効/無効を制御していました。このコミットでは、profileflgの代わりにtextflagを使用することで、よりきめ細やかな制御を可能にしています。特に、goやdeferの内部関数では、プロファイリングが意図しない副作用を引き起こす可能性があったため、これを無効化する必要がありました。
技術的詳細
このコミットの核心は、Goコンパイラとランタイムが、go および defer の内部処理を担う特定の関数(例: runtime.newproc, runtime.deferproc, runtime.deferreturn, runtime.morestack)をコンパイルする際に、特別な textflag を付与するように変更した点にあります。
-
#pragma textflagの導入:src/cmd/cc/cc.hにtextflagという新しいグローバル変数が導入され、pragtextflagという新しいプラグマ処理関数が宣言されました。src/cmd/cc/macbodyでは、#pragma textflagがコンパイラによって認識され、pragtextflag関数が呼び出されるように変更されました。src/cmd/cc/dpchk.cにpragtextflag関数の実装が追加されました。この関数は、#pragma textflagの後に続く数値(デフォルトは7)をtextflagグローバル変数に設定します。これにより、後続の関数定義に対してこのフラグが適用されます。
-
profileflgからtextflagへの移行:- 以前は
profileflgというフラグがプロファイリングの有効/無効を制御していましたが、このコミットではprofileflgの使用が廃止され、textflagに置き換えられました。 src/cmd/cc/lex.cでは、main関数からprofileflg = 1;の初期化が削除されました。src/cmd/cc/dpchk.cのpragtextflag関数内で、以前pragprofileが行っていたprofileflgの設定ロジックがtextflagの設定に置き換えられました。
- 以前は
-
コンパイラバックエンドでの
textflagの利用:src/cmd/6c/txt.c(amd64用Cコンパイラ) とsrc/cmd/8c/txt.c(x86用Cコンパイラ) のgpseudo関数が変更されました。gpseudoは、擬似命令(例えば、関数のエントリポイントを示す命令)を生成する際に呼び出されます。- 変更前は、
p->from.scaleに(profileflg ? 0 : NOPROF)が設定されていましたが、変更後はtextflagの値が直接設定されるようになりました。そして、textflagは設定後すぐに0にリセットされます。これは、#pragma textflagが適用された直後の関数定義にのみそのフラグが有効になるようにするためです。 p->from.scaleは、アセンブラやリンカに対して、シンボル(関数など)の属性や配置に関する情報を提供する役割を担っています。textflagの値をここに設定することで、ランタイムのコア関数が特別な属性を持つコードとして扱われるようになります。
-
ランタイムコードへの
textflagの適用:src/runtime/proc.cにあるsys·newproc(ゴルーチン生成),sys·deferproc(defer処理の開始),sys·deferreturn(defer処理の終了),sys·morestack(スタック拡張) といった重要なランタイム関数に対して、#pragma textflag 7が追加されました。- これにより、これらの関数がコンパイルされる際に、コンパイラは
textflagの値7をp->from.scaleに設定し、特別なコード生成ルールが適用されるようになります。具体的には、プロファイリングコードの挿入が抑制され、これらの関数がランタイムの低レベルな操作を安全に行えるようになります。
この変更により、go や defer の内部処理が、プロファイリングなどの外部要因に影響されずに、常に期待通りに動作することが保証され、潜在的なバグが修正されました。
コアとなるコードの変更箇所
src/cmd/6a/lex.c:pragtextflag関数の宣言と、../cc/lexbodyおよび../cc/macbodyのインクルード。src/cmd/6c/txt.c:gpseudo関数内でp->from.scaleにtextflagを設定し、textflagをリセットする変更。src/cmd/8c/txt.c:src/cmd/6c/txt.cと同様に、gpseudo関数内でp->from.scaleにtextflagを設定し、textflagをリセットする変更。src/cmd/cc/cc.h:profileflgをtextflagに変更し、pragprofileをpragtextflagに変更。src/cmd/cc/dpchk.c:pragprofile関数をpragtextflag関数にリネームし、profileflgの代わりにtextflagを設定するロジックに変更。デフォルト値を7に設定。src/cmd/cc/lex.c:main関数からprofileflg = 1;の初期化を削除。src/cmd/cc/macbody:macprag関数内で、"profile"プラグマの代わりに"textflag"プラグマを認識し、pragtextflagを呼び出すように変更。src/runtime/proc.c:sys·newproc,sys·deferproc,sys·deferreturn,sys·morestackの各関数の定義の前に#pragma textflag 7を追加。
コアとなるコードの解説
src/cmd/6a/lex.c (アセンブラの字句解析器)
+void
+pragtextflag(void)
+{
+ while(getnsc() != '\n')
+ ;
+}
このコードは、#pragma textflag がアセンブラの字句解析器で処理される際に、その行の残りの部分を読み飛ばすための関数です。実際の意味付けはCコンパイラ側で行われます。
src/cmd/6c/txt.c および src/cmd/8c/txt.c (Cコンパイラのコード生成部分)
--- a/src/cmd/6c/txt.c
+++ b/src/cmd/6c/txt.c
@@ -1473,7 +1473,9 @@ gpseudo(int a, Sym *s, Node *n)
p->as = a;
p->from.type = D_EXTERN;
p->from.sym = s;
- p->from.scale = (profileflg ? 0 : NOPROF);
+ p->from.scale = textflag;
+ textflag = 0;
+
if(s->class == CSTATIC)
p->from.type = D_STATIC;
naddr(n, &p->to);
gpseudo 関数は、擬似命令(ここでは関数のエントリポイントなど)を生成する際に呼び出されます。
変更前は、profileflg の値に基づいて NOPROF (No Profiling) フラグを設定していました。
変更後は、グローバル変数 textflag の値を直接 p->from.scale に設定しています。p->from.scale は、生成されるアセンブリコードのシンボルに付与される属性の一部となります。
textflag = 0; は、#pragma textflag が適用された直後の関数にのみその効果が及ぶように、フラグをリセットしています。
src/cmd/cc/cc.h (Cコンパイラのヘッダ)
--- a/src/cmd/cc/cc.h
+++ b/src/cmd/cc/cc.h
@@ -498,7 +498,7 @@ EXTERN Term term[NTERM];
EXTERN int nterm;
EXTERN int packflg;
EXTERN int fproundflg;
-EXTERN int profileflg;
+EXTERN int textflag;
EXTERN int ncontin;
EXTERN int canreach;
EXTERN int warnreach;
@@ -747,7 +747,7 @@ void arginit(void);
void pragvararg(void);
void pragpack(void);
void pragfpround(void);
-void pragprofile(void);
+void pragtextflag(void);
void pragincomplete(void);
profileflg グローバル変数が textflag に変更され、関連するプラグマ処理関数も pragprofile から pragtextflag に変更されました。
src/cmd/cc/dpchk.c (Cコンパイラのプラグマ処理部分)
--- a/src/cmd/cc/dpchk.c
+++ b/src/cmd/cc/dpchk.c
@@ -450,25 +450,19 @@ pragfpround(void)
}
void
-pragprofile(void)
+pragtextflag(void)
{
Sym *s;
- profileflg = 0;
+ textflag = 0;
s = getsym();
- if(s) {
- profileflg = atoi(s->name+1);
- if(strcmp(s->name, "on") == 0 ||
- strcmp(s->name, "yes") == 0)
- profileflg = 1;
- }
+ textflag = 7;
+ if(s)
+ textflag = atoi(s->name+1);
while(getnsc() != '\n')
;
if(debug['f'])
- if(profileflg)
- print("%4ld: profileflg %d\n", lineno, profileflg);
- else
- print("%4ld: profileflg off\n", lineno);
+ print("%4ld: textflag %d\n", lineno, textflag);
}
pragprofile 関数が pragtextflag に変更され、profileflg を設定するロジックが textflag を設定するロジックに置き換えられました。
textflag のデフォルト値は 7 に設定され、もしプラグマに数値が指定されていればその値が使われます。この 7 という値は、Goランタイムの特定の関数に対して、プロファイリングを無効化し、特定のリンケージ属性を付与するためのビットマスクであると考えられます。
src/cmd/cc/lex.c (Cコンパイラの字句解析器)
--- a/src/cmd/cc/lex.c
+++ b/src/cmd/cc/lex.c
@@ -75,7 +75,6 @@ main(int argc, char *argv[])
ginit();
arginit();
- profileflg = 1; /* #pragma can turn it off */
tufield = simplet((1L<<tfield->etype) | BUNSIGNED);
ndef = 0;
outfile = 0;
main 関数から profileflg = 1; の初期化が削除されました。これにより、profileflg は完全に textflag に置き換えられることになります。
src/cmd/cc/macbody (Cコンパイラのマクロ処理部分)
--- a/src/cmd/cc/macbody
+++ b/src/cmd/cc/macbody
@@ -725,8 +725,8 @@ macprag(void)
pragfpround();
return;
}
- if(s && strcmp(s->name, "profile") == 0) {
- pragprofile();
+ if(s && strcmp(s->name, "textflag") == 0) {
+ pragtextflag();
return;
}
if(s && strcmp(s->name, "varargck") == 0) {
#pragma profile が #pragma textflag に変更され、対応する処理関数も pragprofile から pragtextflag に変更されました。
src/runtime/proc.c (Goランタイムのプロセス管理部分)
--- a/src/runtime/proc.c
+++ b/src/runtime/proc.c
@@ -155,6 +155,7 @@ malg(int32 stacksize)
return g;
}
+#pragma textflag 7
void
sys·newproc(int32 siz, byte* fn, byte* arg0)
{
@@ -204,6 +205,7 @@ sys·newproc(int32 siz, byte* fn, byte* arg0)
//printf(" goid=%d\n", newg->goid);
}
+#pragma textflag 7
void
sys·deferproc(int32 siz, byte* fn, byte* arg0)
{
@@ -219,6 +221,7 @@ sys·deferproc(int32 siz, byte* fn, byte* arg0)
g->defer = d;
}
+#pragma textflag 7
void
sys·deferreturn(int32 arg0)
{
@@ -760,6 +763,7 @@ newstack(void)
*(int32*)345 = 123; // never return
}
+#pragma textflag 7
void
sys·morestack(uint64 u)
{
sys·newproc, sys·deferproc, sys·deferreturn, sys·morestack といった、ゴルーチンや defer の実装に不可欠なランタイム関数の定義の直前に #pragma textflag 7 が追加されました。これにより、これらの関数がコンパイルされる際に、textflag の値 7 が適用され、プロファイリングの抑制や特別なリンケージ属性の付与が行われることで、これらの低レベルなランタイム操作が安定して実行されるようになります。
関連リンク
- Go言語の初期のコンパイラに関する情報 (Plan 9 C compiler):
- Go言語の
goステートメントとゴルーチン: - Go言語の
deferステートメント:
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub):
- Go言語の
textflagに関する議論やドキュメント (当時の情報を見つけるのは困難ですが、関連するキーワードで検索):- "Go textflag meaning"
- "Go compiler pragmas"
- "Go runtime newproc deferproc"
- これらのキーワードで当時のGo言語のメーリングリストやIssueトラッカーを検索すると、より詳細な背景情報が見つかる可能性があります。
- Plan 9 C compiler の
pragmaに関する情報:- https://9p.io/sys/doc/compiler.html (上記と同じですが、
pragmaの一般的な概念を理解するのに役立ちます)
- https://9p.io/sys/doc/compiler.html (上記と同じですが、
- Go言語の
runtimeパッケージのソースコード (現在のバージョンでもtextflagの概念は存在します):- https://github.com/golang/go/tree/master/src/runtime
- 特に
asm_amd64.sやproc.goなどでtextflagの使用例が見られます。 src/cmd/internal/obj/textflag.goにtextflagの定義があります。//go:linknameや//go:noescapeなど、Go言語のコンパイラが認識するプラグマに関する情報も参考になります。textflagの値7は、NOPROF | NOSPLIT | DUPLOKの組み合わせに相当する可能性が高いです。 *NOPROF: プロファイリングを無効化。 *NOSPLIT: スタック分割を無効化(ランタイムの低レベル関数ではスタックの挙動を厳密に制御する必要があるため)。 *DUPLOK: 複数の定義があっても問題ないことを示す(リンカ向け)。 * これらのフラグは、ランタイムのコア関数が安定して動作するために重要です。