windows驱动程序框架

windows驱动程序框架 2007-06-14 18:50:49| 分类: 好的技术 | 标签:我的技术 |字号大中小 订阅 .


windows驱动程序框架

[ 2006-11-23 17:15:00 | By: 赛伯 ]




一、驱动程序框架介绍



很多人都用过VC++等图形集成开发环境(IDE)开发过Windows应用程序,当用集成开发环境生成一个工程时,会自动生成一个预先定义好的命令行,这个命令行包含了编译器(compiler)和连接器(linker)某些缺省的配置。很多习惯于图形集成开发环境的人可能对此并不了解。你可能用IDE生成过GUI应用程序,也可能生成过console应用程序,这是两种不同的子系统(subsystem)应用程序,如果你注意观察,可能会发现,console应用程序中是以main函数为入口函数,而GUI应用程序是以WinMain函数为入口函数。在工程设置上,console应用程序的设置是/SUBSYSTEM:CONSOLE,而GUI应用程序的设置是/SUBSYSTEM:WINDOWS。而驱动程序的设置是/SUBSYSTEM:NATIVE。



虽然在工程设置里,你可以通过选项“-entry:DriverEntry”来设定驱动程序的入口函数名字,但驱动程序的入口函数一般都命名为DriverEntry,DriverEntry已经成为官方缺省的驱动函数入口名称。



连接器(linker)根据windows可执行文件PE头的设置,生成最后的二进制文件,PE文件头的设置还决定了这个可执行文件如何被加载的,例如是作为一个可执行文件被加载,还是作为一个动态链接库被加载,还是作为一个驱动程序被加载。加载器(loader)会根据这些设置来验证是否支持所设定的加载模式。我们只需设置好加载模式,加载器就会根据这个设置来加载我们的程序。



一般的驱动程序设置如下:

/SUBSYSTEM:NATIVE /DRIVER:WDM -entry:DriverEntry



在开始写DriverEntry之前,我们先说一下驱动程序的一些特殊之处。我知道,很多人都想能够尽快写一个驱动程序,想看看到底驱动程序是如何工作的。这在写windows应用程序时,经常是这样的,拿一个例子来,改动一下,编译通过后,运行测试。如果运行不正确,应用程序崩溃了,或者消失了,这对系统不会造成多大影响,但是在编写驱动程序时,出现错误会导致蓝屏,当面对蓝屏时,往往会不知所措,如果驱动程序是在系统启动时加载的,情况会更糟糕。这时只有重新启动系统,进入到安全模式,恢复到先前的硬件配置。



首先应该知道的是,驱动程序是加载到系统内核中的,如果驱动程序编写不当,会影响到系统的完整性,驱动程序中的BUG可能会导致整个系统的崩溃。Windows采用虚拟内存机制,系统会将内存中某些页面交换到外部磁盘上来,这对应用程序是透明的,影响

不大,但是有时候驱动程序要求访问的内存是不能被交换到外部磁盘的,必须在内存中,否则可能会引起系统蓝屏。



驱动程序中使用内存必须小心,在某些情况下,如果一个驱动占用了可交换的内存页面,系统会尽可能的将这些页面保持在内存中。如果关闭了应用程序,驱动仍旧占用内存,这bug是很难发现的,除非进行驱动验证(driver verify)。(需深入理解)



关于IRQL和IRP,微软的MSDN有很长的篇幅来描述,这里只是尽可能用比较简单的描述来解释它。



IRQL(Inerrupt Request Level),中断请求级别,任何一个进程都是在线程中执行的,而任何一个线程都运行于一定的IRQL,进程的IRQL决定了线程允许如何被中断。同一个处理器上线程只能被具有更高IRQL级别的线程所中断,低优先级或同等优先级的中断会被屏蔽,只有高级别的IRQL才会中断。在多处理器系统中,每个处理器都有自己独立的IRQL。



系统共有四种级别的IRQL,分别是“Passive”“APC”“Dispatch”“DIRL”。IRQL级别越高,可调用的API函数就越少。MSDN的内核函数API文档中都会注明在哪个中断请求级别上调用。例如DriverEntry函数就是运行在PASSIVE_LEVEL。



PASSIVE_LEVEL是最低级的IRQL,不会屏蔽任何中断。用户态应用程序的线程就运行在这个级别上,可以使用可交换的内存。

APC_LEVEL,异步调用就运行在这个级别,这时会屏蔽APC级别的中断。在这个级别仍可访问可交换内存。当一个APC中断发生时,处理器提升到APC级别,这时,就禁止了其他的APC中断。驱动程序自己提升到APC级别,以便处理同步操作。

DISPATCH_LEVEL,运行于这个级别的处理器会屏蔽除DPC以外的中断,不能访问可交换内存,所以这个级别能调用的API函数大大减少。

DIRQ,设备级中断,这是硬件设备的中断,一般高层的驱动程序不需要处理这个中断,只有底层的驱动程序才处理这个中断。



刚开始学习编写驱动程序,可以先集中精力学习驱动程序的框架,但是,对中断级别要有一定的理解。



IRP(I/O Request Packet),中断请求包,它会沿着驱动程序栈在驱动程序间传递。IRP包是由I/O管理器或者是另一个驱动程序产生,并传递到你的驱动程序中来,驱动程序利用IRP包来传递信息并完成请求任务。IRP包中包含所请求的操作信息。



IRP包在MSDN中的解释很详细,大约有二十多页。IRP包包含一个“子请求”的列表,也称为IRP栈。为了形象的解释IRP栈是如何工作的,我们做一个比喻。假设你有三个人,一个是木匠,一个是管钳工,一个是焊工,他们三个共同建筑一个房子,他

们有自己的工具箱,每个人都要完成自己的工作。而IRP包就是发起建造房子的总命令。一旦建造房子的IRP总命令下达以后,每个人开始自己的工作,每个人都完成自己的工作以后,建造房子的总命令也就完成了。



现在我们开始讨论DriverEntry例程,声明如下:

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath);

参数DRIVER_OBJECT是一个数据结构,表示这个驱动。RegistryPath是一个字符串,在注册表中描述这个驱动的一些信息,驱动程序也可以在注册表的这个位置添加一些特殊的信息。在这个例程中,一般要用到一个有用的数据结构,那就是DEVICE_OBJECT,它表示一个特定的设备,一个驱动程序有可能操纵多个设备,可以用这个数据结构来区分不同的设备。



下面我们来编写DriverEntry例程,第一件事情就是创建一个设备,也许你会感到困惑,没有实际的物理设备,我们如何创建设备,虽然驱动程序往往是和具体的硬件联系在一起,但是驱动程序也可以不和特定的硬件设备相绑定。驱动程序也有很多类型,驱动程序也分不同的层次,并不是所有的驱动程序都和硬件打交道。最高层的驱动程序一般要和用户层的应用程序相交互,最底层的驱动程序一般和具体硬件或者其他驱动打交道。系统中有网卡驱动,显卡驱动,文件驱动等等,每种驱动都有自己的驱动栈。驱动栈或者把一个IRP请求分成几个请求传给其他驱动栈,或者只是简单的把这个请求转发给底层的驱动。



我们以磁盘操作为例,根用户态应用程序交互的驱动程序并不直接和底层的硬件打交道,高层的驱动只是管理文件系统本身,当要进行读写时,它会和位于它下面的中间层驱动交互,中间层驱动和底层的驱动交互,底层的驱动才进行实际的物理操作。



下面分析一下DriverEntry的前一部分



NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath)

{

NTSTATUS NtStatus = STATUS_SUCCESS;

UINT uiIndex = 0;

PDEVICE_OBJECT pDeviceObject = NULL;

UNICODE_STRING usDriverName, usDosDeviceName;



DbgPrint("DriverEntry Called \r\n");



RtlInitUnicodeString(&usDriverName, L"\\Device\\Example");

RtlInitUnicodeString(&usDosDeviceName, L"\\DosDevices\\Example");



NtStatus = IoCreateDevice(pDriverObject, 0,

&usDriverName,

FILE_DEVICE_UNKNOWN,

FILE_DEVICE_SECURE_OPEN,

FALSE, &pDeviceObject);



第一个函数时DbgPrint函数,这相当于应用层的printf函数,它会将调试信息发送给调试器,你可以用软件“DBGVIEW”来查看打印信息,这个软件可以在https://www.360docs.net/doc/c014801245.html,下载。

第二个函数是RtlInitUnicodString,这个函数初始化一

个UNICODE_STRING数据结构,这个数据结构包含三个域,第一个域是这个UNICODE字符串的长度,第二个域是UNICODE字符串最大长度,第三个域是一个指向这个字符串的指针。在驱动程序中很多地方都会使用这个UNICODE字符串结构,记住,这个字符串结构不是以NULL结尾的,因为它有字符长度这个信息,所以无需以NULL结尾。新手有时会以为这种字符串是以NULL结尾的,结果往往会造成蓝屏。

设备有自己的名字,设备的命名往往如下所示:\Device\,这个名字是调用IoCreateDevice时的参数。IoCreateDevice函数的第一个参数一个指向设备的指针,第二个参数是我们设置为0,这个参数的意思是指设备扩展数据结构的大小,可以通过这个数据结构传递驱动的所需的信息,这个参数比较重要,在这个例子中我们没有用到,所以暂时设置为0。

现在我们已经成功的创建了\Device\Example设备驱动,现在需要设置相应IRP包的函数指针。



for(uiIndex = 0; uiIndex < IRP_MJ_MAXIMUM_; uiIndex++)

pDriverObject->Major[uiIndex] = Example_UnSupported;



pDriverObject->Major[IRP_MJ_CLOSE] = Example_Close;

pDriverObject->Major[IRP_MJ_CREATE] = Example_Create;

pDriverObject->Major[IRP_MJ_DEVICE_CONTROL] = Example_IoControl;

pDriverObject->Major[IRP_MJ_READ] = Example_Read;

pDriverObject->Major[IRP_MJ_WRITE] = USE_WRITE_;



我们设置好Create,Close,IoControl,Read,Write等函数指针,当应用层程序调用一定的API函数时,驱动程序就会调用这些设置好的函数。IRP包和API函数的对应关系如下,



CreateFile -> IRP_MJ_CREATE

CloseHandle -> IRP_MJ_CLEANUP & IRP_MJ_CLOSE

WriteFile -> IRP_MJ_WRITE

ReadFile-> IRP_MJ_READ

DeviceIoControl -> IRP_MJ_DEVICE_CONTROL



下一段代码比较简单:

pDriverObject->DriverUnload = Example_Unload;

如果想动态的卸载驱动,必须设置这个函数指针,如果不指定这个函数指针那么你的驱动一旦被装载,系统就不会卸载掉它。



下面的代码使用的是DEVICE_OBJECT,不是DRIVER_OBJECT,这两个数据结构可能有些相似,容易引起混扰,但是它们代表不同的对象。



pDeviceObject->Flags |= IO_TYPE;

pDeviceObject->Flags &= (DO_DEVICE_INITIALIZING);



这里设置设备标志,IO_TYPE这个标志在后面详细描述。

DO_DEVICE_INITIALIZING告诉I/O管理器,设备正在初始化,不要发送I/O请求包给这个驱动。在DriverEntry函数中,这个设置并不需要,因为I/O管理器会自动设置这个标志,并且退出DriverEntry时,I/O管理器会自动清除这个标志。但是如果你在别的函数里调用IoCreateDevice,就必须自己去清除这个标志,其实这个标志是在IoCreateDevice函数里设置的。



最后的代码片

断是建立一个DOS设备名称\DosDevice\Example,函数IoCreateSymbolicLink只是创建了一个符号连接,在NT设备名字和DOS设备名字之间建立一个关联,他们是指同一个设备的。



不同的设备生产厂商编写自己的驱动程序,这些驱动有自己的名字。在系统中不能有两个名字相同的驱动。比如说,你有一个记忆棒,在系统中的映射到E:盘,如果你拔掉记忆棒之后,把一个网络共享映射到E:盘,应用程序可以跟E:盘交互,应用程序并不关心这个E:盘是光盘、软盘、记忆棒还是网络共享。它们都是通过E:盘这个符号连接与其交互。



现在我们把“Example”作为一个DOS设备名字,并与“Device\Example”相关联,在与用户态通信部分,我们将继续讨论如何使用这种映射。



下面是卸载函数例程,为了能动态卸载这个驱动,必须提供卸载例程。这个卸载例程比较简单,删除我们建立的符号连接名字以及设备名字。



VOID Example_Unload(PDRIVER_OBJECT DriverObject)

{



UNICODE_STRING usDosDeviceName;



DbgPrint("Example_Unload Called \r\n");



RtlInitUnicodeString(&usDosDeviceName, L"\\DosDevices\\Example");

IoDeleteSymbolicLink(&usDosDeviceName);



IoDeleteDevice(DriverObject->DeviceObject);

}



很多人都用过WriteFile和ReadFile,在这两个函数里,传递一个缓冲区参数,读操作会把读到的信息填充到这个缓冲区中,写操作会把缓冲区里的数据写到磁盘上去。这样简单的操作,在驱动层有着比较复杂的机制。驱动程序里有三种读写模式,分别是“Direct I/O”“Buffered I/O”“Neither”。在例子中,我们定义



#ifdef __USE_DIRECT__

#define IO_TYPE DO_DIRECT_IO

#define USE_WRITE_ Example_WriteDirectIO

#endif



#ifdef __USE_BUFFERED__

#define IO_TYPE DO_BUFFERED_IO

#define USE_WRITE_ Example_WriteBufferedIO

#endif



#ifndef IO_TYPE

#define IO_TYPE 0

#define USE_WRITE_ Example_WriteNeither

#endif



先来介绍一下 Direct I/O,代码如下



Example_WriteDirectIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)

{

NTSTATUS NtStatus = STATUS_SUCCESS;

PIO_STACK_LOCATION pIoStackIrp = NULL;

PCHAR pWriteDataBuffer;



DbgPrint("Example_WriteDirectIO 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)

{

pWriteDataBuffer =

MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);



if(pWriteDataBuffer)

{

/*

* We need to verify that th

e string

* is NULL terminated. Bad things can happen

* if we access memory not valid while in the Kernel.

*/

if(Example_IsStringTerminated(pWriteDataBuffer,

pIoStackIrp->Parameters.Write.Length))

{

DbgPrint(pWriteDataBuffer);

}

}

}



return NtStatus;

}



函数的入口参数是设备对象,就是请求发送到的设备对象,一个驱动程序可以创建多个设备对象,第一个参数就是即将处理请求的设备对象。第二个参数就是IRP,中断请求包。

函数里做的第一件事情就是调用IoGetCurrentIrpStackLocation,我们会得到属于这个设备的IO_STACK_LOCATION,在我们这个例子里,我们只需要得到这个缓冲区的长度。

通过“MdlAddress”(Memory Deion List),我们可以得到缓冲区的地址,这个地址是用户态地址,我们通过函数“MmGetSystemAddressForMdlSafe”转换成内核可以访问的地址,这样就可以读取缓冲区了。

这种方法一般用于缓冲区较大的情况,因为这种方法不需要内存拷贝。用户态的缓冲区锁定于内存中一直到这个IRP包完成为止,这也是这种方法的缺点。



下面介绍一下Buffered I/O,代码如下



Example_WriteBufferedIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)

{

NTSTATUS NtStatus = STATUS_SUCCESS;

PIO_STACK_LOCATION pIoStackIrp = NULL;

PCHAR pWriteDataBuffer;



DbgPrint("Example_WriteBufferedIO 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)

{

pWriteDataBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;



if(pWriteDataBuffer)

{

/*

* 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(pWriteDataBuffer,

pIoStackIrp->Parameters.Write.Length))

{

DbgPrint(pWriteDataBuffer);

}

}

}



return NtStatus;

}

这种方法就是把数据传递到驱动,系统会把缓冲区分配在不可交换的内存页里,这种方法的优点是别的线程也可以访问这个缓冲区,甚至一些系统进程也可以访问它。缺点是需要分配不可交换内存,并进行数据拷贝,这样在进行读写操作时,会加重系统负担。所以这种方法往往应用于缓冲区较小的情况下。这种方法不象Direct I/O

那样,应用程序被锁定在内存中。



下面是Neither方法,代码如下



Example_WriteNeither(PDEVICE_OBJECT DeviceObject, PIRP Irp)

{

NTSTATUS NtStatus = STATUS_SUCCESS;

PIO_STACK_LOCATION pIoStackIrp = NULL;

PCHAR pWriteDataBuffer;



DbgPrint("Example_WriteNeither 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)

{

/*

* We need this in an exception handler or else we could trap.

*/

__try {



ProbeForRead(Irp->UserBuffer,

pIoStackIrp->Parameters.Write.Length,

TYPE_ALIGNMENT(char));

pWriteDataBuffer = Irp->UserBuffer;



if(pWriteDataBuffer)

{

/*

* 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(pWriteDataBuffer,

pIoStackIrp->Parameters.Write.Length))

{

DbgPrint(pWriteDataBuffer);

}

}



} __except( EXCEPTION_EXECUTE_HANDLER ) {



NtStatus = GetExceptionCode();

}



}



return NtStatus;

}



这种方法直接读取应用程序的地址,它不需要拷贝数据,也不需要把应用程序锁定在内存中。这种方法的缺点是你必须处理这个请求在调用线程的上下文中。



动态加载和卸载驱动,在这里我们只用一些简单的用户态API来加载或者卸载驱动。代码如下



int _cdecl main(void)

{

HANDLE hSCManager;

HANDLE hService;

SERVICE_STATUS ss;



hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);



printf("Load Driver\n");



if(hSCManager)

{

printf("Create Service\n");



hService = CreateService(hSCManager, "Example",

"Example Driver",

SERVICE_START | DELETE | SERVICE_STOP,

SERVICE_KERNEL_DRIVER,

SERVICE_DEMAND_START,

SERVICE_ERROR_IGNORE,

"C:\\example.sys",

NULL, NULL, NULL, NULL, NULL);



if(!hService)

{


hService = OpenService(hSCManager, "Example",

SERVICE_START | DELETE | SERVICE_STOP);

}



if(hService)

{

printf("Start Service\n");



StartService(hService, 0, NULL);

printf("Press Enter to close service\r\n");

getchar();

ControlService(hService, SERVICE_CONTROL_STOP, &ss);



DeleteService(hService);



CloseServiceHandle(hService);



}



CloseServiceHandle(hSCManager);

}



return 0;

}



这段代码加载驱动并启动它,启动类型为SERVICE_DEMAND_START,意思是需要的时候才启动它,它不会随着系统启动就加载。要运行这个程序,把驱动文件example.sys放到c:盘下面。服务启动以后,如果键入回车,就会停止服务,从服务列表中删除并退出。



与驱动通信,下面的代码演示了如何与驱动通信



int _cdecl main(void)

{

HANDLE hFile;

DWORD dwReturn;



hFile = CreateFile("\\\\.\\Example",

GENERIC_READ | GENERIC_WRITE, 0, NULL,

OPEN_EXISTING, 0, NULL);



if(hFile)

{

WriteFile(hFile, "Hello from user mode!",

sizeof("Hello from user mode!"), &dwReturn, NULL);

CloseHandle(hFile);

}



return 0;

}



可以通过DBGVIEW查看调试打印信息,可以看到,只需简单的打开设备名称,获得句柄,就可以对驱动进行读写操作了。


相关文档
最新文档