Open5

CygwinのClangでTLSを共有ライブラリからimportすると壊れる問題

okuokuokuoku

これ難しいな。

現象

C++を使ったコードをclangの最新版でビルドし、BFD ldでリンクすると壊れたexecutableができる。

Cygwin runtime failure: /cygdrive/f/webkit/build/webkit/bin/jsc.exe: Invalid relocation.  Offset 0x2dc64640c at address 0x1004077b0 doesn't fit into 32 bits
00000001004077b0 T __fu0___emutls_v._ZSt15__once_callable
00000001011c49ac T __fu100___emutls_v._ZSt15__once_callable
00000001012dbe70 T __fu101___emutls_v._ZSt15__once_callable

特徴としては、DLL上に存在するTLSが使われているケースで、同じコードはg++では正常にビルド/リンクできるようだ。

okuokuokuoku

EMUTLSを使用しているのは意図通り

Clangが2018年にそうなるようにパッチを当てている。

https://github.com/llvm/llvm-project/commit/9f9e4681ace64e6200f82d9d7f011701fd4e9cc8

ELFのTLSに比べて、Windowsが使用しているPEのTLSは表現力が低く、明示的に dllexport されていないシンボルについて完全に等価な機能を提供することができない。この辺はBFD ldのautoimportとEMUTLSで解決している。

... というかAndroidもEMUTLSなの。。?

https://android.googlesource.com/platform/bionic/+/HEAD/docs/elf-tls.md

一応ネイティブTLSの草案はあるけど。。

okuokuokuoku

gccとの差分を確認する

Stack overflowの質問 https://stackoverflow.com/questions/31617034/stdfuture-and-clang-with-stdlib-libstdc にあったサンプルで問題は再現できた。

#include <iostream>
#include <future>

int main()
{
  std::future<int> f1 = std::async([](){ return 42; });
  f1.wait();
  std::cout << "Magic number is: " << f1.get() << std::endl;
}

Cygwinの該当箇所 https://github.com/cygwin/cygwin/blob/8050ef207494e6d227e968cc7e5850153f943320/winsup/cygwin/pseudo-reloc.cc#L346 はautoimportのための自前のリロケーションを行っていて、ここに出力されているリロケーションが間違っているようだ。

デバッグ用のリンカオプション --enable-extra-pep-debug を使ってデバッグ出力させてみると、

clang++ -Wl,--enable-extra-pep-debug -fno-exceptions -gdwarf-4 check.cpp

clangは32bitのリロケーションを出力しているのに比べ、gccは64bitのリロケーションを出力しているという違いがある。よって、この差が問題を起こしているようだ。

clang
arelent: __emutls_v._ZSt15__once_callable@0x20: add=0
import of 0x0(0x0) sec_addr=0x20 pcrel 32 bit rel. // ★ 32ビット
creating runtime pseudo-reloc entry for __fu1___emutls_v._ZSt15__once_callable (addend=0)
gcc
arelent: __emutls_v._ZSt15__once_callable@0: add=0
import of 0x0(0x0) sec_addr=0x0 64 bit rel. // ★ 64ビット
creating runtime pseudo-reloc entry for __fu17___emutls_v._ZSt15__once_callable (addend=0)

ClangのMinGWサポートもちょっと前までバグっていた

この辺に関連しそうなコミットとしては

https://github.com/llvm/llvm-project/commit/0e4cf807aeaf54a10e02176498a7df13ac722b37

がある。これはMinGWにしか効いていないが、Cygwinでも適用されるのが妥当に見える。実際、 -emit-llvmでビットコードを出力させ、 llvm-dis でLLVM-IRを出力させてみると、

@_ZSt15__once_callable = external dso_local thread_local global i8*, align 8

のように、 dso_local としてEMUTLS変数が宣言されていて、 この dso_local を手で消してアセンブルしてみると正常にリンク/実行できる 。よってClang側をデバッグして、なぜこの変数が dso_local として宣言されてしまうのかを追求すれば良さそうだ。

okuokuokuoku

shouldAssumeDSOLocal の修正

... ここだけで行けるか。。?(ビルド中)

diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp
index 2346176a1562..74bd8614a346 100644
--- a/clang/lib/CodeGen/CodeGenModule.cpp
+++ b/clang/lib/CodeGen/CodeGenModule.cpp
@@ -1059,7 +1059,7 @@ static bool shouldAssumeDSOLocal(const CodeGenModule &CGM,
     return false;

   const llvm::Triple &TT = CGM.getTriple();
-  if (TT.isWindowsGNUEnvironment()) {
+  if (TT.isWindowsGNUEnvironment() || TT.isWindowsCygwinEnvironment()) {
     // In MinGW, variables without DLLImport can still be automatically
     // imported from a DLL by the linker; don't mark variables that
     // potentially could come from another DLL as DSO local.
okuokuokuoku

依然明示的に -femulated-tls が必要

とりあえず修正してみたが、依然 -femulated-tls が必要。。

clang++ -femulated-tls -fno-exceptions check.cpp

のようにしてコンパイルすることで正常に動作するようになった。 ...まだ -fno-exceptions が必要なのは多分ABIが間違っているため。