windows同步原理与调试(PPT)

    最近在公司做了一次关于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帮助文档




你可能感兴趣的:(windows开发)