05内核编程用户层和内核层通讯

05内核编程用户层和内核层通讯

内核通讯概述

内核层有着非常高的权限, 可以得到很多信息, 也可以修改很多信息. 但是在内核层中无法创建界面, 无法便捷地输出信息. 因此, 这就需要携手用户层的程序来功能完成信息的获取与修改, 信息的展示.

内核层驱动主要负责在内核中获取信息, 而用户层程序负责发出指令, 让内核层获取/修改特定数据, 并将内核层驱动程序执行结果显示到界面.

但是用户层程序是不能直接调用内核成的代码的. Windows给开发者预留了一个接口: 符号链接与CreateFile

在内核中, 驱动对象可以创建设备对象, 而设备对象是可以别命名的, 设备对象也可以创建符号链接. 而符号链接又可以在用户层中通过CreateFile打开, 这样一来, 用户层和内核成就可以连接起来了.

建立用户层和内核层的连接 : 符号链接与CreateFile

链接建立的关键点

内核层:

  1. 驱动程序需要创建一个设备对象, 设备对象的命名要符合规则: "\Device\设备名"

  2. 需要为设备对象创建一个符号链接, 符号链接名要符合规则: "\DosDevices\符号链接名"

  3. 符号链接名不要和设备名重名

  4. 设置IRP_MJ_CREATE派遣函数, 并在派遣函数中将处理状态设置为STATUS_SUCCESS.

用户层:

  1. 通过CreateFile打开设备链接, 设备链接的语法是:"\??\符号链接名".

  2. 文件打开方式设置:OPEN_EXISTING

驱动层, DriverEntry.c

#include <ntddk.h>

void Unload(DRIVER_OBJECT* object);
NTSTATUS Create(DEVICE_OBJECT* device, IRP* irp);
NTSTATUS Close(DEVICE_OBJECT* device, IRP* irp);

NTSTATUS DriverEntry(DRIVER_OBJECT* driver, UNICODE_STRING* path)
{
   _asm int 3;
   path;
   NTSTATUS status = STATUS_SUCCESS;
   UNICODE_STRING NtDevName;
   RtlInitUnicodeString(&NtDevName, L"\Device\mydevice");

   // 1. 创建一个设备对象.否则无法绑定符号链接,没有符号链接, 用户层就连接不到内核层
   DEVICE_OBJECT* dev=NULL;
   status = IoCreateDevice(driver,
                           0,
                           &NtDevName,
                           FILE_DEVICE_UNKNOWN,
                           0,
                           0,
                           &dev);
   if (status != STATUS_SUCCESS)
       return status;

   UNICODE_STRING DosSymName;
   RtlInitUnicodeString(&DosSymName, L"\DosDevices\device_001");
   //2. 为设备对象绑定符号链接
   IoCreateSymbolicLink(&DosSymName, &NtDevName);

   //绑定卸载函数
   driver->DriverUnload = Unload;
   // 绑定派遣函数, 如果不绑定这两个函数, 用户层在打开驱动时会失败.
   driver->MajorFunction[IRP_MJ_CREATE] = Create;
   driver->MajorFunction[IRP_MJ_CLOSE] = Close;
   return status;
}

void Unload(DRIVER_OBJECT* driver) {
   // 删除符号链接
   UNICODE_STRING DosSymName;
   RtlInitUnicodeString(&DosSymName, L"\DosDevices\device_001");
   IoDeleteSymbolicLink(&DosSymName);
   // 卸载设备对象
   IoDeleteDevice(driver->DeviceObject);
}

NTSTATUS Create(DEVICE_OBJECT* device, IRP* irp){
   device;
   KdPrint(("驱动被创建啦 "));
   // 设置IRP完成状态
   irp->IoStatus.Status = STATUS_SUCCESS;
   irp->IoStatus.Information = 0;
   IoCompleteRequest(irp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}
NTSTATUS Close(DEVICE_OBJECT* device, IRP* irp) {

   device;
   KdPrint(("驱动被关闭啦 "));
   // 设置IRP完成状态
   irp->IoStatus.Status = STATUS_SUCCESS;
   irp->IoStatus.Information = 0;
   IoCompleteRequest(irp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}

用户层: test.c

#include <Windows.h>
#include <stdio.h>

int main()
{
   // 1. 通过符号链接打开驱动
   HANDLE hSys = INVALID_HANDLE_VALUE;
   hSys = CreateFile(L"\??\device_001",
                     GENERIC_READ | GENERIC_WRITE,
                     FILE_SHARE_READ,
                     0,
                     OPEN_EXISTING,
                     FILE_ATTRIBUTE_NORMAL,
                     0);
   if (hSys == INVALID_HANDLE_VALUE) {
       printf("驱动打开失败:%d ",GetLastError());
   }
   else {
       printf("驱动打开成功 ");
   }

   system("pause");
}

从用户层将数据传递给驱动层

数据传递关键点

用户层 :

  1. 要将用户层的数据传递给内核层时, 使用WriteFile

  2. 想要从内核中获取数据, 在用户层中调用ReadFile

内核层:

  1. 从用户层传递下来的参数保存在IRP结构和IO_STACK_COMPLATE结构中.

    1. 其中, 用户输入/输出缓冲区保存在IRP结构中, IRP结构保存有三种缓冲区.

      这三种缓冲区分别是:

      1. irp->UserBuffer - 这是默认的缓冲区,该缓冲区是用户空间的内存, 在内核层中使用用户空间是很危险的,不建议使用这种默认方式.

      2. irp->AssociatedIrp.SystemBuffer - 在创建设备对象之后,将设备对象的Flags 字段加上DO_BUFFERED_IO标志就会使用这种方式, 使用这种方式时, 系统会将用户空间的数据拷贝一份到内核空间. 如果读写的数据量比较大就不推荐使用这种方式.

      3. irp->MdlAddress - - 在创建设备对象之后, 将设备对象的Flags字段加上DO_DIRECT_IO标志就会使用这种方式. 使用这种方式的优点是被读写的空间仍然是用户领空的, 只不过被内存描述符重新锁定了, 这样做比第一种安全且高效. 推荐使用这种方式. 使用这种方式, 想要获取缓冲区, 就需要调用函数: MmGetSystemAddressForMdlSafe

驱动代码(注意第26行代码): 代码第41行, 第90~112行

#include <ntddk.h>

void Unload(DRIVER_OBJECT* object);
NTSTATUS Create(DEVICE_OBJECT* device, IRP* irp);
NTSTATUS Close(DEVICE_OBJECT* device, IRP* irp);
NTSTATUS Write(DEVICE_OBJECT* device, IRP* irp);

NTSTATUS DriverEntry(DRIVER_OBJECT* driver, UNICODE_STRING* path)
{
   _asm int 3;
   path;
   NTSTATUS status = STATUS_SUCCESS;
   UNICODE_STRING NtDevName;
   RtlInitUnicodeString(&NtDevName, L"\Device\mydevice");

   // 1. 创建一个设备对象.否则无法绑定符号链接,没有符号链接, 用户层就连接不到内核层
   DEVICE_OBJECT* dev=NULL;
   status = IoCreateDevice(driver,
                           0,
                           &NtDevName,
                           FILE_DEVICE_UNKNOWN,
                           0,
                           0,
                           &dev);

   dev->Flags |= DO_DIRECT_IO; // 使用直接IO

   if (status != STATUS_SUCCESS)
       return status;

   UNICODE_STRING DosSymName;
   RtlInitUnicodeString(&DosSymName, L"\DosDevices\device_001");
   //2. 为设备对象绑定符号链接
   IoCreateSymbolicLink(&DosSymName, &NtDevName);

   //绑定卸载函数
   driver->DriverUnload = Unload;
   // 绑定派遣函数, 如果不绑定这两个函数, 用户层在打开驱动时会失败.
   driver->MajorFunction[IRP_MJ_CREATE] = Create;
   driver->MajorFunction[IRP_MJ_CLOSE] = Close;
   driver->MajorFunction[IRP_MJ_WRITE] = Write;
   return status;
}

void Unload(DRIVER_OBJECT* driver) {
   // 删除符号链接
   UNICODE_STRING DosSymName;
   RtlInitUnicodeString(&DosSymName, L"\DosDevices\device_001");
   IoDeleteSymbolicLink(&DosSymName);
   // 卸载设备对象
   IoDeleteDevice(driver->DeviceObject);
}

NTSTATUS Create(DEVICE_OBJECT* device, IRP* irp){
   device;
   KdPrint(("驱动被创建啦 "));
   // 设置IRP完成状态
   irp->IoStatus.Status = STATUS_SUCCESS;
   irp->IoStatus.Information = 0;
   IoCompleteRequest(irp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}
NTSTATUS Close(DEVICE_OBJECT* device, IRP* irp) {

   device;
   KdPrint(("驱动被关闭啦 "));
   // 设置IRP完成状态
   irp->IoStatus.Status = STATUS_SUCCESS;
   irp->IoStatus.Information = 0;
   IoCompleteRequest(irp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}

static void* getUserBuff(IRP* irp) {
   // 获取要写入的数据的缓冲区
   void* pBuff = NULL;
   if (irp->UserBuffer) {
       pBuff = irp->UserBuffer;
   }
   else if (irp->AssociatedIrp.SystemBuffer) {
       pBuff = irp->AssociatedIrp.SystemBuffer;
   }
   else if (irp->MdlAddress) {
       // 将内存锁定并映射到内核空间.
       pBuff = MmGetSystemAddressForMdlSafe(irp->MdlAddress, NormalPagePriority);
   }
   return pBuff;
}

NTSTATUS Write(DEVICE_OBJECT* device, IRP* irp) {
   device;
   KdPrint(("接收到数据: "));

   // 获取用户层传入进来的缓冲区
   char* pBuff = (char*)getUserBuff(irp);

   // 获取当前设备IRP栈位置
   IO_STACK_LOCATION* pIoStack = NULL;
   pIoStack = IoGetCurrentIrpStackLocation(irp);

   // 获取缓冲区的长度(字节数)
   ULONG dwLen = pIoStack->Parameters.Write.Length;

   // 打印,测试是否能够接收到正确的数据
   KdPrint(("字节数:%d 内容:%s", dwLen , pBuff));


   // 设置IRP完成状态
   irp->IoStatus.Status = STATUS_SUCCESS;
   irp->IoStatus.Information = 0;
   IoCompleteRequest(irp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}

用户层代码: 代码32~36行

// 三环程序.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <Windows.h>
#include <stdio.h>

int main()
{
   // 1. 通过符号链接打开驱动
   HANDLE hSys = INVALID_HANDLE_VALUE;
   hSys = CreateFile(L"\??\device_001",
                     GENERIC_READ | GENERIC_WRITE,
                     FILE_SHARE_READ,
                     0,
                     OPEN_EXISTING,
                     FILE_ATTRIBUTE_NORMAL,
                     0);
   if (hSys == INVALID_HANDLE_VALUE) {
       printf("驱动打开失败:%d ",GetLastError());
       system("pause");
       return 0;
   }
   else {
       printf("驱动打开成功 ");
   }

   const char* pStr = "我是从用户层传入的字符串";
   DWORD size = 0;

   // 发出Write的IO请求
   WriteFile(hSys, /*要写入数据的驱动*/
             pStr,
             strlen(pStr) + 1,
             &size,
             NULL);

   system("pause");
}

从内核层读取数据

内核层: 代码第43行, 第92~117行

#include <ntddk.h>

void Unload(DRIVER_OBJECT* object);
NTSTATUS Create(DEVICE_OBJECT* device, IRP* irp);
NTSTATUS Close(DEVICE_OBJECT* device, IRP* irp);
NTSTATUS Write(DEVICE_OBJECT* device, IRP* irp);
NTSTATUS Read(DEVICE_OBJECT* device, IRP* irp);

NTSTATUS DriverEntry(DRIVER_OBJECT* driver, UNICODE_STRING* path)
{
   _asm int 3;
   path;
   NTSTATUS status = STATUS_SUCCESS;
   UNICODE_STRING NtDevName;
   RtlInitUnicodeString(&NtDevName, L"\Device\mydevice");

   // 1. 创建一个设备对象.否则无法绑定符号链接,没有符号链接, 用户层就连接不到内核层
   DEVICE_OBJECT* dev=NULL;
   status = IoCreateDevice(driver,
                           0,
                           &NtDevName,
                           FILE_DEVICE_UNKNOWN,
                           0,
                           0,
                           &dev);

   dev->Flags |= DO_DIRECT_IO; // 使用直接IO

   if (status != STATUS_SUCCESS)
       return status;

   UNICODE_STRING DosSymName;
   RtlInitUnicodeString(&DosSymName, L"\DosDevices\device_001");
   //2. 为设备对象绑定符号链接
   IoCreateSymbolicLink(&DosSymName, &NtDevName);

   //绑定卸载函数
   driver->DriverUnload = Unload;
   // 绑定派遣函数, 如果不绑定这两个函数, 用户层在打开驱动时会失败.
   driver->MajorFunction[IRP_MJ_CREATE] = Create;
   driver->MajorFunction[IRP_MJ_CLOSE] = Close;
   driver->MajorFunction[IRP_MJ_WRITE] = Write;
   driver->MajorFunction[IRP_MJ_READ] = Read;
   return status;
}

void Unload(DRIVER_OBJECT* driver) {
   // 删除符号链接
   UNICODE_STRING DosSymName;
   RtlInitUnicodeString(&DosSymName, L"\DosDevices\device_001");
   IoDeleteSymbolicLink(&DosSymName);
   // 卸载设备对象
   IoDeleteDevice(driver->DeviceObject);
}

NTSTATUS Create(DEVICE_OBJECT* device, IRP* irp){
   device;
   KdPrint(("驱动被创建啦 "));
   // 设置IRP完成状态
   irp->IoStatus.Status = STATUS_SUCCESS;
   irp->IoStatus.Information = 0;
   IoCompleteRequest(irp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}
NTSTATUS Close(DEVICE_OBJECT* device, IRP* irp) {

   device;
   KdPrint(("驱动被关闭啦 "));
   // 设置IRP完成状态
   irp->IoStatus.Status = STATUS_SUCCESS;
   irp->IoStatus.Information = 0;
   IoCompleteRequest(irp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}

static void* getUserBuff(IRP* irp) {
   // 获取要写入的数据的缓冲区
   void* pBuff = NULL;
   if (irp->UserBuffer) {
       pBuff = irp->UserBuffer;
   }
   else if (irp->AssociatedIrp.SystemBuffer) {
       pBuff = irp->AssociatedIrp.SystemBuffer;
   }
   else if (irp->MdlAddress) {
       // 将内存锁定并映射到内核空间.
       pBuff = MmGetSystemAddressForMdlSafe(irp->MdlAddress, NormalPagePriority);
   }
   return pBuff;
}

NTSTATUS Read(DEVICE_OBJECT* device, IRP* irp) {
   device;

   NTSTATUS status = STATUS_SUCCESS;
   ULONG     size = 4;

   // 获取用户层传入进来的缓冲区
   char* pBuff = (char*)getUserBuff(irp);

   // 获取当前设备IRP栈位置
   IO_STACK_LOCATION* pIoStack = NULL;
   pIoStack = IoGetCurrentIrpStackLocation(irp);

   // 获取缓冲区的长度(字节数)
   ULONG dwLen = pIoStack->Parameters.Read.Length;

   if (dwLen < 6) {
       size = 6;
       status = STATUS_BUFFER_TOO_SMALL;
   }
   else {
       _asm push eax;
       _asm mov eax, dword ptr [pBuff];
       _asm sgdt[eax]; // 读取GDT表地址和大小.
       _asm pop eax
   }


   // 设置IRP完成状态
   irp->IoStatus.Status = status;
   irp->IoStatus.Information = size;
   IoCompleteRequest(irp, IO_NO_INCREMENT);
   return status;
}

NTSTATUS Write(DEVICE_OBJECT* device, IRP* irp) {
   device;
   KdPrint(("接收到数据: "));

   // 获取用户层传入进来的缓冲区
   char* pBuff = (char*)getUserBuff(irp);

   // 获取当前设备IRP栈位置
   IO_STACK_LOCATION* pIoStack = NULL;
   pIoStack = IoGetCurrentIrpStackLocation(irp);

   // 获取缓冲区的长度(字节数)
   ULONG dwLen = pIoStack->Parameters.Write.Length;

   // 打印,测试是否能够接收到正确的数据
   KdPrint(("字节数:%d 内容:%s", dwLen , pBuff));


   // 设置IRP完成状态
   irp->IoStatus.Status = STATUS_SUCCESS;
   irp->IoStatus.Information = 0;
   IoCompleteRequest(irp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}

用户层: 代码第38~43行

// 三环程序.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <Windows.h>
#include <stdio.h>

int main()
{
   // 1. 通过符号链接打开驱动
   HANDLE hSys = INVALID_HANDLE_VALUE;
   hSys = CreateFile(L"\??\device_001",
                     GENERIC_READ | GENERIC_WRITE,
                     FILE_SHARE_READ,
                     0,
                     OPEN_EXISTING,
                     FILE_ATTRIBUTE_NORMAL,
                     0);
   if (hSys == INVALID_HANDLE_VALUE) {
       printf("驱动打开失败:%d ",GetLastError());
       system("pause");
       return 0;
   }
   else {
       printf("驱动打开成功 ");
   }

   const char* pStr = "我是从用户层传入的字符串";
   DWORD size = 0;

   WriteFile(hSys,
             pStr,
             strlen(pStr) + 1,
             &size,
             NULL);

   DWORD read = 0;
   if (0 == ReadFile(hSys, NULL, 0, &read, 0)) {
       printf("读取失败,read = %d ",read);
   }

   char gdtr[6] = { 0 };
   ReadFile(hSys, gdtr, 6, &read, 0);

   printf("GDT=%08X SIZE=%04X ", *(unsigned int*)(gdtr+2), *(unsigned short*)(gdtr));
   system("pause");
}

使用DeviceIoContral进行内核通讯

通过符号链接打开设备对象之后, 使用ReadFileWriteFile就能和内核层进行通讯了, 但是这种通讯方式有一个缺点是: 只能读/写, 不能传递额外的命令, 例如,想让驱动读取整个GDT表的内容, 想让驱动遍历所有驱动对象, 这些命令都无法传达. 此时, 通过DeviceIoContral就可以办到

此函数调用之后, 驱动对象的派遣函数IRP_MJ_DEVICE_CONTROL会被调用. 在内核层的派遣函数中, 通过IO_STACK_COMPLATE.Parameters.DeviceIoControl.IoControlCode来得到用户层传入进来的控制码.

函数原型解析

BOOL
WINAPI
DeviceIoControl(
   _In_ HANDLE hDevice, /*通过CreateFile打开的设备对象句柄*/
   _In_ DWORD dwIoControlCode, /*控制码,重要*/
   _In_reads_bytes_opt_(nInBufferSize) LPVOID lpInBuffer, /*输入给驱动处理的换城区*/
   _In_ DWORD nInBufferSize,/*缓冲区的字节数*/
   _Out_writes_bytes_to_opt_(nOutBufferSize,*lpBytesReturned) LPVOID lpOutBuffer, /*用于接收驱动数据的缓冲区*/
   _In_ DWORD nOutBufferSize, /*缓冲区字节数*/
   _Out_opt_ LPDWORD lpBytesReturned, /*驱动实际要输出的字节数*/
   _Inout_opt_ LPOVERLAPPED lpOverlapped
   );

在这个函数中, 第二个参数dwIoControlCode非常重要. 此参数被称为IO控制代码. 这个代码由几个部分组成, 由不同的功能:

1566988471251

  • DeviceType - 设备类型标识, 此值必须和创建设备对象时给出的DEVICE_TYPE一致. 一般创建设备对象时,传递的是FILE_DEVICE_UNKNOWN那么此参数可以传递这个值.

  • FunctionCode - 功能代码, 这个代码是自定义的, 但是0~0x800已经被微软占用,可以使用大于0x800的值, 一般使用这个值最为一个功能代码传递给驱动, 例如,传递0x801可用于表示获取GDT,传递0x802用于表示隐藏一个进程等等,这些对应关系都是自定义的.

  • TransferType - 传输类型, 这个值非常的重要, 它决定了用户层的输入输出缓冲区和内核层的输入输出缓冲区采用何种方式传输. 它可以是以下三种值:

    • METHOD_BUFFERED (0) - 对I/O进行缓冲 ,将输入和输出的数据通过内核层空间拷贝

      • METHOD_IN_DIRECT (1) - 对输入不进行缓冲

      • METHOD_OUT_DIRECT (2) - 对输出不进行缓冲

      • METHOD_NEITHER (3) - 都不缓冲

      • 无论传递的是哪种方式, 在内核层中都可以通过irp->AssociatedIrp.SystemBuffer来获取到输入缓冲区.

  • RequiredAccess - 需要的访问权限: FILE_ANY_ACCESS, FILE_READ_DATA,FILE_WRITE_DATA

可以使用下面的宏来设置(这是微软的定义):

#define CTL_CODE(DeviceType/*设备类型*/, Function/*功能代码*/, Method/*传输方法*/, Access/*访问控制权限*/) (
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))

小结

  1. DeviceIoControl的参数2的dwIoControlCode在内核层的派遣函数中通过IO_STACK_COMPLATE.Parameters.DeviceIoControl.IoControlCode获取到

  2. DeviceIoControl的参数3:lpInBuffer以及参数5:lpOutBuffer在内核层的派遣函数中被合并在一起, 根据参数2的IO设备控制代码来决定使用IRP结构中的哪个字段作为输入输出缓冲区, 一般将传输方式指定为METHOD_OUT_DIRECTMETHOD_IN_DIRECT 时, 使用Irp->MdlAddress 来得到输入输出缓冲区

  3. DeviceIoControl的第4个,第6个参数nInBufferSize,nOutBufferSize在内核层的派遣函数中, 分别通过IO_STACK_COMPLATE.Parameters.DeviceIoControl.InputBufferLengthIO_STACK_COMPLATE.Parameters.DeviceIoControl.OutputBufferLength得到.

驱动层
NTSTATUS  DeviceCtrl(DEVICE_OBJECT* device, IRP* irp)     {
   device;
   NTSTATUS status       = STATUS_SUCCESS;
   ULONG     complateSize = 0;
   HANDLE       hFile = NULL;

   // 设置IRP完成状态
   irp->IoStatus.Status = STATUS_SUCCESS;


   // 1. 获取本层IO栈
   IO_STACK_LOCATION* ioStack = IoGetCurrentIrpStackLocation(irp);

   // 2. 获取传入的参数:
   // 2.1 IO设备控制码
   ULONG ctrlCode = ioStack->Parameters.DeviceIoControl.IoControlCode;

   // 2.2 输入缓冲区的字节数
   //ULONG inputSize = ioStack->Parameters.DeviceIoControl.InputBufferLength;

   // 2.3 获取输出缓冲区的字节数
   ULONG outputSize = ioStack->Parameters.DeviceIoControl.OutputBufferLength;

   // 2.4 获取输入缓冲区
   // 无论在控制码中指定了什么传输方式,都使用irp->AssociatedIrp.SystemBuffer
   // 来保存输入缓冲区的数据
   PVOID pInputBuff = irp->AssociatedIrp.SystemBuffer;

   // 2.5 获取输出缓冲区
   // 如果控制码指定了`METHOD_OUT_DIRECT`, 那么输出缓冲区就以MDL的形式
   // 传入, 如果在调用DeviceIoContral给输出缓冲区传递了NULL时,`MdlAddress`
   // 也会为NULL
   PVOID pOutBuff = NULL;  
   if (irp->MdlAddress && METHOD_FROM_CTL_CODE(ctrlCode) & METHOD_OUT_DIRECT) {
       pOutBuff = MmGetSystemAddressForMdlSafe(irp->MdlAddress,0);
   }

   // 2.6 根据控制码来进行操作
   switch (ctrlCode) // 根据功能号来执行不同功能.
   {
   case DF_GET_FILE_DATA: /*自定义的枚举类型*/
   {
       _asm int 3;
       UNICODE_STRING filePath;
       RtlInitUnicodeString(&filePath, (PCWSTR)pInputBuff);
       KdPrint(("文件路径:%wZ ", filePath));

       // 1. 打开文件
       status = createFile((LPWSTR)pInputBuff, GENERIC_READ, FILE_SHARE_READ, FILE_OPEN_IF, FALSE, &hFile);
       if (STATUS_SUCCESS != status) {
           complateSize = 0;
           break;
       }
       // 2. 获取文件大小
       ULONG64 fileSize = 0;
       status = getFileSize(hFile, &fileSize);
       if (STATUS_SUCCESS != status) {
           complateSize = 0;
           break;
       }

       // 3. 判断输出缓冲区的大小是否能够容纳
       if (pOutBuff==NULL || outputSize < fileSize) {
           // 如果缓冲区为NULL或者输出缓冲区的大小不够大
           // 则设置完成的字节数是所需字节数.
           // 然后不对缓冲区进行输出, 只设置
           // 缓冲区的大小,这样, DeviceIoControl
           // 返回之后,就能够得到所需字节数.
           complateSize = (ULONG)fileSize;
           status = STATUS_BUFFER_TOO_SMALL;// 设置状态码: 缓冲区大小
           break;
       }

       // 4. 读取文件到输出缓冲区
       ULONG read = 0;
       readFile(hFile, 0, 0, (ULONG)fileSize, pOutBuff, &read);

       // 5. 设置读取到的字节数
       complateSize = read; // 保存实际读取到的字节数
   }
   break;
   default:
       irp->IoStatus.Status = STATUS_UNSUCCESSFUL;
       break;
   }

   if (hFile != (HANDLE)-1) {
       ZwClose(hFile);
   }


   // 设置完成的字节数 , 注意, 如果irp->IoStatus.Status没有设置为STATUS_SUCCESS
   // 那么irp->IoStatus.Information字段的值是不会起到作用的.
   irp->IoStatus.Information = complateSize;

   IoCompleteRequest(irp, IO_NO_INCREMENT);
   // 返回状态,当用户层的DeviceIoControl函数返回后,通过GetLastError得到的就是这个结果.
   return status;
}
用户层
// 004_内核通讯_用户层_DeviceIoControl.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <stdio.h>
#include <windows.h>
#include "../003_内核通讯_内核层_DeviceIoControl/commonDef.h"


int main()
{
   HANDLE hSys = CreateFile(L"\??\device_002",
                             GENERIC_READ | GENERIC_WRITE,
                             FILE_SHARE_READ,
                             NULL,
                             OPEN_EXISTING,
                             FILE_ATTRIBUTE_NORMAL,
                             0);
   if (hSys == INVALID_HANDLE_VALUE) {
       printf("驱动打开失败:%d ", GetLastError());
       system("pause");
       return 0;
   }
   else {
       printf("驱动打开成功 ");
   }

   DWORD size = 0;

   const wchar_t* pStr = L"\??\C:\1.txt";
   int inputSize = wcslen(L"\??\C:\1.txt") * 2 + 2;

   // 第一次调用, 不知道保存文件数据的缓冲区的大小是多少字节
   // 可以将输出缓冲区传NULL,大小也传NULL, 通过函数
   // 输出的实际大小来申请缓冲区, 再次调用就能获取了.
   DeviceIoControl(hSys,
                   DF_GET_FILE_DATA,
                   (LPVOID)pStr,
                   inputSize,
                   NULL,/*输出缓冲区*/
                   0, /*输出缓冲区的字节数*/
                   &size,/*实际要输出的字节数*/
                   NULL);

   printf("size = %d ", size);

   // 得到大小之后再次调用函数.
   char* pBuff = new char[size+2];
   memset(pBuff, size+2, 0);
   DeviceIoControl(hSys,
                   DF_GET_FILE_DATA,
                   (LPVOID)pStr,
                   inputSize,
                   pBuff,
                   size,
                   &size,
                   NULL);
   printf("读取到的内容为:%s ", pBuff);

   delete[] pBuff;
   system("pause");
   CloseHandle(hSys);
}
原文地址:https://www.cnblogs.com/ltyandy/p/11425779.html