Home 电脑技术 编程技术 驱动开发之二 --- 输入输出控制
驱动开发之二 --- 输入输出控制 E-mail
作者:洋葱圈   
周日, 22 6月 2008 06:39

这是驱动开发的第二篇,在上篇中讲述了如何创建一个简单的设备驱动程序,为了突出重点,有些细节的东西略过了,这些将在本篇中进行详细介绍,本篇内容涉及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(描述符表),这些表定义了系统属性。我们下面讨论虚拟地址转化为物理地址的过程。描述符表定义一个偏移,然后加到虚拟地址。如果没有采用分页机制,两个地址相加就是物理地址,如果采用了分页机制,两个地址相加就是线性地址,线性地址可以通过分页表转化为物理地址。

这就是最早在pentium CPU芯片中被介绍的被称作分页内存扩展的分页机制。这个机制允许分页表访问36位的地址。然而,偏移仍然是32位的。如果你没有采用分页表,你只能访问4GB,反之,你能访问的物理内存提高到36位。


32位的分页一般是这样做的。CPU中有一个寄存器指向分页目录表的根部,叫做CR3,上边的图表显示了分页机制如何工作的。物理分页的位置并不需要线性化到虚拟地址。黑色的线指明了分页表如何建立的。分页目录表记录了分页表的入口。分页表的又记录了物理内存分页的入口。CPU实际上支持4k-2M的分页,而windows和多数操作系统使用4k的分页。
如果分页被定义位4k,那整个过程如下:

选择器指向描述符表的入口。
描述符表中条目作为虚拟地址的基础偏移,从而创建线性地址。
线性地址的31-22位:在分页目录中的偏移;
线性地址的21-12位:在分页表中的偏移;
线性地址的其它位:在物理内存中的偏移;

windows实现
windows将虚拟地址范围划分位三层。第一层是用户模式地址,在每个进程中,地址唯一。也就是说,每个进程有自己的地址空间。第二层是session 空间,如果你使用快速用户切换或者Terminal Services(远程桌面),你会知道每个用户有自己的桌面。有一些驱动运行在session 空间。例如,显卡驱动和打印机驱动就运行在这个空间。这就是为什么不能跨越session的原因,你不能使用findwindow发现另一个用户的桌面。
最后一层叫系统空间。这部分内存在整个系统空间共享。大部分驱动位于这个空间。

当每次线程切换的时候,CR3重新装载相应的分页表的指针。每个进程有自己的目录指针,并被装载到CR3,这就是为什么windows能够从本质上隔离各个进程的原因。
/PAE
这被称作物理地址扩展。意味着操作系统能够映射36位的物理内存到32位。但并意味你能访问大于4GB的内存。操作系统能够使用大于4GB的内存,但是进程不能访问大于4GB的内存。
有一些API能够管理大于4GB的内存。这些API被称作AWE或者地址window扩展。
/3GB
/3GB的开关意味着用户模式可以有3GB的地址空间。一般的4GB的地址空间被划分为两个部分,一半是用户模式地址空间,一半是内核模式地址空间。设置/3GB开关将会允许用户模式进程有更多的内存,而内核模式有更少的内存。
结论:
同用户模式的进程通信我们将学到更多东西,我们会学习如何实现ReadFile and DeviceIoControl,我们也学习了 如何完成IRP和给用户模式返回值。也学习了创建IOCTL,最后,是在windows内存如何映射。
Trackback(0)
Comments (0)Add Comment

Write comment
quote
bold
italicize
underline
strike
url
image
quote
quote
smile
wink
laugh
grin
angry
sad
shocked
cool
tongue
kiss
cry
smaller | bigger

security code
Write the displayed characters


busy