Watch & Learn

Debugwar Blog

Step in or Step over, this is a problem ...

多线程假象——记一次假多线程场景分析过程

2023-05-09 @ UTC+0

多线程是我们日常开发中经常使用到的技巧,一般使用多线程的目的是为了加快某些操作的速度从而给用户更好的使用体验。近期笔者就遇到了一个多线程相关问题,本来是想加速某些功能但是最总效果却和单线程一样,后来经过排查终于定位到了原因并解决了问题,也同时有了此文。

背景

目前笔者部门正在开发一个程序,其中有一块功能需要和内核做数据交互。程序的界面如下图:


为了更好的用户体验,程序的设计逻辑是:当点击“内核”标签页下的各个子标签页时,每个子标签页将会开一个线程去调用DeviceIoControl函数和内核模块做交互。这样的话每个子标签页的数据加载是独立的,互不影响,在一定程度上会加快数据的获取速度。

根据上述设计,抽象出的编码如下(以伪代码表示):

  1. typedef struct _TAB_PARAMETER {  
  2.     HANDLE DeviceHandle;  
  3.     ULONG_PTR IoControlCode;  
  4. } TAB_PARAMETER, *PTAB_PARAMETER;  
  5.   
  6. VOID subtab_woker_thread(PTAB_PARAMETER Param)  
  7. {  
  8.     UCHAR Buffer[256] = {0};  
  9.     ULONG_PTR Length = 0;  
  10.     DeviceIoControl(Param->DeviceHandle, Param->IoControlCode, \
  11.         NULL, 0, &Buffer, 256, &Length, NULL);
  12.     // 根据Tab标签处理进入不同的业务处理逻辑, 不重要, 省略  
  13.     switch (Param->IoControlCode)  
  14.     {  
  15.         case func1:  
  16.             ....  
  17.         break;  
  18.         ....  
  19.     }  
  20. }  
  21.   
  22. void main()  
  23. {  
  24.     HANDLE DeviceHandle = 0;  
  25.     ULONG_PTR Message = 0;  
  26.     LPCSTR deviceStr = "\\\\.\\KernelModule";  
  27.   
  28.    DeviceHandle = CreateFile( deviceStr, GENERIC_READ | GENERIC_WRITE, \  
  29.             FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, \  
  30.             FILE_ATTRIBUTE_NORMAL, NULL);  
  31.               
  32.     while (GetMessage(&Message, NULL, 0, 0) > 0) {  
  33.       TAB_PARAMETER TabParameters = {0};  
  34.         
  35.       TabParameters.DeviceHandle = DeviceHandle;  
  36.       TabParameters.IoControlCode = <根据不同消息填充不同IOCTL>;  
  37.         
  38.       begin_thread(subtab_woker_thread, 0, &TabParameters);  
  39.     }  
  40.       
  41.     CloseHandle(DeviceHandle);  
  42. }  

如果看到此处,你已经看出问题在哪里了,那么接下来就不用往下看了,因为所有必要的信息已经包含在上面的伪代码中。

问题现象

OK,有了上面的背景铺垫之后,接下来描述一下问题现象。

当前内核模块对各子标签页功能的处理速度有快有慢,我们不妨假设"Object"这个标签页很慢,需要60秒钟才能完成,"卸载模块"这个标签页很快, 5秒钟即可完成。

如果这个时候,我们先点击"Object"标签页, 那么此时其对应的subtab_woker_thread线程会被启动去请求数据。此时我们再点击"卸载模块"标签页,启动其对应工作线程去内核取数据。

这里暂停一下,各位请思考一下我们将会在几秒后看到"卸载模块"的数据?我们期望的答案是5秒,但正确答案应该是65秒。

上述答案也是程序实际的行为,在点击了一个比较耗时的子标签页之后再点击一个耗时很小的子标签页,则耗时很小的子标签页也需要等待很长时间后才会展示数据。

问题分析

从结果上看,虽然程序设计和编码上都是多线程同时工作的,但其实际效果依然是单线程的。是什么原因造成了这个结果呢?接下来我们通过调试器分析一下成因。

笔者使用的是双机调试环境,调试器是运行在内核模式下的,被调试机上运行我们本次的分析对象: QDoctor.exe。

首先在调试机上获取一下进程列表,以获得QDoctor的EPROCESS

  1. 0: kd> !process 0 0  
  2. ......  
  3. PROCESS ffffc58fbbbb5200  
  4.     SessionId: 1  Cid: 1478    Peb: 007fb000  ParentCid: 0ba4  
  5.     DirBase: a80cd000  ObjectTable: ffffdc810a140d00  HandleCount: <Data Not Accessible>  
  6.     Image: QDoctor.exe  
  7. ......  

看一下QDoctor各个线程的状态:

  1. 0: kd> !process ffffc58fbbbb5200 2  
  2. PROCESS ffffc58fbbbb5200  
  3.     SessionId: 1  Cid: 1478    Peb: 007fb000  ParentCid: 0ba4  
  4.     DirBase: a80cd000  ObjectTable: ffffdc810a140d00  HandleCount: <Data Not Accessible>  
  5.     Image: QDoctor.exe  
  6.   
  7.         THREAD ffffc58fbbbfe080  Cid 1478.0d00  Teb: 00000000007fd000 Win32Thread: ffffc58fbe424b20 WAIT: (WrResource) KernelMode Non-Alertable  
  8.             ffffc58fbea8e8c0  SynchronizationEvent  
  9.   
  10.         THREAD ffffc58fbbf5c080  Cid 1478.05cc  Teb: 0000000000600000 Win32Thread: 0000000000000000 WAIT: (WrQueue) UserMode Alertable  
  11.             ffffc58fbbbca700  QueueObject  
  12.   
  13.         THREAD ffffc58fbbf51080  Cid 1478.095c  Teb: 0000000000603000 Win32Thread: 0000000000000000 WAIT: (WrQueue) UserMode Alertable  
  14.             ffffc58fbbbca700  QueueObject  
  15.   
  16.         THREAD ffffc58fbba9a080  Cid 1478.03d4  Teb: 0000000000606000 Win32Thread: 0000000000000000 WAIT: (WrQueue) UserMode Alertable  
  17.             ffffc58fbbbca700  QueueObject  
  18.   
  19.         THREAD ffffc58fbbefe7c0  Cid 1478.03ec  Teb: 0000000000609000 Win32Thread: 0000000000000000 WAIT: (Executive) KernelMode Alertable  
  20.             ffffc58fbc07cf70  SynchronizationEvent  
  21.   
  22.         THREAD ffffc58fbbefd080  Cid 1478.0460  Teb: 000000000060c000 Win32Thread: 0000000000000000 RUNNING on processor 2  
  23.         THREAD ffffc58fbbefb080  Cid 1478.0e1c  Teb: 000000000060f000 Win32Thread: ffffc58fbfd4d7f0 WAIT: (Executive) KernelMode Alertable  
  24.             ffffc58fbc07cf70  SynchronizationEvent  
  25.   
  26.         THREAD ffffc58fbbefa080  Cid 1478.118c  Teb: 0000000000612000 Win32Thread: ffffc58fbb2a2600 WAIT: (Executive) KernelMode Alertable  
  27.             ffffc58fbc07cf70  SynchronizationEvent  
  28.   
  29.         THREAD ffffc58fbc062080  Cid 1478.1340  Teb: 0000000000615000 Win32Thread: ffffc58fbec49830 WAIT: (WrUserRequest) UserMode Non-Alertable  
  30.             ffffc58fbc048bf0  SynchronizationEvent  
  31.   
  32.         THREAD ffffc58fc029b040  Cid 1478.0ee8  Teb: 0000000000618000 Win32Thread: 0000000000000000 WAIT: (WrQueue) UserMode Alertable  
  33.             ffffc58fbbbd6a40  QueueObject  
  34.   
  35.         THREAD ffffc58fc029a080  Cid 1478.0dd4  Teb: 000000000061b000 Win32Thread: 0000000000000000 WAIT: (WrQueue) UserMode Alertable  
  36.             ffffc58fbbbd6a40  QueueObject  
  37.   
  38.         THREAD ffffc58fc0299080  Cid 1478.0bb4  Teb: 000000000061e000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable  
  39.             ffffc58fbb954740  SynchronizationTimer  
  40.   
  41.         THREAD ffffc58fc1693080  Cid 1478.19d8  Teb: 0000000000624000 Win32Thread: 0000000000000000 WAIT: (Executive) KernelMode Alertable  
  42.             ffffc58fbc07cf70  SynchronizationEvent  
  43.   
  44.         THREAD ffffc58fbb7c5080  Cid 1478.19dc  Teb: 0000000000627000 Win32Thread: 0000000000000000 WAIT: (Executive) KernelMode Alertable  
  45.             ffffc58fbc07cf70  SynchronizationEvent  
  46.   
  47.         THREAD ffffc58fbf792640  Cid 1478.19e0  Teb: 000000000062a000 Win32Thread: 0000000000000000 WAIT: (Executive) KernelMode Alertable  
  48.             ffffc58fbc07cf70  SynchronizationEvent  
  49.   
  50.         THREAD ffffc58fc173e080  Cid 1478.19e8  Teb: 000000000062d000 Win32Thread: 0000000000000000 WAIT: (Executive) KernelMode Alertable  
  51.             ffffc58fbc07cf70  SynchronizationEvent  

可以看到,只有0460号线程是RUNNING状态,这个线程位于2号核上。看一下这个线程的堆栈状态:

  1. 0: kd> !thread ffffc58fbbefd080  
  2. THREAD ffffc58fbbefd080  Cid 1478.0460  Teb: 000000000060c000 Win32Thread: 0000000000000000 RUNNING on processor 2  
  3. IRP List:  
  4.     ffffc58fbf969ae0: (0006,0118) Flags: 00060070  Mdl: 00000000  
  5. Not impersonating  
  6. DeviceMap                 ffffdc8101436b60  
  7. Owning Process            ffffc58fbbbb5200       Image:         QDoctor.exe  
  8. Attached Process          N/A            Image:         N/A  
  9. Wait Start TickCount      14901          Ticks: 1 (0:00:00:00.015)  
  10. Context Switch Count      1566           IdealProcessor: 1               
  11. UserTime                  00:00:00.000  
  12. KernelTime                00:00:01.640  
  13. Win32 Start Address 0x0000000077456020  
  14. Stack Init ffffae8152e30c90 Current ffffae8152e2f640  
  15. Base ffffae8152e31000 Limit ffffae8152e2b000 Call 0000000000000000  
  16. Priority 10 BasePriority 8 PriorityDecrement 2 IoPriority 2 PagePriority 5  
  17. Child-SP          RetAddr               : Args to Child                                                           : Call Site  
  18. ffffae81`52e2f950 00000000`00000002     : 00000000`00000001 ffffae81`52e2fd60 ffffc58f`bc07cef0 00000000`00000000 : constantine64+0xc231de  
  19. ffffae81`52e2fa48 00000000`00000001     : ffffae81`52e2fd60 ffffc58f`bc07cef0 00000000`00000000 fffff800`0cb0ca80 : 0x2  
  20. ffffae81`52e2fa50 ffffae81`52e2fd60     : ffffc58f`bc07cef0 00000000`00000000 fffff800`0cb0ca80 fffff805`e3d30182 : 0x1  
  21. ffffae81`52e2fa58 ffffc58f`bc07cef0     : 00000000`00000000 fffff800`0cb0ca80 fffff805`e3d30182 ffffae81`52e2fb70 : 0xffffae81`52e2fd60  
  22. ffffae81`52e2fa60 00000000`00000000     : fffff800`0cb0ca80 fffff805`e3d30182 ffffae81`52e2fb70 00000000`00000002 : 0xffffc58f`bc07cef0  

从上面的第18行可见,此时这个线程正在执行位于constantine64+0xc231de处的指令,而constantine64模块即为我们的内核模块。换句话说,只有0460号线程是在真正干活的,而其他的“工作”线程却在偷懒,比如下面这个线程:

  1. THREAD ffffc58fbbefe7c0  Cid 1478.03ec  Teb: 0000000000609000 Win32Thread: 0000000000000000 WAIT: (Executive) KernelMode Alertable  
  2.     ffffc58fbc07cf70  SynchronizationEvent  

看一下上面这个线程在干嘛:

  1. 0: kd> !thread ffffc58fbbefe7c0  
  2. THREAD ffffc58fbbefe7c0  Cid 1478.03ec  Teb: 0000000000609000 Win32Thread: 0000000000000000 WAIT: (Executive) KernelMode Alertable  
  3.     ffffc58fbc07cf70  SynchronizationEvent  
  4. Not impersonating  
  5. DeviceMap                 ffffdc8101436b60  
  6. Owning Process            ffffc58fbbbb5200       Image:         QDoctor.exe  
  7. Attached Process          N/A            Image:         N/A  
  8. Wait Start TickCount      13091          Ticks: 1811 (0:00:00:28.296)  
  9. Context Switch Count      551            IdealProcessor: 0               
  10. UserTime                  00:00:00.000  
  11. KernelTime                00:00:00.281  
  12. Win32 Start Address 0x0000000077456020  
  13. Stack Init ffffae8152e29c90 Current ffffae8152e294e0  
  14. Base ffffae8152e2a000 Limit ffffae8152e24000 Call 0000000000000000  
  15. Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5  
  16. Child-SP          RetAddr               : Args to Child                                                           : Call Site  
  17. ffffae81`52e29520 fffff800`0c841cdc     : ffffc400`0003b338 80000000`00000000 ffffc400`0003b338 fffff800`0c87c7c6 : nt!KiSwapContext+0x76  
  18. ffffae81`52e29660 fffff800`0c84177f     : ffffae81`52e297a0 fffff800`0c95b72a ffffc58f`bbbec500 00000000`00000000 : nt!KiSwapThread+0x17c  
  19. ffffae81`52e29710 fffff800`0c843547     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiCommitThreadWait+0x14f  
  20. ffffae81`52e297b0 fffff800`0c8bcd03     : ffffc58f`bc07cf70 00000000`00000000 00007fff`ffff0000 00000000`00000000 : nt!KeWaitForSingleObject+0x377  
  21. ffffae81`52e29860 fffff800`0cc7a07d     : ffffc58f`bc07cef0 ffffae81`52e2994b ffffc58f`746c6644 ffffae81`52e29940 : nt!IopWaitForLockAlertable+0x43  
  22. ffffae81`52e298a0 fffff800`0cc07e38     : ffffc58f`bc07cef0 ffffae81`52e29b80 00000000`0022e180 fffff800`0c87f4f2 : nt!IopAcquireFileObjectLock+0x59  
  23. ffffae81`52e298e0 fffff800`0cc07286     : ffffc58f`bbefe7c0 00000000`00000000 00000000`00000000 00000000`00000000 : nt!IopXxxControlFile+0xba8  
  24. ffffae81`52e29a20 fffff800`0c95cc93     : ffffc462`000001d8 ffffc462`31000000 ffffc462`31188000 ffff9b7f`65f8e7e6 : nt!NtDeviceIoControlFile+0x56  
  25. ffffae81`52e29a90 00000000`5947222c     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ ffffae81`52e29b00)  
  26. 00000000`0329f138 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : 0x5947222c  

IdealProcessor可知,此线程在1号核上运行(从0开始计数),按照道理来说应该和位于2号核的线程ffffc58fbbefd080不冲突才对,但是此线程在调用了nt!NtDeviceIoControlFile之后却偏偏进入了nt!KeWaitForSingleObject,而这次等待最终导致内核执行了线程切换(nt!KiSwapThread)。

从栈回溯来看,ffffc58fbbefe7c0线程进入等待的原因是nt!IopAcquireFileObjectLock尝试去拿一个文件对象的锁,实际情况是线程并没有拿到这个锁。

到这里再次暂停一下。如果各位看官看到这里想到了问题成因,那么说明你的基本功比较扎实哦~ ;)

那么nt!IopAcquireFileObjectLock尝试获取的这把锁是什么东西呢?根据对nt!IopXxxControlFile的逆向可知这把锁来自nt!IopAcquireFileObjectLock的第一个参数,其类型为_FILE_OBJECT指针:

  1. __int64 __fastcall IopXxxControlFile(......)  
  2. {  
  3.     ......  
  4.       v19 = (_FILE_OBJECT *)Object;  
  5.       v51 = IopAcquireFileObjectLock(Object, v15, v50, v61);  
  6.     ......  
  7. }  
  8.   
  9. __int64 __fastcall IopAcquireFileObjectLock(_FILE_OBJECT *Object, __int64 a2, __int64 a3, _BYTE *a4)  
  10. {  
  11.   ....  
  12.   do  
  13.   {  
  14.     ....  
  15.     v7 = IopWaitForLockAlertable(&Object->Lock);  
  16.     ....  
  17.   }  
  18.   ....  
  19. }  
  20.   
  21. NTSTATUS __fastcall IopWaitForLockAlertable(PVOID Object, KPROCESSOR_MODE a2, char a3)  
  22. {  
  23.   ....  
  24.   do  
  25.   {  
  26.     ....  
  27.     result = KeWaitForSingleObject(Object, Executive, v7, v6, 0i64);  
  28.   }  
  29.   while (....);  
  30.   ....  
  31. }  

那么看一下这个_FILE_OBJECT具体是个什么东西,根据调用堆栈可知第一个参数是:ffffc58f`bc07cef0windbg观察一下:

  1. 0: kd> !object ffffc58f`bc07cef0  
  2. Object: ffffc58fbc07cef0  Type: (ffffc58fbb0ccf20) File  
  3.     ObjectHeader: ffffc58fbc07cec0 (new version)  
  4.     HandleCount: 1  PointerCount: 32760  
  5. 0: kd> dt _FILE_OBJECT ffffc58f`bc07cef0  
  6. ntdll!_FILE_OBJECT  
  7.    +0x000 Type             : 0n5  
  8.    +0x002 Size             : 0n216  
  9.    +0x008 DeviceObject     : 0xffffc58f`bc4c9060 _DEVICE_OBJECT  
  10.    +0x010 Vpb              : (null)   
  11.    +0x018 FsContext        : (null)   
  12.    +0x020 FsContext2       : (null)   
  13.    +0x028 SectionObjectPointer : (null)   
  14.    +0x030 PrivateCacheMap  : (null)   
  15.    +0x038 FinalStatus      : 0n0  
  16.    +0x040 RelatedFileObject : (null)   
  17.    +0x048 LockOperation    : 0 ''  
  18.    +0x049 DeletePending    : 0 ''  
  19.    +0x04a ReadAccess       : 0 ''  
  20.    +0x04b WriteAccess      : 0 ''  
  21.    +0x04c DeleteAccess     : 0 ''  
  22.    +0x04d SharedRead       : 0 ''  
  23.    +0x04e SharedWrite      : 0 ''  
  24.    +0x04f SharedDelete     : 0 ''  
  25.    +0x050 Flags            : 0x40002  
  26.    +0x058 FileName         : _UNICODE_STRING ""  
  27.    +0x068 CurrentByteOffset : _LARGE_INTEGER 0x0  
  28.    +0x070 Waiters          : 7  
  29.    +0x074 Busy             : 1  
  30.    +0x078 LastLock         : (null)   
  31.    +0x080 Lock             : _KEVENT  
  32.    +0x098 Event            : _KEVENT  
  33.    +0x0b0 CompletionContext : (null)   
  34.    +0x0b8 IrpListLock      : 0  
  35.    +0x0c0 IrpList          : _LIST_ENTRY [ 0xffffc58f`bc07cfb0 - 0xffffc58f`bc07cfb0 ]  
  36.    +0x0d0 FileObjectExtension : (null)   
  37. 0: kd> dx -id 0,0,ffffc58fbeb24780 -r1 ((ntdll!_DEVICE_OBJECT *)0xffffc58fbc4c9060)  
  38. ((ntdll!_DEVICE_OBJECT *)0xffffc58fbc4c9060)                 : 0xffffc58fbc4c9060 : Device for "\FileSystem\QAXANTIROOTKIT" [Type: _DEVICE_OBJECT *]  
  39.     [<Raw View>]     [Type: _DEVICE_OBJECT]  
  40.     Flags            : 0x40  
  41.     UpperDevices     : None  
  42.     LowerDevices     : None  
  43.     Driver           : 0xffffc58fbb346950 : Driver "\FileSystem\QAXANTIROOTKIT" [Type: _DRIVER_OBJECT *]  

找到了一个设备对象ffffc58f`bc4c9060,而这个设备对象为\FileSystem\QAXANTIROOTKIT,这个就是我们上文中提到的constantine64驱动创建的服务。

分析结论

结合本文一开始给出的程序工作流程伪代码,可知当前的工作流程是:

  1. 入口函数打开由constantine64创建的服务,获得其句柄
  2. 启动子标签页工作线程,并传入1中打开的句柄
  3. 子标签页工作线程调用DeviceIoControl请求constantine64的对应功能
  4. DeviceIoControl的内核例程调用nt!IopAcquireFileObjectLock尝试获取1中句柄对应文件对象的锁
  5. 由于句柄是入口函数创建,属于共享资源,有其他RUNNING状态线程已经持有此设备,因此获取文件对象锁失败
  6. 调用nt!KeWaitForSingleObject等待锁解除锁定状态
  7. 线程进入Alertable WAIT状态
  8. 系统进行线程切换

因此,只有第5步中RUNNING状态线程释放占用的锁之后,其他工作线程才能继续工作。回到一开始我们假设的场景中,即只有"Object"的工作线程在60s的工作完成后,"卸载模块"的工作线程才能拿到这把锁,然后用5秒钟把活干完。这也是上文中正确答案65秒的由来。

虽然我们在设计上是多线程的,但是由于对临界资源的使用不当导致最终还是单线程线性工作。其实,在微软的文档里,已经明确告诉我们了。根据CreateFile的API文档,其dwFlagsAndAttributes参数的描述参考1:


还记得一开始我们是如何创建设备句柄的吗?

  1. DeviceHandle = CreateFile( deviceStr, GENERIC_READ | GENERIC_WRITE, \    
  2.             FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, \    
  3.             FILE_ATTRIBUTE_NORMAL, NULL);    

并没有指定OVERLAPPED,因此所有对这个句柄的请求都会是同步IO,这也是为何内核要获取锁的原因。

解决问题

通过上文的分析,我们自然想到可以借助OVERLAPPED实现异步IO。但是这样是有代价的——这意味着R3程序和R0程序都需要作出对应修改,且R0程序还需要借助开启额外的内核线程来完成异步请求的处理,这样会导致本来一个功能对应的后台线程数翻倍。

那么,既然我们R3程序已经开了子线程来处理当前子标签页的请求了,R0程序能不能利用这个子线程,在当前子线程的上下文中完成对应的内核部分工作呢?答案当然是肯定的,而且对应修改也非常简单,重写后的程序工作代码(伪)如下:

  1. VOID subtab_woker_thread(ULONG_PTR *IoControlCode)    
  2. {    
  3.     UCHAR Buffer[256] = {0};    
  4.     ULONG_PTR Length = 0;  
  5.     HANDLE DeviceHandle = 0;  
  6.     LPCSTR deviceStr = "\\\\.\\KernelModule";  
  7.       
  8.     DeviceHandle = CreateFile( deviceStr, GENERIC_READ | GENERIC_WRITE, \    
  9.             FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, \    
  10.             FILE_ATTRIBUTE_NORMAL, NULL);  
  11.   
  12.     DeviceIoControl(DeviceHandle, *IoControlCode, \  
  13.         NULL, 0, &Buffer, 256, &Length, NULL);  
  14.   
  15.     // 根据Tab标签处理进入不同的业务处理逻辑, 不重要, 省略    
  16.     switch (Param->IoControlCode)    
  17.     {    
  18.         case func1:    
  19.             ....    
  20.         break;    
  21.         ....    
  22.     }    
  23.       
  24.     CloseHandle(DeviceHandle);  
  25. }    
  26.     
  27. void main()    
  28. {    
  29.     ULONG_PTR Message = 0;  
  30.              
  31.     while (GetMessage(&Message, NULL, 0, 0) > 0) {  
  32.       begin_thread(subtab_woker_thread, 0, <根据不同消息选择不同IOCTL>);    
  33.     }    
  34. }  

如各位所见,仅需要简单粗暴的将创建设备对象句柄的代码移到工作线程内使其变成一个局部变量即可。

这里又要暂停一下了,不知道看到这里各位有没有和我一样的疑问,这个疑问关键词是:引用计数。

既然我们的所有子线程都是创建自\\\\.\\KernelModule的句柄,那么内核层面不应该是使\\\\.\\KernelModule文件对象的引用计数不停的增加吗?这样的话,由于当前代码我们依然没有使用OVERLAPPED参数,其创建的文件对象又都是一个,那么依然应该是假的多线程才对啊。

带着这个疑问,我们有必要继续深挖一下内核的工作模式。

CreateFile函数流程简要分析

CreateFile函数应该是我们日常编程中使用频率最高的函数之一了,因此对他有必要深入理解一下。通过对WRK的研究,我们可以得出如下内核调用路径:


可见最后会调用到OpbLookupObjectName函数,这个函数本身比较复杂,但是其核心却及其简单:从Object Directory中查找对应名称的对象,然后调用对象的ParseProcedure函数用于生成待创建文件对象。由于本文中我们面对的是Device类型的设备,所以我们关注Device类型对象的ParseProcedure函数,而对Device类型对象的创建工作位于IoCreateObjectTypes函数中,在这里我们可以得到系统默认的ParseProcedure函数值:


由上图可知,Device类型对象的ParseProcedure默认函数为IopParseDevice。而通过对WRK中IopParseDevice的研究,其重点在于对ObCreateObject的调用:


简化后的IopParseDevice逻辑:

  1. NTSTATUS IopParseDevice(......, IN OUT PVOID Context OPTIONAL, ......)  
  2. {  
  3.     ....  
  4.     POPEN_PACKET op;  
  5.     op = Context;  
  6.   
  7.     realFileObjectRequired = !(op->QueryOnly || op->DeleteOnly);  
  8.   
  9.     if (realFileObjectRequired) {  
  10.         ......  
  11.         status = ObCreateObject( KernelMode,  
  12.                                  IoFileObjectType,  
  13.                                  &objectAttributes,  
  14.                                  AccessMode,  
  15.                                  (PVOID) NULL,  
  16.                                  fileObjectSize,  
  17.                                  0,  
  18.                                  0,  
  19.                                  (PVOID *) &fileObject );  
  20.         ......  
  21.     }  
  22.     ......  
  23. }  

可见当 op->QueryOnlyop->DeleteOnly 均为FALSE时,realFileObjectRequired为TRUE,此时需要创建对应的文件对象。而根据OPEN_PACKET的定义:


结合重写后的程序逻辑,目前我们打开对象的操作必然不是 查询、删除 操作,因此一定会走到ObCreateObject的逻辑,即当CreateFile的目标为设备(或设备的软连接)时,一定会(查询、删除除外)产生文件对象创建操作。

由于文件对象都是新创建的,因此本节一开始的认知——其创建的文件对象又都是一个——是不正确的,实际上每个子线程调用CreateFile创建的句柄后面都对应一个新创建的同步IO文件对象,由于是新创建的因此不会产生一开始无法获取到锁的问题(因为只有当前子线程在用这个文件对象)。这也是本篇文章“假多线程”解决方案的根据。

DuplicateHandle

在实际编程中,还有一个和句柄相关的API即DuplicateHandle,通过跟踪这个函数的代码,发现从头到尾,都未调用到ObCreateObject函数,而是在进程PspCidTable中大量使用ObReferenceObDereference家族函数增减对象的引用计数:


因此我们可以得出结论DuplicateHandle函数并不会导致新对象的创建,其作用仅仅是在PspCidTable中新增一项并使新增的项目指向内核重已经存在的一个对象。

总结

纵观整个调试、分析过程,其实本文面对问题产生的原因本质上是因为不合理使用临界资源导致的,只不过是本文中涉及到的临界资源比较隐晦,虽然在微软的官方文档中有相关描述,但是在实际使用的过程中依然很容易忽略从而写出不符合预期的代码。

其实可以认为本文是针对同步IO与异步IO参考2的一个原理性分析,希望可以帮到大家。

参考

  1. CreateFile API文档
  2. Synchronous and Asynchronous I/O
目录
背景
问题现象
问题分析
分析结论
解决问题
CreateFile函数流程简要分析
DuplicateHandle
总结
参考

版权所有 (c) 2020 - 2025 Debugwar.com

由 Hacksign 设计