驱动入门-1

 

子系统( SubSystem )  
 
编译器和链接程序能生成一个 OS 能够理解的二进制文件。在 Windows 中,这种格式被称为 “PE” 。在这种格式中,有一种概念被成为 subsystem 。一个子系统,连同其他在 PE 头中的信息,描述了如何装载一个包含入口点的可执行文件。
使用 VC++IDE 创建工程时,编译器和连接器使用默认的预先设定的选项,我们可以创建 windows 应用程序,或控制台应用程序,这就是 Windows 中不同的子系统, IDE 会生成一个适当的 PE 二进制文件。这也是为什么控制台程序使用 “main” ,而 Windows 应用程序使用 “WinMain” 的原因。当选择了这些工程, VC++ 只是创建一个带有 /SUBSYSTEM:CONSOLE 或者 /SUBSYSTEM:WINDOWS 的工程,如果错误的选择了一个工程,通过直接修改链接程序选项菜单来替代创建一个新程序。
 
一个驱动程序只是简单的使用被称作 “Native” 的一个不同的子系统来链接而已。

驱动程序的 “main”
“Native” 也可以用于一个定义了 “NtProcessStartup” 作为入口点的用户态程序。使用 “Native” 来编译的入口点,类似于可执行程序的 默认 类型链接时使用的 “WinMain” “main” 。通过使用 “-entry:” 的链接程序选项来将默认的入口点改变为项目需要的。如果想要一个程序成为一个驱动,只需要写一个和驱动拥有相同的参数和返回值的入口点。系统在安装了驱动之后会调用这个入口函数。
 
入口点可以随便写。最常用的入口点名字叫做 “DriverEntry” ,所以需要增加一个 “-entry:DriverEntry” 来作为链接程序的命令行参数。如果使用 DDK ,创建一个 “DRIVER” 类型的可执行程序时这会自动由程序完成。 DDK 包含一个拥有使得编译变得更容易的含有 make 文件的目录,这个文件含有一些共用的预设定的编译参数。驱动开发人员可以在 make 文件中修改这些参数或者是对它们视而不见。 make 文件也就是本质上 “DriverEntry” 成为 官方 的驱动入口点的名称的原因。
 
链接程序创建最终的二进制文件,根据 PE 头中的参数来决定系统的行为。装载文件的系统尝试着去做某种程度的校验,以便于将要装载的映像文件可以真正的被期望去通过正在尝试装载的方式来装载。比如,在某些情况下,程序启动的代码被添加到可执行程序中入口点之前 (WinMainCRTStartup 调用 WinMain ,例如要先初始化 CRT) 。你的工作只是简单的以你希望的程序的装载方式作为基础然后正确的设置链接程序的参数以便于其知道如何创建二进制文件而已。

  我们为链接程序设定的参数:
/SUBSYSTEM:NATIVE /DRIVER:WDM –entry:DriverEntry

IRQL
IRQL 就是中断请求级别,处理器将会在一个特殊的 IRQL 的线程中执行代码。处理器的 IRQL 在实质上决定了这个线程是否允许被打断。在同一个处理器上,线程只能被需要运行在更高 IRQL 的代码所打断。相同的或者更低的 IRQL 的中断请求将会被忽略。在一个多处理器的系统中,每一个处理器独立的在它自己的 IRQL 上工作。

  通常需要处理的有四种 IRQL ,他们是 “Passive” “APC” “Dispatch” “DIRQL” IRQL 越高,你能使用的 API 越少。在 MSDN 中说明了当指定的驱动入口点被调用的时候处理器将会运行于何种 IRQL 。例如, “DriverEntry” 将会在 PASSIVE_LEVEL 层被调用。

PASSIVE_LEVEL

   IRQL 最低级别,没有被屏蔽的中断,在这个级别上,线程执行用户模式,可以访问分页内存。

APC_LEVEL

  只有 APC 级别的中断被屏蔽。这是异步程序调用( Asynchronous Procedure Calls )发生的级别。可以访问分页内存。当有 APC 发生时,处理器提升到 APC 级别,这样就能屏蔽掉其他的 APC 。驱动可以手动提升到 APC (或者其它任何级别)以便进行一些 APC 同步的操作。比如,已经在 APC 级别上时 APC 不能被使用。一些 API 函数在 APC 级别无法调用,这就是因为 APC 被禁止了,从而导致一些 I/O 完成 APC 被禁用。

DISPATCH_LEVEL

   DPC 以及更低级中断被屏蔽掉,不能访问分页内存,因此所有需要访问的内存必须是未分页的。如果你在 Dispatch Level 中运行,你能使用的 API 数量大大减少,因为你只能处理未分页的内存。

DIRQL (Device IRQL)

  通常的,更高级的驱动在这个级别上不会处理 IRQL ,但是在这个级别上几乎所有的中断都被屏蔽掉了,事实上,这是 IRQL 的一个范围,也是一个使得谋个设备拥有比其它设备更高优先级的方法。

IRP

“IRP” 就是 输入输出请求 ”“I/O Request Packet”, ,它在驱动堆栈中被传递。这是使得驱动程序相互间通讯和驱动程序用来请求完成某种操作所使用的一种数据结构。 I/O 管理器或者其它驱动程序都有可能创建 IRP 请求然后传递到你的驱动。 IRP 包含了关于所请求的操作。

   IRP 包含了一个列表,这个列表被叫做 ““IRP Stack Location” ,每一个在驱动堆栈中的驱动程序通常都有它们自己对于 IRP 的解释的 子请求 。这个数据结构被称为 “IO_STACK_LOCATION”

创建 DriverEntry

DriverEntry
的原型如下:
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath);

对于 WDM 驱动程序的 DriverEntry 例程,其主要工作是把各种函数指针填入驱动程序对象。
DriverEntry
的第一个参数是一个指针,指向一个刚被初始化的驱动程序对象,该对象就代表你的驱动程序。
DriverEntry 的第二个参数是设备服务键的键名。

    下一部分是向 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  " );

    RtlInitUnicodeString(
& usDriverName, L " /Device/Example " );
    RtlInitUnicodeString(
& usDosDeviceName, L " /DosDevices/Example " ); 

    NtStatus 
=  IoCreateDevice(pDriverObject,  0 & usDriverName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE,  & pDeviceObject);

 

“RtlInitUnicodeString”,这个函数初始化一个UNICODE_STRING数据结构。

 

 

typedef  struct  _UNICODE_STRING  {

    USHORT  Length;

    USHORT  MaximumLength;

    PWSTR  Buffer;

}
 UNICODE_STRING  * PUNICODE_STRING;

 

关于UNICODE_STRING需要记住的事情就是,它们不是以NULL来结尾的,而是以数据结构中的描述大小的参数来决定的。


   设备的名字通常类似于/Device/,通过IoCreateDevice来创建的一个字符串。对于函数IoCreateDevice而言,传入的有:一个驱动程序对象,一个指向包含有调用驱动所使用字串的Unicode字符串,如果这个驱动程序和任何设备都无关的话,那么还会有一个被成为“UNKNOWN”的驱动类型。传入一个指针,以便于接收新创建的驱动对象。第二个参数我们传0进去,这个参数是指定用来创建新设备扩展的字节数的。这是一个驱动可以由驱动开发人员来声明的,并且对于设备并非唯一的数据结构。利用这个数据结构扩展能储存数据的实例所传入信息,比如对驱动程序和创建驱动上下文等。不过在例子中,不会使用这些东西。

 

   现在已经成功的创建了/Device/Example驱动程序,为了让驱动能够响应用户态的IRP主要请求,应当设置驱动对象。当然,在IRP栈中也存在一些子请求,这些子请求就是次要请求。

  以下代码组织好了所有确定的请求:

NTSTATUS

  IoCreateDevice(

    IN PDRIVER_OBJECT  DriverObject,

    IN ULONG  DeviceExtensionSize,

    IN PUNICODE_STRING  DeviceName  OPTIONAL,

    IN DEVICE_TYPE  DeviceType,

    IN ULONG  DeviceCharacteristics,

    IN BOOLEAN  Exclusive,

    OUT PDEVICE_OBJECT  
* DeviceObject

    );




  
for (uiIndex  =   0 ; uiIndex  <  IRP_MJ_MAXIMUM_FUNCTION; uiIndex ++ )
             pDriverObject
-> MajorFunction[uiIndex]  =  Example_UnSupportedFunction;
    
        pDriverObject
-> MajorFunction[IRP_MJ_CLOSE]              =  Example_Close;
        pDriverObject
-> MajorFunction[IRP_MJ_CREATE]             =  Example_Create;
        pDriverObject
-> MajorFunction[IRP_MJ_DEVICE_CONTROL]     =  Example_IoControl;
        pDriverObject
-> MajorFunction[IRP_MJ_READ]               =  Example_Read;
        pDriverObject
-> MajorFunction[IRP_MJ_WRITE]              =  USE_WRITE_FUNCTION;


我们把创建,关闭,I/O控制,读和写都组织好了。这也例程意味着什么呢?当我们和用户态的应用程序通讯的时候,这些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

  需要解释的是,IRP_MJ_CLOSE不是在创建句柄的进程的上下文中调用的。如果你想要进程进行相关的清理操作,你需要通过操纵IRP_MJ_CLEANUP来替代。

   下一步很容易,它是驱动的卸载函数。

   pDriverObject->DriverUnload =  Example_Unload;

  在技术上你可以忽略这个函数,但是如果你想要动态的卸载你的驱动程序,那么它一定要被列出。如果你的驱动中没有这个函数,那么系统不会允许你卸载这个驱动。

   之后的代码实际上是DEVICE_OBJECT,而不是DRIVER_OBJECT。由于他们都是以“D”开头,同时又都是以“_OBJECT”结尾的,所以可能会有点难以区分,所以我们很容易混淆他们。

  pDeviceObject->Flags |= IO_TYPE;

  pDeviceObject->Flags &= (~DO_DEVICE_INITIALIZING);

  我们仅仅是设置了标志。“IO_TYPE”其实是一个用来定义我们所想要做的I/O类型的常数(我在example.h中定义了它)。我在操作用户态写请求的部分会进行解释。

   “DO_DEVICE_INITIALIZING”用来告诉I/O管理器设备正在被初始化以及不要向驱动发送任何的I/O请求。对于在“DriverEntry”的上下文中创建的设备,当“DriverEntry”完成的时候不需要清除这个标志,然而,如果你在“DriverEntry”之外的任何例程中设定了这个标志,那么你必须为任何你使用IoCreateDevice创建的设备通过手动清除这个标志,事实上这个标志是由IoCreateDevice设定的,我们在这儿清除这个标志其实只是For fun而已——因为我们并不需要这么做。

  我们的驱动的最后一部分是使用我们在之前定义的Unicode字符串。“/Device/Example”“/DosDevices/Example”

  IoCreateSymbolicLink(&usDosDeviceName, &usDriverName);

  “IoCreateSymbolicLink”只做了这些事情,它在对象管理器中创建了一个符号链接。一个符号链接仅仅是把一个“DOS设备名映射为“NT设备名而已。在这个例子中,“Example”是我们的DOS设备名,“/Device/Example”是我们的NT设备名。

 

创建卸载例程

 
  删除了之前创建的符号链接和被命名为/Device/Example的设备。


 VOID Example_Unload(PDRIVER_OBJECT  DriverObject)

  
{    

  UNICODE_STRING usDosDeviceName;

  DbgPrint(
"Example_Unload Called  ");

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

  IoDeleteSymbolicLink(
&usDosDeviceName);

  IoDeleteDevice(DriverObject
->DeviceObject);

  }

 

创建IRP_MJ_WRITE

   如果你使用过WriteFileReadFile,你应当知道调用函数的时候你只是传入一个指向缓冲区来实现写入数据或是从设备中读取数据。这些参数就如我们之前解释的那样,在IRP中被传入驱动。尽管输入输出管理器在IRP传递到驱动之前有三种方法来排列数据。这也意味着数据的排列方式决定了驱动程序的读写函数需要如何分析传入的数据。  

这三种方法是直接输入输出被缓冲的输入输出两者皆非

#ifdef __USE_DIRECT__

  
#define  IO_TYPE DO_DIRECT_IO

  
#define  USE_WRITE_FUNCTION  Example_WriteDirectIO

  
#endif

  #ifdef __USE_BUFFERED__

  
#define  IO_TYPE DO_BUFFERED_IO

  
#define  USE_WRITE_FUNCTION  Example_WriteBufferedIO

  
#endif

  #ifndef IO_TYPE

  
#define  IO_TYPE 0

  
#define  USE_WRITE_FUNCTION  Example_WriteNeither

  
#endif



/*
  这段代码意味着,如果你在头文件中定义了“__USE_DIRECT__”,那么IO_TYPE将是 DO_DIRECT_IO并且USE_WRITE_FUNCTION指向Example_WriteDirectIO。如果你在头文件中定义了“__USE_BUFFERED__”,那么IO_TYPE将是 DO_BUFFERED_IO并且USE_WRITE_FUNCTION指向Example_WriteBufferedIO。如果你在头文件中既没有定义__USE_DIRECT__也没有定义__USE_BUFFERED__,那么IO_TYPE将被定义为0(两者皆非)并且写入函数指向Example_WriteNeither。
*/




 

  直接I/O(Direct I/O)
 

   
 调用“IoGetCurrentIrpStackLocation”,这个函数只是给我们提供我们的IO_STACK_LOCATION。在例子中,需要从它获取的参数是提供给驱动的缓冲区长度,它位于Parameters.Write.Length

  直接I/O工作的方法是提供一个“MdlAddress”,也就是一个内存描述表”“Memory Descrīptor List”MDL)。它是一个对用户态内存地址以及如何将其映射到物理地址的描述,我们使用“MmGetSystemAddressForMdlSafe”Irp->MdlAddress来完成他。这个操作可以给我们一个系统的虚拟地址,我们可以使用这个虚拟地址来读内存。

 

NTSTATUS Example_WriteDirectIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)

  
{

  NTSTATUS NtStatus 
= STATUS_SUCCESS;

  PIO_STACK_LOCATION pIoStackIrp 
= NULL;

  PCHAR pWriteDataBuffer;

  DbgPrint(
"Example_WriteDirectIO Called  ");

  
/*

  * 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 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;

  }


 

使用该方法的原因是某些驱动并非总是在所发请求的线程甚至是进程的上下文中来处理用户态的请求。如果你处理一个不同线程的请求,而这个线程却又运行在另外一个进程的上下文中,你也许不能正常的跨越进程分界线来读取用户态内存。如果运行两个应用程序,如果没有操作系统的支持的话,他们不能读写对方的数据。

 

所以,这只是简单的映射被用户态使用的物理页面到系统内存中。我们可以使用它的返回值来访问被用户态所传入的缓冲区。

这种方法通常用于较大的缓冲区因为他不需要内存复制。用户模式的缓冲区在IRP请求完成之前是被锁定的,这是使用直接输入输出的不利方面。这是唯一的不好之处,也是为什么它通常对较大缓冲区而言更有用的原因。

缓冲I/O(Buffered I/O)

  这种方法的好处是传送到驱动程序中的数据可以在任何上下文中,比如另外一个位于其他进程中的线程的上下文中被访问。另外一个原因是映射内存到未分页的页面中,以便于驱动也能够在提升的IRQL中读取。

   你需要在进程上下文之外访问内存的原因也许是一些驱动在SYSTEM进程中创建了线程。然后它们按照程序的要求来同步或不同步的工作。一个比你的驱动更高级别的驱动或许会做这种事情,或者由你的驱动来导致这种情况。

  使用缓冲I/O”的不好之处在于它分配了未分页的内存并且进行了复制。这总是凌驾于驱动每次的读写之上的。这也是它最好被用于较小的缓冲区的原因之一。整个用户模式的页不需要再像直接访问输入输出那样被锁定,这是它的好的方面。另外一个将它用于较大缓冲区的问题是,由于他分配了未分页内存,它也许会需要在连续的未分页内存中分配较大的一个块。

NTSTATUS Example_WriteBufferedIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)

  
{

  NTSTATUS NtStatus 
= STATUS_SUCCESS;

  PIO_STACK_LOCATION pIoStackIrp 
= NULL;

  PCHAR pWriteDataBuffer;

  DbgPrint(
"Example_WriteBufferedIO Called  ");

  
/*

  * 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;

  }


两者皆非

 

  在这个方法中,驱动直接访问用户模式内存。输入输出管理器不会复制数据,但是也不会锁定用户态内存中的页。它只不过将用户态缓冲区的地址交给了驱动。

NTSTATUS Example_WriteNeither(PDEVICE_OBJECT DeviceObject, PIRP Irp)

  
{

  NTSTATUS NtStatus 
= STATUS_SUCCESS;

  PIO_STACK_LOCATION pIoStackIrp 
= NULL;

  PCHAR pWriteDataBuffer;

  DbgPrint(
"Example_WriteNeither Called  ");

  
/*

  * 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;

  }


好处是没有数据被复制,没有内存被分配,并且也没有页被锁定到内存中。不好之处在于你必须在调用线程的上下文中处理,否则你将无法正确的访问到用户态内存。另外一个不好之处是进程自身有可能会在另一个线程中尝试去修改页的访问权限,释放内存,等等。这就是为什么你通常想要使用“ProbeForRead”“ProbeForWrite”函数并且给所有的代码都写上异常处理。无法保证在任何时候页都是可用的,你可以简单的在你尝试读写之前,尝试确认他们是可用的。这个缓冲区位于Irp->UserBuffer

 参考资料:

http://www.codeproject.com/KB/system/driverdev.aspx

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值