はじめに

Clang でコンパイルしたコードをデバッグすると、MSVC よりも遥かにトリッキーな最適化を目にすることが多いです。たまに想定外の動作に遭遇して、「これは絶対コンパイラーが間違っている」 と豪語しても、まじめに調べると間違っているのは大体自分の方です。しかし今回こそはコンパイラーが間違っていると思われる動作に遭遇したので紹介します。

未知との遭遇

問題となるコードを以下に示します。

#include <algorithm>
#include <stdio.h>
#include <windows.h>
#include <winnt.h>
#include <winternl.h>

template <typename T>
struct SimpleHolder {
  T val_ = {};
  void set(const T val) { val_ = val; }
  operator const T&() const { return val_; }
};

PVOID SwapThreadLocalStoragePointer(PVOID newValue) {
  std::swap(::NtCurrentTeb()->Reserved1[11], newValue);
  return newValue;
}

const uint32_t kTlsDataValue = 42;
static thread_local SimpleHolder<uint32_t> sTlsData;

__declspec(noinline) bool TestThreadLocalStorageHead() {
  auto origTlsHead = SwapThreadLocalStoragePointer(nullptr);
  bool isExceptionThrown = false;
  __try {
    sTlsData.set(~kTlsDataValue);
  }
  __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION
                     ? EXCEPTION_EXECUTE_HANDLER
                     : EXCEPTION_CONTINUE_SEARCH) {
    isExceptionThrown = true;
  }
  SwapThreadLocalStoragePointer(origTlsHead);
  sTlsData.set(kTlsDataValue);

  if (!isExceptionThrown) {
    printf("[%s] No exception from setter!\n", __FUNCTION__);
    return false;
  }
  if (sTlsData != kTlsDataValue) {
    printf("[%s] TLS is broken!\n", __FUNCTION__);
    return false;
  }
  printf("[%s] Passed!\n", __FUNCTION__);
  fflush(stdout);
  return true;
}

int main(int argc, char* argv[]) {
  TestThreadLocalStorageHead();
  return 0;
}

何をやっているかといえば、TEB に保存されている TLS Storage へのポインターを null にセットしてから実際に thread local な変数にアクセスし、AV が発生することを確かめています。このコードを Clang 9.0.0 x86_64-pc-windows-msvc の最適化オプション O2 でビルドして Windows 10 x64 上で実行すると、なんと Second chance exception でクラッシュします。

問題 1: Compile-time Memory Ordering

実行時のデバッグ ログです。

0:000> g
(1db4.b68): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for t.exe
t!TestThreadLocalStorageHead+0x60:
00007ff7`94981080 488b04c1        mov     rax,qword ptr [rcx+rax*8] ds:00000000`00000000=????????????????
0:000> g
(1db4.b68): Access violation - code c0000005 (!!! second chance !!!)
t!TestThreadLocalStorageHead+0x60:
00007ff7`94981080 488b04c1        mov     rax,qword ptr [rcx+rax*8] ds:00000000`00000000=????????????????
0:000> uf t!TestThreadLocalStorageHead
t!TestThreadLocalStorageHead:
00007ff7`94981020 55              push    rbp
00007ff7`94981021 56              push    rsi
00007ff7`94981022 4883ec28        sub     rsp,28h
00007ff7`94981026 488d6c2420      lea     rbp,[rsp+20h]
00007ff7`9498102b 65488b042530000000 mov   rax,qword ptr gs:[30h]
00007ff7`94981034 65488b0c2558000000 mov   rcx,qword ptr gs:[58h]
00007ff7`9498103d 488b7058        mov     rsi,qword ptr [rax+58h]
00007ff7`94981041 48c7405800000000 mov     qword ptr [rax+58h],0
00007ff7`94981049 8b05f12f0000    mov     eax,dword ptr [t!_tls_index (00007ff7`94984040)]
00007ff7`9498104f 488b04c1        mov     rax,qword ptr [rcx+rax*8]
00007ff7`94981053 488d8804000000  lea     rcx,[rax+4]
00007ff7`9498105a bad5ffffff      mov     edx,0FFFFFFD5h
00007ff7`9498105f e8bc000000      call    t!SimpleHolder<unsigned int>::set (00007ff7`94981120) <<<< 1st sTlsData.set()
00007ff7`94981064 65488b042530000000 mov   rax,qword ptr gs:[30h]
00007ff7`9498106d 65488b0c2558000000 mov   rcx,qword ptr gs:[58h]
00007ff7`94981076 48897058        mov     qword ptr [rax+58h],rsi
00007ff7`9498107a 8b05c02f0000    mov     eax,dword ptr [t!_tls_index (00007ff7`94984040)]
00007ff7`94981080 488b04c1        mov     rax,qword ptr [rcx+rax*8] <<<< crash!!
00007ff7`94981084 c780040000002a000000 mov dword ptr [rax+4],2Ah <<<< <<<< 2nd sTlsData.set()
00007ff7`9498108e 488d0d871f0000  lea     rcx,[t!`string' (00007ff7`9498301c)]
00007ff7`94981095 488d15641f0000  lea     rdx,[t!`string' (00007ff7`94983000)]
00007ff7`9498109c e88f000000      call    t!printf (00007ff7`94981130)
00007ff7`949810a1 31c0            xor     eax,eax
00007ff7`949810a3 4883c428        add     rsp,28h
00007ff7`949810a7 5e              pop     rsi
00007ff7`949810a8 5d              pop     rbp
00007ff7`949810a9 c3              ret

クラッシュしているのは __try で囲んだ最初の sTlsData.set ではなく、2 度目の sTlsData.set であることが分かります。したがって、例外が捕捉されなかったことが問題なのではなく、最初の sTlsData.set で例外が発生しなかったことがおかしいのです。

クラッシュが起きなかった理由はアセンブリを見ると明らかで、コンパイラーが処理の順番を入れ替えたからです。具体的にはこの部分。

00007ff7`9498102b 65488b042530000000 mov   rax,qword ptr gs:[30h] <<<< TEB
00007ff7`94981034 65488b0c2558000000 mov   rcx,qword ptr gs:[58h] <<<< caching TLS head #1
00007ff7`9498103d 488b7058        mov     rsi,qword ptr [rax+58h] <<<< caching TLS head #2
00007ff7`94981041 48c7405800000000 mov     qword ptr [rax+58h],0 <<<< resetting TLS head
00007ff7`94981049 8b05f12f0000    mov     eax,dword ptr [t!_tls_index (00007ff7`94984040)]
00007ff7`9498104f 488b04c1        mov     rax,qword ptr [rcx+rax*8]
00007ff7`94981053 488d8804000000  lea     rcx,[rax+4] <<<< rcx = sTlsData

本来であれば +104f の mov でクラッシュして欲しいのですが、TLS Head をリセットする +1041 の命令よりも先に、リセット前の TLS head を +1034 の命令でキャッシュしてしまっているため、クラッシュしません。

ソースコードの順番を忠実に守るのであれば、この部分は以下のようにコンパイルされるべきです。TLS Head をキャッシュする処理は SwapThreadLocalStoragePointer ではなく sTlsData.set の一部であるため、TLS Head をリセットした後に実行しなければなりません。

mov   rax,qword ptr gs:[30h] <<<< TEB
mov   rsi,qword ptr [rax+58h] <<<< caching TLS head #2
mov   qword ptr [rax+58h],0 <<<< resetting TLS head

mov   rcx,qword ptr gs:[58h] <<<< caching TLS head #1
mov   eax,dword ptr [t!_tls_index (00007ff7`94984040)]
mov   rax,qword ptr [rcx+rax*8]
lea   rcx,[rax+4] <<<< rcx = sTlsData

推測ですが、Clang はキャッシュ ヒット率を上げるために命令を入れ替えている気がします。つまり、gs セグメントにアクセスする処理をまとめたい、という狙いです。結果的に Windows では gs:[30h]TEB::NtTib.Self として自分自身を参照しているだけで、ptr[gs:[30h] + 58] == gs:[58h] という式が成立するため、順番を入れ替えるメリットは無いはずですが、コンパイラーはその事実を知りません。

この現象を回避するため、まずは基本に忠実に Memory Barrier を入れてみましたが、回避できませんでした。具体的には、以下のように TestThreadLocalStorageHead の冒頭にインライン アセンブリを追加してみましたが、コンパイル結果は Memory Barrier の有無に関わらず全く同じでした。

__declspec(noinline) bool TestThreadLocalStorageHead() {
  auto origTlsHead = SwapThreadLocalStoragePointer(nullptr);
#if defined(__clang__)
  asm volatile("" ::: "memory");
#endif
  bool isExceptionThrown = false;
  __try {
    sTlsData.set(~kTlsDataValue);
  }
  ...

volatile を使っても駄目で、他にエレガントな回避方法が思い浮かばなかったので、SwapThreadLocalStoragePointer のインライン展開を明示的に無効化して回避してみました。

コードをこうします。SwapThreadLocalStoragePointer__declspec(noinline) を付けただけです。

#include <algorithm>
#include <stdio.h>
#include <windows.h>
#include <winnt.h>
#include <winternl.h>

template <typename T>
struct SimpleHolder {
  T val_ = {};
  void set(const T val) { val_ = val; }
  operator const T&() const { return val_; }
};

__declspec(noinline) PVOID SwapThreadLocalStoragePointer(PVOID newValue) {
  std::swap(::NtCurrentTeb()->Reserved1[11], newValue);
  return newValue;
}

const uint32_t kTlsDataValue = 42;
static thread_local SimpleHolder<uint32_t> sTlsData;

__declspec(noinline) bool TestThreadLocalStorageHead() {
  auto origTlsHead = SwapThreadLocalStoragePointer(nullptr);
  bool isExceptionThrown = false;
  __try {
    sTlsData.set(~kTlsDataValue);
  }
  __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION
                     ? EXCEPTION_EXECUTE_HANDLER
                     : EXCEPTION_CONTINUE_SEARCH) {
    isExceptionThrown = true;
  }
  SwapThreadLocalStoragePointer(origTlsHead);
  sTlsData.set(kTlsDataValue);

  if (!isExceptionThrown) {
    printf("[%s] No exception from setter!\n", __FUNCTION__);
    return false;
  }
  if (sTlsData != kTlsDataValue) {
    printf("[%s] TLS is broken!\n", __FUNCTION__);
    return false;
  }
  printf("[%s] Passed!\n", __FUNCTION__);
  fflush(stdout);
  return true;
}

int main(int argc, char* argv[]) {
  TestThreadLocalStorageHead();
  return 0;
}

アセンブリはこうなりました。今度は sTlsData にアクセスするための TLS Head の値を SwapThreadLocalStoragePointer より後の +103b でキャッシュしているため、Ordering の問題は回避成功です。

0:000> uf t!TestThreadLocalStorageHead
t!TestThreadLocalStorageHead:
00007ff7`03a11020 55              push    rbp
00007ff7`03a11021 56              push    rsi
00007ff7`03a11022 4883ec28        sub     rsp,28h
00007ff7`03a11026 488d6c2420      lea     rbp,[rsp+20h]
00007ff7`03a1102b 31c9            xor     ecx,ecx
00007ff7`03a1102d e8ceffffff      call    t!SwapThreadLocalStoragePointer (00007ff7`03a11000)
00007ff7`03a11032 4889c6          mov     rsi,rax
00007ff7`03a11035 8b0505300000    mov     eax,dword ptr [t!_tls_index (00007ff7`03a14040)]
00007ff7`03a1103b 65488b0c2558000000 mov   rcx,qword ptr gs:[58h]
00007ff7`03a11044 488b04c1        mov     rax,qword ptr [rcx+rax*8]
00007ff7`03a11048 488d8804000000  lea     rcx,[rax+4]
00007ff7`03a1104f bad5ffffff      mov     edx,0FFFFFFD5h
00007ff7`03a11054 e8b7000000      call    t!SimpleHolder<unsigned int>::set (00007ff7`03a11110)
00007ff7`03a11059 4889f1          mov     rcx,rsi
00007ff7`03a1105c e89fffffff      call    t!SwapThreadLocalStoragePointer (00007ff7`03a11000)
00007ff7`03a11061 8b05d92f0000    mov     eax,dword ptr [t!_tls_index (00007ff7`03a14040)]
00007ff7`03a11067 65488b0c2558000000 mov   rcx,qword ptr gs:[58h]
00007ff7`03a11070 488b04c1        mov     rax,qword ptr [rcx+rax*8]
00007ff7`03a11074 c780040000002a000000 mov dword ptr [rax+4],2Ah
00007ff7`03a1107e 488d0d961f0000  lea     rcx,[t!`string' (00007ff7`03a1301b)]
00007ff7`03a11085 488d15741f0000  lea     rdx,[t!`string' (00007ff7`03a13000)]
00007ff7`03a1108c e88f000000      call    t!printf (00007ff7`03a11120)
00007ff7`03a11091 31c0            xor     eax,eax
00007ff7`03a11093 4883c428        add     rsp,28h
00007ff7`03a11097 5e              pop     rsi
00007ff7`03a11098 5d              pop     rbp
00007ff7`03a11099 c3              ret
0:000> uf t!SwapThreadLocalStoragePointer
t!SwapThreadLocalStoragePointer:
00007ff7`03a11000 65488b142530000000 mov   rdx,qword ptr gs:[30h]
00007ff7`03a11009 488b4258        mov     rax,qword ptr [rdx+58h]
00007ff7`03a1100d 48894a58        mov     qword ptr [rdx+58h],rcx
00007ff7`03a11011 c3              ret

問題 2: Wrong SEH Context

実はここからが本題です。SwapThreadLocalStoragePointer のインライン展開を無効にした上記プログラムを実行すると、今度は SEH しているはずの初回の sTlsData.set 呼び出しで Second chance exception が発生します。なぜか SEH が機能していません。

0:000> g
(2b80.1890): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for t.exe
rax=0000000000000000 rbx=000002ac11694130 rcx=0000000000000000
rdx=000000d311b64000 rsi=000002ac116939a0 rdi=000002ac1169b400
rip=00007ff703a11044 rsp=000000d31194fa60 rbp=000000d31194fa80
 r8=000002ac1169b400  r9=00007ff975e31ec0 r10=0000000000000012
r11=000002ac11696f40 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
t!TestThreadLocalStorageHead+0x24:
00007ff7`03a11044 488b04c1        mov     rax,qword ptr [rcx+rax*8] ds:00000000`00000000=????????????????
0:000> g
(2b80.1890): Access violation - code c0000005 (!!! second chance !!!)
rax=0000000000000000 rbx=000002ac11694130 rcx=0000000000000000
rdx=000000d311b64000 rsi=000002ac116939a0 rdi=000002ac1169b400
rip=00007ff703a11044 rsp=000000d31194fa60 rbp=000000d31194fa80
 r8=000002ac1169b400  r9=00007ff975e31ec0 r10=0000000000000012
r11=000002ac11696f40 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010244
t!TestThreadLocalStorageHead+0x24:
00007ff7`03a11044 488b04c1        mov     rax,qword ptr [rcx+rax*8] ds:00000000`00000000=???????????????? 

ここでも基本に忠実に、First chance exception からの動作をデバッグすることにします。まずは !exchain で例外ハンドラーのアドレスを調べて、ハンドラーが実行されるかどうかを確認します。

0:000> g
(2bbc.e0c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for t.exe
t!TestThreadLocalStorageHead+0x24:
00007ff7`03a11044 488b04c1        mov     rax,qword ptr [rcx+rax*8] ds:00000000`00000000=????????????????
0:000> !exchain
4 stack frames, scanning for handlers...
Frame 0x00: t!TestThreadLocalStorageHead+0x24 (00007ff7`03a11044)
  ehandler t!_C_specific_handler (00007ff7`03a12060)
Frame 0x02: error getting module for 000000000000001f
Frame 0x03: error getting module for 0000000000000001
0:000> bp (00007ff7`03a12060)
0:000> g
Breakpoint 0 hit
t!_C_specific_handler:
00007ff7`03a12060 ff25d2140000    jmp     qword ptr [t!_imp___C_specific_handler (00007ff7`03a13538)] ds:00007ff7`03a13538={VCRUNTIME140!__C_specific_handler (00007ff9`6123b830)}
0:000> p
VCRUNTIME140!__C_specific_handler:
00007ff9`6123b830 48895c2408      mov     qword ptr [rsp+8],rbx ss:00000013`98efe8c0={t!__favor <PERF> (t+0x5000) (00007ff7`03a15000)}
0:000> kn
 # Child-SP          RetAddr           Call Site
00 00000013`98efe8b8 00007ff9`78dc11ff VCRUNTIME140!__C_specific_handler [d:\agent\_work\2\s\src\vctools\crt\vcruntime\src\eh\riscchandler.cpp @ 175]
01 00000013`98efe8c0 00007ff9`78d8a289 ntdll!RtlpExecuteHandlerForException+0xf
02 00000013`98efe8f0 00007ff9`78dbfe6e ntdll!RtlDispatchException+0x219
03 00000013`98eff000 00007ff7`03a11044 ntdll!KiUserExceptionDispatch+0x2e
04 00000013`98eff7a0 00007ff7`03a11109 t!TestThreadLocalStorageHead+0x24
05 00000013`98eff7e0 00000000`0000001f t!main+0x9
06 00000013`98eff7e8 00000000`00000001 0x1f
07 00000013`98eff7f0 00000000`00000000 0x1

ハンドラーは VCRUNTIME140!__C_specific_handler で確実に実行されており、ここまでは問題なさそうです。この __C_specific_handler 関数については MSDN のページがあります。

__C_specific_handler function - Win32 apps | Microsoft Docs
https://docs.microsoft.com/en-us/windows/win32/devnotes/–c-specific-handler2

実はソースコードが MSVC とともにインストールされているはずです。私の Visual Studio 2019 の環境では、C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.23.28105\crt\src\vcruntime\riscchandler.cpp というファイルに定義がありました。

ファイルが見つかればロジックを見るのは簡単です。__C_specific_handler の第四パラメーターDispatcherContext の中に ScopeTable という配列が保持されていて、この配列の各要素が例外の捕捉開始アドレス、捕捉終了アドレス、例外フィルターのアドレス、そして例外ハンドラーのアドレスを RVA として保持しています。__C_specific_handler は、配列をシーケンシャルにループして、例外の発生場所を範囲に含む ScopeTable の要素が見つかれば、該当する例外フィルターを実行し、その結果が EXCEPTION_EXECUTE_HANDLER であればハンドラーを実行するというシンプルな実装になっています。では、__C_specific_handler が呼ばれた時点での ScopeTable の値を見てみます。

0:000> r
rax=00007ff703a12060 rbx=0000000000000000 rcx=0000001398eff4f0
rdx=0000001398eff7a0 rsi=0000001398eff4f0 rdi=0000000000000000
rip=00007ff96123b830 rsp=0000001398efe8b8 rbp=0000001398efee30
 r8=0000001398eff000  r9=0000001398efee80 r10=00007ff703a10000
r11=0000000000000001 r12=00007ff703a12060 r13=0000001398eff000
r14=0000001398efe930 r15=0000000000000000
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
VCRUNTIME140!__C_specific_handler:
00007ff9`6123b830 48895c2408      mov     qword ptr [rsp+8],rbx ss:00000013`98efe8c0={t!__favor <PERF> (t+0x5000) (00007ff7`03a15000)}
0:000> dv
   ExceptionRecord = 0x00000013`98eff4f0
  EstablisherFrame = 0x00000013`98eff7a0
     ContextRecord = 0x00000013`98eff000
 DispatcherContext = 0x00000013`98efee80
         ImageBase = <value unavailable>
   ExceptionFilter = <value unavailable>
             Index = <value unavailable>
       TargetIndex = <value unavailable>
          TargetPc = <value unavailable>
TerminationHandler = <value unavailable>
        ScopeTable = <value unavailable>
           Handler = <value unavailable>
 ExceptionPointers = struct _EXCEPTION_POINTERS
             Value = <value unavailable>
         ControlPc = <value unavailable>
0:000> dt DispatcherContext HandlerData
Local var @ r9 Type _DISPATCHER_CONTEXT*
   +0x038 HandlerData : 0x00007ff7`03a13b50 Void
0:000> dt vcruntime140!SCOPE_TABLE_AMD64 0x00007ff7`03a13b50
   +0x000 Count            : 1
   +0x004 ScopeRecord      : [1] _SCOPE_TABLE_AMD64::<unnamed-type-ScopeRecord>
0:000> dt vcruntime140!SCOPE_TABLE_AMD64 0x00007ff7`03a13b50 ScopeRecord[0].
   +0x004 ScopeRecord     : [0]
      +0x000 BeginAddress    : 0x1050
      +0x004 EndAddress      : 0x105a
      +0x008 HandlerAddress  : 0x10f0
      +0x00c JumpTarget      : 0x109a

それっぽいレコードが一件ありました。

ところで、riscchandler.cpp の判定条件は以下のようになっています。

        for (Index = DispatcherContext->ScopeIndex; Index < ScopeTable->Count; Index += 1) {
            if ((ControlPc >= ScopeTable->ScopeRecord[Index].BeginAddress) &&
                (ControlPc < ScopeTable->ScopeRecord[Index].EndAddress) &&

したがって、上記デバッグログから見つかった ScopeRecord が定義する [+1050, +105a) の範囲で例外が発生した場合、フィルター +10f0 を実行してからハンドラー +0x109a にジャンプします。

なお、フィールドの名前が HandlerAddress となっていますがこれは間違った名称で、本来は FilterAddress とすべきです。無意味な難読化が施されています。

RVA が分かったので、アセンブリを確認します。BeginAddressEndAddress の RVA は、__try ブロックに基づいてコンパイラーが導出しているはずで、当然 t!TestThreadLocalStorageHead の範囲内にあります。

0:000> u t!TestThreadLocalStorageHead t+105a
t!TestThreadLocalStorageHead:
00007ff7`03a11020 55              push    rbp
00007ff7`03a11021 56              push    rsi
00007ff7`03a11022 4883ec28        sub     rsp,28h
00007ff7`03a11026 488d6c2420      lea     rbp,[rsp+20h]
00007ff7`03a1102b 31c9            xor     ecx,ecx
00007ff7`03a1102d e8ceffffff      call    t!SwapThreadLocalStoragePointer (00007ff7`03a11000)
00007ff7`03a11032 4889c6          mov     rsi,rax
00007ff7`03a11035 8b0505300000    mov     eax,dword ptr [t!_tls_index (00007ff7`03a14040)]
00007ff7`03a1103b 65488b0c2558000000 mov   rcx,qword ptr gs:[58h]
00007ff7`03a11044 488b04c1        mov     rax,qword ptr [rcx+rax*8]
00007ff7`03a11048 488d8804000000  lea     rcx,[rax+4]
00007ff7`03a1104f bad5ffffff      mov     edx,0FFFFFFD5h
00007ff7`03a11054 e8b7000000      call    t!SimpleHolder<unsigned int>::set (00007ff7`03a11110)
00007ff7`03a11059 4889f1          mov     rcx,rsi

[+1050, +105a) という範囲に含まれるのは、+1054 の call と +1059 の mov のみであり、例外が発生した +1044 は範囲外です。これが、Second chance exception の発生した原因です。Clang が間違っている!

Clang が間違った ScopeRecord を生成する理由はちょっと推測できません。Thread local というイレギュラーは除外するにしても、パラメーター渡しのために rcxedx をセットする命令は __try の中に含まれていると考えるのが自然なのですが、それらを行う +1048 や +104f の mov すらも例外捕捉の範囲外となっています。

MSVC の場合

全く同じコードを MSVC でコンパイルした場合、いずれの問題も発生しません。まずは全体のアセンブリ。

0:000> uf t!TestThreadLocalStorageHead
t!TestThreadLocalStorageHead [D:\src\msvc-nmake-template\src\tls.cpp @ 22]:
   22 00007ff6`aa5fdd60 4883ec28        sub     rsp,28h
   23 00007ff6`aa5fdd64 65488b042530000000 mov   rax,qword ptr gs:[30h]
   23 00007ff6`aa5fdd6d 4c8b4058        mov     r8,qword ptr [rax+58h]
   23 00007ff6`aa5fdd71 4c89442430      mov     qword ptr [rsp+30h],r8
   23 00007ff6`aa5fdd76 48c7405800000000 mov     qword ptr [rax+58h],0
   24 00007ff6`aa5fdd7e 4532db          xor     r11b,r11b
   26 00007ff6`aa5fdd81 448b0d04430700  mov     r9d,dword ptr [t!_tls_index (00007ff6`aa67208c)]
   26 00007ff6`aa5fdd88 65488b0c2558000000 mov   rcx,qword ptr gs:[58h]
   26 00007ff6`aa5fdd91 41ba04010000    mov     r10d,104h
   26 00007ff6`aa5fdd97 418bc2          mov     eax,r10d
   26 00007ff6`aa5fdd9a 4a0304c9        add     rax,qword ptr [rcx+r9*8]
   26 00007ff6`aa5fdd9e c700d5ffffff    mov     dword ptr [rax],0FFFFFFD5h
   27 00007ff6`aa5fdda4 eb15            jmp     t!TestThreadLocalStorageHead+0x5b (00007ff6`aa5fddbb)

t!TestThreadLocalStorageHead+0x5b [D:\src\msvc-nmake-template\src\tls.cpp @ 33]:
   33 00007ff6`aa5fddbb 65488b042530000000 mov   rax,qword ptr gs:[30h]
   33 00007ff6`aa5fddc4 4c894058        mov     qword ptr [rax+58h],r8
   34 00007ff6`aa5fddc8 418bd1          mov     edx,r9d
   34 00007ff6`aa5fddcb 65488b042558000000 mov   rax,qword ptr gs:[58h]
   34 00007ff6`aa5fddd4 418bca          mov     ecx,r10d
   34 00007ff6`aa5fddd7 488b04d0        mov     rax,qword ptr [rax+rdx*8]
   34 00007ff6`aa5fdddb c704012a000000  mov     dword ptr [rcx+rax],2Ah
   36 00007ff6`aa5fdde2 488d1577f40500  lea     rdx,[t!`string' (00007ff6`aa65d260)]
   36 00007ff6`aa5fdde9 4584db          test    r11b,r11b
   36 00007ff6`aa5fddec 7513            jne     t!TestThreadLocalStorageHead+0xa1 (00007ff6`aa5fde01)

t!TestThreadLocalStorageHead+0x8e [D:\src\msvc-nmake-template\src\tls.cpp @ 37]:
   37 00007ff6`aa5fddee 488d0d93f40500  lea     rcx,[t!`string' (00007ff6`aa65d288)]
   37 00007ff6`aa5fddf5 e8b947ffff      call    t!ILT+5550(_vfprintf_l) (00007ff6`aa5f25b3)
   38 00007ff6`aa5fddfa 32c0            xor     al,al
   47 00007ff6`aa5fddfc 4883c428        add     rsp,28h
   47 00007ff6`aa5fde00 c3              ret

t!TestThreadLocalStorageHead+0xa1 [D:\src\msvc-nmake-template\src\tls.cpp @ 44]:
   44 00007ff6`aa5fde01 488d0da8f40500  lea     rcx,[t!`string' (00007ff6`aa65d2b0)]
   44 00007ff6`aa5fde08 e8a647ffff      call    t!ILT+5550(_vfprintf_l) (00007ff6`aa5f25b3)
   45 00007ff6`aa5fde0d b901000000      mov     ecx,1
   45 00007ff6`aa5fde12 e8b533ffff      call    t!ILT+455(__acrt_iob_func) (00007ff6`aa5f11cc)
   45 00007ff6`aa5fde17 488bc8          mov     rcx,rax
   45 00007ff6`aa5fde1a e85158ffff      call    t!ILT+9835(fflush) (00007ff6`aa5f3670)
   46 00007ff6`aa5fde1f b001            mov     al,1
   47 00007ff6`aa5fde21 4883c428        add     rsp,28h
   47 00007ff6`aa5fde25 c3              ret

ここで重要なのは +dd88 の mov が +dd76 の mov より後に実行されることです。MSVC の場合、Memory Ordering の問題は SwapThreadLocalStoragePointer がインライン展開されたとしても発生しないことが分かります。

次に ScopeRecord の確認。

0:000> g
(9d0.1984): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
t!TestThreadLocalStorageHead+0x3a:
00007ff6`aa5fdd9a 4a0304c9        add     rax,qword ptr [rcx+r9*8] ds:00000000`00000000=????????????????
0:000> !exchain
5 stack frames, scanning for handlers...
Frame 0x00: t!TestThreadLocalStorageHead+0x3a (00007ff6`aa5fdd9a)
  ehandler t!ILT+7905(__C_specific_handler) (00007ff6`aa5f2ee6)
Frame 0x02: t!__scrt_common_main_seh+0x10c (00007ff6`aa5f98b4)
  ehandler t!ILT+7905(__C_specific_handler) (00007ff6`aa5f2ee6)
Frame 0x04: ntdll!RtlUserThreadStart+0x21 (00007ff9`78d8ced1)
  ehandler ntdll!_C_specific_handler (00007ff9`78dac640)
0:000> g (00007ff6`aa5f2ee6)
t!ILT+7905(__C_specific_handler):
00007ff6`aa5f2ee6 e999780000      jmp     t!__C_specific_handler (00007ff6`aa5fa784)
0:000> p
t!__C_specific_handler:
00007ff6`aa5fa784 48895c2408      mov     qword ptr [rsp+8],rbx ss:0000006d`33f9eb00={t!__dyn_tls_dtor_callback <PERF> (t+0x8554c) (00007ff6`aa67554c)}
0:000> dv
   ExceptionRecord = 0x0000006d`33f9f730
  EstablisherFrame = 0x0000006d`33f9f9e0
     ContextRecord = 0x0000006d`33f9f240
 DispatcherContext = 0x0000006d`33f9f0c0
         ImageBase = <value unavailable>
   ExceptionFilter = <value unavailable>
             Index = <value unavailable>
       TargetIndex = <value unavailable>
          TargetPc = <value unavailable>
TerminationHandler = <value unavailable>
        ScopeTable = <value unavailable>
           Handler = <value unavailable>
 ExceptionPointers = struct _EXCEPTION_POINTERS
             Value = <value unavailable>
         ControlPc = <value unavailable>
0:000> dt DispatcherContext HandlerData
Local var @ r9 Type _DISPATCHER_CONTEXT*
   +0x038 HandlerData : 0x00007ff6`aa66a3c8 Void
0:000> dt t!SCOPE_TABLE_AMD64
   +0x000 Count            : Uint4B
   +0x004 ScopeRecord      : [1] _SCOPE_TABLE_AMD64::<unnamed-type-ScopeRecord>
0:000> dt t!SCOPE_TABLE_AMD64 0x00007ff6`aa66a3c8
   +0x000 Count            : 1
   +0x004 ScopeRecord      : [1] _SCOPE_TABLE_AMD64::<unnamed-type-ScopeRecord>
0:000> dt t!SCOPE_TABLE_AMD64 0x00007ff6`aa66a3c8 ScopeRecord[0].
   +0x004 ScopeRecord     : [0]
      +0x000 BeginAddress    : 0xdd81
      +0x004 EndAddress      : 0xdda6
      +0x008 HandlerAddress  : 0x6a010
      +0x00c JumpTarget      : 0xdda6

範囲は [+dd81, +dda6) で、この範囲には例外発生場所もしっかり含まれています。

   26 00007ff6`aa5fdd81 448b0d04430700  mov     r9d,dword ptr [t!_tls_index (00007ff6`aa67208c)]
   26 00007ff6`aa5fdd88 65488b0c2558000000 mov   rcx,qword ptr gs:[58h]
   26 00007ff6`aa5fdd91 41ba04010000    mov     r10d,104h
   26 00007ff6`aa5fdd97 418bc2          mov     eax,r10d
   26 00007ff6`aa5fdd9a 4a0304c9        add     rax,qword ptr [rcx+r9*8] <<<< Exception!
   26 00007ff6`aa5fdd9e c700d5ffffff    mov     dword ptr [rax],0FFFFFFD5h
   27 00007ff6`aa5fdda4 eb15            jmp     t!TestThreadLocalStorageHead+0x5b (00007ff6`aa5fddbb)

Clang でグローバル変数の場合

Thread local というイレギュラーを除外した場合、Clang が生成する Scope Record がどうなるのか気になるところです。次のコードで試します。SwapThreadLocalStoragePointernoinlinesTlsDatathread_local を削除しました。

#include <algorithm>
#include <stdio.h>
#include <windows.h>
#include <winnt.h>
#include <winternl.h>

template <typename T>
struct SimpleHolder {
  T val_ = {};
  void set(const T val) { val_ = val; }
  operator const T&() const { return val_; }
};

PVOID SwapThreadLocalStoragePointer(PVOID newValue) {
  std::swap(::NtCurrentTeb()->Reserved1[11], newValue);
  return newValue;
}

const uint32_t kTlsDataValue = 42;
static SimpleHolder<uint32_t> sTlsData;

__declspec(noinline) bool TestThreadLocalStorageHead() {
  auto origTlsHead = SwapThreadLocalStoragePointer(nullptr);
  bool isExceptionThrown = false;
  __try {
    sTlsData.set(~kTlsDataValue);
  }
  __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION
                     ? EXCEPTION_EXECUTE_HANDLER
                     : EXCEPTION_CONTINUE_SEARCH) {
    isExceptionThrown = true;
  }
  SwapThreadLocalStoragePointer(origTlsHead);
  sTlsData.set(kTlsDataValue);

  if (!isExceptionThrown) {
    printf("[%s] No exception from setter!\n", __FUNCTION__);
    return false;
  }
  if (sTlsData != kTlsDataValue) {
    printf("[%s] TLS is broken!\n", __FUNCTION__);
    return false;
  }
  printf("[%s] Passed!\n", __FUNCTION__);
  fflush(stdout);
  return true;
}

int main(int argc, char* argv[]) {
  TestThreadLocalStorageHead();
  return 0;
}

TLS は無関係になるので、このプログラムを実行しても例外は発生しません。DispatcherContextHandlerData は対象イメージの .rdata セクションにハードコードされているので、それを目視で見つけることにします。

0:000> !dh t

(...snip...)

SECTION HEADER #2
  .rdata name
     CD4 virtual size
    3000 virtual address
     E00 size of raw data
    1800 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         (no align specified)
         Read Only

(...snip...)

0:000> dd t+3000 t+3cd4

(...snip...)

00007ff6`f3573ad0  7472632d 6165682d 316c2d70 302d312d
00007ff6`f3573ae0  6c6c642e 00000000 00000000 00000000
00007ff6`f3573af0  00000000 00000000 00000000 00000000
00007ff6`f3573b00  00000000 00000000 25040b19 4206030b
00007ff6`f3573b10  50016002 00002040 00000001 00001041
00007ff6`f3573b20  00001052 000010d0 00001084 00010401
00007ff6`f3573b30  00004204 00010401 00004204 00020601
00007ff6`f3573b40  30023206 00040a01 0006340a 7006320a
00007ff6`f3573b50  00020601 30023206 00010401 00004204

(...snip...)

0:000> dt vcruntime140!SCOPE_TABLE_AMD64 00007ff6`f3573b18
   +0x000 Count            : 1
   +0x004 ScopeRecord      : [1] _SCOPE_TABLE_AMD64::<unnamed-type-ScopeRecord>
0:000> dt vcruntime140!SCOPE_TABLE_AMD64 00007ff6`f3573b18 ScopeRecord[0].
   +0x004 ScopeRecord     : [0]
      +0x000 BeginAddress    : 0x1041
      +0x004 EndAddress      : 0x1052
      +0x008 HandlerAddress  : 0x10d0
      +0x00c JumpTarget      : 0x1084

0:000>  u t!TestThreadLocalStorageHead t+1052
t!TestThreadLocalStorageHead:
00007ff6`f3571020 55              push    rbp
00007ff6`f3571021 56              push    rsi
00007ff6`f3571022 4883ec28        sub     rsp,28h
00007ff6`f3571026 488d6c2420      lea     rbp,[rsp+20h]
00007ff6`f357102b 65488b042530000000 mov   rax,qword ptr gs:[30h]
00007ff6`f3571034 488b7058        mov     rsi,qword ptr [rax+58h]
00007ff6`f3571038 48c7405800000000 mov     qword ptr [rax+58h],0
00007ff6`f3571040 488d0ded2f0000  lea     rcx,[t!__scrt_ucrt_dll_is_in_use+0x4 (00007ff6`f3574034)]
00007ff6`f3571047 bad5ffffff      mov     edx,0FFFFFFD5h
00007ff6`f357104c e89f000000      call    t!SimpleHolder<unsigned int>::set (00007ff6`f35710f0)
00007ff6`f3571051 65488b042530000000 mov   rax,qword ptr gs:[30h]

範囲は [+1041, +1052) でした。面白いことに、グローバル変数の場合はパラメーター渡しである +1047 の mov は例外捕捉範囲に含まれています。しかし依然として、sTlsData を設定する +1040 の mov は範囲外です。疑問の残る結果です。

[2019/11/30 追記]

さすがに .rdata セクションを目視する方法は汎用性、及び実用性に欠けるので、x64 における SEH の構造をダンプするエクステンションを on.dll の !ex コマンドとして追加しました。

上記 C++ コードをデバッガーで実行してから、例外が捕捉され得るアドレスを取得します。この場合は __try で囲まれた sTlsData.set(~kTlsDataValue); です。

0:000> uf t!TestThreadLocalStorageHead
t!TestThreadLocalStorageHead:
00007ff6`59f01020 55              push    rbp
00007ff6`59f01021 56              push    rsi
00007ff6`59f01022 4883ec28        sub     rsp,28h
00007ff6`59f01026 488d6c2420      lea     rbp,[rsp+20h]
00007ff6`59f0102b 65488b042530000000 mov   rax,qword ptr gs:[30h]
00007ff6`59f01034 488b7058        mov     rsi,qword ptr [rax+58h]
00007ff6`59f01038 48c7405800000000 mov     qword ptr [rax+58h],0
00007ff6`59f01040 488d0ded2f0000  lea     rcx,[t!sTlsData (00007ff6`59f04034)]
00007ff6`59f01047 bad5ffffff      mov     edx,0FFFFFFD5h
00007ff6`59f0104c e89f000000      call    t!SimpleHolder<unsigned int>::set (00007ff6`59f010f0)
00007ff6`59f01051 65488b042530000000 mov   rax,qword ptr gs:[30h]
00007ff6`59f0105a 48897058        mov     qword ptr [rax+58h],rsi
00007ff6`59f0105e c705cc2f00002a000000 mov dword ptr [t!sTlsData (00007ff6`59f04034)],2Ah
00007ff6`59f01068 488d0dac1f0000  lea     rcx,[t!`string' (00007ff6`59f0301b)]
00007ff6`59f0106f 488d158a1f0000  lea     rdx,[t!`string' (00007ff6`59f03000)]
00007ff6`59f01076 e885000000      call    t!printf (00007ff6`59f01100)
00007ff6`59f0107b 31c0            xor     eax,eax
00007ff6`59f0107d 4883c428        add     rsp,28h
00007ff6`59f01081 5e              pop     rsi
00007ff6`59f01082 5d              pop     rbp
00007ff6`59f01083 c3              ret
0:000> .load on
0:000> !ex t 00007ff6`59f0104c
@00007ff6`59f05000
UNWIND_INFO[0] 00007ff6`59f03b00 [ 00007ff6`59f01020 00007ff6`59f010c4 )
  Version       = 1
  Flags         = 3
  SizeOfProlog  = 11
  FrameRegister = 5
  FrameOffset   = 2
  UnwindCode[0] = {CodeOffset:11 UnwindOp:3 OpInfo:0}
  UnwindCode[1] = {CodeOffset:6 UnwindOp:2 OpInfo:4}
  UnwindCode[2] = {CodeOffset:2 UnwindOp:0 OpInfo:6}
  UnwindCode[3] = {CodeOffset:1 UnwindOp:0 OpInfo:5}
  ExceptionHandler = 00007ff6`59f02040 t!_C_specific_handler
  HandlerData = 00007ff6`59f03b10
  ScopeRecord[0] 00007ff6`59f03b14 = {
    [ 00007ff6`59f01041 00007ff6`59f01052 )
    Filter:  00007ff6`59f010d0 t!TestThreadLocalStorageHead+0xb0
    Handler: 00007ff6`59f01084 t!TestThreadLocalStorageHead+0x64
  }

上記出力から、ScopeRecord の範囲は [ 00007ff6`59f01041 00007ff6`59f01052 ) となっており、この範囲にはパラメーターをセットする +1047 の mov は含まれていますが this ポインターをセットする +1040 の lea が含まれていません。やはり疑問の残る結果です。

おわりに

というわけで、Windows の SEH と thread local 変数にまつわる Clang の不思議な動作についてまとめました。もう少し情報を集めて本当に怪しかったら LLVM にバグ登録しようかと思います。

LLVM にバグ登録しました。

44174 – A range of ScopeRecord does not match the code enclosed by __try
https://bugs.llvm.org/show_bug.cgi?id=44174

本記事で引用したコードは コンパイル パラメーターなども含めて NMAKE プロジェクトとしてここに置きました。

https://github.com/msmania/seh-debug/tree/191124-blogpost