应用程序如何使用驱动程序

 应用程序中使用 CreateFile,ReadFile,WriteFile,DeviceIoControl,CloseHandle 来指示驱动程序完成某种任务。比如我们在应用程序中使用 ReadFile 来让驱动读取硬件设备,我们在应用程序中使用 WriteFile 来让驱动写硬件设备,我们在应用程序中使用 DeviceIoContorl 来让驱动完成某些驱动支持的功能。而 ReadFile, WriteFile, DeviceIoControl 这三个 api 都需要一个句柄作为参数,以确定他们是要哪个驱动来完成他们的请求。这个句柄是通过 CreateFile 获得的。使用 CloseHandle 关闭这个句柄。简单的说就是,应用程序中,首先要通过 CreateFile 获得一个句柄,之后应用程序可以以这个句柄为参数,使用 ReadFile,WriteFile,DeviceIoControl 让驱动程序执行某种操作。当不再使用时,通过 CloseHandle 关闭这个句柄。

这几个 api 都位于 KERNEL32.DLL 中,他们最终会通过系统服务(int 2e)调用内核中的相应的函数,如 NtCreateFile,NtReadFile 等。而 NtCreateFile,NtReadFile 等函数中,会创建一个 IRP,并用传入的参数初始化这个 IRP,然后将这个 IRP 发给驱动,让驱动做处理。相应的 NtCreateFile 产生 IRP_MJ_CREATE 的 IRP ,NtReadFile 产生 IRP_MJ_READ 的 IRP。驱动得到这些 IRP ,根据情况做处理,对于 IRP_MJ_READ ,会调用驱动中处理 IRP_MJ_READ 的部分,可能最后引起读硬件的操作。


 获得指定驱动的句柄

对于希望被应用程序使用的驱动,会在初始化的过程中,把能找到它设备对象的一个 SymbolicLink 放在对象管理器命名空间(Object Manager Namespace)的 /??/ 下。这样用 "////.//那个SymbolicLink的名字" 作为 CreateFile 的 lpFileName 参数,调用 CreateFile ,得到的句柄就可以找到相应的驱动的那个设备对象(//./ 会被转换成 /??/)。之后以这个句柄为参数使用 ReadFile,WriteFile,DeviceIoControl,产生的 IRP 就被发到相应的设备对象。也就是说只要驱动把 设备对象的 SymbolicLink 放在 /??/ 下,并且应用程序知道这个 SymbolicLink 的名字,就可以使用 CreateFile 得到相应的句柄。

HANDLE CreateFile(
LPCTSTR lpFileName, // file name
DWORD dwDesiredAccess, // access mode
DWORD dwShareMode, // share mode
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // SD
DWORD dwCreationDisposition, // how to create
DWORD dwFlagsAndAttributes, // file attributes
HANDLE hTemplateFile // handle to template file
);


可以使用工具 WinObj 来查看 对象管理器命名空间(Object Manager Namespace) 。WinObj 可以从 http://www.sysinternals.com 获得。关于内核对象和命名地址空间的详细介绍,可以参考《 JIURL玩玩Win2k 对象 》,这篇文章可以在我的主页上找到。

在驱动的初始化过程中,会通过调用 IoCreateDevice 创建设备对象,可以指定一个设备名作为IoCreateDevice 的参数(也可以不指定,那样这个设备对象就没有名字)。这样这个设备对象会被放在 对象管理器命名空间(Object Manager Namespace)的 /Device/ 下。不过应用程序只能访问命名空间的 /??/ ,所以如果驱动希望把设备对象暴露给应用程序的话,会为设备对象创建一个 SymbolicLink 放在 /??/ 下。对于放在 /Device/ 下的有名字的设备,其他驱动程序如果知道它的名字,就可以使用 IoGetDeviceObjectPointer 得到这个设备对象的指针。

驱动可以通过 IoCreateSymbolicLink ,在 /??/ 下建立设备对象的 SymbolicLink 。这样应用程序必须要也知道该 SymbolicLink 的名字,然后就可以以这个符号链接名做参数使用 CreateFile ,得到句柄。从 IoCreateSymbolicLink 的参数,我们可以知道,只能使用 IoCreateSymbolicLink 为有名字的设备对象建立 SymbolicLink。

另一种方法是,使用一个 GUID 来标识一个设备接口。驱动使用标识设备的 GUID 做参数调用IoRegisterDeviceInterface ,然后使用 IoSetDeviceInterfaceState ,就会为设备对象在 /??/ 下产生一个符号链接(SymbolicLink)。应用程序使用同一个 GUID 做参数,使用API: SetupDiGetClassDevs, SetupDiEnumDeviceInterfaces, SetupDiGetDeviceInterfaceDetail 就可以得到创建的 /??/ 下的符号链接名,就可以以这个符号链接名做参数使用 CreateFile ,得到句柄。

句柄简介。每个进程都有一个自己的句柄表,句柄表中放着内核对象的指针,句柄就是内核对象在句柄表中的索引。通过句柄就可以在进程的句柄表中找到对应的内核对象的指针。关于句柄的详细介绍,可以参考 《 JIURL玩玩Win2k 进程线程篇 HANDLE_TABLE 》,这篇文章可以在我的主页上找到。

 一些结论

SymbolicLink 对象可以找到相应的 设备对象。SymbolicLink 对象中保存着相应的 设备对象的地址。设备对象不保存它的 SymbolicLink 对象的任何信息。SymbolicLink 对象的地址保存在对象管理器命名空间(Object Manager Namespace)中。也就是说只要知道 SymbolicLink 的名字,就可以在对象管理器命名空间中找到它。应用程序 CreateFile 得到的句柄,通过这个句柄在进程的句柄表中找到的是一个文件对象(File Object)。文件对象 对应的 设备对象 中不保存这个文件对象的任何信息。对应的 SymbolicLink 对象中也不保存这个文件对象的任何信息。这个文件对象的地址,保存在应用程序的句柄表中,应用程序通过句柄可以找到这个文件对象。这个 文件对象 中保存着对应的 设备对象 的地址。可以猜到,应用程序在用 CreateFile 创建的时候,会根据参数中的 SymolicLink 名字,找到 SymolicLink 对象,进而找到该对象中保存的 设备对象 的地址,然后直接把找到的 设备对象 的地址保存在文件对象中。文件对象的 +04 struct _DEVICE_OBJECT *DeviceObject 处,保存着对应的设备对象的地址。

对于需要暴露接口给应用程序的驱动。首先,驱动中需要在 对象管理器命名空间的 /??/ 下,为设备对象建立一个 SymbolicLink ,不管采取何种方式。之后,应用程序要知道这个 SymbolicLink 的名字,不管采取何种方式。然后应用程序以 "////.//那个SymbolicLink的名字" 为参数使用 CreateFile 得到一个句柄。这样,之后的 DeviceIoControl(),WriteFile(),ReadFile() 使用前面用 CreateFile 得到的句柄作为参数,他们可以通过这个句柄,找到对应的文件对象,而这个文件对象中保存有对应的 设备对象 的指针,这样就可以将 IRP 发到这些设备。

上面的结论是通过对一个小例子进行观察得到的。

 IRP 将被发往设备栈的栈顶

IRP 将无论如何被发往设备栈的顶。CreateFile,ReadFile,WriteFile,DeviceIoControl,CloseHandle。他们最终都会产生一个 IRP,发给一个设备对象。对于 CreateFile 来说通过 SymbolicLink的名字 来找到一个设备对象。对于其他的几个函数,通过句柄,找到一个文件对象,文件对象中保存有设备对象的指针。不过产生的 IRP 并不一定发给找到的这个设备对象,而是发给找到的这个设备对象所在设备栈的最顶上的一个设备对象。

而且通常我们传给 CreateFile,ReadFile,WriteFile,DeviceIoControl,CloseHandle 的参数所找到的那个设备对象,会是它所在设备栈的 PDO(也就是它所在设备栈最底下的一个设备对象)。CreateFile, ReadFile, WriteFile, DeviceIoControl, CloseHandle 会首先通过找到的这个设备对象,获得它所在设备栈中最顶端的那个设备对象,然后将 IRP 发向设备栈的最顶端的那个设备对象。所以不管我们通过参数找到的设备对象在它所在的设备栈中处于什么位置,顶端,中间,底下,不管处在什么位置,IRP 都会发往这个设备栈的栈顶。

上面的内容是通过跟踪 nt!NtCreateFile 和 nt!NtReadFile 发现的。

 nt!NtCreateFile简介

我们简单介绍一下 nt!NtCreateFile。

通过一系列的调用 nt!NtCreateFile 最终会调用 nt!IopParseDevice。下面的 call stack 显示了这个调用过程。

...
nt!IopParseDevice+0xa04
nt!ObpLookupObjectName+0x4c4
nt!ObOpenObjectByName+0xc5
nt!IoCreateFile+0x3ec
nt!NtCreateFile+0x2e
nt!KiSystemService+0xc4
...

在 nt!IopParseDevice 中

调用 nt!IoGetAttachedDevice ,获得设备栈最顶端的设备对象。调用 IoAllocateIrp 创建 IRP。调用 nt!ObCreateObject 创建文件对象。初始化这个文件对象。该文件对象的 +04 struct _DEVICE_OBJECT *DeviceObject 赋值为通过传入参数找到的那个设备对象。调用 nt!IopfCallDriver,也就是 IoCallDriver,将 IRP 发给设备栈的栈顶。

驱动处理完这个 IRP 之后,返回 nt!IopParseDevice 继续执行。nt!IopParseDevice 一路返回到 nt!ObOpenObjectByName。在 nt!ObOpenObjectByName 中继续执行,调用 nt!ObpCreateHandle 在进程的句柄表中创建一个新的句柄,这个句柄对应的对象是刚才创建初始化的那个文件对象。

 nt!NtReadFile简介

我们简单介绍一下 nt!NtReadFile。传入参数中有前面 CreateFile 打开的句柄,通过句柄可以在进程句柄表中找到一个文件对象,在这个文件对象中保存有一个设备对象的指针。调用 IoGetRelatedDeviceObject 获得这个设备对象所在设备栈栈顶的设备对象。调用 IoAllocateIrp 创建 IRP。初始化这个 IRP ,并根据传入的参数,设置好这个 IRP。然后调用 IoCallDriver 将这个 IRP 发给设备对象,让驱动进行处理。发往的这个设备对象就是前面使用 IoGetRelatedDeviceObject 所得到的设备栈栈顶的设备对象。下面的 call stack 显示了这个调用过程。

...
nt!IopfCallDriver+0x35
nt!IopSynchronousServiceTail+0x60
nt!NtReadFile+0x5f4
nt!KiSystemService+0xc4
...

 键盘驱动的应用层

哪一个应用程序在使用键盘驱动?它是如何使用键盘驱动的?这是讨论键盘驱动肯定要遇到的问题,我们现在就来简单的讨论它。

3.1 键盘驱动的使用者

键盘驱动的使用者是线程 win32k!RawInputThread 。线程 win32k!RawInputThread 的进程是 csrss.exe。

我最早是通过 WinDbg 的 !irpfind 命令看到了这一点。后来看键盘驱动时,观察kbdclass!KeyboardClassRead,kbdclass!KeyboardClassCreate 的 call stack 也看到了这一点。

kbdclass!KeyboardClassCreate 是,键盘设备栈最顶端的设备对象的驱动中处理 IRP_MJ_CREATE 的函数。所以当有人使用 CreateFile 来打开键盘设备栈上的某个设备对象的句柄的时候,CreateFile 最终会发一个 IRP_MJ_CREATE 的 IRP 给键盘设备栈最顶端的设备对象,这将导致 kbdclass!KeyboardClassCreate 被调用。于是我们在这个函数上下断点,看看是谁引起了这个函数的调用。看看是谁要得到键盘的句柄。

在系统初始化的末期,在 kbdclass!KeyboardClassCreate 上发生了打断,进入调试器。首先我们看看这时的当前线程是谁。

kd> !thread
THREAD fe42e5e0 Cid a0.bc Teb: 00000000 Win32Thread: e194a9e8 RUNNING
IRP List:
fe43e9a8: (0006,0148) Flags: 00000884 Mdl: 00000000
Not impersonating
Owning Process fe43b760
Wait Start TickCount 5168 Elapsed Ticks: 0
Context Switch Count 9
UserTime 0:00:00.0000
KernelTime 0:00:00.0250
Start Address win32k!RawInputThread (0xa000e7cd)
Stack Init f90f0000 Current f90ef864 Base f90f0000 Limit f90ed000 Call 0
Priority 19 BasePriority 13 PriorityDecrement 0 DecrementCount 0

ChildEBP RetAddr Args to Child
f90ef608 8041f54b fe4f5df0 fe43e9a8 fe43e9b8 kbdclass!KeyboardClassCreate
f90ef61c 804a3e54 804a392a fe4dd718 f90ef90c nt!IopfCallDriver+0x35
f90ef7a4 8044e27e fe4dd730 00000000 f90ef850 nt!IopParseDevice+0xa04
f90ef810 804957ae 00000000 f90ef900 00000000 nt!ObpLookupObjectName+0x4c4
f90ef920 804a78b8 00000000 00000000 e18f5900 nt!ObOpenObjectByName+0xc5
f90ef9f4 804a0c5b e197101c 00100001 f90efb14 nt!IoCreateFile+0x3ec
f90efa34 80461691 e197101c 00100001 f90efb14 nt!NtCreateFile+0x2e
f90efa34 804009d1 e197101c 00100001 f90efb14 nt!KiSystemService+0xc4
f90efad8 a000e304 e197101c 00100001 f90efb14 nt!ZwCreateFile+0xb
f90efb2c a000e192 e1971008 80400b46 00000001 win32k!OpenDevice+0x8e
f90efb58 a000eb74 00000001 00000000 00000000 win32k!ProcessDeviceChanges+0x92
f90efda8 804524f6 00000003 00000000 00000000 win32k!RawInputThread+0x463
f90efddc 80465b62 a000e7cd f8d5f7d0 00000000 nt!PspSystemThreadStartup+0x69
00000000 f000ff53 f000e2c3 f000ff53 f000ff53 nt!KiThreadStartup+0x16
f000ff53 00000000 00000000 00000000 00000000 +0xf000ff53

看到 Start Address 为 win32k!RawInputThread。说明线程 win32k!RawInputThread 在通过 CreateFile 来获得键盘的句柄。

看到 Cid 为 a0.bc 。说明线程的进程为 a0。

我们看看 a0 进程是谁。

kd> !process a0 0
Searching for Process with Cid == a0
PROCESS fe43b760 SessionId: 0 Cid: 00a0 Peb: 7ffdf000 ParentCid: 0090
DirBase: 03642000 ObjectTable: fe43b6c8 TableSize: 53.
Image: csrss.exe

看到 a0 进程的 Image 为 csrss.exe。

kbdclass!KeyboardClassRead 是,键盘设备栈最顶端的设备对象的驱动中处理 IRP_MJ_READ 的函数。所以当有人使用 ReadFile 来要求读入数据的时候,ReadFile 最终会发一个 IRP_MJ_Read 的 IRP 给键盘设备栈最顶端的设备对象,这将导致 kbdclass!KeyboardClassRead 被调用。于是我们在这个函数上下断点,看看是谁引起了这个函数的调用。看看是谁要求从键盘读入数据。

在 kbdclass!KeyboardClassCreate 上发生打断后,进入调试器。我们看看这时的当前线程是谁。

kd> !thread
THREAD fe42e5e0 Cid a0.bc Teb: 00000000 Win32Thread: e194a9e8 RUNNING
...
Start Address win32k!RawInputThread (0xa000e7cd)
...

看到 Start Address 为 win32k!RawInputThread。说明线程 win32k!RawInputThread 在通过 ReadFile 来要求从键盘读取数据。

看到 Cid 为 a0.bc 。说明线程的进程还是 a0。

这些足以说明键盘驱动的使用者是线程 win32k!RawInputThread 。线程 win32k!RawInputThread 的进程是 csrss.exe。

 win32k!RawInputThread 获得句柄简介

win32k!RawInputThread 会调用 nt!ZwCreateFile ,获得一个可以找到键盘设备栈的 PDO 的句柄,供以后的 ZwReadFile,ZwDeviceIoControlFile 等使用。

首先我们看看断在 kbdclass!KeyboardClassCreate 时的 call stack ,看看引起 kbdclass!KeyboardClassCreate 的整个调用过程。

# ChildEBP RetAddr Args to Child
00 f90ef608 8041f54b fe4f5df0 fe43e9a8 fe43e9b8 kbdclass!KeyboardClassCreate(struct _DEVICE_OBJECT * DeviceObject = 0xfe4f5df0, struct _IRP * Irp = 0xfe43e9a8) (CONV: stdcall)
01 f90ef61c 804a3e54 804a392a fe4dd718 f90ef90c nt!IopfCallDriver+0x35 (FPO: [0,0,2])
02 f90ef7a4 8044e27e fe4dd730 00000000 f90ef850 nt!IopParseDevice+0xa04 (FPO: [Non-Fpo])
03 f90ef810 804957ae 00000000 f90ef900 00000000 nt!ObpLookupObjectName+0x4c4 (FPO: [Non-Fpo])
04 f90ef920 804a78b8 00000000 00000000 e18f5900 nt!ObOpenObjectByName+0xc5 (FPO: [Non-Fpo])
05 f90ef9f4 804a0c5b e197101c 00100001 f90efb14 nt!IoCreateFile+0x3ec (FPO: [Non-Fpo])
06 f90efa34 80461691 e197101c 00100001 f90efb14 nt!NtCreateFile+0x2e (FPO: [Non-Fpo])
07 f90efa34 804009d1 e197101c 00100001 f90efb14 nt!KiSystemService+0xc4 (FPO: [0,0] TrapFrame @ f90efa68)
08 f90efad8 a000e304 e197101c 00100001 f90efb14 nt!ZwCreateFile+0xb (FPO: [11,0,0])
09 f90efb2c a000e192 e1971008 80400b46 00000001 win32k!OpenDevice+0x8e (FPO: [Non-Fpo])
0a f90efb58 a000eb74 00000001 00000000 00000000 win32k!ProcessDeviceChanges+0x92 (FPO: [EBP 0xf90efda8] [1,5,4])
0b f90efda8 804524f6 00000003 00000000 00000000 win32k!RawInputThread+0x463 (FPO: [Non-Fpo])
0c f90efddc 80465b62 a000e7cd f8d5f7d0 00000000 nt!PspSystemThreadStartup+0x69 (FPO: [Non-Fpo])
0d 00000000 f000ff53 f000e2c3 f000ff53 f000ff53 nt!KiThreadStartup+0x16
WARNING: Frame IP not in any known module. Following frames may be wrong.
0e f000ff53 00000000 00000000 00000000 00000000 0xf000ff53


我简单的跟了一下 win32k!RawInputThread 获得句柄的过程,下面我对这个过程做一个简单的介绍。

win32k!RawInputThread 通过 GUID_CLASS_KEYBOARD 获得键盘设备栈中的 PDO (简单的说 PDO 是设备栈最下面的那个设备对象)的 SymbolicLink(符号链接)名。执行到 win32k!OpenDevice,它的一个参数可以找到 键盘设备栈的 PDO 的符号链接(SymbolicLink)名。win32k!OpenDevice 有一个 OBJECT_ATTRIBUTES 结构的局部变量,它自己初始化这个局部变量,用传入参数中的键盘设备栈的 PDO 的符号链接(SymbolicLink)名 赋值这个 OBJECT_ATTRIBUTES +0x8 处的 PUNICODE_STRING ObjectName 。然后调用 ZwCreateFile。ZwCreateFile 完成得到句柄的工作,最后通过传入的参数返回得到的句柄。win32k!RawInputThread 把得到的句柄保存起来,供后面的 ReadFile, DeviceIoControl等使用。

ZwCreateFile 通过系统服务,调用内核中的 NtCreateFile。NtCreateFile 执行到 nt!IopParseDevice 中 ,
调用 nt!IoGetAttachedDevice ,通过 PDO 的设备对象获得键盘设备栈最顶端的设备对象。用得到的这个设备对象的 +30 char StackSize 作为参数来 IoAllocateIrp,创建 IRP。调用 nt!ObCreateObject 创建文件对象。初始化这个文件对象,+04 struct _DEVICE_OBJECT *DeviceObject 赋值为键盘设备栈的 PDO。调用 nt!IopfCallDriver,将 IRP 发往驱动,让驱动进行相应的处理。之后一系列返回,回到 nt!ObOpenObjectByName。在 nt!ObOpenObjectByName 中继续执行,调用 nt!ObpCreateHandle 在进程(csrss.exe)的句柄表中创建一个新的句柄,这个句柄对应的对象是刚才创建初始化的那个文件对象,文件对象中的 DeviceObject 指向键盘设备栈的 PDO。在 nt!ObpCreateHandle 前后,我们使用命令 !handle 0 3 a0 (a0 为此时 csrss.exe进程的进程id),观察 csrss.exe进程 句柄表的前后变化,看到了多出来的那一个文件对象。

 win32k!RawInputThread 如何从键盘驱动得到按键的数据

win32k!RawInputThread 在获得了句柄之后,会以这个句柄为参数,调用 nt!ZwReadFile,向键盘驱动要求读入数据。nt!ZwReadFile 中会创建一个 IRP_MJ_READ 的 IRP 发给键盘驱动,告诉键盘驱动要求读入数据。键盘驱动通常会使这个 IRP Pending (通常情况下是这样,详细的情况我们在键盘驱动部分讨论)。也就是说这个 IRP_MJ_READ 不会被满足,它会一直被放在那里,等待着来自键盘的数据。而发出这个读请求的线程 win32k!RawInputThread 也会等待,等待着这个读操作的完成。

命中注定,这个 IRP 匆匆的出现,然后用它一生中绝大部分时间,开始一个静静的等待,而当它等到的时候,它就会匆匆的消失。它的一生或许很短,或许很长,取决于它所等待着的出现。它在平静的等待着什么呢?

它在等待着你,按下键盘上的键。我们来说明一下键盘数据的源头,键盘数据的源头就是键盘,当键盘上有键被按下时,就产生了那个 IRP_MJ_READ IRP 等待着的对象。

当键盘上有键被按下时,将触发键盘的那个中断,引起中断服务例程的执行,键盘中断的中断服务例程由键盘驱动提供。键盘驱动从端口读取扫描码,经过一些列的处理之后,最后把从键盘得到的数据交给 IRP,然后结束这个 IRP。

这个 IRP 的结束,将导致 win32k!RawInputThread 线程对这个读操作的等待结束。win32k!RawInputThread 线程将会对得到的数据作出处理,分发给合适的进程。一旦把输入数据处理完之后,win32k!RawInputThread 线程会立刻再调用一个 nt!ZwReadFile,向键盘驱动要求读入数据。于是又开始一个等待,等待着键盘上的键被按下。

简单的说,win32k!RawInputThread 线程总是 nt!ZwReadFile 要求读入数据。然后等待键盘上的键被按下。当键盘上的键被按下,win32k!RawInputThread 处理 nt!ZwReadFile 得到的数据,然后再 nt!ZwReadFile 要求读入数据,再等待键盘上的键被按下。

上面所介绍的内容,是当我在看到键盘驱动对于 IRP_MJ_READ 的处理,在 kbdclass!KeyboardClassRead 中,IRP 并没有获得数据,而是被 IoMarkIrpPending 时,想了想,了解到的。

我简单的跟了一下 win32k!RawInputThread 从键盘驱动获得按键数据的过程,下面我对这个过程做一个简单的介绍。

首先我们看看断在 kbdclass!KeyboardClassRead 时的 call stack ,看看引起 kbdclass!KeyboardClassRead 的整个调用过程。

# ChildEBP RetAddr Args to Child
00 f90ef8dc 8041f54b fe4f5df0 fe43e9a8 fe43e9a8 kbdclass!KeyboardClassRead(struct _DEVICE_OBJECT * Device = 0xfe4f5df0, struct _IRP * Irp = 0xfe43e9a8) (CONV: stdcall)
01 f90ef8f0 804ba5e8 fe43eacc fe43e9a8 00000000 nt!IopfCallDriver+0x35 (FPO: [0,0,2])
02 f90ef904 804a2d4c fe4f5df0 fe43e9a8 fe42d668 nt!IopSynchronousServiceTail+0x60 (FPO: [Non-Fpo])
03 f90ef9d8 80461691 000000d4 00000000 a005c962 nt!NtReadFile+0x5f4 (FPO: [Non-Fpo])
04 f90ef9d8 804011d5 000000d4 00000000 a005c962 nt!KiSystemService+0xc4 (FPO: [0,0] TrapFrame @ f90efa04)
05 f90efa74 a005c91d 000000d4 00000000 a005c962 nt!ZwReadFile+0xb (FPO: [9,0,0])
06 f90efaa8 a005c991 e1971008 fe43e9e8 80430982 win32k!StartDeviceRead+0x8c (FPO: [1,0,3])
07 f90efab4 80430982 e1971008 e1971028 00000000 win32k!InputApc+0x41 (FPO: [3,0,1])
08 f90efae8 80403a44 00000000 00000000 00000000 nt!KiDeliverApc+0xdb (FPO: [Non-Fpo])
09 f90efb08 8042d33d 80400b46 00000001 00000000 nt!KiSwapThread+0xfc (FPO: [EBP 0xf90efb3c] [0,0,4])
0a f90efb3c a000eaf5 00000004 fe42e5a8 00000001 nt!KeWaitForMultipleObjects+0x266 (FPO: [Non-Fpo])
0b f90efda8 804524f6 00000002 00000000 00000000 win32k!RawInputThread+0x3c2 (FPO: [Non-Fpo])
0c f9dc 80465b62 a000e7cd f8d5f7d0 00000000 nt!PspSystemThreadStartup+0x69 (FPO: [Non-Fpo])
0d 00000000 f000ff53 f000e2c3 f000ff53 f000ff53 nt!KiThreadStartup+0x16
WARNING: Frame IP not in any known module. Following frames may be wrong.
0e f000ff53 00000000 00000000 00000000 00000000 0xf000ff53

线程 win32k!RawInputThread 调用 nt!ZwReadFile 要求读入数据。

NTSTATUS
ZwReadFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID Buffer,
IN ULONG Length,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN PULONG Key OPTIONAL
);

我们看到调用时,参数 FileHandle 正是前面 win32k!RawInputThread 用 ZwCreateFile 得到的句柄。而参数 ApcRoutine 为 win32k!InputApc 的入口地址。也就是说当 ReadFile 的 IRP 结束时,win32k!InputApc 将被调用。ZwReadFile 通过系统服务,最终调用 nt!NtReadFile。

nt!NtReadFile 中。作为参数传入的那个句柄,对应着一个文件对象,这个文件对象中保存着键盘设备栈中的 PDO 的设备对象的指针。以这个句柄为参数调用 ObReferenceObjectByHandle,获得句柄对应的文件对象。之后用得到的文件对象做参数调用 IoGetRelatedDeviceObject ,这将获得键盘设备栈中最顶端的设备对象的指针。用得到的键盘设备栈中最顶端的设备对象的 +30 char StackSize 作参数,调用 nt!IoAllocateIrp,构造 IRP ,然后初始化这个 IRP,用传入的参数设置这个 IRP。然后调用 IoCallDriver ,将这个 IRP 发给键盘驱动。

键盘驱动通常会调用 IoMarkIrpPending 使这个 IRP Pending。通常情况下是这样,详细的情况我们在键盘驱动部分讨论。于是这个 IRP 就在那里等待。关于这个等待的 IRP ,我们可以使用 WinDbg 的 !irpfind 命令找到它。反过来这也解释了,我们使用 !irpfind 为什么总能看到一个 pending 的发给键盘设备栈栈顶的 IRP_MJ_READ 的 IRP。

kd> !irpfind
unable to get large pool allocation table - either wrong symbols or pool tagging is disabled

Searching NonPaged pool (fe313000 : fe52b000) for Tag: Irp?

Irp [ Thread ] irpStack: (Mj,Mn) DevObj [Driver]
...
fe439008 [fe427940] irpStack: ( 3, 0) fe4f5df0 [ /Driver/Kbdclass]
...

这个 IRP 的地址为 fe439008 ,我们看看它的详细情况

kd> !irp fe439008
Irp is active with 6 stacks 6 is current (= 0xfe43912c)
No Mdl System buffer = fe426568 Thread fe427940: Irp stack trace.
cmd flg cl Device File Completion-Context
[ 0, 0] 0 0 00000000 00000000 00000000-00000000

Args: 00000000 00000000 00000000 00000000
[ 0, 0] 0 0 00000000 00000000 00000000-00000000

Args: 00000000 00000000 00000000 00000000
[ 0, 0] 0 0 00000000 00000000 00000000-00000000

Args: 00000000 00000000 00000000 00000000
[ 0, 0] 0 0 00000000 00000000 00000000-00000000

Args: 00000000 00000000 00000000 00000000
[ 0, 0] 0 0 00000000 00000000 00000000-00000000

Args: 00000000 00000000 00000000 00000000
>[ 3, 0] 0 1 fe4f5df0 fe426608 00000000-00000000 pending
/Driver/Kbdclass
Args: 00000078 00000000 00000000 00000000
看到了这个 IRP pending。

这时 线程 win32k!RawInputThread 会在一个 nt!KeWaitForMultipleObjects 上等待,等待的对象之一就是,希望从键盘驱动中读数据的那个 IRP 。

当键盘上有键被按下时,引发中断,导致驱动从端口读取按键的扫描码。驱动经过一系列处理,最后调用 IoCompleteRequest 结束那个等待着的 IRP。

IRP 的结束会使得 线程 win32k!RawInputThread 在 nt!KeWaitForMultipleObjects 上对从键盘读取数据的等待的结束。这将使得前面 ZwReadFile 的传入参数 ApcRoutine 即 win32k!InputApc 被执行。

win32k!InputApc 中。调用两个函数,win32k!ProcessKeyboardInput,win32k!StartDeviceRead。win32k!ProcessKeyboardInput 负责处理刚才读到的输入数据,比如分发给应该得到这个键盘按键的进程。数据处理完之后,也就是 win32k!ProcessKeyboardInput 结束之后。win32k!StartDeviceRead 被调用,win32k!StartDeviceRead 会调用 nt!ZwReadFile 要求读入数据。

补充

win32k 实际是一个驱动程序,不属于应用程序,所以把 win32k!RawInputThread 叫做键盘驱动的使用层或许更合适。至于 win32k!RawInputThread 如何把得到的键盘上的按键分发给各个进程,我们不研究。曾经使我奇怪的是,为什么 ZwCreateFile 的参数,能找到的设备对象是键盘设备栈的 PDO,而 ZwCreateFile 产生的 IRP 却是发给键盘设备栈的栈顶。为什么 ZwReadFile 句柄所找到的设备对象是键盘设备栈的 PDO,而 ZwReadFile 产生的 IRP 却是发给键盘设备栈最顶端的设备对象。后来跟踪 NtCreateFile,NtReadFile 找到了原因。从中我们也可以看出,CreateFile,ReadFile,WriteFile,DeviceIoControl,CloseHandle 产生的 IRP 都是发给设备栈的栈顶的,然后 IRP 在设备栈上自上而下。

你可能感兴趣的:(thread,c,object,struct,manager,attributes)