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

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

このコミットは、Go言語のリンカ(liblink)におけるフィールドトラッキングの挙動を修正するものです。具体的には、USEFIELD命令がリンカに到達しなくなった問題に対応し、代わりにR_USEFIELDという新しいタイプのリロケーションを導入することで、参照ピン留め(pinning references)のメカニズムを維持しています。

コミット

commit d9c9665f1dcd09b0e315c9437d192fed795587a5
Author: Russ Cox <rsc@golang.org>
Date:   Tue May 20 00:30:58 2014 -0400

    liblink: fix field tracking
    
    The USEFIELD instructions no longer make it to the linker,
    so we have to do something else to pin the references
    they were pinning. Emit a 0-length relocation of type R_USEFIELD.
    
    Fixes #7486.
    
    LGTM=iant
    R=golang-codereviews, iant
    CC=golang-codereviews, r
    https://golang.org/cl/95530043

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

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

元コミット内容

このコミットの目的は、liblink(Goリンカの内部コンポーネント)におけるフィールドトラッキングの不具合を修正することです。以前はUSEFIELD命令がリンカによって処理されていましたが、何らかの変更によりこれらの命令がリンカに到達しなくなりました。このため、USEFIELD命令が担っていた参照のピン留め(特定のフィールドがリンカによって削除されないようにマークすること)の機能が失われました。

このコミットでは、この問題を解決するために、R_USEFIELDという新しいタイプのリロケーションを導入しています。USEFIELD命令がリンカに到達しない代わりに、コンパイラが0バイト長のリロケーションとしてR_USEFIELDを発行するように変更されました。これにより、リンカは引き続きフィールドの参照を適切に追跡し、必要なフィールドが最終的なバイナリに含まれることを保証します。

コミットメッセージにあるFixes #7486は、Goプロジェクトの内部イシュートラッカーにおける問題番号を指しており、公開されているGitHubのイシューとは異なります。

変更の背景

Go言語のツールチェインには、構造体のフィールド使用状況を追跡する実験的な機能が存在します。これは主にデバッグや最適化の目的で、どのフィールドが実際にバイナリ内で参照されているかをリンカが把握するために利用されます。この機能は、GOEXPERIMENT=fieldtrack環境変数を設定してビルドすることで有効になります。

このフィールドトラッキング機能を実現するために、Goコンパイラは特定のフィールドの使用を示すUSEFIELDという内部的な命令を生成していました。リンカはこれらのUSEFIELD命令を解釈し、関連するフィールドがバイナリから削除されないように(例えば、デッドコード削除の対象とならないように)「ピン留め」していました。

しかし、このコミットが作成される以前のGoツールチェインの変更により、USEFIELD命令がリンカの処理パイプラインに到達しなくなりました。その結果、リンカはフィールドの使用状況を正確に把握できなくなり、フィールドトラッキング機能が正しく動作しなくなる、あるいは意図しない最適化によって必要なフィールドが削除されてしまう可能性が生じました。このコミットは、この内部的な変更によって生じたフィールドトラッキングの機能不全を修正するために導入されました。

前提知識の解説

Go言語のビルドプロセスにおいて、リンカ(cmd/link)は非常に重要な役割を担っています。コンパイラ(cmd/compile)がGoのソースコードをオブジェクトファイル(.oファイル)に変換した後、リンカはこれらのオブジェクトファイルと、標準ライブラリなどの依存関係を結合して、実行可能なバイナリを生成します。リンカの主な機能には以下のようなものがあります。

  • シンボル解決: 異なるオブジェクトファイル間で定義された関数や変数の参照を解決します。
  • リロケーション: コード内のアドレス依存の参照(例えば、グローバル変数へのアクセスや関数呼び出し)を、最終的なメモリレイアウトに基づいて修正します。
  • デッドコード削除: 実行されないコードや参照されないデータを最終的なバイナリから削除し、ファイルサイズを削減します。

liblinkは、これらのリンカの機能を提供する内部ライブラリであり、Goツールチェインの低レベルな部分を構成しています。

リロケーション (Relocation)

リロケーションとは、コンパイル時に決定できなかったアドレスを、リンカが最終的な実行可能ファイルを作成する際に解決するプロセスです。オブジェクトファイル内のコードやデータには、他のセクションや外部ライブラリのシンボルへの参照が含まれることがありますが、これらのシンボルが最終的にメモリ上のどこに配置されるかは、リンク時まで分かりません。

リロケーションエントリは、リンカに対して「この場所の値を、指定されたシンボルの最終アドレスに基づいて修正しなさい」という指示を与えます。例えば、関数呼び出し命令のオペランドや、グローバル変数へのポインタなどがリロケーションの対象となります。

Goリンカには様々な種類のリロケーションタイプ(R_CALL, R_DATA, R_PCRELなど)があり、それぞれ異なる目的と処理方法を持っています。このコミットで導入されたR_USEFIELDもその一つです。

フィールドトラッキング (Field Tracking)

Goのフィールドトラッキングは、構造体の特定のフィールドがプログラム内で実際に使用されているかどうかをリンカが追跡できるようにする実験的な機能です。これは、主にGoの内部開発やプロファイリング、あるいは特定の最適化のために利用されます。

この機能は、GOEXPERIMENT=fieldtrack環境変数を設定してGoプログラムをビルドする際に有効になります。有効な場合、コンパイラは構造体のフィールドが参照される箇所に、リンカが認識できる特別なマーク(以前はUSEFIELD命令、このコミット以降はR_USEFIELDリロケーション)を埋め込みます。これにより、リンカはたとえ直接的なコードパスから参照されていなくても、そのフィールドが「使用されている」と判断し、デッドコード削除の対象から外すことができます。

例えば、rsc.io/tmp/fieldtrackのような実験的なパッケージは、このフィールドトラッキングのデータを利用して、実行中のバイナリからどのフィールドが追跡され、どのように参照されているかといった情報を取得する機能を提供します。

技術的詳細

このコミットの技術的な核心は、USEFIELD命令がリンカに到達しなくなったという内部的な変更に対応し、その機能をR_USEFIELDリロケーションに置き換える点にあります。

  1. R_USEFIELDリロケーションタイプの追加: include/link.hファイルに、新しいリロケーションタイプR_USEFIELDが追加されました。これは、リンカが認識すべきリロケーションの種類を定義する列挙型enumの一部です。これにより、リンカはR_USEFIELDという新しい指示を処理できるようになります。

    enum
    {
        R_PLT0,
        R_PLT1,
        R_PLT2,
        R_USEFIELD, // <-- 新しく追加されたリロケーションタイプ
    };
    
  2. AUSEFIELD命令の処理変更: src/liblink/asm6.csrc/liblink/asm8.cは、それぞれ386アーキテクチャとAMD64アーキテクチャ向けのGoアセンブラのバックエンドコードです。これらのファイル内のasmins関数(アセンブリ命令を処理する関数)が変更されました。

    変更前は、コンパイラが生成したAUSEFIELD(アセンブリレベルでのUSEFIELD命令)がリンカに直接渡され、リンカがその命令を処理することでフィールドの参照をピン留めしていました。

    変更後、asmins関数はAUSEFIELD命令を検出すると、その命令自体を処理するのではなく、addrel関数を呼び出して新しいリロケーションエントリを作成します。この新しいリロケーションは以下の特性を持ちます。

    • r->off = 0;: リロケーションのオフセット(適用される場所)は0です。これは、このリロケーションが特定のコードやデータのバイト列を修正するものではなく、単にシンボル参照をマークするためのものであることを示唆しています。
    • r->siz = 0;: リロケーションのサイズも0です。これもオフセットと同様に、実際のバイナリデータに影響を与えるものではなく、メタデータ的な役割を持つことを意味します。
    • r->sym = p->from.sym;: リロケーションが関連付けられるシンボルは、AUSEFIELD命令のfromフィールドから取得されます。これが、ピン留めしたい対象のフィールドシンボルです。
    • r->type = R_USEFIELD;: リロケーションのタイプは、新しく定義されたR_USEFIELDです。

    この変更により、AUSEFIELD命令はリンカの命令処理パイプラインから事実上削除され、その役割はR_USEFIELDリロケーションに引き継がれました。リンカは、この0バイト長のリロケーションを検出することで、関連するシンボル(フィールド)が使用されていることを認識し、デッドコード削除の対象から外すなどの適切な処理を行うことができます。

このアプローチは、リンカの内部構造の変更に柔軟に対応しつつ、既存のフィールドトラッキング機能のセマンティクスを維持するためのものです。

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

include/link.h

--- a/include/link.h
+++ b/include/link.h
@@ -243,6 +243,7 @@ enum
 	R_PLT0,
 	R_PLT1,
 	R_PLT2,
+	R_USEFIELD, // 新しいリロケーションタイプが追加
 };
 
 // Auto.type

src/liblink/asm6.c

--- a/src/liblink/asm6.c
+++ b/src/liblink/asm6.c
@@ -3443,6 +3443,15 @@ asmins(Link *ctxt, Prog *p)
 	ctxt->andptr = ctxt->and;
 	ctxt->asmode = p->mode;
 	
+	if(p->as == AUSEFIELD) { // AUSEFIELD命令を検出
+		r = addrel(ctxt->cursym); // 新しいリロケーションを作成
+		r->off = 0; // オフセットは0
+		r->siz = 0; // サイズは0
+		r->sym = p->from.sym; // 関連するシンボルを設定
+		r->type = R_USEFIELD; // タイプはR_USEFIELD
+		return; // 命令の処理を終了
+	}
+	
 	if(ctxt->headtype == Hnacl) {
 		if(p->as == AREP) {
 			ctxt->rep++;

src/liblink/asm8.c

--- a/src/liblink/asm8.c
+++ b/src/liblink/asm8.c
@@ -2744,7 +2744,18 @@ static uchar naclret[] = {
 static void
 asmins(Link *ctxt, Prog *p)
 {
+	Reloc *r; // Reloc構造体へのポインタを宣言
+
 	ctxt->andptr = ctxt->and;
+	
+	if(p->as == AUSEFIELD) { // AUSEFIELD命令を検出
+		r = addrel(ctxt->cursym); // 新しいリロケーションを作成
+		r->off = 0; // オフセットは0
+		r->sym = p->from.sym; // 関連するシンボルを設定
+		r->type = R_USEFIELD; // タイプはR_USEFIELD
+		r->siz = 0; // サイズは0
+		return; // 命令の処理を終了
+	}
 
 	if(ctxt->headtype == Hnacl) {
 		switch(p->as) {

コアとなるコードの解説

このコミットの主要な変更は、Goリンカの内部でアセンブリ命令を処理するasmins関数に集中しています。

  1. include/link.h: このヘッダーファイルは、リンカが使用する定数やデータ構造を定義しています。R_USEFIELDenumに追加されたことで、リンカはこれを有効なリロケーションタイプとして認識し、内部で処理できるようになります。これは、リンカの「語彙」に新しい単語が追加されたようなものです。

  2. src/liblink/asm6.c および src/liblink/asm8.c: これらのファイルは、Goのアセンブラが生成する中間表現(Prog構造体で表される)をリンカが処理する部分です。 asmins関数は、個々のアセンブリ命令(Prog *p)を検査し、それに応じてリンカの内部状態を更新したり、最終的なバイナリコードを生成したりします。

    追加されたif(p->as == AUSEFIELD)ブロックがこのコミットの核心です。

    • p->as == AUSEFIELD: これは、現在の命令がAUSEFIELDタイプであるかどうかをチェックしています。AUSEFIELDは、コンパイラが特定のフィールドが使用されていることをリンカに伝えるために生成する内部的なアセンブリ命令です。
    • r = addrel(ctxt->cursym);: addrel関数は、現在のシンボル(ctxt->cursym)に関連付けられた新しいリロケーションエントリを作成します。リロケーションは、リンカが最終的なバイナリを生成する際に、特定のアドレスを修正するための指示です。
    • r->off = 0; および r->siz = 0;: これらの行は、作成されたリロケーションのオフセットとサイズを0に設定しています。通常のリロケーションは、コードやデータの特定の位置(オフセット)にある特定サイズ(サイズ)の値を修正しますが、R_USEFIELDの場合は、実際のバイナリデータを変更する目的はありません。これは、単にリンカに対して「このシンボルは使用されている」というメタデータ的な情報を提供するためのものです。
    • r->sym = p->from.sym;: p->from.symは、AUSEFIELD命令が参照しているシンボル(つまり、追跡対象のフィールド)です。この行は、リロケーションがどのシンボルに関連付けられているかをリンカに伝えます。
    • r->type = R_USEFIELD;: これが最も重要な部分で、リロケーションのタイプを新しく定義されたR_USEFIELDに設定しています。リンカはこのタイプを見て、これがフィールドトラッキングに関連するリロケーションであることを認識します。
    • return;: AUSEFIELD命令がリロケーションとして処理された後、それ以上のアセンブリ命令としての処理は不要なため、関数を終了します。

この変更により、AUSEFIELD命令はもはやリンカによって直接実行される命令ではなくなり、その存在はR_USEFIELDという0バイト長のリロケーションとしてリンカに伝えられるようになりました。これにより、リンカの内部的な変更にもかかわらず、フィールドトラッキングのセマンティクスが維持され、必要なフィールドがバイナリに適切に含まれることが保証されます。

関連リンク

  • Go言語のリンカに関する公式ドキュメント(一般的な情報): https://go.dev/doc/articles/go-toolchain
  • Go言語の内部的なリロケーションタイプに関する議論(より深い理解のため): Goソースコード内のsrc/cmd/link/internal/ld/sym.gosrc/cmd/internal/obj/link.goなどを参照すると、様々なリロケーションタイプとその処理に関する詳細が見つかります。

参考にした情報源リンク

  • Go言語のソースコード(特にsrc/cmd/linkおよびsrc/cmd/internal/objディレクトリ)
  • Go言語のリンカに関する非公式な解説記事やブログポスト(Goのリンカは内部的な詳細が多く、公式ドキュメントだけでは全てを把握しにくい場合があります)
  • Go言語の実験的なフィールドトラッキング機能に関する情報:
  • Go言語のIssueトラッカー(内部的なIssue #7486の文脈理解のため)