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

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

このコミットは、Go言語のsyscallパッケージにおけるFreeBSD 386アーキテクチャ向けのsendfileシステムコールの実装に関するバグ修正です。具体的には、32ビットシステム上で64ビットのoff_tパラメータが正しく扱われていなかった問題を解決し、アーキテクチャ固有のファイルにsendfileの実装を分離することで、この問題を解消しています。

コミット

commit 7b6541a5932d72781e215b2e8e77928ec7ef6839
Author: David G. Andersen <dave.andersen@gmail.com>
Date:   Tue Jul 3 08:16:43 2012 -0700

    syscall: fix FreeBSD 386 sendfile
    The previous version was not handling an off_t (64 bit)
    parameter on 32 bit i386 systems.  This patch splits sendfile
    into two implementations in their respective arch-specific files.
    Tested on FreeBSD amd64 and i386.
    
    R=bradfitz
    CC=golang-dev
    https://golang.org/cl/6356048

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

https://github.com/golang/go/commit/7b6541a5932d72781e215b2e8e77928ec7ef6839

元コミット内容

このコミットは、Go言語のsyscallパッケージにおいて、FreeBSDオペレーティングシステム上の32ビットi386アーキテクチャでsendfileシステムコールが正しく機能しない問題を修正するものです。以前の実装では、sendfileシステムコールが受け取るoffsetパラメータが64ビットのoff_t型であるにもかかわらず、32ビットシステム上でその64ビット値を適切に扱えていませんでした。

この修正は、sendfileの実装をアーキテクチャ固有のファイル(syscall_freebsd_386.gosyscall_freebsd_amd64.go)に分離することで、この問題を解決しています。これにより、各アーキテクチャの特性に合わせたsendfileの実装が可能となり、特に32ビットi386システムでのoff_tの扱いが改善されました。この変更はFreeBSDのamd64とi386の両方でテストされ、動作が確認されています。

変更の背景

この変更の背景には、システムコールにおけるデータ型のサイズとアーキテクチャのビット幅の不一致があります。sendfileシステムコールは、ファイルディスクリプタ間でデータを直接転送するための効率的なメカニズムを提供します。このシステムコールは、転送を開始するファイル内のオフセットを指定するためにoff_t型のパラメータを受け取ります。

問題は、FreeBSDの32ビットi386システムにおいて、off_t型が64ビット幅であるにもかかわらず、Goのsyscallパッケージの汎用的な実装がこの64ビット値を32ビットレジスタに収まるように扱おうとしていた点にありました。これにより、offsetが32ビットで表現できる範囲を超えると、正しくないオフセットがシステムコールに渡され、予期せぬ動作やエラーが発生していました。

この問題を解決するためには、32ビットアーキテクチャと64ビットアーキテクチャでsendfileシステムコールへの引数の渡し方を区別する必要がありました。特に、64ビット値を32ビットシステムコールに渡す際には、上位32ビットと下位32ビットに分割して渡すなどの特別な処理が必要になります。

前提知識の解説

システムコール (Syscall)

システムコールは、オペレーティングシステム (OS) のカーネルが提供するサービスを、ユーザー空間のプログラムが利用するためのインターフェースです。ファイル操作(読み書き)、プロセス管理(作成、終了)、ネットワーク通信など、OSの機能にアクセスする際に使用されます。Go言語のsyscallパッケージは、これらのシステムコールをGoプログラムから直接呼び出すための機能を提供します。

sendfile システムコール

sendfileは、あるファイルディスクリプタから別のファイルディスクリプタへデータを直接転送するためのシステムコールです。通常、ファイルを読み込んでネットワークソケットに書き込む場合、データはカーネル空間からユーザー空間へ、そして再びカーネル空間へとコピーされます。sendfileを使用すると、このユーザー空間へのコピーを省略し、カーネル空間内で直接データ転送を行うため、I/Oの効率が向上し、CPUオーバーヘッドが削減されます。これは特に、静的ファイルのWebサーバーなどで頻繁に利用されます。

sendfileの一般的なシグネチャは以下のようになります(FreeBSDの場合、引数の順序や型は異なることがあります): int sendfile(int fd, int s, off_t offset, size_t nbytes, struct sf_hdtr *hdtr, off_t *sbytes, int flags);

  • fd: 入力ファイルディスクリプタ(通常はファイル)
  • s: 出力ファイルディスクリプタ(通常はソケット)
  • offset: 転送を開始する入力ファイル内のオフセット。このパラメータがoff_t型です。
  • nbytes: 転送するバイト数。
  • sbytes: 実際に転送されたバイト数を格納するポインタ。

off_t

off_tは、ファイルサイズやファイルオフセットを表すために使用されるデータ型です。そのサイズはシステムやアーキテクチャによって異なりますが、多くのUnix系システムでは、32ビットシステムでは32ビット、64ビットシステムでは64ビットとして定義されることが一般的です。しかし、FreeBSDの32ビットi386システムでは、ファイルサイズが2GBを超えるファイルを扱えるようにするため、off_tが64ビットとして定義されています。これが、今回のバグの根本原因となりました。32ビットのレジスタしか持たないCPUで64ビットの値を扱う場合、通常は2つの32ビットレジスタに分割して扱う必要があります。

Syscall9

Go言語のsyscallパッケージでは、SyscallSyscall6Syscall9などの関数が提供されており、これらはそれぞれ異なる数の引数を持つシステムコールを呼び出すために使用されます。Syscall9は最大9つの引数をシステムコールに渡すことができる関数です。これらの関数は、Goの型をCのシステムコールが期待するuintptr型に変換して渡します。

unsafe.Pointer

Go言語のunsafeパッケージは、Goの型システムが提供する安全性をバイパスする機能を提供します。unsafe.Pointerは、任意の型のポインタを保持できる特殊なポインタ型で、ポインタ演算を可能にします。システムコールにポインタを渡す場合など、低レベルの操作が必要な場面で利用されますが、Goのメモリ安全性を損なう可能性があるため、慎重に使用する必要があります。

技術的詳細

このコミットの技術的な核心は、32ビットi386アーキテクチャにおける64ビットoff_tの扱い方です。

元のsrc/pkg/syscall/syscall_freebsd.goでは、Sendfile関数がSyscall9を呼び出す際に、offsetパラメータをuintptr(*offset)として直接渡していました。

// src/pkg/syscall/syscall_freebsd.go (変更前)
func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err error) {
	var writtenOut uint64 = 0
	_, _, e1 := Syscall9(SYS_SENDFILE, uintptr(infd), uintptr(outfd), uintptr(*offset), uintptr(count), 0, uintptr(unsafe.Pointer(&writtenOut)), 0, 0, 0)
	// ...
}

ここで問題となるのは、*offsetint64(64ビット)であるにもかかわらず、uintptrにキャストされる点です。32ビットシステムでは、uintptrは32ビット幅です。したがって、uintptr(*offset)とすると、*offsetの64ビット値の上位32ビットが切り捨てられてしまい、システムコールには誤ったオフセットが渡されていました。

この修正では、sendfileの実装をアーキテクチャ固有のファイルに分離し、32ビットi386アーキテクチャ向けのsyscall_freebsd_386.gooffsetの扱いを変更しています。

// src/pkg/syscall/syscall_freebsd_386.go (変更後)
func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err error) {
	var writtenOut uint64 = 0
	_, _, e1 := Syscall9(SYS_SENDFILE, uintptr(infd), uintptr(outfd), uintptr(*offset), uintptr((*offset)>>32), uintptr(count), 0, uintptr(unsafe.Pointer(&writtenOut)), 0, 0)
	// ...
}

注目すべきは、Syscall9の引数リストにuintptr((*offset)>>32)が追加されている点です。これは、64ビットの*offset値を2つの32ビット値に分割し、それぞれをシステムコールに渡すための一般的な手法です。*offsetの下位32ビットはuintptr(*offset)として渡され、上位32ビットはuintptr((*offset)>>32)として渡されます。FreeBSDの32ビットi386カーネルは、sendfileシステムコールにおいて、この2つの32ビット引数を組み合わせて64ビットのoff_tとして解釈するように設計されています。

一方、64ビットのamd64アーキテクチャでは、uintptrも64ビット幅であるため、uintptr(*offset)とすることで64ビット値全体が正しく渡されます。そのため、syscall_freebsd_amd64.goでは元のコードとほぼ同じ形式が維持されています。

// src/pkg/syscall/syscall_freebsd_amd64.go (変更後)
func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err error) {
	var writtenOut uint64 = 0
	_, _, e1 := Syscall9(SYS_SENDFILE, uintptr(infd), uintptr(outfd), uintptr(*offset), uintptr(count), 0, uintptr(unsafe.Pointer(&writtenOut)), 0, 0)
	// ...
}

このように、アーキテクチャごとにsendfileの実装を分離し、それぞれのアーキテクチャの特性(特にポインタや整数型のサイズ)に合わせてシステムコールへの引数の渡し方を調整することで、クロスプラットフォームでの正確な動作を保証しています。

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

このコミットによる主要なコード変更は以下の3つのファイルにわたります。

  1. src/pkg/syscall/syscall_freebsd.go:

    • 汎用的なSendfile関数の実装が削除されました。このファイルは、アーキテクチャ固有ではないFreeBSDのシステムコール関連の定義を保持する役割に特化されます。
  2. src/pkg/syscall/syscall_freebsd_386.go:

    • Sendfile関数の新しい実装が追加されました。この実装は、32ビットi386アーキテクチャの特性に合わせて、64ビットのoffsetパラメータを2つの32ビット値に分割してSyscall9に渡すように修正されています。
    • import "unsafe"が追加されました。
  3. src/pkg/syscall/syscall_freebsd_amd64.go:

    • Sendfile関数の新しい実装が追加されました。この実装は、64ビットamd64アーキテクチャの特性に合わせて、64ビットのoffsetパラメータを直接Syscall9に渡すように記述されています。元の実装と論理的には同じですが、ファイルが分離されたことでアーキテクチャ固有のコードとして明確になりました。
    • import "unsafe"が追加されました。

src/pkg/syscall/syscall_freebsd.go からの削除

--- a/src/pkg/syscall/syscall_freebsd.go
+++ b/src/pkg/syscall/syscall_freebsd.go
@@ -89,18 +89,6 @@ func Pipe(p []int) (err error) {
 	return
 }
 
-func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err error) {
-	var writtenOut uint64 = 0
-	_, _, e1 := Syscall9(SYS_SENDFILE, uintptr(infd), uintptr(outfd), uintptr(*offset), uintptr(count), 0, uintptr(unsafe.Pointer(&writtenOut)), 0, 0, 0)
-
-	written = int(writtenOut)
-
-	if e1 != 0 {
-		err = e1
-	}
-	return
-}
-
 func GetsockoptIPMreqn(fd, level, opt int) (*IPMreqn, error) {
 	var value IPMreqn
 	vallen := _Socklen(SizeofIPMreqn)

src/pkg/syscall/syscall_freebsd_386.go への追加

--- a/src/pkg/syscall/syscall_freebsd_386.go
+++ b/src/pkg/syscall/syscall_freebsd_386.go
@@ -4,6 +4,8 @@
 
 package syscall
 
+import "unsafe"
+
 func Getpagesize() int { return 4096 }
 
 func TimespecToNsec(ts Timespec) int64 { return int64(ts.Sec)*1e9 + int64(ts.Nsec) }\n@@ -41,4 +43,16 @@ func (cmsg *Cmsghdr) SetLen(length int) {
 	cmsg.Len = uint32(length)
 }
 
+func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err error) {
+	var writtenOut uint64 = 0
+	_, _, e1 := Syscall9(SYS_SENDFILE, uintptr(infd), uintptr(outfd), uintptr(*offset), uintptr((*offset)>>32), uintptr(count), 0, uintptr(unsafe.Pointer(&writtenOut)), 0, 0)
+
+	written = int(writtenOut)
+
+	if e1 != 0 {
+		err = e1
+	}
+	return
+}
+
 func Syscall9(num, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err Errno) // sic

src/pkg/syscall/syscall_freebsd_amd64.go への追加

--- a/src/pkg/syscall/syscall_freebsd_amd64.go
+++ b/src/pkg/syscall/syscall_freebsd_amd64.go
@@ -4,6 +4,8 @@
 
 package syscall
 
+import "unsafe"
+
 func Getpagesize() int { return 4096 }
 
 func TimespecToNsec(ts Timespec) int64 { return int64(ts.Sec)*1e9 + int64(ts.Nsec) }\n@@ -41,4 +43,16 @@ func (cmsg *Cmsghdr) SetLen(length int) {
 	cmsg.Len = uint32(length)
 }
 
+func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err error) {
+	var writtenOut uint64 = 0
+	_, _, e1 := Syscall9(SYS_SENDFILE, uintptr(infd), uintptr(outfd), uintptr(*offset), uintptr(count), 0, uintptr(unsafe.Pointer(&writtenOut)), 0, 0)
+
+	written = int(writtenOut)
+
+	if e1 != 0 {
+		err = e1
+	}
+	return
+}
+
 func Syscall9(num, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err Errno)

コアとなるコードの解説

このコミットの核心は、syscall_freebsd_386.goにおけるSendfile関数の実装です。

func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err error) {
	var writtenOut uint64 = 0
	// ここが重要な変更点
	_, _, e1 := Syscall9(SYS_SENDFILE, uintptr(infd), uintptr(outfd), uintptr(*offset), uintptr((*offset)>>32), uintptr(count), 0, uintptr(unsafe.Pointer(&writtenOut)), 0, 0)

	written = int(writtenOut)

	if e1 != 0 {
		err = e1
	}
	return
}

ここで注目すべきは、Syscall9の第4引数と第5引数です。

  • uintptr(*offset): これはoffsetポインタが指すint64値の下位32ビット(またはシステムが32ビットの場合の全32ビット)をuintptrにキャストして渡します。
  • uintptr((*offset)>>32): これはoffsetポインタが指すint64値の上位32ビットを右シフトして抽出し、それをuintptrにキャストして渡します。

FreeBSDの32ビットi386カーネルでは、sendfileシステムコールは64ビットのオフセットを2つの32ビット引数として受け取るように設計されています。具体的には、下位32ビットが最初の引数として、上位32ビットが次の引数として渡されることを期待します。この修正は、まさにその期待に応える形で引数を渡しています。

一方、syscall_freebsd_amd64.goでは、uintptr(*offset)のみが渡されます。これは、amd64アーキテクチャではuintptrが64ビット幅であり、int64offset値全体を単一のuintptr引数としてシステムコールに渡すことができるためです。

この変更により、Goのsyscallパッケージは、FreeBSDの異なるアーキテクチャ(32ビットi386と64ビットamd64)間でsendfileシステムコールを正しく呼び出すことができるようになり、特に32ビットi386システムでの64ビットオフセットの扱いに関するバグが解消されました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特にsrc/pkg/syscallディレクトリ): https://github.com/golang/go/tree/master/src/pkg/syscall
  • FreeBSDのシステムコールに関するドキュメントやソースコード (特にsys/syscall.hやカーネルのsys/kern/vfs_syscalls.cなど、sendfileの実装に関連する部分)
  • 32ビットシステムにおける64ビット値の扱いに関する一般的な情報 (例: x86アーキテクチャの呼び出し規約、システムコールインターフェース)
  • Go言語のunsafeパッケージに関するドキュメント: https://pkg.go.dev/unsafe
  • Go言語のuintptr型に関するドキュメント: https://pkg.go.dev/builtin#uintptr
  • Go言語のint64型に関するドキュメント: https://pkg.go.dev/builtin#int64
  • Go言語のシステムプログラミングに関する記事やチュートリアル (例: Goにおけるシステムコールの呼び出し方)
  • Stack Overflowや技術ブログでのsendfileoff_t、32ビット/64ビットアーキテクチャ間のデータ型変換に関する議論。
  • Go言語のメーリングリスト (golang-dev) のアーカイブ (コミットメッセージにCC=golang-devとあるため、関連する議論が残っている可能性があります)。
  • Go言語のIssue Tracker (Go Bug Tracker) (関連するバグ報告や議論がある可能性があります)。
  • FreeBSDのハンドブックや開発者向けドキュメント。
  • manページ (特にsendfile(2)syscall(2)types(3)など)。
  • Go言語のコンパイラとランタイムの内部動作に関する情報 (特に、Goの型がシステムコールにどのようにマッピングされるか)。
  • Go言語のクロスコンパイルとアーキテクチャ固有のコード生成に関する情報。
  • Go言語のbuild tags (例: //go:build freebsd,386) の使用方法に関する情報。