[インデックス 13292] ファイルの概要
このコミットは、Go言語のランタイムがNetBSD/386アーキテクチャ上で時間(time)を扱う際のバグ修正に関するものです。具体的には、timeval
およびtimespec
構造体内のtv_sec
フィールドが、NetBSD/386では64ビット整数として扱われるにもかかわらず、Goランタイムがこれを32ビットとして処理していた問題を修正しています。これにより、時間の計算が正しく行われない可能性がありました。修正は、src/pkg/runtime/sys_netbsd_386.s
というNetBSD/386向けのアセンブリファイルに対して行われ、usleep
、time·now
、nanotime
といった時間関連の関数におけるtv_sec
の読み取りと計算ロジックが更新されました。
コミット
commit 2cb74984553e07ae3bc7ca7e89099c11925b01c1
Author: Joel Sing <jsing@google.com>
Date: Wed Jun 6 20:39:27 2012 +1000
runtime: fix tv_sec handling for netbsd/386
On netbsd/386, tv_sec is a 64-bit integer for both timeval and timespec.
Fix the time handling code so that it works correctly.
R=golang-dev, rsc, m4dh4tt3r
CC=golang-dev
https://golang.org/cl/6256056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2cb74984553e07ae3bc7ca7e89099c11925b01c1
元コミット内容
runtime: fix tv_sec handling for netbsd/386
On netbsd/386, tv_sec is a 64-bit integer for both timeval and timespec.
Fix the time handling code so that it works correctly.
変更の背景
この変更の背景には、特定のオペレーティングシステムとアーキテクチャの組み合わせにおける時間表現の差異があります。
多くの32ビットシステムでは、Unix時間(エポックからの秒数)を格納するtime_t
型は32ビット整数として定義されていました。しかし、32ビット整数では2038年1月19日3時14分7秒(UTC)を超えるとオーバーフローが発生し、負の値として扱われる「Year 2038問題」が知られています。この問題に対処するため、一部のOSでは32ビットアーキテクチャ上でもtime_t
や、時間構造体(timeval
やtimespec
)内の秒数フィールド(tv_sec
)を64ビット整数として定義していました。
NetBSD/386(NetBSDオペレーティングシステムをIntel 80386互換の32ビットCPU上で動作させる環境)もその一つで、timeval
およびtimespec
構造体のtv_sec
フィールドが64ビット整数として実装されていました。
Go言語のランタイムは、OSのシステムコールを直接呼び出して時間情報を取得します。この際、Goランタイム内のアセンブリコードが、NetBSD/386のtv_sec
が64ビットであるという事実を考慮せずに、一般的な32ビットシステムと同様に32ビット整数として扱っていたため、取得した秒数情報が正しく解釈されず、時間の計算に誤りが生じていました。特に、tv_sec
が32ビットで表現できる範囲を超えた大きな値になった場合に問題が顕在化します。
このコミットは、このNetBSD/386特有のtv_sec
のサイズに関する不一致を解消し、Goランタイムが正確な時間情報に基づいて動作するようにするための修正です。
前提知識の解説
Go言語のランタイムとアセンブリ
Go言語は、その高性能と並行処理のサポートのために、一部の低レベルな処理をGo言語自体ではなく、アセンブリ言語で実装しています。これは「ランタイム」と呼ばれ、ガベージコレクション、スケジューリング、システムコールインターフェースなど、OSと密接に連携する部分が含まれます。特に、OS固有のシステムコールを呼び出す部分は、OSやアーキテクチャごとに異なるアセンブリコードで書かれることが一般的です。src/pkg/runtime/sys_netbsd_386.s
は、NetBSD/386アーキテクチャ向けのGoランタイムのアセンブリコードファイルです。
NetBSD/386の特性と時間構造体
- NetBSD/386: NetBSDは、UNIX系のオープンソースオペレーティングシステムであり、高い移植性を持つことで知られています。
/386
は、Intel 80386互換の32ビットCPUアーキテクチャを指します。 timeval
構造体: POSIX標準で定義されている時間表現のための構造体で、秒とマイクロ秒で時間を表します。struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ };
timespec
構造体:timeval
と同様に時間を表しますが、秒とナノ秒で時間を表します。struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ };
time_t
型: エポック(通常は1970年1月1日00:00:00 UTC)からの経過秒数を表す型です。多くの32ビットシステムでは32ビット整数ですが、Year 2038問題への対策として、NetBSD/386のように64ビット整数として実装されている場合があります。このコミットの核心は、tv_sec
がNetBSD/386では64ビットであるという点です。
i386アセンブリの基本
このコミットで変更されているのはi386(32ビットx86)アセンブリコードです。Go言語のアセンブリはAT&T構文に似た独自の構文を使用しますが、基本的な概念は共通です。
- レジスタ:
AX
,BX
,CX
,DX
: 汎用レジスタ。演算やデータの一時的な格納に使用されます。SP
(Stack Pointer): スタックの現在のトップを指します。FP
(Frame Pointer): 現在のスタックフレームのベースを指します。関数引数やローカル変数へのアクセスに使用されます。SB
(Static Base): グローバルシンボルや外部シンボルへのオフセット計算に使用される擬似レジスタ。
- 命令:
MOVL
: 32ビットの値を移動(Move Long)。DIVL
: 32ビットの符号なし除算。DX:AX
(64ビット)をオペランドで除算し、商をAX
に、剰余をDX
に格納します。MULL
: 32ビットの符号なし乗算。AX
とオペランドを乗算し、結果をDX:AX
(64ビット)に格納します。IMULL
: 32ビットの符号付き乗算。AX
とオペランドを乗算し、結果をDX:AX
(64ビット)に格納します。ADDL
: 32ビットの加算。ADCL
: 32ビットのキャリー付き加算。前の演算で発生したキャリーフラグを考慮して加算します。64ビット以上の数値を扱う際に、下位32ビットの加算結果のキャリーを上位32ビットの加算に含めるために使用されます。LEAL
: 有効アドレスをロード(Load Effective Address)。メモリのアドレスを計算し、そのアドレスをレジスタに格納します。
- スタックフレーム: 関数呼び出し時に、引数、戻りアドレス、ローカル変数などを格納するためにスタック上に確保される領域です。
SP
やFP
からのオフセットでアクセスされます。
システムコール
OSの機能(ファイルI/O、メモリ管理、時間取得など)を利用するために、ユーザープログラムがカーネルに処理を要求する仕組みです。i386アーキテクチャでは、INT $0x80
命令を使用してシステムコールを呼び出すのが一般的です。システムコールの種類は、AX
レジスタに格納されるシステムコール番号によって指定されます。sys_gettimeofday
は、現在の時刻を取得するためのシステムコールです。
Year 2038問題
32ビットの符号付き整数でUnix時間を表現する場合、最大値は2,147,483,647秒です。これは1970年1月1日からの秒数で、2038年1月19日3時14分7秒(UTC)にこの値を超えます。この瞬間以降、32ビットのtime_t
はオーバーフローし、負の値として解釈されるため、多くのシステムで問題が発生する可能性があります。この問題に対処するため、time_t
を64ビット整数に拡張する動きが進められました。NetBSD/386におけるtv_sec
の64ビット化もその一環です。
技術的詳細
このコミットの技術的詳細は、NetBSD/386におけるtimeval
およびtimespec
構造体のtv_sec
フィールドが64ビットであるという特性を、Goランタイムのアセンブリコードが正しく扱っていなかった点に集約されます。
一般的な32ビットシステムでは、tv_sec
は32ビット整数であり、メモリ上では1つの32ビットワード(4バイト)を占めます。しかし、NetBSD/386では64ビット整数であるため、2つの32ビットワード(8バイト)を占めます。Goランタイムのアセンブリコードがこの違いを認識していなかったため、tv_sec
の低位32ビットしか読み取らず、高位32ビットを無視したり、誤ったメモリ位置から読み取ったりしていました。
修正は、以下の3つのGoランタイム関数に適用されました。
-
runtime·usleep
:- この関数は、指定されたマイクロ秒数だけスリープするために、
timeval
構造体を構築してシステムコールに渡します。 - 以前は
tv_sec
を32ビットとしてスタックに配置していましたが、修正後は64ビットとして配置するように変更されました。具体的には、tv_sec
の低位32ビットの後に、高位32ビットとして$0
(ゼロ)を明示的にスタックにプッシュしています。これは、usleep
が比較的短い時間(マイクロ秒単位)を扱うため、秒数が64ビットの範囲の大きな値になることは稀であり、高位ビットがゼロであることがほとんどであるという仮定に基づいている可能性があります。しかし、これによりtv_sec
が64ビットとして正しくメモリ上に配置されるようになります。 - スタックフレームのサイズも、この追加の4バイト(高位32ビット用)を考慮して
$20
から$24
に増加しました。
- この関数は、指定されたマイクロ秒数だけスリープするために、
-
time·now
:- この関数は、
sys_gettimeofday
システムコールを呼び出して現在の時刻を取得し、その結果をGoのTime
型に変換します。 sys_gettimeofday
は、timeval
構造体を指すポインタを引数として受け取り、その構造体に現在の時刻を書き込みます。NetBSD/386では、この構造体のtv_sec
が64ビットで返されます。- 修正前は、スタック上の
tv_sec
の低位32ビットしか読み取っていませんでした。修正後は、12(SP)
からtv_sec
の低位32ビットを、16(SP)
からtv_sec
の高位32ビットをそれぞれ読み取り、Goのsec
変数(Go内部では64ビット整数として扱われる)に正しく格納するように変更されました。これにより、tv_sec
の全64ビットが正確に取得されるようになりました。 tv_usec
の読み取りオフセットも、tv_sec
が64ビットになったことに伴い、16(SP)
から20(SP)
に調整されました。
- この関数は、
-
runtime·nanotime
:- この関数は、
sys_gettimeofday
システムコールを呼び出して現在の時刻を取得し、それをナノ秒単位の単一の64ビット整数として返します。 - 最も複雑な変更が加えられた部分です。
tv_sec
(64ビット)をナノ秒に変換するために10億(1,000,000,000)を乗算し、さらにtv_usec
(マイクロ秒)をナノ秒に変換して加算する必要があります。 - 64ビット乗算の処理:
tv_sec
は64ビット値であり、これを10億倍すると結果は96ビット(32ビット * 32ビット = 64ビット、64ビット * 32ビット = 96ビット)になる可能性がありますが、ここでは64ビットのナノ秒表現に収まるように処理されます。- 修正前は、
tv_sec
を32ビットとして扱い、MULL CX
(AX
*CX
->DX:AX
)で32ビット乗算を行っていました。 - 修正後は、
tv_sec
の高位32ビット(16(SP)
から読み取ったCX
)と低位32ビット(12(SP)
から読み取ったAX
)をそれぞれ10億倍し、その結果を結合するロジックが導入されました。- まず、
tv_sec
の高位32ビットCX
をIMULL $1000000000, CX
で10億倍します。この結果はCX
に格納されます。 - 次に、
tv_sec
の低位32ビットAX
をMULL BX
(AX
*BX
->DX:AX
)で10億倍します。この結果はDX:AX
(64ビット)に格納されます。 - 最後に、
tv_usec
をナノ秒に変換した値(BX
)をAX
に加算し、ADCL CX, DX
で、高位32ビットの乗算結果CX
をDX
(低位32ビット乗算結果の上位部分)にキャリー付きで加算します。これにより、DX:AX
レジスタペアにtv_sec
とtv_usec
をナノ秒に変換した最終的な64ビット値が正しく格納されます。
- まず、
tv_usec
の読み取りオフセットも、tv_sec
が64ビットになったことに伴い、16(SP)
から20(SP)
に調整されました。
- この関数は、
これらの変更により、GoランタイムはNetBSD/386環境下でtv_sec
の64ビット値を正確に読み取り、時間計算に利用できるようになり、Year 2038問題への対応も強化されました。
コアとなるコードの変更箇所
src/pkg/runtime/sys_netbsd_386.s
ファイルにおける主要な変更箇所を抜粋します。
--- a/src/pkg/runtime/sys_netbsd_386.s
+++ b/src/pkg/runtime/sys_netbsd_386.s
@@ -27,15 +27,16 @@ TEXT runtime·write(SB),7,$-4
INT $0x80
RET
-TEXT runtime·usleep(SB),7,$20
+TEXT runtime·usleep(SB),7,$24
MOVL $0, DX
MOVL usec+0(FP), AX
MOVL $1000000, CX
DIVL CX
-\tMOVL AX, 12(SP)\t\t// tv_sec
+\tMOVL AX, 12(SP)\t\t// tv_sec - l32
+\tMOVL $0, 16(SP)\t\t// tv_sec - h32
MOVL $1000, AX
MULL DX
-\tMOVL AX, 16(SP)\t\t// tv_nsec
+\tMOVL AX, 20(SP)\t\t// tv_nsec
MOVL $0, 0(SP)
LEAL 12(SP), AX
@@ -94,12 +95,13 @@ TEXT time·now(SB), 7, $32
MOVL $0, 8(SP)\t\t// arg 2 - tzp
MOVL $418, AX\t\t// sys_gettimeofday
INT $0x80
-\tMOVL 12(SP), AX\t\t// sec
-\tMOVL 16(SP), BX\t\t// usec
+\tMOVL 12(SP), AX\t\t// sec - l32
+\tMOVL AX, sec+0(FP)
+\tMOVL 16(SP), AX\t\t// sec - h32
+\tMOVL AX, sec+4(FP)
+\tMOVL 20(SP), BX\t\t// usec - should not exceed 999999
-\t// sec is in AX, usec in BX
-\tMOVL AX, sec+0(FP)
-\tMOVL $0, sec+4(FP)
+\tIMULL $1000, BX
\tIMULL $1000, BX
\tMOVL BX, nsec+8(FP)
\tRET
@@ -112,16 +114,18 @@ TEXT runtime·nanotime(SB),7,$32
MOVL $0, 8(SP)\t\t// arg 2 - tzp
MOVL $418, AX\t\t// sys_gettimeofday
INT $0x80
-\tMOVL 12(SP), AX\t\t// sec
-\tMOVL 16(SP), BX\t\t// usec
+\tMOVL 16(SP), CX\t\t// sec - h32
+\tIMULL $1000000000, CX
+\tMOVL 12(SP), AX\t\t// sec - l32
+\tMOVL $1000000000, BX
+\tMULL BX\t\t\t// result in dx:ax
+\tMOVL 20(SP), BX\t\t// usec
+\tIMULL $1000, BX
+\tADDL BX, AX
+\tADCL CX, DX\t\t\t// add high bits with carry
-\t// sec is in AX, usec in BX
-\t// convert to DX:AX nsec
-\tMOVL $1000000000, CX
-\tMULL CX
-\tIMULL $1000, BX
-\tADDL BX, AX
-\tADCL $0, DX
+\tMOVL ret+0(FP), DI
+\tMOVL AX, 0(DI)
\n
コアとなるコードの解説
runtime·usleep
関数
TEXT runtime·usleep(SB),7,$20
からTEXT runtime·usleep(SB),7,$24
:- 関数のスタックフレームサイズが20バイトから24バイトに増加しました。これは、
tv_sec
を64ビットとしてスタックに格納するために追加の4バイトが必要になったためです。
- 関数のスタックフレームサイズが20バイトから24バイトに増加しました。これは、
MOVL AX, 12(SP) // tv_sec
からMOVL AX, 12(SP) // tv_sec - l32
:tv_sec
の低位32ビット(l32
)をスタックオフセット12(SP)
に格納します。
+ MOVL $0, 16(SP) // tv_sec - h32
:- 新しく追加された行です。
tv_sec
の高位32ビット(h32
)として$0
(ゼロ)をスタックオフセット16(SP)
に格納します。これにより、tv_sec
が64ビット値として正しくスタックに配置されます。usleep
は短いスリープを想定しているため、秒数が32ビットで収まる範囲であることがほとんどであり、高位ビットがゼロで問題ないという判断です。
- 新しく追加された行です。
MOVL AX, 16(SP) // tv_nsec
からMOVL AX, 20(SP) // tv_nsec
:tv_nsec
(ナノ秒)のスタックオフセットが16(SP)
から20(SP)
に移動しました。これは、tv_sec
が64ビットになったことで、その分スタック上の位置がずれたためです。
time·now
関数
MOVL 12(SP), AX // sec
(削除) とMOVL 16(SP), BX // usec
(削除):- 以前の
tv_sec
とtv_usec
の読み取りロジックが削除されました。これらはtv_sec
を32ビットとして扱っていました。
- 以前の
+ MOVL 12(SP), AX // sec - l32
:- スタックオフセット
12(SP)
からtv_sec
の低位32ビットをAX
レジスタに読み込みます。
- スタックオフセット
MOVL AX, sec+0(FP)
:AX
(tv_sec
の低位32ビット)を、Goのsec
変数(フレームポインタFP
からのオフセット0
)に格納します。
+ MOVL 16(SP), AX // sec - h32
:- スタックオフセット
16(SP)
からtv_sec
の高位32ビットをAX
レジスタに読み込みます。
- スタックオフセット
MOVL $0, sec+4(FP)
からMOVL AX, sec+4(FP)
:- 以前は
sec
変数の高位32ビットをゼロクリアしていましたが、修正後はAX
(tv_sec
の高位32ビット)をsec
変数の高位部分(フレームポインタFP
からのオフセット4
)に格納します。これにより、sec
変数が64ビットのtv_sec
値を正しく保持するようになります。
- 以前は
+ MOVL 20(SP), BX // usec - should not exceed 999999
:tv_usec
の読み取りオフセットが20(SP)
に移動しました。これは、tv_sec
が64ビットになったことで、その分スタック上の位置がずれたためです。
runtime·nanotime
関数
MOVL 12(SP), AX // sec
(削除) とMOVL 16(SP), BX // usec
(削除):- 以前の
tv_sec
とtv_usec
の読み取りロジックが削除されました。
- 以前の
+ MOVL 16(SP), CX // sec - h32
:- スタックオフセット
16(SP)
からtv_sec
の高位32ビットをCX
レジスタに読み込みます。
- スタックオフセット
+ IMULL $1000000000, CX
:CX
(tv_sec
の高位32ビット)に10億(ナノ秒/秒)を符号付き乗算します。この結果はCX
に格納されます。これは、64ビット乗算の一部です。
+ MOVL 12(SP), AX // sec - l32
:- スタックオフセット
12(SP)
からtv_sec
の低位32ビットをAX
レジスタに読み込みます。
- スタックオフセット
+ MOVL $1000000000, BX
:- 乗算に使用する10億を
BX
レジスタにロードします。
- 乗算に使用する10億を
+ MULL BX // result in dx:ax
:AX
(tv_sec
の低位32ビット)とBX
(10億)を符号なし乗算します。結果はDX:AX
レジスタペアに64ビット値として格納されます(DX
に高位32ビット、AX
に低位32ビット)。
+ MOVL 20(SP), BX // usec
:tv_usec
の読み取りオフセットが20(SP)
に移動しました。
IMULL $1000, BX
:BX
(tv_usec
)に1000(ナノ秒/マイクロ秒)を符号付き乗算します。
ADDL BX, AX
:BX
(tv_usec
をナノ秒に変換した値)をAX
(tv_sec
の低位32ビット乗算結果の低位部分)に加算します。
ADCL $0, DX
からADCL CX, DX // add high bits with carry
:- 以前は
DX
にゼロをキャリー付き加算していましたが、修正後はCX
(tv_sec
の高位32ビット乗算結果)をDX
(tv_sec
の低位32ビット乗算結果の高位部分)にキャリー付きで加算します。これにより、tv_sec
の64ビット乗算結果とtv_usec
のナノ秒変換結果がすべてDX:AX
レジスタペアに正しく結合され、最終的な64ビットのナノ秒値が生成されます。
- 以前は
これらの変更により、NetBSD/386環境におけるGoランタイムの時間処理が、tv_sec
の64ビット表現に完全に対応し、正確な時間計算が可能になりました。
関連リンク
- Go言語公式ドキュメント: https://golang.org/
- NetBSDプロジェクト: https://www.netbsd.org/
- Year 2038問題: https://ja.wikipedia.org/wiki/2038%E5%B9%B4%E5%95%8F%E9%A1%8C
- POSIX time.h (timeval, timespec): https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/time.h.html
参考にした情報源リンク
- Go言語のソースコード(特に
src/pkg/runtime/sys_netbsd_386.s
の周辺コード) - NetBSDのドキュメントやソースコード(
timeval
やtimespec
の定義に関する情報) - i386アセンブリ命令セットリファレンス
- Year 2038問題に関する一般的な情報源
- Go Gerrit Change-Id:
https://golang.org/cl/6256056
(Goのコードレビューシステムにおけるこの変更のページ) - Go言語のランタイムアセンブリに関する解説記事(一般的な知識として)
gettimeofday
システムコールに関するドキュメント# [インデックス 13292] ファイルの概要
このコミットは、Go言語のランタイムがNetBSD/386アーキテクチャ上で時間(time)を扱う際のバグ修正に関するものです。具体的には、timeval
およびtimespec
構造体内のtv_sec
フィールドが、NetBSD/386では64ビット整数として扱われるにもかかわらず、Goランタイムがこれを32ビットとして処理していた問題を修正しています。これにより、時間の計算が正しく行われない可能性がありました。修正は、src/pkg/runtime/sys_netbsd_386.s
というNetBSD/386向けのアセンブリファイルに対して行われ、usleep
、time·now
、nanotime
といった時間関連の関数におけるtv_sec
の読み取りと計算ロジックが更新されました。
コミット
commit 2cb74984553e07ae3bc7ca7e89099c11925b01c1
Author: Joel Sing <jsing@google.com>
Date: Wed Jun 6 20:39:27 2012 +1000
runtime: fix tv_sec handling for netbsd/386
On netbsd/386, tv_sec is a 64-bit integer for both timeval and timespec.
Fix the time handling code so that it works correctly.
R=golang-dev, rsc, m4dh4tt3r
CC=golang-dev
https://golang.org/cl/6256056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2cb74984553e07ae3bc7ca7e89099c11925b01c1
元コミット内容
runtime: fix tv_sec handling for netbsd/386
On netbsd/386, tv_sec is a 64-bit integer for both timeval and timespec.
Fix the time handling code so that it works correctly.
変更の背景
この変更の背景には、特定のオペレーティングシステムとアーキテクチャの組み合わせにおける時間表現の差異があります。
多くの32ビットシステムでは、Unix時間(エポックからの秒数)を格納するtime_t
型は32ビット整数として定義されていました。しかし、32ビット整数では2038年1月19日3時14分7秒(UTC)を超えるとオーバーフローが発生し、負の値として扱われる「Year 2038問題」が知られています。この問題に対処するため、一部のOSでは32ビットアーキテクチャ上でもtime_t
や、時間構造体(timeval
やtimespec
)内の秒数フィールド(tv_sec
)を64ビット整数として定義していました。
NetBSD/386(NetBSDオペレーティングシステムをIntel 80386互換の32ビットCPU上で動作させる環境)もその一つで、timeval
およびtimespec
構造体のtv_sec
フィールドが64ビット整数として実装されていました。
Go言語のランタイムは、OSのシステムコールを直接呼び出して時間情報を取得します。この際、Goランタイム内のアセンブリコードが、NetBSD/386のtv_sec
が64ビットであるという事実を考慮せずに、一般的な32ビットシステムと同様に32ビット整数として扱っていたため、取得した秒数情報が正しく解釈されず、時間の計算に誤りが生じていました。特に、tv_sec
が32ビットで表現できる範囲を超えた大きな値になった場合に問題が顕在化します。
このコミットは、このNetBSD/386特有のtv_sec
のサイズに関する不一致を解消し、Goランタイムが正確な時間情報に基づいて動作するようにするための修正です。
前提知識の解説
Go言語のランタイムとアセンブリ
Go言語は、その高性能と並行処理のサポートのために、一部の低レベルな処理をGo言語自体ではなく、アセンブリ言語で実装しています。これは「ランタイム」と呼ばれ、ガベージコレクション、スケジューリング、システムコールインターフェースなど、OSと密接に連携する部分が含まれます。特に、OS固有のシステムコールを呼び出す部分は、OSやアーキテクチャごとに異なるアセンブリコードで書かれることが一般的です。src/pkg/runtime/sys_netbsd_386.s
は、NetBSD/386アーキテクチャ向けのGoランタイムのアセンブリコードファイルです。
NetBSD/386の特性と時間構造体
- NetBSD/386: NetBSDは、UNIX系のオープンソースオペレーティングシステムであり、高い移植性を持つことで知られています。
/386
は、Intel 80386互換の32ビットCPUアーキテクチャを指します。 timeval
構造体: POSIX標準で定義されている時間表現のための構造体で、秒とマイクロ秒で時間を表します。struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ };
timespec
構造体:timeval
と同様に時間を表しますが、秒とナノ秒で時間を表します。struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ };
time_t
型: エポック(通常は1970年1月1日00:00:00 UTC)からの経過秒数を表す型です。多くの32ビットシステムでは32ビット整数ですが、Year 2038問題への対策として、NetBSD/386のように64ビット整数として実装されている場合があります。このコミットの核心は、tv_sec
がNetBSD/386では64ビットであるという点です。
i386アセンブリの基本
このコミットで変更されているのはi386(32ビットx86)アセンブリコードです。Go言語のアセンブリはAT&T構文に似た独自の構文を使用しますが、基本的な概念は共通です。
- レジスタ:
AX
,BX
,CX
,DX
: 汎用レジスタ。演算やデータの一時的な格納に使用されます。SP
(Stack Pointer): スタックの現在のトップを指します。FP
(Frame Pointer): 現在のスタックフレームのベースを指します。関数引数やローカル変数へのアクセスに使用されます。SB
(Static Base): グローバルシンボルや外部シンボルへのオフセット計算に使用される擬似レジスタ。
- 命令:
MOVL
: 32ビットの値を移動(Move Long)。DIVL
: 32ビットの符号なし除算。DX:AX
(64ビット)をオペランドで除算し、商をAX
に、剰余をDX
に格納します。MULL
: 32ビットの符号なし乗算。AX
とオペランドを乗算し、結果をDX:AX
(64ビット)に格納します。IMULL
: 32ビットの符号付き乗算。AX
とオペランドを乗算し、結果をDX:AX
(64ビット)に格納します。ADDL
: 32ビットの加算。ADCL
: 32ビットのキャリー付き加算。前の演算で発生したキャリーフラグを考慮して加算します。64ビット以上の数値を扱う際に、下位32ビットの加算結果のキャリーを上位32ビットの加算に含めるために使用されます。LEAL
: 有効アドレスをロード(Load Effective Address)。メモリのアドレスを計算し、そのアドレスをレジスタに格納します。
- スタックフレーム: 関数呼び出し時に、引数、戻りアドレス、ローカル変数などを格納するためにスタック上に確保される領域です。
SP
やFP
からのオフセットでアクセスされます。
システムコール
OSの機能(ファイルI/O、メモリ管理、時間取得など)を利用するために、ユーザープログラムがカーネルに処理を要求する仕組みです。i386アーキテクチャでは、INT $0x80
命令を使用してシステムコールを呼び出すのが一般的です。システムコールの種類は、AX
レジスタに格納されるシステムコール番号によって指定されます。sys_gettimeofday
は、現在の時刻を取得するためのシステムコールです。
Year 2038問題
32ビットの符号付き整数でUnix時間を表現する場合、最大値は2,147,483,647秒です。これは1970年1月1日からの秒数で、2038年1月19日3時14分7秒(UTC)にこの値を超えます。この瞬間以降、32ビットのtime_t
はオーバーフローし、負の値として解釈されるため、多くのシステムで問題が発生する可能性があります。この問題に対処するため、time_t
を64ビット整数に拡張する動きが進められました。NetBSD/386におけるtv_sec
の64ビット化もその一環です。
技術的詳細
このコミットの技術的詳細は、NetBSD/386におけるtimeval
およびtimespec
構造体のtv_sec
フィールドが64ビットであるという特性を、Goランタイムのアセンブリコードが正しく扱っていなかった点に集約されます。
一般的な32ビットシステムでは、tv_sec
は32ビット整数であり、メモリ上では1つの32ビットワード(4バイト)を占めます。しかし、NetBSD/386では64ビット整数であるため、2つの32ビットワード(8バイト)を占めます。Goランタイムのアセンブリコードがこの違いを認識していなかったため、tv_sec
の低位32ビットしか読み取らず、高位32ビットを無視したり、誤ったメモリ位置から読み取ったりしていました。
修正は、以下の3つのGoランタイム関数に適用されました。
-
runtime·usleep
:- この関数は、指定されたマイクロ秒数だけスリープするために、
timeval
構造体を構築してシステムコールに渡します。 - 以前は
tv_sec
を32ビットとしてスタックに配置していましたが、修正後は64ビットとして配置するように変更されました。具体的には、tv_sec
の低位32ビットの後に、高位32ビットとして$0
(ゼロ)を明示的にスタックにプッシュしています。これは、usleep
が比較的短い時間(マイクロ秒単位)を扱うため、秒数が64ビットの範囲の大きな値になることは稀であり、高位ビットがゼロであることがほとんどであるという仮定に基づいている可能性があります。しかし、これによりtv_sec
が64ビットとして正しくメモリ上に配置されるようになります。 - スタックフレームのサイズも、この追加の4バイト(高位32ビット用)を考慮して
$20
から$24
に増加しました。
- この関数は、指定されたマイクロ秒数だけスリープするために、
-
time·now
:- この関数は、
sys_gettimeofday
システムコールを呼び出して現在の時刻を取得し、その結果をGoのTime
型に変換します。 sys_gettimeofday
は、timeval
構造体を指すポインタを引数として受け取り、その構造体に現在の時刻を書き込みます。NetBSD/386では、この構造体のtv_sec
が64ビットで返されます。- 修正前は、スタック上の
tv_sec
の低位32ビットしか読み取っていませんでした。修正後は、12(SP)
からtv_sec
の低位32ビットを、16(SP)
からtv_sec
の高位32ビットをそれぞれ読み取り、Goのsec
変数(Go内部では64ビット整数として扱われる)に正しく格納するように変更されました。これにより、tv_sec
の全64ビットが正確に取得されるようになりました。 tv_usec
の読み取りオフセットも、tv_sec
が64ビットになったことに伴い、16(SP)
から20(SP)
に調整されました。
- この関数は、
-
runtime·nanotime
:- この関数は、
sys_gettimeofday
システムコールを呼び出して現在の時刻を取得し、それをナノ秒単位の単一の64ビット整数として返します。 - 最も複雑な変更が加えられた部分です。
tv_sec
(64ビット)をナノ秒に変換するために10億(1,000,000,000)を乗算し、さらにtv_usec
(マイクロ秒)をナノ秒に変換して加算する必要があります。 - 64ビット乗算の処理:
tv_sec
は64ビット値であり、これを10億倍すると結果は96ビット(32ビット * 32ビット = 64ビット、64ビット * 32ビット = 96ビット)になる可能性がありますが、ここでは64ビットのナノ秒表現に収まるように処理されます。- 修正前は、
tv_sec
を32ビットとして扱い、MULL CX
(AX
*CX
->DX:AX
)で32ビット乗算を行っていました。 - 修正後は、
tv_sec
の高位32ビット(16(SP)
から読み取ったCX
)と低位32ビット(12(SP)
から読み取ったAX
)をそれぞれ10億倍し、その結果を結合するロジックが導入されました。- まず、
tv_sec
の高位32ビットCX
をIMULL $1000000000, CX
で10億倍します。この結果はCX
に格納されます。 - 次に、
tv_sec
の低位32ビットAX
をMULL BX
(AX
*BX
->DX:AX
)で10億倍します。結果はDX:AX
(64ビット)に格納されます(DX
に高位32ビット、AX
に低位32ビット)。 - 最後に、
tv_usec
をナノ秒に変換した値(BX
)をAX
に加算し、ADCL CX, DX
で、高位32ビットの乗算結果CX
をDX
(低位32ビット乗算結果の上位部分)にキャリー付きで加算します。これにより、DX:AX
レジスタペアにtv_sec
とtv_usec
をナノ秒に変換した最終的な64ビット値が正しく格納されます。
- まず、
tv_usec
の読み取りオフセットも、tv_sec
が64ビットになったことに伴い、16(SP)
から20(SP)
に調整されました。
- この関数は、
これらの変更により、GoランタイムはNetBSD/386環境下でtv_sec
の64ビット値を正確に読み取り、時間計算に利用できるようになり、Year 2038問題への対応も強化されました。
コアとなるコードの変更箇所
src/pkg/runtime/sys_netbsd_386.s
ファイルにおける主要な変更箇所を抜粋します。
--- a/src/pkg/runtime/sys_netbsd_386.s
+++ b/src/pkg/runtime/sys_netbsd_386.s
@@ -27,15 +27,16 @@ TEXT runtime·write(SB),7,$-4
INT $0x80
RET
-TEXT runtime·usleep(SB),7,$20
+TEXT runtime·usleep(SB),7,$24
MOVL $0, DX
MOVL usec+0(FP), AX
MOVL $1000000, CX
DIVL CX
-\tMOVL AX, 12(SP)\t\t// tv_sec
+\tMOVL AX, 12(SP)\t\t// tv_sec - l32
+\tMOVL $0, 16(SP)\t\t// tv_sec - h32
MOVL $1000, AX
MULL DX
-\tMOVL AX, 16(SP)\t\t// tv_nsec
+\tMOVL AX, 20(SP)\t\t// tv_nsec
MOVL $0, 0(SP)
LEAL 12(SP), AX
@@ -94,12 +95,13 @@ TEXT time·now(SB), 7, $32
MOVL $0, 8(SP)\t\t// arg 2 - tzp
MOVL $418, AX\t\t// sys_gettimeofday
INT $0x80
-\tMOVL 12(SP), AX\t\t// sec
-\tMOVL 16(SP), BX\t\t// usec
+\tMOVL 12(SP), AX\t\t// sec - l32
+\tMOVL AX, sec+0(FP)
+\tMOVL 16(SP), AX\t\t// sec - h32
+\tMOVL AX, sec+4(FP)
+\tMOVL 20(SP), BX\t\t// usec - should not exceed 999999
-\t// sec is in AX, usec in BX
-\tMOVL AX, sec+0(FP)
-\tMOVL $0, sec+4(FP)
+\tIMULL $1000, BX
\tIMULL $1000, BX
\tMOVL BX, nsec+8(FP)
\tRET
@@ -112,16 +114,18 @@ TEXT runtime·nanotime(SB),7,$32
MOVL $0, 8(SP)\t\t// arg 2 - tzp
MOVL $418, AX\t\t// sys_gettimeofday
INT $0x80
-\tMOVL 12(SP), AX\t\t// sec
-\tMOVL 16(SP), BX\t\t// usec
+\tMOVL 16(SP), CX\t\t// sec - h32
+\tIMULL $1000000000, CX
+\tMOVL 12(SP), AX\t\t// sec - l32
+\tMOVL $1000000000, BX
+\tMULL BX\t\t\t// result in dx:ax
+\tMOVL 20(SP), BX\t\t// usec
+\tIMULL $1000, BX
+\tADDL BX, AX
+\tADCL CX, DX\t\t\t// add high bits with carry
-\t// sec is in AX, usec in BX
-\t// convert to DX:AX nsec
-\tMOVL $1000000000, CX
-\tMULL CX
-\tIMULL $1000, BX
-\tADDL BX, AX
-\tADCL $0, DX
+\tMOVL ret+0(FP), DI
+\tMOVL AX, 0(DI)
\n
コアとなるコードの解説
runtime·usleep
関数
TEXT runtime·usleep(SB),7,$20
からTEXT runtime·usleep(SB),7,$24
:- 関数のスタックフレームサイズが20バイトから24バイトに増加しました。これは、
tv_sec
を64ビットとしてスタックに格納するために追加の4バイトが必要になったためです。
- 関数のスタックフレームサイズが20バイトから24バイトに増加しました。これは、
MOVL AX, 12(SP) // tv_sec
からMOVL AX, 12(SP) // tv_sec - l32
:tv_sec
の低位32ビット(l32
)をスタックオフセット12(SP)
に格納します。
+ MOVL $0, 16(SP) // tv_sec - h32
:- 新しく追加された行です。
tv_sec
の高位32ビット(h32
)として$0
(ゼロ)をスタックオフセット16(SP)
に格納します。これにより、tv_sec
が64ビット値として正しくスタックに配置されます。usleep
は短いスリープを想定しているため、秒数が32ビットで収まる範囲であることがほとんどであり、高位ビットがゼロで問題ないという判断です。
- 新しく追加された行です。
MOVL AX, 16(SP) // tv_nsec
からMOVL AX, 20(SP) // tv_nsec
:tv_nsec
(ナノ秒)のスタックオフセットが16(SP)
から20(SP)
に移動しました。これは、tv_sec
が64ビットになったことで、その分スタック上の位置がずれたためです。
time·now
関数
MOVL 12(SP), AX // sec
(削除) とMOVL 16(SP), BX // usec
(削除):- 以前の
tv_sec
とtv_usec
の読み取りロジックが削除されました。これらはtv_sec
を32ビットとして扱っていました。
- 以前の
+ MOVL 12(SP), AX // sec - l32
:- スタックオフセット
12(SP)
からtv_sec
の低位32ビットをAX
レジスタに読み込みます。
- スタックオフセット
MOVL AX, sec+0(FP)
:AX
(tv_sec
の低位32ビット)を、Goのsec
変数(フレームポインタFP
からのオフセット0
)に格納します。
+ MOVL 16(SP), AX // sec - h32
:- スタックオフセット
16(SP)
からtv_sec
の高位32ビットをAX
レジスタに読み込みます。
- スタックオフセット
MOVL $0, sec+4(FP)
からMOVL AX, sec+4(FP)
:- 以前は
sec
変数の高位32ビットをゼロクリアしていましたが、修正後はAX
(tv_sec
の高位32ビット)をsec
変数の高位部分(フレームポインタFP
からのオフセット4
)に格納します。これにより、sec
変数が64ビットのtv_sec
値を正しく保持するようになります。
- 以前は
+ MOVL 20(SP), BX // usec - should not exceed 999999
:tv_usec
の読み取りオフセットが20(SP)
に移動しました。これは、tv_sec
が64ビットになったことで、その分スタック上の位置がずれたためです。
runtime·nanotime
関数
MOVL 12(SP), AX // sec
(削除) とMOVL 16(SP), BX // usec
(削除):- 以前の
tv_sec
とtv_usec
の読み取りロジックが削除されました。
- 以前の
+ MOVL 16(SP), CX // sec - h32
:- スタックオフセット
16(SP)
からtv_sec
の高位32ビットをCX
レジスタに読み込みます。
- スタックオフセット
+ IMULL $1000000000, CX
:CX
(tv_sec
の高位32ビット)に10億(ナノ秒/秒)を符号付き乗算します。この結果はCX
に格納されます。これは、64ビット乗算の一部です。
+ MOVL 12(SP), AX // sec - l32
:- スタックオフセット
12(SP)
からtv_sec
の低位32ビットをAX
レジスタに読み込みます。
- スタックオフセット
+ MOVL $1000000000, BX
:- 乗算に使用する10億を
BX
レジスタにロードします。
- 乗算に使用する10億を
+ MULL BX // result in dx:ax
:AX
(tv_sec
の低位32ビット)とBX
(10億)を符号なし乗算します。結果はDX:AX
レジスタペアに64ビット値として格納されます(DX
に高位32ビット、AX
に低位32ビット)。
+ MOVL 20(SP), BX // usec
:tv_usec
の読み取りオフセットが20(SP)
に移動しました。
IMULL $1000, BX
:BX
(tv_usec
)に1000(ナノ秒/マイクロ秒)を符号付き乗算します。
ADDL BX, AX
:BX
(tv_usec
をナノ秒に変換した値)をAX
(tv_sec
の低位32ビット乗算結果の低位部分)に加算します。
ADCL $0, DX
からADCL CX, DX // add high bits with carry
:- 以前は
DX
にゼロをキャリー付き加算していましたが、修正後はCX
(tv_sec
の高位32ビット乗算結果)をDX
(tv_sec
の低位32ビット乗算結果の高位部分)にキャリー付きで加算します。これにより、tv_sec
の64ビット乗算結果とtv_usec
のナノ秒変換結果がすべてDX:AX
レジスタペアに正しく結合され、最終的な64ビットのナノ秒値が生成されます。
- 以前は
これらの変更により、NetBSD/386環境におけるGoランタイムの時間処理が、tv_sec
の64ビット表現に完全に対応し、正確な時間計算が可能になりました。
関連リンク
- Go言語公式ドキュメント: https://golang.org/
- NetBSDプロジェクト: https://www.netbsd.org/
- Year 2038問題: https://ja.wikipedia.org/wiki/2038%E5%B9%B4%E5%95%8F%E9%A1%8C
- POSIX time.h (timeval, timespec): https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/time.h.html
参考にした情報源リンク
- Go言語のソースコード(特に
src/pkg/runtime/sys_netbsd_386.s
の周辺コード) - NetBSDのドキュメントやソースコード(
timeval
やtimespec
の定義に関する情報) - i386アセンブリ命令セットリファレンス
- Year 2038問題に関する一般的な情報源
- Go Gerrit Change-Id:
https://golang.org/cl/6256056
(Goのコードレビューシステムにおけるこの変更のページ) - Go言語のランタイムアセンブリに関する解説記事(一般的な知識として)
gettimeofday
システムコールに関するドキュメント