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

[インデックス 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関数の主なロジックは以下の通りです。

  1. ノードのチェック: 引数として渡されたNodeOINDREG(間接レジスタ)操作を表すものであるかを確認します。OINDREGでない場合は、この関数は何もせずに関数を終了します。
  2. スタックオフセットの除外: n->val.u.reg == D_SP(スタックポインタSPをベースレジスタとするオフセット)の場合、スタックオフセットは通常、そこまで巨大になることはないため、処理をスキップします。これは、スタックフレームのサイズが通常、32ビットオフセットの範囲内に収まるという仮定に基づいています。
  3. オフセットのサイズチェック: n->xoffset != (int32)n->xoffsetという条件で、現在のオフセット値n->xoffsetが32ビット符号付き整数の範囲に収まっているかを確認します。xoffsetvlong(64ビット整数)型であるため、int32にキャストした値と比較することで、オーバーフローが発生しているか(つまり、オフセットが32ビットの範囲を超えているか)を検出できます。
  4. オフセットの変換:
    • もしオフセットが32ビットの範囲を超えている場合、現在のOINDREGノードnの情報を一時的なNode aにコピーします。
    • a.opOREGISTERに設定し、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ノード自体はオフセットなしでレジスタを間接参照するようにするためです。

このfixlargeoffset関数は、src/cmd/6g/cgen.csrc/cmd/6g/gsubr.c内の、メモリ参照を生成する様々な箇所(例えば、構造体フィールドへのアクセス、配列要素へのアクセスなど)で呼び出されます。これにより、コンパイラがどのような状況で大きなオフセットを生成しようとしても、この関数が介入して適切なアセンブリコードに変換されるようになります。

この修正は、コンパイラのバックエンドが生成するアセンブリコードの正確性を保証し、Go言語が非常に大きなデータ構造を効率的かつ安全に扱えるようにするために不可欠です。

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

このコミットでは、主に以下の4つのファイルが変更されています。

  1. src/cmd/6g/cgen.c:

    • igen関数内で、ODOT(構造体フィールドアクセス)やODOTPTR(ポインタ経由の構造体フィールドアクセス)、OINDEX(配列要素アクセス)などのノードを処理する際に、fixlargeoffset(a)が追加されました。これにより、これらの操作で生成されるオフセットが大きすぎる場合に修正が適用されます。
  2. src/cmd/6g/gg.h:

    • 新しい関数fixlargeoffset(Node *n)のプロトタイプ宣言が追加されました。
  3. src/cmd/6g/gsubr.c:

    • fixlargeoffset関数の実装が追加されました。
    • odclodotoindex_constoindex_const_sudoといった、メモリ参照を生成する可能性のある複数の関数内で、fixlargeoffsetが呼び出されるようになりました。これにより、様々なコード生成パスで大きなオフセットが適切に処理されるようになります。
  4. 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->xoffsetvlong型(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関数がなければ、これらのテストはコンパイルエラーになったり、実行時に不正なメモリアクセスを引き起こしたりするでしょう。

関連リンク

参考にした情報源リンク