最近在公司做了一次关于windows同步方面的培训,以下是本次培训的PPT文字。
Windows同步原理与调试
richard
2012.10.9
主要内容与讲解方式
..用户态同步技术
..内核态同步技术
..使用windbg调试用户态线程死锁
..讲解不面面俱到,不讲API使用
..讲原理的同时穿插windbg的使用
..不具权威性,仅作探讨
用户态同步技术
..原子操作API
..关键区
..内核同步对象
原子操作API
Windows提供的一组API,能够以原子方式对内存进行修改。
.
InterlockedIncrement
.
InterlockedDecrement
.
InterlockedExchange/InterlockedExchangePointer
.
InterlockedExchangeAdd
.
InterlockedCompareExchange/InterlockedCompareExchangePointer
原子操作API的特点
..不同的硬件体系和操作系统版本, windows提供的原子操作API有所不同
0:001> x kernel32!Interlocked*
7c80982e kernel32!InterlockedExchange =
7c809856 kernel32!InterlockedExchangeAdd =
7c80981a kernel32!InterlockedDecrement =
7c809842 kernel32!InterlockedCompareExchange =
7c809806 kernel32!InterlockedIncrement =
查看系统提供的API
原子操作API的特点
..不阻塞线程,耗时小。
..操作单一内置变量。
..可用于跨进程的内存互斥修改。
不带互斥的变量自增
int a; .
a = 0;
00411AAE mov dword ptr [a],0
++ a;
00411AB5 mov eax,dword ptr [a]
00411AB8 add eax,1
00411ABB mov dword ptr [a],eax
xadd & cmpxchg & lock prefix
xadd r1/m r2, r1/m与r2的值交换并把两个操作数的和放到r1/m
.
cmpxchg r1/m r2,把r1/m的值与eax的值进行比较,如相等则把r2的值放到r1/m并置zf为 1,若不等则把r1/m值放到eax并把zf清0
.
对于写内存的一些指令增加了锁地址总线的功能,这些写内存的指令如常见的 sub,add等指令,通过 Lock prefix来实现这功能,使用 Lock prefix将会使 procesor产生
LOCK#信号锁地址总线
InterlockedIncrement实现
0:011> u kernel32!InterlockedIncrement L20
.
kernel32!InterlockedIncrement:
.
7c809806 8b4c2404 mov ecx,dword ptr [esp+4]
.
7c80980a b801000000 mov eax,1
.
7c80980f f00fc101 lock xadd dword ptr [ecx],eax
.
7c809813 40 inc eax
.
7c809814 c20400 ret 4
InterlockedDecrement实现
kernel32!InterlockedDecrement:
.
7c80981a 8b4c2404 mov ecx,dword ptr [esp+4]
.
7c80981e b8ffffffff mov eax,0FFFFFFFFh
.
7c809823 f00fc101 lock xadd dword ptr [ecx],eax
.
7c809827 48 dec eax
.
7c809828 c20400 ret 4
InterlockedCompareExchange实现
LONG __cdecl InterlockedCompareExchange(
_Inout_ LONG volatile *Destination,
_In_ LONG Exchange,
_In_ LONG Comparand );
.
kernel32!InterlockedCompareExchange:
.
7c809842 8b4c2404 mov ecx,dword ptr [esp+4]
.
7c809846 8b542408 mov edx,dword ptr [esp+8]
.
7c80984a 8b44240c mov eax,dword ptr [esp+0Ch]
.
7c80984e f00fb111 lock cmpxchg dword ptr [ecx],edx
.
7c809852 c20c00 ret 0Ch
InterlockedExchange实现
LONG __cdecl InterlockedExchange(
_Inout_ LONG volatile *Target,
_In_ LONG Value );
7c80982e 8b4c2404 mov ecx,dword ptr [esp+4]
.
7c809832 8b542408 mov edx,dword ptr [esp+8]
.
7c809836 8b01 mov eax,dword ptr [ecx]
.
7c809838 f00fb111 lock cmpxchg dword ptr [ecx],edx
.
7c80983c 75fa jne 7c809838
.
7c80983e c20800 ret 8
原子操作API小结
..利用硬件提供的原子操作能力以及API的精巧设计,windows系统为我们提供了简单强大的单一变量互斥操作接口。
关键区(critical section)
..一种应用层同步对象,开销比较小。
..可以实现大块内存的互斥访问。
..不能跨进程使用。
关键区内部结构
0:006> dt_RTL_CRITICAL_SECTION
.
ntdll!_RTL_CRITICAL_SECTION
.
+0x000 DebugInfo : Ptr32
.
+0x004 LockCount : Int4B
.
+0x008 RecursionCount :Int4B
.
+0x00c OwningThread : Ptr32 Void
.
+0x010 LockSemaphore : Ptr32 Void
.
+0x014 SpinCount :Uint4B
关键区内部结构
DebugInfo,此字段包含一个指针,指向系统分配的伴随结构,该结构的类型为 RTL_CRITICAL_SECTION_DEBUG。这一结构中包含更多极有价值的信息,定义于
WINNT.H中。
.
LockCount,这是临界区中最重要的一个字段。它被初始化为数值 -1;此数值等于或大于 0时,表示此临界区被占用。当其不等于 -1时,OwningThread字段包含了拥有此临界区的线程 ID。此字段与 (RecursionCount -1) 数值之间的差值表示有多少个其他线程在等待获得该临界区。
.
OwningThread,此字段包含当前占用此临界区的线程的线程标识符。此线程 ID与 GetCurrentThreadId之类的 API所返回的 ID相同。
.
LockSemaphore,此字段的命名不恰当,它实际上是一个自复位事件,而不是一个信号。它是一个内核对象句柄,用于通知操作系统:该临界区现在空闲。操作系统在一个线程第一次尝试获得该临界区,但被另一个已经拥有该临界区的线程所阻止时,自动创建这样一个句柄。应当调用 DeleteCriticalSection(它将发出一个调用件的 CloseHandle调用,并在必要时释放该调试结构),否则将会发生资源泄漏。
SpinCount,仅用于多处理器系统。 MSDN.文档对此字段进行如下说明:“在多处理器系统中,如果该临界区不可用,调用线程将在对与该临界区相关的信号执行等待操作前,
旋转 SpinCount次。如果该临界区在旋转操作期间变为可用,该调用线程就避免了等待操作。”旋转计数可以在多处理器计算机上提供更佳性能,其原因在于在一个循环中旋转通常要快于进入内核模式等待状态。此字段默认值为零,但可以用 InitializeCriticalSectionAndSpinCount API将其设置为一个不同值
Spincount初始化
..单cpu下置0,多cpu下设置一个大于0的值,cpu的值通过PEB获得。
.
7c931536 mov eax,dword ptr fs:[00000018h] //teb
.
7c93153c mov eax,dword ptr [eax+30h] //PEB
.
7c93153f cmp dword ptr [eax+64h],1 //比较 cpu个数
关键区抢占策略
1.如空闲则抢占。
.
2.若已被占用则忙等spincount次,忙等过程若关键区被释放则抢占。
.
3.忙等结束还没抢占成功,看关键区内的事件对象有没有创建,没有则创建一个,等待这个事件,线程切换。
关键区小结
..利用多cpu下线程并行执行的特点,关键区采用忙等的方式避免线程切换开销,为开发人员提供了一种很好的大内存互斥操作手段。
基于线程切换的内核同步对象
..事件(event)
..互斥量( mutex)
..信号量(sephamore)
..进程( process)
..线程 (thread)
..文件 (file)
..定时器(timer)
特点:
1.存于内核空间,可命名,可跨进程访问。
2.通过阻塞线程来同步,开销较大。
查看线程用户态堆栈(k命令)
0:000> kv
.
ChildEBP RetAddr Args to Child
.
0012fd6c 7c92df5a 7c8025db 000007f4 00000000
ntdll!KiFastSystemCallRet (FPO: [0,0,0])
.
0012fd70 7c8025db 000007f4 00000000 00000000
ntdll!NtWaitForSingleObject+0xc (FPO: [3,0,0])
.
0012fdd4 7c802542 000007f4 ffffffff 00000000
kernel32!WaitForSingleObjectEx+0xa8 (FPO: [Non-Fpo])
.
0012fde8 00411ac0 000007f4 ffffffff 0130f6ee
kernel32!WaitForSingleObject+0x12 (FPO: [Non-Fpo])
.
0012fedc 00411d90 00000001 00380fc8 00381040
CallTest!main+0x50 (FPO: [Non-Fpo]) (CONV: cdecl)
[d:\project\calltest\calltest\calltest.cpp @ 28]
.
0012ffc0 7c817077 0130f6ee 0130f768 7ffd7000
CallTest!mainCRTStartup+0x170 (FPO: [Non-Fpo]) (CONV: cdecl)
[f:\vs70builds\3077\vc\crtbld\crt\src\crt0.c @ 259]
.
0012fff0 00000000 00411339 00000000 78746341 kernel32!B
查看句柄类型(!handle)
0:000> !handle 0x7f4
.
Handle 7f4
.
Type Event
查看内核态堆栈
1.设置调试环境:双机调试,设置符号表等。
.
2.被调试程序放到目标系统运行。
.
3.运行windbg内核调试,连接目标系统
.
4.输入调试命令进行调试交互。
!process
kd> !process 0 0
.
**** NT ACTIVE PROCESS DUMP ****
PROCESS 819bd830 SessionId: none Cid: 0004 Peb: 00000000
ParentCid: 0000
.
DirBase: 003c2000 ObjectTable: e1001c48 HandleCount: 255.
.
Image: System
.
PROCESS 81809da0 SessionId: none Cid: 0230 Peb: 7ffd7000
ParentCid: 0004
.
DirBase: 03e40020 ObjectTable: e1421378 HandleCount: 19.
.
Image: smss.exe
.
PROCESS 816b8770 SessionId: 0 Cid: 0428 Peb: 7ffd5000
ParentCid: 0714
.
DirBase: 03e401c0 ObjectTable: e1d1a598 HandleCount: 9.
.
Image: CallTest.exe
kd> !process 816b8770 2
.
PROCESS 816b8770 SessionId: 0 Cid: 0428
Peb: 7ffd5000 ParentCid: 0714
.
DirBase: 03e401c0 ObjectTable: e1d1a598HandleCount: 9.
.
Image: CallTest.exe
.
THREAD 816d9020 Cid 0428.045c Teb:
7ffdf000 Win32Thread: 00000000 WAIT:
(UserRequest) UserMode Non-Alertable
.
816e9758 SynchronizationEvent
调试环境切换
kd> .process 816b8770
.
Implicit process is now 816b8770
.
WARNING: .cache forcedecodeuser
is not enabled
.
kd> .thread 816d9020
.
Implicit thread is now 816d9020
列出堆栈
kd> kv
.
*** Stack trace for last set context -.thread/.cxr resets it
.
ChildEBP RetAddr Args to Child
.
f8c10cb8 80501cf0 816d9090 816d9020 804fad72 nt!KiSwapContext+0x2e (FPO: [Uses EBP] [0,0,4])
.
f8c10cc4 804fad72 00000000 00000000 00000000 nt!KiSwapThread+0x46 (FPO: [0,0,0])
.
f8c10cec 805b727a 00000001 00000006 00000001 nt!KeWaitForSingleObject+0x1c2 (FPO: [Non-Fpo])
.
f8c10d50 8053e6d8 00000010 00000000 00000000 nt!NtWaitForSingleObject+0x9a (FPO: [Non-Fpo])
.
f8c10d50 7c92e514 00000010 00000000 00000000 nt!KiFastCallEntry+0xf8 (FPO: [0,0] TrapFrame @
f8c10d64)
.
0012fd6c 7c92df5a 7c8025db 00000010 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
.
0012fd70 7c8025db 00000010 00000000 00000000 ntdll!NtWaitForSingleObject+0xc (FPO: [3,0,0])
.
0012fdd4 7c802542 00000010 ffffffff 00000000 kernel32!WaitForSingleObjectEx+0xa8 (FPO: [Non-Fpo])
.
*** WARNING: Unable to verify checksum for Unknown_Module_00400000
.
0012fde8 00411ac0 00000010 ffffffff 02add720 kernel32!WaitForSingleObject+0x12 (FPO: [Non-Fpo])
.
0012fedc 00411d90 00000001 00380b80 00380bd0 Unknown_Module_00400000!main+0x50 (FPO:
[Non-Fpo]) (CONV: cdecl) [d:\project\calltest\calltest\calltest.cpp @ 28]
.
0012ffc0 7c817077 02add720 7c92d96e 7ffd5000 Unknown_Module_00400000!mainCRTStartup+0x170(FPO: [Non-Fpo]) (CONV: cdecl) [f:\vs70builds\3077\vc\crtbld\crt\src\crt0.c @ 259]
.
0012fff0 00000000 00411339 00000000 78746341 kernel32!BaseProcessStart+0x23 (FPO: [Non-Fpo])
.
kd> !process 816b8770 2
内核同步对象小结
..内核对象同步的本质就是线程切换调度,由于比较耗时,如非必要则尽量避免使用。
内核态同步技术
IRQL机制
..低IRQL下的同步
..高IRQL下的同步
IRQL机制
IRQL(interrupt request level ), windows自己实现的中断机制,x86系统用0-31表示IRQL,在x64和IA64中则用 0-15表示,数值越高表示优先级越高。高优先级的中断可以抢占低优先级的中断。
.
实现IRQL的目的在于把硬件中断,软件中断以及异常用统一的机制管理起来,方便扩展和修改。
IRQL机制
IRQL图(粘贴失败,此处略)
低IRQL下的同步
.
低IRQL指的是低于dispatch level的情况。
.
顶层驱动的io分发例程以及自己创建的系统线程都处于低IRQL状态。
.
原子操作(InterlockedXX)
.
分发器对象(事件、互斥、信号量…)
.
执行体资源
.
压栈锁
分发器对象
0:002> dt nt!_dispatcher_header
.
ntdll!_DISPATCHER_HEADER
.
+0x000 Type : UChar
.
+0x001 Absolute : UChar
.
+0x002 Size : UChar
.
+0x003 Inserted : UChar
.
+0x004 SignalState : Int4B
.
+0x008 WaitListHead : _LIST_ENTRY
.
0:002> dt nt!_kwait_block
.
ntdll!_KWAIT_BLOCK
.
+0x000 WaitListEntry : _LIST_ENTRY
.
+0x008 Thread : Ptr32 _KTHREAD
.
+0x00c Object : Ptr32 Void
.
+0x010 NextWaitBlock : Ptr32 _KWAIT_BLOCK
.
+0x014 WaitKey : Uint2B
.
+0x016 WaitType : Uint2B
等待块图
略
高IRQL下的同步
..高IRQL下不能使用分页内存,不能调用任何会导致缺页故障或者线程阻塞的代码。
.
startIo函数,DPC例程,中断服务函数都运行在高IRQL下。
..
原子操作(InterlockedXX、 ExInterlockedXX)
..自旋锁
自旋锁的实现
..单cpu情况,自旋锁的获得仅仅是提升 IRQL
..多cpu的情况,自旋锁的是通过在提升的 IRQL上对内存标志位的互斥抢占实现的。
..自旋锁的使用会带来IRQL的提升,普通自旋锁会提升至dispatch level,硬件中断自旋锁会提升至相应的DIRQL。
内核态同步技术小结
..在开发内核模块时,我们往往无法确定要同步的代码一定在低IRQL下运行,这时保险的做法是使用原子操作或者自旋锁,同时确保同步代码不会发生线程阻塞现象。
调试线程死锁问题
..情形1:获得临界区的线程不正确退出。
..情形2:线程循环等待。
线程不正确退出
DWORD WINAPI ThreadProc( LPVOID pParam )
.
{
.
.
EnterCriticalSection( (LPCRITICAL_SECTION)pParam );
.
return 0;
.
}
.
int _tmain(int argc, _TCHAR* argv[])
.
{
.
int a;
.
a = 1;
.
.
CRITICAL_SECTION cs;
.
InitializeCriticalSection(&cs);
.
.
for ( int i=0; i<3; ++i )
.
{
.
CreateThread( NULL, 0, ThreadProc, &cs, 0, NULL );
.
}
.
.
EnterCriticalSection( &cs );
.
}
线程不正确退出
0:003> !cs -l
.
----------------------------------------.
DebugInfo = 0x7c99e780
.
Critical section = 0x0012feb4 (+0x12FEB4)
.
LOCKED
.
LockCount = 0x2
.
OwningThread = 0x00000668
.
RecursionCount = 0x1
.
LockSemaphore = 0x7D8
.
SpinCount = 0x00000000
线程不正确退出
0:003> ~*kv
.
0 Id: c2c.9c Suspend: 1 Teb: 7ffdd000 Unfrozen
.
ChildEBP RetAddr Args to Child
.
0012fedc 00411e90 00000001 00380fc8 00381040 CallTest!main+0x7e (FPO: [Non-Fpo]) (CONV: cdecl)
[d:\project\calltest\calltest\calltest.cpp @ 36]
.
0012ffc0 7c817077 0130f6ee 0130f768 7ffde000 CallTest!mainCRTStartup+0x170 (FPO: [Non-Fpo])
(CONV: cdecl) [f:\vs70builds\3077\vc\crtbld\crt\src\crt0.c @ 259]
.
0012fff0 00000000 00411343 00000000 78746341 kernel32!BaseProcessStart+0x23 (FPO: [Non-Fpo])
.
1 Id: c2c.f64 Suspend: 1 Teb: 7ffda000 Unfrozen
.
ChildEBP RetAddr Args to Child
.
0072fe48 7c92df5a 7c939b23 000007d8 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
.
0072fe4c 7c939b23 000007d8 00000000 00000000 ntdll!NtWaitForSingleObject+0xc (FPO: [3,0,0])
.
0072fed4 7c921046 0012feb4 00411aca 0012feb4 ntdll!RtlpWaitForCriticalSection+0x132 (FPO: [Non-
Fpo])
.
0072fedc 00411aca 0012feb4 07060504 0b0a0908 ntdll!RtlEnterCriticalSection+0x46 (FPO: [1,0,0])
.
0072ffb4 7c80b729 0012feb4 07060504 0b0a0908 CallTest!ThreadProc+0x2a (FPO: [Non-Fpo]) (CONV:
stdcall) [d:\project\calltest\calltest\calltest.cpp @ 17]
.
0072ffec 00000000 004110be 0012feb4 00000000 kernel32!BaseThreadStart+0x37 (FPO: [Non-Fpo])
.
2 Id: c2c.48c Suspend: 1 Teb: 7ffdb000 Unfrozen
.
ChildEBP RetAddr Args to Child
.
0062fe48 7c92df5a 7c939b23 000007d8 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
.
0062fe4c 7c939b23 000007d8 00000000 00000000 ntdll!NtWaitForSingleObject+0xc (FPO: [3,0,0])
.
0062fed4 7c921046 0012feb4 00411aca 0012feb4 ntdll!RtlpWaitForCriticalSection+0x132 (FPO: [Non-
Fpo])
.
0062fedc 00411aca 0012feb4 07060504 0b0a0908 ntdll!RtlEnterCriticalSection+0x46 (FPO: [1,0,0])
.
0062ffb4 7c80b729 0012feb4 07060504 0b0a0908 CallTest!ThreadProc+0x2a (FPO: [Non-Fpo]) (CONV:
stdcall) [d:\project\calltest\calltest\calltest.cpp @ 17]
.
0062ffec 00000000 004110be 0012feb4 00000000 kernel32!BaseThreadStart+0x37 (FPO: [Non-Fpo])
.
循环等待
~*kv列出所有线程的堆栈
..找出由于等待而阻塞的线程
..用!cs命令列出每个对象的所属线程
..画出等待关系图,找出原因
内核同步对象的调试
..不跨进程的情况与调试临界区类似。
..跨进程的调试需要切换到内核调试状态,
找到问题线程,列出其等待的所有对象(!Process, !thread, dt)
..列出等待对象的属性(!object),找到与等待对象的其他相关线程,分析问题的原因。
同步问题调试小结
..多线程同步是开发实践中经常碰到的问题,借助windbg调试器,分析各个线程的等待关系图是解决问题的关键。
参考文献
..《深入解析windows操作系统》,第四版,mark russinovich, david solomon
..《windows驱动程序开发详解》,张帆 .
Windbg帮助文档