Debugging Windows SEH
はじめに
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 が分かったので、アセンブリを確認します。BeginAddress
と EndAddress
の 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 というイレギュラーは除外するにしても、パラメーター渡しのために rcx
や edx
をセットする命令は __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 がどうなるのか気になるところです。次のコードで試します。SwapThreadLocalStoragePointer
の noinline
と sTlsData
の thread_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 は無関係になるので、このプログラムを実行しても例外は発生しません。DispatcherContext
の HandlerData
は対象イメージの .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 プロジェクトとしてここに置きました。