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

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

コミット

このコミットは、Dave Cheneyによって2013年9月17日に行われました。Goコンパイラおよびアセンブラツールチェーンの一部であるcmd/6c (Cコンパイラ), cmd/6g (Goコンパイラ), cmd/cc (Cコンパイラ)における未定義動作の警告を修正することを目的としています。具体的には、ビットシフト操作における符号付き整数と符号なし整数の扱い、および特定の定数表現に関する問題に対処しています。

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

https://github.com/golang/go/commit/8d5ec52e6cf3a259b9054ee3c3621834f2491860

元コミット内容

cmd/6c, cmd/6g, cmd/cc: fix undefined behavior warnings

Update #5764

Like Tribbles, the more you kill, the more spring up in their place.

R=rsc
CC=golang-dev
https://golang.org/cl/13324049

変更の背景

このコミットは、Goコンパイラおよび関連ツールが生成するコードにおける「未定義動作 (Undefined Behavior)」に関する警告に対処するために行われました。未定義動作とは、C言語やGo言語のようなプログラミング言語の仕様において、特定の操作の結果が規定されていない状態を指します。このような操作が行われた場合、プログラムの動作は予測不可能となり、クラッシュ、誤った結果の生成、セキュリティ脆弱性など、様々な問題を引き起こす可能性があります。

Go言語のツールチェーンは、C言語で書かれた部分も多く含まれており、特にコンパイラのバックエンドやアセンブラはC言語で実装されています。C言語では、符号付き整数のオーバーフロー、負の数を右シフトする、シフト量が型のビット幅を超える、NULLポインタのデリファレンスなど、多くの未定義動作が存在します。これらの未定義動作は、コンパイラの最適化によって予期せぬコードが生成されたり、異なるコンパイラやプラットフォーム間で動作が異なったりする原因となります。

このコミットの背景には、Go Issue 5764 (https://golang.org/issue/5764) があります。このIssueでは、Goコンパイラが生成するコードが、特定の条件下で未定義動作を引き起こす可能性が指摘されていました。特に、ビットシフト操作において、符号付き整数をシフトする際に未定義動作が発生するケースが問題視されていました。コンパイラ開発者は、このような未定義動作を特定し、修正することで、Goツールチェーンの堅牢性と信頼性を向上させる必要がありました。

コミットメッセージにある「Like Tribbles, the more you kill, the more spring up in their place.」という表現は、未定義動作の修正が、一つの問題を解決すると別の問題が浮上するという、いたちごっこのような状況であることを示唆しています。これは、コンパイラのような複雑なシステムにおいて、未定義動作を完全に排除することの難しさを物語っています。

前提知識の解説

未定義動作 (Undefined Behavior, UB)

未定義動作とは、プログラミング言語の標準によってその動作が規定されていない操作や状況を指します。C言語やC++において特に顕著ですが、Go言語のCで書かれた部分にも適用されます。未定義動作が発生した場合、コンパイラはどのようなコードを生成してもよく、プログラムは予測不可能な動作を示します。これは、プログラムがクラッシュしたり、間違った結果を返したり、あるいは一見正しく動作しているように見えても、将来的に問題を引き起こす可能性を秘めています。

未定義動作の例:

  • 符号付き整数のオーバーフロー
  • NULLポインタのデリファレンス
  • 配列の範囲外アクセス
  • シフト演算子において、シフト量が負である、または左オペランドのビット幅以上である場合
  • 負の数を右シフトする (C99以前では未定義、C99以降では実装定義)

コンパイラは未定義動作を利用して積極的な最適化を行うことがあります。例えば、ある条件が未定義動作を引き起こす場合、コンパイラはその条件が絶対に発生しないと仮定してコードを最適化することがあります。これにより、開発者が意図しない動作が発生する可能性があります。

ビットシフト演算子 (<<, >>)

ビットシフト演算子は、数値のビットを左右に移動させる操作です。

  • x << y: x のビットを y ビット左にシフトします。左にシフトされたビットは失われ、右側には0が埋められます。これは通常、x * 2^y と同等です。
  • x >> y: x のビットを y ビット右にシフトします。右にシフトされたビットは失われます。

符号付き整数と符号なし整数におけるシフトの挙動:

  • 符号なし整数 (unsigned int, unsigned long long など):
    • 左シフト (<<): 常に0が右から埋められます。
    • 右シフト (>>): 常に0が左から埋められます (論理シフト)。
  • 符号付き整数 (int, long long など):
    • 左シフト (<<): 最上位ビットが失われる場合、オーバーフローが発生し、これは未定義動作となる可能性があります。
    • 右シフト (>>): 最上位ビットの扱いが実装定義です。算術シフト (符号ビットが複製される) または論理シフト (0が埋められる) のいずれかになります。C言語の標準では、負の数を右シフトした場合の動作はC99以前では未定義、C99以降では実装定義とされています。

このコミットでは、特に符号付き整数に対する左シフト操作が未定義動作を引き起こす可能性があったため、これを符号なし整数にキャストすることで、未定義動作を回避しています。

uvlongvlong (Goコンパイラ内部型)

Goコンパイラの内部では、C言語の型に加えて、特定の目的のために定義された型が使用されます。

  • vlong: long long に相当する符号付き64ビット整数型。
  • uvlong: unsigned long long に相当する符号なし64ビット整数型。

これらの型は、コンパイラが中間表現やコード生成を行う際に、数値やアドレスを表現するために使われます。未定義動作を回避するためには、これらの型を適切に使い分けることが重要です。

mpgetfix (多倍長整数演算)

mpgetfix は、Goコンパイラ内部で使われる多倍長整数 (multi-precision integer) を固定小数点数に変換する関数です。コンパイラは、大きな定数や複雑な数値計算を扱う際に、通常のCPUレジスタに収まらない多倍長整数を使用します。mpgetfix は、これらの多倍長整数から、指定された型の固定小数点数表現を取得するために使用されます。

技術的詳細

このコミットは、Goコンパイラおよび関連ツールにおける3つの異なるファイル、src/cmd/6c/sgen.c, src/cmd/6g/ggen.c, src/cmd/cc/scon.c のコードを変更しています。それぞれの変更は、異なる文脈で発生する未定義動作の警告に対処しています。

  1. src/cmd/6c/sgen.c の変更:

    • gtext 関数内で、スタックオフセットと引数サイズを結合してvlong型の変数vに格納する際に、argsize()の戻り値をuvlong (符号なし64ビット整数) にキャストしています。
    • 元のコード: v = (argsize() << 32) | (stkoff & 0xffffffff);
    • 変更後: v = ((uvlong)argsize() << 32) | (stkoff & 0xffffffff);
    • argsize()は符号付き整数を返す可能性があり、これを32ビット左シフトする際に、結果がvlong (符号付き) の範囲を超えると未定義動作となる可能性があります。uvlongにキャストすることで、シフト操作が符号なし整数に対して行われるようになり、未定義動作が回避されます。符号なし整数に対する左シフトは、オーバーフローしても未定義動作にはなりません(ラップアラウンドします)。
  2. src/cmd/6g/ggen.c の変更:

    • dodiv 関数内で、除算のオーバーフローチェックを行う際に、特定の定数 -1LL<<(t->width*8-1) の表現を修正しています。
    • 元のコード: if(isconst(nl, CTINT) && mpgetfix(nl->val.u.xval) != -1LL<<(t->width*8-1))
    • 変更後: if(isconst(nl, CTINT) && mpgetfix(nl->val.u.xval) != -(1ULL<<(t->width*8-1)))
    • -1LL<<(t->width*8-1) は、符号付き64ビット整数 (long long) の最小値を表現しようとしています。しかし、1LL<<(t->width*8-1) は、long long の最大値に1を加えた値(つまり最小値の絶対値)を生成しようとしますが、これは符号付き整数のオーバーフローを引き起こし、未定義動作となります。
    • -(1ULL<<(t->width*8-1)) とすることで、まず 1ULL (符号なし64ビット整数1) を左シフトし、その結果に負号を適用しています。符号なし整数に対するシフトは未定義動作を引き起こさず、結果として正しい最小値の表現が得られます。これにより、符号付き整数のオーバーフローによる未定義動作を回避しています。
  3. src/cmd/cc/scon.c の変更:

    • evconst 関数内で、左シフト操作 OASHL の結果を計算する際に、左オペランド l->vconstuvlongにキャストしています。
    • 元のコード: v = l->vconst << r->vconst;
    • 変更後: v = (uvlong)l->vconst << r->vconst;
    • l->vconst は符号付き整数である可能性があり、これをシフトする際に未定義動作が発生する可能性があります。uvlongにキャストすることで、シフト操作が符号なし整数に対して行われるようになり、未定義動作が回避されます。これはsgen.cの変更と同様の理由です。

これらの変更はすべて、C言語における符号付き整数のビットシフト操作や定数表現に関する未定義動作を、符号なし整数へのキャストや適切な定数表現を用いることで回避するという共通のパターンに従っています。これにより、コンパイラが生成するコードの堅牢性が向上し、予測可能な動作が保証されます。

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

このコミットでは、以下の3つのファイルが変更されています。

  1. src/cmd/6c/sgen.c

    --- a/src/cmd/6c/sgen.c
    +++ b/src/cmd/6c/sgen.c
    @@ -36,7 +36,7 @@ gtext(Sym *s, int32 stkoff)
     {
     	vlong v;
    
    -	v = (argsize() << 32) | (stkoff & 0xffffffff);
    +	v = ((uvlong)argsize() << 32) | (stkoff & 0xffffffff);
     	if((textflag & NOSPLIT) && stkoff >= 128)
     		yyerror("stack frame too large for NOSPLIT function");
    
  2. src/cmd/6g/ggen.c

    --- a/src/cmd/6g/ggen.c
    +++ b/src/cmd/6g/ggen.c
    @@ -596,7 +596,7 @@ dodiv(int op, Node *nl, Node *nr, Node *res)
     	check = 0;
     	if(issigned[t->etype]) {
     		check = 1;
    -		if(isconst(nl, CTINT) && mpgetfix(nl->val.u.xval) != -1LL<<(t->width*8-1))
    +		if(isconst(nl, CTINT) && mpgetfix(nl->val.u.xval) != -(1ULL<<(t->width*8-1)))
     			check = 0;
     		else if(isconst(nr, CTINT) && mpgetfix(nr->val.u.xval) != -1)
     			check = 0;
    
  3. src/cmd/cc/scon.c

    --- a/src/cmd/cc/scon.c
    +++ b/src/cmd/cc/scon.c
    @@ -186,7 +186,7 @@ evconst(Node *n)
     		break;
    
     	case OASHL:
    -		v = l->vconst << r->vconst;
    +		v = (uvlong)l->vconst << r->vconst;
     		break;
    
     	case OLO:
    

コアとなるコードの解説

src/cmd/6c/sgen.c の変更

  • 変更前: v = (argsize() << 32) | (stkoff & 0xffffffff);
  • 変更後: v = ((uvlong)argsize() << 32) | (stkoff & 0xffffffff);

argsize() の戻り値が符号付き整数である場合、これを32ビット左シフトすると、結果が符号付き64ビット整数 vlong の表現範囲を超えてオーバーフローする可能性があります。C言語の標準では、符号付き整数のオーバーフローは未定義動作です。 変更後では、argsize() の結果を明示的に uvlong (符号なし64ビット整数) にキャストしています。符号なし整数に対する左シフトは、オーバーフローしても未定義動作にはならず、ビットがラップアラウンドします。これにより、未定義動作を回避し、意図した通りのビット操作が保証されます。

src/cmd/6g/ggen.c の変更

  • 変更前: if(isconst(nl, CTINT) && mpgetfix(nl->val.u.xval) != -1LL<<(t->width*8-1))
  • 変更後: if(isconst(nl, CTINT) && mpgetfix(nl->val.u.xval) != -(1ULL<<(t->width*8-1)))

この行は、除算における特定の定数(通常は最小負数)との比較を行っています。 変更前の -1LL<<(t->width*8-1) は、符号付き64ビット整数 long long の最小値を表現しようとしています。しかし、1LL<<(t->width*8-1) の部分で、long long の最大値に1を加えた値(つまり最小値の絶対値)を生成しようとすると、符号付き整数のオーバーフローが発生し、未定義動作となります。 変更後では、1ULL (符号なし64ビット整数1) を左シフトしています。符号なし整数に対するシフトは未定義動作を引き起こしません。その結果に負号を適用することで、符号付き整数の最小値を正しく表現しています。これにより、符号付き整数のオーバーフローによる未定義動作を回避し、比較が正しく行われるようにしています。

src/cmd/cc/scon.c の変更

  • 変更前: v = l->vconst << r->vconst;
  • 変更後: v = (uvlong)l->vconst << r->vconst;

この変更は、src/cmd/6c/sgen.c の変更と同様の理由です。l->vconst が符号付き整数である場合、左シフト操作が未定義動作を引き起こす可能性があります。uvlong にキャストすることで、シフト操作が符号なし整数に対して行われるようになり、未定義動作が回避されます。

これらの変更は、Goコンパイラおよび関連ツールが生成するコードの堅牢性を高め、C言語の未定義動作の落とし穴を回避するための重要な修正です。

関連リンク

参考にした情報源リンク

  • C言語の未定義動作に関する一般的な情報源 (例: C Standard, Stack Overflow, C言語の教科書など)
  • Go言語のコンパイラ内部構造に関するドキュメント (もし公開されているものがあれば)
  • ビットシフト演算子に関するC言語の仕様
  • 符号付き整数と符号なし整数の挙動に関する情報
  • 多倍長整数演算に関する一般的な情報
  • Tribbles (スタートレックの架空の生物) についての知識 (コミットメッセージの比喩を理解するため)