[インデックス 170] ファイルの概要
このコミットは、Go言語の初期段階における実行ファイル(6.out
形式)がシンボルテーブルと行番号情報をサポートするように拡張するものです。具体的には、libmach_amd64
ライブラリ内の実行ファイル解析ロジックと、ビルドスクリプトに変更が加えられています。これにより、デバッグ時により詳細な情報(関数名、変数名、ソースコードの行番号など)が利用可能になり、開発者がプログラムの挙動を追跡しやすくなります。
コミット
commit 34691ccd10e4a27184ad80ad09fb7227f2e9644f
Author: Rob Pike <r@golang.org>
Date: Fri Jun 13 12:55:37 2008 -0700
support symbol tables and line numbers in 6.out executables
SVN=122700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/34691ccd10e4a27184ad80ad09fb7227f2e9644f
元コミット内容
6.out
形式の実行ファイルにおいて、シンボルテーブルと行番号のサポートを追加する。
変更の背景
Go言語の初期開発段階において、生成される実行ファイルにはデバッグに必要なシンボル情報や行番号情報が十分に埋め込まれていませんでした。これにより、デバッガやプロファイラがプログラムの実行状態を正確に解析することが困難でした。このコミットは、これらのデバッグ情報を実行ファイルに含めることで、開発者がGoプログラムのデバッグやプロファイリングをより効率的に行えるようにすることを目的としています。特に、6.out
という形式は、Go言語が初期に採用していた独自の実行ファイルフォーマットであり、このフォーマットに対するデバッグ情報の追加は、当時のGo開発環境の成熟に不可欠なステップでした。
前提知識の解説
6.out
形式: Go言語の初期(特にPlan 9のツールチェインをベースにしていた頃)に、go build
コマンドによって生成されていた実行ファイル形式です。これは、一般的なELF(Executable and Linkable Format)やPE(Portable Executable)とは異なる、Go独自のバイナリフォーマットでした。Goが成熟するにつれて、より標準的なELF(Linux)、Mach-O(macOS)、PE(Windows)形式に移行していきました。- シンボルテーブル (Symbol Table): 実行ファイルやオブジェクトファイル内に含まれるデータ構造で、プログラム内のシンボル(関数名、グローバル変数名など)とそのアドレスや型などの情報が格納されています。デバッガはシンボルテーブルを利用して、ソースコードの変数名や関数名と、実行時のメモリ上のアドレスを対応付けます。
- 行番号情報 (Line Number Information): ソースコードの特定の行と、それに対応するコンパイル済みコード(機械語命令)のアドレスをマッピングする情報です。デバッガはこれを利用して、プログラムの実行がソースコードのどの行に到達したかを正確に表示できます。
- ELF (Executable and Linkable Format): Unix系システムで広く使われている実行ファイル、オブジェクトファイル、共有ライブラリの標準フォーマットです。Go言語も最終的にはこのフォーマットを採用しています。
- セクションヘッダ (Section Header): ELFファイルにおいて、実行ファイル内の各セクション(コード、データ、シンボルテーブルなど)の属性(サイズ、オフセット、パーミッションなど)を記述する構造体です。
- プログラムヘッダ (Program Header): ELFファイルにおいて、プログラムのロード方法を記述する構造体です。実行時にメモリにロードされるセグメント(実行可能コード、初期化済みデータなど)の情報を持ちます。
.gosymtab
: Go言語の実行ファイルに特有のシンボルテーブルセクションです。GoのランタイムやデバッガがGo固有のシンボル情報を解析するために使用します。.gopclntab
: Go言語の実行ファイルに特有のPC-Lineテーブルセクションです。プログラムカウンタ(PC)とソースコードの行番号のマッピング情報が含まれており、スタックトレースの生成やデバッグ時に利用されます。
技術的詳細
このコミットの主要な変更は、src/libmach_amd64/executable.c
ファイルに集中しています。このファイルは、6.out
形式の実行ファイルを解析し、その構造を理解するためのロジックを含んでいます。
変更前は、elf64dotout
関数がELF64形式の実行ファイルを解析する際に、プログラムヘッダ(Phdr64
)のみを読み込んでいました。プログラムヘッダは主に実行時のメモリレイアウトを定義しますが、デバッグ情報(シンボルテーブルや行番号)は通常、セクションヘッダ(Shdr64
)によって記述されるセクションに格納されます。
このコミットでは、以下の点が変更されています。
- セクションヘッダの読み込み:
elf64dotout
関数内で、Shdr64 *sh;
とshsz
変数が追加され、ELFヘッダのshoff
(セクションヘッダテーブルのオフセット)とshnum
(セクションヘッダの数)を利用して、セクションヘッダテーブル全体をメモリに読み込むようになりました。これにより、実行ファイル内のすべてのセクションの情報を参照できるようになります。 - エラーハンドリングの改善:
error:
ラベルが追加され、ph
だけでなくsh
も解放されるようにエラー処理が改善されています。 - Go固有のシンボル/PC-Lineテーブルの検出と設定:
else if(ep->machine == AMD64 && sh != 0)
ブロックが追加されました。これは、AMD64アーキテクチャの実行ファイルで、かつセクションヘッダが正常に読み込まれた場合にのみ実行されます。- セクション名文字列テーブル(
shstrndx
で指定されるセクション)を読み込み、各セクションの名前を解析します。 - セクションをループし、名前が「
.gosymtab
」と「.gopclntab
」であるセクションを探します。 .gosymtab
と.gopclntab
のサイズとオフセットを取得し、これらが連続しているか(.gopclntab
が.gosymtab
の直後に続くか)を検証します。これは、Goのツールチェインがこれらのセクションを特定の順序で配置することを前提としているためです。setsym
関数を呼び出し、取得した.gosymtab
と.gopclntab
のオフセットとサイズを渡します。setsym
関数は、これらの情報をFhdr
構造体(ファイルヘッダ、おそらくデバッガが使用する抽象化された実行ファイル情報)に設定し、デバッガがシンボルと行番号情報を利用できるようにします。
src/make.bash
の変更: ビルドスクリプトであるmake.bash
にbash clean.bash
が追加されました。これは、ビルド前にクリーンアップ処理を実行することで、古いビルドアーティファクトが残ることを防ぎ、常にクリーンな状態でビルドが行われるようにするための変更です。これにより、シンボルテーブルや行番号情報が正しく埋め込まれた新しい実行ファイルが確実に生成されるようになります。
これらの変更により、6.out
形式のGo実行ファイルには、デバッグに必要なシンボルテーブルと行番号情報が適切に埋め込まれるようになり、デバッガがこれらの情報を利用して、よりリッチなデバッグ体験を提供できるようになりました。
コアとなるコードの変更箇所
src/libmach_amd64/executable.c
--- a/src/libmach_amd64/executable.c
+++ b/src/libmach_amd64/executable.c
@@ -626,7 +626,8 @@ elf64dotout(int fd, Fhdr *fp, ExecHdr *hp)
ushort (*swab)(ushort);
Ehdr64 *ep;
Phdr64 *ph;
- int i, it, id, is, phsz;
+ Shdr64 *sh;
+ int i, it, id, is, phsz, shsz;
/* bitswap the header according to the DATA format */
ep = &hp->e.elfhdr64;
@@ -711,6 +712,17 @@ print("entry: 0x%x\n", ep->elfentry);
}
hswal(ph, phsz/sizeof(ulong), swal);
+ shsz = sizeof(Shdr64)*ep->shnum;
+ sh = malloc(shsz);
+ if(sh) {
+ seek(fd, ep->shoff, 0);
+ if(read(fd, sh, shsz) < 0) {
+ free(sh);
+ sh = 0;
+ } else
+ hswal(ph, phsz/sizeof(ulong), swal);
+ }
+
/* find text, data and symbols and install them */
it = id = is = -1;
for(i = 0; i < ep->phnum; i++) {
@@ -748,7 +760,9 @@ print("entry: 0x%x\n", ep->elfentry);
}
werrstr("No TEXT or DATA sections");
+error:
free(ph);
+ free(sh);
return 0;
}
@@ -756,7 +770,40 @@ print("entry: 0x%x\n", ep->elfentry);
setdata(fp, ph[id].vaddr, ph[id].filesz, ph[id].offset, ph[id].memsz - ph[id].filesz);
if(is != -1)
setsym(fp, ph[is].filesz, 0, ph[is].memsz, ph[is].offset);
+ else if(ep->machine == AMD64 && sh != 0){
+ char *buf;
+ uvlong symsize = 0;
+ uvlong symoff = 0;
+ uvlong pclnsz = 0;
+
+ /* load shstrtab names */
+ buf = malloc(sh[ep->shstrndx].size);
+ if (buf == 0)
+ goto done;
+ memset(buf, 0, sizeof buf);
+ seek(fd, sh[ep->shstrndx].offset, 0);
+ read(fd, buf, sh[ep->shstrndx].size);
+
+ for(i = 0; i < ep->shnum; i++) {
+ if (sh[i].type == 2 && strcmp(&buf[sh[i].name], ".gosymtab") == 0) {
+ symsize = sh[i].size;
+ symoff = sh[i].offset;
+ }
+ if (sh[i].type == 2 && strcmp(&buf[sh[i].name], ".gopclntab") == 0) {
+ if (sh[i].offset != symoff+symsize) {
+ werrstr("pc line table not contiguous with symbol table");
+ free(buf);
+ goto error;
+ }
+ pclnsz = sh[i].size;
+ }
+ }
+ setsym(fp, symsize, 0, pclnsz, symoff);
+ free(buf);
+ }
+done:
free(ph);
+ free(sh);
return 1;
}
src/make.bash
--- a/src/make.bash
+++ b/src/make.bash
@@ -3,6 +3,8 @@
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
+bash clean.bash
+
for i in lib9 libbio libmach_amd64
do
cd $i
コアとなるコードの解説
src/libmach_amd64/executable.c
Shdr64 *sh;
とint shsz;
の追加: セクションヘッダを格納するためのポインタと、そのサイズを格納するための変数が追加されました。- セクションヘッダの読み込み:
このブロックは、ELFヘッダからセクションヘッダテーブルのオフセット(shsz = sizeof(Shdr64)*ep->shnum; sh = malloc(shsz); if(sh) { seek(fd, ep->shoff, 0); if(read(fd, sh, shsz) < 0) { free(sh); sh = 0; } else hswal(ph, phsz/sizeof(ulong), swal); // ここはphではなくshが正しいはずだが、コミット時点のコードではphになっている }
ep->shoff
)とエントリ数(ep->shnum
)を取得し、そのサイズ分のメモリを確保してセクションヘッダをファイルから読み込みます。hswal
はバイトオーダーを変換する関数です。 - エラーハンドリングの改善:
error:
ラベルが追加され、エラー発生時にph
(プログラムヘッダ)だけでなく、新しく確保されたsh
(セクションヘッダ)も解放されるようになりました。 - Go固有のシンボル/PC-Lineテーブルの処理:
このブロックは、AMD64アーキテクチャの実行ファイルでセクションヘッダが利用可能な場合に実行されます。else if(ep->machine == AMD64 && sh != 0){ char *buf; uvlong symsize = 0; uvlong symoff = 0; uvlong pclnsz = 0; /* load shstrtab names */ buf = malloc(sh[ep->shstrndx].size); if (buf == 0) goto done; memset(buf, 0, sizeof buf); seek(fd, sh[ep->shstrndx].offset, 0); read(fd, buf, sh[ep->shstrndx].size); for(i = 0; i < ep->shnum; i++) { if (sh[i].type == 2 && strcmp(&buf[sh[i].name], ".gosymtab") == 0) { symsize = sh[i].size; symoff = sh[i].offset; } if (sh[i].type == 2 && strcmp(&buf[sh[i].name], ".gopclntab") == 0) { if (sh[i].offset != symoff+symsize) { werrstr("pc line table not contiguous with symbol table"); free(buf); goto error; } pclnsz = sh[i].size; } } setsym(fp, symsize, 0, pclnsz, symoff); free(buf); }
- セクション名文字列テーブルの読み込み:
ep->shstrndx
で指定されるセクション(セクション名を格納する文字列テーブル)を読み込みます。これにより、各セクションの数値インデックスから実際のセクション名(例: ".text", ".data", ".gosymtab")を取得できるようになります。 .gosymtab
と.gopclntab
の検索: 全てのセクションヘッダをループし、セクションタイプが2
(SHT_SYMTAB
またはSHT_PROGBITS
など、ここではGo固有のセクションを指す)で、かつセクション名が「.gosymtab
」または「.gopclntab
」であるものを探します。見つかった場合、そのサイズとファイルオフセットを記録します。- 連続性の検証:
.gopclntab
が.gosymtab
の直後に続くことを確認します。これはGoのバイナリフォーマットの特定の要件です。 setsym
の呼び出し: 最後に、setsym
関数を呼び出して、取得したシンボルテーブルとPC-Lineテーブルの情報をFhdr
構造体に設定します。これにより、デバッガがこれらの情報を利用できるようになります。
- セクション名文字列テーブルの読み込み:
done:
ラベルとリソース解放: 処理の最後にdone:
ラベルにジャンプし、ph
とsh
の両方のメモリを解放します。
src/make.bash
bash clean.bash
この行が追加されたことで、make.bash
スクリプトが実行されるたびに、まずclean.bash
スクリプトが実行され、以前のビルドによって生成されたファイルが削除されます。これにより、常にクリーンな状態からビルドが開始され、シンボルテーブルや行番号情報が正しく含まれた最新の実行ファイルが生成されることが保証されます。
関連リンク
- ELF (Executable and Linkable Format) - Wikipedia
- Go言語のバイナリフォーマットに関する議論 (初期のメーリングリストなど) (具体的なリンクは検索結果による)
参考にした情報源リンク
- Go言語のソースコードリポジトリ (GitHub)
- ELFフォーマットに関する一般的なドキュメント
- Go言語の初期の設計に関するドキュメントやメーリングリストのアーカイブ (Rob Pikeのコミットメッセージや関連する議論)
libmach
に関する情報 (Plan 9のツールチェインの一部としての文脈)