串口过滤驱动learning
----------------------------------------------------------------------------------Cherry-2016-5-31-----------------------
先看一张图,是我按照<windows内核安全与驱动开发>内容编写编译的,然后用visualddk在win_7_x86 (目标机)debug显示出来的,首先在线 download 一个windows 7超级终端软件,然后新建终端,com1,输入数据传输,最后就在debugview 中显示出来如图一连串的1咯。
Windows安全模型链接:
http://blog.csdn.net/dpsying/article/details/47136191
代码分为以下几层:
DriverEntry()为主函数
Dispatch()函数用来截取I/O管理器发来的irp,并做出相关处理。
Unload()函数用来动态卸载,主要是为了清理分配的内存,因为内核开发的程序如果不手动清理,它会永久占用。
AttachAllCom()用来绑定所有已知的com(串口),
其中有AttachDevice()和OpenComs()。利用一个循 环,OpenAllComs,然后将所有com attach to virtual device(IoCreateDevice).
接下来分析代码:
#define CCP_MAX_COM_ID 32 //表示最大的串口数
Driver Entry:
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)
{
unsigned i;
DbgPrint("Driver Entry");
for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;i++)
{
driver->MajorFunction[i] = ccpDispatch;
}
driver->DriverUnload = ccpUnload;
ccpAttachAllComs(driver);
return STATUS_SUCCESS;
}
注:那个DbgPrint不能写在unsigned i;的前面,会报错,很迷惑。
其中,只在主函数调用一个函数:ccpAttachAllComs();链接所有串口。没错,是手动调用,它并不像其它比如ccpUnload函数那样,由driver的DriverUnload调用(这里说调用,不如说是将ccpUnload函数的指针赋给了driver->DriverUnload,进一步在driver结束时完成driver的卸载)。
当然,ccpDispatch()也是同理,将driver 的所有MajorFunction交给ccpDispatch处理。然后可以在ccpDispatch中进行一系列分配操作。
ccpDispatch:
NTSTATUS ccpDispatch(PDEVICE_OBJECT device,PIRP irp)
{
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
NTSTATUS status;
ULONG i,j;
for(i=0;i<CCP_MAX_COM_ID;i++){
if(s_fltobj[i] == device){
if(irpsp->MajorFunction == IRP_MJ_POWER){
PoStartNextPowerIrp(irp);
IoSkipCurrentIrpStackLocation(irp);
return PoCallDriver(s_nextobj[i],irp);
}
if(irpsp->MajorFunction == IRP_MJ_WRITE){
ULONG len = irpsp->Parameters.Write.Length;
PUCHAR buf = NULL;
if(irp->MdlAddress != NULL)
buf = (PUCHAR)MmGetSystemAddressaForMdlSafe(irp->MdlAddress,
NormalPagePriority);
else buf = (PUCHAR)irp->UserBuffer;
if(buf == NULL) buf = (PUCHAR)irp->AssociatedIrp.SystemBuffer;
for(j=0;j<len;++j) DbgPrint("comcap: Send Data: %2x\r\n",buf[j]);
}
IoSkipCurrentIrpStackLocation(irp);
return IoCallDriver(s_nextobj[i],irp);
}
}
irp->IoStatus.Information = 0;
irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
IoCompleteRequest(irp,IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
有点小长,也有好多函数,好吧从头分析。
此函数传入设备指针(PDEVICE_OBJECT)和从I/O管理器获得的Irp指针(PIRP)
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
//从Irp栈中获得当前传入的irp位置
因为并不知道irp是传给哪个物理串口的,所以需要一个for循环来一个一个判断,如果目标设备和设备栈中的吻合,也就是相等,就进行下去。这儿有俩irp操作:IRP_MJ_POWER,IRP_MJ_WRITE,一个是电源操作,一个是写操作。(在这儿就可以拦截住对串口的一系列操作,比如想要给目标usb设备传入本机数据,这里就可以进行过滤操作)。
假如是电源操作,则PoStartNextPowerIrp(irp);文档里这样解释:
Although power IRPs are completed only once, typically by the bus driver for a device, each driver in the device stack must call PoStartNextPowerIrp as the IRP travels down or back up the stack. Even if a driver fails the IRP, the driver must nevertheless call PoStartNextPowerIrp to signal the power manager that it is ready to handle another power IRP.
表示提示电源管理去准备去处理另一个电源IRP,也就是说,略过当前powerirp。IoSkipCurrentIrpStackLocation(irp);文档解释:
The IoSkipCurrentIrpStackLocation macro modifies the system's IO_STACK_LOCATION array pointer, so that when the current driver calls the next-lower driver, that driver receives the same IO_STACK_LOCATION structure that the current driver received.
When your driver sends an IRP to the next-lower driver, your driver can call IoSkipCurrentIrpStackLocation if you do not intend to provide an IoCompletion routine (the address of which is stored in the driver's IO_STACK_LOCATION structure). If you call IoSkipCurrentIrpStackLocation before calling IoCallDriver, the next-lower driver receives the same IO_STACK_LOCATION that your driver received.
最后返回PoCallDriver(s_nextobj[i],irp);说明已经被处理。
Beginning with Windows Vista, drivers should call IoCallDriver, not PoCallDriver to pass a power IRP to the next-lower driver. However, on Windows Server 2003, Windows XP, and Windows 2000, drivers must call PoCallDriver, not IoCallDriver to pass a power IRP to the next-lower driver. On Windows Server 2003, Windows XP, an Windows 2000, drivers must also call PoStartNextPowerIrp before calling PoCallDriver.
(补一句,这个代码是基于win xp 的,所以会出现这种老掉牙的情况)
这里的意思是,只要是电源操作,一律放过,不做详细处理。
假如是IRP_MJ_WRITE,
注:执行条件为,传输方式必须是DIRECT I/O.然后它的MajorFunction Code才会取到IRP_MJ_WRITE/IRP_MJ_READ...
MdlAddress (可以说是用户缓冲区)
Pointer to an MDL describing a user buffer, if the driver is using direct I/O, and the IRP major function code is one of the following:
Every device driver that transfers data from the system to its device must handle write requests in a DispatchWrite or DispatchReadWrite routine, as must any higher-level driver layered over such a device driver.
Possibly, a user-mode application or Win32 component with a handle for the file object representing the target device object has requested a data transfer to the device.
Possibly, a higher-level driver has created and set up the write IRP.
则先读取PIO_STACK_LOCATION的参数写的长度:
ULONG len = irpsp->Parameters.Write.Length;
struct {
ULONG Length;
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;
} Write;
文档里这样描述Parameters.Write的结构。
The driver's I/O stack location in the IRP indicates how many bytes to transfer at Parameters.Write.Length。
然后获得缓冲区:
如果Irp的MdlAddress不为空:
IRP_MJ_READ (Irp的读指令)
The MDL describes an empty buffer that the device or driver fills in.
IRP_MJ_WRITE (Irp的写指令)
The MDL describes a buffer that contains data for the device or driver.
IRP_MJ_DEVICE_CONTROL or IRP_MJ_INTERNAL_DEVICE_CONTROL (设备控制指令)
If the IOCTL code specifies the METHOD_IN_DIRECT transfer type, the MDL describes a buffer that contains data for the device or driver.
If the IOCTL code specifies the METHOD_OUT_DIRECT transfer type, the MDL describes an empty buffer that the device or driver fills in.
For more information about the buffers that are associated with METHOD_IN_DIRECT and METHOD_OUT_DIRECT transfer types in IOCTL codes, see Buffer Descriptions for I/O Control Codes.
If the driver is not using direct I/O, this pointer is NULL.
则通过内存函数获得系统非分页内存的虚拟地址,传入参数分别为:irp指向的一个相应的虚拟地址映射的缓冲区,普通优先级的虚拟内存页。
The MmGetSystemAddressForMdlSafe macro returns a nonpaged system-space virtual address for the buffer that the specified MDL describes.
PVOID
MmGetSystemAddressForMdlSafe(
__in PMDL Mdl,
__in MM_PAGE_PRIORITY Priority
);
Mdl (说白了,就是一虚拟空间,将分散的内存块连接起来,构成MDL链表)
Pointer to a buffer whose corresponding base virtual address is to be mapped.
Priority
Specifies an MM_PAGE_PRIORITY value that indicates the importance of success under low available PTE conditions. Possible values include LowPagePriority, NormalPagePriority, and HighPagePriority.
· LowPagePriority indicates that the mapping request can fail if the system is fairly low on resources. An example of this situation is a noncritical network connection where the driver can handle the mapping failure.
· NormalPagePriority indicates that the mapping request can fail if the system is very low on resources. An example of this situation is a noncritical local file system request.
· HighPagePriority indicates that the mapping request must not fail unless the system is completely out of resources. An example of this situation is the paging file path in a driver.
MmGetSystemAddressForMdlSafe returns the base system-space virtual address that maps the physical pages that the specified MDL describes. If the pages are not already mapped to system address space and the attempt to map them fails, NULL is returned.
如果Irp的MdlAddress为空:
buf = (PUCHAR)irp->UserBuffer;
UserBuffer
Contains the address of an output buffer if the major function code in the I/O stack location is IRP_MJ_DEVICE_CONTROL or IRP_MJ_INTERNAL_DEVICE_CONTROL and the I/O control code was defined with METHOD_NEITHER.
给buf传入irp的用户缓冲区。
最后判断传入的缓冲区是否为空,如果为空,则传入系统缓冲区。
buf = (PUCHAR)irp->AssociatedIrp.SystemBuffer;
union {
struct _IRP *MasterIrp;
.
.
PVOID SystemBuffer;
} AssociatedIrp;
最后打印内容:(打印的是得到缓冲区内的字符)
for(j=0;j<len;++j)
{
DbgPrint("comcap: Send Data: %2x\r\n",
buf[j]);
}
irp->IoStatus.Information = 0;
//IoStatus
Contains the IO_STATUS_BLOCK structure in which a driver stores status and information before calling IoCompleteRequest.
Information
This is set to a request-dependent value. For example, on successful completion of a transfer request, this is set to the number of bytes transferred. If a transfer request is completed with another STATUS_XXX, this member is set to zero.
irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
//A parameter in the VRP is invalid.
IoCompleteRequest(irp,IO_NO_INCREMENT);
The IoCompleteRequest routine indicates that the caller has completed all processing for a given I/O request and is returning the given IRP to the I/O manager.
VOID
IoCompleteRequest(
IN PIRP Irp,
IN CCHAR PriorityBoost
);
When a driver has finished all processing for a given IRP, it calls IoCompleteRequest.
ccpAttachAllComs():
传入PDRIVER_OBJECT driver 对象,然后设置一个PDEVICE_OBJECT来存储打开的Com对象。
用一个for循环,先打开每一个Com,然后绑定在生成的Device Object数组内对应的对象(ccpAttachDevice)
for(i = 0;i<CCP_MAX_COM_ID;i++){
com_ob = ccpOpenCom(i,&status);
if(com_ob == NULL)
continue;
ccpAttachDevice(driver,com_ob,&s_fltobj[i],&s_nextobj[i]);
}
接下来,一一分析:
ccpOpenCom()传入 第i个串口,打开状态的引用。
ccpAttachDevice()传入 驱动对象,打开的串口设备,生成的过滤设备的引用,最终绑定后的设备的引用。
ccpOpenCom():
PDEVICE_OBJECT ccpOpenCom(ULONG id,NTSTATUS *status){
UNICODE_STRING name_str;
static WCHAR name[32] = { 0 };
PFILE_OBJECT fileobj = NULL;
PDEVICE_OBJECT devobj = NULL;
memset(name,0,sizeof(WCHAR)*32);
RtlStringCchPrintfW(name,32,L"\\Device\\Serial%d",id);
RtlInitUnicodeString(&name_str,name);
//打开设备对象,并返回打开状态
*status = IoGetDeviceObjectPointer(&name_str, FILE_ALL_ACCESS, &fileobj, &devobj);
if (*status == STATUS_SUCCESS)
ObDereferenceObject(fileobj);//若打开成功,则把文件对象解除引用(利用设备名获得的设备对象的指针devobj,同时也生成fileobj)
return devobj;
}
其中,
memset(name,0,sizeof(WCHAR)*32);
RtlStringCchPrintfW(name,32,L"\\Device\\Serial%d",id);
RtlInitUnicodeString(&name_str,name);
根据传入的id来转换成表示串口的名字,并转换成Unicode_string 。
Memset(): 功能:将s所指向的某一块内存中的每个字节的内容全部设置为ch指定的ASCII值,块的大小由第三个参数指定,这个函数通常 为新申请的内存做初始化工作.
memset() 函数常用于内存空间初始化。如:
char str[100];
memset(str,0,100);
ccpAttachDevice():
NTSTATUS
ccpAttachDevice(PDRIVER_OBJECT driver, PDEVICE_OBJECT oldobj,
PDEVICE_OBJECT *fltobj, PDEVICE_OBJECT *next){
NTSTATUS status;
PDEVICE_OBJECT topdev = NULL;
status = IoCreateDevice(driver,//生成过滤设备
0,
NULL,
oldobj->DeviceType,
0,
FALSE,
fltobj);
if (status != STATUS_SUCCESS)
return status;
if(oldobj->Flags & DO_BUFFERED_IO)//复制标志位 缓冲IO
(*fltobj)->Flags |= DO_BUFFERED_IO;
if(oldobj->Flags & DO_DIRECT_IO)
(*fltobj)->Flags |= DO_DIRECT_IO;
if(oldobj->Flags & DO_BUFFERED_IO)
(*fltobj)->Flags |= DO_BUFFERED_IO;
if(oldobj->Characteristics & FILE_DEVICE_SECURE_OPEN)
(*fltobj)->Characteristics |= FILE_DEVICE_SECURE_OPEN;
(*fltobj)->Flags |= DO_POWER_PAGABLE;
//将fltobj附接到oldobj(打开的端口设备)上
topdev = IoAttachDeviceToDeviceStack(*fltobj,oldobj);
if (topdev == NULL){
IoDeleteDevice(*fltobj);
*fltobj = NULL;
status = STATUS_UNSUCCESSFUL;
return status;
}
*next = topdev;
(*fltobj)->Flags = (*fltobj)->Flags & ~DO_DEVICE_INITIALIZING;
return STATUS_SUCCESS;
}
关于以上代码中的复制标志位,我参考了下文档:
Propagating the DO_BUFFERED_IO and DO_DIRECT_IO Flags
After attaching a filter device object to a file system or volume, always be sure to set or clear the DO_BUFFERED_IO and DO_DIRECT_IO flags as needed so that they match the values of the next-lower device object on the driver stack. (For more information about these flags, see Methods for Accessing Data Buffers.) In the FileSpy sample, this is done as follows:
if (FlagOn( DeviceObject->Flags, DO_BUFFERED_IO )) {
SetFlag( filespyDeviceObject->Flags, DO_BUFFERED_IO );
}
if (FlagOn( DeviceObject->Flags, DO_DIRECT_IO )) {
SetFlag( filespyDeviceObject->Flags, DO_DIRECT_IO );
}
In the above code snippet, DeviceObject is a pointer to the device object to which the filter device object has just been attached; filespyDeviceObject is a pointer to the filter device object itself.
关于DO_BUFFERED_IO和DO_DIRECT_IO:
DO_DIRECT_IO还是DO_BUFFERED_IO?这两种方式有什么区别?分别对应什么情况使用?
答:
DO_DIRECT_IO 常用于传输大块的讲求速度的数据。DO_BUFFERED_IO常用于传输小块的,慢速的数据。DO_DIRECT_IO的效率较高(只需内存锁定即可),代价是牺牲物理内存。DO_BUFFERED_IO的内存效率高,但速度差(它要分配内存,复制数据)。造成这种区别的主要原因还是因为处理器的 Ring 0到Ring4的穿越造成的(微软只用了两层)。你无法两全其美,只能取其一(当然有时候能两全其美,后面会提到)。
DO_DIRECT_IO主要用于使用DMA的底层驱动。Mass Storage也用。
DO_BUFFERED_IO主要用于HID(包括Mouse,Keyboard), 一些Video, 还有串口,并口什么的。
不过我看绝大多数设计者都挺自私的,上来就用DIRECT IO,
用它们的作用:
一,CPU进入内核空间后,先在内核空间申请合适大小的内存缓冲区然后把用户空间的数据复制到这里。因为所有的进程以及中断等都共享相同的内核空间页表,所以能保证CPU始终访问到正确的数据。这就是DO_BUFFERED_IO模式。
二,临时为用户空间的缓冲区增添一个系统空间映射,这样就使同一组物理页面有了两个虚拟地址空间,其一就在原来的用户空间虚拟地址,其二则在系统空间的虚拟地址。这样即使发生进程切换,或者在中断函数、DPC函数里面访问都可以通过系统空间的虚拟地址来访问用户空间的缓冲区,直到操作完成返回用户空间时才撤销系统空间。这就是DO_DIRECT_IO模式。
用IoAttachDeviceToDeviceStack()绑定生成的过滤设备和传入的物理串口设备。根据设备对象的指针,而不是IoAttachDevice()的根据串口设备名来绑定两者,并返回相应绑定后的设备对象。
之后如果绑定成功,则将最终绑定的设备赋值给s_next_obj[i],最终在ccpAttachAllComs()完成绑定。
Unload():
传入驱动对象,定义延迟时间...
#define DELAY_ONE_MICROSECOND (-10)
#define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)
#define DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND*1000)
void ccpUnload(PDRIVER_OBJECT drv)
{
ULONG i;
LARGE_INTEGER interval; //定义大整型数据 interval
for(i=0;i<CCP_MAX_COM_ID;i++){ //一个一个解除Attach
if(s_nextobj[i] != NULL)IoDetachDevice(s_nextobj[i]);
}
interval.QuadPart = (5*1000 * DELAY_ONE_MILLISECOND);
KeDelayExecutionThread(KernelMode,FALSE,&interval);//kernelmode表示内核模式,睡眠5s
for(i=0;i<CCP_MAX_COM_ID;i++){ //删除所有解除绑定的过滤设备
if(s_fltobj[i] != NULL) IoDeleteDevice(s_fltobj[i]);
}
}
typedef union _LARGE_INTEGER {
struct {
DWORD LowPart;
LONG HighPart;
};
struct {
DWORD LowPart;
LONG HighPart;
} u;
LONGLONG QuadPart;
} LARGE_INTEGER, *PLARGE_INTEGER;
如果你有编译器直接支持64位整数可以直接使用QuadPart(64位),否则分别对LowPart(32位)和HighPart(32位)存取,HighPart的最高位为符号位。
表示数的范围:--3689348814741910324到+4611686018427387903
LARGE_INTEGER的值等4000000000,在内存中的布局:
00 28 6B EE 00 00 00 00
(低字节 ) (高字节 )
以上是LARGE_INTEGER的struct和定义。(准确说应该是个union)
用法:
LARGE_INTEGER litmp;
LONGLONG qt1,qt2;
double dft,dff,dfm;
//获得时钟频率
QueryPerformanceFrequency(&litmp);//获得时钟频率
dff=(double)litmp.QuadPart;
//获得初始值
QueryPerformanceCounter(&litmp);
qt1=litmp.QuadPart;
//下面一些耗时的操作
Sleep(100);
//获得终止值
QueryPerformanceCounter(&litmp);
qt2=litmp.QuadPart;
//获得对应的时间值,转到毫秒单位上
dfm=(double)(qt2-qt1);
dft=dfm/dff;
printf("用时: %.3f 毫秒\n", dft*1000.0);
上面的测量在工程中非常使用,基本可以在windows平台下测试一些使用时间的情况,比如用套接字发送一个大数据块、显示一个位图等等!对每个应用的程序段时间消耗都清楚,才能写出高质量的程序!
这是MSDN的解释。
UserBuffer与Mdl在不同METHOD下的区别和利用:
"缓冲"方法(METHOD_BUFFERED)
备注:在下面的讨论中,"输入"表示数据从用户模式的应用程序到驱动程序,"输出"表示数据从驱动程序到应用程序。
对于读取请求,I/O管理器分配一个与用户模式的缓冲区大小相同的系统缓冲区。IRP 中的 SystemBuffer字段包含系统地址。 UserBuffer字段包含初始的用户缓冲区地址。当完成请求时,I/O管理器将驱动程序已经提供的数据从系统缓冲区复制到用户缓冲区。对于写入请 求,会分配一个系统缓冲区并将 SystemBuffer 设置为地址。用户缓冲区的内容会被复制到系统缓冲区,但是不设置 UserBuffer。对于IOCTL请求,会分配一个容量大小足以包含输入缓冲区或输出缓冲区的系统缓冲区,并将SystemBuffer设置为分配的缓冲区地址。输入 缓冲区中的数据复制到系统缓冲区。UserBuffer字段设置为用户模式输出缓冲区地址。内核模式驱动程序应当只使用系统缓冲区,且不应使用UserBuffer中存储的地址。
对于IOCTL,驱动程序应当从系统缓冲区获取输入并将输出写入到系统缓冲区。当完成请求时,I/O系统将输出数据从系统缓冲区复制到用户缓冲区。
"直接"方法(METHOD_IN/OUT_DIRECT)
对于读取和写入请求,用户模式缓冲区会被锁定,并且会创建一个内存描述符列表(MDL)。MDL 地址会存储在IRP的MdlAddress字段中。SystemBuffer和UserBuffer均没有任何含义。但是,驱动程序不应当更改这些字段的值。
对于 IOCTL 请求,如果在 METHOD_IN_DIRECT 和 METHOD_OUT_DIRECT 中同时有一个输出缓冲区,则分配一个系统 缓冲区(SystemBuffer 又有了地址)并将输入数据复制到其中。如果有一个输出缓冲区,且它被锁定,则会创建 MDL 并设 置 MdlAddress。UserBuffer 字段没有任何含义。
"两者都不"方法(METHOD_NEITHER)
对于读取和写入请求,UserBuffer 字段被设置为指向初始的用户缓冲区。不执行任何其他操作。 SystemAddress 和 MdlAddress 没有任何含义。对于 IOCTL 请求,I/O 管理器将 UserBuffer 设置为初始的 用户输出缓冲区,而且,它将当前 I/O 栈位置的 Parameters.DeviceIoControl.Type3InputBuffer 设置为 用户输入缓冲区。利用该 I/O 方法,由驱动程序来确定如何处理缓冲区:分配系统缓冲区或创建 MDL。
通常,驱动程序在访问用户数据时不应当将 UserBuffer 字段用作地址,即使当用户缓冲区被锁定时也是如此。这是由于在调用驱动程序时,在系统中 可能看不到调用用户的地址空间。(对于该规则的一个例外是,在最高层驱动程序将 IRP 向下传递到较低层的驱动程序之前,它可能需要使 用 UserBuffer 来复制数据。)如果使用"直接"或"两者都不"方法,在创建 MDL 之后,驱动程序可以使 用 MmGetSystemAddressForMdl 函数来获取有效的系统地址以访问用户缓冲区。
在驱动层,依传输类型的不同,输入缓冲区的位置亦不同,见下表:
METHOD_IN_DIRECT irp->AssociatedIrp.Sy
stemBuffer
METHOD_OUT_DIRECT irp->AssociatedIrp.Syst
emBuffer
METHOD_BUFFERED irp->AssociatedIrp.Syst
emBuffer
METHOD_NEITHER irpStack->Parameters.De
viceIoControl.Type3InputBuffer
在驱动层,依传输类型的不同,输出缓冲区的位置亦不同,见下表:
METHOD_IN_DIRECT irp->MdlAddress
METHOD_OUT_DIRECT irp->MdlAddress
METHOD_BUFFERED irp->AssociatedIrp.S
ystemBuffer
METHOD_NEITHER irp->UserBuffer
驱动开发之 设备读写方式:缓冲区方式
1.
设备对象一共有三种读写方式:缓冲区方式读写(Buffered方式);直接方式读写(Direct方式);Neither方式。这三种方式的Flags分别对应DO_BUFFERED_IO,DO_DIRECT_IO,0
在buffered方式中,I/O管理器先创建一个与用户模式数据缓冲区大小相等的系统缓冲区。而你的驱动程序将使用这个系统缓冲区工作。I/O管理器负责在系统缓冲区和用户模式缓冲区之间复制数据。
在direct方式中,I/O管理器锁定了包含用户模式缓冲区的物理内存页,并创建一个称为MDL(内存描述符表)的辅助数据结构来描述锁定页。因此你的驱动程序将使用MDL工作。
在neither方式中,I/O管理器仅简单地把用户模式的虚拟地址传递给你。而使用用户模式地址的驱动程序应十分小心。
2.
下面介绍缓冲区方式读写。其优点是比较简单的解决了将用户地址传入驱动的问题,缺点是需要用户模式和内核模式之间数据复制,可想而知,运行效率会受到影响。适合少量内存操作时使用的一种方法。
创建好设备IoCreateDevice后,需要设置DO_BUFFERED_IO, pDevObj->Flags |= DO_BUFFERED_IO.
现在以readfile为例,首先应用程序中需要提供一段缓冲区并把缓冲区大小作为参数传入,例如
UCHAR OutputBuffer[10];
DWORD RetLen = 0;
readfile(hDevice,OutputBuffer,sizeof(OutputBuffer),&RetLen,NULL);
OutputBuffer是提供的输出缓冲区,是用户模式的内存地址,操作系统将此缓冲区的数据复制到内核模式下的地址中,sizeof(OutputBuffer)是缓冲区的大小,而RetLen是真正的输出的字节数。
那么内核模式怎么得到此内核模式地址呢?怎么得到writefile或readfile的字节数呢?答案在下面。
此内核模式下的地址可以通过此readfile创建的IRP的AssociatedIrp.SystemBuffer得到。
假如请求的IRP为PIRP pIrp(一般是派遣函数的参数),那么UCHAR* OutputBuffer= (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
而readfile请求的字节数为IO_STACK_LOCATION中的Parameters.Read.Length,writefilew为IO_STACK_LOCATION中的Parameters.Write.Length
//得到当前堆栈
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
//得到readfile缓冲区大小
ULONG cbread = stack->Parameters..Read.Length;
//得到writefile缓冲区大小
ULONG cbwrite = stack->Parameters.Write.Length;
得到了内核模式下的缓冲区地址了就可以对此缓冲区操作了。比如:
UCHAR* OutputBuffer= (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
ULONG cbread = stack->Parameters..Read.Length;
memcpy(OutputBuffer,0xBB,cbread);
这样用户模式下的缓冲区内得到的数据是0xBB。
另外还要设置实际操作的字节数,pIrp->IoStatus.Information = cbread;(实际操作的字节数不一定要设置为缓冲区的大小,但也不应该大于缓冲区的大小)
那么用户模式下readfile的RetLen被设置为cbread。
下面是IRP_MJ_READ的派遣函数:
NTSTATUS DispatchRead(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
KdPrint(("Enter DispatchRead\n"));
//对一般IRP的简单操作,后面会介绍对IRP更复杂的操作
NTSTATUS status = STATUS_SUCCESS;
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
ULONG ulReadLength = stack->Parameters.Read.Length;
// 完成IRP
//设置IRP完成状态
pIrp->IoStatus.Status = status;
//设置IRP操作了多少字节
pIrp->IoStatus.Information = ulReadLength;
memset(pIrp->AssociatedIrp.SystemBuffer,0xAA,ulReadLength);
//处理IRP
IoCompleteRequest( pIrp, IO_NO_INCREMENT );
KdPrint(("Leave DispatchRead\n"));
return status;
}
设备读写方式:直接读取方式:http://blog.csdn.net/liyun123gx/article/details/38043849
Neither方式:http://blog.csdn.net/liyun123gx/article/details/38046865
驱动开发之 设备读写方式:直接方式
1.
直接方式读写设备,操作系统会将用户模式下的缓冲区锁住,然后操作系统将这段缓冲区在内核模式地址再次映射一遍。这样,用户模式的缓冲区和内核模式的缓冲区指向的是同一区域的物理内存。无论操作系统如何切换进程,内核模式地址都保持不变。
创建好设备IoCreateDevice后,需要设置DO_DIRECT_IO, pDevObj->Flags |= DO_DIRECT_IO.
2.
这里涉及到内存描述符表(MDL)
MDL结构的声明如下:
typedef struct _MDL {
struct _MDL *Next;
CSHORT Size;
CSHORT MdlFlags;
struct _EPROCESS *Process;
PVOID MappedSystemVa;
PVOID StartVa; //给出了用户缓冲区的虚拟地址,第一个页地址,这个地址仅在拥有数据缓冲区的用户模式进程上下文中才有效
ULONG ByteCount; //是缓冲区的字节长度
ULONG ByteOffset; //是缓冲区起始位置在一个页帧中的偏移值,那么缓冲区的首地址是mdl->StartVa+mdl->ByteOffset
} MDL, *PMDL;
用图表示内存描述符表(MDL)结构为:
由图可知用户模式的这段缓冲区在虚拟内存上是连续的,但在物理内存上可能是离散的。
3.下面来看一些MDL相关的函数
IoAllocateMdl | 创建MDL |
IoBuildPartialMdl | 创建一个已存在MDL的子MDL |
IoFreeMdl | 销毁MDL |
MmBuildMdlForNonPagedPool | 修改MDL以描述内核模式中一个非分页内存区域 |
MmGetMdlByteCount | 取缓冲区字节大小(得到mdl->ByteCount) |
MmGetMdlByteOffset | 取缓冲区在第一个内存页中的偏移(得到mdl->ByteOffset) |
MmGetMdlVirtualAddress | 取虚拟地址((PVOID)(PCHAR)(mdl->StartVa+mdl->ByteOffset)) |
MmGetSystemAddressForMdl | 创建映射到同一内存位置的内核模式虚拟地址 |
MmGetSystemAddressForMdlSafe | 与MmGetSystemAddressForMdl相同,但Windows 2000首选 |
MmInitializeMdl | (再)初始化MDL以描述一个给定的虚拟缓冲区 |
MmPrepareMdlForReuse | 再初始化MDL |
MmProbeAndLockPages | 地址有效性校验后锁定内存页 |
MmSizeOfMdl | 取为描述一个给定的虚拟缓冲区的MDL所占用的内存大小 |
MmUnlockPages | 为该MDL解锁内存页 |
4.下面以readfile为例介绍直接方式读取设备
用户模式调用readfile:
UCHAR OutputBuffer[10];
DWORD RetLen = 0;
readfile(hDevice,OutputBuffer,sizeof(OutputBuffer),&RetLen,NULL);
内核模式得到要读取的字节数:(与以缓冲区读写方式一样)
//得到当前堆栈
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
//得到readfile要读取的字节数
ULONG cbread = stack->Parameters..Read.Length;
另外,通过IRP的pIrp->MdlAddress得到MDL数据结构,这个结构描述了被锁定的缓冲区的内存。
下面是一个IRP_MJ_READ的派遣函数,仅供参考。
NTSTATUS DispathRead(IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp)
{
KdPrint(("Enter DispathRead\n"));
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
NTSTATUS status = STATUS_SUCCESS;
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
ULONG ulReadLength = stack->Parameters.Read.Length;//得到读取的长度
KdPrint(("ulReadLength:%d\n",ulReadLength));
ULONG mdl_length = MmGetMdlByteCount(pIrp->MdlAddress); //mdl虚拟内存的长度
PVOID mdl_address = MmGetMdlVirtualAddress(pIrp->MdlAddress); //虚拟内存的起始地址
ULONG mdl_offset = MmGetMdlByteOffset(pIrp->MdlAddress); //虚拟内存首地址在第一页的偏移量
KdPrint(("mdl_address:0X%08X\n",mdl_address));
KdPrint(("mdl_length:%d\n",mdl_length));
KdPrint(("mdl_offset:%d\n",mdl_offset));
if (mdl_length!=ulReadLength)
{
//MDL的长度应该和读长度相等,否则该操作应该设为不成功
pIrp->IoStatus.Information = 0;
status = STATUS_UNSUCCESSFUL;
}else
{
//用MmGetSystemAddressForMdlSafe得到MDL在内核模式下的映射,被映射到内核模式下的内存地址,必定在0X80000000-0XFFFFFFFF之间
PVOID kernel_address = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority);
KdPrint(("kernel_address:0X%08X\n",kernel_address));
memset(kernel_address,0XAA,ulReadLength); //对内核模式下的内存地址进行操作
pIrp->IoStatus.Information = ulReadLength;//设置实际操作字节数
}
pIrp->IoStatus.Status = status;
IoCompleteRequest( pIrp, IO_NO_INCREMENT );
KdPrint(("Leave DispatchRead\n"));
return status;
}
驱动开发之 设备读写方式:缓冲区方式 请参考:http://blog.csdn.net/liyun123gx/article/details/38042125
NEITHER方式参考:http://blog.csdn.net/liyun123gx/article/details/38046865