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

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

このコミットは、Goランタイムのamd64アーキテクチャ固有のコードの再編成とモジュール化を目的としています。具体的には、OSに依存しないamd64固有のランタイムコードを専用のamd64/ディレクトリに移動し、既存のrt2_amd64.cファイルをclosure.ctraceback.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.cclosure.ctraceback.cに分割するものです。

変更の背景

Go言語の初期段階において、ランタイムコードは機能ごとに明確に分離されていない部分がありました。特に、特定のアーキテクチャ(この場合はamd64)に特化したコードが、より汎用的なランタイムファイルの中に混在している状況が見られました。このような構造は、コードベースが成長するにつれて、以下の問題を引き起こす可能性があります。

  1. 可読性の低下: 特定のアーキテクチャに固有のロジックが、他のアーキテクチャやOSに共通のロジックと混ざることで、コードの意図を理解しにくくなります。
  2. 保守性の低下: バグ修正や機能追加の際に、影響範囲を特定しにくく、意図しない副作用を引き起こすリスクが高まります。
  3. モジュール性の欠如: コードの再利用性やテストのしやすさが損なわれます。
  4. ビルドシステムの複雑化: アーキテクチャ固有のコンパイルフラグや依存関係の管理が煩雑になります。

このコミットは、これらの問題を解決し、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つの論理的に独立したコンポーネントに分割しています。

  1. closure.c:

    • このファイルには、sys·closure関数が含まれています。この関数は、Goのクロージャを低レベルで実装するためのものです。
    • Goのクロージャは、実行時に動的に生成されるコードと、捕捉された変数を保持するためのデータ構造から構成されます。sys·closure関数は、これらの要素をメモリ上に配置し、新しいクロージャ関数へのポインタを返します。
    • 特に注目すべきは、sys·closure関数がアセンブリ命令を直接メモリに書き込むことで、動的にクロージャの実行コードを生成している点です。これは、SUBQ, MOVQ, CALL, ADDQ, RETなどのAMD64アセンブリ命令のバイトコードを直接操作することで実現されています。これにより、Goのコンパイラとランタイムは、実行時に最適化されたクロージャコードを生成できます。
    • 捕捉された変数は、生成されたコードの後にメモリに配置され、ガベージコレクタがそれらを追跡できるようにします。
  2. traceback.c:

    • このファイルには、traceback関数が含まれています。この関数は、Goプログラムがパニックを起こした際に、現在のゴルーチンのコールスタックを辿り、スタックトレースを生成する役割を担います。
    • スタックトレースの生成は、現在のプログラムカウンタ(PC)とスタックポインタ(SP)から始まり、フレームポインタやリターンアドレスを辿って、呼び出し元の関数情報を収集するプロセスです。これは、デバッグやエラー診断において不可欠な情報を提供します。
    • rt2_amd64.cからclosure関連のコードが削除され、traceback関連のコードのみが残ることで、このファイルの目的がより明確になります。

Makefileの変更

ファイルの移動と分割に伴い、src/runtime/Makefileが更新されています。

  • LIBOFILES変数からrt0_$(GOARCH).$Ort2_$(GOARCH).$Oが削除され、代わりにasm.$O, closure.$O, traceback.$Oが追加されています。これは、新しいファイル名とディレクトリ構造を反映しています。
  • コンパイルルールも更新され、$(GOARCH)/%.c$(GOARCH)/%.sのようなパターンが追加されています。これにより、amd64/ディレクトリ内のCソースファイルやアセンブリソースファイルが正しくコンパイルされるようになります。

これらの変更は、Goランタイムの内部構造をより整理し、将来的な開発とメンテナンスを容易にするための重要なステップです。特に、低レベルなコード生成とスタックトレースのメカニズムが明確に分離されたことで、それぞれの機能の独立性が高まりました。

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

このコミットにおけるコアとなるコードの変更は、主に以下の3つのファイルに集中しています。

  1. src/runtime/Makefile: ビルドプロセスの定義が変更され、新しいファイル構造とコンパイルルールが反映されています。
  2. src/runtime/amd64/closure.c: rt2_amd64.cからsys·closure関数が完全に移動され、新しいファイルとして追加されています。
  3. src/runtime/amd64/traceback.c: rt2_amd64.ctraceback.cにリネームされ、sys·closure関数が削除された後のtraceback関連のコードが残されています。

具体的な変更点を示すために、Makefileclosure.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).$Ort2_$(GOARCH).$Oが削除され、代わりにasm.$Oclosure.$Otraceback.$Oが追加されています。これは、rt0_amd64.samd64/asm.sに、rt2_amd64.camd64/closure.camd64/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)/%.samd64/asm.sのようなアーキテクチャ固有のアセンブリファイルを処理します。

これらのMakefileの変更により、Goのビルドシステムは、再編成されたランタイムソースコードを正しくコンパイルし、リンクできるようになります。

src/runtime/amd64/closure.csys·closure 関数

sys·closure関数は、Goランタイムがクロージャを動的に生成する際の核心的なロジックを含んでいます。この関数は、C言語で記述されていますが、その内部ではAMD64アセンブリ命令のバイトコードを直接メモリに書き込むことで、実行可能なコードを生成しています。

関数のシグネチャは以下のようになっています(コメントから推測): void sys·closure(int32 siz, byte *fn, byte *arg0)

  • siz: クロージャが捕捉する変数の合計サイズ(バイト単位)。
  • fn: クロージャが呼び出す実際の関数(Goの関数ポインタ)。
  • arg0: 捕捉される変数の最初のものへのポインタ。

この関数が行う主要な処理は以下の通りです。

  1. サイズチェックとメモリ確保:

    • sizが負の値でないこと、および8の倍数であることを確認します(AMD64では8バイトアラインメントが一般的)。
    • クロージャのコードと捕捉された変数を格納するために必要なメモリサイズnを計算します。このnは、生成されるアセンブリコードの長さと捕捉される変数のサイズに基づいて決定されます。
    • mal(n)を呼び出して、計算されたサイズのメモリをヒープから確保します。malはGoランタイムのメモリ確保関数で、ガベージコレクタによって管理されるメモリを返します。
  2. 捕捉された変数のコピー:

    • 確保されたメモリ領域の末尾に、捕捉された変数(arg0からsizバイト分)をコピーします。これにより、生成されたクロージャコードがこれらの変数にアクセスできるようになります。
  3. アセンブリコードの生成:

    • ここが最も重要な部分です。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 fn0xe8と32ビットオフセット)を使用します。
      • 収まらない場合は、MOVQ $fn, CX0x48 0xb9 fn)でfnのアドレスをCXレジスタにロードし、CALL *CX0xff 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関数(スタックフレームを辿るためのヘルパー関数)に特化するようになりました。これにより、スタックトレース生成のロジックが独立し、より明確な責任を持つファイルとなりました。

関連リンク

参考にした情報源リンク