[インデックス 19146] ファイルの概要
このコミットは、Go言語のリンカ(cmd/ld)における、ELFシステム上での外部リンカモードにおけるTLS(Thread Local Storage)再配置の取り扱いに関するものです。具体的には、Goプログラムが外部リンカ(例: gcc)を使用してリンクされる際に、スレッドローカル変数へのアクセスが正しく行われるようにするための修正が含まれています。
変更された主なファイルとその役割は以下の通りです。
src/cmd/6l/asm.c: GoリンカのAMD64(6l)アーキテクチャ向けアセンブリコード生成部分。TLS関連の新しい再配置タイプ(R_TLS_LE)の処理が追加されています。src/cmd/8l/asm.c: Goリンカのx86(8l)アーキテクチャ向けアセンブリコード生成部分。TLS関連の新しい再配置タイプ(R_TLS_LE,R_TLS_IE)の処理が追加されています。src/cmd/ld/data.c: Goリンカのデータ処理部分。再配置シンボルの解決ロジックが含まれており、外部リンカモードにおけるTLS再配置の挙動が修正されています。src/cmd/ld/lib.c: Goリンカのライブラリ処理部分。TLS関連のグローバルシンボル(gmsym)のコンテキストへの設定が追加されています。src/liblink/asm6.c:liblinkライブラリのAMD64向けアセンブリ生成部分。TLS変数へのアクセスコード生成ロジックが変更され、R_TLS_IE再配置が明示的に生成されるようになっています。src/liblink/asm8.c:liblinkライブラリのx86向けアセンブリ生成部分。asm6.cと同様に、TLS変数へのアクセスコード生成ロジックが変更され、R_TLS_IE再配置が明示的に生成されるようになっています。src/run.bash: Goのテストスクリプト。この修正に関連する新しいテストケースが追加されています。
コミット
このコミットは、Goリンカ(cmd/ld)がELFシステム上で外部リンカモードを使用する際に、TLS(Thread Local Storage)再配置を正しく処理するように変更します。これにより、Goプログラムが外部リンカと連携してスレッドローカル変数にアクセスする際の問題が解決されます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6f8b120869d5ee86adb163f317b14b1f1ee6d596
元コミット内容
cmd/ld: use TLS relocations on ELF systems in external linking mode
Fixes #7719.
LGTM=iant
R=iant
CC=golang-codereviews
https://golang.org/cl/87760050
変更の背景
このコミットは、Go言語のリンカがELF(Executable and Linkable Format)システム(主にLinuxなどのUnix系OS)上で外部リンカ(通常はgccなどのCコンパイラツールチェーンに含まれるリンカ)を使用する際に発生していた、TLS(Thread Local Storage)関連の問題を解決するために導入されました。
GoプログラムがCgo(GoからCのコードを呼び出す機能)を使用する場合や、特定のシステムライブラリにリンクする必要がある場合、Goの内部リンカではなく、システムの外部リンカを使用する「外部リンカモード」が選択されることがあります。このモードでは、Goのコンパイラとリンカが生成したオブジェクトファイルが、最終的に外部リンカによって結合されます。
TLSは、各スレッドが独自のデータコピーを持つことを可能にするメカニズムです。例えば、エラーコードやスレッド固有の設定など、グローバル変数でありながらスレッドごとに異なる値を持ちたい場合に利用されます。TLS変数へのアクセスは、通常、特定の再配置タイプ(例: R_TLS_LE, R_TLS_IE)を介して行われます。
問題は、Goのリンカが外部リンカモードでELFシステムをターゲットにする際に、これらのTLS再配置を適切に処理していなかった点にありました。これにより、TLS変数へのアクセスが正しく解決されず、プログラムの実行時にクラッシュしたり、予期せぬ動作を引き起こしたりする可能性がありました。コミットメッセージにあるFixes #7719は、この問題がGoのIssueトラッカーで報告されていたことを示しています。
このコミットは、外部リンカがTLS変数を正しく解決できるように、Goリンカが生成するオブジェクトファイル内の再配置情報を修正することで、この問題を解決します。
前提知識の解説
リンカの役割と再配置
リンカは、コンパイラによって生成された複数のオブジェクトファイルやライブラリを結合し、実行可能なプログラムや共有ライブラリを生成するツールです。このプロセスにおいて、リンカの最も重要な役割の一つが「再配置(Relocation)」です。
- 再配置: オブジェクトファイルが生成される時点では、関数や変数の最終的なメモリ上のアドレスは確定していません。リンカは、これらの未解決のシンボル参照(例えば、ある関数が別の関数を呼び出す際のアドレス)を、最終的な実行ファイル内での実際のアドレスに解決し、コード内の参照を修正します。この修正作業が再配置です。再配置情報は、オブジェクトファイル内の「再配置エントリ」として格納されています。
ELF (Executable and Linkable Format)
ELFは、Linux、FreeBSD、SolarisなどのUnix系オペレーティングシステムで広く使用されている、実行可能ファイル、オブジェクトファイル、共有ライブラリの標準フォーマットです。ELFファイルは、プログラムコード、データ、シンボルテーブル、再配置情報など、実行に必要な様々なセクションで構成されています。
TLS (Thread Local Storage)
TLSは、マルチスレッド環境において、各スレッドがグローバル変数や静的変数の独自のコピーを持つことを可能にするメカニズムです。これにより、スレッド間でデータを共有する際の競合状態を避け、スレッドセーフなプログラミングを容易にします。TLS変数へのアクセスは、通常、スレッドのローカルストレージ領域のベースアドレスからのオフセットとして解決されます。
TLS再配置タイプ
ELFシステムでは、TLS変数へのアクセス方法に応じて、いくつかの再配置タイプが定義されています。このコミットで特に重要なのは以下の2つです。
-
R_TLS_LE(TLS Local Executable):- スレッドローカル変数が、現在リンクされている実行可能ファイル自体に存在し、そのオフセットがコンパイル時に既知である場合に用いられます。
- 通常、TLSセグメントの先頭からのオフセットとして表現され、実行時にスレッドのTLSベースアドレスにこのオフセットを加算することで変数にアクセスします。
- このモードは、TLS変数が実行可能ファイル内で静的に割り当てられている場合に効率的です。
-
R_TLS_IE(TLS Initial Executable):- スレッドローカル変数が共有ライブラリに存在し、そのアドレスが実行時に解決される必要がある場合に用いられます。
- このモードでは、通常、GOT (Global Offset Table) やPLT (Procedure Linkage Table) といった間接的な参照メカニズムを介して変数にアクセスします。これにより、共有ライブラリがメモリ上のどこにロードされても、TLS変数に正しくアクセスできるようになります。
R_TLS_IEはR_TLS_LEよりも柔軟ですが、間接的なアクセスを伴うため、わずかにオーバーヘッドが大きくなります。
Goのリンカ (cmd/ld) と外部リンカモード
Go言語には独自のリンカであるcmd/ldがあります。通常、Goプログラムはcmd/ldによって完全にリンクされます。しかし、Cgoを使用する場合や、特定のシステムライブラリ(例えば、libc)に依存する場合など、Goの内部リンカだけでは対応できない状況があります。このような場合、Goのビルドシステムは-linkmode=externalフラグを使用して、最終的なリンク処理をシステムのCリンカ(例: ldまたはgcc)に委ねます。これが「外部リンカモード」です。
外部リンカモードでは、GoのコンパイラはGoのコードをオブジェクトファイルにコンパイルし、これらのオブジェクトファイルが外部リンカによって結合されます。この際、GoのランタイムやCgoによって生成されたTLS関連のシンボルや再配置情報が、外部リンカによって正しく解釈・処理される必要があります。
ctxt->gmsym
ctxtはリンカのコンテキストを表す構造体で、リンク処理に必要な様々な情報(シンボルテーブル、再配置リストなど)を保持しています。gmsym(おそらく "Go Module Symbol" または "Go Main Symbol" の略)は、Goのランタイムが内部的に使用する特定のグローバルシンボルを指します。このコミットでは、外部リンカモードでTLS再配置を正しく処理するために、このgmsymがTLS関連の再配置のターゲットとして設定されるようになります。
技術的詳細
このコミットの核心は、Goリンカが外部リンカモードでELFシステムをターゲットにする際に、TLS再配置の生成と処理を修正することです。
-
src/liblink/asm6.cおよびsrc/liblink/asm8.cにおけるTLSアクセスコードの変更:- これらのファイルは、Goのコンパイラが生成するアセンブリコードの一部として、TLS変数へのアクセス命令を生成する役割を担っています。
- 変更前は、TLS変数へのアクセスが特定の条件下で直接的なオフセットとして扱われることがありましたが、外部リンカモードではこれが問題を引き起こしていました。
- 変更後、
a->index == D_TLS(アドレスがTLSセグメントを指す場合)の条件が追加され、TLS変数へのアクセスに対しては常にR_TLS_IE(TLS Initial Executable)タイプの再配置が明示的に生成されるようになりました。 R_TLS_IE再配置は、リンカに対して、このTLS変数が共有ライブラリ(または外部リンカによって処理される他のモジュール)に存在し、実行時に間接的に解決される必要があることを伝えます。これにより、外部リンカがTLS変数のアドレスを正しく解決できるようになります。- 具体的には、
memset(&rel, 0, sizeof rel); rel.type = R_TLS_IE; rel.siz = 4; rel.sym = nil; rel.add = v; v = 0;のようなコードが挿入され、TLSアクセス命令のオペランドが0に設定され、代わりにR_TLS_IE再配置が追加されます。
-
src/cmd/ld/data.cにおける再配置シンボル解決の変更:relocsym関数は、リンカが再配置エントリを処理する際に呼び出されます。R_TLS_LEおよびR_TLS_IE再配置タイプに対して、linkmode == LinkExternal && iself(外部リンカモードかつELFシステム)の場合に特別な処理が追加されました。- この条件が満たされる場合、再配置のターゲットシンボル(
r->symおよびr->xsym)がctxt->gmsymに設定されます。ctxt->gmsymは、GoのランタイムがTLS関連の処理に使用する特別なシンボルです。 - これにより、外部リンカは、GoのTLS変数を、Goランタイムが期待する形式で処理できるようになります。
r->done = 0;は、この再配置がGoの内部リンカによってまだ最終的に解決されていないことを示し、外部リンカにその解決を委ねることを意味します。
-
src/cmd/6l/asm.cおよびsrc/cmd/8l/asm.cにおけるELF再配置の生成:elfreloc1関数は、ELF形式の再配置エントリを生成する役割を担っています。- このコミットでは、
R_TLS_LEおよびR_TLS_IE再配置タイプに対する処理が追加または修正されました。 - AMD64(
6l)では、R_TLS_LEに対してR_X86_64_TPOFF32(TLSポインタからのオフセット)再配置が生成されます。 - x86(
8l)では、R_TLS_LEおよびR_TLS_IEに対してR_386_TLS_LEまたはR_386_TLS_IE再配置が生成されます。 - これらの変更により、Goリンカは、外部リンカが期待する正しいELF TLS再配置タイプを生成するようになります。
-
src/cmd/ld/lib.cにおけるgmsymの初期化:loadlib関数内で、ctxt->gmsym = gmsym;という行が追加されました。- これにより、リンカのコンテキスト(
ctxt)にgmsymシンボルが確実に設定され、data.cでの再配置処理で利用できるようになります。
これらの変更により、Goのリンカは、外部リンカモードでビルドされたGoプログラムが、ELFシステム上でTLS変数に正しくアクセスできるよう、必要な再配置情報を適切に生成・処理するようになります。
コアとなるコードの変更箇所
src/cmd/ld/data.c
--- a/src/cmd/ld/data.c
+++ b/src/cmd/ld/data.c
@@ -184,9 +184,30 @@ relocsym(LSym *s)
o = r->add;
break;
case R_TLS_LE:
+ if(linkmode == LinkExternal && iself) {
+ r->done = 0;
+ r->sym = ctxt->gmsym;
+ r->xsym = ctxt->gmsym;
+ r->xadd = r->add;
+ o = 0;
+ if(thechar != '6')
+ o = r->add;
+ break;
+ }
o = ctxt->tlsoffset + r->add;
break;
+
case R_TLS_IE:
+ if(linkmode == LinkExternal && iself) {
+ r->done = 0;
+ r->sym = ctxt->gmsym;
+ r->xsym = ctxt->gmsym;
+ r->xadd = r->add;
+ o = 0;
+ if(thechar != '6')
+ o = r->add;
+ break;
+ }
if(iself || ctxt->headtype == Hplan9)
o = ctxt->tlsoffset + r->add;
else if(ctxt->headtype == Hwindows)
src/liblink/asm6.c
--- a/src/liblink/asm6.c
+++ b/src/liblink/asm6.c
@@ -2427,38 +2427,25 @@ asmandsz(Link *ctxt, Addr *a, int r, int rex, int m64)
goto putrelv;
}
if(t >= D_AX && t <= D_R15) {
- // TODO: Remove Hwindows condition.
- if(v == 0 && t != D_BP && t != D_R13 && (a->index != D_TLS || (ctxt->headtype == Hwindows && a->scale == 2))) {
+ if(a->index == D_TLS) {
+ memset(&rel, 0, sizeof rel);
+ rel.type = R_TLS_IE;
+ rel.siz = 4;
+ rel.sym = nil;
+ rel.add = v;
+ v = 0;
+ }
+ if(v == 0 && rel.siz == 0 && t != D_BP && t != D_R13) {
*ctxt->andptr++ = (0 << 6) | (reg[t] << 0) | (r << 3);
return;
}
- if(v >= -128 && v < 128 && (a->index != D_TLS || a->scale != 1)) {
+ if(v >= -128 && v < 128 && rel.siz == 0) {
ctxt->andptr[0] = (1 << 6) | (reg[t] << 0) | (r << 3);
-\t\t\tif(a->index == D_TLS) {\n-\t\t\t\tReloc *r;\n-\t\t\t\tmemset(&rel, 0, sizeof rel);\n-\t\t\t\trel.type = R_TLS_IE;\n-\t\t\t\trel.siz = 1;\n-\t\t\t\trel.sym = nil;\n-\t\t\t\trel.add = v;\n-\t\t\t\tr = addrel(ctxt->cursym);\n-\t\t\t\t*r = rel;\n-\t\t\t\tr->off = ctxt->curp->pc + ctxt->andptr + 1 - ctxt->and;\n-\t\t\t\tv = 0;\n-\t\t\t}\n ctxt->andptr[1] = v;
ctxt->andptr += 2;
return;
}
*ctxt->andptr++ = (2 << 6) | (reg[t] << 0) | (r << 3);
-\t\tif(a->index == D_TLS) {\n-\t\t\tmemset(&rel, 0, sizeof rel);\n-\t\t\trel.type = R_TLS_IE;\n-\t\t\trel.siz = 4;\n-\t\t\trel.sym = nil;\n-\t\t\trel.add = v;\n-\t\t\tv = 0;\n-\t\t}\n goto putrelv;
}
goto bad;
src/liblink/asm8.c
--- a/src/liblink/asm8.c
+++ b/src/liblink/asm8.c
@@ -1857,43 +1857,25 @@ asmand(Link *ctxt, Addr *a, int r)
goto putrelv;
}
if(t >= D_AX && t <= D_DI) {
-\t\t// TODO(rsc): Remove the Hwindows test.\n-\t\t// As written it produces the same byte-identical output as the code it replaced.\n-\t\tif(v == 0 && rel.siz == 0 && t != D_BP && (a->index != D_TLS || ctxt->headtype == Hwindows)) {\n+ if(a->index == D_TLS) {
+ memset(&rel, 0, sizeof rel);
+ rel.type = R_TLS_IE;
+ rel.siz = 4;
+ rel.sym = nil;
+ rel.add = v;
+ v = 0;
+ }
+ if(v == 0 && rel.siz == 0 && t != D_BP) {
*ctxt->andptr++ = (0 << 6) | (reg[t] << 0) | (r << 3);
return;
}
-\t\t// TODO(rsc): Change a->index tests to check D_TLS.\n-\t\t// Then remove the if statement inside the body.\n-\t\t// As written the code is clearly incorrect for external linking,\n-\t\t// but as written it produces the same byte-identical output as the code it replaced.\n-\t\tif(v >= -128 && v < 128 && rel.siz == 0 && (a->index != D_TLS || ctxt->headtype == Hwindows || a->scale != 1)) {\n+ if(v >= -128 && v < 128 && rel.siz == 0) {
ctxt->andptr[0] = (1 << 6) | (reg[t] << 0) | (r << 3);
-\t\t\tif(a->index == D_TLS) {\n-\t\t\t\tReloc *r;\n-\t\t\t\tmemset(&rel, 0, sizeof rel);\n-\t\t\t\trel.type = R_TLS_IE;\n-\t\t\t\trel.siz = 1;\n-\t\t\t\trel.sym = nil;\n-\t\t\t\trel.add = v;\n-\t\t\t\tr = addrel(ctxt->cursym);\n-\t\t\t\t*r = rel;\n-\t\t\t\tr->off = ctxt->curp->pc + ctxt->andptr + 1 - ctxt->and;\n-\t\t\t\tv = 0;\n-\t\t\t}\n ctxt->andptr[1] = v;
ctxt->andptr += 2;
return;
}
*ctxt->andptr++ = (2 << 6) | (reg[t] << 0) | (r << 3);
-\t\tif(a->index == D_TLS) {\n-\t\t\tmemset(&rel, 0, sizeof rel);\n-\t\t\trel.type = R_TLS_IE;\n-\t\t\trel.siz = 4;\n-\t\t\trel.sym = nil;\n-\t\t\trel.add = v;\n-\t\t\tv = 0;\n-\t\t}\n goto putrelv;
}
goto bad;
src/cmd/6l/asm.c
--- a/src/cmd/6l/asm.c
+++ b/src/cmd/6l/asm.c
@@ -281,6 +281,13 @@ elfreloc1(Reloc *r, vlong sectoff)
return -1;
break;
+ case R_TLS_LE:
+ if(r->siz == 4)
+ VPUT(R_X86_64_TPOFF32 | (uint64)elfsym<<32);
+ else
+ return -1;
+ break;
+
case R_PCREL:
if(r->siz == 4) {
if(r->xsym->type == SDYNIMPORT)
src/cmd/8l/asm.c
--- a/src/cmd/8l/asm.c
+++ b/src/cmd/8l/asm.c
@@ -263,7 +263,8 @@ elfreloc1(Reloc *r, vlong sectoff)
return -1;
break;
- case R_TLS:
+ case R_TLS_LE:
+ case R_TLS_IE:
if(r->siz == 4)
LPUT(R_386_TLS_LE | elfsym<<8);
else
src/cmd/ld/lib.c
--- a/src/cmd/ld/lib.c
+++ b/src/cmd/ld/lib.c
@@ -240,6 +240,7 @@ loadlib(void)
gmsym->size = 2*PtrSize;
gmsym->hide = 1;
gmsym->reachable = 1;
+ ctxt->gmsym = gmsym;
// Now that we know the link mode, trim the dynexp list.
x = CgoExportDynamic;
src/run.bash
--- a/src/run.bash
+++ b/src/run.bash
@@ -131,6 +131,7 @@ dragonfly-386 | dragonfly-amd64 | freebsd-386 | freebsd-amd64 | linux-386 | linu
go test -ldflags '-linkmode=external' || exit 1
go test -ldflags '-linkmode=auto' ../testtls || exit 1
go test -ldflags '-linkmode=external' ../testtls || exit 1
+ go test -ldflags '-linkmode=external -extldflags "-static -pthread"' ../testtls || exit 1
esac
) || exit $?
コアとなるコードの解説
src/cmd/ld/data.c の relocsym 関数
この変更は、リンカが再配置エントリを処理する際の挙動を修正します。
case R_TLS_LE:およびcase R_TLS_IE:ブロック:if(linkmode == LinkExternal && iself): この条件は、現在のリンクモードが外部リンカモードであり、かつターゲットシステムがELF形式を使用している場合にのみ、以下の特殊な処理を適用することを示しています。r->done = 0;: この再配置がGoの内部リンカによってまだ最終的に解決されていないことを示します。これにより、外部リンカがこの再配置を処理する責任を負うことになります。r->sym = ctxt->gmsym; r->xsym = ctxt->gmsym;: 再配置のターゲットシンボルをctxt->gmsymに設定します。gmsymはGoランタイムがTLS関連の処理に使用する特別なシンボルであり、外部リンカがこれを認識して適切に処理できるようにします。r->xadd = r->add;: 再配置のオフセット値をxaddにコピーします。o = 0;およびif(thechar != '6') o = r->add;: 再配置のオフセットを調整します。thechar != '6'は、x86(32ビット)アーキテクチャの場合を指し、AMD64(64ビット)とは異なるオフセット計算が必要になることを示唆しています。
src/liblink/asm6.c および src/liblink/asm8.c の asmandsz / asmand 関数
これらの変更は、GoのコンパイラがTLS変数へのアクセス命令を生成する際に、適切な再配置情報を付加するようにします。
if(a->index == D_TLS)ブロック:a->index == D_TLSは、現在処理しているアドレスがTLSセグメント内のデータへの参照であることを示します。memset(&rel, 0, sizeof rel); rel.type = R_TLS_IE; rel.siz = 4; rel.sym = nil; rel.add = v; v = 0;: ここで、R_TLS_IE(TLS Initial Executable)タイプの再配置エントリが明示的に作成されます。R_TLS_IEは、TLS変数が共有ライブラリに存在し、実行時に間接的に解決される必要があることをリンカに伝えます。rel.siz = 4は、再配置のサイズが4バイト(32ビット)であることを示します。rel.sym = nilは、この再配置が特定のシンボルに直接関連付けられていないことを意味し、リンカがTLSベースアドレスからのオフセットとして処理することを示唆します。v = 0;は、元の命令のオペランドを0に設定し、再配置によって実際のオフセットが埋められることを期待します。
- これにより、TLS変数へのアクセス命令は、外部リンカが正しく解釈できる
R_TLS_IE再配置を伴うようになります。
src/cmd/6l/asm.c および src/cmd/8l/asm.c の elfreloc1 関数
これらの変更は、ELF形式の再配置エントリを生成する際に、新しいTLS再配置タイプを正しくマッピングします。
-
src/cmd/6l/asm.cのcase R_TLS_LE::VPUT(R_X86_64_TPOFF32 | (uint64)elfsym<<32);: AMD64アーキテクチャにおいて、GoのR_TLS_LE再配置をELFのR_X86_64_TPOFF32再配置にマッピングします。R_X86_64_TPOFF32は、TLSポインタからの32ビットオフセットを意味します。
-
src/cmd/8l/asm.cのcase R_TLS_LE:およびcase R_TLS_IE::LPUT(R_386_TLS_LE | elfsym<<8);: x86アーキテクチャにおいて、GoのR_TLS_LEまたはR_TLS_IE再配置をELFのR_386_TLS_LE再配置にマッピングします。これは、32ビットシステムにおけるTLSローカル実行可能モデルの再配置です。
src/cmd/ld/lib.c の loadlib 関数
ctxt->gmsym = gmsym;: リンカのコンテキスト(ctxt)にgmsymシンボルを割り当てます。これにより、data.cなどの他のリンカコンポーネントが、TLS関連の再配置処理でこのgmsymシンボルを参照できるようになります。
src/run.bash
go test -ldflags '-linkmode=external -extldflags "-static -pthread"' ../testtls || exit 1: 新しいテストケースが追加されました。これは、外部リンカモードで-static -pthreadフラグを使用してtesttlsパッケージをテストするものです。これにより、TLS再配置の修正が正しく機能するかどうかを確認できます。
これらの変更は全体として、Goのリンカが外部リンカモードでELFシステムをターゲットにする際に、TLS変数へのアクセスが正しく解決されるように、再配置情報の生成と処理を改善しています。
関連リンク
- Go Change List: https://golang.org/cl/87760050
- Go Issue #7719 (このコミットによって修正された問題): https://github.com/golang/go/issues/7719 (直接的な情報が見つからない場合でも、コミットメッセージに記載されているため関連リンクとして記載)
参考にした情報源リンク
- ELF (Executable and Linkable Format) の仕様に関する一般的な情報源 (例: Wikipedia, Linux man pages)
- Thread Local Storage (TLS) に関する一般的な情報源 (例: Wikipedia, OS開発者向けドキュメント)
- Go言語のリンカおよびビルドプロセスに関するドキュメント (Goの公式ドキュメントやブログ記事)
- GoのIssueトラッカーおよびChange Listシステムに関する情報
web_fetchツールによるgolang.org/cl/87760050の要約