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

[インデックス 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の例としては、以下のようなものがあります。

  1. 符号付き整数を負の数だけシフトする、または型のビット幅以上の数だけシフトする: int x = 1; x << -1;x << 32; (32ビット整数型の場合)
  2. 符号付き整数を左シフトした結果が、その型の表現範囲を超過する: 例えば、int型の最大値の半分より大きい値を1ビット左シフトするとオーバーフローし、UBとなります。特に、符号ビットに1がシフトされるようなケースです。

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

  • 左シフト (<<): オペランドのビットを左に指定された数だけ移動させます。右側には0が埋められます。
    • 符号なし整数: x << nx * 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で割った余り)として定義されます。

このコミットでは、以下の原則に基づいて修正が行われています。

  1. 左シフトの前に符号なし型にキャスト: 符号付き整数に対して左シフトを行う前に、その値をuint32uint64のような対応する符号なし整数型に明示的にキャストします。これにより、シフト操作が符号なしのセマンティクスで行われ、未定義動作が回避されます。
  2. リテラルの型指定: 数値リテラル(例: 1)を左シフトする場合、デフォルトではint型と解釈されるため、1UのようにUサフィックスを付けてunsigned intとして扱うことで、シフト操作が符号なしとして行われるようにします。

これらの変更は、コードの論理的な意味を変えることなく、C言語の標準に準拠し、clang -fsanitize=undefinedのようなツールによる警告を解消することを目的としています。これにより、Goツールチェインの堅牢性と、異なるプラットフォームやコンパイラでの移植性が向上します。

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

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

  1. 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がシフトされるようなケースで未定義動作を回避します。

  2. src/liblink/objfile.c:

    • uv = (uint64)(sval<<1) ^ (uint64)(int64)(sval>>63);
    • uv = ((uint64)sval<<1) ^ (uint64)(int64)(sval>>63);

    svaluint64にキャストしてから左シフトを行うように変更。これにより、svalint64の最大値に近い場合でも、sval<<1int64の範囲でオーバーフローして未定義動作になることを防ぎます。

    • return (int64)(uv>>1) ^ ((int64)uv<<63>>63);
    • return (int64)(uv>>1) ^ ((int64)((uint64)uv<<63)>>63);

    uvuint64にキャストしてから左シフトを行うように変更。これは、uvuint64型であるにもかかわらず、その値をint64にキャストしてから左シフトを行うと、int64の範囲で未定義動作を引き起こす可能性があるためです。((uint64)uv<<63)とすることで、シフト操作が符号なしのuint64で行われ、その結果をint64にキャストすることで、意図した符号拡張(または符号の抽出)が安全に行われるようにします。

  3. src/liblink/pcln.c:

    • v |= (*p & 0x7F) << shift;
    • v |= (uint32)(*p & 0x7F) << shift;

    (*p & 0x7F)の結果をuint32にキャストしてから左シフトを行うように変更。*puchar(通常は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<<1svalを2倍する操作ですが、svalint64の最大値の半分より大きい場合、int64の範囲でオーバーフローし、未定義動作となります。((uint64)sval<<1)とすることで、svalがまずuint64に昇格され、その上でシフトが行われるため、オーバーフローが符号なしのセマンティクスで処理され、未定義動作が回避されます。 return (int64)(uv>>1) ^ ((int64)uv<<63>>63); の行は、符号なしのvarint形式uvを元の符号付き整数int64にデコードする処理の一部です。((int64)uv<<63)の部分は、uvの最上位ビット(元のsvalの符号ビット)を抽出するために使用されます。ここでも、uvint64にキャストしてから左シフトを行うと、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言語コードにおけるビット操作の安全性を高め、異なるコンパイラやプラットフォームでのビルド・実行時の堅牢性を向上させる上で重要な意味を持ちます。

関連リンク

参考にした情報源リンク

  • 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のコンパイラに関するブログ記事やカンファレンス発表