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

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

このコミットは、Go言語のランタイムにおける低レベルなシステムコールが予期せぬ失敗をした際にプログラムをクラッシュさせるnotok関数の呼び出しをインライン化する変更です。これにより、クラッシュ発生時のプログラムカウンタがより正確な位置を指すようになり、デバッグ情報が向上します。

コミット

commit 36aa7d4d14c1dbca2405e265b8bbf1260e9d825c
Author: Russ Cox <rsc@golang.org>
Date:   Thu Mar 8 14:03:56 2012 -0500

    runtime: inline calls to notok
    
    When a very low-level system call that should never fail
    does fail, we call notok, which crashes the program.
    Often, we are then left with only the program counter as
    information about the crash, and it is in notok.
    Instead, inline calls to notok (it is just one instruction
    on most systems) so that the program counter will
    tell us which system call is unhappy.
    
    R=golang-dev, gri, minux.ma, bradfitz
    CC=golang-dev
    https://golang.org/cl/5792048

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

https://github.com/golang/go/commit/36aa7d4d14c1dbca2405e265b8bbf1260e9d825c

元コミット内容

Goランタイムにおいて、決して失敗しないはずの低レベルなシステムコールが失敗した場合、notok関数が呼び出され、プログラムがクラッシュします。しかし、このクラッシュ情報にはプログラムカウンタしか含まれておらず、そのプログラムカウンタがnotok関数内を指しているため、どのシステムコールが問題を引き起こしたのかを特定するのが困難でした。

このコミットでは、notok関数への呼び出しをインライン化することで、クラッシュ発生時のプログラムカウンタが、問題のあるシステムコールを呼び出した直後の命令を指すように変更します。これにより、デバッグ時にどのシステムコールが予期せぬ失敗をしたのかをより正確に特定できるようになります。

変更の背景

Go言語のランタイムは、オペレーティングシステムと直接やり取りする低レベルなシステムコールを多数利用しています。これらのシステムコールの中には、設計上「決して失敗しない」と想定されているものがあります。例えば、メモリの確保やスレッドの終了など、OSの基本的な機能に関わるものです。しかし、OSのバグ、ハードウェアの問題、あるいは予期せぬ環境要因によって、これらのシステムコールが稀に失敗する可能性があります。

このような「ありえない」失敗が発生した場合、Goランタイムはプログラムの整合性を保つために即座にクラッシュさせる必要があります。従来、このクラッシュ処理はnotokという独立した関数で行われていました。しかし、クラッシュダンプやデバッガでスタックトレースを確認すると、常にnotok関数内でクラッシュしているように見え、実際にどのシステムコールが失敗したのかという根本原因を特定するのが困難でした。

開発者にとって、クラッシュの原因を特定することはデバッグの第一歩です。notok関数がクラッシュの直接の原因を隠蔽してしまうことは、デバッグ効率を著しく低下させる問題でした。このコミットは、このデバッグ情報の不足という課題を解決するために提案されました。

前提知識の解説

  • Goランタイム (Go Runtime): Goプログラムの実行を管理する部分です。ガベージコレクション、スケジューラ、システムコールインターフェースなど、Goプログラムが動作するために必要な低レベルな機能を提供します。アセンブリ言語で書かれた部分も多く含まれます。
  • システムコール (System Call): オペレーティングシステムが提供するサービスをプログラムが利用するためのインターフェースです。ファイルI/O、メモリ管理、プロセス制御など、OSのカーネルモードで実行される特権的な操作を行います。
  • アセンブリ言語 (Assembly Language): コンピュータのプロセッサが直接理解できる機械語に非常に近い低レベルなプログラミング言語です。特定のCPUアーキテクチャ(例: x86-64, ARM)に特化しており、ハードウェアを直接制御する際に使用されます。Goランタイムの多くの部分は、パフォーマンスとOSとの直接的な連携のためにアセンブリで書かれています。
  • プログラムカウンタ (Program Counter, PC): CPUのレジスタの一つで、次に実行される命令のアドレスを保持しています。プログラムがクラッシュした際、このプログラムカウンタの値は、クラッシュが発生した場所を示す重要な情報となります。
  • インライン化 (Inlining): コンパイラ最適化の一種で、関数呼び出しをその関数の本体のコードで直接置き換えることです。これにより、関数呼び出しのオーバーヘッド(スタックフレームの作成、レジスタの保存・復元など)が削減され、パフォーマンスが向上する可能性があります。このコミットの文脈では、デバッグ情報の改善が主な目的です。
  • MOVL $0xf1, 0xf1: x86/x86-64アーキテクチャのアセンブリ命令です。0xf1という値をレジスタ(この場合は即値として扱われる)に移動させようとしていますが、これは通常、不正なメモリアドレスへの書き込みを意図的に引き起こすことで、プログラムをクラッシュさせるための命令として使われます。0xf1は、デバッグ時に特定のクラッシュポイントを識別するためのマジックナンバーとして機能することもあります。

技術的詳細

この変更の核心は、Goランタイム内のアセンブリコードにおいて、CALL runtime·notok(SB)という命令を、直接クラッシュを引き起こすアセンブリ命令に置き換えることです。

従来のGoランタイムでは、低レベルなシステムコールが失敗した場合、エラーハンドリングの一部としてruntime·notokという関数を呼び出していました。このnotok関数自体は、例えばMOVL $0xf1, 0xf1のような命令を実行して、意図的に不正なメモリアクセスを引き起こし、プログラムをクラッシュさせていました。

問題は、クラッシュが発生した際に生成されるスタックトレースやデバッグ情報において、プログラムカウンタが常にruntime·notok関数の内部を指してしまう点にありました。これにより、開発者は「notokでクラッシュした」という情報しか得られず、そのnotokがどのシステムコールから呼び出されたのか、つまり「どのシステムコールが失敗したのか」という肝心な情報が失われていました。

このコミットでは、runtime·notok関数へのCALL命令を削除し、その代わりにnotok関数が内部で行っていたクラッシュ命令(例: MOVL $0xf1, 0xf1)を、システムコール呼び出しの直後に直接配置します。これにより、システムコールが失敗し、その直後にクラッシュ命令が実行された場合、プログラムカウンタは問題のシステムコール呼び出しの直後の命令を指すことになります。

例えば、以下のような変更が行われています(AMD64アーキテクチャの場合):

-	CALL	runtime·notok(SB)
+	MOVL	$0xf1, 0xf1  // crash

この変更は、Goがサポートする様々なOS(Darwin, FreeBSD, Linux, NetBSD, OpenBSD, Windows)とアーキテクチャ(386, AMD64, ARM)のアセンブリファイルにわたって適用されています。これにより、Goプログラムが低レベルなシステムコールで予期せぬ失敗をした際に、より詳細で有用なデバッグ情報が得られるようになります。

0xf1という値は、特定のデバッグツールやOSのクラッシュレポートにおいて、意図的なクラッシュを示すマーカーとして認識されることがあります。これにより、クラッシュが偶発的なものではなく、ランタイムによって意図的に引き起こされたものであることを示唆し、デバッグの方向性を明確にする助けにもなります。

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

このコミットは、Goランタイムの様々なOSおよびアーキテクチャ固有のアセンブリファイルにわたって、runtime·notok関数への呼び出しを直接的なクラッシュ命令に置き換えています。

主な変更は以下のファイルに見られます。

  • src/pkg/runtime/asm_amd64.s
  • src/pkg/runtime/sys_darwin_386.s
  • src/pkg/runtime/sys_darwin_amd64.s
  • src/pkg/runtime/sys_freebsd_386.s
  • src/pkg/runtime/sys_freebsd_amd64.s
  • src/pkg/runtime/sys_linux_amd64.s
  • src/pkg/runtime/sys_linux_arm.s
  • src/pkg/runtime/sys_netbsd_386.s
  • src/pkg/runtime/sys_netbsd_amd64.s
  • src/pkg/runtime/sys_openbsd_386.s
  • src/pkg/runtime/sys_openbsd_amd64.s
  • src/pkg/runtime/sys_windows_amd64.s (このファイルではnotok関数自体が削除されていますが、他のファイルと同様にCALL runtime·notok(SB)の置き換えが行われています)

これらのファイルでは、CALL runtime·notok(SB)というアセンブリ命令が削除され、代わりにMOVL $0xf1, 0xf1 // crash (x86/x86-64) や MOVW.HI $0, R9MOVW.HI R9, (R9) (ARM) のような、意図的にクラッシュを引き起こす命令が挿入されています。また、一部のファイルでは、独立したruntime·notok関数の定義自体が削除されています。

コアとなるコードの解説

例として、src/pkg/runtime/sys_darwin_amd64.sにおける変更を見てみましょう。

変更前:

TEXT runtime·exit(SB),7,$0
	MOVL	8(SP), DI		// arg 1 exit status
	MOVL	$(0x2000000+1), AX	// syscall entry
	SYSCALL
	CALL	runtime·notok(SB)
	RET

変更後:

TEXT runtime·exit(SB),7,$0
	MOVL	8(SP), DI		// arg 1 exit status
	MOVL	$(0x2000000+1), AX	// syscall entry
	SYSCALL
	MOVL	$0xf1, 0xf1  // crash
	RET

この例では、runtime·exit関数(プログラムを終了させるためのランタイム関数)内で、システムコール(SYSCALL)が実行された後に、もしシステムコールが失敗した場合に備えてruntime·notok(SB)が呼び出されていました。

変更後では、CALL runtime·notok(SB)が削除され、代わりにMOVL $0xf1, 0xf1 // crashという命令が直接挿入されています。

  • MOVL $0xf1, 0xf1: これは、即値0xf1をアドレス0xf1に移動させようとする命令です。通常、アドレス0xf1は有効なメモリ領域ではないため、この操作はページフォルトなどの例外を引き起こし、結果としてプログラムをクラッシュさせます。
  • // crash: これはコメントであり、この命令が意図的にクラッシュを引き起こすものであることを示しています。

この変更により、runtime·exit関数内でシステムコールが失敗し、その直後にMOVL $0xf1, 0xf1が実行された場合、クラッシュ時のプログラムカウンタはMOVL $0xf1, 0xf1命令を指します。これにより、デバッガでスタックトレースを確認した際に、runtime·exit関数内の特定のシステムコール呼び出しの直後で問題が発生したことが明確にわかるようになります。

同様の変更が、munmap, sigprocmask, sigaction, sigaltstackなど、他の低レベルなシステムコールを呼び出すランタイム関数にも適用されています。ARMアーキテクチャでは、MOVW.HI $0, R9MOVW.HI R9, (R9)の組み合わせが同様のクラッシュを引き起こすために使用されています。

関連リンク

  • Go言語のランタイムに関するドキュメント: https://go.dev/doc/go1.1 (Go 1.1のリリースノートなど、当時の情報源を探すのが良いでしょう)
  • Go言語のアセンブリについて: https://go.dev/doc/asm

参考にした情報源リンク

  • https://github.com/golang/go/commit/36aa7d4d14c1dbca2405e265b8bbf1260e9d825c
  • Go言語のソースコード(特にsrc/pkg/runtimeディレクトリ内のアセンブリファイル)
  • x86/x86-64アセンブリ命令セットに関する一般的な知識
  • オペレーティングシステムのシステムコールに関する一般的な知識
  • デバッグとクラッシュダンプ解析に関する一般的な知識
  • Go言語のメーリングリストやIssueトラッカー(当時の議論を追うことで、より深い背景がわかる可能性がありますが、今回は直接参照していません)