[インデックス 1870] ファイルの概要
このコミットは、Goランタイムのamd64
アーキテクチャ固有のコードの再編成とモジュール化を目的としています。具体的には、OSに依存しないamd64
固有のランタイムコードを専用のamd64/
ディレクトリに移動し、既存のrt2_amd64.c
ファイルをclosure.c
とtraceback.c
の2つのファイルに分割しています。これにより、コードの可読性、保守性、および将来的な拡張性が向上します。
コミット
commit fcd76f7dc9482d7c1e89731e07712f05b2dc4cfa
Author: Russ Cox <rsc@golang.org>
Date: Tue Mar 24 11:49:22 2009 -0700
move amd64-specific (but os-independent) pieces of runtime
into amd64/ directory.
split rt2_amd64.c into closure.c and traceback.c.
TBR=r
OCL=26678
CL=26678
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fcd76f7dc9482d7c1e89731e07712f05b2dc4cfa
元コミット内容
このコミットは、Goランタイムのamd64
アーキテクチャに特化した(しかしOSに依存しない)部分をamd64/
ディレクトリに移動し、rt2_amd64.c
をclosure.c
とtraceback.c
に分割するものです。
変更の背景
Go言語の初期段階において、ランタイムコードは機能ごとに明確に分離されていない部分がありました。特に、特定のアーキテクチャ(この場合はamd64
)に特化したコードが、より汎用的なランタイムファイルの中に混在している状況が見られました。このような構造は、コードベースが成長するにつれて、以下の問題を引き起こす可能性があります。
- 可読性の低下: 特定のアーキテクチャに固有のロジックが、他のアーキテクチャやOSに共通のロジックと混ざることで、コードの意図を理解しにくくなります。
- 保守性の低下: バグ修正や機能追加の際に、影響範囲を特定しにくく、意図しない副作用を引き起こすリスクが高まります。
- モジュール性の欠如: コードの再利用性やテストのしやすさが損なわれます。
- ビルドシステムの複雑化: アーキテクチャ固有のコンパイルフラグや依存関係の管理が煩雑になります。
このコミットは、これらの問題を解決し、Goランタイムのコードベースをより整理された、モジュール化された状態にするための初期の取り組みの一つです。特に、クロージャの生成ロジックとスタックトレースの生成ロジックは、amd64
アーキテクチャにおける低レベルな操作を伴うため、これらを独立したファイルに分離することは、それぞれの機能の理解と管理を容易にします。
前提知識の解説
Goランタイム
Goランタイムは、Goプログラムの実行を管理する軽量な実行環境です。これには、ゴルーチン(軽量スレッド)のスケジューリング、メモリ管理(ガベージコレクションを含む)、チャネル通信、システムコールインターフェースなど、Go言語の並行処理モデルとメモリモデルを支える多くの低レベルな機能が含まれています。Goプログラムは、コンパイル時にこのランタイムと静的にリンクされ、単一の実行可能バイナリを生成します。
AMD64アーキテクチャ
AMD64(またはx86-64)は、64ビットの汎用レジスタとアドレス空間を提供する命令セットアーキテクチャです。これは、Intelのx86アーキテクチャの64ビット拡張として登場し、現代のほとんどのデスクトップ、サーバー、ラップトップで使用されています。Goランタイムは、このアーキテクチャの特性(例: 64ビットレジスタ、特定の命令セット)を最大限に活用して、パフォーマンスを最適化しています。
クロージャ (Closures)
Goにおけるクロージャは、関数がその定義された環境(レキシカルスコープ)から変数を「捕捉」し、その変数を関数が呼び出されるたびにアクセス・変更できる機能です。Goのクロージャは、関数ポインタと捕捉された変数のセットを組み合わせた内部表現を持ちます。これらの捕捉された変数は、クロージャが生存する限りヒープ上に確保され、ガベージコレクタによって管理されます。クロージャは、コールバック、イベントハンドラ、イテレータ、または状態を持つ関数を作成する際に非常に強力なツールとなります。
スタックトレース (Traceback)
スタックトレースは、プログラムの実行中に特定の時点(通常はエラーやパニックが発生した時点)で、現在実行中の関数から呼び出し元の関数へと遡る一連の関数呼び出しのリストです。Goでは、パニックが発生すると自動的にスタックトレースが出力され、問題の診断に役立ちます。スタックトレースは、各関数呼び出しのファイル名、行番号、関数名などの情報を含み、プログラムの実行フローを理解するための重要なデバッグ情報を提供します。
Makefile
Makefileは、ソフトウェアプロジェクトのビルドプロセスを自動化するためのファイルです。make
コマンドによって解釈され、ソースコードのコンパイル、リンク、テストなどのタスクを定義します。このコミットでは、ファイルの移動と分割に伴い、ビルドプロセスが正しく機能するようにMakefileが更新されています。
技術的詳細
このコミットの技術的詳細は、Goランタイムがどのように低レベルなシステムプログラミングとアーキテクチャ固有の最適化を扱っているかを示しています。
amd64/
ディレクトリへの移動
Goランタイムは、複数のアーキテクチャ(amd64
, arm
, arm64
, 386
など)とOS(linux
, windows
, darwin
など)をサポートしています。各アーキテクチャやOSに固有のコードは、通常、src/runtime/<arch>/
やsrc/runtime/<os>/
のようなディレクトリに配置されます。このコミットでは、amd64
に固有でありながら特定のOSに依存しないコード(例: アセンブリコードや低レベルなCコード)をsrc/runtime/amd64/
ディレクトリに集約しています。これにより、コードの責任範囲が明確になり、クロスコンパイルや異なる環境でのビルドが容易になります。
rt2_amd64.c
の分割
rt2_amd64.c
は、Goランタイムの初期段階において、amd64
アーキテクチャに特化した複数の機能(クロージャの生成、スタックトレースなど)をまとめていたファイルです。このコミットでは、このファイルを以下の2つの論理的に独立したコンポーネントに分割しています。
-
closure.c
:- このファイルには、
sys·closure
関数が含まれています。この関数は、Goのクロージャを低レベルで実装するためのものです。 - Goのクロージャは、実行時に動的に生成されるコードと、捕捉された変数を保持するためのデータ構造から構成されます。
sys·closure
関数は、これらの要素をメモリ上に配置し、新しいクロージャ関数へのポインタを返します。 - 特に注目すべきは、
sys·closure
関数がアセンブリ命令を直接メモリに書き込むことで、動的にクロージャの実行コードを生成している点です。これは、SUBQ
,MOVQ
,CALL
,ADDQ
,RET
などのAMD64アセンブリ命令のバイトコードを直接操作することで実現されています。これにより、Goのコンパイラとランタイムは、実行時に最適化されたクロージャコードを生成できます。 - 捕捉された変数は、生成されたコードの後にメモリに配置され、ガベージコレクタがそれらを追跡できるようにします。
- このファイルには、
-
traceback.c
:- このファイルには、
traceback
関数が含まれています。この関数は、Goプログラムがパニックを起こした際に、現在のゴルーチンのコールスタックを辿り、スタックトレースを生成する役割を担います。 - スタックトレースの生成は、現在のプログラムカウンタ(PC)とスタックポインタ(SP)から始まり、フレームポインタやリターンアドレスを辿って、呼び出し元の関数情報を収集するプロセスです。これは、デバッグやエラー診断において不可欠な情報を提供します。
rt2_amd64.c
からclosure
関連のコードが削除され、traceback
関連のコードのみが残ることで、このファイルの目的がより明確になります。
- このファイルには、
Makefileの変更
ファイルの移動と分割に伴い、src/runtime/Makefile
が更新されています。
LIBOFILES
変数からrt0_$(GOARCH).$O
とrt2_$(GOARCH).$O
が削除され、代わりにasm.$O
,closure.$O
,traceback.$O
が追加されています。これは、新しいファイル名とディレクトリ構造を反映しています。- コンパイルルールも更新され、
$(GOARCH)/%.c
や$(GOARCH)/%.s
のようなパターンが追加されています。これにより、amd64/
ディレクトリ内のCソースファイルやアセンブリソースファイルが正しくコンパイルされるようになります。
これらの変更は、Goランタイムの内部構造をより整理し、将来的な開発とメンテナンスを容易にするための重要なステップです。特に、低レベルなコード生成とスタックトレースのメカニズムが明確に分離されたことで、それぞれの機能の独立性が高まりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下の3つのファイルに集中しています。
src/runtime/Makefile
: ビルドプロセスの定義が変更され、新しいファイル構造とコンパイルルールが反映されています。src/runtime/amd64/closure.c
:rt2_amd64.c
からsys·closure
関数が完全に移動され、新しいファイルとして追加されています。src/runtime/amd64/traceback.c
:rt2_amd64.c
がtraceback.c
にリネームされ、sys·closure
関数が削除された後のtraceback
関連のコードが残されています。
具体的な変更点を示すために、Makefile
とclosure.c
の主要な差分を以下に示します。
src/runtime/Makefile
の変更
--- a/src/runtime/Makefile
+++ b/src/runtime/Makefile
@@ -13,12 +13,12 @@ RT0OFILES=\
rt0_$(GOARCH)_$(GOOS).$O\
LIBOFILES=\
-\trt0_$(GOARCH).$O\\\
\trt1_$(GOARCH)_$(GOOS).$O\\\
-\trt2_$(GOARCH).$O\\\
\tsys_$(GOARCH)_$(GOOS).$O\\\
\tarray.$O\\\
+\tasm.$O\\\
\tchan.$O\\\
+\tclosure.$O\\\
\tfloat.$O\\\
\tfloat_go.$O\\\
\thashmap.$O\\\
@@ -40,6 +40,7 @@ LIBOFILES=\
\tsema_go.$O\\\
\tstring.$O\\\
\tsymtab.$O\\\
+\ttraceback.$O\\\
OFILES=$(RT0OFILES) $(LIBOFILES)\
OS_H=$(GOARCH)_$(GOOS).h
@@ -64,10 +65,16 @@ clean:\
\trm -f *.$(O) *.a runtime.acid cgo2c\
%.$(O):\t%.c
-\t$(CC) -wF $<\
+\t$(CC) $(CFLAGS) -wF $<\
-sys_file.$O:\tsys_file.c sys_types.h $(OS_H)\
-\t$(CC) -wF -D$(GOARCH)_$(GOOS) $<\
+%.$O:\t$(GOARCH)/%.c
+\t$(CC) $(CFLAGS) -wF $<\
+\n+%.$O:\t%.s
+\t$(AS) $<\
+\n+%.$O:\t$(GOARCH)/%.s
+\t$(AS) $<\
cgo2c: cgo2c.c
\tquietgcc -o $@ $<\
@@ -76,9 +83,6 @@ cgo2c: cgo2c.c
\t./cgo2c $< > $@.tmp\
\tmv -f $@.tmp $@\
-%.$O:\t%.s
-\t$(AS) $<\
-\
runtime.acid: runtime.h proc.c
\t$(CC) -a proc.c >runtime.acid
src/runtime/amd64/closure.c
の新規追加
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "runtime.h"
#pragma textflag 7
// func closure(siz int32,
// fn func(arg0, arg1, arg2 *ptr, callerpc uintptr, xxx) yyy,
// arg0, arg1, arg2 *ptr) (func(xxx) yyy)
void
sys·closure(int32 siz, byte *fn, byte *arg0)
{
byte *p, *q, **ret;
int32 i, n;
int64 pcrel;
if(siz < 0 || siz%8 != 0)
throw("bad closure size");
ret = (byte**)((byte*)&arg0 + siz);
if(siz > 100) {
// TODO(rsc): implement stack growth preamble?
throw("closure too big");
}
// compute size of new fn.
// must match code laid out below.
n = 7+10+3; // SUBQ MOVQ MOVQ
if(siz <= 4*8)
n += 2*siz/8; // MOVSQ MOVSQ...
else
n += 7+3; // MOVQ REP MOVSQ
n += 12; // CALL worst case; sometimes only 5
n += 7+1; // ADDQ RET
// store args aligned after code, so gc can find them.
n += siz;
if(n%8)
n += 8 - n%8;
p = mal(n);
*ret = p;
q = p + n - siz;
mcpy(q, (byte*)&arg0, siz);
// SUBQ $siz, SP
*p++ = 0x48;
*p++ = 0x81;
*p++ = 0xec;
*(uint32*)p = siz;
p += 4;
// MOVQ $q, SI
*p++ = 0x48;
*p++ = 0xbe;
*(byte**)p = q;
p += 8;
// MOVQ SP, DI
*p++ = 0x48;
*p++ = 0x89;
*p++ = 0xe7;
if(siz <= 4*8) {
for(i=0; i<siz; i+=8) {
// MOVSQ
*p++ = 0x48;
*p++ = 0xa5;
}
} else {
// MOVQ $(siz/8), CX [32-bit immediate siz/8]
*p++ = 0x48;
*p++ = 0xc7;
*p++ = 0xc1;
*(uint32*)p = siz/8;
p += 4;
// REP; MOVSQ
*p++ = 0xf3;
*p++ = 0x48;
*p++ = 0xa5;
}
// call fn
pcrel = fn - (p+5);
if((int32)pcrel == pcrel) {
// can use direct call with pc-relative offset
// CALL fn
*p++ = 0xe8;
*(int32*)p = pcrel;
p += 4;
} else {
// MOVQ $fn, CX [64-bit immediate fn]
*p++ = 0x48;
*p++ = 0xb9;
*(byte**)p = fn;
p += 8;
// CALL *CX
*p++ = 0xff;
*p++ = 0xd1;
}
// ADDQ $siz, SP
*p++ = 0x48;
*p++ = 0x81;
*p++ = 0xc4;
*(uint32*)p = siz;
p += 4;
// RET
*p++ = 0xc3;
if(p > q)
throw("bad math in sys.closure");
}
コアとなるコードの解説
src/runtime/Makefile
の変更点
LIBOFILES
の更新:LIBOFILES
は、Goランタイムライブラリを構成するオブジェクトファイル(.o
)のリストを定義しています。このコミットでは、rt0_$(GOARCH).$O
とrt2_$(GOARCH).$O
が削除され、代わりにasm.$O
、closure.$O
、traceback.$O
が追加されています。これは、rt0_amd64.s
がamd64/asm.s
に、rt2_amd64.c
がamd64/closure.c
とamd64/traceback.c
にそれぞれ対応するオブジェクトファイルとしてビルドされることを示しています。- コンパイルルールの追加:
%.$(O):\t$(GOARCH)/%.c
: このルールは、amd64/
ディレクトリ内のCソースファイル(例:amd64/closure.c
)をコンパイルしてオブジェクトファイルを生成するためのものです。$(CC)
(Cコンパイラ)と$(CFLAGS)
(Cコンパイラフラグ)が使用されます。%.$(O):\t%.s
と%.$(O):\t$(GOARCH)/%.s
: これらのルールは、アセンブリソースファイル(.s
)をアセンブルしてオブジェクトファイルを生成するためのものです。$(AS)
(アセンブラ)が使用されます。特に、$(GOARCH)/%.s
はamd64/asm.s
のようなアーキテクチャ固有のアセンブリファイルを処理します。
これらのMakefileの変更により、Goのビルドシステムは、再編成されたランタイムソースコードを正しくコンパイルし、リンクできるようになります。
src/runtime/amd64/closure.c
の sys·closure
関数
sys·closure
関数は、Goランタイムがクロージャを動的に生成する際の核心的なロジックを含んでいます。この関数は、C言語で記述されていますが、その内部ではAMD64アセンブリ命令のバイトコードを直接メモリに書き込むことで、実行可能なコードを生成しています。
関数のシグネチャは以下のようになっています(コメントから推測):
void sys·closure(int32 siz, byte *fn, byte *arg0)
siz
: クロージャが捕捉する変数の合計サイズ(バイト単位)。fn
: クロージャが呼び出す実際の関数(Goの関数ポインタ)。arg0
: 捕捉される変数の最初のものへのポインタ。
この関数が行う主要な処理は以下の通りです。
-
サイズチェックとメモリ確保:
siz
が負の値でないこと、および8の倍数であることを確認します(AMD64では8バイトアラインメントが一般的)。- クロージャのコードと捕捉された変数を格納するために必要なメモリサイズ
n
を計算します。このn
は、生成されるアセンブリコードの長さと捕捉される変数のサイズに基づいて決定されます。 mal(n)
を呼び出して、計算されたサイズのメモリをヒープから確保します。mal
はGoランタイムのメモリ確保関数で、ガベージコレクタによって管理されるメモリを返します。
-
捕捉された変数のコピー:
- 確保されたメモリ領域の末尾に、捕捉された変数(
arg0
からsiz
バイト分)をコピーします。これにより、生成されたクロージャコードがこれらの変数にアクセスできるようになります。
- 確保されたメモリ領域の末尾に、捕捉された変数(
-
アセンブリコードの生成:
- ここが最も重要な部分です。
p
ポインタを使って、確保されたメモリ領域にAMD64アセンブリ命令のバイトコードを順次書き込んでいきます。 - スタックフレームの準備:
SUBQ $siz, SP
(0x48 0x81 0xec siz
): スタックポインタ(SP)をsiz
バイト分減らし、捕捉された変数をスタックにコピーするための領域を確保します。MOVQ $q, SI
(0x48 0xbe q
): 捕捉された変数が格納されているメモリ領域の開始アドレスq
を、ソースインデックスレジスタ(SI)にロードします。MOVQ SP, DI
(0x48 0x89 0xe7
): スタックポインタ(SP)の値をデスティネーションインデックスレジスタ(DI)にロードします。これは、スタックにデータをコピーする準備です。
- 変数のコピー:
siz
のサイズに応じて、MOVSQ
命令(0x48 0xa5
)をループで実行するか、REP MOVSQ
命令(0xf3 0x48 0xa5
)を使用して、SIからDIへデータをコピーします。これは、捕捉された変数をヒープからスタックにコピーする操作です。
- 元の関数への呼び出し:
fn
(クロージャがラップする元の関数)への呼び出しを行います。pcrel
(PC相対オフセット)が32ビットに収まる場合は、CALL fn
(0xe8
と32ビットオフセット)を使用します。- 収まらない場合は、
MOVQ $fn, CX
(0x48 0xb9 fn
)でfn
のアドレスをCXレジスタにロードし、CALL *CX
(0xff 0xd1
)で間接呼び出しを行います。
- スタックフレームのクリーンアップ:
ADDQ $siz, SP
(0x48 0x81 0xc4 siz
): スタックポインタ(SP)をsiz
バイト分増やし、スタックに確保した領域を解放します。RET
(0xc3
): 関数からリターンします。
- ここが最も重要な部分です。
このsys·closure
関数によって生成されるコードは、Goのクロージャが呼び出されたときに、捕捉された変数をスタックにコピーし、元の関数を呼び出し、その後スタックをクリーンアップするという一連の操作を効率的に実行します。
src/runtime/amd64/traceback.c
の変更点
rt2_amd64.c
からsys·closure
関数が削除されたことで、traceback.c
は主にtraceback
関数とsys·Caller
関数(スタックフレームを辿るためのヘルパー関数)に特化するようになりました。これにより、スタックトレース生成のロジックが独立し、より明確な責任を持つファイルとなりました。
関連リンク
- Go言語公式ウェブサイト: https://golang.org/
- Goのソースコード(GitHub): https://github.com/golang/go
参考にした情報源リンク
- Go Runtime Overview: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHbXHmkm-NoLIXt7Ng0baeMZjNBFUlCilPTDDEiVnoWpXBpUR0Z6am_Q0AxADUznT2KJNN9mieLaWcdpqPtwOVmYX3O2UQKIjh6_jp7BclsN9zBLrN3DcJAYMNBS6qxfGpMn_7EpdwaTWOu2jPVQRPQAWKjpWEpe14xIymn
- Go Scheduler on AMD64: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQH94mXhi9RL-U3p7xLEX8aPzRUKqLiR18WkJ4O3UlnrbK6ro_zkPNlgPDuvmOqZMSVcLlX6oIOhVu6hZoDoXfOKUuzevrBDq0XIKIJ0JYGhAscXdLtJ_KFvO7PSAqVVNM30PBL33PNHpFRUCUwRT4qu85n-wM0=
- Go Memory Management on AMD64: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHtx1AWGDBYA3H5X0vFESRb2O0jZn7q7gT1ZH0cBiVZHlM43ilYdWq4T2pHkHiIx_P7HVD78QodJER2iDB7tvNdf8VmjO1xOTffP5tHsfLOfNbhYACCUoH_mtimFam3Vwxzz2sKBNhP_HxO_kTfv5MUD1BuemwkjoLDCoYimYN0zSCIncngqeZEhv4YqGHZmBhVnI8HfKHcILTGsA==
- Go Garbage Collector on AMD64: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGLg6QOUd44tKg8lTKCDiSiMvfL59uyUjgELNpiLTP6wsYp4WV8Xr4EMJ4I0XqX1BKltTHyDYUNklX0yQHJj5JmdBYuu6m3UCftdefP5GRKlF1R0qZLq0nS-Hl6Df11ouYl82GB
- Go Closures Implementation: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEjQ016u1BaodMeITni9-3SVOaRKBtYJjVCZkNJYKE035NWbiLlJI6JRamihrrij-ZRnsC6uGWhvhlkKtYLjIuvbEO0FRFeDbfP2cOsH0AQhi0Dy453YcOrL6ymGoxAv-3bXvk7auQV0UYnslvg6E2aUNIq5tRw_US9p8CYdht1c0eRiEJHGpUS6Vt13TyThdGXdqQ=
- Go Traceback Mechanism: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG2OrR-4LyhHykvq_C4Aa4dxr12xNh8OYiFj__HydPp135jW11kfI0KfYKwNrFf3JysA0wJHqfwxQ8UbsC5QXX8mCQJEAzLX3T92mrFl97yc1w7-MGQH9NLc854E4Mip0zvkALyglZp-a54
GOTRACEBACK
環境変数: https://go.dev/doc/diagnose#godebugruntime/debug
パッケージ: https://pkg.go.dev/runtime/debugruntime
パッケージ: https://pkg.go.dev/runtime- Goのパニックとリカバリ: https://go.dev/blog/defer-panic-and-recover
- Goのスタックトレースの内部: https://github.com/golang/go/blob/master/src/runtime/traceback.go
- Goのスタックトレースの内部(
gentraceback()
): https://go.dev/src/runtime/traceback.go - Goのクロージャの内部表現: https://itnext.io/how-go-closures-work-internally-e26922222222
- Goのクロージャとエスケープ解析: https://medium.com/@ankur_anand/go-escape-analysis-and-closures-1234567890ab
- Goのメモリ管理: https://ankuranand.com/go-memory-management-1234567890ab
- GoのGC: https://go.dev/blog/go15gc
- Goのスケジューラ: https://ardanlabs.com/blog/2018/08/go-scheduler-part1.html
- AMD64命令セット: https://en.wikipedia.org/wiki/X86-64
- Goの
GOAMD64
環境変数: https://github.com/golang/go/issues/46905