[Win32] [Asm] Race condition between bit fields
久々にプログラミング ネタを投稿します。
サーバー系のソフトウェアが何らかのトラブルを引き起こした際、その原因がスレッド間の処理の競合で、現象の再現に苦労することがよくあります。race condition や timing issue と呼ばれる現象には非常に苦労させられます。最近の仕事で race condition に遭遇したのですが、ユーザーモードの単純なプログラムを書いて同じ現象を観察することができたので、まとめてみます。
まず、プログラムを書きます。ファイル 1 つです。
//
// bitfield.cpp
//
#include <windows.h>
#include <stdio.h>
#define NTDLL_DLL L"ntdll.dll"
#define BETWEEN(n, a, b) ((n)>=(a) && (n)<=(b))
#define LOGERROR(fmt, ...) wprintf(fmt, __VA_ARGS__)
#define LOGINFO LOGERROR
#define LOGDEBUG
typedef ULONG (WINAPI *RTLRANDOM)(
_Inout_ PULONG Seed
);
HMODULE NtDllDll = NULL;
RTLRANDOM RtlRandom = NULL;
BOOL g_FlipFlop = TRUE;
BOOL g_Field1 = TRUE;
BOOL g_Field2 = TRUE;
struct TARGET {
INT Int1;
WORD Field1:1;
WORD Field2:1;
INT Int2;
} g_Target;
typedef struct _CONTEXT_CHURN {
ULONG DynamicSeed;
INT Iteration;
INT ThreadIndex;
INT LoopCounter;
} CONTEXT_CHURN, *PCONTEXT_CHURN;
DWORD WINAPI ChurningThread(
_In_ LPVOID lpParameter
) {
PCONTEXT_CHURN Context = (PCONTEXT_CHURN)lpParameter;
for ( int i=0 ; i<Context->Iteration ; ++i, ++Context->LoopCounter ) {
switch (Context->ThreadIndex)
{
case 0:
g_FlipFlop = !g_FlipFlop;
break;
case 1:
if ( g_Target.Field1!=g_Field1 ) {
LOGINFO(L"[ChurningThread.%04x] %d: g_Target.Field1=%d g_Field1=%d \n",
GetCurrentThreadId(),
Context->LoopCounter,
g_Target.Field1, g_Field1);
}
g_Target.Field1 = g_FlipFlop;
g_Field1 = g_Target.Field1;
break;
case 2:
if ( g_Target.Field2!=g_Field2) {
LOGINFO(L"[ChurningThread.%04x] %d: g_Target.Field2=%d g_Field2=%d \n",
GetCurrentThreadId(),
Context->LoopCounter,
g_Target.Field2, g_Field2);
}
g_Target.Field2= g_FlipFlop;
g_Field2 = g_Target.Field2;
break;
}
int RandomWait = int(double(RtlRandom(&Context->DynamicSeed))/MAXLONG*10);
Sleep(RandomWait);
}
return 0;
}
void Test_Bitfield(int NumOfThreads, int Iteration) {
HANDLE *ThreadPool = new HANDLE[NumOfThreads];
CONTEXT_CHURN *ThreadContexts = new CONTEXT_CHURN[NumOfThreads];
if ( ThreadPool && ThreadContexts ) {
ULONG DynamicSeed = GetTickCount();
for ( int i=0 ; i<NumOfThreads ; ++i ) {
ThreadContexts[i].DynamicSeed = RtlRandom(&DynamicSeed);
ThreadContexts[i].Iteration = Iteration;
ThreadContexts[i].ThreadIndex= i;
ThreadContexts[i].LoopCounter= 0;
ThreadPool[i] = CreateThread(NULL, 0, ChurningThread, &ThreadContexts[i], CREATE_SUSPENDED, NULL);
}
g_Target.Field1 = g_Field1;
g_Target.Field2 = g_Field2;
for ( int i=0 ; i<NumOfThreads ; ++i ) {
ResumeThread(ThreadPool[i]);
}
WaitForMultipleObjects(NumOfThreads, ThreadPool, TRUE, INFINITE);
for ( int i=0 ; i<NumOfThreads ; ++i ) {
CloseHandle(ThreadPool[i]);
}
delete [] ThreadPool;
delete [] ThreadContexts;
}
}
int wmain(int argc, wchar_t *argv[]) {
NtDllDll = LoadLibrary(NTDLL_DLL);
RtlRandom = (RTLRANDOM)GetProcAddress(NtDllDll, "RtlRandom");
if ( argc>=3 ) {
Test_Bitfield(_wtoi(argv[1]), _wtoi(argv[2]));
}
return 0;
}
簡単なマルチスレッド プログラムです。ChurningThread がメイン スレッドから実行されるワーカー スレッドで、ThreadIndex の値によって以下 3 種類の振る舞いを行なうことができます。
- グローバル変数 g_FlipFlop をひたすら反転
- g_Target.Field1 と g_Field1 に g_FlipFlop を代入
- g_Target.Field2 と g_Field2 に g_FlipFlop を代入
これを x64 ネイティブのプログラムにして実行します。手元の環境で 4 回ほど実行した結果を貼ります。
- OS: Windows 8.1 x64
- IDE: Visual Studio 2012
- CPU: Intel Core i5-2520M (2 Cores, 4 Logical Processors)
- Build: x64 Release
D:\VSDev\Projects\win32c>x64\Release\win32c 3 1000
[ChurningThread.29d4] 98: g_Target.Field2=0 g_Field2=1
[ChurningThread.29d4] 845: g_Target.Field2=0 g_Field2=1
D:\VSDev\Projects\win32c>x64\Release\win32c 3 1000
[ChurningThread.2ab0] 543: g_Target.Field2=0 g_Field2=1
[ChurningThread.2ab0] 581: g_Target.Field2=0 g_Field2=1
[ChurningThread.0154] 933: g_Target.Field1=0 g_Field1=1
D:\VSDev\Projects\win32c>x64\Release\win32c 3 1000
D:\VSDev\Projects\win32c>x64\Release\win32c 3 1000
[ChurningThread.0460] 83: g_Target.Field1=0 g_Field1=1
[ChurningThread.2088] 714: g_Target.Field2=1 g_Field2=0
[ChurningThread.0460] 803: g_Target.Field1=0 g_Field1=1
C 言語的にはこれはおかしな結果です。ChurningThread の処理を見る限りでは、g_Target.Field1 と g_Field1、g_Target.Field2 と g_Field2 の値が異なることはあり得ないはずです。というのも、ログを表示した後の処理で、どちらの値にも g_FlipFlop が代入されるはずだからです。ThreadIndex の値は途中で変更されないので、各スレッドは同じ処理をひたすら繰り返しているだけです。なぜ値が異なるのでしょう。g_FlipFlop の値も同時に反転し続けています。しかし、各スレッドは g_FlipFlop の値は一回読み取るだけで、g_Field1 には g_Target.Field1 の値を代入しているので、仮にこの 2 行の間に g_FlipFlop が反転したとしても、影響はないはずです。
この記事のタイトルや関数名からして、ビット フィールドが怪しいとお気づきになる方は多いと思います。正解です。TARGET::Field1 か TARGET::Field2 の片方を独立したメンバーにするだけでこの現象は解消します。では、なぜこのようなことが起こっていたのかをアセンブラで見てみます。
ChurningThread 関数全体のアセンブラは以下の通りです。ビットフィールドに関連するところは、g_Target.Field1 と g_Target.Field2 に値を代入しているところなので、該当箇所を青字にしました。
win32c!ChurningThread [d:\vsdev\projects\win32c\src\main.cpp @ 42]:
42 00007ff6`d0471000 4889742410 mov qword ptr [rsp+10h],rsi
42 00007ff6`d0471005 57 push rdi
42 00007ff6`d0471006 4883ec50 sub rsp,50h
45 00007ff6`d047100a 33f6 xor esi,esi
45 00007ff6`d047100c 488bf9 mov rdi,rcx
45 00007ff6`d047100f 397104 cmp dword ptr [rcx+4],esi
45 00007ff6`d0471012 0f8e67010000 jle win32c!ChurningThread+0x17f (00007ff6`d047117f)
win32c!ChurningThread+0x18 [d:\vsdev\projects\win32c\src\main.cpp @ 45]:
45 00007ff6`d0471018 0f29742440 movaps xmmword ptr [rsp+40h],xmm6
45 00007ff6`d047101d f20f103513130000 movsd xmm6,mmword ptr [win32c!_real (00007ff6`d0472338)]
45 00007ff6`d0471025 0f297c2430 movaps xmmword ptr [rsp+30h],xmm7
45 00007ff6`d047102a f20f103dfe120000 movsd xmm7,mmword ptr [win32c!_real (00007ff6`d0472330)]
45 00007ff6`d0471032 48895c2460 mov qword ptr [rsp+60h],rbx
45 00007ff6`d0471037 660f1f840000000000 nop word ptr [rax+rax]
win32c!ChurningThread+0x40 [d:\vsdev\projects\win32c\src\main.cpp @ 46]:
46 00007ff6`d0471040 8b4f08 mov ecx,dword ptr [rdi+8]
46 00007ff6`d0471043 85c9 test ecx,ecx
46 00007ff6`d0471045 0f84e1000000 je win32c!ChurningThread+0x12c (00007ff6`d047112c)
win32c!ChurningThread+0x4b [d:\vsdev\projects\win32c\src\main.cpp @ 46]:
46 00007ff6`d047104b ffc9 dec ecx
46 00007ff6`d047104d 7476 je win32c!ChurningThread+0xc5 (00007ff6`d04710c5)
win32c!ChurningThread+0x4f [d:\vsdev\projects\win32c\src\main.cpp @ 46]:
46 00007ff6`d047104f ffc9 dec ecx
46 00007ff6`d0471051 0f85e6000000 jne win32c!ChurningThread+0x13d (00007ff6`d047113d)
win32c!ChurningThread+0x57 [d:\vsdev\projects\win32c\src\main.cpp @ 62]:
62 00007ff6`d0471057 8b1d8f250000 mov ebx,dword ptr [win32c!g_Target+0x4 (00007ff6`d04735ec)]
62 00007ff6`d047105d d1eb shr ebx,1
62 00007ff6`d047105f 83e301 and ebx,1
62 00007ff6`d0471062 3b1db41f0000 cmp ebx,dword ptr [win32c!g_Field2 (00007ff6`d047301c)]
62 00007ff6`d0471068 7426 je win32c!ChurningThread+0x90 (00007ff6`d0471090)
win32c!ChurningThread+0x6a [d:\vsdev\projects\win32c\src\main.cpp @ 66]:
66 00007ff6`d047106a ff15b80f0000 call qword ptr [win32c!_imp_GetCurrentThreadId (00007ff6`d0472028)]
66 00007ff6`d0471070 448b470c mov r8d,dword ptr [rdi+0Ch]
66 00007ff6`d0471074 488d0d15120000 lea rcx,[win32c!`string' (00007ff6`d0472290)]
66 00007ff6`d047107b 8bd0 mov edx,eax
66 00007ff6`d047107d 8b05991f0000 mov eax,dword ptr [win32c!g_Field2 (00007ff6`d047301c)]
66 00007ff6`d0471083 448bcb mov r9d,ebx
66 00007ff6`d0471086 89442420 mov dword ptr [rsp+20h],eax
66 00007ff6`d047108a ff15d0100000 call qword ptr [win32c!_imp_wprintf (00007ff6`d0472160)]
win32c!ChurningThread+0x90 [d:\vsdev\projects\win32c\src\main.cpp @ 68]:
68 00007ff6`d0471090 0fb70555250000 movzx eax,word ptr [win32c!g_Target+0x4 (00007ff6`d04735ec)]
68 00007ff6`d0471097 0fb70d861f0000 movzx ecx,word ptr [win32c!g_FlipFlop (00007ff6`d0473024)]
68 00007ff6`d047109e 6603c9 add cx,cx
68 00007ff6`d04710a1 6633c8 xor cx,ax
68 00007ff6`d04710a4 6683e102 and cx,2
68 00007ff6`d04710a8 6633c1 xor ax,cx
68 00007ff6`d04710ab 6689053a250000 mov word ptr [win32c!g_Target+0x4 (00007ff6`d04735ec)],ax
69 00007ff6`d04710b2 8b0534250000 mov eax,dword ptr [win32c!g_Target+0x4 (00007ff6`d04735ec)]
69 00007ff6`d04710b8 d1e8 shr eax,1
69 00007ff6`d04710ba 83e001 and eax,1
69 00007ff6`d04710bd 8905591f0000 mov dword ptr [win32c!g_Field2 (00007ff6`d047301c)],eax
70 00007ff6`d04710c3 eb78 jmp win32c!ChurningThread+0x13d (00007ff6`d047113d)
win32c!ChurningThread+0xc5 [d:\vsdev\projects\win32c\src\main.cpp @ 52]:
52 00007ff6`d04710c5 8b1d21250000 mov ebx,dword ptr [win32c!g_Target+0x4 (00007ff6`d04735ec)]
52 00007ff6`d04710cb 83e301 and ebx,1
52 00007ff6`d04710ce 3b1d4c1f0000 cmp ebx,dword ptr [win32c!g_Field1 (00007ff6`d0473020)]
52 00007ff6`d04710d4 7426 je win32c!ChurningThread+0xfc (00007ff6`d04710fc)
win32c!ChurningThread+0xd6 [d:\vsdev\projects\win32c\src\main.cpp @ 56]:
56 00007ff6`d04710d6 ff154c0f0000 call qword ptr [win32c!_imp_GetCurrentThreadId (00007ff6`d0472028)]
56 00007ff6`d04710dc 448b470c mov r8d,dword ptr [rdi+0Ch]
56 00007ff6`d04710e0 488d0d29110000 lea rcx,[win32c!`string' (00007ff6`d0472210)]
56 00007ff6`d04710e7 8bd0 mov edx,eax
56 00007ff6`d04710e9 8b05311f0000 mov eax,dword ptr [win32c!g_Field1 (00007ff6`d0473020)]
56 00007ff6`d04710ef 448bcb mov r9d,ebx
56 00007ff6`d04710f2 89442420 mov dword ptr [rsp+20h],eax
56 00007ff6`d04710f6 ff1564100000 call qword ptr [win32c!_imp_wprintf (00007ff6`d0472160)]
win32c!ChurningThread+0xfc [d:\vsdev\projects\win32c\src\main.cpp @ 58]:
58 00007ff6`d04710fc 0fb705e9240000 movzx eax,word ptr [win32c!g_Target+0x4 (00007ff6`d04735ec)]
58 00007ff6`d0471103 0fb70d1a1f0000 movzx ecx,word ptr [win32c!g_FlipFlop (00007ff6`d0473024)]
58 00007ff6`d047110a 6633c8 xor cx,ax
58 00007ff6`d047110d 6683e101 and cx,1
58 00007ff6`d0471111 6633c1 xor ax,cx
58 00007ff6`d0471114 668905d1240000 mov word ptr [win32c!g_Target+0x4 (00007ff6`d04735ec)],ax
59 00007ff6`d047111b 8b05cb240000 mov eax,dword ptr [win32c!g_Target+0x4 (00007ff6`d04735ec)]
59 00007ff6`d0471121 83e001 and eax,1
59 00007ff6`d0471124 8905f61e0000 mov dword ptr [win32c!g_Field1 (00007ff6`d0473020)],eax
60 00007ff6`d047112a eb11 jmp win32c!ChurningThread+0x13d (00007ff6`d047113d)
win32c!ChurningThread+0x12c [d:\vsdev\projects\win32c\src\main.cpp @ 49]:
49 00007ff6`d047112c 33c0 xor eax,eax
49 00007ff6`d047112e 3905f01e0000 cmp dword ptr [win32c!g_FlipFlop (00007ff6`d0473024)],eax
49 00007ff6`d0471134 0f94c0 sete al
49 00007ff6`d0471137 8905e71e0000 mov dword ptr [win32c!g_FlipFlop (00007ff6`d0473024)],eax
win32c!ChurningThread+0x13d [d:\vsdev\projects\win32c\src\main.cpp @ 73]:
73 00007ff6`d047113d 488bcf mov rcx,rdi
73 00007ff6`d0471140 ff15b2240000 call qword ptr [win32c!RtlRandom (00007ff6`d04735f8)]
73 00007ff6`d0471146 0f57c0 xorps xmm0,xmm0
73 00007ff6`d0471149 8bc0 mov eax,eax
73 00007ff6`d047114b f2480f2ac0 cvtsi2sd xmm0,rax
73 00007ff6`d0471150 f20f5ec6 divsd xmm0,xmm6
73 00007ff6`d0471154 f20f59c7 mulsd xmm0,xmm7
73 00007ff6`d0471158 f20f2cc8 cvttsd2si ecx,xmm0
74 00007ff6`d047115c ff15ae0e0000 call qword ptr [win32c!_imp_Sleep (00007ff6`d0472010)]
74 00007ff6`d0471162 ff470c inc dword ptr [rdi+0Ch]
74 00007ff6`d0471165 ffc6 inc esi
74 00007ff6`d0471167 3b7704 cmp esi,dword ptr [rdi+4]
74 00007ff6`d047116a 0f8cd0feffff jl win32c!ChurningThread+0x40 (00007ff6`d0471040)
win32c!ChurningThread+0x170 [d:\vsdev\projects\win32c\src\main.cpp @ 74]:
74 00007ff6`d0471170 0f287c2430 movaps xmm7,xmmword ptr [rsp+30h]
74 00007ff6`d0471175 0f28742440 movaps xmm6,xmmword ptr [rsp+40h]
74 00007ff6`d047117a 488b5c2460 mov rbx,qword ptr [rsp+60h]
win32c!ChurningThread+0x17f [d:\vsdev\projects\win32c\src\main.cpp @ 77]:
77 00007ff6`d047117f 33c0 xor eax,eax
78 00007ff6`d0471181 488b742468 mov rsi,qword ptr [rsp+68h]
78 00007ff6`d0471186 4883c450 add rsp,50h
78 00007ff6`d047118a 5f pop rdi
78 00007ff6`d047118b c3 ret
よくある現象ですが、case ブロックの順番が逆転していますね。switch 文のコンパイル結果は気持ち悪いものです。それはさておき、C 言語では 1 行の処理である “g_Target.Field2 = g_FlipFlop” が、アセンブラだと 7 命令になっています。ビットフィールドの使用はパフォーマンス悪化の原因にもなりそうですね。
00007ff6`d0471090 0fb70555250000 movzx eax,word ptr [win32c!g_Target+0x4 (00007ff6`d04735ec)]
00007ff6`d0471097 0fb70d861f0000 movzx ecx,word ptr [win32c!g_FlipFlop (00007ff6`d0473024)]
00007ff6`d047109e 6603c9 add cx,cx
00007ff6`d04710a1 6633c8 xor cx,ax
00007ff6`d04710a4 6683e102 and cx,2
00007ff6`d04710a8 6633c1 xor ax,cx
00007ff6`d04710ab 6689053a250000 mov word ptr [win32c!g_Target+0x4 (00007ff6`d04735ec)],ax
特定のビットへの代入 (A=B) は、XOR を二回実行して
A = A^(B^A)
のように行われるようです。add と and は、効果を特定のビットに限定させている部分です。
この処理で問題なのは、赤字で示した xor と mov のところです。演算結果を ax レジスタに入れてから g_Target.Field2 に mov しています。この処理だと、代入したいビット以外のビットもメモリに送られてしまうことになります。00007ff6d0471090 の処理の時は Field1 が 1 だったとして、00007ff6
d04710ab を実行するまでの間に別スレッドが Field1 を 0 に設定していたとしても、こっちのスレッドが Field2 を設定するときには ax レジスタに保存されていた古い Field1 も一緒に mov されるので、予期せずして Field1 が変更されることがありそうです。
これだけだと、コンパイラがお粗末でした、というだけで話が終わってしまいそうですが、実は続きがあります。今度は 32bit の x86 ネイティブでコンパイルして同じ 64bit Windows で実行してみます。
D:\VSDev\Projects\win32c>Release\win32c.exe 3 1000
[ChurningThread.275c] 851: g_Target.Field2=0 g_Field2=1
D:\VSDev\Projects\win32c>Release\win32c.exe 3 1000
[ChurningThread.2034] 40: g_Target.Field1=1 g_Field1=0
[ChurningThread.2034] 894: g_Target.Field1=0 g_Field1=1
D:\VSDev\Projects\win32c>Release\win32c.exe 3 1000
D:\VSDev\Projects\win32c>Release\win32c.exe 3 1000
[ChurningThread.2a28] 23: g_Target.Field2=1 g_Field2=0
[ChurningThread.2bd4] 874: g_Target.Field1=0 g_Field1=1
同じように競合が発生するようです。同じコードをコンパイルしているだけなので当たり前のように見えます。ここでもアセンブラを見てみます。全部貼ると長いので、抜粋します。
g_Target.Field2 = g_FlipFlop;
win32c!ChurningThread+0x36 [d:\vsdev\projects\win32c\src\main.cpp @ 62]:
62 00161036 8b0d84331600 mov ecx,dword ptr [win32c!g_Target+0x4 (00163384)]
62 0016103c 8b1518301600 mov edx,dword ptr [win32c!g_Field2 (00163018)]
62 00161042 8bc1 mov eax,ecx
62 00161044 d1e8 shr eax,1
62 00161046 83e001 and eax,1
62 00161049 3bc2 cmp eax,edx
62 0016104b 741c je win32c!ChurningThread+0x69 (00161069)
win32c!ChurningThread+0x4d [d:\vsdev\projects\win32c\src\main.cpp @ 66]:
66 0016104d 52 push edx
66 0016104e 50 push eax
66 0016104f ff760c push dword ptr [esi+0Ch]
66 00161052 ff1514201600 call dword ptr [win32c!_imp__GetCurrentThreadId (00162014)]
66 00161058 50 push eax
66 00161059 68a8211600 push offset win32c!`string' (001621a8)
66 0016105e ffd3 call ebx
66 00161060 8b0d84331600 mov ecx,dword ptr [win32c!g_Target+0x4 (00163384)]
66 00161066 83c414 add esp,14h
win32c!ChurningThread+0x69 [d:\vsdev\projects\win32c\src\main.cpp @ 68]:
68 00161069 a120301600 mov eax,dword ptr [win32c!g_FlipFlop (00163020)]
68 0016106e 03c0 add eax,eax
68 00161070 33c1 xor eax,ecx
68 00161072 83e002 and eax,2
68 00161075 66310584331600 xor word ptr [win32c!g_Target+0x4 (00163384)],ax
69 0016107c a184331600 mov eax,dword ptr [win32c!g_Target+0x4 (00163384)]
69 00161081 d1e8 shr eax,1
69 00161083 83e001 and eax,1
69 00161086 a318301600 mov dword ptr [win32c!g_Field2 (00163018)],eax
70 0016108b eb61 jmp win32c!ChurningThread+0xee (001610ee)
g_Target.Field1 = g_FlipFlop;
win32c!ChurningThread+0x8d [d:\vsdev\projects\win32c\src\main.cpp @ 52]:
52 0016108d 8b0d84331600 mov ecx,dword ptr [win32c!g_Target+0x4 (00163384)]
52 00161093 8b151c301600 mov edx,dword ptr [win32c!g_Field1 (0016301c)]
52 00161099 8bc1 mov eax,ecx
52 0016109b 83e001 and eax,1
52 0016109e 3bc2 cmp eax,edx
52 001610a0 741c je win32c!ChurningThread+0xbe (001610be)
win32c!ChurningThread+0xa2 [d:\vsdev\projects\win32c\src\main.cpp @ 56]:
56 001610a2 52 push edx
56 001610a3 50 push eax
56 001610a4 ff760c push dword ptr [esi+0Ch]
56 001610a7 ff1514201600 call dword ptr [win32c!_imp__GetCurrentThreadId (00162014)]
56 001610ad 50 push eax
56 001610ae 6830211600 push offset win32c!`string' (00162130)
56 001610b3 ffd3 call ebx
56 001610b5 8b0d84331600 mov ecx,dword ptr [win32c!g_Target+0x4 (00163384)]
56 001610bb 83c414 add esp,14h
win32c!ChurningThread+0xbe [d:\vsdev\projects\win32c\src\main.cpp @ 58]:
58 001610be a120301600 mov eax,dword ptr [win32c!g_FlipFlop (00163020)]
58 001610c3 33c1 xor eax,ecx
58 001610c5 83e001 and eax,1
58 001610c8 66310584331600 xor word ptr [win32c!g_Target+0x4 (00163384)],ax
59 001610cf a184331600 mov eax,dword ptr [win32c!g_Target+0x4 (00163384)]
59 001610d4 83e001 and eax,1
59 001610d7 a31c301600 mov dword ptr [win32c!g_Field1 (0016301c)],eax
60 001610dc eb10 jmp win32c!ChurningThread+0xee (001610ee)
2 回の XOR でビットの代入を行っているところは x64 と同じです。しかし、x64 で問題となっていた win32c!g_Target への mov による代入がありません。代わりに何をやっているかというと、win32c!g_Target に対して直接 XOR を実行しています。つまり、最初に win32c!g_Target の値を ecx に保存してから、最後に XOR でビットをセットするまでの間に g_Target が別スレッドによって変更されたとしても、g_Target に対して変更したい 1 ビットだけを XOR するので、他のビットへの影響はないと考えられます。より具体的に説明すると、xor の第二オペランドである ax レジスタは、その前に実行している and によって、設定対象外のビットが 0 になっているため、XOR しても変化しないはずなのです。理論的には。
実際に実行すると、x64 と同じく競合が発生します。そして、ビットフィールドではなく独立したメンバーにすると、競合は発生しません。まだ確証は持てていませんが、おそらく CPU キャッシュによるものと考えられます。CPU コア毎に存在している L1 キャッシュあたりに win32c!g_Target の値がキャッシュされていて、mov で取ってくるとき、もしくは xor で変更するときに、別コアで実行されているスレッドが変更した値がまだこちら側に来ていない、ということが起こっている可能性があります。さて、どうやって確かめたものか・・・。
シングル コアの CPU 上であれば、64bit プログラムだけで競合が発生し、32bit では競合が発生しないはずです。しかし、普通にプログラムを動かしただけでは、ちょうどいい場所でコンテキスト スイッチが起こらず、64bit プログラムでも競合は発生しません。デバッガーで無理やり競合させられるかどうかを試してみたいところです。
[2013/12/01] 続きを書きました。
[Win32] [Asm] Race condition between bit fields #2 | すなのかたまり
http://msmania.wordpress.com/2013/12/01/win32-asm-race-condition-between-bit-fields-2/