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

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

このコミットは、Go言語のコンパイラツールチェーンの一部である cmd/cc における定数評価時の符号なし整数(uint)の右シフトに関するバグ修正です。具体的には、uvlong (unsigned long long) 型へのキャストと、その後の右シフト演算の挙動が、特定の条件下で意図しない結果を生む可能性があった問題を解決しています。

コミット

commit 34ad3995e06fb4a4e209adfbd11367cc3d22f8b8
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Fri May 25 00:08:52 2012 +0800

    cmd/cc: fix uint right shift in constant evaluation
            Fixes #3664.
    
    R=golang-dev, bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/6249048

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

https://github.com/golang/go/commit/34ad3995e06fb4a4e209adfbd11367cc3d22f8b8

元コミット内容

cmd/cc: fix uint right shift in constant evaluation Fixes #3664.

このコミットは、Go言語のCコンパイラ(cmd/cc)における定数評価の際に発生する、符号なし整数(uint)の右シフト演算のバグを修正するものです。この修正は、内部のバグトラッカーで報告された問題 #3664 に対応しています。

変更の背景

Go言語のコンパイラツールチェーンには、Go言語自体で書かれたコードをコンパイルするためのCコンパイラ cmd/cc が含まれています。これは、Goコンパイラのブートストラップ(自己コンパイル)プロセスにおいて重要な役割を果たします。

定数評価は、コンパイル時にプログラム内の定数式を計算するプロセスです。これにより、実行時の計算を減らし、パフォーマンスを向上させることができます。しかし、この定数評価のロジックにバグがあると、コンパイルされたプログラムが誤った結果を生成したり、予期せぬ動作をしたりする可能性があります。

このコミットの背景にある問題は、符号なし整数(uint)の右シフト演算が、特定の条件下で正しく処理されていなかったことにあります。特に、uvlong (unsigned long long) 型へのキャストが絡む場合に、ビット幅の扱いが不適切であったと考えられます。C言語では、整数型のビット幅や符号の有無によって、シフト演算の挙動が異なるため、これらの細かな違いがバグの原因となることがあります。

このバグは、Goコンパイラが自身をコンパイルする際に、定数評価の段階で誤った結果を生成し、それが最終的なバイナリの誤動作につながる可能性があったため、修正が必要とされました。

前提知識の解説

1. cmd/cc とは

cmd/cc は、Go言語のソースコードをコンパイルするために使用されるCコンパイラです。Go言語のコンパイラ自体がGo言語で書かれているため、Goコンパイラをビルドする際には、まず既存のCコンパイラ(またはGoのブートストラップコンパイラ)を使用して、Goコンパイラの初期バージョンをコンパイルする必要があります。この初期バージョンが、その後のGoコンパイラの開発や自己コンパイルに使用されます。cmd/cc は、このブートストラッププロセスの一部として、Goランタイムや標準ライブラリの一部をコンパイルするために利用されます。

2. 定数評価 (Constant Evaluation)

定数評価とは、コンパイル時にプログラム内の定数式(例: 1 + 2, 10 << 2, 0xFF & 0x0F など)の値を計算するプロセスです。これにより、実行時にこれらの計算を行う必要がなくなり、プログラムの実行速度が向上します。コンパイラは、定数式をその計算結果で置き換えることで、最適化を行います。

3. 符号なし整数 (Unsigned Integers)

符号なし整数は、負の値を表現しない整数型です。すべてのビットが数値の大きさを表すため、同じビット幅の符号付き整数よりも大きな正の値を表現できます。C言語では、unsigned int, unsigned long, unsigned long long などがあります。Go言語では、uint, uint8, uint16, uint32, uint64, uintptr などがあります。

4. 右シフト演算 (Right Shift Operator, >>)

右シフト演算子 >> は、数値のビットを右に指定された回数だけ移動させます。

  • 符号なし整数 (unsigned integers) の場合: 右シフトでは、常に左側から0が埋められます(論理シフト)。 例: (unsigned int)8 >> 1 (バイナリ 1000 を右に1ビットシフト) は 4 (バイナリ 0100) になります。

  • 符号付き整数 (signed integers) の場合: 右シフトの挙動は、実装定義(または処理系依存)です。多くの場合、最上位ビット(符号ビット)が複製されて埋められます(算術シフト)。これにより、負の数の符号が保持されます。 例: (-8) >> 1 (バイナリ ...11111000 を右に1ビットシフト) は -4 (バイナリ ...11111100) になります。

このコミットでは、符号なし整数に対する右シフトが問題となっており、特にビット幅の異なる型へのキャストが絡む場合に、期待通りの結果が得られない可能性がありました。

5. uvlong

uvlong は、Goコンパイラの内部で使われる型で、通常は unsigned long long に対応します。これは、C言語における64ビットの符号なし整数型です。

技術的詳細

このバグは、cmd/cc の定数評価ロジック、特に evconst 関数内で、符号なし整数(uint)の右シフト演算 OLSHR (Opcode Left SHift Right) を処理する際に発生していました。

元のコードでは、右シフト演算の左オペランド l の値が uvlong 型にキャストされ、その後右オペランド r の値だけ右シフトされていました。

// 変更前
case OLSHR:
    v = (uvlong)l->vconst >> r->vconst;
    break;

ここで問題となるのは、l->vconstuvlong よりも小さいビット幅の型(例えば uintunsigned int、32ビット幅)である場合です。C言語では、整数昇格規則により、シフト演算の前にオペランドが int または unsigned int に昇格されることがあります。しかし、ここでは明示的に (uvlong) にキャストされています。

もし l->vconst が32ビットの符号なし整数で、その値が 0xFFFFFFFF のような最大値に近い場合、uvlong にキャストされると、その値は正しく64ビットの uvlong に拡張されます。しかし、その後の右シフト演算で、元の32ビットの範囲を超えたビットが考慮されるべきかどうかが問題になります。

このコミットの修正は、l->type->width (左オペランドの型のビット幅) が sizeof(uvlong) (通常64ビット) と異なる場合、つまり左オペランドが uvlong よりも小さいビット幅の型である場合に、追加のマスク処理 & 0xffffffffULL を導入しています。

// 変更後
case OLSHR:
    if(l->type->width != sizeof(uvlong))
        v = ((uvlong)l->vconst & 0xffffffffULL) >> r->vconst;
    else
        v = (uvlong)l->vconst >> r->vconst;
    break;

この & 0xffffffffULL マスクは、l->vconstuvlong にキャストした後、その値の下位32ビットのみを保持することを保証します。これは、元の値が32ビットの符号なし整数であった場合、その値が64ビットの uvlong に拡張されたとしても、シフト演算の際には元の32ビットの範囲内での挙動を期待していることを示唆しています。

例えば、32ビットの unsigned int0x80000000 (最上位ビットが1) を右シフトする場合を考えます。 もしこれが uvlong にキャストされると 0x0000000080000000ULL となります。 この値をそのまま右シフトすると、64ビットの範囲でシフトが行われます。 しかし、もし元の意図が32ビットの範囲でのシフトであった場合、この挙動は異なります。

この修正は、左オペランドが uvlong よりも小さい型である場合に、その値が uvlong に昇格された後も、元の型のビット幅のセマンティクスを維持して右シフトを行うためのものです。具体的には、元の型が32ビットの場合、0xffffffffULL でマスクすることで、上位32ビットが意図せずシフト演算に影響を与えることを防ぎ、32ビットの符号なし整数としての右シフトの挙動をエミュレートしています。

これにより、定数評価における符号なし整数の右シフトが、元の型のビット幅を正しく考慮して行われるようになり、バグが修正されました。

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

変更は src/cmd/cc/scon.c ファイルの evconst 関数内、OLSHR (右シフト) のケースで行われています。

--- a/src/cmd/cc/scon.c
+++ b/src/cmd/cc/scon.c
@@ -175,7 +175,10 @@ evconst(Node *n)
 		break;
 
 	case OLSHR:
-		v = (uvlong)l->vconst >> r->vconst;
+		if(l->type->width != sizeof(uvlong))
+			v = ((uvlong)l->vconst & 0xffffffffULL) >> r->vconst;
+		else
+			v = (uvlong)l->vconst >> r->vconst;
 		break;
 
 	case OASHR:

コアとなるコードの解説

evconst 関数は、GoコンパイラのCコンパイラ部分 (cmd/cc) における定数評価ロジックを担っています。この関数は、抽象構文木 (AST) のノード n を受け取り、そのノードが表す定数式の値を計算します。

変更された OLSHR のケースは、右シフト演算子 >> の処理を担当しています。

  • l は左オペランドのノード、r は右オペランドのノードを表します。
  • l->vconstr->vconst は、それぞれ左オペランドと右オペランドの定数値です。
  • v は計算結果を格納する uvlong 型の変数です。

変更前のコード: v = (uvlong)l->vconst >> r->vconst; この行は、左オペランドの定数値を uvlong (64ビット符号なし整数) にキャストし、その結果を右オペランドの定数値だけ右シフトしていました。問題は、l->vconst が元々32ビットの符号なし整数であった場合、64ビットに拡張された後にシフト演算が行われると、期待される32ビットの挙動と異なる結果になる可能性があったことです。

変更後のコード:

if(l->type->width != sizeof(uvlong))
    v = ((uvlong)l->vconst & 0xffffffffULL) >> r->vconst;
else
    v = (uvlong)l->vconst >> r->vconst;

この修正では、条件分岐が追加されています。

  1. if(l->type->width != sizeof(uvlong)) この条件は、左オペランドの型 (l->type) のビット幅が、uvlong のビット幅(通常64ビット)と異なるかどうかをチェックします。つまり、左オペランドが uvlong よりも小さいビット幅の型(例: 32ビットの unsigned int)である場合に、この if ブロックが実行されます。

  2. v = ((uvlong)l->vconst & 0xffffffffULL) >> r->vconst; この行は、左オペランドが uvlong よりも小さいビット幅の型である場合に実行されます。

    • (uvlong)l->vconst: まず、l->vconstuvlong にキャストします。これにより、値は64ビットに拡張されます。
    • & 0xffffffffULL: 次に、0xffffffffULL (64ビットの 0x00000000FFFFFFFF) とビットAND演算を行います。このマスクにより、l->vconst が元々32ビットの符号なし整数であった場合、64ビットに拡張された値の上位32ビットがクリアされ、下位32ビットのみが保持されます。これにより、元の32ビットの範囲での値が保証されます。
    • >> r->vconst: マスクされた値を右オペランドの定数値だけ右シフトします。この結果、32ビットの符号なし整数としての右シフトのセマンティクスが、64ビットの uvlong 型上で正しくエミュレートされます。
  3. else v = (uvlong)l->vconst >> r->vconst; この else ブロックは、左オペランドの型が既に uvlong と同じビット幅である場合(つまり、元々64ビットの符号なし整数である場合)に実行されます。この場合、追加のマスクは不要であり、元のコードと同じように直接 uvlong にキャストして右シフトを行います。

この修正により、cmd/cc の定数評価において、異なるビット幅の符号なし整数に対する右シフト演算が、C言語の標準的なセマンティクスに従って正しく行われるようになりました。

関連リンク

参考にした情報源リンク

  • C言語の整数型とビット演算に関する一般的な情報源 (例: C言語の教科書、オンラインリファレンス)
  • Go言語のコンパイラ設計に関する資料 (もしあれば)
  • Go言語の内部バグトラッカー (公開されていないため、直接参照はできませんが、Fixes #3664 からその存在が示唆されます)