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

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

このコミットは、Goランタイムにおける古いクロージャ関連のコードを削除するものです。特に、Go 1.1の関数呼び出しに関する変更の一環として、ランタイムのトレースバック処理から特定のアーキテクチャ(ARM, x86)におけるクロージャの検出と処理ロジックが削除されています。これにより、Goのクロージャの実装がより効率的かつ統一されたものになったことを示唆しています。

コミット

commit a48ed66447d13d0a411114eaa987278ce90ab23b
Author: Russ Cox <rsc@golang.org>
Date:   Fri Feb 22 15:24:29 2013 -0500

    runtime: delete old closure code
    
    Step 4 of http://golang.org/s/go11func.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7393049

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

https://github.com/golang/go/commit/a48ed66447d13d0a411114eaa987278ce90ab23b

元コミット内容

runtime: delete old closure code

Step 4 of http://golang.org/s/go11func.

R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7393049

変更の背景

このコミットは、Go 1.1における関数呼び出しの内部実装変更の一環として行われました。コミットメッセージにある http://golang.org/s/go11func は、Go 1.1で導入された関数関連の変更、特にクロージャのランタイムコード生成を避けるための設計や提案に関連するものです。

Go言語の初期のバージョンでは、クロージャ(関数内で外部の変数を参照する関数)の実行には、特定のアーキテクチャ(ARM, x86など)において、ランタイムが動的にコードを生成したり、スタックトレース時に特殊な処理を行ったりする必要がありました。これは、クロージャが通常の関数とは異なる呼び出し規約やスタックフレーム構造を持つ可能性があったためです。

Go 1.1では、これらのクロージャの扱いが改善され、より効率的で統一的なメカニズムが導入されました。その結果、以前のバージョンで必要とされていた、特定のアーキテクチャに依存するクロージャ検出・処理ロジックが不要となり、削除されることになりました。このコミットは、その「古いクロージャコード」を削除する「Step 4」に該当します。

前提知識の解説

  • Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステム。ガベージコレクション、スケジューリング、スタック管理、システムコールなど、Go言語の並行処理やメモリ管理の基盤を提供します。
  • クロージャ (Closure): 関数が定義された環境(レキシカルスコープ)を記憶し、その環境内の変数を参照できる関数のこと。Go言語では、匿名関数が外部の変数をキャプチャすることでクロージャが作成されます。
  • スタックトレース (Stack Trace): プログラムの実行中にエラーやパニックが発生した際に、関数呼び出しの履歴(コールスタック)を表示する機能。デバッグ時に問題の原因を特定するために不可欠です。
  • traceback_arm.c, traceback_x86.c: Goランタイムのソースコードの一部で、それぞれARMアーキテクチャとx86アーキテクチャにおけるスタックトレースの生成ロジックを実装しています。これらのファイルは、特定のアーキテクチャのレジスタやスタックフレームの構造を理解し、関数呼び出しの連鎖を辿るために低レベルな処理を行います。
  • cacheflush: CPUのキャッシュ(特に命令キャッシュ)をフラッシュ(無効化)する操作。動的に生成されたコードを実行する際など、キャッシュの内容が古くなる可能性がある場合に、CPUが最新の命令をメモリから読み込むようにするために使用されます。古いクロージャの実装では、動的なコード生成を伴う場合があったため、cacheflushが必要とされていました。
  • Go 1.1の関数呼び出しの変更: Go 1.1では、関数呼び出しのメカニズムが大幅に改善されました。特に、MakeFuncの導入や、メソッド値とクロージャの内部表現の最適化が行われ、ランタイムでの動的なコード生成を減らす方向へと進みました。これにより、パフォーマンスの向上とコードの複雑性の軽減が図られました。

技術的詳細

このコミットの主要な変更点は、Goランタイムがクロージャを処理する方法の進化を反映しています。

  1. cacheflush関数の削除:

    • src/pkg/runtime/sys_freebsd_arm.s
    • src/pkg/runtime/sys_linux_arm.s
    • src/pkg/runtime/sys_netbsd_arm.s これらのファイルから、runtime·cacheflushというアセンブリ言語で実装された関数が削除されています。これは、Goの古いクロージャ実装が、動的に生成されたコードを実行する際に命令キャッシュのフラッシュを必要としていたためです。Go 1.1でのクロージャの内部表現の変更により、このような低レベルなキャッシュ操作が不要になったことを示しています。
  2. トレースバックロジックからのクロージャ検出コードの削除:

    • src/pkg/runtime/traceback_arm.c
    • src/pkg/runtime/traceback_x86.c これらのC言語ファイルでは、runtime·gentraceback関数内で、スタックトレース中にクロージャを特別に検出・処理するための複雑なロジックが削除されています。
    • ARMアーキテクチャ (traceback_arm.c): 以前は、pc <= 0x1000 || (f = runtime·findfunc(pc)) == nil の条件に加えて、命令ストリームをデコードしてクロージャの開始や終了を特定するコードブロックが存在しました。これは、特定のARM命令パターン(例: MOVW.P frame(R13), R15, MOVW $SYS_ARM_cacheflush, R7など)を解析し、スタックポインタやプログラムカウンタを調整するものでした。この複雑なロジックが完全に削除され、runtime·findfunc(pc)で関数が見つからない場合は単純にトレースバックを中断するようになりました。
    • x86アーキテクチャ (traceback_x86.c): 同様に、x86アーキテクチャでも、ADDQ $wwxxyyzz, SP; RETのような特定の命令パターンを検出してクロージャを処理するコードや、isclosureentry関数を呼び出してクロージャのエントリポイントを特定するロジックが削除されています。isclosureentry関数自体もこのコミットで完全に削除されています。

これらの変更は、Go 1.1でクロージャが通常の関数とより一貫した方法で表現されるようになったことを強く示唆しています。これにより、ランタイムはクロージャを特別扱いする必要がなくなり、スタックトレースのロジックが簡素化され、保守性が向上しました。また、動的なコード生成が減ることで、セキュリティリスクの低減や、JITコンパイルが許可されない環境での実行可能性も向上します。

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

このコミットのコアとなる変更は、主に以下のファイルに集中しています。

  • src/pkg/runtime/sys_freebsd_arm.s: runtime·cacheflush関数の削除
  • src/pkg/runtime/sys_linux_arm.s: runtime·cacheflush関数の削除と関連する#define SYS_ARM_cacheflushの削除
  • src/pkg/runtime/sys_netbsd_arm.s: runtime·cacheflush関数の削除
  • src/pkg/runtime/traceback_arm.c: runtime·gentraceback関数内のクロージャ検出・処理ロジックの削除
  • src/pkg/runtime/traceback_x86.c: runtime·gentraceback関数内のクロージャ検出・処理ロジックの削除、およびisclosureentry関数の完全な削除

特に、traceback_arm.ctraceback_x86.cにおける変更は、スタックトレースの根幹に関わる部分であり、Goランタイムがクロージャをどのように「認識」し、「辿る」かという低レベルなメカニズムが根本的に変わったことを示しています。

コアとなるコードの解説

src/pkg/runtime/traceback_arm.c の変更

--- a/src/pkg/runtime/traceback_arm.c
+++ b/src/pkg/runtime/traceback_arm.c
@@ -21,7 +21,7 @@ runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *gp, int32 skip, uintptr
  {\n  \tint32 i, n, iter;\n  \tuintptr pc, lr, tracepc, x;\n-\tbyte *fp, *p;\n+\tbyte *fp;\n  \tbool waspanic;\n  \tStktop *stk;\n  \tFunc *f;\n@@ -66,43 +66,8 @@ runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *gp, int32 skip, uintptr
  \t\t\tcontinue;\n  \t\t}\n  \t\t\n-\t\tif(pc <= 0x1000 || (f = runtime·findfunc(pc)) == nil) {\n-\t\t\t// Dangerous, but worthwhile: see if this is a closure by\n-\t\t\t// decoding the instruction stream.\n-\t\t\t//\n-\t\t\t// We check p < p+4 to avoid wrapping and faulting if\n-\t\t\t// we have lost track of where we are.\n-\t\t\tp = (byte*)pc;\n-\t\t\tif((pc&3) == 0 && p < p+4 &&\n-\t\t\t   runtime·mheap->arena_start < p &&\n-\t\t\t   p+4 < runtime·mheap->arena_used) {\n-\t\t\t   \tx = *(uintptr*)p;\n-\t\t\t\tif((x&0xfffff000) == 0xe49df000) {\n-\t\t\t\t\t// End of closure:\n-\t\t\t\t\t// MOVW.P frame(R13), R15\n-\t\t\t\t\tpc = *(uintptr*)sp;\n-\t\t\t\t\tlr = 0;\n-\t\t\t\t\tsp += x & 0xfff;\n-\t\t\t\t\tfp = nil;\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t\tif((x&0xfffff000) == 0xe52de000 && lr == (uintptr)runtime·goexit) {\n-\t\t\t\t\t// Beginning of closure.\n-\t\t\t\t\t// Closure at top of stack, not yet started.\n-\t\t\t\t\tp += 5*4;\n-\t\t\t\t\tif((x&0xfff) != 4) {\n-\t\t\t\t\t\t// argument copying\n-\t\t\t\t\t\tp += 7*4;\n-\t\t\t\t\t}\n-\t\t\t\t\tif((byte*)pc < p && p < p+4 && p+4 < runtime·mheap->arena_used) {\n-\t\t\t\t\t\tpc = *(uintptr*)p;\n-\t\t\t\t\t\tfp = nil;\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n+\t\tif(pc <= 0x1000 || (f = runtime·findfunc(pc)) == nil)\n  \t\t\tbreak;\n-\t\t}\n  \t\t\n  \t\t// Found an actual function.\n  \t\tif(lr == 0)

この差分は、runtime·gentraceback関数から、ARMアーキテクチャにおけるクロージャの特殊な処理ロジックが削除されたことを示しています。

  • byte *p; 変数が削除されました。これは、命令ストリームを直接読み取るために使用されていました。
  • if(pc <= 0x1000 || (f = runtime·findfunc(pc)) == nil) のブロック内にあった、命令デコードによるクロージャの検出とスタックポインタ/プログラムカウンタの調整を行う約40行のコードが完全に削除されました。
  • 変更後、runtime·findfunc(pc)で関数が見つからない場合(nilが返される場合)は、すぐにbreakしてトレースバックを終了するようになりました。これは、クロージャが通常の関数として扱われるようになり、特別な検出ロジックが不要になったことを意味します。

src/pkg/runtime/traceback_x86.c の変更

--- a/src/pkg/runtime/traceback_x86.c
+++ b/src/pkg/runtime/traceback_x86.c
@@ -8,7 +8,6 @@
  #include "arch_GOARCH.h"\n  #include "malloc.h"\n  \n-static uintptr isclosureentry(uintptr);\n  void runtime·deferproc(void);\n  void runtime·newproc(void);\n  void runtime·newstack(void);\n@@ -25,7 +24,6 @@ void runtime·sigpanic(void);\n  int32\n  runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *gp, int32 skip, uintptr *pcbuf, int32 max)\n  {\n-\tbyte *p;\n  \tint32 i, n, iter, sawnewstack;\n  \tuintptr pc, lr, tracepc;\n  \tbyte *fp;\n@@ -75,33 +73,8 @@ runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *gp, int32 skip, uintptr
  \t\t\tstk = (Stktop*)stk->stackbase;\n  \t\t\tcontinue;\n  \t\t}\n-\t\tif(pc <= 0x1000 || (f = runtime·findfunc(pc)) == nil) {\n-\t\t\t// Dangerous, but worthwhile: see if this is a closure:\n-\t\t\t//\tADDQ $wwxxyyzz, SP; RET\n-\t\t\t//\t[48] 81 c4 zz yy xx ww c3\n-\t\t\t// The 0x48 byte is only on amd64.\n-\t\t\tp = (byte*)pc;\n-\t\t\t// We check p < p+8 to avoid wrapping and faulting if we lose track.\n-\t\t\tif(runtime·mheap->arena_start < p && p < p+8 && p+8 < runtime·mheap->arena_used &&  // pointer in allocated memory\n-\t\t\t   (sizeof(uintptr) != 8 || *p++ == 0x48) &&  // skip 0x48 byte on amd64\n-\t\t\t   p[0] == 0x81 && p[1] == 0xc4 && p[6] == 0xc3) {\n-\t\t\t\tsp += *(uint32*)(p+2);\n-\t\t\t\tpc = *(uintptr*)sp;\n-\t\t\t\tsp += sizeof(uintptr);\n-\t\t\t\tlr = 0;\n-\t\t\t\tfp = nil;\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\t\t\t\n-\t\t\t// Closure at top of stack, not yet started.\n-\t\t\tif(lr == (uintptr)runtime·goexit && (pc = isclosureentry(pc)) != 0) {\n-\t\t\t\tfp = sp;\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\n-\t\t\t// Unknown pc: stop.\n+\t\tif(pc <= 0x1000 || (f = runtime·findfunc(pc)) == nil)\n  \t\t\tbreak;\n-\t\t}\n  \n  \t\t// Found an actual function.\n  \t\tif(fp == nil) {\n@@ -228,77 +201,3 @@ runtime·callers(int32 skip, uintptr *pcbuf, int32 m)\n  \n  \treturn runtime·gentraceback(pc, sp, nil, g, skip, pcbuf, m);\n  }\n-\n-static uintptr\n-isclosureentry(uintptr pc)\n-{\n-\tbyte *p;\n-\tint32 i, siz;\n-\t\n-\tp = (byte*)pc;\n-\tif(p < runtime·mheap->arena_start || p+32 > runtime·mheap->arena_used)\n-\t\treturn 0;\n-\n-\tif(*p == 0xe8) {\n-\t\t// CALL fn\n-\t\treturn pc+5+*(int32*)(p+1);\n-\t}\n-\t\n-\tif(sizeof(uintptr) == 8 && p[0] == 0x48 && p[1] == 0xb9 && p[10] == 0xff && p[11] == 0xd1) {\n-\t\t// MOVQ $fn, CX; CALL *CX\n-\t\treturn *(uintptr*)(p+2);\n-\t}\n-\n-\t// SUBQ $siz, SP\n-\tif((sizeof(uintptr) == 8 && *p++ != 0x48) || *p++ != 0x81 || *p++ != 0xec)\n-\t\treturn 0;\n-\tsiz = *(uint32*)p;\n-\tp += 4;\n-\t\n-\t// MOVQ $q, SI\n-\tif((sizeof(uintptr) == 8 && *p++ != 0x48) || *p++ != 0xbe)\n-\t\treturn 0;\n-\tp += sizeof(uintptr);\n-\n-\t// MOVQ SP, DI\n-\tif((sizeof(uintptr) == 8 && *p++ != 0x48) || *p++ != 0x89 || *p++ != 0xe7)\n-\t\treturn 0;\n-\n-\t// CLD on 32-bit\n-\tif(sizeof(uintptr) == 4 && *p++ != 0xfc)\n-\t\treturn 0;\n-\n-\tif(siz <= 4*sizeof(uintptr)) {\n-\t\t// MOVSQ...\n-\t\tfor(i=0; i<siz; i+=sizeof(uintptr))\n-\t\t\tif((sizeof(uintptr) == 8 && *p++ != 0x48) || *p++ != 0xa5)\n-\t\t\t\treturn 0;\n-\t} else {\n-\t\t// MOVQ $(siz/8), CX  [32-bit immediate siz/8]\n-\t\tif((sizeof(uintptr) == 8 && *p++ != 0x48) || *p++ != 0xc7 || *p++ != 0xc1)\n-\t\t\treturn 0;\n-\t\tp += 4;\n-\t\t\n-\t\t// REP MOVSQ\n-\t\tif(*p++ != 0xf3 || (sizeof(uintptr) == 8 && *p++ != 0x48) || *p++ != 0xa5)\n-\t\t\treturn 0;\n-\t}\n-\t\n-\t// CALL fn\n-\tif(*p == 0xe8) {\n-\t\tp++;\n-\t\treturn (uintptr)p+4 + *(int32*)p;\n-\t}\n-\t\n-\t// MOVQ $fn, CX; CALL *CX\n-\tif(sizeof(uintptr) != 8 || *p++ != 0x48 || *p++ != 0xb9)\n-\t\treturn 0;\n-\n-\tpc = *(uintptr*)p;\n-\tp += 8;\n-\t\n-\tif(*p++ != 0xff || *p != 0xd1)\n-\t\treturn 0;\n-\n-\treturn pc;\n-}

この差分は、x86アーキテクチャにおけるクロージャの特殊な処理ロジックがruntime·gentraceback関数から削除されたことを示しています。

  • static uintptr isclosureentry(uintptr); の前方宣言が削除され、それに伴いisclosureentry関数自体の実装もファイル末尾から完全に削除されました。この関数は、特定の命令パターンを解析してクロージャのエントリポイントを特定する役割を担っていました。
  • byte *p; 変数が削除されました。
  • if(pc <= 0x1000 || (f = runtime·findfunc(pc)) == nil) のブロック内にあった、命令デコードによるクロージャの検出(例: ADDQ $wwxxyyzz, SP; RETパターン)やisclosureentry関数の呼び出しを含む約30行のコードが完全に削除されました。
  • 変更後、ARMの場合と同様に、runtime·findfunc(pc)で関数が見つからない場合は、すぐにbreakしてトレースバックを終了するようになりました。

これらの変更は、Go 1.1でクロージャの内部表現が標準化され、ランタイムがクロージャを通常の関数と区別する必要がなくなったことを明確に示しています。これにより、スタックトレースのロジックが大幅に簡素化され、特定のアーキテクチャに依存する複雑なアセンブリコードの解析が不要になりました。

関連リンク

  • Go 1.1の関数呼び出しに関する変更の背景: http://golang.org/s/go11func
  • このコミットが参照しているGo Change List (CL): https://golang.org/cl/7393049 (ただし、このCL自体は-trimpathフラグに関するもので、直接的なコード変更の理由とは異なりますが、コミットメッセージに記載されているため含めます。)

参考にした情報源リンク

  • Go 1.1 Release Notes (公式ドキュメントや関連する設計ドキュメント): Go 1.1のリリースノートや、golang.org/s/go11funcで言及されている設計ドキュメントは、この変更の背景を理解する上で非常に重要です。
  • Go言語のランタイムソースコード: 実際のコード変更を理解するために、GoのGitHubリポジトリにあるランタイムのソースコード(特にsrc/pkg/runtimeディレクトリ)を参照しました。
  • Go言語のクロージャに関するドキュメントや解説記事: クロージャの概念とGoにおける実装について理解を深めるために、一般的なGo言語のドキュメントや技術記事を参照しました。