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

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

このコミットは、Go言語のリンカ(cmd/ld)において、外部リンカを使用する際にThread-Local Storage (TLS) の再配置(relocation)情報をELF形式のオブジェクトファイル(.oファイル)に出力するように変更するものです。これにより、ホストリンカ(gccなど)がGoのランタイムにおけるm(マシン)とg(ゴルーチン)の最終的なメモリ配置を決定できるようになります。特に、Linux/ELFシステム、中でもXen環境下でのTLSアクセスに関する特殊な要件に対応しています。

コミット

commit 30e29ee9b6b71a4c40f781c513a8680e8f2c4f64
Author: Ian Lance Taylor <iant@golang.org>
Date:   Wed Mar 27 13:27:35 2013 -0700

    cmd/ld: emit TLS relocations during external linking
    
    This CL was written by rsc.  I just tweaked 8l.
    
    This CL adds TLS relocation to the ELF .o file we write during external linking,
    so that the host linker (gcc) can decide the final location of m and g.
    
    Similar relocations are not necessary on OS X because we use an alternate
    program start-time mechanism to acquire thread-local storage.
    
    Similar relocations are not necessary on ARM or Plan 9 or Windows
    because external linking mode is not yet supported on those systems.
    
    On almost all ELF systems, the references we use are like %fs:-0x4 or %gs:-0x4,
    which we write in 6a/8a as -0x4(FS) or -0x4(GS). On Linux/ELF, however,
    Xen's lack of support for this mode forced us long ago to use a two-instruction
    sequence: first we load %gs:0x0 into a register r, and then we use -0x4(r).
    (The ELF program loader arranges that %gs:0x0 contains a regular pointer to
    that same memory location.) In order to relocate those -0x4(r) references,
    the linker must know where they are. This CL adds the equivalent notation
    -0x4(r)(GS*1) for this purpose: it assembles to the same encoding as -0x4(r)
    but the (GS*1) indicates to the linker that this is one of those thread-local
    references that needs relocation.
    
    Thanks to Elias Naur for reminding me about this missing piece and
    also for writing the test.
    
    R=r
    CC=golang-dev
    https://golang.org/cl/7891047

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

https://github.com/golang/go/commit/30e29ee9b6b71a4c40f781c513a8680e8f2c4f64

元コミット内容

cmd/ld: emit TLS relocations during external linking

This CL was written by rsc.  I just tweaked 8l.

This CL adds TLS relocation to the ELF .o file we write during external linking,
so that the host linker (gcc) can decide the final location of m and g.

Similar relocations are not necessary on OS X because we use an alternate
program start-time mechanism to acquire thread-local storage.

Similar relocations are not necessary on ARM or Plan 9 or Windows
because external linking mode is not yet supported on those systems.

On almost all ELF systems, the references we use are like %fs:-0x4 or %gs:-0x4,
which we write in 6a/8a as -0x4(FS) or -0x4(GS). On Linux/ELF, however,
Xen's lack of support for this mode forced us long ago to use a two-instruction
sequence: first we load %gs:0x0 into a register r, and then we use -0x4(r).
(The ELF program loader arranges that %gs:0x0 contains a regular pointer to
that same memory location.) In order to relocate those -0x4(r) references,
the linker must know where they are. This CL adds the equivalent notation
-0x4(r)(GS*1) for this purpose: it assembles to the same encoding as -0x4(r)
but the (GS*1) indicates to the linker that this is one of those thread-local
references that needs relocation.

Thanks to Elias Naur for reminding me about this missing piece and
also for writing the test.

R=r
CC=golang-dev
https://golang.org/cl/7891047

変更の背景

Go言語は通常、静的リンクをデフォルトとしていますが、Cgo(GoとC言語の相互運用機能)を使用する場合など、外部リンカ(gccclangなど)を利用してリンクを行うことがあります。この際、Goランタイムが使用するm(マシン、OSスレッドに対応)とg(ゴルーチン)という重要なスレッドローカル変数(Thread-Local Storage, TLS)のメモリ配置が問題となります。

従来のGoリンカは、これらのTLS変数の再配置情報をELF形式のオブジェクトファイルに適切に出力していませんでした。その結果、外部リンカがこれらの変数の最終的なアドレスを解決できず、特にLinux/ELFシステム、そしてXen仮想化環境下で問題が発生していました。

特にLinux/ELFシステムでは、TLS変数へのアクセス方法に特殊な事情がありました。通常、%fs:-0x4%gs:-0x4のようなセグメントレジスタとオフセットを用いた直接的なアクセスが一般的ですが、Xen環境ではこのモードがサポートされていなかったため、Goは以前から2つの命令シーケンスを使用する必要がありました。具体的には、まず%gs:0x0をレジスタにロードし、次にそのレジスタからのオフセット(-0x4(r))を使用するという方法です。この-0x4(r)参照がTLS関連であることをリンカに伝えるためのメカニズムが不足しており、このコミットによってその情報がELFファイルに適切に埋め込まれるようになりました。

この変更は、Goが外部リンカを使用する際の互換性と堅牢性を向上させ、特にLinux/Xen環境でのGoプログラムの動作を安定させるために不可欠でした。

前提知識の解説

Thread-Local Storage (TLS)

Thread-Local Storage (TLS) は、マルチスレッドプログラミングにおいて、各スレッドがそれぞれ独立した変数を持つためのメカニズムです。グローバル変数や静的変数はすべてのスレッドで共有されますが、TLS変数はスレッドごとに異なる値を保持できます。これにより、スレッド間のデータ競合を防ぎ、スレッドセーフなコードを記述する上で非常に重要です。TLS変数の実際のメモリ位置は、プログラムの実行時、スレッドの作成時、または動的ライブラリのロード時に決定されるため、コンパイル時にはその最終アドレスは不明です。このため、リンカによる再配置処理が必要となります。

ELF (Executable and Linkable Format)

ELFは、Unix系システムで広く使用されている実行可能ファイル、オブジェクトファイル、共有ライブラリの標準ファイル形式です。ELFファイルは、プログラムのコード、データ、シンボル情報、再配置情報などを含んでいます。リンカは、複数のELFオブジェクトファイルを結合して実行可能ファイルや共有ライブラリを生成する際に、これらの情報を用いてシンボル参照を解決し、最終的なメモリ配置を決定します。

再配置 (Relocation) と R_X86_64_TPOFF32

再配置とは、コンパイル時にアドレスが確定できないシンボル参照(変数や関数のアドレスなど)を、リンク時に実際のメモリ位置に解決するプロセスです。リンカは、オブジェクトファイル内の再配置エントリを読み取り、シンボルの最終アドレスに基づいて参照を修正します。

R_X86_64_TPOFF32は、x86-64アーキテクチャのELFファイルで使用される特定の再配置タイプです。これはThread Pointer Offset (TPOFF) を計算するための32ビットの再配置であり、TLS変数へのアクセスに関連します。この再配置は、スレッドポインタ(各スレッドのTLSブロックの基底アドレスを指すレジスタまたはメモリ位置)からのオフセットとしてTLS変数のアドレスを解決するために使用されます。

Goの mg

Goランタイムは、独自のスケジューラを持っており、ゴルーチン(g)をOSスレッド(m)上で実行します。

  • g (Goroutine): Goの軽量な並行処理単位です。OSスレッドよりもはるかに軽量で、Goランタイムによって管理されます。
  • m (Machine): OSスレッドに対応します。ゴルーチンはM上で実行されます。

Goランタイムは、これらのmgに関する情報をスレッドローカルに保持することがあります。例えば、現在のゴルーチンやMのコンテキスト情報などです。これらの情報はTLS変数として扱われるため、リンカが適切に再配置する必要があります。

Goの外部リンクモード

Goは通常、Goのコードとランタイムをすべて含む単一の静的バイナリを生成します。しかし、Cgoを使用する場合や、特定のシステムライブラリにリンクする必要がある場合など、外部リンカ(gccclang)を使用する「外部リンクモード」でビルドすることができます。このモードでは、Goコンパイラは中間的なELFオブジェクトファイルを生成し、それを外部リンカに渡して最終的な実行可能ファイルを生成します。

Plan 9リンカ (8l, 6l)

Go言語の初期のツールチェインは、Plan 9オペレーティングシステムの設計思想に強く影響を受けていました。8lは32ビットx86アーキテクチャ(Plan 9の用語で「8ビット」)用のリンカ、6lはAMD64(x86-64)アーキテクチャ(「6ビット」)用のリンカでした。これらのリンカはGoのビルドプロセスで重要な役割を果たしていましたが、Goの進化に伴い、現在はGo自身で書かれた内部リンカが主流となっています。しかし、このコミットが書かれた時点では、これらのリンカがまだGoのツールチェインの一部として機能していました。

Xenと%gs:0x0スレッドローカルストレージ

Xenは、複数のゲストOSを単一の物理ハードウェア上で実行できるハイパーバイザです。仮想化環境では、OSがハードウェアに直接アクセスするのではなく、ハイパーバイザを介してアクセスするため、特定の低レベルなCPU機能の振る舞いが異なる場合があります。

x86アーキテクチャでは、%fs%gsといったセグメントレジスタがTLSの実装によく使われます。特に64ビットシステムでは、%gsレジスタがカーネルのパーCPUデータやスレッドローカルデータへのアクセスに用いられることがあります。

コミットメッセージにある「Xen's lack of support for this mode forced us long ago to use a two-instruction sequence: first we load %gs:0x0 into a register r, and then we use -0x4(r)」という記述は、Xen環境下で%gsレジスタを直接TLSの基底アドレスとして使用する一般的な方法が機能しなかったことを示唆しています。そのため、Goは回避策として、まず%gs:0x0%gsセグメントの基底アドレス)をレジスタにロードし、そのレジスタを基点としてオフセット-0x4でTLS変数にアクセスするという2段階の命令シーケンスを採用していました。これは、ELFプログラムローダが%gs:0x0にそのメモリ位置への通常のポインタを配置するように手配するためです。

技術的詳細

このコミットの核心は、Goのリンカが外部リンカに対して、TLS変数への特殊なアクセスパターンを適切に伝えるための新しいメカニズムを導入した点にあります。

通常のELFシステムでは、TLS変数への参照は%fs:-0x4%gs:-0x4のように、セグメントレジスタとオフセットを直接指定する形式で表現されます。Goの6a/8aアセンブラでは、これらは-0x4(FS)-0x4(GS)と記述されます。

しかし、Linux/ELF上のXen環境では、この直接的なアクセス方法が問題を引き起こしました。そのため、Goは以前から以下のような2命令シーケンスを使用していました。

  1. %gs:0x0をレジスタrにロードする。
  2. rからのオフセット-0x4でTLS変数にアクセスする(例: -0x4(r))。

この2命令シーケンスにおいて、-0x4(r)という参照がTLS関連であることをリンカに明示的に伝える必要がありました。リンカがこの参照を通常のレジスタ間接参照と区別し、TLS再配置として処理できるようにするためです。

このコミットでは、この目的のために新しい表記法-0x4(r)(GS*1)が導入されました。

  • -0x4(r): 実際の命令エンコーディングとしては、従来のレジスタ間接参照と同じです。
  • (GS*1): これはリンカに対するヒントであり、この参照がTLS関連であり、再配置が必要なものであることを示します。具体的には、GSセグメントレジスタに関連するTLSオフセット再配置(R_X86_64_TPOFF32など)として処理されるべきであることをリンカに伝えます。

この新しい表記法をGoのアセンブラ(8a)が解析できるように、文法定義ファイル(a.y)と生成されたパーサのコード(y.tab.c)が更新されました。また、リンカ(6l8l)は、この新しいTLS再配置タイプ(D_TLS)を認識し、ELFオブジェクトファイルに適切な再配置エントリ(R_X86_64_TPOFF32)を出力するように変更されました。これにより、外部リンカはmgの最終的なTLSオフセットを正しく解決できるようになります。

R_X86_64_TPOFF32再配置は、スレッドポインタからの32ビットオフセットを計算するために使用されます。この再配置タイプは、特に共有オブジェクトやPosition-Independent Executable (PIE) をビルドする際に、32ビットオフセットが位置独立コードに適さないためにリンカエラーを引き起こすことがありますが、このGoのケースでは、特定のTLSアクセスパターンを外部リンカに正しく伝えるために導入されました。

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

このコミットで変更された主要なファイルは以下の通りです。

  • misc/cgo/testtls/: 新しいCgoテストスイートが追加されました。これは、TLS再配置が外部リンカで正しく機能するかを検証するためのものです。
    • tls.go: Cgoを介してC言語のTLS関数を呼び出すGoのテストコード。
    • tls_test.go: tls.goのテストを実行するGoのテストファイル。
    • tls_unix.c: スレッドローカル変数tlsを定義し、その設定と取得を行うC言語の関数setTLSgetTLSを実装。
  • src/cmd/5l/5.out.h, src/cmd/6l/6.out.h, src/cmd/8l/8.out.h: リンカの出力形式を定義するヘッダファイルで、新しい再配置タイプD_TLSが追加されました。
  • src/cmd/6l/asm.c: x86-64リンカのアセンブラ関連コードで、D_TLS再配置タイプに対応するELF再配置エントリ(R_X86_64_TPOFF32)を出力するロジックが追加されました。
  • src/cmd/6l/obj.c: リンカのメインオブジェクトファイルで、リンクモードの初期設定がLinkInternalからLinkAutoに変更されました。これは、外部リンカの使用をより自動的に検出・利用するための変更です。
  • src/cmd/6l/span.c: x86-64リンカのコード生成および再配置処理を行うファイルで、D_INDIR+D_FS%fsセグメントレジスタからの間接参照)が外部リンクモードかつELFシステムの場合に、新しいD_TLS再配置タイプとして処理されるロジックが追加されました。ここでruntime.gruntime.mシンボルがTLSセクションに配置されるようにマークされます。
  • src/cmd/8a/a.y: 8aアセンブラの文法定義ファイル(Bison/Yacc形式)で、新しいTLS参照表記-0x4(r)(GS*1)を解析するための文法ルールが追加されました。
  • src/cmd/8a/y.tab.c: a.yから自動生成されるパーサのC言語ソースファイルです。a.yの変更に伴い、このファイルも大幅に更新されています。
  • src/cmd/8l/pass.c: 8lリンカのパス処理を行うファイルで、TLS関連のシンボル処理が更新されました。
  • src/cmd/ld/data.c, src/cmd/ld/elf.c, src/cmd/ld/lib.h, src/cmd/ld/symtab.c: 汎用リンカ(cmd/ld)のデータ構造、ELF出力、ライブラリ定義、シンボルテーブル関連のファイルで、TLS再配置をサポートするための変更が加えられました。
  • src/cmd/dist/buildruntime.c: ランタイムビルド関連のファイルで、TLS関連の変更が反映されています。
  • src/run.bash: テスト実行スクリプトで、新しいTLSテストが追加されました。

コアとなるコードの解説

src/cmd/8a/a.y および src/cmd/8a/y.tab.c

これらのファイルは、Goのアセンブラが新しいTLS参照表記-0x4(r)(GS*1)を正しく解釈できるようにするための変更を含んでいます。a.yはアセンブラの文法を定義するYacc/Bisonの入力ファイルであり、y.tab.cはその定義から自動生成されるC言語のパーサコードです。

a.yでは、omem(オペランドのメモリ参照)のルールに以下の新しいプロダクションが追加されました。

omem:
    con '(' LLREG ')' '(' LSREG '*' con ')'
    {
        $$ = nullgen;
        $$.type = D_INDIR+$3;
        $$.offset = $1;
        $$.index = $6;
        $$.scale = $8;
        checkscale($$.scale);
    }

このルールは、定数(レジスタ)(レジスタ*定数)という形式のメモリ参照を解析します。特に、con '(' LLREG ')' '(' LSREG '*' con ')'は、-0x4(r)(GS*1)のような形式に対応します。ここで、LLREGは汎用レジスタ(r)、LSREGはセグメントレジスタ(GS)、conは定数(-0x41)を表します。この変更により、アセンブラはTLS再配置が必要な特殊なメモリ参照を認識できるようになります。

src/cmd/6l/span.c

このファイルは、x86-64リンカのコード生成と再配置処理の中心的な部分です。特に、putrelv関数内で、D_INDIR+D_FS%fsセグメントレジスタからの間接参照)が外部リンクモードかつELFシステムの場合に、TLS再配置として処理されるロジックが追加されました。

    } else if(iself && linkmode == LinkExternal && a->type == D_INDIR+D_FS) {
        Reloc *r;
        Sym *s;
        
        r = addrel(cursym);
        r->off = curp->pc + andptr - and;
        r->add = 0;
        r->xadd = 0;
        r->siz = 4;
        r->type = D_TLS; // 新しいTLS再配置タイプを設定
        if(a->offset == tlsoffset+0)
            s = lookup("runtime.g", 0); // runtime.gシンボルをTLSとして扱う
        else
            s = lookup("runtime.m", 0); // runtime.mシンボルをTLSとして扱う
        s->type = STLSBSS; // シンボルタイプをTLS BSSに設定
        s->reachable = 1;
        s->size = PtrSize;
        s->hide = 1;
        r->sym = s;
        r->xsym = s;
        v = 0;
    }

このコードブロックは、%fsまたは%gsからの間接参照が検出された際に、それがruntime.gまたはruntime.mへのTLS参照であると判断し、新しいD_TLS再配置タイプを持つ再配置エントリを作成します。これにより、リンカはこれらの参照をTLS変数として扱い、外部リンカに適切な再配置情報を渡すことができます。

src/cmd/6l/asm.c

このファイルは、ELF再配置エントリを生成する部分です。elfreloc1関数にD_TLS再配置タイプを処理するケースが追加されました。

    case D_TLS:
        if(r->siz == 4)
            VPUT(R_X86_64_TPOFF32 | (uint64)elfsym<<32); // R_X86_64_TPOFF32再配置を出力
        else
            return -1;
        break;

このコードは、D_TLS再配置タイプが指定された場合に、R_X86_64_TPOFF32というELF再配置タイプを生成します。これは、x86-64アーキテクチャにおいて、スレッドポインタからのオフセットとしてTLS変数を参照するための標準的な再配置タイプです。この再配置エントリがELFオブジェクトファイルに書き込まれることで、外部リンカはTLS変数の最終的なアドレスを正しく解決できるようになります。

関連リンク

参考にした情報源リンク