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

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

このコミットは、Go言語のリンカ(liblink)において、オブジェクトファイルフォーマットに「リーフビット(leaf bit)」を追加する変更を導入しています。この変更の主な目的は、特にARMアーキテクチャにおいて、正確なスタックトレースを生成するために、リンカがシンボルテーブルに正しいフレームサイズを記録できるようにすることです。リーフビットは、関数が他の関数を呼び出さない(つまり、リーフ関数である)ことを示し、これによりスタックフレームの処理、特にリンクレジスタの保存に関する最適化と正確な情報記録が可能になります。

コミット

commit cc08d9232c4875a11b9e2a8097e069467d79f31f
Author: Russ Cox <rsc@golang.org>
Date:   Wed Apr 16 17:11:44 2014 -0400

    liblink: add leaf bit to object file format
    
    Without the leaf bit, the linker cannot record
    the correct frame size in the symbol table, and
    then stack traces get mangled. (Only for ARM.)
    
    Fixes #7338.
    Fixes #7347.
    
    LGTM=iant
    R=iant
    CC=golang-codereviews
    https://golang.org/cl/88550043

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

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

元コミット内容

liblink: オブジェクトファイルフォーマットにリーフビットを追加。

リーフビットがない場合、リンカはシンボルテーブルに正しいフレームサイズを記録できず、その結果スタックトレースが破損する(ARMのみ)。

Issue #7338 と #7347 を修正。

変更の背景

この変更の背景には、Go言語のランタイム、特にARMアーキテクチャにおけるスタックトレースの正確性の問題がありました。Goのランタイムは、ガベージコレクションやデバッグのために、実行中のゴルーチンのスタック情報を正確に把握する必要があります。スタックトレースは、プログラムの実行パスを追跡し、どの関数がどの関数を呼び出したかを示す重要なデバッグ情報です。

問題は、リンカがオブジェクトファイルから関数に関する情報を読み取る際に、特定の関数(特に「リーフ関数」)のスタックフレームサイズを正確に決定できないことにありました。リーフ関数とは、他の関数を呼び出さない関数のことです。ARMアーキテクチャでは、関数が他の関数を呼び出す場合、呼び出し元のアドレス(リンクレジスタ LR)をスタックに保存する必要があります。しかし、リーフ関数は LR を保存する必要がないため、そのスタックフレームの構造が異なります。

リーフビットがないと、リンカは関数がリーフ関数であるかどうかを判断できず、すべての関数に対して一律に LR の保存を考慮したフレームサイズを計算してしまう可能性がありました。これにより、リーフ関数の実際のスタックフレームサイズと、リンカが記録するサイズとの間に不一致が生じ、結果としてスタックトレースが「破損」する(mangled)という問題が発生していました。これは、デバッグの困難さや、ガベージコレクションの不正確さにつながる可能性がありました。

このコミットは、オブジェクトファイルフォーマットに明示的にリーフビットを追加することで、リンカがこの情報を利用し、ARMアーキテクチャにおけるスタックトレースの正確性を保証することを目的としています。具体的には、Issue #7338 と #7347 で報告された問題がこの変更によって修正されました。

前提知識の解説

このコミットを理解するためには、以下の概念について理解しておく必要があります。

  1. Goのリンカ (liblink): Go言語のビルドプロセスにおいて、liblink はGoコンパイラによって生成されたオブジェクトファイル(.o ファイル)を結合し、実行可能なバイナリを生成する役割を担うライブラリです。リンカは、シンボル解決、アドレスの再配置、実行時情報の埋め込みなど、様々なタスクを実行します。Goのリンカは、C言語のリンカとは異なり、Go独自のオブジェクトファイルフォーマットとランタイムの要件に合わせて設計されています。

  2. オブジェクトファイルフォーマット: Goのコンパイラは、ソースコードをコンパイルしてオブジェクトファイルを生成します。このオブジェクトファイルには、機械語コード、データ、シンボル情報(関数名、変数名、そのアドレスなど)、そしてPCLNテーブルなどのランタイム情報が含まれています。このコミットは、このオブジェクトファイルフォーマットに新しい情報(リーフビット)を追加するものです。

  3. PCLNテーブル (PC-line table): PCLN(Program Counter-Line Number)テーブルは、Goの実行可能バイナリに埋め込まれる重要なデバッグ情報の一つです。これは、プログラムカウンタ(PC)の値と、対応するソースコードのファイル名および行番号とのマッピングを提供します。Goのランタイムは、パニック発生時やプロファイリング時に、このPCLNテーブルを使用してスタックトレースを生成し、人間が読める形式で実行パスを表示します。PCLNテーブルには、関数の開始アドレス、終了アドレス、スタックフレームサイズ、引数のサイズなどの情報も含まれています。

  4. スタックフレームとスタックトレース: 関数が呼び出されるたびに、その関数に必要なローカル変数、引数、戻りアドレスなどを格納するための領域がスタック上に確保されます。この領域を「スタックフレーム」と呼びます。スタックトレースは、現在実行中の関数から始まり、その関数を呼び出した関数、さらにその関数を呼び出した関数と、呼び出しの連鎖を遡って表示したものです。正確なスタックトレースを生成するためには、各関数のスタックフレームサイズを正確に知る必要があります。

  5. リーフ関数 (Leaf function): リーフ関数とは、その関数内で他の関数を一切呼び出さない関数のことです。コンパイラやリンカは、リーフ関数に対して特定の最適化を適用できる場合があります。特にARMアーキテクチャのような一部のISA(命令セットアーキテクチャ)では、関数呼び出しの際にリンクレジスタ(LR)に呼び出し元のアドレスを保存しますが、リーフ関数は LR を保存する必要がないため、スタックフレームの構造が非リーフ関数とは異なります。この違いをリンカが認識することが、正確なスタックフレームサイズの計算に不可欠です。

  6. src/pkg/debug/goobj/read.go: このファイルは、Goのオブジェクトファイルを読み取るためのパッケージ debug/goobj の一部です。このパッケージは、Goのツール(例えば、go tool objdump など)がオブジェクトファイルの内容を解析するために使用されます。このコミットでは、新しいリーフビット情報をオブジェクトファイルから読み取れるように、このパッケージの構造体と読み取りロジックが更新されています。

技術的詳細

このコミットの技術的な核心は、Goのオブジェクトファイルフォーマットに leaf ビットを追加し、その情報をリンカとデバッグツールが利用できるようにすることです。

  1. オブジェクトファイルフォーマットへの leaf ビットの追加: src/liblink/objfile.cwritesym 関数と readsym 関数が変更され、シンボル情報の一部として s->leaf という新しいフィールドが読み書きされるようになりました。 writesym 関数では、STEXT (テキストセクション、つまり関数コード) のシンボルを書き出す際に、s->leaf の値がオブジェクトファイルに書き込まれます。 readsym 関数では、オブジェクトファイルを読み込む際に、この leaf ビットが LSym 構造体の leaf フィールドに読み込まれます。 これにより、リンカは各関数がリーフ関数であるかどうかの情報をオブジェクトファイルから直接取得できるようになります。

  2. PCLNテーブルにおけるフレームサイズの計算の修正 (src/cmd/ld/pcln.c): pclntab 関数は、PCLNテーブルを構築する際に、各関数のスタックフレームサイズを計算します。 変更前は、ctxt->cursym->locals + PtrSize という式でフレームサイズを計算していました。ここで PtrSize はポインタのサイズ(通常4バイトまたは8バイト)であり、これはリンクレジスタ(LR)の保存に必要な領域を考慮したものです。 変更後、ctxt->cursym->leaftrue の場合、frameptrsize0 に設定されます。これは、リーフ関数は LR をスタックに保存する必要がないため、その分のサイズをフレームサイズに含める必要がないことを意味します。 最終的なフレームサイズは ctxt->cursym->locals + frameptrsize となり、リーフ関数では PtrSize 分が減算されることで、より正確なフレームサイズがPCLNテーブルに記録されるようになります。

  3. debug/goobj パッケージの更新 (src/pkg/debug/goobj/read.go): debug/goobj パッケージは、Goのオブジェクトファイルを解析するためのAPIを提供します。このコミットでは、Func 構造体に Leaf bool フィールドが追加されました。 parseObject 関数内で、オブジェクトファイルから関数情報を読み取る際に、新しく追加されたリーフビットが読み取られ、Func 構造体の Leaf フィールドに設定されます。 これにより、debug/goobj を利用するツール(例えば、go tool objdump やデバッガ)が、関数のリーフ情報を正確に取得できるようになります。

これらの変更により、リンカは各関数の特性(リーフ関数であるか否か)を正確に把握し、それに基づいてスタックフレームサイズを計算できるようになります。特にARMアーキテクチャでは、この正確なフレームサイズ情報がスタックトレースの破損を防ぎ、デバッグやプロファイリングの精度を向上させる上で不可欠です。

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

src/cmd/ld/pcln.c

--- a/src/cmd/ld/pcln.c
+++ b/src/cmd/ld/pcln.c
@@ -112,7 +112,7 @@ pclntab(void)
 {
 	int32 i, nfunc, start, funcstart;
 	LSym *ftab, *s;
-	int32 off, end;
+	int32 off, end, frameptrsize;
 	int64 funcdata_bytes;
 	Pcln *pcln;
 	Pciter it;
@@ -173,7 +173,10 @@ pclntab(void)
 		// when a called function doesn't have argument information.
 		// We need to make sure everything has argument information
 		// and then remove this.
-		off = setuint32(ctxt, ftab, off, ctxt->cursym->locals + PtrSize);
+		frameptrsize = PtrSize;
+		if(ctxt->cursym->leaf)
+			frameptrsize = 0;
+		off = setuint32(ctxt, ftab, off, ctxt->cursym->locals + frameptrsize);
 		
 		if(pcln != &zpcln) {
 			renumberfiles(pcln->file, pcln->nfile, &pcln->pcfile);

src/liblink/objfile.c

--- a/src/liblink/objfile.c
+++ b/src/liblink/objfile.c
@@ -49,6 +49,7 @@
 //
 //	- args [int]
 //	- locals [int]
+//	- leaf [int]
 //	- nlocal [int]
 //	- local [nlocal automatics]
 //	- pcln [pcln table]
@@ -291,8 +292,11 @@ writesym(Link *ctxt, Biobuf *b, LSym *s)
 		if(s->dupok)
 			Bprint(ctxt->bso, "dupok ");
 		Bprint(ctxt->bso, "size=%lld value=%lld", (vlong)s->size, (vlong)s->value);
-		if(s->type == STEXT)
+		if(s->type == STEXT) {
 			Bprint(ctxt->bso, " args=%#llux locals=%#llux", (uvlong)s->args, (uvlong)s->locals);
+			if(s->leaf)
+				Bprint(ctxt->bso, " leaf");
+		}
 		Bprint(ctxt->bso, "\n");
 		for(p=s->text; p != nil; p = p->link)
 			Bprint(ctxt->bso, "\t%#06ux %P\n", (int)p->pc, p);
@@ -346,6 +350,7 @@ writesym(Link *ctxt, Biobuf *b, LSym *s)
 	if(s->type == STEXT) {
 		wrint(b, s->args);
 		wrint(b, s->locals);
+		wrint(b, s->leaf);
 		n = 0;
 		for(a = s->autom; a != nil; a = a->link)
 			n++;
@@ -566,6 +571,7 @@ readsym(Link *ctxt, Biobuf *f, char *pkg, char *pn)
 	if(s->type == STEXT) {
 		s->args = rdint(f);
 		s->locals = rdint(f);
+		s->leaf = rdint(f);
 		n = rdint(f);
 		for(i=0; i<n; i++) {
 			a = emallocz(sizeof *a);

src/pkg/debug/goobj/read.go

--- a/src/pkg/debug/goobj/read.go
+++ b/src/pkg/debug/goobj/read.go
@@ -190,6 +190,7 @@ type Var struct {
 type Func struct {
 	Args     int        // size in bytes of of argument frame: inputs and outputs
 	Frame    int        // size in bytes of local variable frame
+	Leaf     bool       // function omits save of link register (ARM)
 	Var      []Var      // detail about local variables
 	PCSP     Data       // PC → SP offset map
 	PCFile   Data       // PC → file number map (index into File)
@@ -621,6 +622,7 @@ func (r *objReader) parseObject(prefix []byte) error {\
 		s.Func = f
 		f.Args = r.readInt()
 		f.Frame = r.readInt()
+		f.Leaf = r.readInt() != 0
 		f.Var = make([]Var, r.readInt())
 		for i := range f.Var {
 			v := &f.Var[i]

コアとなるコードの解説

src/cmd/ld/pcln.c の変更点

このファイルは、GoのPCLN(Program Counter-Line Number)テーブルを生成するリンカの一部です。PCLNテーブルは、実行時のスタックトレースやプロファイリングに不可欠な情報を含んでいます。

@@ -112,7 +112,7 @@ pclntab(void)
  {
  	int32 i, nfunc, start, funcstart;
  	LSym *ftab, *s;
-	int32 off, end;
+	int32 off, end, frameptrsize; // frameptrsize が追加された
  	int64 funcdata_bytes;
  	Pcln *pcln;
  	Pciter it;
@@ -173,7 +173,10 @@ pclntab(void)
  		// when a called function doesn't have argument information.
  		// We need to make sure everything has argument information
  		// and then remove this.
-		off = setuint32(ctxt, ftab, off, ctxt->cursym->locals + PtrSize); // 変更前
+		frameptrsize = PtrSize; // デフォルトでポインタサイズ分を考慮
+		if(ctxt->cursym->leaf) // 現在のシンボル (関数) がリーフ関数である場合
+			frameptrsize = 0; // ポインタサイズ分は不要 (リンクレジスタを保存しないため)
+		off = setuint32(ctxt, ftab, off, ctxt->cursym->locals + frameptrsize); // 変更後
  		
  		if(pcln != &zpcln) {
  			renumberfiles(pcln->file, pcln->nfile, &pcln->pcfile);
  • frameptrsize 変数の導入: 新しく frameptrsize という変数が導入されました。これは、スタックフレームのサイズ計算において、リンクレジスタ(LR)などのポインタを保存するための領域を考慮するかどうかを制御します。
  • リーフ関数に応じた frameptrsize の調整:
    • デフォルトでは frameptrsizePtrSize(ポインタのサイズ、通常4バイトまたは8バイト)に設定されます。これは、一般的な関数が呼び出し元のアドレス(リンクレジスタ)をスタックに保存することを想定しています。
    • しかし、ctxt->cursym->leaftrue(つまり、現在の関数がリーフ関数である)の場合、frameptrsize0 に設定されます。これは、リーフ関数は他の関数を呼び出さないため、リンクレジスタをスタックに保存する必要がないという最適化を反映しています。
  • スタックフレームサイズの計算: setuint32(ctxt, ftab, off, ctxt->cursym->locals + frameptrsize) の行で、最終的なスタックフレームサイズがPCLNテーブルに書き込まれます。この変更により、リーフ関数では PtrSize 分がフレームサイズから除外され、より正確なスタックフレームサイズが記録されるようになります。

src/liblink/objfile.c の変更点

このファイルは、Goのオブジェクトファイルの読み書きを扱うリンカのコア部分です。

@@ -49,6 +49,7 @@
  //
  //	- args [int]
  //	- locals [int]
+//	- leaf [int] // 新しく leaf フィールドが追加された
  //	- nlocal [int]
  //	- local [nlocal automatics]
  //	- pcln [pcln table]
@@ -291,8 +292,11 @@ writesym(Link *ctxt, Biobuf *b, LSym *s)
  		if(s->dupok)
  			Bprint(ctxt->bso, "dupok ");
  		Bprint(ctxt->bso, "size=%lld value=%lld", (vlong)s->size, (vlong)s->value);
-		if(s->type == STEXT)
+		if(s->type == STEXT) { // STEXT (関数) シンボルの場合
  			Bprint(ctxt->bso, " args=%#llux locals=%#llux", (uvlong)s->args, (uvlong)s->locals);
+			if(s->leaf) // leaf が true の場合、" leaf" を出力
+				Bprint(ctxt->bso, " leaf");
+		}
  		Bprint(ctxt->bso, "\n");
  		for(p=s->text; p != nil; p = p->link)
  			Bprint(ctxt->bso, "\t%#06ux %P\n", (int)p->pc, p);
@@ -346,6 +350,7 @@ writesym(Link *ctxt, Biobuf *b, LSym *s)
  	if(s->type == STEXT) {
  		wrint(b, s->args);
  		wrint(b, s->locals);
+		wrint(b, s->leaf); // s->leaf の値をオブジェクトファイルに書き込む
  		n = 0;
  		for(a = s->autom; a != nil; a = a->link)
  			n++;
@@ -566,6 +571,7 @@ readsym(Link *ctxt, Biobuf *f, char *pkg, char *pn)
  	if(s->type == STEXT) {
  		s->args = rdint(f);
  		s->locals = rdint(f);
+		s->leaf = rdint(f); // オブジェクトファイルから leaf の値を読み込む
  		n = rdint(f);
  		for(i=0; i<n; i++) {
  			a = emallocz(sizeof *a);
  • オブジェクトファイルフォーマットの更新: コメント行に leaf [int] が追加され、オブジェクトファイル内の関数シンボル情報に leaf フィールドが追加されたことを示しています。
  • writesym 関数:
    • STEXT タイプのシンボル(関数)をオブジェクトファイルに書き出す際に、s->leaf の値も書き込まれるようになりました。これにより、リンカは関数がリーフ関数であるかどうかの情報をオブジェクトファイルに永続化できます。
    • デバッグ出力 (Bprint) にも leaf フラグが表示されるようになりました。
  • readsym 関数:
    • オブジェクトファイルを読み込む際に、s->leaf = rdint(f); の行が追加され、オブジェクトファイルから leaf の値が読み取られ、LSym 構造体の leaf フィールドに設定されるようになりました。これにより、リンカはオブジェクトファイルからリーフ情報を取得できるようになります。

src/pkg/debug/goobj/read.go の変更点

このファイルは、Goのオブジェクトファイルを解析するための debug/goobj パッケージの一部です。

@@ -190,6 +190,7 @@ type Var struct {
  type Func struct {
  	Args     int        // size in bytes of of argument frame: inputs and outputs
  	Frame    int        // size in bytes of local variable frame
+	Leaf     bool       // function omits save of link register (ARM) // Leaf フィールドが追加された
  	Var      []Var      // detail about local variables
  	PCSP     Data       // PC → SP offset map
  	PCFile   Data       // PC → file number map (index into File)
@@ -621,6 +622,7 @@ func (r *objReader) parseObject(prefix []byte) error {
  		s.Func = f
  		f.Args = r.readInt()
  		f.Frame = r.readInt()
+		f.Leaf = r.readInt() != 0 // オブジェクトファイルから leaf の値を読み取り、bool 型に変換して設定
  		f.Var = make([]Var, r.readInt())
  		for i := range f.Var {
  			v := &f.Var[i]
  • Func 構造体への Leaf フィールドの追加: Func 構造体に Leaf bool フィールドが追加されました。これは、関数がリンクレジスタの保存を省略するかどうか(つまり、リーフ関数であるかどうか)を示すブール値です。
  • parseObject 関数での Leaf フィールドの読み取り: parseObject 関数内で、オブジェクトファイルから関数情報を読み取る際に、f.Leaf = r.readInt() != 0 の行が追加されました。これにより、オブジェクトファイルに書き込まれたリーフビットが読み取られ、Func 構造体の Leaf フィールドに設定されます。readInt() は整数値を読み取るため、!= 0 でブール値に変換しています。

これらの変更により、Goのビルドツールチェーン全体でリーフ関数の情報が適切に伝達され、特にARMアーキテクチャにおけるスタックトレースの正確性が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (上記コミットのファイル群)
  • Go言語のIssueトラッカー (上記関連リンク)
  • Go言語のドキュメント (一般的なGoのビルドプロセス、ランタイム、リンカに関する情報)
  • ARMアーキテクチャの関数呼び出し規約に関する一般的な知識
  • スタックフレームとスタックトレースに関する一般的なコンピュータサイエンスの知識
  • Goのオブジェクトファイルフォーマットに関する一般的な知識 (Goのソースコードや関連ツールから推測)