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

[インデックス 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言語の標準は、特定の操作の結果を意図的に規定していません。これを未定義動作と呼びます。未定義動作が発生すると、コンパイラはどのようなコードを生成してもよく、プログラムは予期せぬ動作をする可能性があります。これは、コンパイラが未定義動作を「決して起こらない」ものとして最適化を行うためによく発生します。

このコミットで特に問題となっている未定義動作は、主に以下のカテゴリに分類されます。

  1. 符号付き整数のオーバーフロー (Signed Integer Overflow): 符号付き整数型で表現できる最大値を超えたり、最小値を下回ったりする演算の結果は未定義です。例えば、int max_int = 2147483647; int result = max_int + 1; のような場合です。
  2. 負の値を左シフトする (Left Shift of a Negative Value): 負の整数値を左にシフトする操作 (-1 << 1) は未定義です。
  3. シフト量が型のビット幅以上または負の値である (Shift Amount Exceeds Type Width or is Negative): シフト演算子 (<<, >>) の右オペランドが、左オペランドの型のビット幅以上である場合(例: 1 << 32 for a 32-bit int)や、負の値である場合は未定義です。
  4. 符号付き整数に対するビット演算の解釈 (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; のように定義される)。

ビットシフト演算において、左オペランドの型が符号付きである場合、結果がオーバーフローすると未定義動作になります。これを避けるためには、左オペランドを符号なし型にキャストするか、符号なしリテラルを使用することが推奨されます。

技術的詳細

このコミットで行われている修正は、主に以下のパターンに集約されます。

  1. 符号付きリテラル 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に戻る)として扱われます。
  2. int32 から uint32 への型変更:

    • ハッシュ計算やビットマスクの生成など、ビット演算が頻繁に行われる変数 (h, mask) の型が int32 から uint32 に変更されています。
    • 符号付き整数に対するビット演算は、符号拡張の挙動がコンパイラやプラットフォームによって異なる場合があり、意図しない結果を招く可能性があります。uint32 を使用することで、これらの操作が常に符号なしのセマンティクスで実行されることが保証され、未定義動作や移植性の問題を回避します。
    • 特に 1 << (i % WORDBITS) のようなビットマスク生成では、1int 型であり、シフト量が大きい場合に符号付きオーバーフローの可能性があります。1U << ... または (uint32)1 << ... のようにすることで、1 を符号なしとして扱い、未定義動作を回避します。
  3. 明示的な符号なしキャストの追加:

    • ((uint32)(bp)->ebuf[(bp)->icount-1]<<24)(uvlong)n*16 + c のように、ビットシフトのオペランドや算術演算の途中で、明示的に uint32uvlong (unsigned vlong) へキャストしています。
    • これは、C言語の整数昇格規則によって、小さい整数型(例: char, short)が int に昇格される際に、その値が負であると符号拡張されてしまうことを防ぐためです。特に、バイト配列から多バイト整数を構築する際に、最上位バイトが負の値として解釈されると、意図しない結果になります。明示的な符号なしキャストにより、値が符号なしとして扱われ、ビットパターンが正しく保持されます。
    • また、-(uvlong)v のように、負の値を扱う際に、まず符号なし型にキャストしてから否定することで、符号付き整数の最小値を否定する際の未定義動作を回避しています。符号付き整数の最小値(例: -2^31)は、その絶対値を符号付き整数で表現できないため、否定すると未定義動作になります。
  4. 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言語コードの堅牢性向上に貢献しています。

関連リンク

参考にした情報源リンク

  • C言語の未定義動作に関する一般的な情報源 (例: C Standard, Stack Overflow, 各種プログラミングブログ)
  • Clang Undefined Behavior Sanitizer (UBSan) のドキュメント
  • Go言語のソースコードリポジトリ (特に src/cmd および src/libmach ディレクトリ内のC言語コード)
  • C言語のビット演算と整数昇格に関する資料