[インデックス 17546] ファイルの概要
このコミットは、Goコンパイラおよびリンカのビルドシステムにおける「未定義動作」の修正を目的としています。具体的には、以下の4つのファイルが変更されています。
src/cmd/6g/gsubr.c
: Goコンパイラのx86-64アーキテクチャ向けコード生成ルーチン。src/cmd/6l/obj.c
: Goリンカのオブジェクトファイル処理ルーチン。src/cmd/6l/span.c
: Goリンカのコードスパン(命令の長さ計算)ルーチン。src/libbio/bgetc.c
: GoランタイムライブラリのバイナリI/Oルーチン。
コミット
commit 6034406eae500a10ed9bb4085559935cda275ec0
Author: Russ Cox <rsc@golang.org>
Date: Tue Sep 10 14:54:55 2013 -0400
build: more "undefined behavior" fixes
Fixes #5764.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/13441051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6034406eae500a10ed9bb4085559935cda275ec0
元コミット内容
build: more "undefined behavior" fixes
Fixes #5764.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/13441051
変更の背景
このコミットは、Goコンパイラとリンカにおける「未定義動作 (Undefined Behavior)」を修正することを目的としています。コミットメッセージに Fixes #5764
とあることから、GoのIssue 5764に関連する問題が修正されたことがわかります。
Issue 5764は、Goのツールチェインが生成するコードにおいて、C言語のコンパイラが未定義動作と見なす可能性のあるコードパターンが存在するという問題提起でした。特に、符号付き整数に対するビットシフト操作や、負の数に対するビット演算が問題視されていました。C言語の標準では、これらの操作が未定義動作を引き起こす場合があり、最適化コンパイラが予期せぬコードを生成する可能性があります。これにより、Goプログラムの動作が不安定になったり、異なるCコンパイラや最適化レベルでビルドした場合に動作が変わったりするリスクがありました。
このコミットは、Goのツールチェイン自体がC言語で書かれている部分があるため、そのCコードが未定義動作を引き起こさないように修正することで、Goコンパイラやリンカの堅牢性を高めることを目指しています。
前提知識の解説
未定義動作 (Undefined Behavior)
未定義動作とは、C言語やC++などのプログラミング言語において、言語仕様がその動作を規定していないコードの実行結果を指します。未定義動作が発生すると、プログラムは予測不能な振る舞いをします。例えば、クラッシュする、間違った結果を返す、セキュリティホールを生む、あるいはたまたま正しく動作するように見えるが、コンパイラのバージョンや最適化レベル、実行環境が変わると動作が変わる、といったことが起こりえます。
未定義動作の一般的な例としては以下のようなものがあります。
- 符号付き整数のオーバーフロー: 符号付き整数型で表現できる最大値を超えた演算を行った場合。
- NULLポインタのデリファレンス: NULLポインタが指すメモリにアクセスしようとした場合。
- 配列の範囲外アクセス: 配列の宣言された範囲を超えて要素にアクセスしようとした場合。
- 符号付き整数に対する特定のビットシフト操作: 負の数を右シフトしたり、シフト量が型のビット幅以上だったりする場合。
コンパイラは未定義動作を前提として最適化を行うため、未定義動作を含むコードは非常に危険です。コンパイラは、未定義動作が発生しないという仮定のもとで、コードを積極的に最適化し、その結果、プログラマが意図しないコードが生成されることがあります。
ビットシフト演算と符号付き/符号なし整数
コンピュータの内部では、数値はビット列として表現されます。ビットシフト演算は、このビット列を左右に移動させる操作です。
- 左シフト (
<<
): ビット列を左に指定された数だけ移動させます。右側には0が埋められます。通常、2のべき乗を掛ける操作として使われます。符号付き整数でオーバーフローが発生すると未定義動作になる可能性があります。 - 右シフト (
>>
): ビット列を右に指定された数だけ移動させます。- 論理右シフト: 左側には常に0が埋められます。
- 算術右シフト: 左側には最上位ビット(符号ビット)が複製されます。これにより、負の数の符号が保持されます。
C言語では、符号なし整数に対する右シフトは常に論理右シフトですが、符号付き整数に対する右シフトは実装定義(処理系によって異なる)であり、算術右シフトが一般的です。しかし、負の数を右シフトした場合の動作は未定義動作となる場合があります。
vlong
と uvlong
(Goの内部型)
GoのコンパイラやリンカのC言語コードでは、vlong
や uvlong
といった型が使われています。これらはそれぞれ long long
(64ビット符号付き整数) および unsigned long long
(64ビット符号なし整数) に対応するGo内部の型エイリアスです。
vlong
:int64
に相当する符号付き64ビット整数。uvlong
:uint64
に相当する符号なし64ビット整数。
これらの型を適切に使い分けることで、符号の扱いに関する未定義動作を回避することができます。
技術的詳細
このコミットでは、主に以下の3つの未定義動作のパターンが修正されています。
-
符号付き整数に対する負の定数のビットシフト:
src/cmd/6g/gsubr.c
の修正。c < -1LL<<31
という式は、1LL<<31
がlong long
型の2^31
を表し、その前に単項マイナス演算子が付いています。C言語の仕様では、負の定数に対するビットシフトは未定義動作となる場合があります。特に、1LL<<31
は2^31
であり、これは32ビット符号付き整数の最大値+1に相当します。この値を負にした-2^31
は、32ビット符号付き整数の最小値です。この式が-(1LL<<31)
に変更されたことで、1LL<<31
が先に計算され、その結果にマイナスが適用されるため、未定義動作を回避できます。これは、1LL<<31
がlong long
型であるため、2^31
がオーバーフローすることなく表現され、その後に負の符号が適用されるためです。 -
符号付き整数へのビットシフト結果の代入:
src/cmd/6l/obj.c
の修正。a->offset |= (vlong)BGETLE4(f) << 32;
という行は、BGETLE4(f)
が返す32ビット値をvlong
(符号付き64ビット整数) にキャストしてから32ビット左シフトしています。しかし、BGETLE4(f)
が返す値が負の場合、vlong
へのキャスト後に左シフトすると、符号ビットが拡張された状態でシフトされ、意図しない結果になる可能性があります。これを(uvlong)BGETLE4(f) << 32;
に変更することで、BGETLE4(f)
の結果をまずuvlong
(符号なし64ビット整数) にキャストし、その後に左シフトを行うことで、符号拡張の影響を受けずにビットパターンを正確にシフトし、上位32ビットに配置することができます。これにより、64ビットオフセットの構築が正しく行われます。 -
符号付き整数に対するビットシフト結果の結合:
src/libbio/bgetc.c
の修正。return l|(h<<16);
という行は、h
がint
型(通常32ビット符号付き)である場合、h<<16
の結果が符号付き32ビット整数の範囲を超えると未定義動作になる可能性があります。特に、h
の最上位ビットが1の場合、左シフトによって符号ビットが変化し、予期せぬ結果を招くことがあります。これをreturn l|((uint32)h<<16);
に変更することで、h
をuint32
(符号なし32ビット整数) にキャストしてから左シフトを行うことで、符号拡張やオーバーフローによる未定義動作を回避し、ビットパターンを正確に結合することができます。 -
リンカにおけるアセンブリ命令テーブルの範囲チェック:
src/cmd/6l/span.c
の修正。if(z >= nelem(o->op))
という行が追加されました。これは、アセンブリ命令のオペコードをルックアップする際に、配列の範囲外アクセスが発生しないようにするためのチェックです。nelem(o->op)
は配列o->op
の要素数を返すマクロです。z
がこの要素数以上になった場合、それは無効なオペコードが渡されたことを意味するため、sysfatal
を呼び出してプログラムを終了させます。これにより、リンカの堅牢性が向上し、不正な入力に対するクラッシュを防ぎます。
これらの修正は、GoのツールチェインがC言語で書かれている部分において、C言語の未定義動作の落とし穴を回避し、より堅牢で移植性の高いコードを生成するための重要なステップです。
コアとなるコードの変更箇所
src/cmd/6g/gsubr.c
--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -540,7 +540,7 @@ ginscon(int as, vlong c, Node *n2)
nodconst(&n1, types[TINT64], c);
- if(as != AMOVQ && (c < -1LL<<31 || c >= 1LL<<31)) {
+ if(as != AMOVQ && (c < -(1LL<<31) || c >= 1LL<<31)) {
// cannot have 64-bit immediokate in ADD, etc.
// instead, MOV into register first.
regalloc(&ntmp, types[TINT64], N);
src/cmd/6l/obj.c
--- a/src/cmd/6l/obj.c
+++ b/src/cmd/6l/obj.c
@@ -346,7 +346,7 @@ zaddr(char *pn, Biobuf *f, Adr *a, Sym *h[])
a->offset = BGETLE4(f);
if(t & T_64) {
a->offset &= 0xFFFFFFFFULL;
- a->offset |= (vlong)BGETLE4(f) << 32;
+ a->offset |= (uvlong)BGETLE4(f) << 32;
}
}
a->sym = S;
src/cmd/6l/span.c
--- a/src/cmd/6l/span.c
+++ b/src/cmd/6l/span.c
@@ -1237,6 +1237,8 @@ found:
break;
}
+ if(z >= nelem(o->op))
+ sysfatal("asmins bad table %P", p);
op = o->op[z];
if(op == 0x0f) {
*andptr++ = op;
src/libbio/bgetc.c
--- a/src/libbio/bgetc.c
+++ b/src/libbio/bgetc.c
@@ -83,7 +83,7 @@ Bgetle4(Biobuf *bp)
l = Bgetle2(bp);
h = Bgetle2(bp);
- return l|(h<<16);
+ return l|((uint32)h<<16);
}
int
コアとなるコードの解説
src/cmd/6g/gsubr.c
の修正
if(as != AMOVQ && (c < -1LL<<31 || c >= 1LL<<31))
から if(as != AMOVQ && (c < -(1LL<<31) || c >= 1LL<<31))
への変更は、C言語における負の定数に対するビットシフトの未定義動作を回避するためのものです。
- 元のコード
c < -1LL<<31
: ここで-1LL<<31
は、1LL
(64ビット符号付き整数1) を31ビット左シフトした結果に単項マイナス演算子を適用しています。1LL<<31
は0x80000000
(2^31) を表します。この値にマイナスを適用すると、0xFFFFFFFF80000000
となります。しかし、C言語の仕様では、負の定数に対するビットシフトは未定義動作となる場合があります。特に、1LL<<31
の結果がlong long
型の範囲内で表現可能であっても、その後に負の符号を適用する際に、コンパイラが予期せぬ最適化を行う可能性があります。 - 修正後のコード
c < -(1LL<<31)
: この変更により、まず(1LL<<31)
が計算され、その結果(0x80000000
)に対して単項マイナス演算子が適用されます。これにより、0xFFFFFFFF80000000
という値が安全に生成されます。この修正は、C言語のコンパイラが未定義動作を前提とした最適化を行うことを防ぎ、意図した通りの比較が行われるようにします。
src/cmd/6l/obj.c
の修正
a->offset |= (vlong)BGETLE4(f) << 32;
から a->offset |= (uvlong)BGETLE4(f) << 32;
への変更は、64ビットオフセットを構築する際に、符号拡張による問題を回避するためのものです。
BGETLE4(f)
は、ファイルから4バイト(32ビット)のリトルエンディアン整数を読み込む関数です。この値は符号付きである可能性があります。- 元のコード
(vlong)BGETLE4(f) << 32;
:BGETLE4(f)
の結果をvlong
(符号付き64ビット整数) にキャストしてから32ビット左シフトしています。もしBGETLE4(f)
が負の値(例えば0xFFFFFFFF
)を返した場合、vlong
にキャストされると符号拡張が行われ、0xFFFFFFFFFFFFFFFF
となります。これを32ビット左シフトすると、上位32ビットがすべて1で埋め尽くされ、意図しない大きな負の値になってしまいます。 - 修正後のコード
(uvlong)BGETLE4(f) << 32;
:BGETLE4(f)
の結果をuvlong
(符号なし64ビット整数) にキャストしてから32ビット左シフトしています。uvlong
にキャストすることで、BGETLE4(f)
が負の値であっても、そのビットパターンがそのまま符号なしとして解釈され、符号拡張は行われません。例えば0xFFFFFFFF
は0x00000000FFFFFFFF
となります。これを32ビット左シフトすると、0xFFFFFFFF00000000
となり、下位32ビットが0で埋められ、上位32ビットに元の値が正確に配置されます。これにより、64ビットオフセットが正しく構築されます。
src/cmd/6l/span.c
の修正
if(z >= nelem(o->op))
の追加は、リンカがアセンブリ命令のオペコードテーブルを検索する際の配列の範囲外アクセスを防ぐための防御的なプログラミングです。
z
は、アセンブリ命令のオペコードに対応するインデックスです。nelem(o->op)
は、配列o->op
の要素数を返します。- このチェックが追加されることで、もし
z
が有効なインデックスの範囲外になった場合(つまり、未知のまたは不正なオペコードが渡された場合)、sysfatal
関数が呼び出され、エラーメッセージとともにプログラムが終了します。これにより、リンカが不正なデータによってクラッシュしたり、予期せぬ動作をしたりするのを防ぎ、デバッグを容易にします。
src/libbio/bgetc.c
の修正
return l|(h<<16);
から return l|((uint32)h<<16);
への変更は、32ビット整数を結合する際に、符号付き整数に対するビットシフトの未定義動作を回避するためのものです。
l
とh
はそれぞれ16ビットの値を表すint
型の変数です。- 元のコード
l|(h<<16);
:h
を16ビット左シフトしています。もしh
がint
型で、その値が0x8000
以上(つまり、符号ビットが1になるような値)であった場合、h<<16
の結果が32ビット符号付き整数の範囲を超えると未定義動作になる可能性があります。C言語では、符号付き整数に対する左シフトでオーバーフローが発生した場合の動作は未定義です。 - 修正後のコード
l|((uint32)h<<16);
:h
をuint32
(符号なし32ビット整数) にキャストしてから16ビット左シフトしています。uint32
にキャストすることで、h
の値が符号なしとして扱われ、左シフトによるオーバーフローが定義された動作(ビットが切り捨てられる)になります。これにより、l
とh
のビットパターンが正確に結合され、正しい32ビット値が生成されます。
これらの修正は、GoのツールチェインのC言語部分における細かなビット操作や型変換の安全性を高め、潜在的な未定義動作を排除することで、より堅牢で信頼性の高いビルドシステムを構築することに貢献しています。
関連リンク
- Go Issue 5764: https://github.com/golang/go/issues/5764
- Gerrit Change-Id:
I13441051
(GoのGerritコードレビューシステムでの変更): https://golang.org/cl/13441051
参考にした情報源リンク
- C言語の未定義動作に関する一般的な情報源 (例: C Standard, Stack Overflow, 各種プログラミングブログ)
- Go言語のソースコードリポジトリ (GitHub)
- Go言語のIssueトラッカー (GitHub Issues)
- Go言語のGerritコードレビューシステム
- 符号付き/符号なし整数、ビットシフト演算に関するプログラミングの基礎知識