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

[インデックス 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)によって記述されるセクションに格納されます。

このコミットでは、以下の点が変更されています。

  1. セクションヘッダの読み込み: elf64dotout関数内で、Shdr64 *sh;shsz変数が追加され、ELFヘッダのshoff(セクションヘッダテーブルのオフセット)とshnum(セクションヘッダの数)を利用して、セクションヘッダテーブル全体をメモリに読み込むようになりました。これにより、実行ファイル内のすべてのセクションの情報を参照できるようになります。
  2. エラーハンドリングの改善: error:ラベルが追加され、phだけでなくshも解放されるようにエラー処理が改善されています。
  3. Go固有のシンボル/PC-Lineテーブルの検出と設定:
    • else if(ep->machine == AMD64 && sh != 0)ブロックが追加されました。これは、AMD64アーキテクチャの実行ファイルで、かつセクションヘッダが正常に読み込まれた場合にのみ実行されます。
    • セクション名文字列テーブル(shstrndxで指定されるセクション)を読み込み、各セクションの名前を解析します。
    • セクションをループし、名前が「.gosymtab」と「.gopclntab」であるセクションを探します。
    • .gosymtab.gopclntabのサイズとオフセットを取得し、これらが連続しているか(.gopclntab.gosymtabの直後に続くか)を検証します。これは、Goのツールチェインがこれらのセクションを特定の順序で配置することを前提としているためです。
    • setsym関数を呼び出し、取得した.gosymtab.gopclntabのオフセットとサイズを渡します。setsym関数は、これらの情報をFhdr構造体(ファイルヘッダ、おそらくデバッガが使用する抽象化された実行ファイル情報)に設定し、デバッガがシンボルと行番号情報を利用できるようにします。
  4. src/make.bashの変更: ビルドスクリプトであるmake.bashbash 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; の追加: セクションヘッダを格納するためのポインタと、そのサイズを格納するための変数が追加されました。
  • セクションヘッダの読み込み:
    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になっている
    }
    
    このブロックは、ELFヘッダからセクションヘッダテーブルのオフセット(ep->shoff)とエントリ数(ep->shnum)を取得し、そのサイズ分のメモリを確保してセクションヘッダをファイルから読み込みます。hswalはバイトオーダーを変換する関数です。
  • エラーハンドリングの改善: error:ラベルが追加され、エラー発生時にph(プログラムヘッダ)だけでなく、新しく確保されたsh(セクションヘッダ)も解放されるようになりました。
  • Go固有のシンボル/PC-Lineテーブルの処理:
    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);
    }
    
    このブロックは、AMD64アーキテクチャの実行ファイルでセクションヘッダが利用可能な場合に実行されます。
    1. セクション名文字列テーブルの読み込み: ep->shstrndxで指定されるセクション(セクション名を格納する文字列テーブル)を読み込みます。これにより、各セクションの数値インデックスから実際のセクション名(例: ".text", ".data", ".gosymtab")を取得できるようになります。
    2. .gosymtab.gopclntabの検索: 全てのセクションヘッダをループし、セクションタイプが2SHT_SYMTABまたはSHT_PROGBITSなど、ここではGo固有のセクションを指す)で、かつセクション名が「.gosymtab」または「.gopclntab」であるものを探します。見つかった場合、そのサイズとファイルオフセットを記録します。
    3. 連続性の検証: .gopclntab.gosymtabの直後に続くことを確認します。これはGoのバイナリフォーマットの特定の要件です。
    4. setsymの呼び出し: 最後に、setsym関数を呼び出して、取得したシンボルテーブルとPC-Lineテーブルの情報をFhdr構造体に設定します。これにより、デバッガがこれらの情報を利用できるようになります。
  • done:ラベルとリソース解放: 処理の最後にdone:ラベルにジャンプし、phshの両方のメモリを解放します。

src/make.bash

bash clean.bash

この行が追加されたことで、make.bashスクリプトが実行されるたびに、まずclean.bashスクリプトが実行され、以前のビルドによって生成されたファイルが削除されます。これにより、常にクリーンな状態からビルドが開始され、シンボルテーブルや行番号情報が正しく含まれた最新の実行ファイルが生成されることが保証されます。

関連リンク

参考にした情報源リンク

  • Go言語のソースコードリポジトリ (GitHub)
  • ELFフォーマットに関する一般的なドキュメント
  • Go言語の初期の設計に関するドキュメントやメーリングリストのアーカイブ (Rob Pikeのコミットメッセージや関連する議論)
  • libmachに関する情報 (Plan 9のツールチェインの一部としての文脈)