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

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

このコミットは、Go言語のtimeパッケージ内のzoneinfo_read.goファイルにおける、uint32からintへの型変換に関する潜在的な問題を修正するものです。具体的には、int型が将来的に64ビットになる可能性に備え、32ビットの符号拡張を明示的に行うように変更されています。これにより、プラットフォームに依存しない正確な型変換が保証されます。

コミット

commit edce6349639e321c3b1a34036a8fbc08ad363cd3
Author: Russ Cox <rsc@golang.org>
Date:   Sun Sep 23 23:04:28 2012 -0400

    time: prepare for 64-bit ints
    
    The code was trying to interpret a uint32 n as a signed quantity
    and then store it in an int. For this, int(n) currently works, but when
    int becomes 64 bits one must write int(int32(n)) instead, to get
    the 32-bit sign extension.
    
    Update #2188.
    
    R=golang-dev, nigeltao
    CC=golang-dev
    https://golang.org/cl/6551068

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

https://github.com/golang/go/commit/edce6349639e321c3b1a34036a8fbc08ad363cd3

元コミット内容

time: prepare for 64-bit ints

The code was trying to interpret a uint32 n as a signed quantity
and then store it in an int. For this, int(n) currently works, but when
int becomes 64 bits one must write int(int32(n)) instead, to get
the 32-bit sign extension.

Update #2188.

R=golang-dev, nigeltao
CC=golang-dev
https://golang.org/cl/6551068

変更の背景

この変更は、Go言語のint型が将来的に32ビットから64ビットに移行する可能性に備えるためのものです。Go言語のint型は、その実行環境のアーキテクチャに依存してサイズが変化します(32ビットシステムでは32ビット、64ビットシステムでは64ビット)。

コミットメッセージによると、既存のコードではuint32型の変数nを符号付きの量として解釈し、それをint型に格納しようとしていました。当時のGoのint型が32ビットであったため、int(n)という直接的な変換は意図した通りに機能していました。しかし、int型が64ビットに拡張された場合、uint32の値を直接intにキャストすると、上位32ビットがゼロで埋められてしまい、元の32ビット値が負数であった場合の符号拡張が正しく行われないという問題が発生します。

この問題は、GoのIssue #2188で議論されており、int型のサイズがプラットフォームに依存すること、およびuintからintへの変換における挙動の曖昧さが指摘されていました。このコミットは、この将来的な互換性の問題を解決し、int型が64ビットになってもuint32の符号拡張が正しく行われるようにするための予防的な措置です。

前提知識の解説

Go言語の整数型

Go言語には、固定サイズの整数型と、プラットフォーム依存のサイズの整数型があります。

  • 固定サイズの整数型:

    • 符号付き: int8, int16, int32, int64
    • 符号なし: uint8, uint16, uint32, uint64 これらの型は、名前が示す通りのビット幅を持ち、どのシステムで実行されてもそのサイズは変わりません。
  • プラットフォーム依存の整数型:

    • int: 符号付き整数型。そのサイズは実行環境のCPUアーキテクチャに依存します。32ビットシステムでは32ビット、64ビットシステムでは64ビットになります。
    • uint: 符号なし整数型。intと同様に、そのサイズは実行環境のCPUアーキテクチャに依存します。
    • uintptr: ポインタ値を保持するのに十分な大きさの符号なし整数型。

2の補数表現 (Two's Complement)

コンピュータでは、負の数を表現するために2の補数表現が広く用いられています。Nビットの2の補数表現では、最上位ビット(MSB: Most Significant Bit)が符号ビットとして機能します。MSBが0であれば正の数、1であれば負の数を示します。

例えば、32ビットの符号付き整数(int32)の場合、0xFFFFFFFF-1を表します。これは、すべてのビットが1であるため、2の補数表現の定義により負の最大値となります。

符号拡張 (Sign Extension)

符号拡張とは、より小さいビット幅の符号付き整数を、より大きいビット幅の符号付き整数に変換する際に、元の数の符号を保持するために行われる操作です。具体的には、元の数の最上位ビット(符号ビット)を、新しいビット幅の上位ビットにコピーして埋めることで、数値の値を維持します。

例: 8ビットの符号付き整数 11111111 (-1) を16ビットに符号拡張する場合、新しい上位8ビットも1で埋められ、1111111111111111 (-1) となります。

uintからintへの変換と符号拡張の課題

Go言語において、uint(符号なし)からint(符号付き)への変換は、単純なビットパターンの再解釈として行われます。

  • uint32から32ビットintへの変換: uint32の最上位ビットが1の場合(つまり、math.MaxInt32より大きい値の場合)、そのビットパターンは32ビットのintとして解釈されると負の値になります。例えば、uint32(0xFFFFFFFF)は32ビットintでは-1になります。これは、ビットパターンがそのままコピーされ、符号ビットとして解釈されるためです。

  • uint32から64ビットintへの変換: intが64ビットの場合、uint32のすべての値は64ビットのintの正の範囲に収まります。この場合、uint32の値はそのまま64ビットのintに変換され、上位32ビットはゼロで埋められます。このとき、元のuint32が負の値を意図していたとしても、符号拡張は行われず、正の値として扱われてしまいます。これが、このコミットが解決しようとしている問題です。

技術的詳細

このコミットが対処している問題は、Go言語のint型がプラットフォーム依存であることに起因します。当時のGoのint型は32ビットシステムでは32ビット、64ビットシステムでは64ビットでした。しかし、Goの設計思想として、将来的にはすべてのプラットフォームでint型を64ビットに統一する可能性がありました(あるいは、少なくとも64ビット環境でのintの挙動を考慮する必要がありました)。

src/pkg/time/zoneinfo_read.go内のコードでは、zonedata.big4()から取得したuint32型の値nを、タイムゾーンのオフセットとしてzone[i].offsetint型)に格納していました。

元のコード: zone[i].offset = int(n)

このコードは、intが32ビットである環境では正しく機能していました。uint32nが例えば0xFFFFFFFF(符号なしで4294967295)であった場合、32ビットのintにキャストされると、2の補数表現により-1として解釈されます。これは、タイムゾーンのオフセットが負の値を取りうるため、意図された挙動でした。

しかし、もしint型が64ビットになった場合、int(n)というキャストでは、uint32n0xFFFFFFFFであっても、それはそのままint644294967295という正の値として格納されてしまいます。これは、64ビットのint型ではuint32の最大値が正の範囲に収まるため、上位32ビットがゼロで埋められるためです。結果として、タイムゾーンのオフセットが正しく負の値として解釈されなくなるというバグが発生する可能性がありました。

この問題を解決するためには、uint32の値を一度明示的に32ビットの符号付き整数として解釈し、その上でint型に変換する必要があります。これにより、int型が64ビットになったとしても、32ビットの符号拡張が正しく行われ、元のuint32が負の値を意図していた場合に、その負の値が64ビットのintに正しく伝播されるようになります。

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

--- a/src/pkg/time/zoneinfo_read.go
+++ b/src/pkg/time/zoneinfo_read.go
@@ -141,7 +141,7 @@ func loadZoneData(bytes []byte) (l *Location, err) {
 		if n, ok = zonedata.big4(); !ok {
 			return nil, badData
 		}
-		zone[i].offset = int(n)
+		zone[i].offset = int(int32(n))
 		var b byte
 		if b, ok = zonedata.byte(); !ok {
 			return nil, badData

コアとなるコードの解説

変更された行は以下の通りです。

-		zone[i].offset = int(n)
+		zone[i].offset = int(int32(n))
  • 変更前: zone[i].offset = int(n) nuint32型です。この行は、uint32型のnを直接int型にキャストしています。当時の32ビット環境では、intも32ビットであったため、uint32のビットパターンがそのままint32として解釈され、負の値が正しく表現されていました。しかし、intが64ビットになると、uint32のビットパターンは64ビットのintにゼロ拡張されてしまい、負の値が正しく伝播されません。

  • 変更後: zone[i].offset = int(int32(n)) この変更では、uint32型のnをまず明示的にint32型にキャストしています。

    1. int32(n): ここで、uint32nが32ビットの符号付き整数として解釈されます。もしnuint32の範囲で負の値を意図するビットパターン(例: 0xFFFFFFFF)であれば、この段階でint32-1として正しく解釈されます。
    2. int(...): 次に、このint32の値をint型にキャストします。もしintが32ビットであれば、そのまま値がコピーされます。もしintが64ビットであれば、int32の値が64ビットのintに符号拡張されます。つまり、int32-1int64-1として正しく拡張されます。

この二段階のキャストにより、int型が将来的に64ビットになったとしても、uint32からintへの変換時に32ビットの符号拡張が保証され、タイムゾーンのオフセットが常に正しく解釈されるようになります。これは、Go言語の型変換における将来の互換性と堅牢性を高めるための重要な修正です。

関連リンク

参考にした情報源リンク