[インデックス 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
だが、FS
やGS
/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
(スタックセグメント) などのセグメントレジスタが存在します。GS
とFS
もその一部であり、特定のメモリセグメントへのアクセスに使用されます。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アクセスの複雑さを解消し、より統一的で理解しやすいメカニズムを導入することです。そのために、以下の技術的変更が加えられました。
-
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にロード)。
- 386およびamd64アーキテクチャ向けに、
-
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命令形式を使用し、リンクモードの決定は最終的なリンク時まで遅延されるようになります。
- 2命令形式と1命令形式: 上記の
-
既存の動作の維持とテスト:
- この大規模な変更が既存の動作に影響を与えないことを保証するため、開発者は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がサポートするすべての
この変更により、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
が認識されるように定義が追加されました。
- 386およびamd64アセンブラの字句解析器に、新しい疑似レジスタ
src/cmd/6c/txt.c
,src/cmd/8c/txt.c
:- Cコンパイラにおいて、外部レジスタ(
OEXREG
)の参照タイプがD_INDIR + D_GS
からD_INDIR + D_TLS
に変更されました。これにより、コンパイラがTLSを抽象的なTLS
レジスタとして扱うようになります。
- Cコンパイラにおいて、外部レジスタ(
src/cmd/6l/6.out.h
,src/cmd/8l/8.out.h
:- リンカの出力ヘッダファイルに、新しいアドレスタイプ
D_TLS
が追加され、既存の定義の順序が調整されました。
- リンカの出力ヘッダファイルに、新しいアドレスタイプ
src/cmd/ld/data.c
:- リンカのデータ処理部分に、新しく導入された
R_TLS_LE
とR_TLS_IE
リロケーションを処理するためのロジックが追加されました。これにより、リンカはTLSオフセットをOSやリンクモードに応じて適切に計算・適用できるようになります。
- リンカのデータ処理部分に、新しく導入された
src/cmd/dist/buildruntime.c
:- ランタイムビルドスクリプトから、OS固有の
get_tls
,g
,m
マクロの定義が削除され、より汎用的なTLS
ベースの定義に置き換えられました。これは、TLSアクセスの詳細がliblink
のより低レベルな部分に集約されたことを反映しています。
- ランタイムビルドスクリプトから、OS固有の
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
リロケーションが発行されるようになりました。ymovtab
にTLS
レジスタをロードするための新しいエントリが追加されました。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
疑似レジスタに言及するように変更されました。
- ランタイムヘッダファイル内のコメントが更新され、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
疑似レジスタに置き換えられている箇所が見られます。
- これらのOS固有のアセンブリファイル内のコメントやTLS関連の命令が、新しい
コアとなるコードの解説
このコミットの核心は、Goのリンカ(liblink
)がTLSアクセスをどのように抽象化し、最適化するかという点にあります。特にsrc/liblink/obj6.c
およびsrc/liblink/obj8.c
のprogedit
関数と、それに関連する新しいリロケーションタイプが重要な役割を果たします。
TLS
疑似レジスタによる抽象化
以前は、TLSへのアクセスはGS
やFS
といった具体的なセグメントレジスタに依存し、OSやリンクモードによってその解釈や書き換えが複雑かつ一貫性がありませんでした。このコミットでは、TLS
という抽象的な疑似レジスタを導入することで、この複雑さを解消しました。コンパイラやアセンブラは、TLSへのアクセスをTLS
疑似レジスタを介して表現するようになり、具体的なセグメントレジスタの選択やオフセットの調整はリンカの役割となりました。
progedit
関数におけるTLS命令の書き換え
progedit
関数は、コンパイラによって生成されたTLS関連の命令を、ターゲットOSの特性とリンクモードに応じて最終的な機械語命令に変換する主要なロジックを含んでいます。
-
canuselocaltls(Link *ctxt)
関数:- このヘルパー関数は、現在のリンカコンテキスト (
ctxt
) において、TLSへの直接アクセス(ELF TLS Local Execモード)が可能かどうかを判断します。 Hwindows
,Hlinux
,Hnacl
,Hplan9
などの特定のOSでは、TLSへの直接アクセスがサポートされていないため、この関数はfalse
を返します。これは、これらのシステムではTLSベースアドレスをレジスタにロードしてからオフセットでアクセスする2命令形式(Initial Execモード)が必要であることを意味します。
- このヘルパー関数は、現在のリンカコンテキスト (
-
Local Execモードへの「緩和」:
canuselocaltls
がtrue
を返すシステム(TLSへの直接アクセスが可能なシステム)では、progedit
は以下のような最適化を行います。MOVQ TLS, BX
のような、TLSベースを汎用レジスタにロードする命令をNOP
(何もしない命令)に変換します。これは、TLSベースが直接アクセス可能であるため、中間レジスタへのロードが不要になり、命令数を削減できるためです。off(BX)(TLS*1)
のように、ロードされたレジスタを介してTLSにアクセスする命令を、off(TLS)
のように直接TLS
疑似レジスタを介してアクセスする形式に変換します。これにより、より効率的な1命令形式のTLSアクセスが実現されます。
-
Initial Execモードへの変換(Cコンパイラ向け):
canuselocaltls
がfalse
を返すシステム(TLSへの直接アクセスができないシステム)では、progedit
はCコンパイラが生成するoff(TLS)
形式の命令を、2命令形式のInitial Execモードに変換します。- 具体的には、
MOVL off(TLS), BX
のような命令を、MOVL TLS, BX
とMOVL off(BX)(TLS*1), BX
のシーケンスに展開します。これは、CコンパイラがTLSへの参照を直接off(TLS)
形式で出力することを可能にしつつ、リンカがそれを適切な実行時アクセスパターンに変換できるようにするための「配慮」です。
新しいリロケーションタイプ
include/link.h
で定義されたR_TLS_LE
とR_TLS_IE
は、それぞれTLS Local ExecモードとTLS Initial Execモードに対応する新しいリロケーションタイプです。
-
R_TLS_LE
(TLS Local Exec Offset):- このリロケーションは、TLSセグメントレジスタ(OSによって
FS
またはGS
)からのローカル実行オフセットを処理するために使用されます。 src/cmd/ld/data.c
のrelocsym
関数内で、ctxt->tlsoffset
(リンカが決定するTLSのベースオフセット)とリロケーションの加算値 (r->add
) を合計して最終的なオフセットを計算します。
- このリロケーションは、TLSセグメントレジスタ(OSによって
-
R_TLS_IE
(TLS Initial Exec Offset):- このリロケーションは、TLSベースポインタからの初期実行オフセットを処理するために使用されます。
relocsym
関数内で、ターゲットOS(iself
、Hplan9
、Hwindows
など)に応じて異なるオフセット計算ロジックが適用されます。これにより、各OSのTLSアクセス規約に合わせた正確なリロケーションが可能になります。
これらの変更により、GoのリンカはTLSアクセスをより抽象的かつ効率的に処理できるようになり、OSやリンクモードによる複雑な差異を内部で吸収することで、Go言語のポータビリティとランタイムの堅牢性が向上しました。
関連リンク
- Go CL: https://golang.org/cl/87920043
- Fixes issue #7164: https://github.com/golang/go/issues/7164
参考にした情報源リンク
- ELF TLS Initial Exec Mode and Local Exec Mode explanation:
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQELBgPUL98nVy4uyajirumtXYf9FxsuLGkrLH-mKrxWir1bwTIaKq25nDS7szOprPuEVu0pULk-hUc3KEtG6kI4_YGA6WDxx-GxIvbjNHBzjRgmooMagiWFphkWt6lLcmi96ZZIAuY14V-raxmhzdSjiYWMkXoXmPYUnAw-YhEMAePZ
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFUQEip5gWmM4LLxm2jYBjwgtb5SNU_sizMtYbS90Ag-fggOBdhTloFbMDw-4svYbQz0HYueuQU_B93kO-QrHRIvyzLlQsweUPmekoe2RGZSl9XKXKWPuEQpVR-lgrQ02LWyS1ZhWZRIGPpk9D2EwlZwhoJS9V4m3D_zUCXXfS93tRJkVn3_SyXyP8KJT48Wqj6iNCZr_dSBMn_WsBKIt4KKufYeRD7nzw6KIsDXg==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGPyL5Sf3eEThQWH1HMXJpBifCg_Cw73dyninbvY4pUUcbZAroua6NJ3ev6wRgfZlYzN8GjvMsuO61Qo5KzM0nJ919cizun6hFV7Xl3uL_SWQz4SadW1sRJ_BSj_u68ehTyUdSkGTPZb2r9hqON1vW0xRTSn-sQhN3PfxJi
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGArAVzhPK2XcIc63jwBp0TyTHHqGNCXgSafqFOH4DJYjkJbqwdYsjedh7UcBhh4FvHqqRWw60yU3ba_7_ywfDoui6qUNjNtGT0Iore-6pTmSLx9xIsThDq-_DfNBJiH8jScYRIzLRckI7CYC1E8rP6tFTy2BogGK9X
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFEKpv1nuQI5fAX42ZJY_Tqdkm4IjauakkPM-AkTs3xZvF1asHXNgHHJZWq8hE7DIvkWrEMXUBcMyYUfAxUdcg0yEqLawByWhFgYjALsPQQBhJ7UZIZSE01_tig893JC_Wb76_bPCs8ilGXf4_Wnmw34rHE0gxf2u6M9OYk
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHFK0Wo60WykYfiKAxDWgOn8atlH9tZI94DkHJi-VJGUKTXXHm0U0ry4dFn2L1xIRy2CKjpdVECvUI1lG41AEq7FFC1c5Hg68pWhPTZvW1WL-Z3eW038N6t54UJSGve55lrsW_DwCkDTyfzTjJNF7Oj-ShG3xbcFoywgUpQHSAla5A5d8XL
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFyv8rquPBWC_HSlgtr13guFOE0siTCbvN7Nhcai99gpJ1dzf6ckecccGBy46WGlkSgjNi_wcjzXbCG4G07PJrTIiDJAafXGKXjj8OgdUtUGb3KjX2gY665HEM=