好久没写关于Wdf框架的博客了,因为有各种琐碎事缠身,得赶在RS4 RTM前把状态机(3)系列完成。WdfObject状态机(2)系列将注意力集中在驱动程序的继承层次上只存在单薄一层WdfDriver上;而这篇文章将要分析的情形将具备一定的普遍性:在代码上参考并修改了toaster\kmdf\func\simple。下图是默认KMDF对象层次结构  

Wdf框架之WdfObject状态机(3)-前篇_3c

simple虽然没有包含图中所有元素,但它仍然具有WDFDRIVER/WDFDEVICE/WDFIOREQUEST/WDFQUEUE这些派生自WdfObject的对象并且在继承图上具有父子关系,因此选取它简单而又不失代表性。下面贴出部分代码:

VOID DevEvtCleanupCallback(WDFOBJECT devObj)
{
FDO_DATA* fdoCtx = ToasterFdoGetData(devObj);
fdoCtx->devRef = 2;
}

VOID QueEvtDestroyCallback(WDFOBJECT queObj)
{
QUE_DATA* queCtx = ToasterQueGetData(queObj);
queCtx->queRef = 1;
}

VOID QueEvtCleanupCallback(WDFOBJECT queObj)
{
QUE_DATA* queCtx = ToasterQueGetData(queObj);
queCtx->queRef = 2;
}

NTSTATUS
ToasterEvtDeviceAdd(
IN WDFDRIVER Driver,
IN PWDFDEVICE_INIT DeviceInit
)
{
NTSTATUS status = STATUS_SUCCESS;
PFDO_DATA fdoData;
WDF_IO_QUEUE_CONFIG queueConfig;
WDF_OBJECT_ATTRIBUTES fdoAttributes, queueAttributes;
WDFDEVICE hDevice;
WDFQUEUE queue;

// 为了观察停用设备时WDFQUEUE和WDFDEVICE状态变化,及由状态变化时,调用EvtCleanupCallback/
// EvtDestroyCallback的时机,分别设置WDFDEVICE/WDFQUEUE的属性
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&fdoAttributes, FDO_DATA);
fdoAttributes.EvtCleanupCallback = DevEvtCleanupCallback;
fdoAttributes.EvtDestroyCallback = DevEvtDestroyCallback;

status = WdfDeviceCreate(&DeviceInit, &fdoAttributes, &hDevice);
if (!NT_SUCCESS(status)) {
KdPrint( ("WdfDeviceCreate failed with status code 0x%x\n", status));
return status;
}

fdoData = ToasterFdoGetData(hDevice);

status = WdfDeviceCreateDeviceInterface(
hDevice,
(LPGUID) &GUID_DEVINTERFACE_TOASTER,
NULL // ReferenceString
);
...
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&queueAttributes, QUE_DATA);
queueAttributes.EvtCleanupCallback = QueEvtCleanupCallback;
queueAttributes.EvtDestroyCallback = QueEvtDestroyCallback;

WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&queueConfig, WdfIoQueueDispatchParallel);

queueConfig.EvtIoRead = ToasterEvtIoRead;
queueConfig.EvtIoWrite = ToasterEvtIoWrite;
queueConfig.EvtIoDeviceControl = ToasterEvtIoDeviceControl;

status = WdfIoQueueCreate(
hDevice,
&queueConfig,
&queueAttributes,
&queue
);
...
return status;
}

    为了获得queue和hDevice的句柄值,得在ToasterEvtDeviceAdd函数返回前下断点:

kd> bu wdfsimple!ToasterEvtDeviceAdd ;下延迟断点
kd> g
Breakpoint 0 hit
wdfsimple!ToasterEvtDeviceAdd:
8e5f4010 8bff mov edi,edi
kd> g
Breakpoint 1 hit
wdfsimple!ToasterEvtDeviceAdd+0x13e: ;在AddDevice返回到OS之前下的断点
8e5f414e 837db000 cmp dword ptr [ebp-50h],0
kd> ?? queue
struct WDFQUEUE__ * 0x7f05e218
+0x000 unused : ??
kd> ?? hDevice
struct WDFDEVICE__ * 0x4c3b8890
+0x000 unused : ??

queue和hDevice的句柄值分别为0x7f05e218和0x4c3b8890,我们需要借由这两个句柄值获得对应FxObject对象的地址。这些值在后面的调试过程中会反复用到:

kd> .load wdfkd ;加载wdf调试扩展
kd> !wdfhandle 0x7f05e218 ;查看WDFQUEUE句柄对应FxObject对象信息

Dumping WDFHANDLE 0x7f05e218
=============================
Handle type is WDFQUEUE
Refcount: 1
Contexts:
context: dt 0x80fa1ff8 QUE_DATA (size is 0x4 bytes)
EvtCleanupCallback 8e5f1180 wdfsimple!QueEvtCleanupCallback
EvtDestroyCallback 8e5f1130 wdfsimple!QueEvtDestroyCallback

Parent: !wdfobject 0x80ef96d0, internal type is FxPkgIo ;windbg分析得出WDFQUEUE的父对象是FxpkgIo
Owning device: !wdfdevice 0x4c3b8890

!wdfobject 0x80fa1de0
kd> !wdfhandle 0x4c3b8890 ;查看WDFDEIVCE句柄对应FxObject对象信息

Dumping WDFHANDLE 0x4c3b8890
=============================
Handle type is WDFDEVICE
Refcount: 1
Contexts:
context: dt 0xb3c478f8 FDO_DATA (size is 0x28 bytes)
EvtCleanupCallback 8e5f1100 wdfsimple!DevEvtCleanupCallback
EvtDestroyCallback 8e5f1080 wdfsimple!DevEvtDestroyCallback

Parent: !wdfhandle 0x4cb03cd0, type is WDFDRIVER
Owning device: !wdfdevice 0x4c3b8890

!wdfobject 0xb3c47768

从windbg的输出可以看到,目前WDFDEVICE的父对象是WDFDRIVER,这符合开头部分的继承图;但是WDFQUEUE的父对象竟然不是继承图中的WDFDEIVCE,而是是不在五行之中的FxpkgIo,我只能认为原图作者著书时间与当前WDF框架相去甚远(大概有5年了吧),可能有所出路。但是,这并不影响对父子对象状态变化的研究,所以让我们继续探索。

kd> dt wdf01000!FxObject -y m_ObjectState ;搜索m_ObjectState字段在FxObject结构中的偏移
+0x012 m_ObjectState : Uint2B
kd> ba w 2 0x80fa1de0+0x12 ;为WDFQUEUE对应的FxObject!m_ObjectState设置访问断点
kd> ba w 2 0xb3c47768+0x12 ;为WDFDEVEICE对应的FxObject!m_ObjectState设置访问断点

按照MSDN的说法,在WDF框架中删除父对象时会递归的删除该对象下所有子对象,同时修改对象状态值。为了观察父子对象状态值变动的先后顺序,以及确定调用EvtCleanupCallback/EvtDestroyCallback的时机,我分别为WDFDEVICE/WDFQUEUE对象设置了访问断点。至此,所有准备工作完成,就静静的等待设备停用了~

kd> bl ;列出已设置的断点,后面触发中断时,可以到此索引断点位置
0 e 8e5f4010 [c:\winddk\7600.16385.1\src\general\toaster\kmdf\func\simple\toaster.c @ 155] 0001 (0001) wdfsimple!ToasterEvtDeviceAdd
1 e 8e5f414e [c:\winddk\7600.16385.1\src\general\toaster\kmdf\func\simple\toaster.c @ 256] 0001 (0001) wdfsimple!ToasterEvtDeviceAdd+0x13e
2 e 80fa1df2 w 2 0001 (0001)
3 e b3c4777a w 2 0001 (0001)
4 e 8e5f1086 [c:\winddk\7600.16385.1\src\general\toaster\kmdf\func\simple\toaster.c @ 110] 0001 (0001) wdfsimple!DevEvtDestroyCallback+0x6
5 e 8e5f1106 [c:\winddk\7600.16385.1\src\general\toaster\kmdf\func\simple\toaster.c @ 116] 0001 (0001) wdfsimple!DevEvtCleanupCallback+0x6
6 e 8e5f1136 [c:\winddk\7600.16385.1\src\general\toaster\kmdf\func\simple\toaster.c @ 122] 0001 (0001) wdfsimple!QueEvtDestroyCallback+0x6
7 e 8e5f1186 [c:\winddk\7600.16385.1\src\general\toaster\kmdf\func\simple\toaster.c @ 128] 0001 (0001) wdfsimple!QueEvtCleanupCallback+0x6

另外,根据我的观察,对于win10x86的机器,每次修改FxObject!m_ObjectState值时,OS都使用esi寄存器存放对象首地址,以[esi+0x12]间址寻址方式来读写m_ObjectState字段。如下例:

kd> g
Breakpoint 3 hit
Wdf01000!FxObject::SetObjectStateLocked+0x1f:
8607453f 5e pop esi
kd> r @esi
esi=b3c47768
kd> !wdfobject b3c47768

The type for object 0xb3c47768 is FxDevice
State: FxObjectStateDisposingEarly (0x3)
!wdfhandle 0x4c3b8890

所以,为了行文简洁,在接下去的调试过程中,我直接用寄存器esi作为!wdfobject命令的参数,打印对象状态值。


F5运行调试器,并在设备管理器中停用Toast,windbg马上会发生中断:

kd> g
Breakpoint 3 hit
kd> !wdfobject b3c47768
The type for object 0xb3c47768 is FxDevice
State: FxObjectStateDisposingEarly (0x3)
!wdfhandle 0x4c3b8890
dt FxDevice 0xb3c47768
Contexts:
context: dt 0xb3c478f8 FDO_DATA (size is 0x28 bytes)
EvtCleanupCallback 8e5f1100 wdfsimple!DevEvtCleanupCallback
EvtDestroyCallback 8e5f1080 wdfsimple!DevEvtDestroyCallback
Owning device: !wdfdevice 0x4c3b8890
kd> !wdfhandle 0x4c3b8890
Handle type is WDFDEVICE
Refcount: 1
;以上片段是WDFDEVICE第一次修改状态
kd> g
Breakpoint 3 hit
kd> !wdfobject @esi
The type for object 0xb3c47768 is FxDevice
State: FxObjectStateDisposingDisposeChildren (0x4)
!wdfhandle 0x4c3b8890
dt FxDevice 0xb3c47768
Contexts:
context: dt 0xb3c478f8 FDO_DATA (size is 0x28 bytes)
EvtCleanupCallback 8e5f1100 wdfsimple!DevEvtCleanupCallback
EvtDestroyCallback 8e5f1080 wdfsimple!DevEvtDestroyCallback
Owning device: !wdfdevice 0x4c3b8890
kd> !wdfhandle 0x4c3b8890
Handle type is WDFDEVICE
Refcount: 1
Contexts:
context: dt 0xb3c478f8 FDO_DATA (size is 0x28 bytes)
EvtCleanupCallback 8e5f1100 wdfsimple!DevEvtCleanupCallback
EvtDestroyCallback 8e5f1080 wdfsimple!DevEvtDestroyCallback
Owning device: !wdfdevice 0x4c3b8890

!wdfobject 0xb3c47768
;以上片段是WDFDEIVEC第二次修改状态

停用设备后,作为父对象的WDFDEVICE会先于子对象WDFQUEUE,发生两次状态改变。设备状态从FxObjectStateDisposingEarly跃迁到FxObjectStateDisposingDisposeChildren,这两个对象状态在以往单层设备驱动中从未谋面。 其次对象状态虽有改动,但其句柄的引用计数维持不变,不要错过这个信号,这是代码中是否可以使用句柄的依据。最后,EvtCleanupCallback尚未调用,更别说EvtDestroyCallback回调了。

再次运行并触发断点时,是因为OS尝试修改子对象WDFQUEUE的状态:

kd> g
Breakpoint 2 hit
Wdf01000!FxObject::SetObjectStateLocked+0x1f:
kd> !wdfobject @esi ;FxIoQueue的状态值由FxObjectStateCreate变为FxObjectStateDisposingEarly
The type for object 0x80fa1de0 is FxIoQueue
State: FxObjectStateDisposingEarly (0x3)
!wdfhandle 0x7f05e218
dt FxIoQueue 0x80fa1de0
Contexts:
context: dt 0x80fa1ff8 QUE_DATA (size is 0x4 bytes)
EvtCleanupCallback 8e5f1180 wdfsimple!QueEvtCleanupCallback
EvtDestroyCallback 8e5f1130 wdfsimple!QueEvtDestroyCallback
Parent: !wdfobject 0x80ef96d0
Owning device: !wdfdevice 0x4c3b8890
kd> !wdfhandle 0x7f05e218
Refcount: 1 ;检查当前句柄的引用计数
Contexts:
context: dt 0x80fa1ff8 QUE_DATA (size is 0x4 bytes)
EvtCleanupCallback 8e5f1180 wdfsimple!QueEvtCleanupCallback
EvtDestroyCallback 8e5f1130 wdfsimple!QueEvtDestroyCallback
kd> g
Breakpoint 2 hit
Wdf01000!FxObject::SetObjectStateLocked+0x1f:
kd> !wdfobject @esi ;状态机再次修改FxIoQueue的状态,可以说,前一个状态FxObjectStateDisposingEarly是个短暂的状态
The type for object 0x80fa1de0 is FxIoQueue
State: FxObjectStateDisposingDisposeChildren (0x4)
!wdfhandle 0x7f05e218
dt FxIoQueue 0x80fa1de0
Contexts:
context: dt 0x80fa1ff8 QUE_DATA (size is 0x4 bytes)
EvtCleanupCallback 8e5f1180 wdfsimple!QueEvtCleanupCallback
EvtDestroyCallback 8e5f1130 wdfsimple!QueEvtDestroyCallback
kd> !wdfhandle 0x7f05e218
Handle type is WDFQUEUE
Refcount: 1 ;句柄值依然不变
Contexts:
context: dt 0x80fa1ff8 QUE_DATA (size is 0x4 bytes)
EvtCleanupCallback 8e5f1180 wdfsimple!QueEvtCleanupCallback
EvtDestroyCallback 8e5f1130 wdfsimple!QueEvtDestroyCallback

对比FxDevice和FxIoQueue的状态变化可知,为了删除整棵对象树中的各个对象,根对象在FxObjectStateDisposingDisposeChildren阶段向下遍历所有子对象,每次寻获的子对象又如法炮制的递归上述过程,直到遇到对象树中的离根对象最远的叶对象。那么,遇到叶对象后,会发生怎样的剧情?

kd> g
Breakpoint 7 hit
wdfsimple!QueEvtCleanupCallback+0x6:
kd> g
Breakpoint 2 hit
Wdf01000!FxObject::SetObjectStateLocked+0x1f:
kd> !wdfobject @esi
The type for object 0x80fa1de0 is FxIoQueue
State: FxObjectStateDisposed (0x2)
!wdfhandle 0x7f05e218
dt FxIoQueue 0x80fa1de0
Contexts:
context: dt 0x80fa1ff8 QUE_DATA (size is 0x4 bytes)
EvtDestroyCallback 8e5f1130 wdfsimple!QueEvtDestroyCallback
kd> !wdfhandle 0x7f05e218
Refcount: 1
Contexts:
context: dt 0x80fa1ff8 QUE_DATA (size is 0x4 bytes)
EvtDestroyCallback 8e5f1130 wdfsimple!QueEvtDestroyCallback

FxIoQueue遍历完子对象了后,开始自身的清理工作:调用QueEvtCleanupCallback,而后修改状态FxObjectStateDisposed。如果,把上述过程换个轻松的说法,就是这样:首先对象树上的对象看破红尘,想脱世抛开肉身(调用EvtCleanupCallback做清理工作);无奈涉世太深---子嗣多,所以对外宣布和众子孙断绝关系(改变状态为FxObjectStateDisposingDisposeChildren);最后,成功出家(子嗣都抛弃了,disposed有抛弃的意思。状态修改为FxObjectStateDisposed)。但是,在断绝子孙的过程中,子孙们不理解啊:出家有啥好的,搞不懂,我也要体验一把,结果纷纷效仿。毕竟,青年人入世浅,所以率先于他们的祖宗出世。

在继续下去之前,来做个选择题,预测一下驱动程序的走向:

1).既然调用了QueEvtCleanupCallback,状态机将着手准备调用QueEvtDestroyCallback,清理FxIoQueue对象;

2).状态机回溯到父对象FxDevice,调用DevEvtCleanupCallback,然后修改FxObject!m_ObjectState状态值;

你选了那个选项?到后面看看有没有选对,反正当时我是没选对~

开始公布答案:

kd> g
Breakpoint 5 hit
wdfsimple!DevEvtCleanupCallback+0x6:
kd> !wdfhandle 0x4c3b8890
Handle type is WDFDEVICE
Refcount: 1
Contexts:
context: dt 0xb3c478f8 FDO_DATA (size is 0x28 bytes)
EvtCleanupCallback 8e5f1100 wdfsimple!DevEvtCleanupCallback
EvtDestroyCallback 8e5f1080 wdfsimple!DevEvtDestroyCallback
Owning device: !wdfdevice 0x4c3b8890

!wdfobject 0xb3c47768
d> g
Breakpoint 3 hit
Wdf01000!FxObject::SetObjectStateLocked+0x1f:
kd> !wdfobject @esi
The type for object 0xb3c47768 is FxDevice
State: FxObjectStateDisposed (0x2)
!wdfhandle 0x4c3b8890
dt FxDevice 0xb3c47768
Contexts:
context: dt 0xb3c478f8 FDO_DATA (size is 0x28 bytes)
EvtDestroyCallback 8e5f1080 wdfsimple!DevEvtDestroyCallback
Owning device: !wdfdevice 0x4c3b8890
kd> !wdfhandle 0x4c3b8890
Handle type is WDFDEVICE
Refcount: 1
Contexts:
context: dt 0xb3c478f8 FDO_DATA (size is 0x28 bytes)
EvtDestroyCallback 8e5f1080 wdfsimple!DevEvtDestroyCallback

windbg给出的标准答案是2)。不知道各位有没有选对?按照windbg给出的答案,可以得到这样的结论:删除某个FxObject对象时,状态机将遵循下列顺序:

1).先修改该对象的状态为FxObjectStateDisposingDisposeChildren;

2).接着保持该对象的状态并在该对象的对象树中递归式(由近及远)修改各个子对象的状态;

3).当递归遇到对象树中的叶子节点时,调用EvtCleanupCallback;

4).修改对象状态(不在保持对象状态),使其从FxObjectStateDisposingDisposeChildren变为FxObjectStateDisposed;

5).从子对象向发生删除操作的对象返回,沿途调用EvtCleanupCallback,顺带修改对象状态。

执行完EvtCleanupCallback后,剩下的事就毫无悬念了,肯定就是调用EvtDestroyCallback。状态机按:

1).先修改子对象的状态为FxObjectStateDeletedAndDisposed;

2).调用EvtDestroyCallback;

3).调用FxObject的析构函数,并修改子对象的状态为FxObjectStateDestroyed;

4).回溯到父对象,父对象执行步骤1)-3)。

需要注意的是,和调用EvtCleanupCallback略有不同:父对象并没有先修改状态为FxObjectStateDeletedAndDisposed,然后等待子对象清理完回来后再调用EvtDestroyCallback;而是父对象一直后知后觉的等到子对象返回,才做上述操作。

kd> g
Breakpoint 2 hit
Wdf01000!FxObject::SetObjectStateLocked+0x1f:
kd> !wdfobject @esi
The type for object 0x80fa1de0 is FxIoQueue
State: FxObjectStateDeletedAndDisposed (0xa)
!wdfhandle 0x7f05e218
dt FxIoQueue 0x80fa1de0
Contexts:
context: dt 0x80fa1ff8 QUE_DATA (size is 0x4 bytes)
EvtDestroyCallback 8e5f1130 wdfsimple!QueEvtDestroyCallback
Owning device: !wdfdevice 0x4c3b8890
kd> !wdfhandle 0x7f05e218
Handle type is WDFQUEUE
Refcount: 1
Contexts:
context: dt 0x80fa1ff8 QUE_DATA (size is 0x4 bytes)
EvtDestroyCallback 8e5f1130 wdfsimple!QueEvtDestroyCallback
Owning device: !wdfdevice 0x4c3b8890
!wdfobject 0x80fa1de0
kd> g
Breakpoint 6 hit
wdfsimple!QueEvtDestroyCallback+0x6:
8e5f1136 8b4508 mov eax,dword ptr [ebp+8]
kd> g
Breakpoint 2 hit
Wdf01000!FxObject::~FxObject+0x51:
8607cc21 5e pop esi
kd> !wdfobject @esi
The type for object 0x80fa1de0 is FxIoQueue
State: FxObjectStateDestroyed (0xc)
!wdfhandle 0x7f05e218
dt FxIoQueue 0x80fa1de0
Contexts:
context: dt 0x80fa1ff8 QUE_DATA (size is 0x4 bytes)
<no associated attribute callbacks>
Owning device: !wdfdevice 0x4c3b8890
kd> !wdfhandle 0x7f05e218
Handle type is WDFQUEUE
Refcount: 0
Contexts:
context: dt 0x80fa1ff8 QUE_DATA (size is 0x4 bytes)
<no associated attribute callbacks>
;从这往上是子对象的改变
;从这往下是父对象的改变
kd> g
Breakpoint 3 hit
Wdf01000!FxObject::SetObjectStateLocked+0x1f:
kd> !wdfobject @esi
The type for object 0xb3c47768 is FxDevice
State: FxObjectStateDeletedAndDisposed (0xa)
!wdfhandle 0x4c3b8890
dt FxDevice 0xb3c47768
Contexts:
context: dt 0xb3c478f8 FDO_DATA (size is 0x28 bytes)
EvtDestroyCallback 8e5f1080 wdfsimple!DevEvtDestroyCallback
kd> !wdfhandle 0x4c3b8890
Handle type is WDFDEVICE
Refcount: 1
Contexts:
context: dt 0xb3c478f8 FDO_DATA (size is 0x28 bytes)
EvtDestroyCallback 8e5f1080 wdfsimple!DevEvtDestroyCallback
Owning device: !wdfdevice 0x4c3b8890
kd> g
Breakpoint 4 hit
wdfsimple!DevEvtDestroyCallback+0x6:
kd> g
Breakpoint 3 hit
Wdf01000!FxObject::~FxObject+0x51:
8607cc21 5e pop esi
kd> !wdfobject @esi
The type for object 0xb3c47768 is FxDevice
State: FxObjectStateDestroyed (0xc)
!wdfhandle 0x4c3b8890
dt FxDevice 0xb3c47768
Contexts:
context: dt 0xb3c478f8 FDO_DATA (size is 0x28 bytes)
<no associated attribute callbacks>
Owning device: !wdfdevice 0x4c3b8890
kd> !wdfhandle 0x4c3b8890
Handle type is WDFDEVICE
Refcount: 0
Contexts:
context: dt 0xb3c478f8 FDO_DATA (size is 0x28 bytes)
<no associated attribute callbacks>
Owning device: !wdfdevice 0x4c3b8890

!wdfobject 0xb3c47768