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

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

このコミットは、Go言語のリンカ (cmd/ld) に、ELF (Executable and Linkable Format) 形式の実行ファイルにビルドID (Build ID) を埋め込むための -B オプションを追加するものです。ビルドIDは、特定のバイナリがどのソースコードからビルドされたかを一意に識別するためのメカニズムであり、デバッグやプロファイリング、システム監査において非常に有用です。

コミット

commit 32316bba5b7c198c320681104c9bfcbc622e31df
Author: Ian Lance Taylor <iant@golang.org>
Date:   Tue Oct 9 15:29:43 2012 -0700

    cmd/ld: add -B option to set build ID
    
    Background on build ID:
    http://fedoraproject.org/wiki/RolandMcGrath/BuildID
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/6625072

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

https://github.com/golang/go/commit/32316bba5b7c198c320681104c9bfcbc622e31df

元コミット内容

cmd/ld: ビルドIDを設定するための -B オプションを追加

ビルドIDに関する背景情報: http://fedoraproject.org/wiki/RolandMcGrath/BuildID

変更の背景

この変更の主な背景は、ELF実行ファイルにビルドIDを埋め込む機能を提供することです。ビルドIDは、バイナリの同一性を保証し、デバッグシンボルやソースコードとの関連付けを容易にするために使用されます。特に、異なるビルド環境や時間で生成されたバイナリであっても、そのバイナリがどの特定のソースコードリビジョンから生成されたかを一意に識別できることは、ソフトウェアのライフサイクル管理において非常に重要です。

Fedora ProjectのWikiページで言及されているように、ビルドIDは、デバッグ情報が分離された環境(例: デバッグシンボルパッケージ)で特に役立ちます。バイナリとデバッグ情報が異なる場所に存在する場合でも、ビルドIDをキーとして正確なデバッグ情報を探し出すことができます。これにより、クラッシュレポートの解析や、本番環境で発生した問題の再現とデバッグが格段に容易になります。

Go言語のツールチェインにおいて、リンカがこの機能に対応することで、Goでビルドされた実行ファイルもこの標準的なビルドIDメカニズムの恩恵を受けられるようになります。

前提知識の解説

ELF (Executable and Linkable Format)

ELFは、Unix系オペレーティングシステム(Linux、FreeBSD、Solarisなど)で広く使用されている実行ファイル、オブジェクトファイル、共有ライブラリの標準ファイル形式です。ELFファイルは、ヘッダ、プログラムヘッダテーブル、セクションヘッダテーブル、および様々なセクション(コード、データ、シンボルテーブルなど)で構成されます。

ビルドID (Build ID)

ビルドIDは、コンパイルされたバイナリを一意に識別するためのハッシュ値またはUUID(Universally Unique Identifier)です。通常、バイナリのコンテンツ(特に実行可能なコードや初期化済みデータ)から計算されたハッシュ値が使用されます。このIDは、ELFファイル内の特別な「ノート (Note)」セクション(通常 .note.gnu.build-id)に埋め込まれます。

ビルドIDの主な目的は以下の通りです。

  • バイナリの同一性保証: 同じソースコードから同じビルド環境でビルドされたバイナリは、同じビルドIDを持つべきです。これにより、バイナリの改ざんや予期せぬ変更を検出できます。
  • デバッグの効率化: クラッシュダンプや本番環境のバイナリからビルドIDを抽出することで、対応する正確なデバッグシンボルやソースコードリビジョンを特定し、デバッグプロセスを加速できます。
  • パッケージ管理: ディストリビューションのパッケージマネージャは、ビルドIDを使用して、バイナリとデバッグ情報パッケージ(debuginfo パッケージなど)の整合性を確認できます。

ELFのノートセクション (Note Section)

ELFファイルには、特定の情報を埋め込むための「ノート」セクションが存在します。これは、プログラムの実行には直接必要ないが、ツールやシステムが利用できるメタデータを格納するために使用されます。ビルドIDは、NT_GNU_BUILD_ID というタイプを持つノートとして、通常 .note.gnu.build-id セクションに格納されます。

ノートセクションの構造は以下のようになります。

  • n_namesz: ベンダー名のサイズ
  • n_descsz: 記述子(descriptor)のサイズ
  • n_type: ノートのタイプ(例: NT_GNU_BUILD_ID
  • name: ベンダー名(例: "GNU")
  • descriptor: 実際のデータ(例: ビルドIDのハッシュ値)

リンカ (cmd/ld)

Go言語のリンカ (cmd/ld) は、Goのコンパイラによって生成されたオブジェクトファイルや、他のライブラリを結合して実行可能なバイナリを生成するツールです。このコミットでは、リンカがELFファイルを生成する際に、ビルドIDを埋め込むロジックが追加されています。

技術的詳細

このコミットは、GoリンカのELF出力処理にビルドIDのサポートを統合しています。主な変更点は以下の通りです。

  1. -B オプションの追加: リンカのコマンドラインオプションに -B が追加されました。このオプションは、ビルドIDとして埋め込む16進数文字列を受け取ります。例えば、-B 0xabcdef1234567890 のように使用されます。入力値は 0x で始まり、偶数桁の16進数である必要があります。
  2. buildinfo 変数と buildinfolen 変数:
    • buildinfo は、指定されたビルドIDのバイナリデータを格納するための char 配列です。最大32バイト(64桁の16進数)のビルドIDをサポートします。
    • buildinfolen は、buildinfo に格納されたビルドIDの実際の長さを保持します。
  3. addbuildinfo 関数の追加:
    • この関数は、-B オプションで渡された16進数文字列を解析し、buildinfo 配列にバイナリデータとして変換して格納します。
    • 入力値の形式(0x プレフィックス、偶数桁、有効な16進数)を厳密に検証し、不正な場合はエラーを出力して終了します。
    • ビルドIDの長さが buildinfo の容量を超える場合もエラーを報告します。
  4. ELFノートセクションの追加ロジック:
    • src/cmd/*/asm.c (5l, 6l, 8l はそれぞれARM、x86-64、x86のリンカ) ファイルにおいて、ELFファイルの生成時にビルドID用のノートセクション (.note.gnu.build-id) を追加するロジックが組み込まれています。
    • ElfStrNoteBuildInfo という新しい文字列定数が追加され、.note.gnu.build-id セクション名を表します。
    • doelf 関数内で、buildinfolen > 0 の場合にこのセクション名がELF文字列テーブルに追加されます。
    • asmb 関数内で、buildinfolen > 0 の場合に elftextsh (ELFテキストセクションの数) がインクリメントされ、新しいプログラムヘッダ (PT_NOTE タイプ) が作成されるか、既存のノートプログラムヘッダが再利用されます。
    • elfbuildinfo 関数が呼び出され、ビルドIDノートセクションのサイズが計算されます。
    • elfwritebuildinfo 関数が呼び出され、実際のビルドIDデータがELFファイルに書き込まれます。この関数は、ELF_NOTE_BUILDINFO_NAMESZ (4バイト、"GNU" + null終端)、ELF_NOTE_BUILDINFO_TAG (3)、ELF_NOTE_BUILDINFO_NAME ("GNU\0") といった定義を使用して、標準的なGNUビルドIDノートのフォーマットに従ってデータを書き込みます。
    • データは4バイト境界にアラインされるようにパディングされます。

この変更により、Goリンカは、指定されたビルドIDをELFバイナリに埋め込むことが可能になり、Goでビルドされたアプリケーションのデバッグと管理が容易になります。

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

このコミットは、主に以下のファイル群に影響を与えています。

  • src/cmd/5l/asm.c, src/cmd/6l/asm.c, src/cmd/8l/asm.c: 各アーキテクチャ(ARM, x86-64, x86)のリンカのアセンブリ出力部分。ELFノートセクションの追加ロジックが実装されています。
  • src/cmd/5l/obj.c, src/cmd/6l/obj.c, src/cmd/8l/obj.c: 各アーキテクチャのリンカのメイン処理部分。コマンドライン引数パーシングに -B オプションの処理が追加されています。
  • src/cmd/ld/doc.go: リンカのドキュメント。-B オプションの説明が追加されています。
  • src/cmd/ld/elf.c: ELFファイル生成に関する共通ロジック。addbuildinfo, elfbuildinfo, elfwritebuildinfo 関数が追加され、ビルドIDの処理が実装されています。
  • src/cmd/ld/elf.h: elf.c で定義された関数のプロトタイプと、buildinfolen グローバル変数の宣言が追加されています。

src/cmd/ld/elf.c の主要な変更点

// 新しいグローバル変数: ビルドIDのバイナリデータを格納
static char buildinfo[32];

// -B オプションで渡されたビルドID文字列を解析し、buildinfoに格納する関数
void
addbuildinfo(char *val)
{
    char *ov;
    int i, b, j;

    // 入力値の形式検証 (0xで始まるか、偶数桁か、有効な16進数か)
    if(val[0] != '0' || val[1] != 'x') {
        fprintf(2, "%s: -B argument must start with 0x: %s\\n", argv0, val);
        exits("usage");
    }
    ov = val;
    val += 2;
    i = 0;
    while(*val != '\\0') {
        if(val[1] == '\\0') {
            fprintf(2, "%s: -B argument must have even number of digits: %s\\n", argv0, ov);
            exits("usage");
        }
        b = 0;
        for(j = 0; j < 2; j++, val++) {
            // 16進数文字を数値に変換
            if(*val >= '0' && *val <= '9')
                b += *val - '0';
            else if(*val >= 'a' && *val <= 'f')
                b += *val - 'a' + 10;
            else if(*val >= 'A' && *val <= 'F')
                b += *val - 'A' + 10;
            else {
                fprintf(2, "%s: -B argument contains invalid hex digit %c: %s\\n", argv0, *val, ov);
                exits("usage");
            }
        }
        // buildinfo配列に格納。サイズチェックも行う
        if(i >= nelem(buildinfo)) {
            fprintf(2, "%s: -B option too long (max %d digits): %s\\n", argv0, (int)nelem(buildinfo), ov);
            exits("usage");
        }
        buildinfo[i++] = b;
    }
    buildinfolen = i; // 実際のビルドIDの長さを設定
}

// ビルド情報ノートの定義
#define ELF_NOTE_BUILDINFO_NAMESZ       4       // "GNU" + null終端
#define ELF_NOTE_BUILDINFO_TAG          3       // NT_GNU_BUILD_ID
#define ELF_NOTE_BUILDINFO_NAME         "GNU\\0"

// ビルド情報ノートセクションのサイズを計算する関数
int
elfbuildinfo(ElfShdr *sh, uint64 startva, uint64 resoff)
{
    int n;
    // ノートのサイズ = 名前サイズ + (ビルドIDの長さ + パディング)
    n = ELF_NOTE_BUILDINFO_NAMESZ + rnd(buildinfolen, 4);
    return elfnote(sh, startva, resoff, n);
}

// ビルド情報ノートをELFファイルに書き込む関数
int
elfwritebuildinfo(vlong stridx)
{
    ElfShdr *sh;

    // ノートヘッダの書き込み
    sh = elfwritenotehdr(stridx, ELF_NOTE_BUILDINFO_NAMESZ, buildinfolen, ELF_NOTE_BUILDINFO_TAG);
    if(sh == nil)
        return 0;

    // ベンダー名 ("GNU\\0") の書き込み
    cwrite(ELF_NOTE_BUILDINFO_NAME, ELF_NOTE_BUILDINFO_NAMESZ);
    // ビルドIDのバイナリデータの書き込み
    cwrite(buildinfo, buildinfolen);
    // 4バイトアラインメントのためのパディング
    cwrite("\\0\\0\\0", rnd(buildinfolen, 4) - buildinfolen);

    return sh->size; // 書き込んだサイズを返す
}

src/cmd/*/asm.c の主要な変更点 (例: src/cmd/5l/asm.c)

// 新しいELF文字列定数の追加
enum {
    // ...
    ElfStrNoteBuildInfo, // .note.gnu.build-id
    // ...
};

// doelf関数内: buildinfolen > 0 の場合に .note.gnu.build-id をELF文字列テーブルに追加
if(buildinfolen > 0)
    elfstr[ElfStrNoteBuildInfo] = addstring(shstrtab, ".note.gnu.build-id");

// asmb関数内:
// プログラムヘッダとセクションヘッダの処理
// buildinfolen > 0 の場合に elftextsh (ELFテキストセクションの数) をインクリメント
if(buildinfolen > 0)
    elftextsh += 1;

// ノートセクションのプログラムヘッダ (pnote) の初期化
pnote = nil;

// buildinfolen > 0 の場合にビルドIDノートセクションを追加
if(buildinfolen > 0) {
    sh = newElfShdr(elfstr[ElfStrNoteBuildInfo]); // 新しいセクションヘッダを作成
    resoff -= elfbuildinfo(sh, startva, resoff); // ビルド情報ノートのサイズを計算

    if(pnote == nil) { // ノート用のプログラムヘッダがまだない場合
        pnote = newElfPhdr();
        pnote->type = PT_NOTE;
        pnote->flags = PF_R; // 読み取り可能
    }
    phsh(pnote, sh); // プログラムヘッダとセクションヘッダを関連付ける
}

// asmb関数内: 実際のノートデータの書き込み
// buildinfolen > 0 の場合にビルド情報ノートを書き込む
if(buildinfolen > 0)
    a += elfwritebuildinfo(elfstr[ElfStrNoteBuildInfo]);

src/cmd/*/obj.c の主要な変更点 (例: src/cmd/5l/obj.c)

// main関数内: コマンドライン引数の処理
case 'B': // -B オプションの処理
    val = EARGF(usage()); // 引数の値を取得
    addbuildinfo(val);    // addbuildinfo関数を呼び出してビルドIDを処理
    break;

コアとなるコードの解説

このコミットの核となるのは、ELFファイルにビルドIDを埋め込むための新しい関数と、既存のリンカのELF生成フローへのそれらの統合です。

  1. addbuildinfo(char *val):

    • この関数は、ユーザーが -B オプションで指定したビルドIDの文字列(例: "0xabcdef...")を受け取ります。
    • 文字列の先頭が "0x" であること、全体の桁数が偶数であること、そして各文字が有効な16進数であることを厳密に検証します。これは、不正な入力によるリンカのクラッシュや、無効なビルドIDの生成を防ぐための重要なバリデーションです。
    • 検証が成功すると、16進数文字列をバイト列に変換し、グローバルな buildinfo 配列に格納します。同時に、そのバイト列の長さが buildinfolen に設定されます。この buildinfo 配列が、最終的にELFファイルに埋め込まれるビルドIDの生データとなります。
  2. elfbuildinfo(ElfShdr *sh, uint64 startva, uint64 resoff):

    • この関数は、ビルドIDを格納するELFノートセクションのサイズを計算します。
    • ELFノートの標準的な構造(名前サイズ、記述子サイズ、タイプ)に基づいて、必要な領域を決定します。特に、ビルドIDのデータ自体は4バイト境界にアラインされる必要があるため、rnd(buildinfolen, 4) を使用してパディングを考慮したサイズを計算しています。
  3. elfwritebuildinfo(vlong stridx):

    • この関数は、実際にビルドIDのデータをELFファイルに書き込みます。
    • まず、elfwritenotehdr を呼び出して、ビルドIDノートのヘッダ(n_namesz, n_descsz, n_type)を書き込みます。ここで ELF_NOTE_BUILDINFO_NAMESZ (4), buildinfolen, ELF_NOTE_BUILDINFO_TAG (3) が使用され、それぞれベンダー名サイズ、記述子サイズ(ビルドIDの長さ)、ノートタイプ (NT_GNU_BUILD_ID) を指定します。
    • 次に、cwrite を使用して、ベンダー名 "GNU\\0" と、addbuildinfo で準備された buildinfo 配列の内容(実際のビルドIDデータ)を書き込みます。
    • 最後に、ビルドIDデータが4バイト境界にアラインされるように、必要に応じてヌルバイトでパディングを行います。

これらの関数は、各アーキテクチャのリンカ (src/cmd/5l/asm.c, src/cmd/6l/asm.c, src/cmd/8l/asm.c) のELF生成フェーズに組み込まれています。具体的には、ELFセクションヘッダやプログラムヘッダを構築する際に、buildinfolen が0より大きい(つまり、ユーザーがビルドIDを指定した)場合に、ビルドID用のノートセクションが追加され、そのデータが書き込まれるようになっています。

この一連の変更により、Goリンカは、標準的なELFビルドIDの仕様に準拠した形で、バイナリに一意の識別子を埋め込む機能を提供できるようになりました。

関連リンク

  • Go言語のリンカに関するドキュメント:
    • Goのリンカの内部構造や動作に関する公式ドキュメントは、Goのソースコードリポジトリ内の src/cmd/ld/doc.go や、Goの公式ブログ記事などで見つけることができます。
  • ELF (Executable and Linkable Format) の仕様:
    • ELFの詳細は、UNIX System V Application Binary Interface (ABI) のドキュメントや、Linuxの man elf ページなどで確認できます。
  • GNU Binutils (ld) のビルドIDに関する情報:
    • GNUのリンカ ld もビルドIDをサポートしており、その実装や関連するドキュメントが参考になります。

参考にした情報源リンク

  • Fedora Project Wiki - RolandMcGrath/BuildID:
    • http://fedoraproject.org/wiki/RolandMcGrath/BuildID
    • このコミットメッセージで直接参照されている、ビルドIDの背景と目的について説明している主要な情報源です。ビルドIDの概念、その利点、およびELFファイルでの実装方法について詳しく解説されています。
  • GNU Binutils の ld マニュアル:
    • GNUのリンカ ld のドキュメントは、ELFファイル形式やビルドIDの概念を理解する上で役立ちます。
  • ELFに関する一般的な技術記事や書籍:
    • ELFファイル形式の構造や、ノートセクションのような特定の要素に関する詳細な情報は、オペレーティングシステムやコンパイラの技術書、またはオンラインの技術記事で広く入手可能です。