这是驱动开发的第二篇,在上篇中讲述了如何创建一个简单的设备驱动程序,为了突出重点,有些细节的东西略过了,这些将在本篇中进行详细介绍,本篇内容涉及Read函数,输入/输出控制,以及一些irp知识。在开始之前有几个问题需要说明下。
在驱动中可以包含windows.h吗?不能混用windows SDK头文件和windows DDK头文件。如果你这样使用,在编译的时候就会报错,他们在定义中是冲突的。有时候,用户模式的应用程序可能包含DDK的头文件,一般是使用DDK或者SDK中的类型。在其它正常使用中,是将他们分开的。
可以实现x类型的驱动吗?
在windows中大部分的驱动都有一般的框架。正如第一篇文章提出的,驱动并不是必须访问硬件,一般是组成一个驱动栈。如果你在实现一个某种类型的驱 动,这有一个理解驱动如何工作的开始点。这个差别在于你的驱动如何注册到系统中,实现了那些IOCTL,与那些下一层的驱动通信,为了支持上层驱动或者用 户模式的组件,你需要做那些额外的工作。如果你正在写某种类型的驱动,你可以从MSDN和DDK种获得帮助,有些框架实际上是你要做的工作的简化版本,你 可以作为参考。
可以在驱动中使用c/c++运行时库吗?
你应该避免在驱动中使用这些,取而代之的时等价的内核模式的API,Kernel Run Time Library(内核运行时库)也包含了字符串功 能。在内核模式中编程,有一些需要注意的问题,在内核API中,每个API都会告诉你它可以运行在那个IRQL级别,避免使用标准的运行库会节省你的调试 和发现问题的时间。完成功能ReadFile
在前面提过的,有三种IO类型,Direct, Buffered 和 Neither。下面将会解释新的功能,return values(返回值)。在WriteFile 的实现中,我们不需要担心返回值。正确的实现是告诉用户模式的应用程序,写了多少数据。在下面ReadFile的实现中,需要返回值,这不但是为了告诉用户模式的应用程序,而且还要通知IO管理器。
如果你回忆buffered IO是如何工作的,用户模式的内存被copy到另外的内存缓冲中。如果我们想从驱动中读数据,IO管理器需要知道从用户模式的临时缓冲中copy多少数据。如果我们不这么做,用户模式的应用程序将不会获得任何数据。
NTSTATUS Example_ReadDirectIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_BUFFER_TOO_SMALL;
PIO_STACK_LOCATION pIoStackIrp = NULL;
PCHAR pReturnData = "Example_ReadDirectIO - Hello from the Kernel!";
UINT dwDataSize = sizeof("Example_ReadDirectIO - Hello from the Kernel!");
UINT dwDataRead = 0;
PCHAR pReadDataBuffer;
DbgPrint("Example_ReadDirectIO Called \r\n");
/*
* Each time the IRP is passed down the driver stack a
* new stack location is added
* specifying certain parameters for the IRP to the
* driver.
*/
pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
if(pIoStackIrp && Irp->MdlAddress)
{
pReadDataBuffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress,
NormalPagePriority);
if(pReadDataBuffer &&
pIoStackIrp->Parameters.Read.Length >= dwDataSize)
{
/*
* We use "RtlCopyMemory" in the kernel instead
* of memcpy.
* RtlCopyMemory *IS* memcpy, however it's best
* to use the
* wrapper in case this changes in the future.
*/
RtlCopyMemory(pReadDataBuffer, pReturnData,
dwDataSize);
dwDataRead = dwDataSize;
NtStatus = STATUS_SUCCESS;
}
}
实现返回值
使用IRP中的IO_STATUS_BLOCK 实现返回值。依赖与实现的主要功能,可以改变IRP中成员变量。在我们实现的主要功能中,Status等价于返回值,information包含了读写的数量。看下面的代码,注意IoCompleteRequest。
当驱动完成IRP调用后,IoCompleteRequest被调用。在上一篇文章的例子中,我们没有调用IoCompleteRequest,因为IO管理器已经做了这个工作,但是驱动最好在完成IRP后调用这个函数。
Irp->IoStatus.Status = NtStatus;
Irp->IoStatus.Information = dwDataRead;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return NtStatus;
}
IoCompleteRequest 的第二个参数指定了等待这个IRP完成的线程的推进的优先级。例如,一个线程等待网络操作很长时间,这个就有助于调度程序重新运行这个线程。
ntdll!_IO_STATUS_BLOCK
+0x000 Status : Int4B
+0x000 Pointer : Ptr32 Void
+0x004 Information : Uint4B
严格的参数验证和错误校验
现在的代码完成更为严格的参数和错误校验。在你的驱动中,你要确认用户模式不应该传递无效的内存地址给驱动。同时在返回值上,也要做得更好,而不是简单的 返回正确或者错误。需要告诉用户模式的程序错误的具体原因。你可能比较喜欢能够通过GetLastError 获得具体错误原因的API,这样就可以改正 上层代码。如果只是简单的返回true或者false,那用户模式的应用程序就很难使用你的驱动。
输入输出控制(IOCTL)
IOCTL更多的用来应用程序和驱动的通信,而不是简单的读写数据。一般情况,驱动导出一些IOCTL并定义一些在通信中使用的数据结构。一般情况,这些 数据结构不能包含指针,因为IO管理器不能解释这些结构。所有的数据应该被包含在一个块中。如果你想创建指针,你可以创建块内的偏移。如果你还记得,即使 在一个进程中,驱动也不能访问用户模式的数据。所以,如果要实现内存指针,驱动要copy页内存或者锁定页内存。用户模式的程序使用 DeviceIoControl完成通信。
定义IOCTL
我们要做的第一件事情是定义在应用程序和驱动间通信的IOCTL代码。首先,跟用户模式的某些东西关联IOCTL,你可能想到windows 消息。 IOCTL 是一个32位的数字。最低两位定义为传送类型: METHOD_OUT_DIRECT, METHOD_IN_DIRECT, METHOD_BUFFERED or METHOD_NEITHER.
从2-13位定义为功能码,最高位定义为定制位,这个位决定了是用户定义还是系统定义。
接下两位的定义:决定了 如果IO管理器打开设备失败,该如何处理。例如FILE_READ_DATA 或者FILE_WRITE_DATA。剩余的位代表了设备类型。最高位仍是定制位。
下面是快速定义IOCTL的宏。
/*
* IOCTL's are defined by the following bit layout.
* [Common |Device Type|Required Access|Custom|Function Code|Transfer Type]
* 31 30 16 15 14 13 12 2 1 0
*
* Common - 1 bit. This is set for user-defined
* device types.
* Device Type - This is the type of device the IOCTL
* belongs to. This can be user defined
* (Common bit set). This must match the
* device type of the device object.
* Required Access - FILE_READ_DATA, FILE_WRITE_DATA, etc.
* This is the required access for the
* device.
* Custom - 1 bit. This is set for user-defined
* IOCTL's. This is used in the same
* manner as "WM_USER".
* Function Code - This is the function code that the
* system or the user defined (custom
* bit set)
* Transfer Type - METHOD_IN_DIRECT, METHOD_OUT_DIRECT,
* METHOD_NEITHER, METHOD_BUFFERED, This
* the data transfer method to be used.
*
*/
#define IOCTL_EXAMPLE_SAMPLE_DIRECT_IN_IO \
CTL_CODE(FILE_DEVICE_UNKNOWN, \
0x800, \
METHOD_IN_DIRECT, \
FILE_READ_DATA | FILE_WRITE_DATA)
#define IOCTL_EXAMPLE_SAMPLE_DIRECT_OUT_IO \
CTL_CODE(FILE_DEVICE_UNKNOWN, \
0x801, \
METHOD_OUT_DIRECT, \
FILE_READ_DATA | FILE_WRITE_DATA)
#define IOCTL_EXAMPLE_SAMPLE_BUFFERED_IO \
CTL_CODE(FILE_DEVICE_UNKNOWN, \
0x802, \
METHOD_BUFFERED, \
FILE_READ_DATA | FILE_WRITE_DATA)
#define IOCTL_EXAMPLE_SAMPLE_NEITHER_IO \
CTL_CODE(FILE_DEVICE_UNKNOWN, \
0x803, \
METHOD_NEITHER, \
FILE_READ_DATA | FILE_WRITE_DATA)
上边的代码显示了我们如何定义IOCTL。
实现IOCTL
第一件事是如何分发IOCTL 到它各自的实现代码中。IO_STACK_LOCATION中的Parameters.DeviceIoControl.IoControlCode包含了被调用的IOCTL的代码。下面就是分发每个IOCTL到它实现函数中的代码。
NTSTATUS Example_IoControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_NOT_SUPPORTED;
PIO_STACK_LOCATION pIoStackIrp = NULL;
UINT dwDataWritten = 0;
DbgPrint("Example_IoControl Called \r\n");
pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
if(pIoStackIrp) /* Should Never Be NULL! */
{
switch(pIoStackIrp->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_EXAMPLE_SAMPLE_DIRECT_IN_IO:
NtStatus = Example_HandleSampleIoctl_DirectInIo(Irp,
pIoStackIrp, &dwDataWritten);
break;
case IOCTL_EXAMPLE_SAMPLE_DIRECT_OUT_IO:
NtStatus = Example_HandleSampleIoctl_DirectOutIo(Irp,
pIoStackIrp, &dwDataWritten);
break;
case IOCTL_EXAMPLE_SAMPLE_BUFFERED_IO:
NtStatus = Example_HandleSampleIoctl_BufferedIo(Irp,
pIoStackIrp, &dwDataWritten);
break;
case IOCTL_EXAMPLE_SAMPLE_NEITHER_IO:
NtStatus = Example_HandleSampleIoctl_NeitherIo(Irp,
pIoStackIrp, &dwDataWritten);
break;
}
}
Irp->IoStatus.Status = NtStatus;
Irp->IoStatus.Information = dwDataWritten;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return NtStatus;
}
如果你理解了ReadFile and WriteFile的实现,以上代码就容易理解,只是简单的将实现集成到一个调用中。
METHOD_x_DIRECT
我们将同时解释METHOD_IN_DIRECT 和 METHOD_OUT_DIRECT。两个基本上是相同的,INPUT缓冲使用BUFFERED的 实现,当我们解释Raad/Write我们使用MdlAddress 作为输出缓冲。IN和OUT的区别在于:如果你使用IN,你可以使用输出缓冲传递数 据,如果你使用OUT,只能用作返回数据。在这个例子中,我们在传递数据时,不使用IN实现。本质上,IN和OUT的实现是相同的。基于这个原因,我只讲 OUT的实现。
NTSTATUS Example_HandleSampleIoctl_DirectOutIo(PIRP Irp,
PIO_STACK_LOCATION pIoStackIrp, UINT *pdwDataWritten)
{
NTSTATUS NtStatus = STATUS_UNSUCCESSFUL;
PCHAR pInputBuffer;
PCHAR pOutputBuffer;
UINT dwDataRead = 0, dwDataWritten = 0;
PCHAR pReturnData = "IOCTL - Direct Out I/O From Kernel!";
UINT dwDataSize = sizeof("IOCTL - Direct Out I/O From Kernel!");
DbgPrint("Example_HandleSampleIoctl_DirectOutIo Called \r\n");
/*
* METHOD_OUT_DIRECT
*
* Input Buffer = Irp->AssociatedIrp.SystemBuffer
* Ouput Buffer = Irp->MdlAddress
*
* Input Size = Parameters.DeviceIoControl.InputBufferLength
* Output Size = Parameters.DeviceIoControl.OutputBufferLength
*
* What's the difference between METHOD_IN_DIRECT && METHOD_OUT_DIRECT?
*
* The function which we implemented METHOD_IN_DIRECT
* is actually *WRONG*!!!! We are using the output buffer
* as an output buffer! The difference is that METHOD_IN_DIRECT creates
* an MDL for the outputbuffer with
* *READ* access so the user mode application
* can send large amounts of data to the driver for reading.
*
* METHOD_OUT_DIRECT creates an MDL
* for the outputbuffer with *WRITE* access so the user mode
* application can recieve large amounts of data from the driver!
*
* In both cases, the Input buffer is in the same place,
* the SystemBuffer. There is a lot
* of consfusion as people do think that
* the MdlAddress contains the input buffer and this
* is not true in either case.
*/
pInputBuffer = Irp->AssociatedIrp.SystemBuffer;
pOutputBuffer = NULL;
if(Irp->MdlAddress)
{
pOutputBuffer =
MmGetSystemAddressForMdlSafe(Irp->MdlAddress,
NormalPagePriority);
}
if(pInputBuffer && pOutputBuffer)
{
/*
* We need to verify that the string
* is NULL terminated. Bad things can happen
* if we access memory not valid while in the Kernel.
*/
if(Example_IsStringTerminated(pInputBuffer,
pIoStackIrp->Parameters.DeviceIoControl.InputBufferLength,
&dwDataRead)) {
DbgPrint("UserModeMessage = '%s'", pInputBuffer);
DbgPrint("%i >= %i",
pIoStackIrp->Parameters.DeviceIoControl.OutputBufferLength,
dwDataSize);
if(pIoStackIrp->
Parameters.DeviceIoControl.OutputBufferLength >= dwDataSize)
{
/*
* We use "RtlCopyMemory" in the kernel instead of memcpy.
* RtlCopyMemory *IS* memcpy, however it's best to use the
* wrapper in case this changes in the future.
*/
RtlCopyMemory(pOutputBuffer, pReturnData, dwDataSize);
*pdwDataWritten = dwDataSize;
NtStatus = STATUS_SUCCESS;
}
else
{
*pdwDataWritten = dwDataSize;
NtStatus = STATUS_BUFFER_TOO_SMALL;
}
}
}
return NtStatus;
}
METHOD_BUFFERED
METHOD_BUFFERED的实现本质上和Read,Write的实现相同。先分配缓冲,再从这个缓冲copy数据,缓冲是两倍大小,输入和输出缓 冲。读缓冲被copy到新的缓冲。在你返回前,你只是copy返回值到相同的缓冲。返回值被放到IO_STATUS_BLOCK ,IO管理器copy数 据到输出缓冲。
NTSTATUS Example_HandleSampleIoctl_BufferedIo(PIRP Irp,
PIO_STACK_LOCATION pIoStackIrp, UINT *pdwDataWritten)
{
NTSTATUS NtStatus = STATUS_UNSUCCESSFUL;
PCHAR pInputBuffer;
PCHAR pOutputBuffer;
UINT dwDataRead = 0, dwDataWritten = 0;
PCHAR pReturnData = "IOCTL - Buffered I/O From Kernel!";
UINT dwDataSize = sizeof("IOCTL - Buffered I/O From Kernel!");
DbgPrint("Example_HandleSampleIoctl_BufferedIo Called \r\n");
/*
* METHOD_BUFFERED
*
* Input Buffer = Irp->AssociatedIrp.SystemBuffer
* Ouput Buffer = Irp->AssociatedIrp.SystemBuffer
*
* Input Size = Parameters.DeviceIoControl.InputBufferLength
* Output Size = Parameters.DeviceIoControl.OutputBufferLength
*
* Since they both use the same location
* so the "buffer" allocated by the I/O
* manager is the size of the larger value (Output vs. Input)
*/
pInputBuffer = Irp->AssociatedIrp.SystemBuffer;
pOutputBuffer = Irp->AssociatedIrp.SystemBuffer;
if(pInputBuffer && pOutputBuffer)
{
/*
* We need to verify that the string
* is NULL terminated. Bad things can happen
* if we access memory not valid while in the Kernel.
*/
if(Example_IsStringTerminated(pInputBuffer,
pIoStackIrp->Parameters.DeviceIoControl.InputBufferLength,
&dwDataRead)) {
DbgPrint("UserModeMessage = '%s'", pInputBuffer);
DbgPrint("%i >= %i",
pIoStackIrp->Parameters.DeviceIoControl.OutputBufferLength,
dwDataSize);
if(pIoStackIrp->Parameters.DeviceIoControl.OutputBufferLength
>= dwDataSize)
{
/*
* We use "RtlCopyMemory" in the kernel instead of memcpy.
* RtlCopyMemory *IS* memcpy, however it's best to use the
* wrapper in case this changes in the future.
*/
RtlCopyMemory(pOutputBuffer, pReturnData, dwDataSize);
*pdwDataWritten = dwDataSize;
NtStatus = STATUS_SUCCESS;
}
else
{
*pdwDataWritten = dwDataSize;
NtStatus = STATUS_BUFFER_TOO_SMALL;
}
}
}
return NtStatus;
}
METHOD_NEITHER
这与neither I/O的实现相同。用户模式的缓冲被传递给驱动。
NTSTATUS Example_HandleSampleIoctl_NeitherIo(PIRP Irp,
PIO_STACK_LOCATION pIoStackIrp, UINT *pdwDataWritten)
{
NTSTATUS NtStatus = STATUS_UNSUCCESSFUL;
PCHAR pInputBuffer;
PCHAR pOutputBuffer;
UINT dwDataRead = 0, dwDataWritten = 0;
PCHAR pReturnData = "IOCTL - Neither I/O From Kernel!";
UINT dwDataSize = sizeof("IOCTL - Neither I/O From Kernel!");
DbgPrint("Example_HandleSampleIoctl_NeitherIo Called \r\n");
/*
* METHOD_NEITHER
*
* Input Buffer = Parameters.DeviceIoControl.Type3InputBuffer
* Ouput Buffer = Irp->UserBuffer
*
* Input Size = Parameters.DeviceIoControl.InputBufferLength
* Output Size = Parameters.DeviceIoControl.OutputBufferLength
*
*/
pInputBuffer = pIoStackIrp->Parameters.DeviceIoControl.Type3InputBuffer;
pOutputBuffer = Irp->UserBuffer;
if(pInputBuffer && pOutputBuffer)
{
/*
* We need this in an exception handler or else we could trap.
*/
__try {
ProbeForRead(pInputBuffer,
pIoStackIrp->Parameters.DeviceIoControl.InputBufferLength,
TYPE_ALIGNMENT(char));
/*
* We need to verify that the string
* is NULL terminated. Bad things can happen
* if we access memory not valid while in the Kernel.
*/
if(Example_IsStringTerminated(pInputBuffer,
pIoStackIrp->Parameters.DeviceIoControl.InputBufferLength,
&dwDataRead))
{
DbgPrint("UserModeMessage = '%s'", pInputBuffer);
ProbeForWrite(pOutputBuffer,
pIoStackIrp->Parameters.DeviceIoControl.OutputBufferLength,
TYPE_ALIGNMENT(char));
if(pIoStackIrp->
Parameters.DeviceIoControl.OutputBufferLength
>= dwDataSize)
{
/*
* We use "RtlCopyMemory"
* in the kernel instead of memcpy.
* RtlCopyMemory *IS* memcpy,
* however it's best to use the
* wrapper in case this changes in the future.
*/
RtlCopyMemory(pOutputBuffer,
pReturnData,
dwDataSize);
*pdwDataWritten = dwDataSize;
NtStatus = STATUS_SUCCESS;
}
else
{
*pdwDataWritten = dwDataSize;
NtStatus = STATUS_BUFFER_TOO_SMALL;
}
}
} __except( EXCEPTION_EXECUTE_HANDLER ) {
NtStatus = GetExceptionCode();
}
}
return NtStatus;
}
调用DeviceIoControl
以下是一个简单的实现。
ZeroMemory(szTemp, sizeof(szTemp));
DeviceIoControl(hFile,
IOCTL_EXAMPLE_SAMPLE_DIRECT_IN_IO,
"** Hello from User Mode Direct IN I/O",
sizeof("** Hello from User Mode Direct IN I/O"),
szTemp,
sizeof(szTemp),
&dwReturn,
NULL);
printf(szTemp);
printf("\n");
ZeroMemory(szTemp, sizeof(szTemp));
DeviceIoControl(hFile,
IOCTL_EXAMPLE_SAMPLE_DIRECT_OUT_IO,
"** Hello from User Mode Direct OUT I/O",
sizeof("** Hello from User Mode Direct OUT I/O"),
szTemp,
sizeof(szTemp),
&dwReturn,
NULL);
printf(szTemp);
printf("\n");
ZeroMemory(szTemp, sizeof(szTemp));
DeviceIoControl(hFile,
IOCTL_EXAMPLE_SAMPLE_BUFFERED_IO,
"** Hello from User Mode Buffered I/O",
sizeof("** Hello from User Mode Buffered I/O"),
szTemp,
sizeof(szTemp),
&dwReturn,
NULL);
printf(szTemp);
printf("\n");
ZeroMemory(szTemp, sizeof(szTemp));
DeviceIoControl(hFile,
IOCTL_EXAMPLE_SAMPLE_NEITHER_IO,
"** Hello from User Mode Neither I/O",
sizeof("** Hello from User Mode Neither I/O"),
szTemp,
sizeof(szTemp),
&dwReturn,
NULL);
printf(szTemp);
printf("\n");
系统内存的布局
现在来学习windows内存布局是一个好的时机。我们首先需要看intel处理器如何处理虚拟内存。虽然有好几种实现,我只解释一般的实现。这被称作虚拟地址转换。
虚拟地址转换
所有的段寄存器在保护模式中变为选择器。为了更加熟悉X86如何工作,我们来简单复习以下分页机制。在CPU中有其它的寄存器指向 descriptor tables(描述符表),这些表定义了系统属性。我们下面讨论虚拟地址转化为物理地址的过程。描述符表定义一个偏移,然后加到虚 拟地址。如果没有采用分页机制,两个地址相加就是物理地址,如果采用了分页机制,两个地址相加就是线性地址,线性地址可以通过分页表转化为物理地址。
![](https://i-blog.csdnimg.cn/blog_migrate/cc245cc667ed94ad818971c59fe01f4b.jpeg)
这就是最早在pentium CPU芯片中被介绍的被称作分页内存扩展的分页机制。这个机制允许分页表访问36位的地址。然而,偏移仍然是32位的。如果你没有采用分页表,你只能访问4GB,反之,你能访问的物理内存提高到36位。
![](https://i-blog.csdnimg.cn/blog_migrate/0196541ee0609f503ab75a261793f41e.jpeg)
32位的分页一般是这样做的。CPU中有一个寄存器指向分页目录表的根部,叫做CR3,上边的图表显示了分页机制如何工作的。物理分页的位置并不需要线性 化到虚拟地址。黑色的线指明了分页表如何建立的。分页目录表记录了分页表的入口。分页表的又记录了物理内存分页的入口。CPU实际上支持4k-2M的分 页,而windows和多数操作系统使用4k的分页。
如果分页被定义位4k,那整个过程如下:
选择器指向描述符表的入口。
描述符表中条目作为虚拟地址的基础偏移,从而创建线性地址。
线性地址的31-22位:在分页目录中的偏移;
线性地址的21-12位:在分页表中的偏移;
线性地址的其它位:在物理内存中的偏移;
windows实现
windows将虚拟地址范围划分位三层。第一层是用户模式地址,在每个进程中,地址唯一。也就是说,每个进程有自己的地址空间。第二层是session 空间,如果你使用快速用户切换或者Terminal Services(远程桌面),你会知道每个用户有自己的桌面。有一些驱动运行在session 空 间。例如,显卡驱动和打印机驱动就运行在这个空间。这就是为什么不能跨越session的原因,你不能使用findwindow发现另一个用户的桌面。
最后一层叫系统空间。这部分内存在整个系统空间共享。大部分驱动位于这个空间。
![](https://i-blog.csdnimg.cn/blog_migrate/ea64f8fc6f34a38dc7673e74c5aa1183.jpeg)
/PAE
这被称作物理地址扩展。意味着操作系统能够映射36位的物理内存到32位。但并意味你能访问大于4GB的内存。操作系统能够使用大于4GB的内存,但是进程不能访问大于4GB的内存。
有一些API能够管理大于4GB的内存。这些API被称作AWE或者地址window扩展。
/3GB
/3GB的开关意味着用户模式可以有3GB的地址空间。一般的4GB的地址空间被划分为两个部分,一半是用户模式地址空间,一半是内核模式地址空间。设置/3GB开关将会允许用户模式进程有更多的内存,而内核模式有更少的内存。
结论:
同用户模式的进程通信我们将学到更多东西,我们会学习如何实现ReadFile and DeviceIoControl,我们也学习了 如何完成IRP和给用户模式返回值。也学习了创建IOCTL,最后,是在windows内存如何映射。