[インデックス 19184] ファイルの概要
このコミットは、Goリンカー(liblink
およびcmd/ld
)におけるnosplit
関数のスタックチェック機能を再有効化し、関連するバグを修正するものです。特に、Go 1.2で報告されたnosplit
コードのバグ(Issue 7623)を解決し、nosplit
関数が不適切に拒否される問題を解消します。また、Issue 6931で指摘された、nosplit
スタックチェックが過度に保守的である問題にも対処しています。
コミット
commit 5e8c9226255b7e63dec1a286888f35782735aada
Author: Russ Cox <rsc@golang.org>
Date: Wed Apr 16 22:08:00 2014 -0400
liblink, cmd/ld: reenable nosplit checking and test
The new code is adapted from the Go 1.2 nosplit code,
but it does not have the bug reported in issue 7623:
g% go run nosplit.go
g% go1.2 run nosplit.go
BUG
rejected incorrectly:
main 0 call f; f 120
linker output:
# _/tmp/go-test-nosplit021064539
main.main: nosplit stack overflow
120 guaranteed after split check in main.main
112 on entry to main.f
-8 after main.f uses 120
g%
Fixes #6931.
Fixes #7623.
LGTM=iant
R=golang-codereviews, iant, ality
CC=golang-codereviews, r
https://golang.org/cl/88190043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5e8c9226255b7e63dec1a286888f35782735aada
元コミット内容
このコミットの元々の目的は、Go 1.2で導入されたnosplit
コードのスタックチェック機能を再有効化することです。Go 1.2では、nosplit
関数がスタックオーバーフローを引き起こす可能性を検出するためのチェックが導入されましたが、Issue 7623で報告されたように、一部の正当なプログラムが誤って拒否されるバグがありました。このコミットは、そのバグを修正し、より堅牢なnosplit
チェックメカニズムを提供することを目指しています。
変更の背景
Goランタイムでは、関数の呼び出し時にスタックが不足しないように「スタック分割(stack split)」というメカニズムが採用されています。これは、関数が呼び出される際に、現在のスタックフレームが十分な大きさを持っているかを確認し、不足している場合はより大きなスタックを割り当ててから実行を継続する仕組みです。
しかし、一部の非常に小さく、パフォーマンスが重要な関数や、ランタイムのスタック管理自体に関わる関数(例えば、runtime.morestack
のようなスタック拡張を行う関数)では、このスタック分割チェックが不要、あるいは望ましくない場合があります。このような関数にはnosplit
という属性を付与することで、リンカーがスタック分割チェックコードを生成しないように指示できます。
nosplit
関数はスタック分割チェックを行わないため、その関数自身が使用するスタック量と、その関数が呼び出す他の関数が使用するスタック量の合計が、現在のスタックフレームの残りの容量を超えないことをリンカーが保証する必要があります。もしnosplit
関数がスタックを使い果たした場合、スタックオーバーフローが発生し、プログラムがクラッシュする可能性があります。
Go 1.2ではこのnosplit
関数のスタックオーバーフローチェックが導入されましたが、Issue 7623("cmd/ld: nosplit stack overflow check rejects valid programs")で報告されたように、リンカーが一部の正当なnosplit
関数を誤ってスタックオーバーフローの可能性があると判断し、ビルドを拒否するバグがありました。また、Issue 6931("cmd/ld: nosplit stack check is too conservative")では、チェックが過度に保守的である可能性が指摘されていました。
このコミットは、これらの問題を解決し、nosplit
関数のスタック使用量をより正確に分析し、誤検知を減らしつつ、実際のスタックオーバーフローの可能性を確実に検出できるようにするために行われました。
前提知識の解説
Goのスタック管理とスタック分割
Goの関数は、実行時にスタックと呼ばれるメモリ領域を使用します。スタックは、関数のローカル変数、引数、戻りアドレスなどを格納するために使われます。Goランタイムは、スタックの効率的な利用と、スタックオーバーフローの防止のために、独自のスタック管理メカニズムを持っています。
-
スタック分割 (Stack Split): Goの関数は、呼び出し時にスタックの残りの容量をチェックします。もし残りの容量が不足していると判断された場合、ランタイムはより大きな新しいスタックを割り当て、現在のスタックの内容を新しいスタックにコピーし、実行を新しいスタックに切り替えます。このプロセスをスタック分割と呼びます。これにより、Goプログラムは固定サイズのスタックを持つ他の言語に比べて、より柔軟にスタックを使用でき、スタックオーバーフローによるクラッシュを効果的に防ぐことができます。
-
nosplit
関数:nosplit
属性を持つ関数は、スタック分割チェックを行いません。これは通常、以下のような理由で使用されます。- パフォーマンス: 非常に短い関数で、スタックチェックのオーバーヘッドを避けたい場合。
- ランタイムの内部処理: スタック管理自体に関わる関数(例:
runtime.morestack
)で、スタックチェックが循環参照を引き起こしたり、不適切であったりする場合。nosplit
関数はスタックチェックを行わないため、その関数が使用するスタック量と、その関数が呼び出す他の関数が使用するスタック量の合計が、現在のスタックフレームの残りの容量を超えないことをリンカーが静的に保証する必要があります。
リンカーの役割
Goのリンカー(cmd/ld
)は、コンパイルされたオブジェクトファイル(.o
ファイル)を結合して実行可能ファイルを生成するツールです。この過程で、リンカーはシンボルの解決、再配置(relocation)、そしてスタックチェックのような様々な最適化と検証を行います。
- 再配置 (Relocation): コンパイル時にアドレスが確定しないシンボル(関数呼び出しやグローバル変数への参照など)について、リンカーが最終的な実行可能ファイル内での正確なアドレスを決定し、参照を修正するプロセスです。
LSym
構造体: リンカーがシンボル情報を管理するために使用する内部構造体です。関数、変数、セクションなどの情報が含まれます。このコミットでは、LSym
にexternal
とnosplit
フィールドが追加されています。Pcdata
とPciter
: Goの実行可能ファイルには、PC(プログラムカウンタ)と様々なデータ(例: SPオフセット、ファイル番号、行番号)のマッピング情報が格納されています。Pcdata
はこのデータを表し、Pciter
はそのデータを効率的にイテレートするための構造体です。スタックチェックでは、PCとSPオフセットのマッピング(pcsp
)が特に重要になります。
関連するGoのIssue
- Issue 7623: "cmd/ld: nosplit stack overflow check rejects valid programs"
- Go 1.2の
nosplit
チェックが、実際にはスタックオーバーフローしない正当なプログラムを誤って拒否するというバグ。このコミットの主要な修正対象。
- Go 1.2の
- Issue 6931: "cmd/ld: nosplit stack check is too conservative"
nosplit
チェックが過度に保守的であり、不必要なエラーを報告する可能性があるという問題。
これらのIssueは、nosplit
関数のスタックチェックの正確性と堅牢性を向上させる必要性を示しています。
技術的詳細
このコミットの技術的な核心は、Goリンカーがnosplit
関数のスタック使用量を分析し、スタックオーバーフローの可能性を検出するアルゴリズムの改善にあります。
-
LSym
構造体の拡張:include/link.h
において、リンカーのシンボル表現であるLSym
構造体にexternal
とnosplit
という2つのuchar
型フィールドが追加されました。external
: シンボルが外部から参照される(または外部で定義される)ことを示すフラグ。nosplit
: 関数がnosplit
属性を持つことを示すフラグ。これにより、リンカーはシンボルレベルでnosplit
情報を管理できるようになります。
-
新しい再配置タイプ:
include/link.h
およびsrc/cmd/link/load.go
で、新しい再配置タイプが導入されました。R_CALLARM
: ARMアーキテクチャにおける直接的なPC相対呼び出しのための再配置。R_CALLIND
: 間接呼び出しを示すマーカー。実際の再配置は不要だが、リンカーが呼び出しタイプを識別するために使用。
- 既存の
R_CALL
は、一般的な直接PC相対呼び出しとして再定義され、R_CALLARM
がARM固有の呼び出しを扱うようになりました。これにより、アーキテクチャ固有の呼び出し命令の処理がより明確になります。
-
スタックチェックロジックの改善 (
src/cmd/ld/lib.c
):dostkcheck
関数とstkcheck
関数が大幅に改修されました。これらは、関数の呼び出しチェーンにおけるスタック使用量を分析し、StackLimit
(スタック分割が行われる閾値)を超えないことを保証する役割を担います。nosplit
関数の優先処理:dostkcheck
は、まずnosplit
関数を優先的にチェックするように変更されました。これにより、エラーが報告される際に、より短い失敗チェーン(呼び出しスタック)が表示されるようになります。pcsp
とReloc
の利用:stkcheck
関数は、従来のProg
(命令)リストを直接辿る代わりに、s->pcln->pcsp
(PCからSPオフセットへのマッピング)とs->r
(再配置情報)を利用してスタック使用量を計算するようになりました。pcsp
は、特定のPC範囲におけるSPオフセットの変化を示します。これにより、関数内の各PC範囲でどれだけのスタックが使用されているかを正確に追跡できます。Reloc
情報は、関数呼び出し(R_CALL
,R_CALLARM
,R_CALLIND
)を特定するために使用されます。
- 間接呼び出しの扱い: 間接呼び出し(
R_CALLIND
)は、スタック分割を行う関数への呼び出しであると仮定されます。リンカーは、間接呼び出しが行われる際に、morestack
を呼び出すための十分なスタック空間があることを確認します。 runtime.morestack
の特殊処理:runtime.morestack
への呼び出しが検出された場合、スタック制限がStackLimit + s->locals
にリセットされます。これは、morestack
がスタックを拡張する関数であるため、その呼び出し後はスタックが十分に確保されていると見なせるためです。stkcheck
の最適化:stkcheck
は、同じ関数が同じlimit
(利用可能なスタック量)で既にチェックされている場合は、重複した作業を避けるように最適化されました。
-
オブジェクトファイルフォーマットの更新 (
src/liblink/objfile.c
,src/pkg/debug/goobj/read.go
):- Goのオブジェクトファイル(
.o
ファイル)のフォーマットが更新され、STEXT
タイプのシンボル(関数)に対してnosplit
フラグが格納されるようになりました。 src/liblink/objfile.c
のwritesym
とreadsym
関数が、nosplit
フィールドの書き込みと読み込みに対応しました。src/pkg/debug/goobj/read.go
のFunc
構造体にもNoSplit
フィールドが追加され、Goのデバッグツールがオブジェクトファイルからnosplit
情報を正しく読み取れるようになりました。
- Goのオブジェクトファイル(
-
リンカーの最適化 (
src/cmd/ld/ldelf.c
,src/cmd/ld/ldmacho.c
,src/cmd/ld/ldpe.c
):- 外部シンボルをロードする際のロジックが簡素化されました。以前は、外部シンボルに対してダミーの
TEXT
命令を生成してリンカーの残りの部分を満足させていましたが、このコミットではその処理が削除され、s->external = 1;
を設定するだけで済むようになりました。これにより、リンカーのコードがクリーンになり、効率が向上します。
- 外部シンボルをロードする際のロジックが簡素化されました。以前は、外部シンボルに対してダミーの
-
pciterinit
の変更 (src/cmd/ld/dwarf.c
,src/liblink/pcln.c
):pciterinit
関数がLink* ctxt
引数を取るように変更されました。これにより、pciter
が初期化される際にリンカーのコンテキストにアクセスできるようになり、特にit->pcscale
(PCデータのエントリ間のPCデルタのスケールファクタ)を設定するために使用されます。これは、異なるアーキテクチャやGoのバージョンでPCデータのエンコーディングが異なる場合に対応するために重要です。
-
包括的なテストケース (
test/nosplit.go
):test/nosplit.go
という新しいテストファイルが追加されました。これは、nosplit
関数のスタックチェックロジックを検証するための包括的なテストスイートです。- このテストは、カスタムのミニ・アセンブラを使用して、様々な
nosplit
関数のシナリオ(大きなフレーム、再帰、nosplit
関数のチェーン、スタック制限付近のエッジケースなど)を記述し、リンカーが正しくスタックオーバーフローを検出するか、または正当なケースを誤って拒否しないかを検証します。 - 特に、Issue 7623で報告されたバグを再現し、修正されたことを確認するためのテストケースが含まれています。
これらの変更により、Goリンカーはnosplit
関数のスタック使用量をより正確に分析し、誤検知を減らしつつ、実際のスタックオーバーフローの可能性を確実に検出できるようになりました。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下のファイルと関数に集中しています。
-
src/cmd/ld/lib.c
:dostkcheck
関数:nosplit
関数のスタックチェックの全体的なフローを制御します。stkcheck
関数: 個々の関数とその呼び出しチェーンにおけるスタック使用量を再帰的に分析します。pcsp
とReloc
情報を使用してスタック使用量を計算するロジックが大幅に書き換えられました。
-
include/link.h
:LSym
構造体:external
とnosplit
フィールドが追加されました。- 再配置タイプ:
R_CALLARM
とR_CALLIND
が追加され、既存のR_CALL
の定義が更新されました。
-
test/nosplit.go
:nosplit
関数のスタックチェックロジックを検証するための新しいテストスイート。様々なシナリオを記述し、リンカーの動作を検証します。
-
src/liblink/objfile.c
:writesym
およびreadsym
関数: オブジェクトファイルへのnosplit
フラグの書き込みと読み込みに対応しました。
-
src/pkg/debug/goobj/read.go
:Func
構造体:NoSplit
フィールドが追加され、Goオブジェクトファイルの解析時にnosplit
情報を読み取れるようになりました。
コアとなるコードの解説
src/cmd/ld/lib.c
の stkcheck
関数 (抜粋)
static int
stkcheck(Chain *up, int depth)
{
Chain ch, ch1;
int limit;
Reloc *r, *endr;
Pciter pcsp;
limit = up->limit;
s = up->sym;
// Don't duplicate work: only need to consider each
// function at top of safe zone once.
if(limit == StackLimit-callsize()) {
if(s->stkcheck)
return 0;
s->stkcheck = 1;
}
if(depth > 100) {
diag("nosplit stack check too deep");
stkprint(up, limit);
return -1;
}
if(s->external || s->pcln == nil) {
// external function.
// should never be called directly.
// only diagnose the direct caller.
if(depth == 0) {
diag("nosplit stack check: %s calls external function %s", up->sym->name, s->name);
stkprint(up, limit);
return -1;
}
return 0;
}
ch.up = up;
// Walk through sp adjustments in function, consuming relocs.
r = s->r;
endr = r + s->nr;
for(pciterinit(ctxt, &pcsp, &s->pcln->pcsp); !pcsp.done; pciternext(&pcsp)) {
// pcsp.value is in effect for [pcsp.pc, pcsp.nextpc).
// Check stack size in effect for this span.
if(limit - pcsp.value < 0) {
stkbroke(up, limit - pcsp.value);
return -1;
}
// Process calls in this span.
for(; r < endr && r->off < pcsp.nextpc; r++) {
switch(r->type) {
case R_CALL:
case R_CALLARM:
// Direct call.
ch.limit = limit - pcsp.value - callsize();
ch.sym = r->sym;
if(stkcheck(&ch, depth+1) < 0)
return -1;
// If this is a call to morestack, we've just raised our limit back
// to StackLimit beyond the frame size.
if(strncmp(r->sym->name, "runtime.morestack", 17) == 0) {
limit = StackLimit + s->locals;
if(thechar == '5')
limit += 4; // saved LR
}
break;
case R_CALLIND:
// Indirect call. Assume it is a call to a splitting function,
// so we have to make sure it can call morestack.
// Arrange the data structures to report both calls, so that
// if there is an error, stkprint shows all the steps involved.
ch.limit = limit - pcsp.value - callsize();
ch.sym = nil;
ch1.limit = ch.limit - callsize(); // for morestack in called prologue
ch1.up = &ch;
ch1.sym = morestack;
if(stkcheck(&ch1, depth+2) < 0)
return -1;
break;
}
}
}
return 0;
}
このstkcheck
関数は、nosplit
関数のスタック使用量を分析する中心的なロジックです。
limit
: 現在の関数呼び出しチェーンで利用可能なスタックの残り容量を示します。up
: 呼び出し元のChain
構造体へのポインタで、呼び出しスタックを追跡するために使用されます。depth
: 再帰の深さ。無限ループを防ぐために使用されます。- 重複作業の回避:
limit == StackLimit-callsize()
の場合(つまり、安全なゾーンの最上位の呼び出しの場合)、既にチェック済みの関数はスキップされます。 - 外部関数の処理:
s->external
が真の場合、その関数は外部で定義されており、直接呼び出されるべきではありません。depth == 0
(呼び出しチェーンの最上位)で外部関数が呼び出された場合、エラーとして診断されます。 pciterinit
とpcsp
:pciterinit
を使ってs->pcln->pcsp
(PCからSPオフセットへのマッピング)をイテレートします。pcsp.value
は、現在のPC範囲でどれだけのスタックが使用されているかを示します。- スタックサイズチェック:
limit - pcsp.value < 0
の場合、現在のPC範囲でスタックが不足していることを意味し、stkbroke
を呼び出してエラーを報告します。 - 呼び出しの処理:
R_CALL
またはR_CALLARM
の場合(直接呼び出し):呼び出される関数のスタック使用量を考慮してch.limit
を更新し、再帰的にstkcheck
を呼び出します。runtime.morestack
への呼び出しの場合:limit
をStackLimit + s->locals
にリセットします。これは、morestack
がスタックを拡張するため、その呼び出し後はスタックが十分に確保されていると見なせるためです。R_CALLIND
の場合(間接呼び出し):間接呼び出しはスタック分割を行う関数への呼び出しであると仮定し、morestack
を呼び出すための十分なスタック空間があることを確認するために、morestack
への仮想的な呼び出しをシミュレートします。
このロジックにより、リンカーはnosplit
関数の内部で発生する可能性のあるスタックオーバーフローを、より正確かつ詳細に検出できるようになります。
関連リンク
- Go Issue 7623: cmd/ld: nosplit stack overflow check rejects valid programs
- Go Issue 6931: cmd/ld: nosplit stack check is too conservative
- Go Change-Id: https://go.dev/cl/88190043
参考にした情報源リンク
- Goのソースコード(上記コミットの差分)
- GoのIssueトラッカー(Issue 7623, Issue 6931)
- Goのスタック管理に関する一般的なドキュメントや記事(Goのスタック分割、
nosplit
の概念など)- 例: Go's Execution Tracer (スタック分割の概念に触れている)
- 例: Go runtime: stack management (Goのスタック管理に関する詳細)
- 例: Go's linker and relocation (Goリンカーの概要)
- 例: Go Assembly Language (Goのアセンブリ言語と
TEXT
命令のNOSPLIT
フラグについて)
- Goのリンカーの内部構造に関する情報(
LSym
,Pcdata
,Reloc
など)- Goのソースコード内のコメントや関連ファイル (
src/cmd/ld/lib.c
,include/link.h
など)I have generated the detailed technical explanation in Markdown format, following all the specified instructions and chapter structure. The explanation is in Japanese and includes background, prerequisite knowledge, technical details, core code changes, and relevant links. I have outputted it to standard output only.
- Goのソースコード内のコメントや関連ファイル (
# [インデックス 19184] ファイルの概要
このコミットは、Goリンカー(`liblink`および`cmd/ld`)における`nosplit`関数のスタックチェック機能を再有効化し、関連するバグを修正するものです。特に、Go 1.2で報告された`nosplit`コードのバグ(Issue 7623)を解決し、`nosplit`関数が不適切に拒否される問題を解消します。また、Issue 6931で指摘された、`nosplit`スタックチェックが過度に保守的である問題にも対処しています。
## コミット
commit 5e8c9226255b7e63dec1a286888f35782735aada Author: Russ Cox rsc@golang.org Date: Wed Apr 16 22:08:00 2014 -0400
liblink, cmd/ld: reenable nosplit checking and test
The new code is adapted from the Go 1.2 nosplit code,
but it does not have the bug reported in issue 7623:
g% go run nosplit.go
g% go1.2 run nosplit.go
BUG
rejected incorrectly:
main 0 call f; f 120
linker output:
# _/tmp/go-test-nosplit021064539
main.main: nosplit stack overflow
120 guaranteed after split check in main.main
112 on entry to main.f
-8 after main.f uses 120
g%
Fixes #6931.
Fixes #7623.
LGTM=iant
R=golang-codereviews, iant, ality
CC=golang-codereviews, r
https://golang.org/cl/88190043
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/5e8c9226255b7e63dec1a286888f35782735aada](https://github.com/golang/go/commit/5e8c9226255b7e63dec1a286888f35782735aada)
## 元コミット内容
このコミットの元々の目的は、Go 1.2で導入された`nosplit`コードのスタックチェック機能を再有効化することです。Go 1.2では、`nosplit`関数がスタックオーバーフローを引き起こす可能性を検出するためのチェックが導入されましたが、Issue 7623で報告されたように、一部の正当なプログラムが誤って拒否されるバグがありました。このコミットは、そのバグを修正し、より堅牢な`nosplit`チェックメカニズムを提供することを目指しています。
## 変更の背景
Goランタイムでは、関数の呼び出し時にスタックが不足しないように「スタック分割(stack split)」というメカニズムが採用されています。これは、関数が呼び出される際に、現在のスタックフレームが十分な大きさを持っているかを確認し、不足している場合はより大きなスタックを割り当ててから実行を継続する仕組みです。
しかし、一部の非常に小さく、パフォーマンスが重要な関数や、ランタイムのスタック管理自体に関わる関数(例えば、`runtime.morestack`のようなスタック拡張を行う関数)では、このスタック分割チェックが不要、あるいは望ましくない場合があります。このような関数には`nosplit`という属性を付与することで、リンカーがスタック分割チェックコードを生成しないように指示できます。
`nosplit`関数はスタック分割チェックを行わないため、その関数自身が使用するスタック量と、その関数が呼び出す他の関数が使用するスタック量の合計が、現在のスタックフレームの残りの容量を超えないことをリンカーが保証する必要があります。もし`nosplit`関数がスタックを使い果たした場合、スタックオーバーフローが発生し、プログラムがクラッシュする可能性があります。
Go 1.2ではこの`nosplit`関数のスタックオーバーフローチェックが導入されましたが、Issue 7623("cmd/ld: nosplit stack overflow check rejects valid programs")で報告されたように、リンカーが一部の正当な`nosplit`関数を誤ってスタックオーバーフローの可能性があると判断し、ビルドを拒否するバグがありました。また、Issue 6931("cmd/ld: nosplit stack check is too conservative")では、チェックが過度に保守的である可能性が指摘されていました。
このコミットは、これらの問題を解決し、`nosplit`関数のスタック使用量をより正確に分析し、誤検知を減らしつつ、実際のスタックオーバーフローの可能性を確実に検出できるようにするために行われました。
## 前提知識の解説
### Goのスタック管理とスタック分割
Goの関数は、実行時にスタックと呼ばれるメモリ領域を使用します。スタックは、関数のローカル変数、引数、戻りアドレスなどを格納するために使われます。Goランタイムは、スタックの効率的な利用と、スタックオーバーフローの防止のために、独自のスタック管理メカニズムを持っています。
- **スタック分割 (Stack Split)**: Goの関数は、呼び出し時にスタックの残りの容量をチェックします。もし残りの容量が不足していると判断された場合、ランタイムはより大きな新しいスタックを割り当て、現在のスタックの内容を新しいスタックにコピーし、実行を新しいスタックに切り替えます。このプロセスをスタック分割と呼びます。これにより、Goプログラムは固定サイズのスタックを持つ他の言語に比べて、より柔軟にスタックを使用でき、スタックオーバーフローによるクラッシュを効果的に防ぐことができます。
- **`nosplit`関数**: `nosplit`属性を持つ関数は、スタック分割チェックを行いません。これは通常、以下のような理由で使用されます。
- **パフォーマンス**: 非常に短い関数で、スタックチェックのオーバーヘッドを避けたい場合。
- **ランタイムの内部処理**: スタック管理自体に関わる関数(例: `runtime.morestack`)で、スタックチェックが循環参照を引き起こしたり、不適切であったりする場合。
`nosplit`関数はスタックチェックを行わないため、その関数が使用するスタック量と、その関数が呼び出す他の関数が使用するスタック量の合計が、現在のスタックフレームの残りの容量を超えないことをリンカーが静的に保証する必要があります。
### リンカーの役割
Goのリンカー(`cmd/ld`)は、コンパイルされたオブジェクトファイル(`.o`ファイル)を結合して実行可能ファイルを生成するツールです。この過程で、リンカーはシンボルの解決、再配置(relocation)、そしてスタックチェックのような様々な最適化と検証を行います。
- **再配置 (Relocation)**: コンパイル時にアドレスが確定しないシンボル(関数呼び出しやグローバル変数への参照など)について、リンカーが最終的な実行可能ファイル内での正確なアドレスを決定し、参照を修正するプロセスです。
- **`LSym`構造体**: リンカーがシンボル情報を管理するために使用する内部構造体です。関数、変数、セクションなどの情報が含まれます。このコミットでは、`LSym`に`external`と`nosplit`フィールドが追加されています。
- **`Pcdata`と`Pciter`**: Goの実行可能ファイルには、PC(プログラムカウンタ)と様々なデータ(例: SPオフセット、ファイル番号、行番号)のマッピング情報が格納されています。`Pcdata`はこのデータを表し、`Pciter`はそのデータを効率的にイテレートするための構造体です。スタックチェックでは、PCとSPオフセットのマッピング(`pcsp`)が特に重要になります。
### 関連するGoのIssue
- **Issue 7623**: "cmd/ld: nosplit stack overflow check rejects valid programs"
- Go 1.2の`nosplit`チェックが、実際にはスタックオーバーフローしない正当なプログラムを誤って拒否するというバグ。このコミットの主要な修正対象。
- **Issue 6931**: "cmd/ld: nosplit stack check is too conservative"
- `nosplit`チェックが過度に保守的であり、不必要なエラーを報告する可能性があるという問題。
これらのIssueは、`nosplit`関数のスタックチェックの正確性と堅牢性を向上させる必要性を示しています。
## 技術的詳細
このコミットの技術的な核心は、Goリンカーが`nosplit`関数のスタック使用量を分析し、スタックオーバーフローの可能性を検出するアルゴリズムの改善にあります。
1. **`LSym`構造体の拡張**:
* `include/link.h`において、リンカーのシンボル表現である`LSym`構造体に`external`と`nosplit`という2つの`uchar`型フィールドが追加されました。
* `external`: シンボルが外部から参照される(または外部で定義される)ことを示すフラグ。
* `nosplit`: 関数が`nosplit`属性を持つことを示すフラグ。これにより、リンカーはシンボルレベルで`nosplit`情報を管理できるようになります。
2. **新しい再配置タイプ**:
* `include/link.h`および`src/cmd/link/load.go`で、新しい再配置タイプが導入されました。
* `R_CALLARM`: ARMアーキテクチャにおける直接的なPC相対呼び出しのための再配置。
* `R_CALLIND`: 間接呼び出しを示すマーカー。実際の再配置は不要だが、リンカーが呼び出しタイプを識別するために使用。
* 既存の`R_CALL`は、一般的な直接PC相対呼び出しとして再定義され、`R_CALLARM`がARM固有の呼び出しを扱うようになりました。これにより、アーキテクチャ固有の呼び出し命令の処理がより明確になります。
3. **スタックチェックロジックの改善 (`src/cmd/ld/lib.c`)**:
* `dostkcheck`関数と`stkcheck`関数が大幅に改修されました。これらは、関数の呼び出しチェーンにおけるスタック使用量を分析し、`StackLimit`(スタック分割が行われる閾値)を超えないことを保証する役割を担います。
* **`nosplit`関数の優先処理**: `dostkcheck`は、まず`nosplit`関数を優先的にチェックするように変更されました。これにより、エラーが報告される際に、より短い失敗チェーン(呼び出しスタック)が表示されるようになります。
* **`pcsp`と`Reloc`の利用**: `stkcheck`関数は、従来の`Prog`(命令)リストを直接辿る代わりに、`s->pcln->pcsp`(PCからSPオフセットへのマッピング)と`s->r`(再配置情報)を利用してスタック使用量を計算するようになりました。
* `pcsp`は、特定のPC範囲におけるSPオフセットの変化を示します。これにより、関数内の各PC範囲でどれだけのスタックが使用されているかを正確に追跡できます。
* `Reloc`情報は、関数呼び出し(`R_CALL`, `R_CALLARM`, `R_CALLIND`)を特定するために使用されます。
* **間接呼び出しの扱い**: 間接呼び出し(`R_CALLIND`)は、スタック分割を行う関数への呼び出しであると仮定されます。リンカーは、間接呼び出しが行われる際に、`morestack`を呼び出すための十分なスタック空間があることを確認します。
* **`runtime.morestack`の特殊処理**: `runtime.morestack`への呼び出しが検出された場合、スタック制限が`StackLimit + s->locals`にリセットされます。これは、`morestack`がスタックを拡張する関数であるため、その呼び出し後はスタックが十分に確保されていると見なせるためです。
* **`stkcheck`の最適化**: `stkcheck`は、同じ関数が同じ`limit`(利用可能なスタック量)で既にチェックされている場合は、重複した作業を避けるように最適化されました。
4. **オブジェクトファイルフォーマットの更新 (`src/liblink/objfile.c`, `src/pkg/debug/goobj/read.go`)**:
* Goのオブジェクトファイル(`.o`ファイル)のフォーマットが更新され、`STEXT`タイプのシンボル(関数)に対して`nosplit`フラグが格納されるようになりました。
* `src/liblink/objfile.c`の`writesym`と`readsym`関数が、`nosplit`フィールドの書き込みと読み込みに対応しました。
* `src/pkg/debug/goobj/read.go`の`Func`構造体にも`NoSplit`フィールドが追加され、Goのデバッグツールがオブジェクトファイルから`nosplit`情報を正しく読み取れるようになりました。
5. **リンカーの最適化 (`src/cmd/ld/ldelf.c`, `src/cmd/ld/ldmacho.c`, `src/cmd/ld/ldpe.c`)**:
* 外部シンボルをロードする際のロジックが簡素化されました。以前は、外部シンボルに対してダミーの`TEXT`命令を生成してリンカーの残りの部分を満足させていましたが、このコミットではその処理が削除され、`s->external = 1;`を設定するだけで済むようになりました。これにより、リンカーのコードがクリーンになり、効率が向上します。
6. **`pciterinit`の変更 (`src/cmd/ld/dwarf.c`, `src/liblink/pcln.c`)**:
* `pciterinit`関数が`Link* ctxt`引数を取るように変更されました。これにより、`pciter`が初期化される際にリンカーのコンテキストにアクセスできるようになり、特に`it->pcscale`(PCデータのエントリ間のPCデルタのスケールファクタ)を設定するために使用されます。これは、異なるアーキテクチャやGoのバージョンでPCデータのエンコーディングが異なる場合に対応するために重要です。
7. **包括的なテストケース (`test/nosplit.go`)**:
* `test/nosplit.go`という新しいテストファイルが追加されました。これは、`nosplit`関数のスタックチェックロジックを検証するための包括的なテストスイートです。
* このテストは、カスタムのミニ・アセンブラを使用して、様々な`nosplit`関数のシナリオ(大きなフレーム、再帰、`nosplit`関数のチェーン、スタック制限付近のエッジケースなど)を記述し、リンカーが正しくスタックオーバーフローを検出するか、または正当なケースを誤って拒否しないかを検証します。
* 特に、Issue 7623で報告されたバグを再現し、修正されたことを確認するためのテストケースが含まれています。
これらの変更により、Goリンカーは`nosplit`関数のスタック使用量をより正確に分析し、誤検知を減らしつつ、実際のスタックオーバーフローの可能性を確実に検出できるようになりました。
## コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下のファイルと関数に集中しています。
1. **`src/cmd/ld/lib.c`**:
* `dostkcheck`関数: `nosplit`関数のスタックチェックの全体的なフローを制御します。
* `stkcheck`関数: 個々の関数とその呼び出しチェーンにおけるスタック使用量を再帰的に分析します。`pcsp`と`Reloc`情報を使用してスタック使用量を計算するロジックが大幅に書き換えられました。
2. **`include/link.h`**:
* `LSym`構造体: `external`と`nosplit`フィールドが追加されました。
* 再配置タイプ: `R_CALLARM`と`R_CALLIND`が追加され、既存の`R_CALL`の定義が更新されました。
3. **`test/nosplit.go`**:
* `nosplit`関数のスタックチェックロジックを検証するための新しいテストスイート。様々なシナリオを記述し、リンカーの動作を検証します。
4. **`src/liblink/objfile.c`**:
* `writesym`および`readsym`関数: オブジェクトファイルへの`nosplit`フラグの書き込みと読み込みに対応しました。
5. **`src/pkg/debug/goobj/read.go`**:
* `Func`構造体: `NoSplit`フィールドが追加され、Goオブジェクトファイルの解析時に`nosplit`情報を読み取れるようになりました。
## コアとなるコードの解説
### `src/cmd/ld/lib.c` の `stkcheck` 関数 (抜粋)
```c
static int
stkcheck(Chain *up, int depth)
{
Chain ch, ch1;
int limit;
Reloc *r, *endr;
Pciter pcsp;
limit = up->limit;
s = up->sym;
// Don't duplicate work: only need to consider each
// function at top of safe zone once.
if(limit == StackLimit-callsize()) {
if(s->stkcheck)
return 0;
s->stkcheck = 1;
}
if(depth > 100) {
diag("nosplit stack check too deep");
stkprint(up, limit);
return -1;
}
if(s->external || s->pcln == nil) {
// external function.
// should never be called directly.
// only diagnose the direct caller.
if(depth == 0) {
diag("nosplit stack check: %s calls external function %s", up->sym->name, s->name);
stkprint(up, limit);
return -1;
}
return 0;
}
ch.up = up;
// Walk through sp adjustments in function, consuming relocs.
r = s->r;
endr = r + s->nr;
for(pciterinit(ctxt, &pcsp, &s->pcln->pcsp); !pcsp.done; pciternext(&pcsp)) {
// pcsp.value is in effect for [pcsp.pc, pcsp.nextpc).
// Check stack size in effect for this span.
if(limit - pcsp.value < 0) {
stkbroke(up, limit - pcsp.value);
return -1;
}
// Process calls in this span.
for(; r < endr && r->off < pcsp.nextpc; r++) {
switch(r->type) {
case R_CALL:
case R_CALLARM:
// Direct call.
ch.limit = limit - pcsp.value - callsize();
ch.sym = r->sym;
if(stkcheck(&ch, depth+1) < 0)
return -1;
// If this is a call to morestack, we've just raised our limit back
// to StackLimit beyond the frame size.
if(strncmp(r->sym->name, "runtime.morestack", 17) == 0) {
limit = StackLimit + s->locals;
if(thechar == '5')
limit += 4; // saved LR
}
break;
case R_CALLIND:
// Indirect call. Assume it is a call to a splitting function,
// so we have to make sure it can call morestack.
// Arrange the data structures to report both calls, so that
// if there is an error, stkprint shows all the steps involved.
ch.limit = limit - pcsp.value - callsize();
ch.sym = nil;
ch1.limit = ch.limit - callsize(); // for morestack in called prologue
ch1.up = &ch;
ch1.sym = morestack;
if(stkcheck(&ch1, depth+2) < 0)
return -1;
break;
}
}
}
return 0;
}
このstkcheck
関数は、nosplit
関数のスタック使用量を分析する中心的なロジックです。
limit
: 現在の関数呼び出しチェーンで利用可能なスタックの残り容量を示します。up
: 呼び出し元のChain
構造体へのポインタで、呼び出しスタックを追跡するために使用されます。depth
: 再帰の深さ。無限ループを防ぐために使用されます。- 重複作業の回避:
limit == StackLimit-callsize()
の場合(つまり、安全なゾーンの最上位の呼び出しの場合)、既にチェック済みの関数はスキップされます。 - 外部関数の処理:
s->external
が真の場合、その関数は外部で定義されており、直接呼び出されるべきではありません。depth == 0
(呼び出しチェーンの最上位)で外部関数が呼び出された場合、エラーとして診断されます。 pciterinit
とpcsp
:pciterinit
を使ってs->pcln->pcsp
(PCからSPオフセットへのマッピング)をイテレートします。pcsp.value
は、現在のPC範囲でどれだけのスタックが使用されているかを示します。- スタックサイズチェック:
limit - pcsp.value < 0
の場合、現在のPC範囲でスタックが不足していることを意味し、stkbroke
を呼び出してエラーを報告します。 - 呼び出しの処理:
R_CALL
またはR_CALLARM
の場合(直接呼び出し):呼び出される関数のスタック使用量を考慮してch.limit
を更新し、再帰的にstkcheck
を呼び出します。runtime.morestack
への呼び出しの場合:limit
をStackLimit + s->locals
にリセットします。これは、morestack
がスタックを拡張するため、その呼び出し後はスタックが十分に確保されていると見なせるためです。R_CALLIND
の場合(間接呼び出し):間接呼び出しはスタック分割を行う関数への呼び出しであると仮定し、morestack
を呼び出すための十分なスタック空間があることを確認するために、morestack
への仮想的な呼び出しをシミュレートします。
このロジックにより、リンカーはnosplit
関数の内部で発生する可能性のあるスタックオーバーフローを、より正確かつ詳細に検出できるようになります。
関連リンク
- Go Issue 7623: cmd/ld: nosplit stack overflow check rejects valid programs
- Go Issue 6931: cmd/ld: nosplit stack check is too conservative
- Go Change-Id: https://go.dev/cl/88190043
参考にした情報源リンク
- Goのソースコード(上記コミットの差分)
- GoのIssueトラッカー(Issue 7623, Issue 6931)
- Goのスタック管理に関する一般的なドキュメントや記事(Goのスタック分割、
nosplit
の概念など)- 例: Go's Execution Tracer (スタック分割の概念に触れている)
- 例: Go runtime: stack management (Goのスタック管理に関する詳細)
- 例: Go's linker and relocation (Goリンカーの概要)
- 例: Go Assembly Language (Goのアセンブリ言語と
TEXT
命令のNOSPLIT
フラグについて)
- Goのリンカーの内部構造に関する情報(
LSym
,Pcdata
,Reloc
など)- Goのソースコード内のコメントや関連ファイル (
src/cmd/ld/lib.c
,include/link.h
など)
- Goのソースコード内のコメントや関連ファイル (