多线程是我们日常开发中经常使用到的技巧,一般使用多线程的目的是为了加快某些操作的速度从而给用户更好的使用体验。近期笔者就遇到了一个多线程相关问题,本来是想加速某些功能但是最总效果却和单线程一样,后来经过排查终于定位到了原因并解决了问题,也同时有了此文。
目前笔者部门正在开发一个程序,其中有一块功能需要和内核做数据交互。程序的界面如下图:
为了更好的用户体验,程序的设计逻辑是:当点击“内核”标签页下的各个子标签页时,每个子标签页将会开一个线程去调用DeviceIoControl函数和内核模块做交互。这样的话每个子标签页的数据加载是独立的,互不影响,在一定程度上会加快数据的获取速度。
根据上述设计,抽象出的编码如下(以伪代码表示):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
typedef struct _TAB_PARAMETER {
HANDLE DeviceHandle;
ULONG_PTR IoControlCode;
} TAB_PARAMETER,
*
PTAB_PARAMETER;
VOID subtab_woker_thread(PTAB_PARAMETER Param)
{
UCHAR
Buffer
[
256
]
=
{
0
};
ULONG_PTR Length
=
0
;
DeviceIoControl(Param
-
>DeviceHandle, Param
-
>IoControlCode, \
NULL,
0
, &
Buffer
,
256
, &Length, NULL);
/
/
根据Tab标签处理进入不同的业务处理逻辑, 不重要, 省略
switch (Param
-
>IoControlCode)
{
case func1:
....
break
;
....
}
}
void main()
{
HANDLE DeviceHandle
=
0
;
ULONG_PTR Message
=
0
;
LPCSTR deviceStr
=
"\\\\.\\KernelModule"
;
DeviceHandle
=
CreateFile( deviceStr, GENERIC_READ | GENERIC_WRITE, \
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, \
FILE_ATTRIBUTE_NORMAL, NULL);
while
(GetMessage(&Message, NULL,
0
,
0
) >
0
) {
TAB_PARAMETER TabParameters
=
{
0
};
TabParameters.DeviceHandle
=
DeviceHandle;
TabParameters.IoControlCode
=
<根据不同消息填充不同IOCTL>;
begin_thread(subtab_woker_thread,
0
, &TabParameters);
}
CloseHandle(DeviceHandle);
}
|
如果看到此处,你已经看出问题在哪里了,那么接下来就不用往下看了,因为所有必要的信息已经包含在上面的伪代码中。
OK,有了上面的背景铺垫之后,接下来描述一下问题现象。
当前内核模块对各子标签页功能的处理速度有快有慢,我们不妨假设"Object"这个标签页很慢,需要60秒钟才能完成,"卸载模块"这个标签页很快, 5秒钟即可完成。
如果这个时候,我们先点击"Object"标签页, 那么此时其对应的subtab_woker_thread线程会被启动去请求数据。此时我们再点击"卸载模块"标签页,启动其对应工作线程去内核取数据。
这里暂停一下,各位请思考一下我们将会在几秒后看到"卸载模块"的数据?我们期望的答案是5秒,但正确答案应该是65秒。
上述答案也是程序实际的行为,在点击了一个比较耗时的子标签页之后再点击一个耗时很小的子标签页,则耗时很小的子标签页也需要等待很长时间后才会展示数据。
从结果上看,虽然程序设计和编码上都是多线程同时工作的,但其实际效果依然是单线程的。是什么原因造成了这个结果呢?接下来我们通过调试器分析一下成因。
笔者使用的是双机调试环境,调试器是运行在内核模式下的,被调试机上运行我们本次的分析对象: QDoctor.exe。
首先在调试机上获取一下进程列表,以获得QDoctor的EPROCESS:
1
2
3
4
5
6
7
|
0
: kd> !process
0
0
......
PROCESS ffffc58fbbbb5200
SessionId:
1
Cid:
1478
Peb:
007fb000
ParentCid:
0ba4
DirBase: a80cd000 ObjectTable: ffffdc810a140d00 HandleCount: <Data Not Accessible>
Image: QDoctor.exe
......
|
看一下QDoctor各个线程的状态:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
0
: kd> !process ffffc58fbbbb5200
2
PROCESS ffffc58fbbbb5200
SessionId:
1
Cid:
1478
Peb:
007fb000
ParentCid:
0ba4
DirBase: a80cd000 ObjectTable: ffffdc810a140d00 HandleCount: <Data Not Accessible>
Image: QDoctor.exe
THREAD ffffc58fbbbfe080 Cid
1478.0d00
Teb:
00000000007fd000
Win32Thread: ffffc58fbe424b20 WAIT: (WrResource) KernelMode Non
-
Alertable
ffffc58fbea8e8c0 SynchronizationEvent
THREAD ffffc58fbbf5c080 Cid
1478.05cc
Teb:
0000000000600000
Win32Thread:
0000000000000000
WAIT: (WrQueue) UserMode Alertable
ffffc58fbbbca700 QueueObject
THREAD ffffc58fbbf51080 Cid
1478.095c
Teb:
0000000000603000
Win32Thread:
0000000000000000
WAIT: (WrQueue) UserMode Alertable
ffffc58fbbbca700 QueueObject
THREAD ffffc58fbba9a080 Cid
1478.03d4
Teb:
0000000000606000
Win32Thread:
0000000000000000
WAIT: (WrQueue) UserMode Alertable
ffffc58fbbbca700 QueueObject
THREAD ffffc58fbbefe7c0 Cid
1478.03ec
Teb:
0000000000609000
Win32Thread:
0000000000000000
WAIT: (Executive) KernelMode Alertable
ffffc58fbc07cf70 SynchronizationEvent
THREAD ffffc58fbbefd080 Cid
1478.0460
Teb:
000000000060c000
Win32Thread:
0000000000000000
RUNNING on processor
2
THREAD ffffc58fbbefb080 Cid
1478.0e1c
Teb:
000000000060f000
Win32Thread: ffffc58fbfd4d7f0 WAIT: (Executive) KernelMode Alertable
ffffc58fbc07cf70 SynchronizationEvent
THREAD ffffc58fbbefa080 Cid
1478.118c
Teb:
0000000000612000
Win32Thread: ffffc58fbb2a2600 WAIT: (Executive) KernelMode Alertable
ffffc58fbc07cf70 SynchronizationEvent
THREAD ffffc58fbc062080 Cid
1478.1340
Teb:
0000000000615000
Win32Thread: ffffc58fbec49830 WAIT: (WrUserRequest) UserMode Non
-
Alertable
ffffc58fbc048bf0 SynchronizationEvent
THREAD ffffc58fc029b040 Cid
1478.0ee8
Teb:
0000000000618000
Win32Thread:
0000000000000000
WAIT: (WrQueue) UserMode Alertable
ffffc58fbbbd6a40 QueueObject
THREAD ffffc58fc029a080 Cid
1478.0dd4
Teb:
000000000061b000
Win32Thread:
0000000000000000
WAIT: (WrQueue) UserMode Alertable
ffffc58fbbbd6a40 QueueObject
THREAD ffffc58fc0299080 Cid
1478.0bb4
Teb:
000000000061e000
Win32Thread:
0000000000000000
WAIT: (UserRequest) UserMode Non
-
Alertable
ffffc58fbb954740 SynchronizationTimer
THREAD ffffc58fc1693080 Cid
1478.19d8
Teb:
0000000000624000
Win32Thread:
0000000000000000
WAIT: (Executive) KernelMode Alertable
ffffc58fbc07cf70 SynchronizationEvent
THREAD ffffc58fbb7c5080 Cid
1478.19dc
Teb:
0000000000627000
Win32Thread:
0000000000000000
WAIT: (Executive) KernelMode Alertable
ffffc58fbc07cf70 SynchronizationEvent
THREAD ffffc58fbf792640 Cid
1478.19e0
Teb:
000000000062a000
Win32Thread:
0000000000000000
WAIT: (Executive) KernelMode Alertable
ffffc58fbc07cf70 SynchronizationEvent
THREAD ffffc58fc173e080 Cid
1478.19e8
Teb:
000000000062d000
Win32Thread:
0000000000000000
WAIT: (Executive) KernelMode Alertable
ffffc58fbc07cf70 SynchronizationEvent
|
可以看到,只有0460号线程是RUNNING状态,这个线程位于2号核上。看一下这个线程的堆栈状态:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
0
: kd> !thread ffffc58fbbefd080
THREAD ffffc58fbbefd080 Cid
1478.0460
Teb:
000000000060c000
Win32Thread:
0000000000000000
RUNNING on processor
2
IRP
List
:
ffffc58fbf969ae0: (
0006
,
0118
) Flags:
00060070
Mdl:
00000000
Not impersonating
DeviceMap ffffdc8101436b60
Owning Process ffffc58fbbbb5200 Image: QDoctor.exe
Attached Process N
/
A Image: N
/
A
Wait Start TickCount
14901
Ticks:
1
(
0
:
00
:
00
:
00.015
)
Context Switch Count
1566
IdealProcessor:
1
UserTime
00
:
00
:
00.000
KernelTime
00
:
00
:
01.640
Win32 Start Address
0x0000000077456020
Stack Init ffffae8152e30c90 Current ffffae8152e2f640
Base ffffae8152e31000 Limit ffffae8152e2b000 Call
0000000000000000
Priority
10
BasePriority
8
PriorityDecrement
2
IoPriority
2
PagePriority
5
Child
-
SP RetAddr : Args to Child : Call Site
ffffae81`
52e2f950
00000000
`
00000002
:
00000000
`
00000001
ffffae81`
52e2fd60
ffffc58f`bc07cef0
00000000
`
00000000
: constantine64
+
0xc231de
ffffae81`
52e2fa48
00000000
`
00000001
: ffffae81`
52e2fd60
ffffc58f`bc07cef0
00000000
`
00000000
fffff800`
0cb0ca80
:
0x2
ffffae81`
52e2fa50
ffffae81`
52e2fd60
: ffffc58f`bc07cef0
00000000
`
00000000
fffff800`
0cb0ca80
fffff805`e3d30182 :
0x1
ffffae81`
52e2fa58
ffffc58f`bc07cef0 :
00000000
`
00000000
fffff800`
0cb0ca80
fffff805`e3d30182 ffffae81`
52e2fb70
:
0xffffae81
`
52e2fd60
ffffae81`
52e2fa60
00000000
`
00000000
: fffff800`
0cb0ca80
fffff805`e3d30182 ffffae81`
52e2fb70
00000000
`
00000002
:
0xffffc58f
`bc07cef0
|
从上面的第18行可见,此时这个线程正在执行位于constantine64+0xc231de处的指令,而constantine64模块即为我们的内核模块。换句话说,只有0460号线程是在真正干活的,而其他的“工作”线程却在偷懒,比如下面这个线程:
1
2
|
THREAD ffffc58fbbefe7c0 Cid
1478.03ec
Teb:
0000000000609000
Win32Thread:
0000000000000000
WAIT: (Executive) KernelMode Alertable
ffffc58fbc07cf70 SynchronizationEvent
|
看一下上面这个线程在干嘛:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
0
: kd> !thread ffffc58fbbefe7c0
THREAD ffffc58fbbefe7c0 Cid
1478.03ec
Teb:
0000000000609000
Win32Thread:
0000000000000000
WAIT: (Executive) KernelMode Alertable
ffffc58fbc07cf70 SynchronizationEvent
Not impersonating
DeviceMap ffffdc8101436b60
Owning Process ffffc58fbbbb5200 Image: QDoctor.exe
Attached Process N
/
A Image: N
/
A
Wait Start TickCount
13091
Ticks:
1811
(
0
:
00
:
00
:
28.296
)
Context Switch Count
551
IdealProcessor:
0
UserTime
00
:
00
:
00.000
KernelTime
00
:
00
:
00.281
Win32 Start Address
0x0000000077456020
Stack Init ffffae8152e29c90 Current ffffae8152e294e0
Base ffffae8152e2a000 Limit ffffae8152e24000 Call
0000000000000000
Priority
8
BasePriority
8
PriorityDecrement
0
IoPriority
2
PagePriority
5
Child
-
SP RetAddr : Args to Child : Call Site
ffffae81`
52e29520
fffff800`
0c841cdc
: ffffc400`
0003b338
80000000
`
00000000
ffffc400`
0003b338
fffff800`
0c87c7c6
: nt!KiSwapContext
+
0x76
ffffae81`
52e29660
fffff800`
0c84177f
: ffffae81`
52e297a0
fffff800`
0c95b72a
ffffc58f`bbbec500
00000000
`
00000000
: nt!KiSwapThread
+
0x17c
ffffae81`
52e29710
fffff800`
0c843547
:
00000000
`
00000000
00000000
`
00000000
00000000
`
00000000
00000000
`
00000000
: nt!KiCommitThreadWait
+
0x14f
ffffae81`
52e297b0
fffff800`
0c8bcd03
: ffffc58f`bc07cf70
00000000
`
00000000
00007fff
`ffff0000
00000000
`
00000000
: nt!KeWaitForSingleObject
+
0x377
ffffae81`
52e29860
fffff800`
0cc7a07d
: ffffc58f`bc07cef0 ffffae81`
52e2994b
ffffc58f`
746c6644
ffffae81`
52e29940
: nt!IopWaitForLockAlertable
+
0x43
ffffae81`
52e298a0
fffff800`
0cc07e38
: ffffc58f`bc07cef0 ffffae81`
52e29b80
00000000
`
0022e180
fffff800`
0c87f4f2
: nt!IopAcquireFileObjectLock
+
0x59
ffffae81`
52e298e0
fffff800`
0cc07286
: ffffc58f`bbefe7c0
00000000
`
00000000
00000000
`
00000000
00000000
`
00000000
: nt!IopXxxControlFile
+
0xba8
ffffae81`
52e29a20
fffff800`
0c95cc93
: ffffc462`
000001d8
ffffc462`
31000000
ffffc462`
31188000
ffff9b7f`
65f8e7e6
: nt!NtDeviceIoControlFile
+
0x56
ffffae81`
52e29a90
00000000
`
5947222c
:
00000000
`
00000000
00000000
`
00000000
00000000
`
00000000
00000000
`
00000000
: nt!KiSystemServiceCopyEnd
+
0x13
(TrapFrame @ ffffae81`
52e29b00
)
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
__int64 __fastcall IopXxxControlFile(......)
{
......
v19
=
(_FILE_OBJECT
*
)
Object
;
v51
=
IopAcquireFileObjectLock(
Object
, v15, v50, v61);
......
}
__int64 __fastcall IopAcquireFileObjectLock(_FILE_OBJECT
*
Object
, __int64 a2, __int64 a3, _BYTE
*
a4)
{
....
do
{
....
v7
=
IopWaitForLockAlertable(&
Object
-
>Lock);
....
}
....
}
NTSTATUS __fastcall IopWaitForLockAlertable(PVOID
Object
, KPROCESSOR_MODE a2, char a3)
{
....
do
{
....
result
=
KeWaitForSingleObject(
Object
, Executive, v7, v6,
0i64
);
}
while
(....);
....
}
|
那么看一下这个_FILE_OBJECT具体是个什么东西,根据调用堆栈可知第一个参数是:ffffc58f`bc07cef0,windbg观察一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
0
: kd> !
object
ffffc58f`bc07cef0
Object
: ffffc58fbc07cef0
Type
: (ffffc58fbb0ccf20)
File
ObjectHeader: ffffc58fbc07cec0 (new version)
HandleCount:
1
PointerCount:
32760
0
: kd> dt _FILE_OBJECT ffffc58f`bc07cef0
ntdll!_FILE_OBJECT
+
0x000
Type
:
0n5
+
0x002
Size :
0n216
+
0x008
DeviceObject :
0xffffc58f
`bc4c9060 _DEVICE_OBJECT
+
0x010
Vpb : (null)
+
0x018
FsContext : (null)
+
0x020
FsContext2 : (null)
+
0x028
SectionObjectPointer : (null)
+
0x030
PrivateCacheMap : (null)
+
0x038
FinalStatus :
0n0
+
0x040
RelatedFileObject : (null)
+
0x048
LockOperation :
0
''
+
0x049
DeletePending :
0
''
+
0x04a
ReadAccess :
0
''
+
0x04b
WriteAccess :
0
''
+
0x04c
DeleteAccess :
0
''
+
0x04d
SharedRead :
0
''
+
0x04e
SharedWrite :
0
''
+
0x04f
SharedDelete :
0
''
+
0x050
Flags :
0x40002
+
0x058
FileName : _UNICODE_STRING ""
+
0x068
CurrentByteOffset : _LARGE_INTEGER
0x0
+
0x070
Waiters :
7
+
0x074
Busy :
1
+
0x078
LastLock : (null)
+
0x080
Lock : _KEVENT
+
0x098
Event : _KEVENT
+
0x0b0
CompletionContext : (null)
+
0x0b8
IrpListLock :
0
+
0x0c0
IrpList : _LIST_ENTRY [
0xffffc58f
`bc07cfb0
-
0xffffc58f
`bc07cfb0 ]
+
0x0d0
FileObjectExtension : (null)
0
: kd> dx
-
id
0
,
0
,ffffc58fbeb24780
-
r1 ((ntdll!_DEVICE_OBJECT
*
)
0xffffc58fbc4c9060
)
((ntdll!_DEVICE_OBJECT
*
)
0xffffc58fbc4c9060
) :
0xffffc58fbc4c9060
: Device
for
"\FileSystem\QAXANTIROOTKIT"
[
Type
: _DEVICE_OBJECT
*
]
[<Raw View>] [
Type
: _DEVICE_OBJECT]
Flags :
0x40
UpperDevices :
None
LowerDevices :
None
Driver :
0xffffc58fbb346950
: Driver
"\FileSystem\QAXANTIROOTKIT"
[
Type
: _DRIVER_OBJECT
*
]
|
找到了一个设备对象ffffc58f`bc4c9060,而这个设备对象为\FileSystem\QAXANTIROOTKIT,这个就是我们上文中提到的constantine64驱动创建的服务。
结合本文一开始给出的程序工作流程伪代码,可知当前的工作流程是:
虽然我们在设计上是多线程的,但是由于对临界资源的使用不当导致最终还是单线程线性工作。其实,在微软的文档里,已经明确告诉我们了。根据CreateFile的API文档,其dwFlagsAndAttributes参数的描述(参考1):
还记得一开始我们是如何创建设备句柄的吗?
1
2
3
|
DeviceHandle
=
CreateFile( deviceStr, GENERIC_READ | GENERIC_WRITE, \
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, \
FILE_ATTRIBUTE_NORMAL, NULL);
|
并没有指定OVERLAPPED,因此所有对这个句柄的请求都会是同步IO,这也是为何内核要获取锁的原因。
通过上文的分析,我们自然想到可以借助OVERLAPPED实现异步IO。但是这样是有代价的——这意味着R3程序和R0程序都需要作出对应修改,且R0程序还需要借助开启额外的内核线程来完成异步请求的处理,这样会导致本来一个功能对应的后台线程数翻倍。
那么,既然我们R3程序已经开了子线程来处理当前子标签页的请求了,R0程序能不能利用这个子线程,在当前子线程的上下文中完成对应的内核部分工作呢?答案当然是肯定的,而且对应修改也非常简单,重写后的程序工作代码(伪)如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
VOID subtab_woker_thread(ULONG_PTR
*
IoControlCode)
{
UCHAR
Buffer
[
256
]
=
{
0
};
ULONG_PTR Length
=
0
;
HANDLE DeviceHandle
=
0
;
LPCSTR deviceStr
=
"\\\\.\\KernelModule"
;
DeviceHandle
=
CreateFile( deviceStr, GENERIC_READ | GENERIC_WRITE, \
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, \
FILE_ATTRIBUTE_NORMAL, NULL);
DeviceIoControl(DeviceHandle,
*
IoControlCode, \
NULL,
0
, &
Buffer
,
256
, &Length, NULL);
/
/
根据Tab标签处理进入不同的业务处理逻辑, 不重要, 省略
switch (Param
-
>IoControlCode)
{
case func1:
....
break
;
....
}
CloseHandle(DeviceHandle);
}
void main()
{
ULONG_PTR Message
=
0
;
while
(GetMessage(&Message, NULL,
0
,
0
) >
0
) {
begin_thread(subtab_woker_thread,
0
, <根据不同消息选择不同IOCTL>);
}
}
|
如各位所见,仅需要简单粗暴的将创建设备对象句柄的代码移到工作线程内使其变成一个局部变量即可。
这里又要暂停一下了,不知道看到这里各位有没有和我一样的疑问,这个疑问关键词是:引用计数。
既然我们的所有子线程都是创建自\\.\KernelModule的句柄,那么内核层面不应该是使\\.\KernelModule文件对象的引用计数不停的增加吗?这样的话,由于当前代码我们依然没有使用OVERLAPPED参数,其创建的文件对象又都是一个,那么依然应该是假的多线程才对啊。
带着这个疑问,我们有必要继续深挖一下内核的工作模式。
CreateFile函数应该是我们日常编程中使用频率最高的函数之一了,因此对他有必要深入理解一下。通过对WRK的研究,我们可以得出如下内核调用路径:
可见最后会调用到OpbLookupObjectName函数,这个函数本身比较复杂,但是其核心却及其简单:从Object Directory中查找对应名称的对象,然后调用对象的ParseProcedure函数用于生成待创建文件对象。由于本文中我们面对的是Device类型的设备,所以我们关注Device类型对象的ParseProcedure函数,而对Device类型对象的创建工作位于IoCreateObjectTypes函数中,在这里我们可以得到系统默认的ParseProcedure函数值:
由上图可知,Device类型对象的ParseProcedure默认函数为IopParseDevice。而通过对WRK中IopParseDevice的研究,其重点在于对ObCreateObject的调用:
简化后的IopParseDevice逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
NTSTATUS IopParseDevice(......, IN OUT PVOID Context OPTIONAL, ......)
{
....
POPEN_PACKET op;
op
=
Context;
realFileObjectRequired
=
!(op
-
>QueryOnly || op
-
>DeleteOnly);
if
(realFileObjectRequired) {
......
status
=
ObCreateObject( KernelMode,
IoFileObjectType,
&objectAttributes,
AccessMode,
(PVOID) NULL,
fileObjectSize,
0
,
0
,
(PVOID
*
) &fileObject );
......
}
......
}
|
可见当 op->QueryOnly 和 op->DeleteOnly 均为FALSE时,realFileObjectRequired为TRUE,此时需要创建对应的文件对象。而根据OPEN_PACKET的定义:
结合重写后的程序逻辑,目前我们打开对象的操作必然不是 查询、删除 操作,因此一定会走到ObCreateObject的逻辑,即当CreateFile的目标为设备(或设备的软连接)时,一定会(查询、删除除外)产生文件对象创建操作。
由于文件对象都是新创建的,因此本节一开始的认知——其创建的文件对象又都是一个——是不正确的,实际上每个子线程调用CreateFile创建的句柄后面都对应一个新创建的同步IO文件对象,由于是新创建的因此不会产生一开始无法获取到锁的问题(因为只有当前子线程在用这个文件对象)。这也是本篇文章“假多线程”解决方案的根据。
在实际编程中,还有一个和句柄相关的API即DuplicateHandle,通过跟踪这个函数的代码,发现从头到尾,都未调用到ObCreateObject函数,而是在进程PspCidTable中大量使用ObReference和ObDereference家族函数增减对象的引用计数:
因此我们可以得出结论DuplicateHandle函数并不会导致新对象的创建,其作用仅仅是在PspCidTable中新增一项并使新增的项目指向内核重已经存在的一个对象。
纵观整个调试、分析过程,其实本文面对问题产生的原因本质上是因为不合理使用临界资源导致的,只不过是本文中涉及到的临界资源比较隐晦,虽然在微软的官方文档中有相关描述,但是在实际使用的过程中依然很容易忽略从而写出不符合预期的代码。
其实可以认为本文是针对同步IO与异步IO(参考2)的一个原理性分析,希望可以帮到大家。
更多【 多线程假象——记一次假多线程场景分析过程】相关视频教程:www.yxfzedu.com