[インデックス 18069] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)およびリンカ(liblink
)のソースコードにおけるビットシフト操作に関連する複数の警告を解決することを目的としています。具体的には、clang -fsanitize=undefined
というコンパイラオプションによって検出される、符号付き整数のビットシフトにおける未定義動作(Undefined Behavior: UB)の可能性に対処しています。
コミット
commit 3f6dbfc44ca7b5ed6e01f82b8833e626227320d9
Author: Dave Cheney <dave@cheney.net>
Date: Thu Dec 19 10:34:33 2013 +1100
liblink, cmd/gc: resolve several shift warnings
Address several warnings generated by clang -fsanitize=undefined
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/43050043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3f6dbfc44ca7b5ed6e01f82b8833e626227320d9
元コミット内容
liblink, cmd/gc: resolve several shift warnings
Address several warnings generated by clang -fsanitize=undefined
このコミットは、liblink
(Goリンカのライブラリ)とcmd/gc
(Goコンパイラのバックエンドの一部)において、ビットシフト操作に関する複数の警告を解決します。これらの警告は、clang
コンパイラの-fsanitize=undefined
オプションによって生成されたもので、コード内の未定義動作の可能性を指摘しています。
変更の背景
Go言語のツールチェイン、特にコンパイラとリンカは、C言語で実装されています。C言語では、ビットシフト操作、特に符号付き整数に対する左シフトは、特定の条件下で「未定義動作(Undefined Behavior: UB)」を引き起こす可能性があります。未定義動作とは、C標準がその動作を規定していない状況を指し、プログラムのクラッシュ、予期せぬ結果、セキュリティ脆弱性など、あらゆる結果をもたらす可能性があります。
clang
コンパイラの-fsanitize=undefined
オプションは、このような未定義動作をランタイムで検出するための強力なツールです。このオプションを有効にしてGoツールチェインをビルド・テストした際に、既存のコードベースでビットシフトに関する警告が多数報告されました。これらの警告は、Goツールチェインの堅牢性と移植性を確保するために解決する必要がありました。
具体的には、符号付き整数をその型のビット幅の分だけ左シフトしたり、負の数を左シフトしたり、符号付き整数の最上位ビット(符号ビット)に1をシフトしたりする操作は、C言語の標準で未定義動作とされています。このコミットは、これらの潜在的な問題を修正し、コードの安全性を高めることを目的としています。
前提知識の解説
未定義動作 (Undefined Behavior: UB)
C言語において、未定義動作とは、特定の操作の結果がC標準によって規定されていない状態を指します。これは、コンパイラがその状況に遭遇した際に、どのようなコードを生成してもよいことを意味します。結果として、プログラムはクラッシュしたり、間違った結果を生成したり、あるいは一見正しく動作しているように見えても、異なるコンパイラや最適化レベル、実行環境では異なる動作をする可能性があります。ビットシフトにおけるUBの例としては、以下のようなものがあります。
- 符号付き整数を負の数だけシフトする、または型のビット幅以上の数だけシフトする:
int x = 1; x << -1;
やx << 32;
(32ビット整数型の場合) - 符号付き整数を左シフトした結果が、その型の表現範囲を超過する: 例えば、
int
型の最大値の半分より大きい値を1ビット左シフトするとオーバーフローし、UBとなります。特に、符号ビットに1がシフトされるようなケースです。
ビットシフト演算子 (<<
, >>
)
- 左シフト (
<<
): オペランドのビットを左に指定された数だけ移動させます。右側には0が埋められます。- 符号なし整数:
x << n
はx * 2^n
と同等です。 - 符号付き整数: 正の数に対する左シフトは通常
x * 2^n
と同等ですが、結果が型の表現範囲を超えると未定義動作になります。負の数に対する左シフトは常に未定義動作です。
- 符号なし整数:
- 右シフト (
>>
): オペランドのビットを右に指定された数だけ移動させます。- 符号なし整数: 左側には0が埋められます(論理シフト)。
- 符号付き整数: 左側に符号ビットが埋められるか(算術シフト)、0が埋められるか(論理シフト)は実装定義です。通常は算術シフトが行われます。
clang -fsanitize=undefined
clang
はLLVMプロジェクトの一部であるC/C++/Objective-Cコンパイラです。-fsanitize=undefined
オプションは、コンパイル時にコードにインストルメンテーション(計測コードの挿入)を追加し、実行時に未定義動作を検出するとエラーを報告するようにします。これにより、開発者は潜在的なバグや移植性の問題を早期に発見できます。このコミットは、このサニタイザーによって報告された警告に対処しています。
Goツールチェインの構造
cmd/gc
: Goコンパイラの主要部分であり、Goのソースコードを中間表現に変換し、最終的にアセンブリコードを生成する役割を担います。liblink
: Goリンカのコアライブラリであり、コンパイラによって生成されたオブジェクトファイルを結合し、実行可能ファイルを生成します。
これらのコンポーネントはC言語で書かれており、C言語のビットシフト規則に厳密に従う必要があります。
技術的詳細
このコミットの技術的な核心は、C言語における符号付き整数と符号なし整数のビットシフト動作の違いを理解し、未定義動作を回避するために明示的な型キャストを適用することにあります。
C言語の標準では、符号付き整数を左シフトする際に、結果が元の型の表現範囲を超えた場合(特に符号ビットが変更される場合)は未定義動作と規定されています。一方、符号なし整数に対するビットシフトは、オーバーフローが発生しても未定義動作にはならず、モジュロ演算(2の補数表現の最大値+1で割った余り)として定義されます。
このコミットでは、以下の原則に基づいて修正が行われています。
- 左シフトの前に符号なし型にキャスト: 符号付き整数に対して左シフトを行う前に、その値を
uint32
やuint64
のような対応する符号なし整数型に明示的にキャストします。これにより、シフト操作が符号なしのセマンティクスで行われ、未定義動作が回避されます。 - リテラルの型指定: 数値リテラル(例:
1
)を左シフトする場合、デフォルトではint
型と解釈されるため、1U
のようにU
サフィックスを付けてunsigned int
として扱うことで、シフト操作が符号なしとして行われるようにします。
これらの変更は、コードの論理的な意味を変えることなく、C言語の標準に準拠し、clang -fsanitize=undefined
のようなツールによる警告を解消することを目的としています。これにより、Goツールチェインの堅牢性と、異なるプラットフォームやコンパイラでの移植性が向上します。
コアとなるコードの変更箇所
このコミットでは、以下の3つのファイルが変更されています。
-
src/cmd/gc/bv.c
:mask = 1 << (i % WORDBITS);
- ↓
mask = 1U << (i % WORDBITS);
1
というリテラルをunsigned int
(1U
) として扱うことで、左シフト操作が符号なしのセマンティクスで行われるように変更。WORDBITS
は通常、int
型が何ビットであるかを示す定数(例: 32または64)であり、i % WORDBITS
の結果が0になる場合、1 << 0
は問題ないが、1 << 31
(32ビットシステムの場合) のように符号ビットに1がシフトされるようなケースで未定義動作を回避します。 -
src/liblink/objfile.c
:uv = (uint64)(sval<<1) ^ (uint64)(int64)(sval>>63);
- ↓
uv = ((uint64)sval<<1) ^ (uint64)(int64)(sval>>63);
sval
をuint64
にキャストしてから左シフトを行うように変更。これにより、sval
がint64
の最大値に近い場合でも、sval<<1
がint64
の範囲でオーバーフローして未定義動作になることを防ぎます。return (int64)(uv>>1) ^ ((int64)uv<<63>>63);
- ↓
return (int64)(uv>>1) ^ ((int64)((uint64)uv<<63)>>63);
uv
をuint64
にキャストしてから左シフトを行うように変更。これは、uv
がuint64
型であるにもかかわらず、その値をint64
にキャストしてから左シフトを行うと、int64
の範囲で未定義動作を引き起こす可能性があるためです。((uint64)uv<<63)
とすることで、シフト操作が符号なしのuint64
で行われ、その結果をint64
にキャストすることで、意図した符号拡張(または符号の抽出)が安全に行われるようにします。 -
src/liblink/pcln.c
:v |= (*p & 0x7F) << shift;
- ↓
v |= (uint32)(*p & 0x7F) << shift;
(*p & 0x7F)
の結果をuint32
にキャストしてから左シフトを行うように変更。*p
はuchar
(通常はunsigned char
)ですが、& 0x7F
の結果はデフォルトの整数昇格規則によりint
型になる可能性があります。shift
の値が大きくなり、int
型の符号ビットに影響を与えるようなシフトが発生すると未定義動作になるため、明示的にuint32
にキャストすることでこれを回避します。
コアとなるコードの解説
これらの変更は、C言語のビットシフト操作における「未定義動作」の回避という共通のテーマを持っています。
-
src/cmd/gc/bv.c
の変更:bvget
関数はビットベクトルから特定のビットの値を取得するものです。mask = 1 << (i % WORDBITS);
の行は、指定されたビット位置i
に対応するマスクを生成しています。WORDBITS
は通常、int
型が何ビットであるかを示す定数(例: 32または64)です。i % WORDBITS
の結果は0からWORDBITS-1
の範囲になります。もしWORDBITS
が32でi % WORDBITS
が31の場合、1 << 31
は32ビット符号付き整数で最上位ビット(符号ビット)に1をシフトすることになり、これは未定義動作です。1U
とすることで、1
が符号なし整数として扱われ、シフト操作も符号なしのセマンティクスで行われるため、この未定義動作が回避されます。 -
src/liblink/objfile.c
の変更:wrint
関数とrdint
関数は、可変長整数(varint)のエンコード/デコードに関連しています。Goのバイナリフォーマット(オブジェクトファイルや実行可能ファイル)では、サイズやオフセットなどの数値を効率的に格納するためにvarintが使用されます。uv = (uint64)(sval<<1) ^ (uint64)(int64)(sval>>63);
の行は、符号付き整数sval
を符号なしのvarint形式に変換する処理の一部です。sval<<1
はsval
を2倍する操作ですが、sval
がint64
の最大値の半分より大きい場合、int64
の範囲でオーバーフローし、未定義動作となります。((uint64)sval<<1)
とすることで、sval
がまずuint64
に昇格され、その上でシフトが行われるため、オーバーフローが符号なしのセマンティクスで処理され、未定義動作が回避されます。return (int64)(uv>>1) ^ ((int64)uv<<63>>63);
の行は、符号なしのvarint形式uv
を元の符号付き整数int64
にデコードする処理の一部です。((int64)uv<<63)
の部分は、uv
の最上位ビット(元のsval
の符号ビット)を抽出するために使用されます。ここでも、uv
をint64
にキャストしてから左シフトを行うと、int64
の範囲で未定義動作を引き起こす可能性があります。((int64)((uint64)uv<<63))
とすることで、シフト操作が安全なuint64
で行われ、その結果がint64
にキャストされることで、意図した符号ビットの抽出が確実に行われます。 -
src/liblink/pcln.c
の変更:getvarint
関数は、プログラムカウンタと行番号の情報を格納するテーブル(pcln
テーブル)から可変長整数を読み出すものです。v |= (*p & 0x7F) << shift;
の行は、varintの各バイトから7ビットのデータを抽出し、v
に結合していく処理です。(*p & 0x7F)
の結果は、C言語のデフォルトの整数昇格規則によりint
型になる可能性があります。shift
の値は0, 7, 14, ... と増加していきますが、shift
が31(32ビットシステムの場合)や63(64ビットシステムの場合)に達すると、int
型(またはlong
型)の符号ビットに影響を与えるようなシフトが発生し、未定義動作となる可能性があります。(uint32)(*p & 0x7F)
とすることで、シフト操作が符号なしのuint32
で行われるため、この未定義動作が回避されます。
これらの修正は、Goツールチェインの低レベルなC言語コードにおけるビット操作の安全性を高め、異なるコンパイラやプラットフォームでのビルド・実行時の堅牢性を向上させる上で重要な意味を持ちます。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go Code Review Comments (CL 43050043): https://golang.org/cl/43050043 (これは古いGo Gerritのリンクであり、現在はGitHubのコミットページにリダイレクトされます)
- Clang
UndefinedBehaviorSanitizer
(UBSan) のドキュメント: https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
参考にした情報源リンク
- C++ Standard (ISO/IEC 14882:2011) - Section 5.8 Shift operators (C言語のビットシフト規則も同様の原則に基づいています)
- Undefined Behavior in C: https://en.wikipedia.org/wiki/Undefined_behavior
- Bitwise operations in C: https://en.wikipedia.org/wiki/Bitwise_operations_in_C
- Dave Cheneyのブログ (Go言語に関する多くの技術記事を執筆): https://dave.cheney.net/ (このコミットの作者)
- Go言語のコンパイラとリンカの内部構造に関する資料 (一般的な情報源):
- "Go Programming Language" by Alan A. A. Donovan and Brian W. Kernighan
- Goのソースコード自体 (
src/cmd/gc
,src/liblink
ディレクトリ) - Goのコンパイラに関するブログ記事やカンファレンス発表