天书夜读:从汇编语言到Windows内核编程笔记(2)

时间:2022-03-18 14:36:10

内核线程

在驱动中生成的线程一般是系统线程。系统线程所在的进程名为“System”。

NTSTATUS

PsCreateSystemThread(

  OUT PHANDLE ThreadHandle,                          //用来返回句柄,放入一个句柄指针即可

  IN ULONG DesiredAccess,                               //一般总是填写0

  IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,     //NULL

  IN HANDLE ProcessHandle OPTIONAL,             //NULL

  OUT PCLIENT_ID ClientId OPTIONAL,              //NULL

  IN PKSTART_ROUTINE StartRoutine,              //用于该线程启动的时候执行的函数,传入函数名即可

  IN PVOID StartContext);                               //用于传入该函数的参数

线程的结束应该在线程中自己调用PsTerminateSystemThread来完成。此外得到的句柄也必须要用ZwClose来关闭。关闭句柄并不结束线程。

例子:

  VOID MyThreadProc(PVOID context)             //我的线程函数,传入一个参数,这个参数是一个字符串

  {  

      PUNICODE_STRING str = (PUNICODESTRING)context;

      KdPrint(("PrintInMyThread:%wZ\r\n",str));

      PsTerminateSystemThread(STATUS_SUCCESS);          //终止一个线程

  }

  VOID MyFunction()

  {

    UNICODE_STRING str = RTL_CONSTANT_STRING(L"Hello!");

    HANDLE thread = NULL;

    status = PsCreateSystemThread(&thread,0L,NULL,NULL,NULL,MyThreadProc,(PVOID)&str);         //开始一个线程

    if(!NT_SUCCESS(status))

    {

      //错如处理

    }

    //如果成功了,可以继续做自己的事,之后得到的句柄要关闭

    ZwClose(thread);

  }

  注意  :MyThreadProc执行的时候,MyFunction可能已经执行完毕了,str就会无效,再执行K的Print去打印str一定会蓝屏。解决方法是在队中分配str的空间,或者str必须在全局空间中。

睡眠

#define DELAY_ONE_MICROSECOND (-10)        //10个100纳秒 = 1 微秒, -10表示相对时间

#define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)

VOID MySleep(LONG msec)

{

  LARGE_INTEGER my_interval;

  my_interval.QuadPart = DELAY_ONE_MILLISECOND;

  my_interval.QuadPart *= msec;

  KeDelayExecutionThread(KernelMode,       //表示在内核编程中使用

                0,       //是否允许线程报警(用于重新唤醒)

                &my_interval); //表示要睡眠多久

}

事件

  内核中的事件是一个数据结构。这个结构的指针可以当作一个参数传入一个等待函数中。如果这个事件不被“设置”,则这个等待函数不会返回,这个线程被阻塞。如果这个事件被“设置”,则等待结束,可以继续下去。

  事件不需要销毁。

  可以发现,关于事件的操作这一部分(如事件的重设,同步等),其原理和MFC中是很类似的,只是MFC中封封装了更好处理的API函数而已。

  实际上等待线程结束并不一定要用事件。线程本身也可以当作一个事件来等待。

 例子:

  KEVENT event;                                                                  //定义一个事件

  KeInitializeEvent(&event,SyschronizationEvent,TRUE);            //事件初始化,SynchoronizationEvent为“自动重设”事件:只有一个线程的wait可以通过,通过之后被自动重设,其他的线程就只能继续等待了;若为NotificationEvent,这个事件必须手动重设KeResetEvent(&event)之后才能使用,否则所有等待线程都通过了。

  KeWaitForSingleObject(&event,Executive,KernelMode,0,0);     //等待这个事件event,直到这个事件被人设置

  

  KeSetEvent(&event);                                                      //在另一个地方,设置这个事件,前面等待的地方将继续执行

驱动与设备和请求处理

#include <ntddk.h>

NTSTATUS

  DriverEntry (

    IN PDRIVER_OBJECT DriverObject,

    IN PUNICODE_STRING RegistryPath

    )

{

  NTSTATUS status = STATUS_UNSUCCESSFUL;

  return status;

}

  函数DriverEntry是每个驱动程序中必须的。如同Win32应用程序里的WinMain。

  DriverEntry的第一个参数就是一个 DRIVER_OBJECT的指针。这个DRIVER_OBJECT结构就对应当前编写的驱动程序。其内存是Windows系统已经分配的。

  第二个参数RegistryPath是一个字符串。代表一个注册表子键。这个子键是专门分配给这个驱动程序使用的。用于保存驱动配置信息到注册表中。

  

  DRIVER_OBJECT中含有分发函数指针,分发函数指针的个数为IRP_MJ_MAXIMUM_FUNCTION,保存在一个数组中。这些函数用来处理发到这个驱动的各种请求。Windows总是自己调用DRIVER_OBJECT下的分发函数来处理这些请求。所以编写一个驱动程序,本质就是自己编写这些处理请求的分发函数。

NTSTATUS

    DriverEntry (

      IN PDRIVER_OBJECT DriverObject,

      IN PUNICODE_STRING RegistryPath

    )

{

  ULONG i;

  for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;++i)

  {

    DriverObject->MajorFunctions[i] = MyDispatchFunction;                //分发函数定义,所有的分发函数指针都指向一个分发函数MyDispatchFunction中

  }

}

NTSTATUS MyDispatchFunction(PDEVICE_OBJECT device,PIRP irp)               //处理发送给device的irp请求

{

……

}

VOID MyDriverUnload(PDRIVER_OBJECT driver)

{

……

}

这个函数的地址设置到DriverObject->DriverUnload即可。

设备与符号链接


如果驱动程序要和应用程序之间通信,则应该生成设备。此外还必须为设备生成应用程序可以访问的符号链接(就像文件路径一样)

“\\.\”意味后面是一个符号链接名。 目前生成设备,请总是生成在\Device\目录下。

例子:

#include  <ntifs.h>        

NTSTATUS DriverEntry(

  PDRIVER_OBJECT driver,

  PUNICODE_STRING reg_path)

{

  NTSTATUS status;

  PDEVICE_OBJECT device;

  UNICODE_STRING device_name = RTL_CONSTANT_STRING("\\Device\\MyCDO");         //设备名

  UNICODE_STRING symb_link = RTL_CONSTANT_STRING("\\DosDevices\\MyCDOSL");         //符号链接名

  status = IoCreateDevice(                                     //生成设备,这个设备必须用系统权限才能访问,IoCreateDeviceSecure可以产生用户权限可以访问的设备

    driver,                                                         //生成设备的驱动对象

    0,      //当用户需要在设备上记录一些额外的信息,就要指定设备扩展区内存的大小,以后就可以从DeviceObject->DeviceExtension中来获取这些信息了

    device_name,                                             //设备名字

    FILE_DEVICE_UNKNOWN,                            //设备类型

    0,

    FALSE,

    &device);

  if(!NT_SUCCESS(status))

    return status;

  status = IoCreateSymbolicLink(

    &symb_link,

    &device_name);

  if(!NT_SUCCESS(status))

  {

    IoDeleteDevice(device);

    return status;

  }

  device->Flags &= ~DO_DEVICE_INITIALIZENG;         //设备生成之后,打开初始化完成标记

  return status;

}

符号链接与用户相关性

  某个用户穿件的符号链接只能被该用户访问,系统创建的符号链接可以被所有用户访问。下面的例子生成的符号链接总是随时可以使用:

UNICODE_STRING device_name;

UNICODE_STRING symbl_name;

if(IoIsWdmVersionAvailable(1,0x10))

{

  RtlInitUnicodeString(&symbl_name,L"\\DosDevices\\Global\\SymbolicLinkName"); 

         //如果是支持符号链接用户相关性的版本的系统,一个用户产生的符号链接只能被这一个用户访问,其它用户则不能访问,所以必须产生全局符号

}

else

{

  RtlInitUnicodeString(&symbl_name,L"\\DosDevices\\SymbolicLinkName");    //如果系统不支持符号链接的用户相关性,则直接创建符号链接即可,所有用户都可以访问

}

IoCreateSymbolicLink(&symbl_name,&device_name);

请求处理

  应用程序为了和驱动通信,首先必须打开设备。然后发送或者接收信息。最后关闭它。这至少需要三个IRP:第一个是打开请求。第二个发送或者接收信息。第三个是关闭请求。

  IRP的种类取决于主功能号。

  应用层调用的API 驱动层收到的IRP主功能号-----即DRIVER_OBJECT中分发函数指针数组中的索引。

  IRP的主功能号在IRP的当前栈空间中;IRP总是发送给一个设备栈,到每个设备上的时候拥有一个“当前栈空间”来保存在这个设备上的请求信息。

  这些功能都有应用层API引发,对应关系如下:

    CreateFile         IRP_MJ_CREATE

    CloseHandle         IRP_MJ_CLOSE

    DeviceIoControl  IRP_MJ_DEVICE_CONTROL

    ReadFile      IRP_MJ_READ

    WriteFile      IRP_MJ_WRITE

返回时一个IRP成功是一个三部曲

  (1)设置irp->IoStatus.Information为0;

  (2)设置irp->IoStatus.Status的状态;成功或者失败

  (3)调用IoCompleteRequest(irp,IO_NO_INCREMENT),这个函数完成IRP;

  最后返回irp->IoStatus.Status即可。

打开和关闭请求

  下面的函数能够实现打开和关闭请求:

  NTSTATUS

  MyCreateClose(

    IN PDEVICE_OBJECT device,

    IN PIRP irp)

  {

    irp->IoStatus.Information = 0;

    irp->IoStatus.Status = STATUS_SUCCESS;

    IoCompleteRequest(irp,IO_NO_INCREMENT);

    return irp->IoStatus.Status;

  }

  驱动的分发函数设置如下:

    DriverObject->MajorFunctions[IRP_MJ_CREATE] = MyCreateClose;

    DriverObject->MajorFunctions[IRP_MJ_CLOSE] = MyCreateClose;

应用层信息的传入

  可以使用WriteFile或者DeviceIoControl(双向的);

  DeviceIoControl:设备控制接口,可以发送一个带有特定控制码的IRP,同时提供输入和输出缓冲区;应用程序可以定义一个控制码,然后把相应的参数填写在输入缓冲区中,同时可以从输出缓冲区得到返回的更多信息。

  驱动获得一个DeviceIoControl产生的IRP的时候,需要获得当前的控制码、输入输出缓冲区的位置和长度;控制码必须预先用一个宏定义:

  #define MY_DVC_IN_CODE \

    (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN, \

      0xa01,  \             //自定义的值,其它照抄即可

      METHOD_BUFFERED,  \

      FILE_READ_DATA|FILE_WRITE_DATA)

  可以通过设备的栈空间获得三个要素。

驱动层信息的传出   

  应用程序开启一个线程,通过调用DeviceIoControl(或者ReadFile)实现该功能,驱动没有消息的时候,则阻塞这个IRP的处理,等待有消息的时候返回。

  具体实现见《天书夜读:从汇编语言到windows内核开发》第92-93页。

关于上述几项内容的专题论述,请参见相关文档。[5,6]

参考
[1] http://www.cnblogs.com/phinecos/archive/2009/02/19/1393803.html
[2] http://www.cnblogs.com/qsilence/archive/2009/06/11/1501511.html
[3 http://msdn.microsoft.com/en-us/library/ff557565%28VS.85%29.aspx
[4] http://www.cnblogs.com/wanghao111/archive/2009/05/25/1489041.html
[5] Windows驱动编程基础教程.doc
[6] 天书夜读-从汇编语言到windows内核编程(改)
[7] Windows DDK

[8] 天书夜读——从汇编语言到Windows内核编程
http://download.csdn.net/source/2754275
http://msdn.microsoft.com/en-us/library/ff557573%28VS.85%29.aspx