[インデックス 11784] ファイルの概要
コミット
このコミットは、Goコンパイラ(cmd/gc
)におけるインライン化された関数のボディの型チェック中に、safemode
を一時的に無効にする変更を導入しています。これにより、インライン化されたコードがunsafe
パッケージを参照している場合でも、正しく型チェックが行われるようになります。
GitHub上でのコミットページへのリンク
https://github.com/golang.org/go/commit/e0b2ce34017472c684dfceae23879925711e0f88
元コミット内容
commit e0b2ce34017472c684dfceae23879925711e0f88
Author: Luuk van Dijk <lvd@golang.org>
Date: Fri Feb 10 22:50:55 2012 +0100
cmd/gc: suspend safemode during typecheck of inlined bodies.
Should be obviously correct. Includes minimal test case.
A future CL should clear up the logic around typecheckok and importpkg != nil someday.
R=rsc, dsymonds, rsc
CC=golang-dev
https://golang.org/cl/5652057
---
src/cmd/gc/inl.c | 72 +++++++++++++++++++++++++++++++++++++++++++++-----\n test/golden.out | 2 ++\n test/run | 2 +-\n test/safe/main.go | 14 ++++++++++\n test/safe/nousesafe.go | 8 ++++++\n test/safe/pkg.go | 16 +++++++++++\n test/safe/usesafe.go | 8 ++++++\n 7 files changed, 115 insertions(+), 7 deletions(-)\n```
## 変更の背景
Goコンパイラには、`safemode`という概念が存在します。これは、コンパイラが特定のコードパスを処理する際に、`unsafe`パッケージの使用を制限または監視するためのモードです。`unsafe`パッケージは、Goの型安全性をバイパスして低レベルのメモリ操作を可能にする強力な機能ですが、誤用するとプログラムのクラッシュやセキュリティ脆弱性につながる可能性があります。
このコミット以前のGoコンパイラでは、インライン化(inlining)された関数のボディを型チェックする際に、`safemode`が常に有効になっていました。しかし、インポートされたパッケージが内部的に`unsafe`パッケージを使用している場合、その関数がインライン化されると、`safemode`が原因で型チェックエラーが発生する可能性がありました。これは、インポート元のパッケージが`unsafe`の使用を適切に管理しているにもかかわらず、インライン化の過程で不必要な制限がかかってしまうという問題です。
この変更の目的は、インポートされた関数のインライン化されたボディの型チェック時に、`unsafe`パッケージの使用がそのパッケージのインポート時に既にチェック済みであるという前提に基づき、`safemode`を一時的に無効にすることです。これにより、正当な`unsafe`の使用を含むコードがインライン化された際に、コンパイラが不必要にエラーを報告するのを防ぎます。
## 前提知識の解説
* **Goコンパイラ (`cmd/gc`)**: Go言語の公式コンパイラです。ソースコードを機械語に変換する役割を担います。
* **インライン化 (Inlining)**: コンパイラの最適化手法の一つで、関数呼び出しのオーバーヘッドを削減するために、呼び出される関数のコードを呼び出し元に直接埋め込むことです。これにより、実行時のパフォーマンスが向上する可能性があります。
* **型チェック (Type Checking)**: プログラムの各部分が期待されるデータ型と一致しているかを確認するプロセスです。Goは静的型付け言語であるため、コンパイル時に厳密な型チェックが行われます。
* **`unsafe`パッケージ**: Go言語の標準ライブラリの一つで、Goの型システムとメモリ安全性の保証をバイパスする機能を提供します。ポインタ演算や、異なる型の間の変換など、低レベルの操作を可能にします。通常は、パフォーマンスが非常に重要な場合や、特定のハードウェアとのインタフェースが必要な場合など、特殊な状況でのみ使用されます。
* **`safemode`**: Goコンパイラ内部のフラグまたは状態であり、`unsafe`パッケージの使用に関する特定の制約を強制するために使用されます。例えば、`unsafe`パッケージをインポートする際に、そのパッケージが安全であるとマークされていない場合、コンパイルエラーを発生させることがあります。
* **`Node`**: GoコンパイラのAST (Abstract Syntax Tree) におけるノードを表すデータ構造です。プログラムの各要素(関数、変数、式など)は`Node`として表現されます。
* **`Pkg` (Package)**: Go言語におけるコードの組織単位です。関連する関数やデータ型をまとめたものです。
* **`localpkg`**: 現在コンパイル中のパッケージを指します。
* **`importpkg`**: 以前のGoコンパイラで、インポートされたパッケージのコンテキストを追跡するために使用されていた変数です。このコミットでは、`importpkg`の直接的な使用が削除され、より汎用的な`fnpkg`関数と`pkg`変数に置き換えられています。
## 技術的詳細
このコミットの核心は、`src/cmd/gc/inl.c`ファイル内の`typecheckinl`関数と`mkinlcall`関数の変更にあります。
1. **`typecheckinl`関数の変更**:
* この関数は、インポートされた関数のボディを型チェックするために使用されます。
* 変更前は、`importpkg`というグローバル変数を使用してインポートされたパッケージのコンテキストを管理していました。
* 変更後、`fnpkg(fn)`という新しいヘルパー関数が導入され、型チェック対象の関数`fn`が属するパッケージ(`Pkg`)を取得します。
* 最も重要な変更は、`safemode`の扱い方です。`typecheckinl`が呼び出される際、現在の`safemode`の状態を`save_safemode`に保存し、`safemode`を`0`(無効)に設定します。型チェックが完了した後、`safemode`を元の値に戻します。
* このロジックは、インポートされた関数が`unsafe`パッケージを使用している場合でも、その`unsafe`の使用がパッケージのインポート時に既に検証済みであるため、インライン化されたボディの型チェック時には`safemode`による追加の制限は不要であるという前提に基づいています。
2. **`mkinlcall`関数の変更**:
* この関数は、関数呼び出しをインライン化されたコードに置き換える処理を担当します。
* 変更前は、`mkinlcall`が直接インライン化のロジックを含んでいました。
* 変更後、`mkinlcall`は`mkinlcall1`という新しいヘルパー関数を呼び出すラッパー関数となりました。
* `mkinlcall`の内部で、`typecheckinl`と同様に、インライン化される関数がインポートされた関数である場合(`pkg != localpkg && pkg != nil`)、`safemode`を一時的に`0`に設定します。これにより、インライン化されたコードが`unsafe`を参照していても、その後の処理で問題が発生しないようにします。処理後、`safemode`は元の値に戻されます。
3. **`fnpkg`ヘルパー関数の追加**:
* この新しい静的関数は、与えられた`Node`(関数)が属するパッケージを決定します。
* 通常の関数では`fn->sym->pkg`からパッケージを取得しますが、インポートされたメソッドの場合、レシーバの型からパッケージ情報を抽出するロジックが含まれています。これは、メソッドのシンボルがローカルパッケージで再利用される可能性があるためです。
これらの変更により、Goコンパイラは、`unsafe`パッケージを内部的に使用するインポートされた関数がインライン化された場合でも、不必要なコンパイルエラーを発生させることなく、正しく処理できるようになりました。
## コアとなるコードの変更箇所
`src/cmd/gc/inl.c`
```diff
--- a/src/cmd/gc/inl.c
+++ b/src/cmd/gc/inl.c
@@ -53,22 +53,62 @@ static Node *inlfn; // function currently being inlined
static Node *inlretlabel; // target of the goto substituted in place of a return
static NodeList *inlretvars; // temp out variables
-// Lazy typechecking of imported bodies.
-// TODO avoid redoing local functions (imporpkg would be wrong)
+// Get the function's package. For ordinary functions it's on the ->sym, but for imported methods
+// the ->sym can be re-used in the local package, so peel it off the receiver's type.
+static Pkg*
+fnpkg(Node *fn)
+{
+
+ Type *rcvr;
+
+ if(fn->type->thistuple) {
+ // method
+ rcvr = getthisx(fn->type)->type->type;
+ if(isptr[rcvr->etype])
+ rcvr = rcvr->type;
+ if(!rcvr->sym)
+ fatal("receiver with no sym: [%S] %lN (%T)", fn->sym, fn, rcvr);
+ return rcvr->sym->pkg;
+ }
+ // non-method
+ return fn->sym->pkg;
+}
+
+// Lazy typechecking of imported bodies. For local functions, caninl will set ->typecheck
+// because they're a copy of an already checked body.
void
typecheckinl(Node *fn)
{
Node *savefn;
+ Pkg *pkg;
+ int save_safemode, lno;
+
+ if(fn->typecheck)
+ return;
+
+ lno = setlineno(fn);
+
if (debug['m']>2)
print("typecheck import [%S] %lN { %#H }\n", fn->sym, fn, fn->inl);
+ // typecheckinl is only used for imported functions;
+ // their bodies may refer to unsafe as long as the package
+ // was marked safe during import (which was checked then).
+ pkg = fnpkg(fn);
+ if (pkg == localpkg || pkg == nil)
+ fatal("typecheckinl on local function %lN", fn);
+
+ save_safemode = safemode;
+ safemode = 0;
+
savefn = curfn;
curfn = fn;
- importpkg = fn->sym->pkg;
typechecklist(fn->inl, Etop);
- importpkg = nil;
+ fn->typecheck = 1;
curfn = savefn;
+
+ safemode = save_safemode;
+
+ lineno = lno;
}
// Caninl determines whether fn is inlineable. Currently that means:
@@ -105,6 +145,8 @@ caninl(Node *fn)
fn->nname->inl = fn->nbody;
fn->nbody = inlcopylist(fn->nname->inl);
+ // nbody will have been typechecked, so we can set this:
+ fn->typecheck = 1;
// hack, TODO, check for better way to link method nodes back to the thing with the ->inl
// this is so export can find the body of a method
@@ -444,12 +486,30 @@ inlnode(Node **np)
lineno = lno;
}
+static void mkinlcall1(Node **np, Node *fn);
+
+static void
+mkinlcall(Node **np, Node *fn)
+{
+ int save_safemode;
+ Pkg *pkg;
+
+ save_safemode = safemode;
+
+ // imported functions may refer to unsafe as long as the
+ // package was marked safe during import (already checked).
+ pkg = fnpkg(fn);
+ if(pkg != localpkg && pkg != nil)
+ safemode = 0;
+ mkinlcall1(np, fn);
+ safemode = save_safemode;
+}
// if *np is a call, and fn is a function with an inlinable body, substitute *np with an OINLCALL.\n // On return ninit has the parameter assignments, the nbody is the\n // inlined function body and list, rlist contain the input, output\n // parameters.\n static void
-mkinlcall(Node **np, Node *fn)
+mkinlcall1(Node **np, Node *fn)
{
int i;
Node *n, *call, *saveinlfn, *as, *m;
@@ -598,7 +658,7 @@ mkinlcall(Node **np, Node *fn)
*np = call;
inlfn = saveinlfn;
-
+
// transitive inlining
// TODO do this pre-expansion on fn->inl directly. requires
// either supporting exporting statemetns with complex ninits
コアとなるコードの解説
上記の差分は、Goコンパイラのインライン化処理におけるsafemode
の管理方法を改善しています。
-
fnpkg
関数の追加:- この関数は、与えられた
Node *fn
(関数を表すASTノード)が属するパッケージ(Pkg
)を返します。 - 通常の関数(メソッドではない関数)の場合、
fn->sym->pkg
から直接パッケージ情報を取得します。 - メソッドの場合、レシーバの型(
fn->type->thistuple
が真の場合)からパッケージ情報を抽出します。これは、インポートされたメソッドのシンボルがローカルパッケージで再利用される可能性があるため、レシーバの型を通じて元のパッケージを特定する必要があるためです。
- この関数は、与えられた
-
typecheckinl
関数の変更:typecheckinl
は、インポートされた関数のボディを遅延型チェックするための関数です。if(fn->typecheck) return;
:既に型チェック済みであれば処理をスキップします。lno = setlineno(fn);
:現在の行番号を保存し、関数の行番号に設定します。これはデバッグやエラー報告のために重要です。pkg = fnpkg(fn);
:型チェック対象の関数が属するパッケージを取得します。if (pkg == localpkg || pkg == nil) fatal("typecheckinl on local function %lN", fn);
:typecheckinl
はインポートされた関数にのみ使用されるべきであり、ローカル関数に対して呼び出された場合は致命的なエラーとします。save_safemode = safemode; safemode = 0;
:ここが重要な変更点です。 現在のsafemode
の状態を保存し、safemode
を一時的に0
(無効)に設定します。これは、インポートされたパッケージがunsafe
を使用している場合でも、その使用はインポート時に既に検証済みであるため、インライン化されたボディの型チェック時にはsafemode
による追加の制限は不要であるという前提に基づいています。typechecklist(fn->inl, Etop);
:インライン化されたボディ(fn->inl
)の型チェックを実行します。fn->typecheck = 1;
:型チェックが完了したことをマークします。safemode = save_safemode;
:safemode
を元の状態に戻します。lineno = lno;
:保存しておいた元の行番号に戻します。
-
mkinlcall
とmkinlcall1
関数の変更:mkinlcall
は、関数呼び出しをインライン化されたコードに置き換える主要な関数です。- 新しい
mkinlcall1
関数が導入され、元のmkinlcall
のロジックの大部分がこちらに移動しました。 mkinlcall
は、mkinlcall1
を呼び出す前にsafemode
を一時的に調整するラッパー関数となりました。save_safemode = safemode;
:現在のsafemode
の状態を保存します。pkg = fnpkg(fn); if(pkg != localpkg && pkg != nil) safemode = 0;
:インライン化される関数がインポートされた関数である場合、safemode
を一時的に0
に設定します。これにより、インライン化されたコードがunsafe
を参照していても、その後の処理で問題が発生しないようにします。mkinlcall1(np, fn);
:実際のインライン化処理を実行します。safemode = save_safemode;
:safemode
を元の状態に戻します。
これらの変更により、Goコンパイラは、unsafe
パッケージを内部的に使用するインポートされた関数がインライン化された場合でも、不必要なコンパイルエラーを発生させることなく、正しく処理できるようになりました。これは、コンパイラの堅牢性と、unsafe
パッケージのより柔軟な使用を可能にするための重要な改善です。
関連リンク
- Go言語の
unsafe
パッケージに関する公式ドキュメント: https://pkg.go.dev/unsafe - Goコンパイラのインライン化に関する一般的な情報(Goのバージョンによって実装は異なりますが、概念は共通です)
参考にした情報源リンク
- Go言語のソースコード(特に
src/cmd/gc/
ディレクトリ) - Go言語のコンパイラ設計に関する一般的な知識
- Go言語の
unsafe
パッケージの動作に関する知識