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

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

このコミットは、GoランタイムがFreeBSD/amd64環境で直面していたカーネルバグへのワークアラウンドを実装したものです。具体的には、システムコールを再開する際のSYSCALL命令の不具合を回避するため、より堅牢なINT $0x80命令に切り替える変更が行われました。

コミット

commit 555da73c566c156a6982da0e06d49c71f9ea25d5
Author: Russ Cox <rsc@golang.org>
Date:   Mon Sep 16 14:04:32 2013 -0400

    runtime, syscall: work around FreeBSD/amd64 kernel bug
    
    The kernel implementation of the fast system call path,
    the one invoked by the SYSCALL instruction, is broken for
    restarting system calls. A C program demonstrating this is below.
    
    Change the system calls to use INT $0x80 instead, because
    that (perhaps slightly slower) system call path actually works.
    
    I filed http://www.freebsd.org/cgi/query-pr.cgi?pr=182161.
    
    The C program demonstrating that it is FreeBSD's fault is below.
    It reports the same "Bad address" failures from wait.
    
    #include <sys/time.h>
    #include <sys/signal.h>
    #include <pthread.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    
    static void handler(int);
    static void* looper(void*);
    
    int
    main(void)
    {
            int i;
            struct sigaction sa;
            pthread_cond_t cond;
            pthread_mutex_t mu;
    
            memset(&sa, 0, sizeof sa);
            sa.sa_handler = handler;
            sa.sa_flags = SA_RESTART;
            memset(&sa.sa_mask, 0xff, sizeof sa.sa_mask);
            sigaction(SIGCHLD, &sa, 0);
    
            for(i=0; i<2; i++)
                    pthread_create(0, 0, looper, 0);
    
            pthread_mutex_init(&mu, 0);
            pthread_mutex_lock(&mu);
            pthread_cond_init(&cond, 0);
            for(;;)
                    pthread_cond_wait(&cond, &mu);
    
            return 0;
    }
    
    static void
    handler(int sig)
    {
    }
    
    int
    mywait4(int pid, int *stat, int options, struct rusage *rusage)
    {
            int result;
    
            asm("movq %%rcx, %%r10; syscall"
                    : "=a" (result)
                    : "a" (7),
                      "D" (pid),
                      "S" (stat),
                      "d" (options),
                      "c" (rusage));
    }
    
    static void*
    looper(void *v)
    {
            int pid, stat, out;
            struct rusage rusage;
    
            for(;;) {
                    if((pid = fork()) == 0)
                            _exit(0);
                    out = mywait4(pid, &stat, 0, &rusage);
                    if(out != pid) {
                            printf("wait4 returned %d\n", out);
                    }
            }
    }
    
    Fixes #6372.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/13582047

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

https://github.com/golang/go/commit/555da73c566c156a6982da0e06d49c71f9ea25d5

元コミット内容

このコミットは、Goランタイムとsyscallパッケージにおいて、FreeBSD/amd64カーネルのバグを回避するための変更を導入しています。具体的には、SYSCALL命令を用いた高速なシステムコールパスが、システムコール再開時に正しく機能しないという問題に対処しています。この問題は、シグナルがシステムコール実行中に到着し、カーネルがシステムコールを再開しようとする際に、レジスタの状態が正しく復元されないことに起因します。

コミットメッセージには、このバグを再現するためのC言語のプログラムが含まれており、wait4システムコールが「Bad address」エラーを報告することが示されています。このCプログラムは、SYSCALL命令をインラインアセンブリで直接呼び出すmywait4関数を使用しており、FreeBSDカーネルの不具合を明確に示しています。

解決策として、GoランタイムはSYSCALL命令の代わりにINT $0x80命令を使用するように変更されました。INT $0x80は、SYSCALLよりもわずかに遅い可能性がありますが、システムコール再開時のレジスタ復元が正しく行われるため、このバグを回避できます。

この変更は、Go issue #6372 に関連しています。

変更の背景

この変更の背景には、FreeBSD/amd64カーネルにおける特定のバグが存在します。Goプログラムがシステムコール(特にwait4のような、シグナルによって中断され再開される可能性のあるシステムコール)を実行する際、カーネルのSYSCALL命令の実装に問題がありました。

具体的には、SYSCALL命令を使用してシステムコールを呼び出し、そのシステムコールが内部的にERESTARTを返してカーネルがPC(プログラムカウンタ)を巻き戻し、システムコールを再実行しようとする場合、カーネルは一部のレジスタ(特に第4引数を保持するR10レジスタ)を正しく復元しませんでした。FreeBSD 9では、第5引数(R8)と第6引数(R9)も同様に復元されない問題がありました。この不具合は、FreeBSDのamd64/amd64/exception.Sにあるfast_syscallの実装に起因しており、システムコールからの復帰時にDI, SI, DX, AX, RFLAGSのみを復元していました。

この問題により、Goプログラムがwait4などのシステムコールを呼び出すと、不正な引数が渡され、「Bad address」のようなエラーが発生する可能性がありました。これはGoプログラムの安定性と信頼性に直接影響を与えるため、Goランタイム側でこのカーネルバグを回避する必要がありました。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. システムコール (System Call): オペレーティングシステムが提供するサービス(ファイルI/O、メモリ管理、プロセス管理など)をユーザープログラムが利用するためのインターフェースです。ユーザーモードのプログラムは直接ハードウェアにアクセスできないため、システムコールを通じてカーネルモードに移行し、カーネルに処理を依頼します。

  2. x86-64アーキテクチャにおけるシステムコール呼び出し規約: x86-64アーキテクチャでは、システムコールを呼び出すための複数のメカニズムがあります。

    • SYSCALL命令: Intelが導入した高速なシステムコール命令です。レジスタ渡しで引数を渡し、高速にカーネルモードに移行します。Linuxでは主にこの命令が使用されます。FreeBSD/amd64では、システムコール番号はRAXに、引数はRDI, RSI, RDX, R10, R8, R9に渡されます。
    • INT $0x80命令: 従来のソフトウェア割り込みによるシステムコール呼び出し方法です。SYSCALL命令よりもオーバーヘッドが大きいですが、より広範な互換性があります。FreeBSD/amd64では、システムコール番号はRAXに、引数はRDI, RSI, RDX, RCX, R8, R9に渡されます。このコミットで言及されているように、INT $0x80パスでは第3引数がR10ではなくRCXに渡されるという違いがあります。
  3. システムコール再開 (Restarting System Calls): 一部のシステムコール(例: read, write, wait4など)は、実行中にシグナルを受信すると中断されることがあります。カーネルは、これらのシステムコールが中断された後に自動的に再開するメカニズムを提供することがあります。これは、システムコールがEINTRエラーを返す代わりに、透過的に再実行されるようにするものです。この再開処理中に、カーネルがレジスタの状態を正しく復元しないと問題が発生します。

  4. レジスタの役割 (x86-64): x86-64アーキテクチャでは、関数呼び出し規約(System V AMD64 ABI)に従って、引数は特定のレジスタに渡されます。

    • RDI, RSI, RDX, RCX, R8, R9 は、それぞれ第1引数から第6引数までを保持するために使用されます。
    • RAX は、システムコール番号や関数の戻り値を保持するために使用されます。
    • R10 は、SYSCALL命令を使用する際に第4引数を保持するために使用されることがあります(Linuxのシステムコール規約ではR10が第4引数、FreeBSDのSYSCALL規約でもR10が第4引数)。しかし、INT $0x80ではRCXが第4引数として使われることがあります。
  5. FreeBSDのカーネル実装: FreeBSDカーネルのamd64/amd64/exception.Sにあるfast_syscallルーチンは、SYSCALL命令によるシステムコール処理を担当します。このルーチンが、システムコール再開時に一部のレジスタを正しく復元しないというバグを抱えていました。

技術的詳細

このコミットが対処している問題は、FreeBSD/amd64カーネルのSYSCALL命令の実装における特定のバグです。このバグは、システムコールがシグナルによって中断され、カーネルがそのシステムコールを再開しようとする際に顕在化します。

通常、システムコールが中断されると、カーネルはERESTARTのような内部エラーコードを返し、プログラムカウンタ (PC) をシステムコール命令の直前に戻して、システムコールを再実行させます。この再実行の際に、カーネルはシステムコールの引数を保持していたレジスタの状態を完全に復元する必要があります。

しかし、FreeBSD 8、FreeBSD 9、およびそれ以前のバージョンにおけるSYSCALL命令の高速パス(amd64/amd64/exception.Sfast_syscall)では、このレジスタ復元が不完全でした。具体的には、第4引数を保持するR10レジスタが復元されませんでした。FreeBSD 9ではさらに、第5引数(R8)と第6引数(R9)も復元されないという問題がありました。これにより、再開されたシステムコールは不正な引数で実行され、予期せぬ動作やエラー(例: wait4からの「Bad address」)を引き起こしていました。

この問題は、http://fxr.watson.org/fxr/source/amd64/amd64/exception.S?v=FREEBSD91#L399 で示されているように、fast_syscallがシステムコールからの復帰時にDI, SI, DX, AX, RFLAGSのみを復元し、他の引数レジスタを無視していたことに起因します。

Goランタイムは、このカーネルバグを回避するために、システムコール呼び出しの方法を変更しました。具体的には、高速だがバグのあるSYSCALL命令の使用を避け、代わりにINT $0x80命令を使用するようにしました。INT $0x80命令は、ソフトウェア割り込みを介してシステムコールを呼び出す従来のメカニズムです。このパス(amd64/ia32/ia32_exception.Sint0x80_syscall)は、レジスタの復元が正しく行われるため、このバグの影響を受けません。

ただし、INT $0x80パスはSYSCALLパスとは異なる引数渡し規約を持つ場合があります。特に、第3引数がR10ではなくRCXに渡されることがあります。このため、Goのシステムコールアセンブリコードをすべて書き直すのではなく、SYSCALLマクロを再定義することで、この違いを吸収し、既存のコードベースへの影響を最小限に抑えるアプローチが取られました。

この変更により、GoプログラムはFreeBSD/amd64環境でシステムコールを安定して実行できるようになり、カーネルバグによるクラッシュや不正な動作が回避されます。INT $0x80SYSCALLよりもわずかに遅い可能性がありますが、正確性が優先されました。

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

このコミットによる主要なコード変更は、以下の2つのアセンブリファイルに集中しています。

  1. src/pkg/runtime/sys_freebsd_amd64.s
  2. src/pkg/syscall/asm_freebsd_amd64.s

両ファイルで、SYSCALLというマクロが再定義されています。

変更前: (明示的なSYSCALLマクロの定義は示されていないが、おそらくGoのアセンブラがデフォルトでSYSCALL命令に展開していたか、直接SYSCALL命令を使用していた。)

変更後:

--- a/src/pkg/runtime/sys_freebsd_amd64.s
+++ b/src/pkg/runtime/sys_freebsd_amd64.s
@@ -8,6 +8,31 @@
 #include "zasm_GOOS_GOARCH.h"
 #include "../../cmd/ld/textflag.h"
 
+// FreeBSD 8, FreeBSD 9, and older versions that I have checked
+// do not restore R10 on exit from a "restarted" system call
+// if you use the SYSCALL instruction. This means that, for example,
+// if a signal arrives while the wait4 system call is executing,
+// the wait4 internally returns ERESTART, which makes the kernel
+// back up the PC to execute the SYSCALL instruction a second time.
+// However, since the kernel does not restore R10, the fourth
+// argument to the system call has been lost. (FreeBSD 9 also fails
+// to restore the fifth and sixth arguments, R8 and R9, although
+// some earlier versions did restore those correctly.)
+// The broken code is in fast_syscall in FreeBSD's amd64/amd64/exception.S.
+// It restores only DI, SI, DX, AX, and RFLAGS on system call return.
+// http://fxr.watson.org/fxr/source/amd64/amd64/exception.S?v=FREEBSD91#L399
+//
+// The INT $0x80 system call path (int0x80_syscall in FreeBSD's 
+// amd64/ia32/ia32_exception.S) does not have this problem,
+// but it expects the third argument in R10. Instead of rewriting
+// all the assembly in this file, #define SYSCALL to a safe simulation
+// using INT $0x80.
+//
+// INT $0x80 is a little slower than SYSCALL, but correctness wins.
+//
+// See golang.org/issue/6372.
+#define SYSCALL MOVQ R10, CX; INT $0x80
 	
 TEXT runtime·sys_umtx_op(SB),NOSPLIT,$0
 	MOVQ 8(SP), DI
--- a/src/pkg/syscall/asm_freebsd_amd64.s
+++ b/src/pkg/syscall/asm_freebsd_amd64.s
@@ -8,6 +8,11 @@
 // System call support for AMD64, FreeBSD
 //
 
+// The SYSCALL variant for invoking system calls is broken in FreeBSD.
+// See comment at top of ../runtime/sys_freebsd_amd64.c and
+// golang.org/issue/6372.
+#define SYSCALL MOVQ R10, CX; INT $0x80
+// func Syscall(trap int64, a1, a2, a3 int64) (r1, r2, err int64);
 // func Syscall6(trap int64, a1, a2, a3, a4, a5, a6 int64) (r1, r2, err int64);
 // func Syscall9(trap int64, a1, a2, a3, a4, a5, a6, a7, a8, a9 int64) (r1, r2, err int64)

コアとなるコードの解説

このコミットの核心は、SYSCALLマクロの再定義にあります。

#define SYSCALL MOVQ R10, CX; INT $0x80

この一行が、FreeBSD/amd64カーネルのバグを回避するための主要な変更です。

  1. MOVQ R10, CX: これは、SYSCALL命令を使用する際の引数渡し規約と、INT $0x80命令を使用する際の引数渡し規約の違いを吸収するためのものです。

    • FreeBSD/amd64のSYSCALL規約では、システムコールの第4引数はR10レジスタに渡されます。
    • しかし、INT $0x80規約では、第4引数はRCXレジスタに渡されることが一般的です(または、GoのアセンブリコードがRCXを期待するように書かれている)。 このMOVQ R10, CX命令は、Goのアセンブリコードが既に第4引数をR10にロードしていると仮定し、その値をRCXに移動させることで、INT $0x80命令が正しい引数を受け取れるように調整しています。これにより、既存のGoのアセンブリコードを大幅に書き換えることなく、システムコール呼び出しメカニズムを切り替えることが可能になります。
  2. INT $0x80: これは、ソフトウェア割り込み番号0x80を発生させる命令です。この割り込みは、FreeBSD/amd64カーネルにおいてシステムコールを処理するためのエントリポイントとして機能します。前述の通り、INT $0x80パスはSYSCALLパスとは異なり、システムコール再開時のレジスタ復元が正しく行われるため、カーネルのバグの影響を受けません。

このマクロの導入により、Goランタイムとsyscallパッケージ内のすべてのアセンブリコードでSYSCALLマクロが使用されている箇所は、自動的にMOVQ R10, CX; INT $0x80という命令シーケンスに展開されるようになります。これにより、GoプログラムはFreeBSD/amd64上で安定して動作し、カーネルのバグによる問題から保護されます。

コメントブロックには、この変更の理由と、FreeBSDカーネルのバグに関する詳細な説明が含まれています。特に、fast_syscallルーチンがR10(およびR8, R9)を復元しないこと、そしてINT $0x80パスがこの問題を抱えていないことが明記されています。また、パフォーマンスのトレードオフ(INT $0x80がわずかに遅い可能性)についても言及されていますが、正確性が優先されたことが強調されています。

関連リンク

  • Go issue #6372: https://golang.org/issue/6372
  • FreeBSD Bug Report pr/182161: http://www.freebsd.org/cgi/query-pr.cgi?pr=182161
  • Go CL 13582047: https://golang.org/cl/13582047

参考にした情報源リンク

  • FreeBSD exception.S source code (FreeBSD 9.1): http://fxr.watson.org/fxr/source/amd64/amd64/exception.S?v=FREEBSD91#L399
  • System V Application Binary Interface AMD64 Architecture Processor Supplement: https://refspecs.linuxfoundation.org/elf/x86-64-abi-0.99.pdf (x86-64の呼び出し規約に関する一般的な情報)
  • Go Assembly Language (Goのアセンブリに関する一般的な情報): https://go.dev/doc/asm
  • Go runtime source code (Goのランタイムに関する一般的な情報): https://github.com/golang/go/tree/master/src/runtime
  • Go syscall source code (Goのsyscallに関する一般的な情報): https://github.com/golang/go/tree/master/src/syscall