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

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

このコミットは、Go言語のランタイムがNetBSD/386アーキテクチャ上で時間(time)を扱う際のバグ修正に関するものです。具体的には、timevalおよびtimespec構造体内のtv_secフィールドが、NetBSD/386では64ビット整数として扱われるにもかかわらず、Goランタイムがこれを32ビットとして処理していた問題を修正しています。これにより、時間の計算が正しく行われない可能性がありました。修正は、src/pkg/runtime/sys_netbsd_386.sというNetBSD/386向けのアセンブリファイルに対して行われ、usleeptime·nownanotimeといった時間関連の関数における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や、時間構造体(timevaltimespec)内の秒数フィールド(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)。メモリのアドレスを計算し、そのアドレスをレジスタに格納します。
  • スタックフレーム: 関数呼び出し時に、引数、戻りアドレス、ローカル変数などを格納するためにスタック上に確保される領域です。SPFPからのオフセットでアクセスされます。

システムコール

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ランタイム関数に適用されました。

  1. runtime·usleep:

    • この関数は、指定されたマイクロ秒数だけスリープするために、timeval構造体を構築してシステムコールに渡します。
    • 以前はtv_secを32ビットとしてスタックに配置していましたが、修正後は64ビットとして配置するように変更されました。具体的には、tv_secの低位32ビットの後に、高位32ビットとして$0(ゼロ)を明示的にスタックにプッシュしています。これは、usleepが比較的短い時間(マイクロ秒単位)を扱うため、秒数が64ビットの範囲の大きな値になることは稀であり、高位ビットがゼロであることがほとんどであるという仮定に基づいている可能性があります。しかし、これによりtv_secが64ビットとして正しくメモリ上に配置されるようになります。
    • スタックフレームのサイズも、この追加の4バイト(高位32ビット用)を考慮して$20から$24に増加しました。
  2. 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)に調整されました。
  3. 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 CXAX * CX -> DX:AX)で32ビット乗算を行っていました。
      • 修正後は、tv_secの高位32ビット(16(SP)から読み取ったCX)と低位32ビット(12(SP)から読み取ったAX)をそれぞれ10億倍し、その結果を結合するロジックが導入されました。
        • まず、tv_secの高位32ビットCXIMULL $1000000000, CXで10億倍します。この結果はCXに格納されます。
        • 次に、tv_secの低位32ビットAXMULL BXAX * BX -> DX:AX)で10億倍します。この結果はDX:AX(64ビット)に格納されます。
        • 最後に、tv_usecをナノ秒に変換した値(BX)をAXに加算し、ADCL CX, DXで、高位32ビットの乗算結果CXDX(低位32ビット乗算結果の上位部分)にキャリー付きで加算します。これにより、DX:AXレジスタペアにtv_sectv_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バイトが必要になったためです。
  • 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_sectv_usecの読み取りロジックが削除されました。これらはtv_secを32ビットとして扱っていました。
  • + MOVL 12(SP), AX // sec - l32:
    • スタックオフセット12(SP)からtv_secの低位32ビットをAXレジスタに読み込みます。
  • MOVL AX, sec+0(FP):
    • AXtv_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ビットをゼロクリアしていましたが、修正後はAXtv_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_sectv_usecの読み取りロジックが削除されました。
  • + MOVL 16(SP), CX // sec - h32:
    • スタックオフセット16(SP)からtv_secの高位32ビットをCXレジスタに読み込みます。
  • + IMULL $1000000000, CX:
    • CXtv_secの高位32ビット)に10億(ナノ秒/秒)を符号付き乗算します。この結果はCXに格納されます。これは、64ビット乗算の一部です。
  • + MOVL 12(SP), AX // sec - l32:
    • スタックオフセット12(SP)からtv_secの低位32ビットをAXレジスタに読み込みます。
  • + MOVL $1000000000, BX:
    • 乗算に使用する10億をBXレジスタにロードします。
  • + MULL BX // result in dx:ax:
    • AXtv_secの低位32ビット)とBX(10億)を符号なし乗算します。結果はDX:AXレジスタペアに64ビット値として格納されます(DXに高位32ビット、AXに低位32ビット)。
  • + MOVL 20(SP), BX // usec:
    • tv_usecの読み取りオフセットが20(SP)に移動しました。
  • IMULL $1000, BX:
    • BXtv_usec)に1000(ナノ秒/マイクロ秒)を符号付き乗算します。
  • ADDL BX, AX:
    • BXtv_usecをナノ秒に変換した値)をAXtv_secの低位32ビット乗算結果の低位部分)に加算します。
  • ADCL $0, DX から ADCL CX, DX // add high bits with carry:
    • 以前はDXにゼロをキャリー付き加算していましたが、修正後はCXtv_secの高位32ビット乗算結果)をDXtv_secの低位32ビット乗算結果の高位部分)にキャリー付きで加算します。これにより、tv_secの64ビット乗算結果とtv_usecのナノ秒変換結果がすべてDX:AXレジスタペアに正しく結合され、最終的な64ビットのナノ秒値が生成されます。

これらの変更により、NetBSD/386環境におけるGoランタイムの時間処理が、tv_secの64ビット表現に完全に対応し、正確な時間計算が可能になりました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(特にsrc/pkg/runtime/sys_netbsd_386.sの周辺コード)
  • NetBSDのドキュメントやソースコード(timevaltimespecの定義に関する情報)
  • 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向けのアセンブリファイルに対して行われ、usleeptime·nownanotimeといった時間関連の関数における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や、時間構造体(timevaltimespec)内の秒数フィールド(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)。メモリのアドレスを計算し、そのアドレスをレジスタに格納します。
  • スタックフレーム: 関数呼び出し時に、引数、戻りアドレス、ローカル変数などを格納するためにスタック上に確保される領域です。SPFPからのオフセットでアクセスされます。

システムコール

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ランタイム関数に適用されました。

  1. runtime·usleep:

    • この関数は、指定されたマイクロ秒数だけスリープするために、timeval構造体を構築してシステムコールに渡します。
    • 以前はtv_secを32ビットとしてスタックに配置していましたが、修正後は64ビットとして配置するように変更されました。具体的には、tv_secの低位32ビットの後に、高位32ビットとして$0(ゼロ)を明示的にスタックにプッシュしています。これは、usleepが比較的短い時間(マイクロ秒単位)を扱うため、秒数が64ビットの範囲の大きな値になることは稀であり、高位ビットがゼロであることがほとんどであるという仮定に基づいている可能性があります。しかし、これによりtv_secが64ビットとして正しくメモリ上に配置されるようになります。
    • スタックフレームのサイズも、この追加の4バイト(高位32ビット用)を考慮して$20から$24に増加しました。
  2. 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)に調整されました。
  3. 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 CXAX * CX -> DX:AX)で32ビット乗算を行っていました。
      • 修正後は、tv_secの高位32ビット(16(SP)から読み取ったCX)と低位32ビット(12(SP)から読み取ったAX)をそれぞれ10億倍し、その結果を結合するロジックが導入されました。
        • まず、tv_secの高位32ビットCXIMULL $1000000000, CXで10億倍します。この結果はCXに格納されます。
        • 次に、tv_secの低位32ビットAXMULL BXAX * BX -> DX:AX)で10億倍します。結果はDX:AX(64ビット)に格納されます(DXに高位32ビット、AXに低位32ビット)。
        • 最後に、tv_usecをナノ秒に変換した値(BX)をAXに加算し、ADCL CX, DXで、高位32ビットの乗算結果CXDX(低位32ビット乗算結果の上位部分)にキャリー付きで加算します。これにより、DX:AXレジスタペアにtv_sectv_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バイトが必要になったためです。
  • 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_sectv_usecの読み取りロジックが削除されました。これらはtv_secを32ビットとして扱っていました。
  • + MOVL 12(SP), AX // sec - l32:
    • スタックオフセット12(SP)からtv_secの低位32ビットをAXレジスタに読み込みます。
  • MOVL AX, sec+0(FP):
    • AXtv_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ビットをゼロクリアしていましたが、修正後はAXtv_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_sectv_usecの読み取りロジックが削除されました。
  • + MOVL 16(SP), CX // sec - h32:
    • スタックオフセット16(SP)からtv_secの高位32ビットをCXレジスタに読み込みます。
  • + IMULL $1000000000, CX:
    • CXtv_secの高位32ビット)に10億(ナノ秒/秒)を符号付き乗算します。この結果はCXに格納されます。これは、64ビット乗算の一部です。
  • + MOVL 12(SP), AX // sec - l32:
    • スタックオフセット12(SP)からtv_secの低位32ビットをAXレジスタに読み込みます。
  • + MOVL $1000000000, BX:
    • 乗算に使用する10億をBXレジスタにロードします。
  • + MULL BX // result in dx:ax:
    • AXtv_secの低位32ビット)とBX(10億)を符号なし乗算します。結果はDX:AXレジスタペアに64ビット値として格納されます(DXに高位32ビット、AXに低位32ビット)。
  • + MOVL 20(SP), BX // usec:
    • tv_usecの読み取りオフセットが20(SP)に移動しました。
  • IMULL $1000, BX:
    • BXtv_usec)に1000(ナノ秒/マイクロ秒)を符号付き乗算します。
  • ADDL BX, AX:
    • BXtv_usecをナノ秒に変換した値)をAXtv_secの低位32ビット乗算結果の低位部分)に加算します。
  • ADCL $0, DX から ADCL CX, DX // add high bits with carry:
    • 以前はDXにゼロをキャリー付き加算していましたが、修正後はCXtv_secの高位32ビット乗算結果)をDXtv_secの低位32ビット乗算結果の高位部分)にキャリー付きで加算します。これにより、tv_secの64ビット乗算結果とtv_usecのナノ秒変換結果がすべてDX:AXレジスタペアに正しく結合され、最終的な64ビットのナノ秒値が生成されます。

これらの変更により、NetBSD/386環境におけるGoランタイムの時間処理が、tv_secの64ビット表現に完全に対応し、正確な時間計算が可能になりました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(特にsrc/pkg/runtime/sys_netbsd_386.sの周辺コード)
  • NetBSDのドキュメントやソースコード(timevaltimespecの定義に関する情報)
  • i386アセンブリ命令セットリファレンス
  • Year 2038問題に関する一般的な情報源
  • Go Gerrit Change-Id: https://golang.org/cl/6256056 (Goのコードレビューシステムにおけるこの変更のページ)
  • Go言語のランタイムアセンブリに関する解説記事(一般的な知識として)
  • gettimeofdayシステムコールに関するドキュメント