windows应用程序和内核交互
在windows内核中,有一种数据结构IRP(I/O Request Package)输入输出请求包,事输入输出相关的重要数据结构,上层应用程序与底层驱动程序通信时应用程序会发出I/o请求,操作系统将I/O请求转化为相应的IRP数据,不同类型的IRP会根据类型传递到不同的派遣函数内。IRP两个基本类型MajorFunction和MinorFunction分别记录IRP的主类型和子类型,操作系统根据MajorFunction将IRP“派遣”到不同的派遣函数中,在派遣函数中还可以继续判断这个IRP属于哪种MinorFunction。
一、读写设备
驱动程序所创建的设备一般有三种读写方式:
1、缓冲区方式(用户模式地址与内核模式地址的数据复制)
以缓冲区方式写设备时,操作系统将writefile提供的将用户模式缓冲区复制到内核模式地址下以缓冲区方式读设备时,操作系统会分配一段内核模式下的内存,这段内存大小等于readfile或writefile指定的字节数,当IRP请求结束时这段内存地址会被复制到readfile提供的缓冲区;复制过程由操作系统负责,用户模式地址由readfile或者writefile提供,内核模式地址由操作系统负责分配和回收。
2、直接方式
直接方式读写设备,操作系统会将用户模式下的缓冲区锁住,然后操作系统将这段缓冲区在内核模式地址再次映射一遍,用户模式的缓冲区和内核模式的缓冲区指向同一块区域物理内存,操作系统先将用户模式的地址锁定后,操作系统用内存描述符(MDL数据结构)记录这段内存,用户模式的这段缓冲区在虚拟内存上是连续的,但是在物理内存上可能是离散的。
3、其他方式
其他方式读写设备时,派遣函数直接读写应用程序提供的缓冲区地址,只有驱动程序与应用程序运行在相同线程上下文的情况下,才使用这种方式,驱动程序使用用户模式地址前,需要探测这段内存是否可读或者可写。
二、I/O设备控制操作
除了ReadFile和WriteFile以外,还可以使用Win32 API DeviceIoControl操作设备,DeviceIoControl内部会使操作系统创建一个IRP_MJ_DEVICE_CONTROL类型的IRP,然后操作系统会将这个IRP转发到派遣函数,使用DeviceIoControl定义读写还可以让程序和驱动程序进行通信。
以I/O设备控制操作为例:
得到当前I/O堆栈、得到输入缓冲区大小、得到输出缓冲区大小、得到IOCTL码、缓冲区方式、操作输出缓冲区、设置实际操作输出缓冲区长度、设置IRP完成状态、设置IRP请求操作的字节数、结束IRP请求
应用程序——>内核
CreateFile函数
HANDLE CreateFile(
LPCSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
#define MYDEVICE L"\\\\.\\MyWDF_LINK" //以“\\\\ .”开头并加上我们为它提供的符号链接名
HANDLE hDevice = CreateFile(MYDEVICE, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
倒数第二参数不能为空,在win10下能正常运行,在win7下运行会阻塞,原因目前木知啊;
*备注:*
要检索设备句柄,必须使用设备名称或与设备关联的驱动程序名称来调用CreateFile函数。 要指定设备名称,请使用以下格式:
*\\\\.\DeviceName*\1. 应用层调用CreateFile函数
\2. 这个函数实际上被封装到了kernel32.dll中,在这个函数中调用NtCreateFile,这就是调用ntdll.dll中的native api ,ntdll.dll中一般又两组函数——以Nt开头,以Zw开头的,这两组函数本身没有什么太大的区别。
\3. native api中通过中断 int 2eh(windows 2000 及以下),或者通过sysenter指令(windows xp及以上)进入内核,这种方式称为软中断,在产生中断时会带上一个服务号,根据服务号在ssdt表中以服务号进行查找(类似与8086中的中断机制)
\4. 根据SSDT表中记录的服务函数地址,调用相关的服务函数。
\5. 然后进入到执行组件中,对于CreateFile的操作,这个时候会调用IO管理器,IO管理器负责发起IO操作请求,并管理这些请求,主要时生成一个IRP结构,系统中有不同的管理器,主要有这样几个——虚拟内存管理器,IO管理器,对象管理器,进程管理器,线程管理器,配置管理器。
\6. 管理器生成一个IRP请求,并调用内核中的驱动,来相应这个操作,对于CreateFile来说会调用NtCreateFile函数。
\7. 最后调用内核实现部分,也就是硬件抽象层。最后由硬件抽象层操作硬件,完成打开或者创建文件的操作。
DeviceIoControl函数:
BOOL WINAPI DeviceIoControl(
_In_ HANDLE hDevice, //已经打开的设备
_In_ DWORD dwIoControlCode, //I/O控制码IOCTL
_In_opt_ LPVOID lpInBuffer, //输入缓冲区
_In_ DWORD nInBufferSize, //输入缓冲区大小
_Out_opt_ LPVOID lpOutBuffer, //输出缓冲区
_In_ DWORD nOutBufferSize, //输出缓冲区大小
_Out_opt_ LPDWORD lpBytesReturned, //实际返回字节数
_Inout_opt_ LPOVERLAPPED lpOverlapped //是否OVERLAP操作
);
CTL_CODE(DeviceType, Function, Method, Access);
/*
DeviceType: 设备对象的类型,这个类型应和创建设备(IoCreateDevice)时的类型相匹配,
Function: 这是驱动程序定义的IOCTL码
0X0000到0X7FFF微软自己保留;0X8000到0XFFFF程序员自己定义
Method:操作模式
(缓冲区METHOD_BUFFERED、
直接写METHOD_IN_DIRECT、
直接读METHOD_OUT_DIRECT、
其他方式METHOD_NEITHER)
Access:访问权限,没特殊要求,一般FILE_ANY_ACCESS
*/
*返回值:*
如果操作成功完成,DeviceIoControl将返回一个非零值。如果操作失败或正在等待,则DeviceIoControl返回零。 要获得扩展的错误信息,请调用GetLastError。
应用程序:
state = DeviceIoControl(DeviceHandle, PCIe_DMA_BUFFER_SIZE_READ,
NULL, NULL, &outBuf, 4, NULL, NULL);
内核读操作
status = WdfRequestRetrieveOutputBuffer(
Request,
sizeof(ULONG64),
&outBuffer,
NULL
);
if (!NT_SUCCESS(status)) {
goto Exit;
}
value_u64 = READ_REGISTER_ULONG64((PULONG64)&pDevContext->DMABufferSize);
WRITE_REGISTER_ULONG64((ULONG64*)outBuffer, value_u64*pDevContext->DMABufferNum);
WdfRequestCompleteWithInformation(Request, status, sizeof(ULONG));
KdPrint(("PCIe_DMA_BUFFER_SIZE_READ %llu\n", value_u64));
KdPrint(("PCIe_DMA_BUFFER_SIZE_READ OK\n"));
内核——>应用程序
内核写操作
status = WdfRequestRetrieveInputBuffer( // 检索I/O请求的输入缓冲区
Request,
sizeof(ULONG),
&inBuffer,
NULL
);
value = READ_REGISTER_ULONG((ULONG*)inBuffer); //计数单位为双字
KdPrint(("DMA_WRITE_SIZE %d \n", value));
WRITE_REGISTER_ULONG((PULONG)&pDevContext->Regs->RD_MEM_SIZE_L, value);
WRITE_REGISTER_ULONG((PULONG)&pDevContext->Regs->WR_MEM_SIZE_L, value);
pDevContext->Length = value * 4;
WdfRequestCompleteWithInformation(Request, status, sizeof(ULONG));
if (!NT_SUCCESS(status)) {
goto Exit;
}
KdPrint(("PCIe_DMA_WRITE_SIZE OK\n"));
应用程序:
state = DeviceIoControl(DeviceHandle, PCIe_WRITE_SIZE,
&inData, 4, NULL, NULL, NULL, NULL);
函数:
NTSTATUS WdfRequestRetrieveOutputBuffer(
WDFREQUEST Request,
size_t MinimumRequiredSize,
PVOID *Buffer,
size_t *Length
);
NTSTATUS WdfRequestRetrieveInputBuffer(
WDFREQUEST Request,
size_t MinimumRequiredLength,
PVOID *Buffer,
size_t *Length
);
将write请求的数据写入设备对象上下文,然后当read请求过来的时候,将设备上下文里面的数据读出来并且返回给用户模式程序,wdf提供两个函数,这两个函数可以获取请求的输入输出缓冲,read请求只有outptbuffer,write请求只有inputbuffer;IOcontrol请求同事拥有输入输出缓冲;
status = WdfRequestRetrieveOutputBuffer(
Request,
sizeof(ULONG64),
&outBuffer,
NULL
);
这段代码获取request的输出请求,outbuffer是一个指针,如果函数调用成功,那么outbuffer将指向一个输出缓冲,指向一个内核模式下的地址,可直接读取或写入数据,read请求函数成功,write请求则失败。