[インデックス 17517] ファイルの概要
このコミットは、Goコンパイラ、リンカ、および関連ツールチェーンのC言語で書かれた部分における、様々な「未定義動作 (Undefined Behavior)」の使用を排除することを目的としています。特に、clang -fsanitize=undefined
(Undefined Behavior Sanitizer, UBSan) のような厳格な診断ツールによって報告された問題を修正しています。
コミット
commit 7d734d9252febfd91cb0ff5fc54f11defc5f4daa
Author: Russ Cox <rsc@golang.org>
Date: Mon Sep 9 15:07:23 2013 -0400
build: remove various uses of C undefined behavior
If you thought gcc -ansi -pedantic was pedantic, just wait
until you meet clang -fsanitize=undefined.
I think this addresses all the reported "errors", but we'll
need another run to be sure.
all.bash still passes.
Update #5764
Dave, can you please try again?
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/13334049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7d734d9252febfd91cb0ff5fc54f11defc5f4daa
元コミット内容
build: remove various uses of C undefined behavior
If you thought gcc -ansi -pedantic was pedantic, just wait
until you meet clang -fsanitize=undefined.
I think this addresses all the reported "errors", but we'll
need another run to be sure.
all.bash still passes.
Update #5764
Dave, can you please try again?
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/13334049
変更の背景
このコミットの主な背景は、C言語のコードベースにおける「未定義動作 (Undefined Behavior, UB)」の検出と修正です。コミットメッセージに明記されているように、clang -fsanitize=undefined
(UBSan) というツールが、従来のコンパイラの警告(例: gcc -ansi -pedantic
)では見過ごされがちだった、より厳密な未定義動作を報告し始めたことがきっかけです。
未定義動作は、C言語の仕様において、特定の操作の結果が規定されていない状態を指します。このような操作が行われた場合、プログラムの動作は予測不可能となり、クラッシュ、誤った計算結果、セキュリティ脆弱性など、様々な問題を引き起こす可能性があります。開発環境やコンパイラのバージョン、最適化レベルによって動作が変わるため、デバッグが非常に困難です。
Goのツールチェーン(特にコンパイラやリンカなど、C言語で書かれた部分)の安定性と信頼性を確保するためには、これらの未定義動作を特定し、修正することが不可欠でした。このコミットは、UBSanによって報告された「エラー」に対処し、コードベースの堅牢性を向上させることを目的としています。
前提知識の解説
C言語の未定義動作 (Undefined Behavior, UB)
C言語の標準は、特定の操作の結果を意図的に規定していません。これを未定義動作と呼びます。未定義動作が発生すると、コンパイラはどのようなコードを生成してもよく、プログラムは予期せぬ動作をする可能性があります。これは、コンパイラが未定義動作を「決して起こらない」ものとして最適化を行うためによく発生します。
このコミットで特に問題となっている未定義動作は、主に以下のカテゴリに分類されます。
- 符号付き整数のオーバーフロー (Signed Integer Overflow): 符号付き整数型で表現できる最大値を超えたり、最小値を下回ったりする演算の結果は未定義です。例えば、
int max_int = 2147483647; int result = max_int + 1;
のような場合です。 - 負の値を左シフトする (Left Shift of a Negative Value): 負の整数値を左にシフトする操作 (
-1 << 1
) は未定義です。 - シフト量が型のビット幅以上または負の値である (Shift Amount Exceeds Type Width or is Negative): シフト演算子 (
<<
,>>
) の右オペランドが、左オペランドの型のビット幅以上である場合(例:1 << 32
for a 32-bit int)や、負の値である場合は未定義です。 - 符号付き整数に対するビット演算の解釈 (Interpretation of Bitwise Operations on Signed Integers): 符号付き整数に対するビット演算は、符号拡張の挙動など、プラットフォームやコンパイラによって異なる解釈をされることがあり、意図しない結果を招くことがあります。
Undefined Behavior Sanitizer (UBSan)
clang -fsanitize=undefined
は、Clangコンパイラに組み込まれた動的解析ツールであるUndefined Behavior Sanitizer (UBSan) を有効にするフラグです。UBSanは、コンパイル時に特定の未定義動作を検出するためのコードを挿入し、実行時にそれらの動作が発生した場合に警告やエラーを報告します。これにより、開発者は未定義動作を早期に特定し、修正することができます。
C言語の整数型とリテラル
int
: 通常32ビットの符号付き整数。long long
: 少なくとも64ビットの符号付き整数。unsigned int
: 符号なし整数。unsigned long long
: 少なくとも64ビットの符号なし整数。1LL
:long long
型のリテラル。符号付き。1ULL
:unsigned long long
型のリテラル。符号なし。uint32
: 32ビットの符号なし整数型(通常、typedef unsigned int uint32;
のように定義される)。uvlong
: 符号なしvlong
型(通常、typedef unsigned long long uvlong;
のように定義される)。
ビットシフト演算において、左オペランドの型が符号付きである場合、結果がオーバーフローすると未定義動作になります。これを避けるためには、左オペランドを符号なし型にキャストするか、符号なしリテラルを使用することが推奨されます。
技術的詳細
このコミットで行われている修正は、主に以下のパターンに集約されます。
-
符号付きリテラル
1LL
から1ULL
への変更:1LL << (l->type->width*8)
のような式で、1LL
は符号付きのlong long
です。l->type->width*8
が63(64ビットシステムの場合)になると、1LL << 63
は符号ビットがセットされ、結果が負の値になります。これは符号付き整数のオーバーフローであり、未定義動作です。1ULL << (l->type->width*8)
に変更することで、1ULL
は符号なしのunsigned long long
となり、シフト演算は符号なしのコンテキストで行われます。これにより、結果がオーバーフローしても未定義動作にはならず、モジュロ演算(2の補数表現の最大値を超えると0に戻る)として扱われます。
-
int32
からuint32
への型変更:- ハッシュ計算やビットマスクの生成など、ビット演算が頻繁に行われる変数 (
h
,mask
) の型がint32
からuint32
に変更されています。 - 符号付き整数に対するビット演算は、符号拡張の挙動がコンパイラやプラットフォームによって異なる場合があり、意図しない結果を招く可能性があります。
uint32
を使用することで、これらの操作が常に符号なしのセマンティクスで実行されることが保証され、未定義動作や移植性の問題を回避します。 - 特に
1 << (i % WORDBITS)
のようなビットマスク生成では、1
がint
型であり、シフト量が大きい場合に符号付きオーバーフローの可能性があります。1U << ...
または(uint32)1 << ...
のようにすることで、1
を符号なしとして扱い、未定義動作を回避します。
- ハッシュ計算やビットマスクの生成など、ビット演算が頻繁に行われる変数 (
-
明示的な符号なしキャストの追加:
((uint32)(bp)->ebuf[(bp)->icount-1]<<24)
や(uvlong)n*16 + c
のように、ビットシフトのオペランドや算術演算の途中で、明示的にuint32
やuvlong
(unsigned vlong) へキャストしています。- これは、C言語の整数昇格規則によって、小さい整数型(例:
char
,short
)がint
に昇格される際に、その値が負であると符号拡張されてしまうことを防ぐためです。特に、バイト配列から多バイト整数を構築する際に、最上位バイトが負の値として解釈されると、意図しない結果になります。明示的な符号なしキャストにより、値が符号なしとして扱われ、ビットパターンが正しく保持されます。 - また、
-(uvlong)v
のように、負の値を扱う際に、まず符号なし型にキャストしてから否定することで、符号付き整数の最小値を否定する際の未定義動作を回避しています。符号付き整数の最小値(例: -2^31)は、その絶対値を符号付き整数で表現できないため、否定すると未定義動作になります。
-
Strlit
構造体の変更:src/cmd/gc/go.h
において、Strlit
構造体のchar s[3]
がchar s[1]
に変更されています。これは、C言語における「フレキシブル配列メンバ (Flexible Array Member, FAM)」のイディオムに合わせた変更である可能性が高いです。- FAMは、構造体の最後のメンバとして宣言され、配列のサイズが実行時に決定されることを示します。
char s[1]
は、実際には1バイトの配列ではなく、その後に可変長のデータが続くことをコンパイラに伝えるためのプレースホルダーとして機能します。これにより、malloc
などで構造体と文字列データを連続したメモリ領域に確保し、メモリ管理を効率化できます。この変更自体は直接的な未定義動作の修正というよりは、メモリレイアウトや配列アクセスの堅牢性向上に関連している可能性があります。
これらの変更は、C言語の厳密な規則に従い、コンパイラの最適化によって予期せぬ動作が発生するリスクを排除し、コードの移植性と信頼性を高めるためのものです。
コアとなるコードの変更箇所
このコミットは広範囲にわたるファイルに影響を与えていますが、特に重要な変更パターンは以下のファイルに見られます。
include/bio.h
: ビットシフト演算におけるuint32
キャストの追加。--- a/include/bio.h +++ b/include/bio.h @@ -79,7 +79,7 @@ struct Biobuf #define BGETLE2(bp)\ ((bp)->icount<=-2?((bp)->icount+=2,((bp)->ebuf[(bp)->icount-2])|((bp)->ebuf[(bp)->icount-1]<<8)):Bgetle2((bp)))\ #define BGETLE4(bp)\ - ((bp)->icount<=-4?((bp)->icount+=4,((bp)->ebuf[(bp)->icount-4])|((bp)->ebuf[(bp)->icount-3]<<8)|((bp)->ebuf[(bp)->icount-2]<<16)|((bp)->ebuf[(bp)->icount-1]<<24)):Bgetle4((bp)))\ + (int)((bp)->icount<=-4?((bp)->icount+=4,((bp)->ebuf[(bp)->icount-4])|((bp)->ebuf[(bp)->icount-3]<<8)|((bp)->ebuf[(bp)->icount-2]<<16)|((uint32)(bp)->ebuf[(bp)->icount-1]<<24)):Bgetle4((bp)))\
src/cmd/cc/com.c
:1LL
から1ULL
への変更。--- a/src/cmd/cc/com.c +++ b/src/cmd/cc/com.c @@ -1325,10 +1325,10 @@ compar(Node *n, int reverse)\ if(lt->width == 8)\ hi = big(0, ~0ULL);\ else\ - hi = big(0, (1LL<<(l->type->width*8))-1);\ + hi = big(0, (1ULL<<(l->type->width*8))-1);\ }else{\ - lo = big(~0ULL, -(1LL<<(l->type->width*8-1)));\ - hi = big(0, (1LL<<(l->type->width*8-1))-1);\ + lo = big(~0ULL, -(1ULL<<(l->type->width*8-1)));\ + hi = big(0, (1ULL<<(l->type->width*8-1))-1);\ }\
src/cmd/cc/lex.c
:n*16
の計算におけるuvlong
キャスト。--- a/src/cmd/cc/lex.c +++ b/src/cmd/cc/lex.c @@ -1019,7 +1019,7 @@ hex:\ tc += 10-'A';\ else\ goto bad;\ - nn = n*16 + c;\ + nn = (uvlong)n*16 + c;\ if(n < 0 && nn >= 0)\ goto bad;\ n = nn;\
src/cmd/cc/lexbody
:int32
からuint32
への型変更、およびyylval.lval
のシフト演算におけるuvlong
キャスト。--- a/src/cmd/cc/lexbody +++ b/src/cmd/cc/lexbody @@ -224,7 +224,7 @@ Sym* lookup(void) { Sym *s;\ - int32 h;\ + uint32 h;\ char *p;\ int c, l;\ char *r, *w;\ @@ -400,7 +400,7 @@ l1:\ if(c >= '0' && c <= '9') {\ if(c > '7' && c1 == 3)\ break;\ - yylval.lval <<= c1;\ + yylval.lval = (uvlong)yylval.lval << c1;\ yylval.lval += c - '0';\ c = GETC();\ continue;\ @@ -410,7 +410,7 @@ l1:\ if(c >= 'A' && c <= 'F')\ c += 'a' - 'A';\ if(c >= 'a' && c <= 'f') {\ - yylval.lval <<= c1;\ + yylval.lval = (uvlong)yylval.lval << c1;\ yylval.lval += c - 'a' + 10;\ c = GETC();\ continue;\ @@ -770,6 +770,6 @@ ieeedtod(Ieee *ieee, double native)\ f = 65536L;\ fr = modf(fr*f, &ho);\ ieee->l = ho;\ - ieee->l <<= 16;\ + ieee->l = (uint32)ieee->l << 16;\ ieee->l |= (int32)(fr*f);\ }\
src/cmd/gc/bv.c
: ビットマスク生成における1U
の使用。--- a/src/cmd/gc/bv.c +++ b/src/cmd/gc/bv.c @@ -41,7 +41,7 @@ bvset(Bvec *bv, int32 i)\ if(i < 0 || i >= bv->n)\ fatal("bvset: index %d is out of bounds with length %d\n", i, bv->n);\ - mask = 1 << (i % WORDBITS);\ + mask = 1U << (i % WORDBITS);\ bv->b[i / WORDBITS] |= mask;\ }\
src/cmd/gc/go.h
:Strlit
構造体のs
メンバのサイズ変更。--- a/src/cmd/gc/go.h +++ b/src/cmd/gc/go.h @@ -78,7 +78,7 @@ typedef struct Strlit Strlit; struct Strlit { int32 len;\ - char s[3]; // variable\ + char s[1]; // variable\ };
src/cmd/gc/md5.c
: バイト配列から整数を構築する際のuint32
キャスト。--- a/src/cmd/gc/md5.c +++ b/src/cmd/gc/md5.c @@ -196,7 +196,7 @@ md5block(MD5 *dig, uchar *p, int nn)\ for(i=0; i<16; i++) {\ j = i*4;\ - X[i] = p[j] | (p[j+1]<<8) | (p[j+2]<<16) | (p[j+3]<<24);\ + X[i] = p[j] | (p[j+1]<<8) | (p[j+2]<<16) | ((uint32)p[j+3]<<24);\ }\
src/cmd/gc/mparith2.c
:vlong
からuvlong
への変更、および負の値の否定におけるuvlong
キャスト。--- a/src/cmd/gc/mparith2.c +++ b/src/cmd/gc/mparith2.c @@ -565,11 +565,11 @@ mpgetfix(Mpint *a)\ return 0;\ }\ - v = (vlong)a->a[0];\ - v |= (vlong)a->a[1] << Mpscale;\ - v |= (vlong)a->a[2] << (Mpscale+Mpscale);\ + v = (uvlong)a->a[0];\ + v |= (uvlong)a->a[1] << Mpscale;\ + v |= (uvlong)a->a[2] << (Mpscale+Mpscale);\ if(a->neg)\ - v = -v;\ + v = -(uvlong)v;\ return v;\ }\ @@ -586,7 +586,7 @@ mpmovecfix(Mpint *a, vlong c)\ x = c;\ if(x < 0) {\ a->neg = 1;\ - x = -x;\ + x = -(uvlong)x;\ }\
src/cmd/gc/subr.c
: ハッシュ計算におけるint32
からuint32
への型変更、および負のハッシュ値の処理。--- a/src/cmd/gc/subr.c +++ b/src/cmd/gc/subr.c @@ -322,7 +322,7 @@ setlineno(Node *n)\ uint32 stringhash(char *p) { - int32 h;\ + uint32 h;\ int c;\ h = 0;\ @@ -333,9 +333,9 @@ stringhash(char *p)\ h = h*PRIME1 + c;\ }\ - if(h < 0) {\ + if((int32)h < 0) {\ h = -h;\ - if(h < 0)\ + if((int32)h < 0)\ h = 0;\ }\ return h;\
src/cmd/ld/go.c
: ハッシュ計算におけるint
からuint32
への型変更。--- a/src/cmd/ld/go.c +++ b/src/cmd/ld/go.c @@ -37,13 +37,12 @@ static void imported(char *pkg, char *import);\ static int hashstr(char *name) { - int h;\ + uint32 h;\ char *cp;\ h = 0;\ for(cp = name; *cp; h += *cp++)\ h *= 1119;\ - // not if(h < 0) h = ~h, because gcc 4.3 -O2 miscompiles it.\ h &= 0xffffff;\ return h;\ }\
src/cmd/ld/lib.c
: ハッシュ計算におけるint32
からuint32
への型変更、およびバイト配列から整数を構築する際のuint32
キャスト。--- a/src/cmd/ld/lib.c +++ b/src/cmd/ld/lib.c @@ -951,7 +951,7 @@ _lookup(char *symb, int v, int creat)\ { Sym *s;\ char *p;\ - int32 h;\ + uint32 h;\ int c;\ h = v;\ @@ -1613,7 +1613,7 @@ le16(uchar *b)\ uint32 le32(uchar *b) { - return b[0] | b[1]<<8 | b[2]<<16 | b[3]<<24;\ + return b[0] | b[1]<<8 | b[2]<<16 | (uint32)b[3]<<24;\ }\ uint64 @@ -1631,7 +1631,7 @@ be16(uchar *b)\ uint32 be32(uchar *b) { - return b[0]<<24 | b[1]<<16 | b[2]<<8 | b[3];\ + return (uint32)b[0]<<24 | b[1]<<16 | b[2]<<8 | b[3];\ }\
src/cmd/pack/ar.c
: ハッシュ計算におけるint
からuint32
への型変更、およびビットマスクの変更。--- a/src/cmd/pack/ar.c +++ b/src/cmd/pack/ar.c @@ -937,21 +937,12 @@ objsym(Sym *s, void *p)\ int hashstr(char *name) { - int h;\ + uint32 h;\ char *cp;\ h = 0;\ for(cp = name; *cp; h += *cp++)\ h *= 1119;\ - - // the code used to say - // if(h < 0) - // h = ~h; - // but on gcc 4.3 with -O2 on some systems, - // the if(h < 0) gets compiled away as not possible.\ - // use a mask instead, leaving plenty of bits but - // definitely not the sign bit.\ - return h & 0xfffffff;\ }\
src/libmach/5obj.c
,src/libmach/8obj.c
:BGETLE4(bp)
からBgetle4(bp)
への変更。これはマクロから関数呼び出しへの変更で、マクロ内で未定義動作を引き起こす可能性のある式評価を避けるためと考えられます。--- a/src/libmach/5obj.c +++ b/src/libmach/5obj.c @@ -130,7 +130,7 @@ addr(Biobuf *bp)\ BGETC(bp);\ break;\ case D_CONST2:\ - BGETLE4(bp); // fall through\ + Bgetle4(bp); // fall through\ case D_OREG:\ case D_CONST:\ case D_BRANCH:\
src/libmach/6obj.c
: 負の値の否定におけるuvlong
キャスト。--- a/src/libmach/6obj.c +++ b/src/libmach/6obj.c @@ -134,7 +134,7 @@ addr(Biobuf *bp)\ \toff = ((vlong)l << 32) | (off & 0xFFFFFFFF);\ }\ if(off < 0)\ - off = -off;\ + off = -(uvlong)off;\ }\
src/libmach/obj.c
: ハッシュ計算におけるint32
からuint32
への型変更。--- a/src/libmach/obj.c +++ b/src/libmach/obj.c @@ -244,7 +244,7 @@ processprog(Prog *p, int doautos)\ static void objlookup(int id, char *name, int type, uint sig) { - int32 h;\ + uint32 h;\ char *cp;\ Sym *s;\ Symtab *sp;\
コアとなるコードの解説
上記の変更箇所は、C言語の未定義動作を回避するための典型的なパターンを示しています。
1LL
から1ULL
: これは、ビットシフト演算の左オペランドが符号付きであることによるオーバーフローの未定義動作を回避するための最も直接的な方法です。1ULL
を使用することで、シフト演算が常に符号なしのコンテキストで行われ、結果が予測可能になります。int32
からuint32
: ハッシュ値やビットマスクなど、負の値を持つことが想定されない変数に対してuint32
を使用することで、符号拡張や符号付きオーバーフローのリスクを排除し、ビット演算のセマンティクスを明確にします。- 明示的なキャスト
(uint32)
や(uvlong)
: これは、C言語の整数昇格規則によって、意図せず符号付き整数に昇格されてしまうことを防ぐために重要です。特に、バイトデータを結合して多バイト整数を構築する際に、最上位バイトが負の値として解釈されることを防ぎ、正しいビットパターンを保証します。また、負の値を否定する際に-(uvlong)v
のようにすることで、符号付き整数の最小値を否定する際の未定義動作を回避します。 BGETLE4(bp)
からBgetle4(bp)
: これは、マクロの代わりに同名の関数を呼び出す変更です。マクロはプリプロセッサによって展開されるため、引数に副作用のある式が含まれている場合、意図しない動作を引き起こす可能性があります。関数呼び出しにすることで、引数の評価順序が保証され、より安全なコードになります。この場合、マクロ内の(bp)->icount
の複数回評価が問題となる可能性があったと考えられます。Strlit
構造体のchar s[1]
: これは、フレキシブル配列メンバのイディオムを採用したものです。これにより、構造体のインスタンスが動的に確保される際に、文字列データのためのメモリを構造体の直後に連続して確保できるようになり、メモリ効率とアクセス性能が向上します。これは直接的な未定義動作の修正ではありませんが、メモリ管理の堅牢性向上に寄与します。
これらの変更は、Goのツールチェーンが様々なプラットフォームやコンパイラ環境で安定して動作するために不可欠な、低レベルのC言語コードの堅牢性向上に貢献しています。
関連リンク
- Go Issue #5764: https://code.google.com/p/go/issues/detail?id=5764 (古いGo issue trackerのリンクですが、関連する議論がある可能性があります)
- Go Code Review 13334049: https://golang.org/cl/13334049 (このコミットのコードレビューページ)
参考にした情報源リンク
- C言語の未定義動作に関する一般的な情報源 (例: C Standard, Stack Overflow, 各種プログラミングブログ)
- Clang Undefined Behavior Sanitizer (UBSan) のドキュメント
- Go言語のソースコードリポジトリ (特に
src/cmd
およびsrc/libmach
ディレクトリ内のC言語コード) - C言語のビット演算と整数昇格に関する資料