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

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

このコミットは、Go言語のリンカである6lにおける2つの重要な問題を修正します。一つは、ファイルの末尾にあるデッドコード(到達不能なコード)が削除される際に、関連する行番号の履歴が失われる問題です。もう一つは、Valgrindによって発見された、初期化されていないメモリ使用によるエラーの修正です。これにより、デバッグ情報の正確性が向上し、リンカの安定性が確保されます。

コミット

  • コミットハッシュ: 47e27758dbe26e17aa0955780f37f41036151a2a
  • Author: Russ Cox rsc@golang.org
  • Date: Fri Jan 30 17:10:10 2009 -0800

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

https://github.com/golang/go/commit/47e27758dbe26e17aa0955780f37f41036151a2a

元コミット内容

keep line number history even when
throwing away dead code at end of file.

also fix an uninitialized memory error
found by valgrind.

R=r
DELTA=7  (5 added, 2 deleted, 0 changed)
OCL=23991
CL=23994

変更の背景

このコミットは、Go言語の初期開発段階におけるリンカの品質向上を目的としています。

  1. 行番号履歴の保持: Goプログラムのデバッグやプロファイリングにおいて、正確な行番号情報は不可欠です。リンカがデッドコードを削除する際に、そのコードに関連する行番号情報まで破棄してしまうと、デバッグ時にスタックトレースやエラーメッセージが行番号を正確に示せなくなる問題が発生します。特にファイルの末尾に存在するデッドコードの場合、この問題が顕著でした。このコミットは、デッドコードが削除されても、その行番号履歴が適切に保持されるようにリンカの挙動を修正します。

  2. Valgrindによる未初期化メモリのエラー修正: Valgrindは、メモリ関連のバグ(メモリリーク、未初期化メモリの使用、無効なメモリアクセスなど)を検出するための強力なツールです。GoリンカのコードベースをValgrindで解析した結果、未初期化のメモリが使用されている箇所が発見されました。未初期化メモリの使用は、プログラムの予測不能な動作、クラッシュ、またはセキュリティ脆弱性につながる可能性があるため、早期に修正する必要がありました。

これらの問題は、Go言語の安定性とデバッグ体験に直接影響するため、重要な修正とされました。

前提知識の解説

Go 6l リンカ

6lは、Go言語の初期バージョンで使用されていたリンカの一つで、特に64ビットx86アーキテクチャ(amd64)向けに設計されていました。Goツールチェーンにおけるリンカの主な役割は以下の通りです。

  • コンパイル済みコードの結合: コンパイラによって生成されたオブジェクトファイル(.oファイル)や、Go標準ライブラリ、その他の依存関係を結合し、単一の実行可能バイナリを生成します。
  • シンボルの解決: 関数や変数の参照を、それらが定義されている実際のメモリ位置に解決します。
  • デッドコード削除 (Dead Code Elimination - DCE): プログラムのエントリポイント(通常はmain関数)から到達できない関数や変数などのコードを特定し、最終的なバイナリから削除します。これにより、実行ファイルのサイズが削減されます。
  • 再配置 (Relocation): コンパイル時には不明だった、他のパッケージの関数呼び出しやデータへの参照の最終的なメモリ位置を決定し、アドレスを割り当てます。
  • DWARF情報の生成: デバッグ情報(ソースコードの行番号、変数情報など)を生成し、バイナリに埋め込みます。これはデバッガがプログラムの実行を追跡するために使用します。

現在のGoでは、6lのようなアーキテクチャ固有のリンカコマンドは、go buildコマンドを通じて間接的に呼び出される統合されたリンカに置き換えられています。

Valgrindと未初期化メモリのエラー

Valgrindは、主にC/C++プログラムのメモリ管理とスレッド関連のバグを検出するためのオープンソースのインストゥルメンテーションフレームワークです。その中でもMemcheckツールは、メモリリーク、無効なメモリアクセス、そして「未初期化メモリの使用」を検出するのに非常に優れています。

未初期化メモリのエラーとは、プログラムが明示的に値が割り当てられていないメモリ領域からデータを読み取ろうとしたときに発生する問題です。C言語では、ローカル変数やmallocで確保されたメモリは、初期化されない限り、そのメモリ位置に以前存在していた「ゴミ」データを含んでいます。このゴミデータを使用すると、プログラムの動作が予測不能になったり、クラッシュしたり、誤った結果を生成したりする可能性があります。

Valgrindが「Conditional jump or move depends on uninitialized value(s)」のようなエラーを報告する場合、それはプログラムの条件分岐(if文、forループ、whileループなど)が未初期化の値に基づいて行われていることを意味します。--track-origins=yesオプションをValgrindに渡すことで、未初期化の値がどこで生成されたかを追跡し、デバッグを大幅に支援できます。

デッドコード削除と行番号情報

Goリンカのデッドコード削除は、最終的な実行ファイルのサイズを最適化するために不可欠なプロセスです。しかし、このプロセスはデバッグ情報、特に行番号情報に影響を与えます。リンカがコードを「デッド」と判断してバイナリから削除すると、その削除されたコードに関連する行番号データも当然ながらバイナリから取り除かれます。

通常、これは問題ありません。なぜなら、デッドコードは実行されないため、その行番号情報がデバッグ時に必要になることはないからです。しかし、リンカの実装によっては、デッドコードの削除が意図せず、生きているコードの行番号履歴に影響を与えたり、特定の状況下でデバッグ情報が不完全になったりする可能性があります。このコミットの目的は、リンカがデッドコードを削除する際にも、デバッグに必要な行番号履歴が適切に保持されるようにすることです。

技術的詳細

このコミットは、Goリンカの6lにおける2つの異なる問題に対処しています。

  1. 行番号履歴の保持: src/cmd/6l/span.cの変更は、リンカがシンボル情報を処理する際のロジックを調整しています。以前のコードでは、STEXT型(テキストセクション、つまり実行可能なコード)ではないシンボルを早期にスキップしていました。しかし、ファイルの末尾にあるデッドコードの場合、そのコード自体は実行されなくても、関連する行番号情報(ヒストリ)はデバッグのために保持されるべきでした。このコミットでは、if(s->type != STEXT) continue;というチェックを、ファイル名やその他の自動生成シンボルを処理した後、かつputsymbでシンボルを書き出す直前に移動しています。これにより、STEXTではないシンボル(例えば、デッドコードに関連するシンボル)であっても、ファイル名などのデバッグ関連情報が適切に処理された後でスキップされるようになり、行番号履歴が不必要に失われることを防ぎます。

  2. 未初期化メモリのエラー修正: src/cmd/6l/obj.caddhist関数における変更は、Valgrindによって検出された未初期化メモリのエラーを修正します。addhist関数は、リンカがソースコードの行番号履歴を構築する際に使用されます。s->nameはシンボルの名前を格納するためのバッファですが、このバッファが適切にヌル終端されていない場合、後続の処理で未初期化のメモリが読み取られる可能性があります。 追加されたs->name[0] = 0;は、s->nameバッファの先頭を明示的にヌル文字で初期化します。また、s->name[j] = 0;s->name[j+1] = 0;は、histfrog配列から取得した値がs->nameにコピーされた後、バッファの末尾を確実にヌル終端します。これにより、s->nameが常に有効な文字列として扱われ、未初期化メモリの読み取りを防ぎます。

これらの修正は、リンカの内部的なデータ構造の整合性を保ち、デバッグ情報の正確性を高めるとともに、メモリ安全性を向上させるものです。

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

src/cmd/6l/obj.c

--- a/src/cmd/6l/obj.c
+++ b/src/cmd/6l/obj.c
@@ -757,6 +757,7 @@ addhist(int32 line, int type)
 	u->link = curhist;
 	curhist = u;
 
+	s->name[0] = 0;
 	j = 1;
 	for(i=0; i<histfrogp; i++) {
 		k = histfrog[i]->value;
@@ -764,6 +765,8 @@ addhist(int32 line, int type)
 		s->name[j+1] = k;
 		j += 2;
 	}
+	s->name[j] = 0;
+	s->name[j+1] = 0;
 }
 
 void

src/cmd/6l/span.c

--- a/src/cmd/6l/span.c
+++ b/src/cmd/6l/span.c
@@ -240,8 +240,6 @@ asmsym(void)
 
 	for(p=textp; p!=P; p=p->pcond) {
 		s = p->from.sym;
-\t\tif(s->type != STEXT)
-\t\t\tcontinue;
 
 		/* filenames first */
 		for(a=p->to.autom; a; a=a->link)
@@ -251,6 +249,8 @@ asmsym(void)
 			if(a->type == D_FILE1)
 				putsymb(a->asym->name, 'Z', a->aoffset, 0, nil);
 
+		if(s->type != STEXT)
+			continue;
 		putsymb(s->name, 'T', s->value, s->version, gotypefor(s->name));
 
 		/* frame, auto and param after */

コアとなるコードの解説

src/cmd/6l/obj.c の変更

addhist関数は、リンカがシンボルの行番号履歴を記録する際に呼び出されます。

  • s->name[0] = 0; の追加: これは、s->nameというシンボル名を格納するバッファの最初のバイトをヌル文字(\0)で初期化しています。これにより、s->nameが常に有効なC文字列として開始されることが保証され、Valgrindが検出した未初期化メモリの使用エラーを防ぎます。特に、histfrog配列からデータがコピーされる前に、バッファがクリーンな状態であることを保証します。

  • s->name[j] = 0;s->name[j+1] = 0; の追加: histfrog配列からの値のコピーが完了した後、s->nameバッファの末尾にヌル文字を2つ追加しています。これは、s->nameが常に適切にヌル終端された文字列であることを保証するためのものです。jはコピーされたデータの長さを追跡しており、jj+1にヌル文字を置くことで、文字列の終端を明確にし、後続の文字列操作関数が未初期化メモリを読み込むことを防ぎます。

これらの変更は、s->nameバッファの初期化とヌル終端を徹底することで、メモリ安全性を向上させ、Valgrindのエラーを解消します。

src/cmd/6l/span.c の変更

asmsym関数は、リンカがアセンブリシンボルを処理する部分です。

  • if(s->type != STEXT) continue; の移動: 元のコードでは、ループの早い段階でs->typeSTEXT(実行可能なコード)でない場合に、そのシンボルの処理をスキップしていました。 変更後のコードでは、このチェックがfilenames firstとコメントされたセクション(ファイル名などのデバッグ関連情報を処理する部分)の後に移動されています。 これにより、STEXTではないシンボル(例えば、デッドコードに関連するシンボルや、データセクションのシンボル)であっても、D_FILE1のようなファイル名シンボルが先に処理されるようになります。これは、デッドコードであっても、そのコードが元々どのファイルに属していたかという情報(行番号履歴の一部)をリンカが保持できるようにするために重要です。putsymb関数が呼び出される直前にチェックを移動することで、シンボル自体の出力はSTEXTに限定しつつ、関連するデバッグ情報が適切に処理されることを保証します。

この変更は、リンカがデッドコードを削除する際にも、デバッグに必要な行番号履歴が適切に保持されるようにするためのロジックの調整です。

関連リンク

参考にした情報源リンク