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

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

このコミットは、Goコンパイラ(cmd/gc)におけるスタックサイズのメモリアライメントに関するバグ修正です。特にamd64p32アーキテクチャ環境下で発生していた、内部コンパイラエラー「twobitwalktype1: invalid initial alignment」を解決することを目的としています。このエラーは、ポインタ領域が32ビット境界でアライメントされることによって引き起こされていました。

コミット

このコミットは、Goコンパイラのコード生成部分(src/cmd/gc/pgen.c)において、スタック上のポインタサイズおよびゼロ初期化領域のサイズ計算におけるアライメント規則を修正します。具体的には、これらのサイズをレジスタ幅(widthreg)にアライメントするように変更し、以前のポインタ幅(widthptr)へのアライメントから修正しています。これにより、特定のアーキテクチャ(amd64p32)でのコンパイラ内部エラーが解消されます。

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

https://github.com/golang/go/commit/3d5e219e020115e98762821ac688e77b1b50787d

元コミット内容

cmd/gc: enforce register-width alignment for stack sizes.

This fixes the following amd64p32 issue:
    pkg/time/format.go:724: internal compiler error: twobitwalktype1: invalid initial alignment, Time

caused by the pointer zone ending on a 32-bit-aligned boundary.

LGTM=rsc
R=rsc, dave
CC=golang-codereviews
https://golang.org/cl/72270046

変更の背景

この変更は、amd64p32という特定のアーキテクチャ環境で発生していたコンパイラの内部エラーを修正するために行われました。エラーメッセージは「internal compiler error: twobitwalktype1: invalid initial alignment, Time」であり、pkg/time/format.goの特定の行で発生していました。

この問題の根本原因は、Goコンパイラがスタックフレーム内のポインタ領域(pointer zone)のサイズを計算する際に、そのアライメントが不適切であったことにあります。具体的には、ポインタ領域が32ビット境界でアライメントされることで、twobitwalktype1という型ウォーク処理が不正な初期アライメントを検出し、コンパイラがクラッシュしていました。

amd64p32は、64ビットのレジスタと命令セットを持つx86-64アーキテクチャ上で、ポインタサイズが32ビットに制限された環境を指します。このような環境では、データのアライメント要件が通常のamd64(ポインタが64ビット)とは異なるため、コンパイラがスタックレイアウトを決定する際に特別な考慮が必要です。

Goコンパイラは、ガベージコレクションのためにスタック上のポインタを正確に識別する必要があります。そのため、スタックフレーム内のポインタの配置とサイズは厳密に管理され、特定のアライメント要件を満たす必要があります。このコミットは、このアライメント要件がamd64p32環境で満たされていなかった問題を解決します。

前提知識の解説

このコミットを理解するためには、以下の概念について理解しておく必要があります。

  1. メモリアライメント (Memory Alignment): コンピュータのメモリは、バイト単位でアドレス指定されますが、CPUがデータを効率的に読み書きするためには、特定のデータ型が特定のアドレス境界に配置されている必要があります。これをメモリアライメントと呼びます。例えば、4バイトの整数は4の倍数のアドレスに、8バイトのポインタは8の倍数のアドレスに配置されることが一般的です。アライメントが不適切だと、CPUがデータを読み書きする際にパフォーマンスが低下したり、最悪の場合、ハードウェア例外(アライメント違反)が発生したりすることがあります。

  2. スタックフレーム (Stack Frame): 関数が呼び出されると、その関数に必要なローカル変数、引数、戻りアドレスなどを格納するために、メモリのスタック領域に「スタックフレーム」が割り当てられます。スタックフレームのサイズやレイアウトは、コンパイラによって決定されます。

  3. Goコンパイラ (cmd/gc): Go言語の公式コンパイラは、gc(Go Compiler)と呼ばれ、src/cmd/gcディレクトリにそのソースコードがあります。このコンパイラは、Goのソースコードを機械語に変換するだけでなく、ランタイムの挙動(ガベージコレクションなど)に必要なメタデータも生成します。

  4. amd64p32 アーキテクチャ: amd64は、Intel/AMDの64ビットCPUアーキテクチャ(x86-64)を指します。通常、amd64環境ではポインタサイズは64ビットです。しかし、amd64p32は、64ビットCPU上で動作しながらも、ポインタサイズが32ビットに制限された特殊な環境です。これは、メモリ使用量を削減したり、既存の32ビットコードとの互換性を維持したりするために使用されることがあります。このような環境では、64ビットのレジスタは利用できるものの、アドレス空間は32ビットに制限されるため、メモリアライメントの規則が複雑になることがあります。

  5. ガベージコレクション (Garbage Collection) とポインタ情報: Go言語はガベージコレクタ(GC)を備えています。GCは、プログラムが不要になったメモリを自動的に解放する役割を担います。GCが正しく動作するためには、スタックやヒープ上のどのメモリ領域がポインタであり、どのポインタが他のオブジェクトを参照しているかを正確に知る必要があります。コンパイラは、このGCに必要なポインタ情報を生成し、実行時にランタイムが利用できるようにします。

  6. twobitwalktype1: これはGoコンパイラの内部関数名であり、型情報をウォーク(走査)してポインタの配置を特定する処理の一部です。この関数が「invalid initial alignment」というエラーを出すということは、GCがポインタを追跡するために期待するアライメントが満たされていないことを意味します。

  7. rnd 関数: rnd(size, align)は、sizealignの倍数に切り上げる(アライメントする)ためのユーティリティ関数です。例えば、rnd(10, 8)は16を返します。

  8. widthregwidthptr:

    • widthreg: レジスタの幅(サイズ)を表す定数です。amd64では通常8バイト(64ビット)です。
    • widthptr: ポインタの幅(サイズ)を表す定数です。amd64では通常8バイト(64ビット)ですが、amd64p32では4バイト(32ビット)になります。

技術的詳細

このコミットが修正する問題は、Goコンパイラがスタックフレームを構築する際のメモリアライメントの不整合に起因します。

Goのガベージコレクタは、スタック上のポインタを正確にスキャンするために、スタックフレーム内のポインタの配置に関する厳密な情報(ポインタマップ)を必要とします。この情報が正しく生成されるためには、スタック上の各領域、特にポインタを含む領域が、特定のバイト境界にアライメントされている必要があります。

問題が発生していたamd64p32環境では、ポインタのサイズは32ビット(4バイト)ですが、レジスタのサイズは64ビット(8バイト)です。Goコンパイラのsrc/cmd/gc/pgen.cファイル内のallocauto関数は、関数のスタックフレームを割り当てる際に、スタック上の様々な領域(ローカル変数、引数、ポインタ領域、ゼロ初期化領域など)のサイズを計算し、アライメントを行います。

以前のコードでは、stkptrsize(スタック上のポインタ領域のサイズ)とstkzerosize(ゼロ初期化されるスタック領域のサイズ)を計算する際に、widthptr(ポインタ幅)にアライメントしていました。

stkptrsize = rnd(stkptrsize, widthptr);
stkzerosize = rnd(stkzerosize, widthptr);

しかし、amd64p32のような環境では、ポインタは32ビットですが、CPUのレジスタは64ビットであり、スタックフレーム全体の整合性やガベージコレクタが期待するアライメントは、レジスタ幅(widthreg)に合わせる必要がある場合があります。ポインタ領域がwidthptr(32ビット)にアライメントされることで、スタックフレーム全体のレイアウトが崩れ、特にポインタ領域の終端が64ビット境界に揃わない状況が発生していました。

この不適切なアライメントが、ガベージコレクタがスタックをスキャンする際に使用するtwobitwalktype1関数によって検出され、「invalid initial alignment」という内部コンパイラエラーとして報告されていました。これは、GCがポインタマップを生成する際に、期待するメモリアライメントと実際のスタックレイアウトが一致しないために発生する重大なエラーです。

このコミットでは、stkptrsizestkzerosizeのアライメントをwidthptrからwidthregに変更することで、この問題を解決しています。

stkptrsize = rnd(stkptrsize, widthreg);
stkzerosize = rnd(stkzerosize, widthreg);

これにより、スタック上のポインタ領域とゼロ初期化領域が常にレジスタ幅(64ビット)にアライメントされるようになり、amd64p32環境においてもガベージコレクタが期待するスタックレイアウトが保証され、コンパイラエラーが解消されます。この修正は、特定のアーキテクチャにおけるメモリアライメントの厳密な要件と、ガベージコレクションの正確性を維持するためのコンパイラの役割の重要性を示しています。

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

変更はsrc/cmd/gc/pgen.cファイル内のallocauto関数にあります。

--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -429,8 +429,8 @@ allocauto(Prog* ptxt)
 		n->stkdelta = -stksize - n->xoffset;
 	}
 	stksize = rnd(stksize, widthreg);
-	stkptrsize = rnd(stkptrsize, widthptr);
-	stkzerosize = rnd(stkzerosize, widthptr);
+	stkptrsize = rnd(stkptrsize, widthreg);
+	stkzerosize = rnd(stkzerosize, widthreg);

 	fixautoused(ptxt);

具体的には、以下の2行が変更されています。

  1. stkptrsize = rnd(stkptrsize, widthptr);stkptrsize = rnd(stkptrsize, widthreg); に変更。

  2. stkzerosize = rnd(stkzerosize, widthptr);stkzerosize = rnd(stkzerosize, widthreg); に変更。

コアとなるコードの解説

allocauto関数は、Goコンパイラが関数のスタックフレームを割り当てる際に呼び出される重要な関数です。この関数は、ローカル変数や引数、そしてガベージコレクションに必要なポインタ情報のための領域など、スタック上の様々な要素のサイズとオフセットを計算します。

変更された2行は、スタックフレーム内で特に重要な2つの領域のサイズ計算に関わっています。

  • stkptrsize: これは、スタック上に存在するポインタの合計サイズ、またはポインタが配置される領域のサイズを表します。Goのガベージコレクタは、この領域をスキャンして、ヒープ上のオブジェクトへの参照を見つけ出します。
  • stkzerosize: これは、ゼロ初期化が必要なスタック領域のサイズを表します。Goでは、新しい変数はデフォルトでゼロ値に初期化されるため、この領域も適切に管理される必要があります。

rnd(size, align)関数は、sizealignの最も近い倍数に切り上げる(アライメントする)ために使用されます。

変更前は、stkptrsizestkzerosizewidthptr(ポインタの幅、amd64p32では4バイト)にアライメントされていました。これは、ポインタ自体が4バイトであるため、一見すると理にかなっているように見えます。

しかし、amd64p32環境では、CPUのレジスタは64ビット(8バイト)であり、スタックフレーム全体の整合性や、ガベージコレクタがスタックを効率的かつ正確にスキャンするために期待するアライメントは、レジスタ幅(widthreg、8バイト)に合わせる必要がありました。ポインタ領域が32ビット境界で終わってしまうと、64ビット境界を期待するGCの内部処理(twobitwalktype1)が不正なアライメントを検出し、コンパイラエラーを引き起こしていました。

このコミットでは、stkptrsizestkzerosizeのアライメントをwidthptrからwidthregに変更することで、これらの領域が常に8バイト境界にアライメントされるようにしました。これにより、amd64p32環境においてもスタックフレームのレイアウトがガベージコレクタの期待する形式に適合し、コンパイラエラーが解消されました。

この修正は、コンパイラが生成するコードの正確性と、ガベージコレクションの信頼性を保証するために、メモリアライメントがいかに重要であるかを示しています。特に、異なるアーキテクチャやポインタサイズが混在する環境では、このような細かなアライメントの調整が不可欠となります。

関連リンク

参考にした情報源リンク

  • Go言語のガベージコレクションに関するドキュメントやブログ記事 (一般的なGCの仕組みとポインタスキャンについて)
  • x86-64アーキテクチャにおけるメモリアライメントの原則
  • Goコンパイラのソースコード(src/cmd/gc/pgen.c)の関連部分
  • amd64p32に関する情報 (特にポインタサイズとレジスタ幅の違いについて)
  • Goの内部コンパイラエラーに関する議論や報告 (特にtwobitwalktype1に関連するもの)
  • Goのコードレビューシステム (Gerrit) のCL (Change List) ページ (CL 72270046)