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

相关推荐

麒麟V10等保三级配置实战:从auditd规则到PAM锁定的可信基线构建

Linux安全基线是信息系统可信运行的基础能力,其核心在于身份鉴别、访问控制与安全审计三大机制的协同落地。在国产化信创环境中,麒麟V10作为深度适配飞腾/鲲鹏平台的操作系统,其等保三级合规并非简单套用CentOS加固脚本,而是需围绕内核审计(auditd)、可插拔认证模块(PAM)及国产安全增强机制(如kysec)进行精细化编排。技术价值体现在将抽象条款(如GB/T 22239-2019中6.1.2、6.3.3条)转化为可验证的命令行操作,确保登录失败可锁定、关键行为可追溯、时间偏差不致日志失效。典型应用场

weixin_29969209的博客 599

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

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

weixin_39956110的博客 751

M2DGR数据集在主流SLAM框架中的实战配置指南:从ORB-SLAM到FAST-LIO的完整流程

本文提供了M2DGR多传感器数据集在主流SLAM框架中的实战配置指南。详细介绍了从ORB-SLAM、VINS-Mono等视觉/视觉惯性框架,到A-LOAM、FAST-LIO等激光/激光惯性框架的完整配置流程、参数调整要点及常见问题解决方案。通过实际测试对比,分析了各算法在M2DGR数据集上的表现,为机器人定位与建图研究提供了宝贵的实战经验。

k9l0m1的博客 481

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

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

Go Forward Forever! 1505

自动驾驶仿真:基于Carsim开发的加速度请求模型

1、自动驾驶行业中,算法端对于纵向控制的功能预留接口基本都是加速度,我们需要给控制器仿真加速度的信号,控制器才会实现对纵向功能的控制。2、Carsim或者其他动力学软件,主要提供者节气门开度和主缸压力进行控制。我们在进行联合仿真时,往往都要通过PID的方式将请求加速度转换成对应的节气门开度与主缸压力传给动力学软件进行闭环控制。

zataji的博客 6917

详解大端模式和小端模式

详解大端模式和小端模式 一、大端模式和小端模式的起源         关于大端小端名词的由来,有一个有趣的故事,来自于Jonathan Swift的《格利佛游记》:Lilliput和Blefuscu这两个强国在过去的36个月中一直在苦战。战争的原因:大家都知道,吃鸡蛋的时候,原始的方法是打破鸡蛋较大的一端,可以那时的皇帝的祖父由于小时侯吃鸡蛋,按这种方法把手指弄破了,因此他的父亲,就下令,命令

ce123的技术博客 19万+

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

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

yuyin86的专栏 3305

Open3D——平面点云凸多边形轮廓提取【2025最新版】

平面点云凸多边形轮廓提起,博客长期更新,本文最近更新时间为:2025年1月6日。

点云侠的博客 6754

Windows键盘和鼠标输入过程

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

xsinlink的博客 1550

选课系统进不去?我用 Python 做了个“捡漏挂机”脚本,全自动监控空余名额

频率控制:脚本请求频率不要太高(建议间隔 3-5 秒以上),否则可能被教务系统封 IP,甚至会被教务处请喝茶。仅供学习:本文旨在通过选课场景教学requests和网络抓包原理,请勿用于非法用途或破坏公平性。刷新页面 -> 等待加载 -> 用眼睛看人数 -> 没满?-> 点击选课 -> 确认这个流程最大的瓶颈在于**“等待加载”和“人的反应时间”**。网页加载需要渲染 HTML、CSS、图片,而 Python 脚本只需要请求一个微小的 JSON 数据接口,速度是浏览器的10 倍以上。fill:#333;

BUG猿的博客 294

PINN驱动的3D不可压缩流场重建——从稀疏2D2C观测到完整物理场反演——含源码实现

多尺度融合策略在此发挥作用:高分辨率的2D2C数据用于约束平面内的精细结构,而低分辨率的3D3C数据提供第三维度的粗尺度信息。迁移学习架构在此发挥关键作用。这种策略在湍流封闭模型构建中尤为有效,其中RANS提供的平均流场与DNS的雷诺应力数据融合,显著提升了分离流的预测精度。在稀疏数据场景下,注意力权重根据观测数据的分布动态调整,强化数据缺失区域的物理约束,同时避免过拟合于观测点。这种机制与自适应采样的结合,实现了计算效率与重建精度的平衡,在仅保留约百分之十关键节点的情况下,仍能保持高物理一致性。

u012133341的博客 137

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

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

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

DeepSeek+即梦:AI视频创作从0到1全突破

本文为 DeepSeek 与即梦生成视频全攻略,从前期注册登录、熟悉工具界面及硬件网络准备入手,助读者开启 AI 视频创作。详细阐述利用 DeepSeek 构思脚本、即梦实现画面生成与静态图转动态视频的方法,包括提示词编写、参数调整等技巧。同时介绍后期合成音画、添加字幕特效及多平台适配要点,结合实战案例解析全流程。还分享进阶提示词优化、版权注意等避坑指南,为创作者提供从 0 到 1 的 AI 视频创作完整解决方案。

邓邓子的博客 8121

JIURL键盘驱动 1

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

jiurl的专栏 3617

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

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

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

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

IEM模型,地表粗糙度模拟

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

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

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

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

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

键盘驱动系列---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

3855

【转帖】应用程序如何使用驱动程序

 应用程序中使用 CreateFile,ReadFile,WriteFile,DeviceIoControl,CloseHandle 来指示驱动程序完成某种任务。比如我们在应用程序中使用 ReadFile 来让驱动读取硬件设备,我们在应用程序中使用 WriteFile 来让驱动写硬件设备,我们在应用程序中使用 DeviceIoContorl 来让驱动完成某些驱动支持的功能。而 ReadFi

floweronwarmbed的专栏 1645
上一篇: JIURL键盘驱动 3
下一篇: JIURL键盘驱动 1
iiprogram
博客等级 码龄25年 1068粉丝 1515原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值