备注:
1.在下面的讨论中,"输入"表示数据从用户模式的应用程序到驱动程序,"输出"表示数据从驱动程序到应用程序。
2.IRP 中的 SystemBuffer 字段包含系统地址。UserBuffer 字段包含初始的用户缓冲区地址。
参考文章链接:
https://www.cnblogs.com/endenvor/p/9057856.html/ ,个人觉得原文有些乱,并且代码不完整,在此做了整理和补充。
一、I/O设备控制操作
R3通过DeviceControl函数来写入和读取R0的数据。R0层通过一下几种方式读取R3发送的数据和向R3发送数据。
(1)"缓冲"内存IOCTL -- METHOD_BUFFERED
对于 IOCTL 请求,会分配一个容量大小足以包含输入缓冲区或输出缓冲区的系统缓冲区,并将 SystemBuffer 设置为分配的缓冲区地址。输入缓冲区中的数据复制到系统缓冲区。UserBuffer 字段设置为用户模式输出缓冲区地址。内核模式驱动程序应当只使用系统缓冲区,且不应使用 UserBuffer 中存储的地址。
对于 IOCTL,驱动程序应当从系统缓冲区获取输入并将输出写入到系统缓冲区。当完成请求时,I/O 系统将输出数据从系统缓冲区复制到用户缓冲区。(ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。)
输入缓冲区大小:stack->Parameters.DeviceIoControl.InputBufferLength
输出缓冲区大小:stack->Parameters.DeviceIoControl.OutputBufferLength
输入缓冲区:pIrp->AssociatedIrp.SystemBuffer
输出缓冲区:pIrp->AssociatedIrp.SystemBuffer
(2)"直接"方法IOCTL -- METHOD_IN_DIRECT/METHOD_OUT_DIRECT
对于读取和写入请求,用户模式缓冲区会被锁定,并且会重新映射一个内存描述符列表 (MDL)。MDL 地址会存储在 IRP 的 MdlAddress 字段中。根据 Irp->MdlAddress 重新为这片物理地址映射一份虚拟地址。通过这份虚拟地址向R3层发送数据。
SystemBuffer 和 UserBuffer 均没有任何含义。但是,驱动程序不应当更改这些字段的值。
输入缓冲区大小:stack->Parameters.DeviceIoControl.InputBufferLength
输出缓冲区大小:stack->Parameters.DeviceIoControl.OutputBufferLength
输入缓冲区:MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority)
输出缓冲区:pIrp->AssociatedIrp.SystemBuffer
METHOD_IN_DIRECT/METHOD_OUT_DIRECT区别:
1) 只读权限打开设备,METHOD_IN_DIRECT的IOCTL操作成功,而METHOD_OUT_DIRECT的IOCTL操作失败
2) 读写权限打开设备,METHOD_IN_DIRECT与METHOD_OUT_DIRECT的IOCTL操作都成功
(3)"两种都不"方法IOCTL(其他内存模式) -- MEHTOD_NEITHER
对于 IOCTL 请求,I/O 管理器将 UserBuffer 设置为初始的用户输出缓冲区,而且,它将当前 I/O 栈位置的 Parameters.DeviceIoControl.Type3InputBuffer 设置为用户输入缓冲区。利用该 I/O 方法,由驱动程序来确定如何处理缓冲区:分配系统缓冲区或创建 MDL。
输入缓冲区大小:stack->Parameters.DeviceIoControl.InputBufferLength
输出缓冲区大小:stack->Parameters.DeviceIoControl.OutputBufferLength
输入缓冲区:ProbeForRead(stack->Parameters.DeviceIoControl.Type3InputBuffer)
输出缓冲区:ProbeForWrite(pIrp->UserBuffer)
二、读写操作
分为一下三种,ReadFile,WirteFile方式的缓冲区设备读写,直接方式读写,和其他方式读写(DO_BUFFERED_IO、DO_DIRECT_IO、METHOD_NEITHER)。
在R3层通过 ReadFile,WriteFile 函数进行读写数据,在R0层通过 IRP_MJ_READ 与 IRP_MJ_WRITE 对应的派遣函数中处理以下三种缓存区来进行数据读写。
(1)缓冲区方式读写 -- DO_BUFFERED_IO
在创建 Device 后,须要指定方式为 Device 的 Flags 有 DO_BUFFERED_IO。通过应用层 Api 函数 ReadFile,WriteFile 等函数。ntoskrnl.exe创建Irp后,ReadFile和WriteFile参数的缓冲区就在irp->AssociatedIrp.Systembuffer。
同时要求读写的偏移量,和长度都在 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp) 数据类型中的stack->Parameters.Read.Length,stack->Parameters.Read.ByteOffset(该类型为Large_Interge类型)。
对于读取请求,I/O 管理器分配一个与用户模式的缓冲区大小相同的系统缓冲区 SystemBuffer。当完成请求时,I/O 管理器将驱动程序已经提供的数据从系统缓冲区复制到用户缓冲区。
对于写入请求,会分配一个系统缓冲区并将 SystemBuffer 设置为地址。用户缓冲区的内容会被复制到系统缓冲区,但是不设置 UserBuffer。
ReadFile /WriteFile
读取/写入字节数:stack->Parameters.Read.Length
偏移:stack->Parameters.Read.ByteOffset.QuadPart
输出/写入缓冲区:pIrp->AssociatedIrp.SystemBuffer
输出/返回字节数:pIrp->IoStatus.Information
IRP_MJ_QUERY_INFORMATION
FILE_INFORMATION_CLASS: stack->Parameters.QueryFile.FileInformation
输入输出缓冲区:pIrp->AssociatedIrp.SystemBuffer
返回字节数:pIrp->IoStatus.Information=stack->Parameters.QueryFile.Length
(2)直接方式读写 -- DO_DIRECT_IO
在创建 Device 后,须要指定方式为 Device 的 Flags 有 DO_DIRECT_IO 。通过应用层 APi 函数 ReadFile,WriteFile 等函数,ntoskrnl.exe创建的Irp后,ReadFile 和 WriteFile 参数的缓冲区将被锁住,然后操作系统将这段缓冲区在内核模式地址再次映射一遍,这样应用层的缓冲区和内存层的就指向同一个物理内存。而内核模式用MDL数据结构记录这段内存,这个虚拟内存大小在MmGetByteCount(pIrp->MdlAddress),首地址在MmGetMdlVirtualAddress(pIrp->MdlAddress);
偏移量为MmGetMdlByteOffset(pIrp->MdlAddress)(这里的偏移量不是文件读写的偏移量,而是在MDL中的偏移量),然后文件的长度还是stack->Parameters.Read.Length,这个值和MmGetByteCount(pIrp->MdlAddress)是一样的,要不然就出错了,而真正的读写偏移量还是在stack->Parameters.Read.ByteOffset。
这里无论是读还是写,都要得到MDL在内核模式下的映射,因此还要用MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority)在内核中根据 pIrp->MdlAddress 重新为其对应的物理地址映射一份虚拟地址,然后可以读写该地址,就会转化到应用层相应的内存。
读取/写入字节数:stack->Parameters.Read.Length
偏移:stack->Parameters.Read.ByteOffset.QuadPart
输出/写入缓冲区:MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority)
输出/返回字节数:pIrp->IoStatus.Information
(3)其他方式读写 -- METHOD_NEITHER
在创建 Device 后,Flags 既不标志 DO_BUFFERED_IO 也不标志 DO_DIRECT_IO,ReadFile 和 WriteFile 提供的缓冲区内存地址,可以再 IRP 的 pIrp->UserBuffer 字段得到,而长度和偏移量还是在 stack->Paameters.Read 中。
但是用这种方法须要注意的是 ReadFile 可能把空指针地址或者非法地址传递给驱动程序,因此驱动程序使用用户模式地址钱须要检查是否可读或者可写,可以用 ProbeForWrite 或者 ProbeForWrite 函数和try模块。
对于写请求,UserBuffer 字段被设置为指向输出缓冲区。
对于读请求,Type3InputBuffer 字段被设置为输入缓冲区。不执行任何其他操作。
SystemAddress 和 MdlAddress 没有任何含义。
读取/写入字节数:stack->Parameters.Read.Length
偏移:stack->Parameters.Read.ByteOffset.QuadPart
输出/写入缓冲区:ProbeForWrite(pIrp->UserBuffer)
输出/返回字节数:pIrp->IoStatus.Information
R0层代码:
#include <ntddk.h> #define CTRL_CODE1 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_IN_DIRECT, FILE_ANY_ACCESS) // "直接"方法 #define CTRL_CODE2 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS) // "缓冲"方法 #define CTRL_CODE3 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_NEITHER, FILE_ANY_ACCESS) // "两者都不"方法 // 创建一个设备对象 NTSTATUS CreateDevice(PDRIVER_OBJECT DriverObject, PUNICODE_STRING DeviceName, PUNICODE_STRING SymbolName, UINT32 DO) { PDEVICE_OBJECT DeviceObject = NULL; // 1. 创建一个设备对象 NTSTATUS Status = IoCreateDevice(DriverObject, 0, DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject); if (NT_SUCCESS(Status)) { // 2. 添加符号链接名 Status = IoCreateSymbolicLink(SymbolName, DeviceName); // 3. 设置设备的通讯方式 DeviceObject->Flags |= DO; } // 4. 设置返回值 return Status; } NTSTATUS DefaultProc(struct _DEVICE_OBJECT *DeviceObject, struct _IRP *Irp) { UNREFERENCED_PARAMETER(DeviceObject); // 1. 设置处理的字节数量 Irp->IoStatus.Information = 0; // 2. 设置状态 Irp->IoStatus.Status = STATUS_SUCCESS; // 3. 表示已完成所有IRP处理 IoCompleteRequest(Irp, IO_NO_INCREMENT); // 4. 设置返回值 return STATUS_SUCCESS; } // 不设置这个函数,则Ring3调用CreateFile会返回1 // IRP_MJ_CREATE 处理函数 NTSTATUS CreateProc(struct _DEVICE_OBJECT *DeviceObject, struct _IRP *Irp) { UNREFERENCED_PARAMETER(DeviceObject); // 1. 设置处理的字节数量 Irp->IoStatus.Information = 0; // 2. 设置状态 Irp->IoStatus.Status = STATUS_SUCCESS; // 3. 表示已完成所有IRP处理 IoCompleteRequest(Irp, IO_NO_INCREMENT); // 4. 设置返回值 return Irp->IoStatus.Status; } /*******************************缓冲区方式读写操作*********************************/ NTSTATUS WriteProc(struct _DEVICE_OBJECT *DeviceObject, struct _IRP *Irp) { UNREFERENCED_PARAMETER(DeviceObject); PIO_STACK_LOCATION pstack = IoGetCurrentIrpStackLocation(Irp); ULONG WriteSize = pstack->Parameters.Write.Length; char *Buffer = NULL; // 判断读写方式 if (Irp->AssociatedIrp.SystemBuffer) // DO_BUFFERED_IO 缓冲方法 Buffer = Irp->AssociatedIrp.SystemBuffer; else if (Irp->MdlAddress) // DO_DIRECT_IO 直接方法 Buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority); else if (Irp->UserBuffer) // METHOD_NEITHER 方法 Buffer = Irp->UserBuffer; // 对该地址区域写入一些数据,让程序读出去 KdPrint(("缓冲区方式读写操作 - 来自R3的数据:%S\n", Buffer)); Irp->IoStatus.Information = WriteSize; Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } NTSTATUS ReadProc(struct _DEVICE_OBJECT *DeviceObject, struct _IRP *Irp) { UNREFERENCED_PARAMETER(DeviceObject); char *Buffer = NULL; // 判断读写方式 if (Irp->AssociatedIrp.SystemBuffer) // DO_BUFFERED_IO 缓冲方法 Buffer = Irp->AssociatedIrp.SystemBuffer; else if (Irp->MdlAddress) // DO_DIRECT_IO 直接方法 Buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority); else if (Irp->UserBuffer) // METHOD_NEITHER 方法 Buffer = Irp->UserBuffer; // 对该地址区域写入一些数据,让程序读出去 RtlCopyMemory(Buffer, L"Hello 15PB", 22); Irp->IoStatus.Information = 22; Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } /*******************************IO设备控制操作**********************************/ NTSTATUS DeviceIoCtrlProc(struct _DEVICE_OBJECT *DeviceObject, struct _IRP *Irp) { UNREFERENCED_PARAMETER(DeviceObject); PIO_STACK_LOCATION pStack = IoGetCurrentIrpStackLocation(Irp); ULONG CtrlCode = pStack->Parameters.DeviceIoControl.IoControlCode; ULONG OutputLength = pStack->Parameters.DeviceIoControl.OutputBufferLength; ULONG Information = 0; switch (CtrlCode) { // "直接"方法 case CTRL_CODE1: { // 输入缓冲I/O方法,来自R3的数据打印 char *SystemBuffer = Irp->AssociatedIrp.SystemBuffer; KdPrint(("IO设备控制操作 - 缓冲方式 - 来自R3的数据:%S\n", SystemBuffer)); Information = 64; // 直接输出 // MDL 地址会存储在 IRP 的 MdlAddress 字段中。根据 Irp->MdlAddress 重新为这片物理地址映射一份虚拟地址。 // MmGetSystemAddressForMdlSafe是一个宏,它为MDL描述的缓冲区返回一个非分页的系统空间虚拟地址。 char *MdlBuffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority); // Irp->MdlAddress:0xfffffa80`1afe6010 RtlCopyMemory(MdlBuffer, L"CTRL_CODE1 - METHOD_IN_DIRECT\n", Information); // MdlBuffer:0xfffff880`0bf968f0 break; } // 输入输出缓冲I/O方法 case CTRL_CODE2: { // IRP 中的 SystemBuffer 字段包含系统地址。UserBuffer 字段包含初始的用户缓冲区地址。 // 对于读取请求,I/O 管理器分配一个与用户模式的缓冲区大小相同的系统缓冲区。 // 当完成请求时,I/O 管理器将驱动程序已经提供的数据从系统缓冲区复制到用户缓冲区。 // 对于写入请求,会分配一个系统缓冲区并将 SystemBuffer 设置为地址。 // 用户缓冲区的内容会被复制到系统缓冲区,但是不设置 UserBuffer。 char *Buffer = Irp->AssociatedIrp.SystemBuffer; KdPrint(("IO设备控制操作 - 缓冲方式 - 来自R3的数据:%S\n", Buffer)); // 输入缓冲I/O方法,来自R3的数据打印 Information = 62; RtlCopyMemory(Buffer, L"CTRL_CODE2 - METHOD_BUFFERED\n", Information); // 输出缓冲I/O方法,R0传送数据 break; } // "两种都不"方法 case CTRL_CODE3: { // 对于写请求,UserBuffer 字段被设置为指向输出缓冲区。 // 对于读请求,Type3InputBuffer 字段被设置为输入缓冲区。 // 不执行任何其他操作。SystemAddress 和 MdlAddress 没有任何含义。 char *InBuffer = pStack->Parameters.DeviceIoControl.Type3InputBuffer; char *OutBuffer = Irp->UserBuffer; KdPrint(("IO设备控制操作 - 两种都不方式 - 来自R3的数据:%S\n", InBuffer)); // "两种都不"方法,来自R3的数据打印 Information = 60; RtlCopyMemory(OutBuffer, L"CTRL_CODE3 - METHOD_NEITHER\n", Information); break; } default: KdPrint(("failed!\n")); break; } // 1. 设置处理的字节数量 Irp->IoStatus.Information = OutputLength; // 2. 设置状态 Irp->IoStatus.Status = STATUS_SUCCESS; // 3. 表示已完成所有IRP处理 IoCompleteRequest(Irp, IO_NO_INCREMENT); // 4. 设置返回值 return STATUS_SUCCESS; } // 卸载函数 void DriverUnload(PDRIVER_OBJECT DriverObject) { // 获取设备对象链表 PDEVICE_OBJECT DeviceObject = DriverObject->DeviceObject; for (int i = 0; i < 3; ++i) { IoDeleteDevice(DriverObject->DeviceObject); DeviceObject = DeviceObject->NextDevice; } // 卸载设备符号名 UNICODE_STRING SymbolLinkName1 = RTL_CONSTANT_STRING(L"\\DosDevices\\devicelink1"); UNICODE_STRING SymbolLinkName2 = RTL_CONSTANT_STRING(L"\\DosDevices\\devicelink2"); UNICODE_STRING SymbolLinkName3 = RTL_CONSTANT_STRING(L"\\DosDevices\\devicelink3"); IoDeleteSymbolicLink(&SymbolLinkName1); IoDeleteSymbolicLink(&SymbolLinkName2); IoDeleteSymbolicLink(&SymbolLinkName3); } NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING UnicodeString) { //DbgBreakPoint(); UNREFERENCED_PARAMETER(UnicodeString); // 创建不同读写方式的设备 UNICODE_STRING DeviceName1 = RTL_CONSTANT_STRING(L"\\Device\\device1"); UNICODE_STRING SymbolName1 = RTL_CONSTANT_STRING(L"\\DosDevices\\devicelink1"); if (NT_SUCCESS(CreateDevice(DriverObject, &DeviceName1, &SymbolName1, DO_BUFFERED_IO))) KdPrint(("Create device success: \\Device\\device1\n")); UNICODE_STRING DeviceName2 = RTL_CONSTANT_STRING(L"\\Device\\device2"); UNICODE_STRING SymbolName2 = RTL_CONSTANT_STRING(L"\\DosDevices\\devicelink2"); if (NT_SUCCESS(CreateDevice(DriverObject, &DeviceName2, &SymbolName2, DO_DIRECT_IO))) KdPrint(("Create device success: \\Device\\device2\n")); UNICODE_STRING DeviceName3 = RTL_CONSTANT_STRING(L"\\Device\\device3"); UNICODE_STRING SymbolName3 = RTL_CONSTANT_STRING(L"\\DosDevices\\devicelink3"); if (NT_SUCCESS(CreateDevice(DriverObject, &DeviceName3, &SymbolName3, 0))) KdPrint(("Create device success: \\Device\\device3\n")); // 设置卸载函数 DriverObject->DriverUnload = DriverUnload; // 初始化IRP派遣函数 for (UINT32 i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) DriverObject->MajorFunction[i] = DefaultProc; DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateProc; DriverObject->MajorFunction[IRP_MJ_READ] = ReadProc; DriverObject->MajorFunction[IRP_MJ_WRITE] = WriteProc; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DeviceIoCtrlProc; return STATUS_SUCCESS; }
R3层代码:
#include <cstdio> #include <windows.h> #include <tchar.h> #define CTRL_CODE1 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_IN_DIRECT, FILE_ANY_ACCESS) // "直接"方法 #define CTRL_CODE2 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS) // "缓冲"方法 #define CTRL_CODE3 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_NEITHER, FILE_ANY_ACCESS) // "两者都不"方法 int main() { _tsetlocale(0, _T("Chinese-simplified")); //支持中文 // 创建内核对象的设备对象连接 --- 对应驱动层中 IRP_MJ_WRITE 请求的过滤函数 HANDLE DeviceHandle1 = CreateFile(L"\\\\.\\devicelink1", FILE_ALL_ACCESS, FILE_SHARE_WRITE | FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); HANDLE DeviceHandle2 = CreateFile(L"\\\\.\\devicelink2", FILE_ALL_ACCESS, FILE_SHARE_WRITE | FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); HANDLE DeviceHandle3 = CreateFile(L"\\\\.\\devicelink3", FILE_ALL_ACCESS, FILE_SHARE_WRITE | FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); WCHAR ReadBuffer[100] = { 0 }; DWORD ReadByte = 0; DWORD WriteByte = 0; /**********************************************************************************************/ // 使用WriteFile、ReadFile函数传送数据 --- 对应驱动层中 IRP_MJ_CREATE 与 IRP_MJ_READ 请求的过滤函数 // 根据驱动中Device创建时所选择的通信缓冲方法,进行数据传送 wprintf(L"缓冲区方式读写操作:\n"); printf("(1)device1 - DO_BUFFERED_IO:\n"); WriteFile(DeviceHandle1, L"device1 - DO_BUFFERED_IO", 52, &WriteByte, NULL); // DeviceHandle1 创建时选择了 DO_BUFFERED_IO 缓冲方法 printf("\tWriteBytes: %d\n", WriteByte); ReadFile(DeviceHandle1, ReadBuffer, 100, &ReadByte, NULL); wprintf(L"\tRead: %d(%s)\n\n", ReadByte, ReadBuffer); // 打印出22<Hello 15PB>,这是对的 printf("(2)device2 - DO_DIRECT_IO:\n"); WriteFile(DeviceHandle2, L"device2 - DO_DIRECT_IO", 48, &WriteByte, NULL); // DeviceHandle2 创建时选择了 DO_DIRECT_IO 直接方法 printf("\tWriteBytes: %d\n", WriteByte); ReadFile(DeviceHandle2, ReadBuffer, 100, &ReadByte, NULL); wprintf(L"\tRead: %d(%s)\n\n", ReadByte, ReadBuffer); // 打印出22<Hello 15PB>,这是对的 printf("这个有BUG\n"); printf("(3)device3 - METHOD_NEITHER:\n"); WriteFile(DeviceHandle3, L"device3 - METHOD_NEITHER", 32, &WriteByte, NULL); // DeviceHandle3 创建时选择了 METHOD_NEITHER 方法 printf("\tdevice3 - Write: %d\n", WriteByte); ReadFile(DeviceHandle3, ReadBuffer, 100, &ReadByte, NULL); printf("\tdevice3 - Read: %d(%Ls)\n\t", ReadByte, ReadBuffer); // 打印出22<H>,有bug?? /**********************************************************************************************/ // 使用DeviceIoControl函数传送数据 --- 对应驱动层中Write与Read的过滤函数 wprintf(L"\n\nIO设备控制操作:\n"); DeviceIoControl(DeviceHandle1, CTRL_CODE1, (LPVOID)L"CTRL_CODE1", 24, ReadBuffer, 100, &ReadByte, NULL); // "直接"方法 wprintf(L"(1)直接方法: \n\t ReadByte - ReadBuffer: %d - %s\n", ReadByte, ReadBuffer); DeviceIoControl(DeviceHandle1, CTRL_CODE2, (LPVOID)L"CTRL_CODE2", 24, ReadBuffer, 100, &ReadByte, NULL); // 输入输出缓冲I/O方法 wprintf(L"(2)输入输出缓冲I/O方法: \n\t ReadByte - ReadBuffer: %d - %s\n", ReadByte, ReadBuffer); DeviceIoControl(DeviceHandle1, CTRL_CODE3, (LPVOID)L"CTRL_CODE3", 24, ReadBuffer, 100, &ReadByte, NULL); // 上面方法都不是(METHOD_NEITHER) wprintf(L"(3)METHOD_NEITHER方法: \n\t ReadByte - ReadBuffer: %d - %s\n", ReadByte, ReadBuffer); system("pause"); return 0; }