[インデックス 17515] ファイルの概要
このコミットは、Goコンパイラの6g
(AMD64アーキテクチャ向けコンパイラ)のバックエンドにおける、非常に大きなオフセット値の取り扱いに関するバグ修正を目的としています。具体的には、OINDREG
(間接レジスタ)命令が32ビットを超えるオフセットを生成してしまう問題(Issue 6036)を解決します。この修正により、コンパイラが生成するアセンブリコードが、メモリ上の非常に大きなデータ構造(例えば、巨大な配列や構造体)の要素に正しくアクセスできるようになります。
コミット
commit 9c21ce54dd3625aac3b948b509a9562b684434bc
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Mon Sep 9 20:36:19 2013 +0200
cmd/6g: handle very wide offsets.
Fixes #6036.
R=golang-dev, bradfitz, rsc
CC=golang-dev
https://golang.org/cl/12992043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9c21ce54dd3625aac3b948b509a9562b684434bc
元コミット内容
cmd/6g: handle very wide offsets.
Fixes #6036.
R=golang-dev, bradfitz, rsc
CC=golang-dev
https://golang.org/cl/12992043
変更の背景
このコミットは、Goコンパイラの6g
(AMD64アーキテクチャ向けのGoコンパイラ)が抱えていた、非常に大きなメモリオフセットを扱う際のバグ(Issue 6036)を修正するために導入されました。
問題の核心は、6g
のバックエンドが、OINDREG
という中間表現(IR)を生成する際に、32ビット符号付き整数で表現できる範囲を超えるオフセット値を生成してしまう可能性があった点にあります。AMD64アーキテクチャでは、メモリ参照は通常、ベースレジスタとオフセットの組み合わせで行われます。このオフセットは、多くの場合、32ビット符号付き整数として扱われます。しかし、Goプログラムが非常に大きな配列や構造体(例えば、[1 << 31]byte
のようなサイズ)を扱う場合、その要素へのアクセスに必要なオフセットが32ビットの範囲を超えてしまうことがあります。
コンパイラがこのような「広すぎるオフセット」を持つOINDREG
命令を生成すると、最終的に生成されるアセンブリコードが不正になり、実行時にメモリへのアクセスが失敗したり、予期せぬ動作を引き起こしたりする可能性がありました。このバグは、特に大規模なデータ構造を扱うアプリケーションや、メモリレイアウトが厳密に管理されるシステムプログラミングにおいて、深刻な問題となり得ました。
この修正は、コンパイラが生成するオフセットが32ビットの範囲を超える場合に、そのオフセットをレジスタ加算に変換することで、この問題を回避し、Goプログラムが巨大なデータ構造を正しく扱えるようにすることを目的としています。
前提知識の解説
このコミットの理解には、以下の概念が役立ちます。
- Goコンパイラ (6g): Go言語のコンパイラは、ターゲットアーキテクチャごとに異なるバックエンドを持っています。
6g
は、AMD64(x86-64)アーキテクチャ向けのGoコンパイラのバックエンドを指します。コンパイラのフロントエンドがGoのソースコードを解析し、中間表現(IR)を生成した後、バックエンドがそのIRをターゲットアーキテクチャの機械語に変換します。 - 中間表現 (IR): コンパイラがソースコードを直接機械語に変換するのではなく、まず抽象的な中間形式に変換します。これにより、コンパイラの設計がモジュール化され、異なるフロントエンドやバックエンドを組み合わせることが容易になります。Goコンパイラも独自のIRを使用しており、
OINDREG
はそのIRにおける一種のノード(操作)です。 OINDREG
(Offset Indirect Register): これはGoコンパイラの中間表現におけるノードの一種で、「レジスタにオフセットを加算したアドレスにあるメモリ内容を参照する」操作を表します。例えば、*(base_register + offset)
のようなメモリアクセスに対応します。アセンブリ言語では、MOV (RBP + 0x1234), RAX
のように表現されることがあります。ここでRBP
がベースレジスタ、0x1234
がオフセットです。- 32ビット符号付き整数: 32ビットで表現できる符号付き整数の範囲は、約 -2,147,483,648 から 2,147,483,647 までです。多くのCPUアーキテクチャでは、メモリ参照のオフセットとしてこの範囲の値を効率的に扱えます。しかし、この範囲を超えるオフセットは、直接命令のオペランドとして指定できないため、特別な処理が必要になります。
- メモリレイアウトとオフセット: プログラムが使用するデータはメモリ上に配置されます。構造体のフィールドや配列の要素は、その構造体や配列の先頭アドレスからの相対的な位置(オフセット)で識別されます。非常に大きな配列や構造体の場合、その内部の要素へのオフセットが大きくなることがあります。
- レジスタ加算: オフセットが大きすぎて直接命令に含められない場合、そのオフセット値を別のレジスタにロードし、そのレジスタの値をベースレジスタに加算することで、目的のアドレスを計算します。例えば、
MOV RDX, 0x100000000
(オフセットをRDXにロード) ->ADD RAX, RDX
(RAXにRDXを加算) ->MOV (RAX), RBX
(RAXが指すアドレスからロード) のように、複数の命令に分割して処理されます。
技術的詳細
このコミットの技術的解決策は、fixlargeoffset
という新しい関数を導入し、コンパイラのコード生成パスの複数の箇所でこの関数を呼び出すことで、32ビット符号付き整数の範囲を超えるオフセットを検出・処理することにあります。
fixlargeoffset
関数の主なロジックは以下の通りです。
- ノードのチェック: 引数として渡された
Node
がOINDREG
(間接レジスタ)操作を表すものであるかを確認します。OINDREG
でない場合は、この関数は何もせずに関数を終了します。 - スタックオフセットの除外:
n->val.u.reg == D_SP
(スタックポインタSP
をベースレジスタとするオフセット)の場合、スタックオフセットは通常、そこまで巨大になることはないため、処理をスキップします。これは、スタックフレームのサイズが通常、32ビットオフセットの範囲内に収まるという仮定に基づいています。 - オフセットのサイズチェック:
n->xoffset != (int32)n->xoffset
という条件で、現在のオフセット値n->xoffset
が32ビット符号付き整数の範囲に収まっているかを確認します。xoffset
はvlong
(64ビット整数)型であるため、int32
にキャストした値と比較することで、オーバーフローが発生しているか(つまり、オフセットが32ビットの範囲を超えているか)を検出できます。 - オフセットの変換:
- もしオフセットが32ビットの範囲を超えている場合、現在の
OINDREG
ノードn
の情報を一時的なNode a
にコピーします。 a.op
をOREGISTER
に設定し、a.type
をポインタ型(types[tptr]
)に設定し、a.xoffset
を0に設定します。これは、a
がベースレジスタ自体を表すようにするためです。cgen_checknil(&a)
を呼び出し、ポインタがnilでないことを確認するコードを生成します。これは、Goのランタイムチェックの一部です。ginscon(optoas(OADD, types[tptr]), n->xoffset, &a)
を呼び出します。これは、n->xoffset
(大きなオフセット値)をa
が指すレジスタ(元のOINDREG
のベースレジスタ)に加算するアセンブリ命令を生成します。これにより、ベースレジスタに大きなオフセット値が加算され、目的のメモリアドレスが計算されます。- 最後に、元の
n->xoffset
を0にリセットします。これは、オフセット値がすでにベースレジスタに加算されたため、OINDREG
ノード自体はオフセットなしでレジスタを間接参照するようにするためです。
- もしオフセットが32ビットの範囲を超えている場合、現在の
このfixlargeoffset
関数は、src/cmd/6g/cgen.c
とsrc/cmd/6g/gsubr.c
内の、メモリ参照を生成する様々な箇所(例えば、構造体フィールドへのアクセス、配列要素へのアクセスなど)で呼び出されます。これにより、コンパイラがどのような状況で大きなオフセットを生成しようとしても、この関数が介入して適切なアセンブリコードに変換されるようになります。
この修正は、コンパイラのバックエンドが生成するアセンブリコードの正確性を保証し、Go言語が非常に大きなデータ構造を効率的かつ安全に扱えるようにするために不可欠です。
コアとなるコードの変更箇所
このコミットでは、主に以下の4つのファイルが変更されています。
-
src/cmd/6g/cgen.c
:igen
関数内で、ODOT
(構造体フィールドアクセス)やODOTPTR
(ポインタ経由の構造体フィールドアクセス)、OINDEX
(配列要素アクセス)などのノードを処理する際に、fixlargeoffset(a)
が追加されました。これにより、これらの操作で生成されるオフセットが大きすぎる場合に修正が適用されます。
-
src/cmd/6g/gg.h
:- 新しい関数
fixlargeoffset(Node *n)
のプロトタイプ宣言が追加されました。
- 新しい関数
-
src/cmd/6g/gsubr.c
:fixlargeoffset
関数の実装が追加されました。odcl
、odot
、oindex_const
、oindex_const_sudo
といった、メモリ参照を生成する可能性のある複数の関数内で、fixlargeoffset
が呼び出されるようになりました。これにより、様々なコード生成パスで大きなオフセットが適切に処理されるようになります。
-
test/fixedbugs/issue6036.go
:- このバグを再現し、修正が正しく機能することを確認するための新しいテストケースが追加されました。このテストケースには、
[1 << 31]byte
のような非常に大きな配列や、[1<<15 + 1][1<<15 + 1]int
のような多次元配列、[1<<29 + 1]S
のような大きな構造体の配列が含まれており、これらが正しくコンパイル・実行できることを検証します。
- このバグを再現し、修正が正しく機能することを確認するための新しいテストケースが追加されました。このテストケースには、
コアとなるコードの解説
src/cmd/6g/gsubr.c
における fixlargeoffset
関数
void
fixlargeoffset(Node *n)
{
Node a;
if(n == N)
return;
if(n->op != OINDREG)
return;
if(n->val.u.reg == D_SP) // stack offset cannot be large
return;
if(n->xoffset != (int32)n->xoffset) {
// offset too large, add to register instead.
a = *n;
a.op = OREGISTER;
a.type = types[tptr];
a.xoffset = 0;
cgen_checknil(&a);
ginscon(optoas(OADD, types[tptr]), n->xoffset, &a);
n->xoffset = 0;
}
}
この関数は、Node *n
で表される中間表現ノードが、大きすぎるオフセットを持つOINDREG
(間接レジスタ)操作である場合に、そのオフセットを修正します。
if(n == N) return;
: ノードがNULLの場合は何もしません。if(n->op != OINDREG) return;
: ノードの操作がOINDREG
でない場合も何もしません。この関数は、レジスタとオフセットによるメモリ参照に特化しています。if(n->val.u.reg == D_SP) return;
: ベースレジスタがスタックポインタ(D_SP
)の場合、通常スタックオフセットは32ビットの範囲を超えることはないため、処理をスキップします。これは最適化の一種です。if(n->xoffset != (int32)n->xoffset)
: ここがオフセットのサイズチェックの核心です。n->xoffset
はvlong
型(64ビット整数)であり、これをint32
にキャストした値と比較することで、n->xoffset
が32ビット符号付き整数の範囲に収まっているかどうかを判定します。もし両者が異なる場合、n->xoffset
は32ビットの範囲を超えていることになります。a = *n; a.op = OREGISTER; a.type = types[tptr]; a.xoffset = 0;
: 新しいNode a
を作成し、元のn
の情報をコピーします。そして、a
をレジスタ自体を表すノード(OREGISTER
)として設定し、型をポインタ型に、オフセットを0に設定します。これは、a
が元のOINDREG
ノードのベースレジスタを指すようにするためです。cgen_checknil(&a);
: ポインタがnilでないことを確認するランタイムチェックのコードを生成します。ginscon(optoas(OADD, types[tptr]), n->xoffset, &a);
: ここで、大きなオフセットをベースレジスタに加算するアセンブリ命令を生成します。ginscon
は定数をレジスタに加算する命令を生成する関数です。optoas(OADD, types[tptr])
は加算命令のオペコードを生成し、n->xoffset
が加算する定数(大きなオフセット)、&a
が加算対象のレジスタ(元のOINDREG
のベースレジスタ)です。n->xoffset = 0;
: オフセットがすでにベースレジスタに加算されたため、元のOINDREG
ノードのオフセットを0にリセットします。これにより、OINDREG
ノードはオフセットなしでレジスタを間接参照するようになります。
test/fixedbugs/issue6036.go
テストケース
このテストケースは、fixlargeoffset
関数が正しく機能することを検証するために設計されています。
// +build amd64
// compile
// Copyright 2013 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.
// Issue 6036: 6g's backend generates OINDREG with
// offsets larger than 32-bit.
package main
type T struct {
Large [1 << 31]byte // 2GBの巨大なバイト配列
A int
B int
}
func F(t *T) {
t.B = t.A // t.Bへのアクセスで大きなオフセットが発生する可能性
}
type T2 [1<<31 + 2]byte // 2GBを超えるバイト配列
func F2(t *T2) {
t[1<<31+1] = 42 // 2GBを超えるインデックスへのアクセス
}
type T3 [1<<15 + 1][1<<15 + 1]int // 非常に大きな2次元配列
func F3(t *T3) {
t[1<<15][1<<15] = 42 // 巨大なオフセットを持つ要素へのアクセス
}
type S struct {
A int32
B int32
}
type T4 [1<<29 + 1]S // 巨大な構造体の配列
func F4(t *T4) {
t[1<<29].B = 42 // 巨大なオフセットを持つ構造体要素へのアクセス
}
このテストケースは、以下のようなシナリオで大きなオフセットが生成されることを意図しています。
T
構造体内のLarge
フィールドは[1 << 31]byte
という非常に大きなサイズを持ちます。t.B
へのアクセスは、t
のベースアドレスからLarge
フィールドのサイズをスキップした後のオフセットを必要とします。このオフセットが32ビットの範囲を超える可能性があります。T2
配列は1<<31 + 2
という巨大なサイズを持ち、t[1<<31+1]
のようなインデックスへのアクセスは、32ビットの範囲を超えるオフセットを生成します。T3
は非常に大きな2次元配列であり、t[1<<15][1<<15]
のようなアクセスは、行と列のインデックスを組み合わせることで、巨大なオフセットを生成します。T4
は巨大な構造体の配列であり、t[1<<29].B
のようなアクセスは、配列のインデックスと構造体内のフィールドオフセットを組み合わせることで、巨大なオフセットを生成します。
これらのテストケースは、6g
コンパイラがこれらの巨大なオフセットを正しく処理し、有効なアセンブリコードを生成できることを保証します。もしfixlargeoffset
関数がなければ、これらのテストはコンパイルエラーになったり、実行時に不正なメモリアクセスを引き起こしたりするでしょう。
関連リンク
- Go Issue 6036: https://go.dev/issue/6036 (Goの公式Issueトラッカー)
- Go CL 12992043: https://golang.org/cl/12992043 (このコミットに対応するGoのコードレビュー)
参考にした情報源リンク
- googlesource.com (https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHVXVajMKYaK80Mg0UM-zveM04DyESh_3aOVjbfRvr-bPxEMarwAdIWZWPDzer59q_O-B4WmIJQrAd5P3n5dBHWnjkDTbTpvH7b2ymJN6osKVIMLNgnVzMfZuXOFeqL_CyHXAhD_yrEGMxLtVGMr2I0F-gQ_LAZtyzFgF00N4I2)
- Go言語のソースコード (特に
src/cmd/6g/
ディレクトリ内のファイル) - Go言語のコンパイラ設計に関する一般的な知識