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

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

このコミットは、Go言語のリンカ(liblink)において、386およびamd64アーキテクチャ向けにスレッドローカルストレージ(TLS)アクセスを標準化し、明確化するために、新しい疑似レジスタTLSを導入するものです。これにより、これまでGS/FSレジスタの書き換えによって行われていた複雑で一貫性のないTLS処理が簡素化され、コードの可読性が向上し、共有ライブラリやリンクに関連する問題の解決を目指しています。

コミット

Author: Russ Cox <rsc@golang.org>
Date:   Tue Apr 15 13:45:39 2014 -0400

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

https://github.com/golang/go/commit/90093f0634d0143c6294e827e5c83fc0818ff8aa

元コミット内容

liblink: introduce TLS register on 386 and amd64

When I did the original 386 ports on Linux and OS X, I chose to
define GS-relative expressions like 4(GS) as relative to the actual
thread-local storage base, which was usually GS but might not be
(it might be FS, or it might be a different constant offset from GS or FS).

The original scope was limited but since then the rewrites have
gotten out of control. Sometimes GS is rewritten, sometimes FS.
Some ports do other rewrites to enable shared libraries and
other linking. At no point in the code is it clear whether you are
looking at the real GS/FS or some synthesized thing that will be
rewritten. The code manipulating all these is duplicated in many
places.

The first step to fixing issue 7719 is to make the code intelligible
again.

This CL adds an explicit TLS pseudo-register to the 386 and amd64.
As a register, TLS refers to the thread-local storage base, and it
can only be loaded into another register:

        MOVQ TLS, AX

An offset from the thread-local storage base is written off(reg)(TLS*1).
Semantically it is off(reg), but the (TLS*1) annotation marks this as
indexing from the loaded TLS base. This emits a relocation so that
if the linker needs to adjust the offset, it can. For example:

        MOVQ TLS, AX
        MOVQ 8(AX)(TLS*1), CX // load m into CX

On systems that support direct access to the TLS memory, this
pair of instructions can be reduced to a direct TLS memory reference:

        MOVQ 8(TLS), CX // load m into CX

The 2-instruction and 1-instruction forms correspond roughly to
ELF TLS initial exec mode and ELF TLS local exec mode, respectively.

Liblink applies this rewrite on systems that support the 1-instruction form.
The decision is made using only the operating system (and probably
the -shared flag, eventually), not the link mode. If some link modes
on a particular operating system require the 2-instruction form,
then all builds for that operating system will use the 2-instruction
form, so that the link mode decision can be delayed to link time.

Obviously it is late to be making changes like this, but I despair
of correcting issue 7719 and issue 7164 without it. To make sure
I am not changing existing behavior, I built a "hello world" program
for every GOOS/GOARCH combination we have and then worked
to make sure that the rewrite generates exactly the same binaries,
byte for byte. There are a handful of TODOs in the code marking
kludges to get the byte-for-byte property, but at least now I can
explain exactly how each binary is handled.

The targets I tested this way are:

        darwin-386
        darwin-amd64
        dragonfly-386
        dragonfly-amd64
        freebsd-386
        freebsd-amd64
        freebsd-arm
        linux-386
        linux-amd64
        linux-arm
        nacl-386
        nacl-amd64p32
        netbsd-386
        netbsd-amd64
        openbsd-386
        openbsd-amd64
        plan9-386
        plan9-amd64
        solaris-amd64
        windows-386
        windows-amd64

There were four exceptions to the byte-for-byte goal:

windows-386 and windows-amd64 have a time stamp
at bytes 137 and 138 of the header.

darwin-386 and plan9-386 have five or six modified
bytes in the middle of the Go symbol table, caused by
editing comments in runtime/sys_{darwin,plan9}_386.s.

Fixes #7164.

LGTM=iant
R=iant, aram, minux.ma, dave
CC=golang-codereviews
https://golang.org/cl/87920043

変更の背景

このコミットの背景には、Go言語のリンカ(liblink)におけるスレッドローカルストレージ(TLS)の扱いの複雑さと一貫性の欠如がありました。

元々、LinuxやOS X向けの386ポートでは、4(GS)のようなGS相対式は、実際のTLSベース(通常はGSだが、FSGS/FSからの異なる定数オフセットである場合もある)に対して相対的に定義されていました。しかし、その後の共有ライブラリやその他のリンクを可能にするための書き換えにより、TLSの処理は「手に負えない」状態になっていました。コードのどの部分が実際のGS/FSレジスタを参照しているのか、あるいは書き換えられる合成された値を参照しているのかが不明瞭であり、これらの処理を行うコードが多くの場所で重複していました。

このような状況は、コードの可読性と保守性を著しく低下させており、特にGoランタイムにおけるTLS関連のバグ(issue #7719やissue #7164)の修正を困難にしていました。このコミットは、これらの問題を解決し、TLS関連のコードを再び「理解可能」にすることを目的としています。

前提知識の解説

スレッドローカルストレージ (TLS: Thread-Local Storage)

TLSは、マルチスレッドプログラミングにおいて、各スレッドがグローバル変数や静的変数の独自のコピーを持つことを可能にするメモリ領域です。これにより、スレッド間で共有されないデータを効率的に管理し、競合状態を避けることができます。例えば、C++のthread_localやGCCの__thread拡張などがTLSを利用しています。

GS/FS セグメントレジスタ

x86およびx86-64アーキテクチャには、CS (コードセグメント)、DS (データセグメント)、SS (スタックセグメント) などのセグメントレジスタが存在します。GSFSもその一部であり、特定のメモリセグメントへのアクセスに使用されます。OSやアーキテクチャによっては、スレッドローカルストレージのベースアドレスを指すためにGSまたはFSレジスタが利用されることがあります。これらのレジスタは、スレッド固有のデータへの高速なアクセスを提供するために活用されます。

ELF TLS アクセスモデル

Executable and Linkable Format (ELF) におけるTLSは、スレッドローカル変数へのアクセス方法を定義するいくつかのモデルを提供します。これらのモデルは、汎用性と効率性のバランスを取るために設計されています。

ELF TLS Initial Exec Mode (IE)

  • 概要: このモデルは、スレッドローカル変数がメイン実行可能ファイル内、またはプログラム起動時にロードされる共有オブジェクト内で定義されている場合に使用されます。
  • 特徴: スレッドポインタ (TP) からのスレッドローカル変数のオフセットはロード時に既知ですが、共有オブジェクトの場合はリンク時定数ではない場合があります。
  • 使用例: コンパイラは通常、Position-Independent Code (PIC) フラグなしでコンパイルする場合、または非PICコンテキストでexternで変数が宣言されている場合に、このモードをデフォルトで選択します。共有オブジェクトもパフォーマンス向上のためにこのモデルでコンパイルされることがありますが、その場合、dlopenによる動的ロードが制限される可能性があります。
  • 効率: より一般的な「Global Dynamic」や「Local Dynamic」モデルよりも効率的ですが、「Local Exec」モードよりは効率が低いとされています。通常、TLSベースアドレスをレジスタにロードし、そのレジスタからのオフセットで変数にアクセスする2命令形式のアクセスパターンを伴います。

ELF TLS Local Exec Mode (LE)

  • 概要: 最も最適化された高速なTLSアクセスモデルです。
  • 特徴: このモデルは、スレッドローカル変数がメイン実行可能ファイル内で定義され、かつメイン実行可能ファイルからアクセスされる場合にのみ使用できます。スレッドポインタからの変数のオフセットはリンク時定数です。
  • 使用例: コンパイラは、変数が同じソースファイル内で定義および使用されている場合、特にPICなしでコンパイルする場合にこのモードを選択できます。リンカは、条件が満たされている場合(つまり、変数がメイン実行可能ファイル内にある場合)、「Initial Exec」アクセスを「Local Exec」に「緩和」(最適化)することができます。
  • 効率: 固定オフセットでスレッドローカル変数に直接アクセスできるため、最も効率的です。Global Offset Table (GOT) のルックアップが不要になることが多く、単一命令でTLS変数にアクセスできる場合があります。

技術的詳細

このコミットの主要な目的は、GoランタイムにおけるTLSアクセスの複雑さを解消し、より統一的で理解しやすいメカニズムを導入することです。そのために、以下の技術的変更が加えられました。

  1. TLS疑似レジスタの導入:

    • 386およびamd64アーキテクチャ向けに、TLSという新しい明示的な疑似レジスタが導入されました。このTLSは、スレッドローカルストレージのベースアドレスを抽象的に表現します。
    • TLSレジスタは、直接メモリにアクセスするのではなく、他の汎用レジスタにロードすることによってのみ使用できます。例えば、MOVQ TLS, AXのように記述されます。
    • TLSベースからのオフセットを持つアクセスは、off(reg)(TLS*1)という新しいアノテーション付きの形式で記述されます。これはセマンティックにはoff(reg)ですが、(TLS*1)というアノテーションが付加されることで、リンカがTLSベースからのインデックスであることを認識し、必要に応じてオフセットを調整するためのリロケーションを発行できるようになります。例: MOVQ TLS, AX; MOVQ 8(AX)(TLS*1), CX (mをCXにロード)。
  2. TLSアクセス命令の最適化と書き換え:

    • 2命令形式と1命令形式: 上記のMOVQ TLS, AX; MOVQ 8(AX)(TLS*1), CXのような2命令形式は、ELF TLSの「Initial Exec Mode」に相当します。一方、TLSメモリへの直接アクセスをサポートするシステムでは、この2命令がMOVQ 8(TLS), CXのような1命令形式に削減されます。これはELF TLSの「Local Exec Mode」に相当し、より効率的です。
    • liblinkによる書き換えの適用: liblinkは、1命令形式(Local Exec Mode)をサポートするシステムに対して、この最適化された書き換えを適用します。この決定は、リンクモード(例: 外部リンカを使用するかどうか)ではなく、オペレーティングシステム(および将来的には-sharedフラグ)のみに基づいて行われます。これにより、特定のOSで一部のリンクモードが2命令形式を必要とする場合でも、そのOS向けのすべてのビルドは2命令形式を使用し、リンクモードの決定は最終的なリンク時まで遅延されるようになります。
  3. 既存の動作の維持とテスト:

    • この大規模な変更が既存の動作に影響を与えないことを保証するため、開発者はGoがサポートするすべてのGOOS/GOARCHの組み合わせ(darwin-386, linux-amd64, windows-386など、合計21種類)で「hello world」プログラムをビルドし、生成されるバイナリがバイト単位で完全に同一であることを確認しました。
    • ただし、以下の4つの例外がありました。
      • windows-386およびwindows-amd64: ヘッダの137バイト目と138バイト目にタイムスタンプが含まれるため、常に同一にはなりません。
      • darwin-386およびplan9-386: runtime/sys_{darwin,plan9}_386.s内のコメント編集が原因で、Goシンボルテーブルの中央で5〜6バイトが変更されました。
    • コード内には、バイト単位の同一性を達成するための「kludges」(一時的な回避策)を示すTODOコメントがいくつか残されていますが、これにより各バイナリがどのように処理されるかを正確に説明できるようになりました。

この変更により、GoランタイムのTLSアクセスコードは、より明確で保守しやすくなり、将来的なTLS関連の機能拡張やバグ修正が容易になる基盤が築かれました。

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

このコミットでは、Goのコンパイラ、リンカ、ランタイムの複数のファイルにわたって広範な変更が行われています。主要な変更箇所は以下の通りです。

  • include/link.h:
    • 新しいリロケーションタイプ R_TLS_LE (TLS local exec offset from TLS segment register) と R_TLS_IE (TLS initial exec offset from TLS base pointer) が追加されました。
    • Link構造体からlinkmodeフィールドが削除されました。
  • src/cmd/6a/lex.c, src/cmd/8a/lex.c:
    • 386およびamd64アセンブラの字句解析器に、新しい疑似レジスタTLSが認識されるように定義が追加されました。
  • src/cmd/6c/txt.c, src/cmd/8c/txt.c:
    • Cコンパイラにおいて、外部レジスタ(OEXREG)の参照タイプがD_INDIR + D_GSからD_INDIR + D_TLSに変更されました。これにより、コンパイラがTLSを抽象的なTLSレジスタとして扱うようになります。
  • src/cmd/6l/6.out.h, src/cmd/8l/8.out.h:
    • リンカの出力ヘッダファイルに、新しいアドレスタイプD_TLSが追加され、既存の定義の順序が調整されました。
  • src/cmd/ld/data.c:
    • リンカのデータ処理部分に、新しく導入されたR_TLS_LER_TLS_IEリロケーションを処理するためのロジックが追加されました。これにより、リンカはTLSオフセットをOSやリンクモードに応じて適切に計算・適用できるようになります。
  • src/cmd/dist/buildruntime.c:
    • ランタイムビルドスクリプトから、OS固有のget_tls, g, mマクロの定義が削除され、より汎用的なTLSベースの定義に置き換えられました。これは、TLSアクセスの詳細がliblinkのより低レベルな部分に集約されたことを反映しています。
  • src/liblink/asm6.c, src/liblink/asm8.c:
    • アセンブラコード生成部分において、prefixof関数がLink *ctxt引数を取るように変更され、D_INDIR+D_TLSタイプのアドレスに対するセグメントプレフィックス(FSまたはGS)を決定するロジックが追加されました。
    • oclass関数にD_TLSが追加されました。
    • vaddr関数にD_INDIR+D_TLSタイプのアドレスに対するリロケーションタイプR_TLS_LEの設定ロジックが追加されました。
    • asmandsz関数でD_TLSの処理が追加され、R_TLS_IEリロケーションが発行されるようになりました。
    • ymovtabTLSレジスタをロードするための新しいエントリが追加されました。
    • doasm関数でprefixofの呼び出しが更新されました。
    • mov tls, r命令の処理ロジックが追加され、OS (Hsolaris, Hwindows, Hlinux, Hnacl, Hplan9) ごとに異なるTLSベースのロード方法が実装されました。
  • src/liblink/obj6.c, src/liblink/obj8.c:
    • リンカのオブジェクトファイル処理部分において、canuselocaltls関数が追加され、特定のOSではローカルTLSを使用できないことを判断するようになりました。
    • progedit関数が大幅に書き換えられ、TLS疑似レジスタを介したTLSアクセスを処理する新しいロジックが導入されました。これには、Initial ExecモードからLocal Execモードへの「緩和」処理や、Cコンパイラからの参照を適切なシーケンスに変換する処理が含まれます。
    • load_g_cx関数もTLSアクセスをD_INDIR+D_TLSを使用するように変更されました。
  • src/pkg/runtime/runtime.h:
    • ランタイムヘッダファイル内のコメントが更新され、TLSアクセスに関する説明がGSの代わりにTLS疑似レジスタに言及するように変更されました。
  • src/pkg/runtime/sys_darwin_386.s, src/pkg/runtime/sys_linux_386.s, src/pkg/runtime/sys_nacl_amd64p32.s, src/pkg/runtime/sys_plan9_386.s, src/pkg/runtime/sys_plan9_amd64.s:
    • これらのOS固有のアセンブリファイル内のコメントやTLS関連の命令が、新しいTLS疑似レジスタの概念に合わせて更新されました。特にsys_nacl_amd64p32.sでは、GSレジスタへの直接的な参照がTLS疑似レジスタに置き換えられている箇所が見られます。

コアとなるコードの解説

このコミットの核心は、Goのリンカ(liblink)がTLSアクセスをどのように抽象化し、最適化するかという点にあります。特にsrc/liblink/obj6.cおよびsrc/liblink/obj8.cprogedit関数と、それに関連する新しいリロケーションタイプが重要な役割を果たします。

TLS疑似レジスタによる抽象化

以前は、TLSへのアクセスはGSFSといった具体的なセグメントレジスタに依存し、OSやリンクモードによってその解釈や書き換えが複雑かつ一貫性がありませんでした。このコミットでは、TLSという抽象的な疑似レジスタを導入することで、この複雑さを解消しました。コンパイラやアセンブラは、TLSへのアクセスをTLS疑似レジスタを介して表現するようになり、具体的なセグメントレジスタの選択やオフセットの調整はリンカの役割となりました。

progedit関数におけるTLS命令の書き換え

progedit関数は、コンパイラによって生成されたTLS関連の命令を、ターゲットOSの特性とリンクモードに応じて最終的な機械語命令に変換する主要なロジックを含んでいます。

  1. canuselocaltls(Link *ctxt)関数:

    • このヘルパー関数は、現在のリンカコンテキスト (ctxt) において、TLSへの直接アクセス(ELF TLS Local Execモード)が可能かどうかを判断します。
    • Hwindows, Hlinux, Hnacl, Hplan9などの特定のOSでは、TLSへの直接アクセスがサポートされていないため、この関数はfalseを返します。これは、これらのシステムではTLSベースアドレスをレジスタにロードしてからオフセットでアクセスする2命令形式(Initial Execモード)が必要であることを意味します。
  2. Local Execモードへの「緩和」:

    • canuselocaltlstrueを返すシステム(TLSへの直接アクセスが可能なシステム)では、progeditは以下のような最適化を行います。
      • MOVQ TLS, BXのような、TLSベースを汎用レジスタにロードする命令をNOP(何もしない命令)に変換します。これは、TLSベースが直接アクセス可能であるため、中間レジスタへのロードが不要になり、命令数を削減できるためです。
      • off(BX)(TLS*1)のように、ロードされたレジスタを介してTLSにアクセスする命令を、off(TLS)のように直接TLS疑似レジスタを介してアクセスする形式に変換します。これにより、より効率的な1命令形式のTLSアクセスが実現されます。
  3. Initial Execモードへの変換(Cコンパイラ向け):

    • canuselocaltlsfalseを返すシステム(TLSへの直接アクセスができないシステム)では、progeditはCコンパイラが生成するoff(TLS)形式の命令を、2命令形式のInitial Execモードに変換します。
    • 具体的には、MOVL off(TLS), BXのような命令を、MOVL TLS, BXMOVL off(BX)(TLS*1), BXのシーケンスに展開します。これは、CコンパイラがTLSへの参照を直接off(TLS)形式で出力することを可能にしつつ、リンカがそれを適切な実行時アクセスパターンに変換できるようにするための「配慮」です。

新しいリロケーションタイプ

include/link.hで定義されたR_TLS_LER_TLS_IEは、それぞれTLS Local ExecモードとTLS Initial Execモードに対応する新しいリロケーションタイプです。

  • R_TLS_LE (TLS Local Exec Offset):

    • このリロケーションは、TLSセグメントレジスタ(OSによってFSまたはGS)からのローカル実行オフセットを処理するために使用されます。
    • src/cmd/ld/data.crelocsym関数内で、ctxt->tlsoffset(リンカが決定するTLSのベースオフセット)とリロケーションの加算値 (r->add) を合計して最終的なオフセットを計算します。
  • R_TLS_IE (TLS Initial Exec Offset):

    • このリロケーションは、TLSベースポインタからの初期実行オフセットを処理するために使用されます。
    • relocsym関数内で、ターゲットOS(iselfHplan9Hwindowsなど)に応じて異なるオフセット計算ロジックが適用されます。これにより、各OSのTLSアクセス規約に合わせた正確なリロケーションが可能になります。

これらの変更により、GoのリンカはTLSアクセスをより抽象的かつ効率的に処理できるようになり、OSやリンクモードによる複雑な差異を内部で吸収することで、Go言語のポータビリティとランタイムの堅牢性が向上しました。

関連リンク

参考にした情報源リンク