JIURL键盘驱动 2

JIURL键盘驱动 3 JIURL键盘驱动 3作者: JIURL                 主页: http://jiurl.yeah.net     日期: 2003-12-13 4 编译与调试环境简介4.1 源码    ps/2键盘驱动的设备栈有3层,最底层设备对象的驱动是 acpi,中间层设备对 阅读详情

JIURL键盘驱动 2

作者: JIURL

                主页: http://jiurl.yeah.net

    日期: 2003-12-13


2 应用层基础知识

    在讨论使用键盘的应用程序这个问题之前,我们首先介绍一下 Windows 中,应用程序使用驱动,应用程序与驱动通信的一些问题。

2.1 应用程序如何使用驱动

    应用程序中使用 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 的部分,可能最后引起读硬件的操作。


2.2 获得指定驱动的句柄

    对于希望被应用程序使用的驱动,会在初始化的过程中,把能找到它设备对象的一个 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 》,这篇文章可以在我的主页上找到。

2.3 一些结论

    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 发到这些设备。 

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

2.4 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 发现的。

2.5 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 在进程的句柄表中创建一个新的句柄,这个句柄对应的对象是刚才创建初始化的那个文件对象。

2.6 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 键盘驱动的应用层

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

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。

3.2 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进程 句柄表的前后变化,看到了多出来的那一个文件对象。

3.3 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 要求读入数据。

3.4 补充

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


欢迎交流,欢迎交朋友,
欢迎访问
主页 http://jiurl.yeah.net http://jiurl.nease.net 论坛 http://jiurl.cosoft.org.cn/forum

f啊k,不带你们这样的啊,有好事不叫我。

 

 

JIURL品牌键盘驱动程序详解与安装指南 传统驱动开发往往把所有参数写死在代码里:轮询率是多少?哪些键能重映射?背光模式有哪些?一旦要改就得重新编译。但 JIURL 的做法完全不同——它采用“驱动核心 + 外部配置”的分离架构。这意味着:✅ 同一份.ko或.sys文件,可以通过加载不同的.ini或.xml文件,变成办公版、游戏版、紧凑60%版……✅ 用户无需更新驱动即可自定义按键、设置宏、调整性能参数。✅ 厂商发布新机型时,只要提供新的配置文件,就能快速上线支持。 阅读详情

相关推荐

复合材料热压罐固化残余应力仿真与优化

复合材料在热压罐固化过程中产生的残余应力是影响构件性能的关键因素,尤其在航空航天和风电叶片等高端制造领域。通过多物理场耦合仿真技术,可以精确预测残余应力分布,优化工艺参数。顺序耦合方法分步求解热分析和结构分析,既保证了计算精度,又控制了成本。Abaqus等有限元工具在此过程中发挥重要作用,通过合理设置边界条件、材料属性和接触关系,能够有效模拟温度场和应力场的相互作用。工程实践中,这种仿真方法已成功应用于无人机机翼、卫星支架等实际项目,显著降低了固化变形和微裂纹风险。

weixin_33146151的博客 214

ntcreatefile使用_红队战术:在C#中使用syscall之编写代码

前言本文专注于实际编写代码,利用在上一篇文章中学到的内容,实现一个有效的syscall。除了编写代码外,也会介绍如何对“工具”代码进行管理,以及如何为之后与其他工具的集成做准备。在上一篇文章中,我们介绍了一些在C#中使用syscall需要了解的基本概念,其中触及了一些比较深入的主题,例如Windows内部结构及系统调用的概念。还讨论了.NET 框架的工作原理以及如何在C#中利用非托管代码...

weixin_39956110的博客 751

微处理器实验报告-实验四

实验四 可编程定时/计数器8253实验 一、实验要求 利用8086外接8253可编程定时/计数器,可以实现方波的产生。 二、实验目的 1、学习8086与8253的连接方法。 2、学习8253的控制方法。 3、掌握8253定时器/计数器的工作方式和编程原理

青竹小轩的博客 5110

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

一段写的比较好的文章,收藏用,不知道怎么用的可以看看,也可以学习下调试的一些用法 应用程序如何使用驱动 应用程序中使用 CreateFile,ReadFile,WriteFile,DeviceIoControl,CloseHandle 来指示驱动程序完成某种任务。比如我们在应用程序中使用 ReadFile 来让驱动读取硬件设备,我们在应用程序中使用 WriteFile 来让驱动写硬件设

Go Forward Forever! 1505

学习笔记9--智能驾驶的功能安全设计之功能安全设计

本系列博客包括6个专栏,分别为:《自动驾驶技术概览》、《自动驾驶汽车平台技术基础》、《自动驾驶汽车定位技术》、《自动驾驶汽车环境感知》、《自动驾驶汽车决策与控制》、《自动驾驶系统设计及应用》。此专栏是关于《自动驾驶系统设计及应用》书籍的笔记.

FUXI_Willard 1794

SFR算法实现详解:从边缘检测到MTF计算的完整流程

本文详细解析了SFR算法从边缘检测到MTF计算的完整流程,包括边缘检测、ESF超采样、LSF差分等关键步骤。通过代码示例和实战经验,帮助开发者高效实现成像系统解析力评估,优化MTF测量精度。适用于摄像头模组调试、工业检测等场景。

Black的专栏 1009

应用层调用驱动程序的方法

转:http://hi.baidu.com/cealy/blog/item/3d300ed7941182 d9a144df5a.html Windows 中,应用程序使用驱动,应用程序与驱动通信的一些问题。   2.1 应用程序如何使用驱动   应用程序中使用 CreateFile,ReadFile,WriteFile,DeviceIoControl,CloseHandle 来指示驱动程序完成某种

yuyin86的专栏 3305

基于Codex Skill的抖音爆款分析与带货视频自动化生成实战

这次我们来看一个基于 Codex 的自动化内容创作项目。核心是利用 Codex 平台的能力,通过自定义的 Skill(技能)来拆解抖音爆款博主的视频模式,并自动生成自己的带货视频。对于内容创作者、电商运营或对短视频自动化感兴趣的技术开发者来说,这是一个将大语言模型(LLM)能力与具体业务场景深度结合的实战案例。 项目的核心价值在于流程自动化。它试图解决一个常见痛点:分析爆款视频的脚本结构、话术、节奏需要大量人工,而模仿创作又耗时费力。通过 Codex 的 Skill 机制,可以将“分析”和“生成”这两个环节

weixin_30420305的博客 383

Windows键盘和鼠标输入过程

从上面分析我们可以知道针对鼠标和键盘有两种处理方案。首先使用来设置鼠标键盘钩子(或者低级钩子),这样就可以在鼠标和键盘响应之前捕获消息,但是如果需要针对CTRL + ALT + DELETE键进行处理,那这两个操作就达不到要求了。鉴于的局限,网上有人实现了一种比较偏门的键盘热键拦截的方案,就是HOOK函数,从上面的分析我们可以发现,这个函数在所有键盘事件处理之前,因此肯定可以拦截所有消息了。但是对于这种方案的稳定性和兼容性本人没法做相关保证,因此如果正式项目中还是谨慎使用。

xsinlink的博客 1550

YOLOv8n行人检测实战:从数据集准备到模型训练

本文详细介绍了使用YOLOv8n进行行人检测的完整流程。首先阐述了YOLOv8的优势:高速推理(300+FPS)、更高精度、易用性和轻量化(仅6MB)。接着通过Docker搭建GPU训练环境,配置32GB共享内存避免数据加载问题,并安装必要的框架和OpenCV组件。数据集采用COCO2017标注格式,包含归一化坐标的YOLO标签文件。通过可视化脚本验证标注准确性后,提出多数据集融合训练策略以增强模型泛化能力。最后使用Ultralytics框架启动训练,展现了从环境搭建到模型训练的完整技术路径。该方案兼顾检测

Hi20240217的博客 1090

Autosar诊断实战系列05-Dcm之0x27加解密服务配置开发详解

UDS 0x27服务实战开发介绍,对解锁尝试次数及delay时间进一步解释。

initiallizer的博客 1991

Windows 进程的句柄表

本文源自《Windows内核原理与实现》第3章Windows 进程和线程 3.4.1 Windows 进程的句柄表(1) 上一章介绍了Windows 内核中的对象管理器,Windows 执行体实现了一套对象机制来管理各种资源或实体。每种对象都有一个类型对象,类型对象定义了该类对象的一些特性和方法。对象管理器也定义了一个全局名字空间,提供了根据名称来解析对象的统一机制(参考O

qiaoli的知识备忘录 2722

使用 Pfam 和 InterProScan 进行蛋白质家族和功能域的分析

本文介绍了使用Pfam和InterProScan进行蛋白质家族与功能域分析的方法。主要内容包括:1) Pfam数据库的本地化使用,通过hmmscan工具比对Pfam-A.hmm数据库,并提供了批量处理Python脚本;2) InterProScan的安装配置与命令行参数详解,支持多数据库整合分析;3) 补充介绍了基于curl的Uniprot在线查询方法。文章提供了从软件安装到实战分析的完整流程,适用于本地化、批量化蛋白质功能注释需求,特别是无网络环境或大规模数据分析场景,适合不同水平的生物信息学用户。

Protein Designer的博客 1918

键盘驱动系列---JIURL键盘驱动 2

2 应用层基础知识     在讨论使用键盘的应用程序这个问题之前,我们首先介绍一下 Windows 中,应用程序使用驱动,应用程序与驱动通信的一些问题。 2.1 应用程序如何使用驱动     应用程序中使用 CreateFile,ReadFile,WriteFile,DeviceIoControl,CloseHandle 来指示驱动程序完成某种任务。比如我们在应用程序中使用 Re

多媒体编程、网络编程、系统编程、网络安全编程、驱动编程 963

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

JIURL键盘驱动 1

JIURL键盘驱动 1作者: JIURL                 主页: http://jiurl.yeah.net     日期: 2003-12-13 0 概述    我们将讨论 ps/2 键盘的驱动。主要讨论的内容有,ps/2 键盘的硬件,使用键盘驱动的应用层,键盘驱动

jiurl的专栏 3617

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

键盘驱动系列---JIURL键盘驱动 1

0 概述     我们将讨论 ps/2 键盘的驱动。主要讨论的内容有,ps/2 键盘的硬件,使用键盘驱动的应用层,键盘驱动的初始化,键盘驱动如何完成自己的工作,以及一些涉及到的相关内容。需要注意的是,以后我们提到的键盘,如果没有特殊说明,都是指 ps/2 键盘。 1 ps/2 键盘的硬件     要以写一个硬件的驱动为目的的话,需要对这个硬件有一定的了解,但并不需要太深入,只要对

多媒体编程、网络编程、系统编程、网络安全编程、驱动编程 1206

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

键盘驱动系列---JIURL键盘驱动 4

6.2 初始化与注册表 在驱动的初始化中,注册表起着非常重要的作用。 6.2.1 重要认识     每台计算机的硬件配置可能是不同的,系统是如何知道需要为哪些硬件载入驱动?系统是如何知道要载入的驱动文件是哪个文件?答案是通过注册表,注册表中保存着这些信息。那么注册表中的这些信息是哪里来的?答案是在安装驱动的时候,安装程序放到注册表中的。 6.2.2 确定驱动载入顺序的基本知

多媒体编程、网络编程、系统编程、网络安全编程、驱动编程 2282

键盘驱动系列---JIURL键盘驱动 5

7 键盘驱动的运作 7.1 输入数据队列简介 i8042prt 和 kbdclass 各有一个输入数据队列,他们是循环使用的缓冲区。他们的每个单元是一个 KEYBOARD_INPUT_DATA 结构。 i8042prt 的自定义的设备扩展中,保存着一些指针和计数值,用来使用它的那个输入数据队列。 PKEYBOARD_INPUT_DATA 类型的 InputData , Da

多媒体编程、网络编程、系统编程、网络安全编程、驱动编程 2027

键盘驱动系列---JIURL键盘驱动 3

4 编译与调试环境简介 4.1 源码     ps/2键盘驱动的设备栈有3层,最底层设备对象的驱动是 acpi,中间层设备对象的驱动是 i8042prt,最高层设备对象的驱动是 kbdclass。     DDK 所附的源码中有 i8042prt 和 kbdclass 的源码,分别位于 ...\NTDDK\src\input\pnpi8042 ,...\NTDDK\src\inp

多媒体编程、网络编程、系统编程、网络安全编程、驱动编程 1192

键盘驱动笔记

前段时间想写一个简单的键盘驱动,就找网上找了一些文档看看。JIURL的键盘驱动5篇文章都很精彩,让人很快就能明白键盘驱动的原理。一般情况微软建议写的键盘驱动都是属于keyboard filter driver,它位于kbdclass和i8042prt驱动之间。利用IOCTL_INTERNAL_KEYBOARD_CONNECT来挂接KeyboardClassServiceCallback,然后在I8

3854
上一篇: 驱动对象 设备对象 设备栈 乱杂谈
下一篇: IRP 乱杂谈
imquestion
博客等级 码龄25年 50粉丝 34原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值