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

[インデックス 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構造体: リンカーがシンボル情報を管理するために使用する内部構造体です。関数、変数、セクションなどの情報が含まれます。このコミットでは、LSymexternalnosplitフィールドが追加されています。
  • PcdataPciter: 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構造体にexternalnosplitという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関数を優先的にチェックするように変更されました。これにより、エラーが報告される際に、より短い失敗チェーン(呼び出しスタック)が表示されるようになります。
    • pcspRelocの利用: 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.cwritesymreadsym関数が、nosplitフィールドの書き込みと読み込みに対応しました。
    • src/pkg/debug/goobj/read.goFunc構造体にも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関数: 個々の関数とその呼び出しチェーンにおけるスタック使用量を再帰的に分析します。pcspReloc情報を使用してスタック使用量を計算するロジックが大幅に書き換えられました。
  2. include/link.h:

    • LSym構造体: externalnosplitフィールドが追加されました。
    • 再配置タイプ: R_CALLARMR_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.cstkcheck 関数 (抜粋)

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(呼び出しチェーンの最上位)で外部関数が呼び出された場合、エラーとして診断されます。
  • pciterinitpcsp: pciterinitを使ってs->pcln->pcsp(PCからSPオフセットへのマッピング)をイテレートします。pcsp.valueは、現在のPC範囲でどれだけのスタックが使用されているかを示します。
  • スタックサイズチェック: limit - pcsp.value < 0の場合、現在のPC範囲でスタックが不足していることを意味し、stkbrokeを呼び出してエラーを報告します。
  • 呼び出しの処理:
    • R_CALLまたはR_CALLARMの場合(直接呼び出し):呼び出される関数のスタック使用量を考慮してch.limitを更新し、再帰的にstkcheckを呼び出します。
    • runtime.morestackへの呼び出しの場合:limitStackLimit + s->localsにリセットします。これは、morestackがスタックを拡張するため、その呼び出し後はスタックが十分に確保されていると見なせるためです。
    • R_CALLINDの場合(間接呼び出し):間接呼び出しはスタック分割を行う関数への呼び出しであると仮定し、morestackを呼び出すための十分なスタック空間があることを確認するために、morestackへの仮想的な呼び出しをシミュレートします。

このロジックにより、リンカーはnosplit関数の内部で発生する可能性のあるスタックオーバーフローを、より正確かつ詳細に検出できるようになります。

関連リンク

参考にした情報源リンク

  • Goのソースコード(上記コミットの差分)
  • GoのIssueトラッカー(Issue 7623, Issue 6931)
  • Goのスタック管理に関する一般的なドキュメントや記事(Goのスタック分割、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.
# [インデックス 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(呼び出しチェーンの最上位)で外部関数が呼び出された場合、エラーとして診断されます。
  • pciterinitpcsp: pciterinitを使ってs->pcln->pcsp(PCからSPオフセットへのマッピング)をイテレートします。pcsp.valueは、現在のPC範囲でどれだけのスタックが使用されているかを示します。
  • スタックサイズチェック: limit - pcsp.value < 0の場合、現在のPC範囲でスタックが不足していることを意味し、stkbrokeを呼び出してエラーを報告します。
  • 呼び出しの処理:
    • R_CALLまたはR_CALLARMの場合(直接呼び出し):呼び出される関数のスタック使用量を考慮してch.limitを更新し、再帰的にstkcheckを呼び出します。
    • runtime.morestackへの呼び出しの場合:limitStackLimit + s->localsにリセットします。これは、morestackがスタックを拡張するため、その呼び出し後はスタックが十分に確保されていると見なせるためです。
    • R_CALLINDの場合(間接呼び出し):間接呼び出しはスタック分割を行う関数への呼び出しであると仮定し、morestackを呼び出すための十分なスタック空間があることを確認するために、morestackへの仮想的な呼び出しをシミュレートします。

このロジックにより、リンカーはnosplit関数の内部で発生する可能性のあるスタックオーバーフローを、より正確かつ詳細に検出できるようになります。

関連リンク

参考にした情報源リンク

  • Goのソースコード(上記コミットの差分)
  • GoのIssueトラッカー(Issue 7623, Issue 6931)
  • Goのスタック管理に関する一般的なドキュメントや記事(Goのスタック分割、nosplitの概念など)
  • Goのリンカーの内部構造に関する情報(LSym, Pcdata, Relocなど)
    • Goのソースコード内のコメントや関連ファイル (src/cmd/ld/lib.c, include/link.hなど)