驱动开发学习笔记(3-6)–Four-F的驱动开发教程-全功能的驱动程序分析

5. 全功能的驱动程序分析

※ 本篇的源代码同第4节的源代码:KmdKit\examples\simple\VirtToPhys

5.1 VirtToPhys驱动程序的源代码

现在是到看看一个全功能驱动程序源代码的时候了,这里就是:

;@echo off
;goto make
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; VirtToPhys - Kernel Mode Driver
;  Translates virtual addres to physical address
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              I N C L U D E   F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc
include \masm32\include\w2k\w2kundoc.inc
includelib \masm32\lib\w2k\ntoskrnl.lib
include \masm32\Macros\Strings.mac
include ..\common.inc
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                      C O N S T A N T S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.const
CCOUNTED_UNICODE_STRING    "\\Device\\devVirtToPhys", g_usDeviceName, 4
CCOUNTED_UNICODE_STRING    "\\??\\slVirtToPhys", g_usSymbolicLinkName, 4
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                            C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                    GetPhysicalAddress
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
GetPhysicalAddress proc dwAddress:DWORD

    mov eax, dwAddress
    mov ecx, eax
    shr eax, 22
    shl eax, 2
    mov eax, [0C0300000h][eax]

    .if ( eax & (mask pde4kValid) )
        .if !( eax & (mask pde4kLargePage) )
            mov eax, ecx
            shr eax, 10
            and eax, 1111111111111111111100y
            add eax, 0C0000000h
            mov eax, [eax]

            .if eax & (mask pteValid)
                and eax, mask ptePageFrameNumber

                and ecx, 00000000000000000000111111111111y
                add eax, ecx
            .else
                xor eax, eax
            .endif
        .else
            and eax, mask pde4mPageFrameNumber
            and ecx, 00000000001111111111111111111111y
            add eax, ecx
        .endif
    .else
        xor eax, eax
    .endif
    ret

GetPhysicalAddress endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                   DispatchCreateClose
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

    mov eax, pIrp
    assume eax:ptr _IRP
    mov [eax].IoStatus.Status, STATUS_SUCCESS
    and [eax].IoStatus.Information, 0
    assume eax:nothing

    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT

    mov eax, STATUS_SUCCESS
    ret

DispatchCreateClose endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                     DispatchControl
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DispatchControl proc uses esi edi ebx pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

local status:NTSTATUS
local dwBytesReturned:DWORD

    and dwBytesReturned, 0

    mov esi, pIrp
    assume esi:ptr _IRP

    IoGetCurrentIrpStackLocation esi
    mov edi, eax
    assume edi:ptr IO_STACK_LOCATION

    .if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_GET_PHYS_ADDRESS
        .if ( [edi].Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE ) &&\
	( [edi].Parameters.DeviceIoControl.InputBufferLength >= DATA_SIZE )

            mov edi, [esi].AssociatedIrp.SystemBuffer
            assume edi:ptr DWORD
            xor ebx, ebx
            .while ebx < NUM_DATA_ENTRY
                invoke GetPhysicalAddress, [edi][ebx*(sizeof DWORD)]
                mov [edi][ebx*(sizeof DWORD)], eax
                inc ebx
            .endw
            mov dwBytesReturned, DATA_SIZE
            mov status, STATUS_SUCCESS
        .else
            mov status, STATUS_BUFFER_TOO_SMALL
        .endif
    .else
        mov status, STATUS_INVALID_DEVICE_REQUEST
    .endif
    assume edi:nothing

    push status
    pop [esi].IoStatus.Status
    push dwBytesReturned
    pop [esi].IoStatus.Information

    assume esi:nothing
    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
    mov eax, status
    ret

DispatchControl endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverUnload
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DriverUnload proc pDriverObject:PDRIVER_OBJECT

        invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
        mov eax, pDriverObject
        invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject
    ret

DriverUnload endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              D I S C A R D A B L E   C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code INIT
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

local status:NTSTATUS
local pDeviceObject:PVOID

    mov status, STATUS_DEVICE_CONFIGURATION_ERROR

    invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
                                             0, FALSE, addr pDeviceObject

    .if eax == STATUS_SUCCESS
        invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName
        .if eax == STATUS_SUCCESS
            mov eax, pDriverObject
            assume eax:PTR DRIVER_OBJECT
            mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)], offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)], offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)], offset DispatchControl
            mov [eax].DriverUnload, offset DriverUnload
            assume eax:nothing
            mov status, STATUS_SUCCESS
        .else
            invoke IoDeleteDevice, pDeviceObject
        .endif
    .endif

    mov eax, status
    ret

DriverEntry endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end DriverEntry

:make
set drv=VirtToPhys
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj rsrc.obj

del %drv%.obj
move %drv%.sys ..

echo.
pause

5.2 驱动程序名称和符号连接名称

要描述设备以及符号连接的名称,就要先从UNICODE_STRING结构的定义讲起,我在前面已经提及,这种结构的字符串在内核中使用得非常普遍。
几乎在所有的驱动源码中–不管是用汇编还是用C写的–下面这样的字符定义序列是很常见的:

.const
uszDeviceName dw “\”,”D”,”e”,”v”,”i”,”c”,”e”,”\”,”D”,”e”,”v”,”N”,”a”,”m”,”e”,0
uszSymbolicLinkName dw “\”,”?”,”?”,”\”,”D”,”e”,”v”,”N”,”a”,”m”,”e”,0

.code
DriverEntry proc . . .
. . .
local usDeviceName:UNICODE_STRING
local usSymbolicLinkName:UNICODE_STRING
. . .
invoke RtlInitUnicodeString, addr usDeviceName, offset uszDeviceName
invoke RtlInitUnicodeString, addr usSymbolicLinkName, offset uszSymbolicLinkName

RtlInitUnicodeString函数的作用是计算Unicode字符串的大小并且填充UNICODE_STRING结构,一般来说,Unicode字符串都是在代码中静态定义的,并且在运行中保持不变,所以在链接的时候就把UNICODE_STRING结构给填好是完全可能的并且是很容易的,这样更容易理解、 更节省空间(省去8字节的UNICODE_STRING结构、最多3字节的对齐空间以及至少14字节调用RtlInitUnicodeString的代码)。这就是我为什么不喜欢以上代码的原因,我经常使用CCOUNTED_UNICODE_STRING宏来完成它,这样上面的代码就可以用2行来完成:

CCOUNTED_UNICODE_STRING “\\Device\\DevName”, usDeviceName, 4
CCOUNTED_UNICODE_STRING “\\??\\DevName”, usSymbolicLinkName, 4

如果你认同我的做法的话,也可以在自己的驱动程序中这样定义驱动名称和符号连接名称:

.const
CCOUNTED_UNICODE_STRING “\\Device\\devVirtToPhys”, g_usDeviceName, 4
CCOUNTED_UNICODE_STRING “\\??\\slVirtToPhys”, g_usSymbolicLinkName, 4

(注:原作者的宏在处理英文的Unicode字符串的时候是不错的,但是中文字符串就不行了,所以如果用到中文串,还是乖乖地动态转换最方便,常用的方法是先用RtlInitAnsiString函数生成一个ANSI_STRING结构,再用RtlAnsiStringToUnicodeString函数将ANSI_STRING转换到UNICODE_STRING即可,把这两句写成一个子程序或者宏的话,使用起来也是很方便的)。

在早些的Windows NT版本中,对象管理器中的”\??”目录是没有的,所以在那种情况下使用要将”\??”改为”\DosDevices”,这种用法在后续的Windows版本中也可以使用。为了向前兼容,系统在根目录下创建了一个”\DosDevices”连接,直接指向”\??”目录。

5.3 编写DriverEntry过程

每个内核模式驱动程序必须公开一个名为DriverEntry(当然,你完全可以取另外的名称)的过程,用来初始化驱动程序使用的各种资源,如常用的数据结构等。I/O管理器在装载驱动的时候调用这个过程,该过程在IRQL = PASSIVE_LEVEL下运行,所以在过程中可以存取分页的内存。DriverEntry过程在系统进程上下文中运行。
在深入一步之前,请注意这一行:

.code INIT

所有这样标记的代码将被放入PE文件的INIT节区中,在驱动程序初始化后,这部分代码就再也用不着了。INIT节区中的代码可以在DriverEntry过程返回后被丢弃,系统会自己决定在合适的时候丢弃它。
对于我们这个小小的驱动来说,这样做似乎没有多大的意义,因为我们的驱动是32字节对齐的(链接的时候使用了/align:32参数),这样这个节区占用不到一页的内存空间,所以即使指定了”INIT”也不会使它被丢弃。但是早些版本的Windows NT驱动程序往往有个很大的DriverEntry过程,其中有创建设备对象、申请资源、配置设备等大量代码,这样做的话就能明显地节省内存,所以如果你的DriverEntry足够大的话,这样做的意义就很明显了。

mov status, STATUS_DEVICE_CONFIGURATION_ERROR

默认情况下,我们先把返回值设置成”失败”,这样用户模式程序的StartService函数调用就会返回失败。

5.3.1 创建虚拟设备

    invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
              0, FALSE, addr pDeviceObject

既然驱动程序的主要作用是用于控制一些设备,包括物理设备、虚拟设备或者逻辑设备,那么我们首先必须将这些设备创建起来(本例中是虚拟设备),这可以通过调用IoCreateDevice函数来完成,函数将创建并初始化一个由驱动程序使用的设备对象(DEVICE_OBJECT结构),其原型如下:

IoCreateDevice proto stdcall DriverObject:PDRIVER_OBJECT, DeviceExtensionSize:DWORD, \
                             DeviceName:PUNICODE_STRING,  DeviceType:DEVICE_TYPE, \
                             DeviceCharacteristics:DWORD, Exclusive:BOOL, \
                             DeviceObject: PDEVICE_OBJECT

函数的参数描述如下:
◎ DriverObject–指向驱动对象(DRIVER_OBJECT结构),每个驱动程序在DriverEntry过程中会通过参数收到一个指向它的驱动对象的指针
◎ DeviceExtensionSize–指定设备扩展结构的大小(注:I/O管理器将自动分配这个内存,并把设备对象中的DeviceExtension指针指向这块内存),扩展结构的数据结构定义由驱动程序自己决定,我们的驱动太简单了,就没必要使用这个了
◎ DeviceName–指向一个Unicode字符串,用来指定设备名称,该名称必须是全路径的名称,在这里路径的含义并不是指硬盘上的路径,而是指对象管理器命名空间中的路径。这个参数在本例中是必须的,因为我们必须创建一个命名的设备,否则就无法创建一个符号连接,那样用户模式的进程也就无法访问设备了。设备名称在系统中必须是唯一的(注:在其他的应用中,你也可以创建不命名的设备)
◎ DeviceType–在系统定义的FILE_DEVICE_XXX常数中选定一个,用于指定设备的类型,当然也可以使用自定义的类型来表示一个新的类别,这里我们使用FILE_DEVICE_UNKNOWN
◎ DeviceCharacteristics–指明设备的额外属性,本例中使用0
◎ Exclusive–指明设备对象是否必须被独占使用,也就是说同时只能有一个句柄可以向设备发送I/O请求,在CreateFile函数的dwShareMode参数中可以指明是否独占设备。我们并不需要独占设备,这样这里使用FALSE
◎ DeviceObject–指向一个变量,如果函数调用成功的话,变量中将返回指向新创建的设备对象(DEVICE_OBJECT结构)的指针。

接下来,如果对IoCreateSymbolicLink的调用失败的话,我们需要从系统中将设备删除,所以要将IoCreateDevice函数返回的设备对象指针保存起来,以便在删除设备的时候使用。
设备对象指针在卸载驱动的DriverUnload过程中也要用到,但是那时在驱动对象中也可以得到设备对象指针,所以没有必要专门定义一个全局变量将设备对象指针保留到那个时候。

5.3.2 创建符号连接

    .if eax == STATUS_SUCCESS
        invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName

如果设备被成功地创建,那么为了使它能被Windows子系统”看见”,我们还需要创建符号连接(前面已经介绍过什么是符号连接了,不是吗?),这可以通过调用IoCreateSymbolicLink来完成,该函数需要的两个参数都是UNICODE_STRING类型的字符串指针–用来指定已存在的设备名称和还有需要新创建的连接名称。

5.3.3 指定分派过程

    .if eax == STATUS_SUCCESS
        mov eax, pDriverObject
        assume eax:PTR DRIVER_OBJECT
        mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],offset DispatchCreateClose
        mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],offset DispatchCreateClose
        mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],offset DispatchControl

符号连接成功创建后,就可以开始下一步了。
每个驱动程序都包括一个过程入口指针数组,用来指明不同的I/O请求被分派到那个函数来处理。每个驱动程序必须至少设置一个过程入口,用来处理IRP_MJ_XXX类型的请求。不同的驱动程序可以设置多个不同的过程入口,用来处理不同的IRP_MJ_XXX请求代码。例如,如果你需要得到”系统将要关闭”的通知的话,就必须”申明”处理该请求的分派过程,也就是在驱动对象的MajorFunction表中的IRP_MJ_SHUTDOWN一栏中填入该分派过程的地址。如果不需要处理某个请求,那么什么都不用做,因为I/O管理器在调用DriverEntry前默认将MajorFunction表中的每一项都填成了系统内部的IopInvalidDeviceRequest过程的地址,该过程会返回一个错误代码。
所以,你的责任就是要为每个你想要响应的I/O代码提供分派过程。
在驱动中我们必须至少处理3种I/O请求包,每个内核模式的驱动程序必须支持功能码IRP_MJ_CREATE,这样才能响应Win32的CreateFile函数调用,没有这个分派过程的话,Win32应用程序将无法获取设备的句柄;同样,IRP_MJ_CLOSE也是必须被支持的,否则就无法响应Win32的CloseHandle调用;最后,IRP_MJ_DEVICE_CONTROL允许用户模式程序通过Win32的DeviceIoControl调用来和驱动程序通讯,所以也必须被支持。
下面是这些功能码的说明:
◎ IRP_MJ_CREATE–用户模式代码调用CreateFile函数来获取目标设备对象的文件对象句柄时,I/O管理器发送此代码
◎ IRP_MJ_DEVICE_CONTROL–用户模式代码调用DeviceIoControl函数时,I/O管理器发送此代码
◎ IRP_MJ_CLOSE–用户模式代码调用CloseHandle函数来关闭目标设备对象的文件对象句柄时,I/O管理器发送此代码

例子程序中在同一个DispatchCreateClose过程中处理IRP_MJ_CREATE和IRP_MJ_CLOSE代码,我们稍候再详细分析这样做的原因。
在ntddk.inc中,还可以找到很多我们感兴趣的IRP_MJ_XXX类型的代码定义:

IRP_MJ_CREATE equ 0
. . .
IRP_MJ_CLOSE equ 2
IRP_MJ_READ equ 3
IRP_MJ_WRITE equ 4
. . .
IRP_MJ_DEVICE_CONTROL equ 0Eh
. . .
IRP_MJ_CLEANUP equ 12h

所有的IRP_MJ_XXX代码在ntddk.inc中都有定义,它们实际上就是MajorFunction数组的索引值而已,前面的代码只是填充了MajorFunction数组的三个元素而已:

mov [eax].DriverUnload,offset DriverUnload

DriverUnload过程的意图在于清理DriverEntry过程申请的一些资源,如果驱动需要被动态卸载的话,我们就必须提供卸载进程的分派过程,当用户模式代码使用SERVICE_CONTROL_STOP参数调用ControlService函数时,该分派过程就会被调用。

assume eax:nothing
mov status, STATUS_SUCCESS

这两句的意思是:驱动程序成功地完成初始化工作的话,那么就向系统返回STATUS_SUCCESS代码表示操作成功。

5.3.4 清理工作

	.else
            invoke IoDeleteDevice, pDeviceObject
        .endif
    .endif

如果调用IoCreateSymbolicLink失败,那么我们必须释放前面申请的一些资源,这里我们要删除前面用IoCreateDevice创建的设备对象,这可以通过调用IoDeleteDevice函数来完成。如果你还申请了别的一些资源的话,在这里也应该全部将它归还给系统。
请不要忘了,你必须随时留意你申请的内存和其他一些系统资源,在不需要再使用的话,要将它们释放掉。因为你现在是在内核模式下运行,这些清理工作必须自己完成,没人会帮你做这些事情。

mov eax, status
ret

最后,我们向系统返回状态代码,如果代码是STATUS_SUCCESS的话,驱动程序将保留在内存中,接下来I/O管理器会将对应的IRP请求发送给它;如果返回的是其他数值的话,系统会将驱动程序从内存中清除。

5.3.5 这里是新的对象

DriverEntry成功返回后,系统中多了三个新的对象,驱动”\Driver\VirtToPhys”,设备”\Device\devVirtToPhys”以及到设备的符号连接”\??\slVirtToPhys”。
驱动对象描述了系统中存在的独立的驱动程序,I/O管理器通过驱动对象获取每个驱动中不同的分派过程的入口地址。
设备对象描述了系统中的一个设备,包括设备的各种特征。通过设备对象,I/O管理器得到管理这个设备的驱动对象的指针。
文件对象是设备对象在用户模式上的表现,通过文件对象,I/O管理器得到设备对象的指针。
符号连接对用户模式是可见的,它被对象管理器所使用。
图5.1显示了各对象之间的相互联系,它能帮你更彻底地理解后面的内容。

图5.1 驱动、设备和文件对象之间的关系

5.4 I/O分派过程

I/O管理器调用分派过程来响应用户模式或者内核模式的请求,在单层或者多层中的最高层的驱动中,分派过程保证是在发起I/O请求的线程上下文中执行的,就像DriverEntry过程一样,分派过程也是在IRQL = PASSIVE_LEVEL下执行的,这意味着它们可以存取分页的系统资源。
所有的分派过程的申明如下:

DispatchRoutine proto stdcall pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

参数描述如下:
◎ pDeviceObject–指向设备对象(DEVICE_OBJECT结构),如果同一个驱动程序负责多个设备的话,从这个参数就能分辨出是哪个设备发送过来的IRP
◎ pIrp–指向描述I/O请求的IRP结构

I/O管理器创建一个IRP结构,用来描述I/O请求,并把它的指针通过pIrp参数传递给设备驱动程序,具体怎样处理就是设备驱动程序的事情了。
这种统一格式的接口的好处在于:I/O管理器可以用同样的方法调用任何的分派过程,而不需要知道驱动程序内部的细节知识(注:反过来想一下,如果不同分派过程的调用格式不同,那么I/O管理器必须知道所有的过程的调用格式和参数定义)。

5.5 IRP_MJ_CREATE和IRP_MJ_CLOSE的分派过程

为什么不同类型的IRP可以用同一个分派过程来处理呢?这是因为在我们这个简单的驱动程序中,唯一要在IRP_MJ_CREATE和IRP_MJ_CLOSE中要做的事情就是将IRP标记为已处理。
如果两者的处理方法不同的话,你还是应该创建独立的DispatchCreate的DispatchClose过程。
前面已经说过,处理IRP_MJ_CREATE是为了响应CreateFile的调用,如果不处理这个代码的话,Win32应用程序将无法获取设备句柄;同样处理IRP_MJ_CLOSE代码是为了响应对CloseHandle的调用。

DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

    mov eax, pIrp
    assume eax:ptr _IRP
    mov [eax].IoStatus.Status, STATUS_SUCCESS
    and [eax].IoStatus.Information, 0
    assume eax:nothing

我们填写I/O状态块来表示IRP的处理结果。
I/O状态块的Information字段被设置为0,表示设备句柄可以被打开。该字段对关闭的请求来说没有什么含义,但对其他的请求可能有不同的含义。
Status字段表示决定了CreateFile或CloseHandle的调用是否成功返回,所以我们要在这里填写STATUS_SUCCESS。

    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
    mov eax, STATUS_SUCCESS
    ret

DispatchCreateClose endp

现在必须调用IoCompleteRequest函数来表示驱动程序已经完成了IRP的处理,并将IRP返回给I/O管理器;然后返回STATUS_SUCCESS表示设备已经可以接收另一个I/O请求的处理了。
IoCompleteRequest的第一个参数告诉I/O管理器哪个IRP已经被处理完毕,第二个参数返回一个系统定义的表示实时优先级的常数,这是驱动程序为了补偿其他线程(在驱动的执行中)进行等待而给予的瞬间的优先级提高,例如对于音频设备,DDK建议使用IO_SOUND_INCREMENT值(等于8)。
例子程序中使用IO_NO_INCREMENT(等于0),也就是说当前线程的优先级保持不变。
IofCompleteRequest是一个fastcall类型的函数(注意名称中的f前缀),同样函数的stdcall版本是IoCompleteRequest,这是使用fastcall版本仅仅是为了教学的目的。

5.6 调用的约定

Windows NT内核API使用了3种调用约定:__stdcall、__cdecl和__fastcall,不幸的是,MASM编译器不支持最后一种调用方式。
__fastcall调用约定将前面两个dword类型的参数放入ECX和EDX寄存器,剩余的参数从右到左压入堆栈,由被调用的过程负责将参数从堆栈中清除。
Fastcall函数名称的修饰方式如下:函数名前面加上一个@符号,函数名后面加上@符号以及表示传递给函数的参数字节数的10进制数字,例如,IofCompleteRequest函数被修饰为:
@IofCompleteRequest@8
上面的修饰名表示这是一个fastcall类型的函数,函数名是IofCompleteRequest,它有两个dword类型的参数。
这个函数在\include\w2k\ntoskrnl.inc中定义,注意前缀SYSCALL

EXTERNDEF SYSCALL @IofCompleteRequest@8:PROC
IofCompleteRequest TEXTEQU <@IofCompleteRequest@8>

为了方便地调用fastcall类型的函数,我写了下面的宏:

fastcall MACRO api:REQ, p1, p2, px:VARARG

local arg

    ifnb 
        % for arg, @ArgRev(  )
            push arg
        endm
    endif

    ifnb 

        ifdifi , 
            mov ecx, p1
        endif

        ifnb 
            ifdifi , 
                mov edx, p2
            endif
        endif

    endif

    call api

ENDM

这里列出的是简化版的宏,全功能的版本在\include\w2k\ntddk.inc里面,当然原始的ntddk.h里面是没有这个宏的。

5.7 内存缓冲管理

I/O管理器提供了3种缓冲管理方式:buffered方式、direct方式和neither方式。
程序中只演示了使用DeviceIoControl函数来进行I/O处理,使用ReadFile和WriteFile函数来进行I/O处理的方法有点不同,你可以在\src\NtBuild中找到相关的例子。

5.7.1 Buffered I/O方式

开始I/O操作后,I/O管理器将用户缓冲区所属的虚拟内存页面提交以使其有效,然后从非分页内存池中分配一块足够容纳用户请求的内存块。
创建IRP的时候,I/O管理器将用户缓冲区的数据拷贝到申请的缓冲区中,并将其地址通过IRP结构的AssociatedIrp.SystemBuffer字段传递给驱动程序,数据的长度由IO_STACK_LOCATION结构的Parameters.DeviceIoControl.InputBufferLength字段指定(该结构的地址由IRP结构的Tail.Overlay.CurrentStackLocation字段指定,IoGetCurrentIrpStackLocation宏用来获取该结构地址)。
驱动程序处理IRP,并将输出数据拷贝到同一个缓冲区中。
当调用IofCompleteRequest函数来将IRP标志为已处理完毕的时候,I/O管理器将缓冲区中的数据拷贝到用户缓冲区,并释放缓冲区占用的内存,要拷贝的内存数量由IRP结构的IoStatus.Information字段指定。
正如读者所见,I/O管理器在整个过程中拷贝了2次数据,所以buffered I/O模式常用于一些慢速的、不传输大量数据的设备,就像我们的VirtToPhys一样。
但是这种模式也有很大的优点:I/O管理器负责解决了内存传输中可能出现的种种问题,我们根本不用去关心它。

5.7.2 Direct I/O方式

这种模式供direct memory access (DMA)使用。
我并没有详细研究过这种模式,所以在这篇教程中没有使用它。
当I/O管理器创建IRP时,它锁定用户缓冲区(将它标志为不可分页)并让驱动的代码可以通过80000000h以上的地址来存取它,I/O管理器用MemoryDescriptorLlist (MDL)结构来描述这块内存并将结构的指针放在IRP结构的MdlAddress字段中传递给驱动程序,当IRP使用完毕后,I/O管理器将缓冲区解锁。

5.7.3 Neither I/O方式

这种方式下,I/O管理器不进行任何方式的缓冲管理,一切由设备驱动程序自行处理。
驱动程序可以从stack location的Type3InputBuffer参数中得到输入缓冲区的用户模式虚拟地址,也可以从IRP的UserBuffer字段得到输出缓冲区的用户模式地址,但是如果你无法确定是否运行在用户模式调用者的进程上下文中的时候,这两个地址都是无法使用的。当然,作为作者,在本例中我们很清楚自己的驱动程序不是分层的,所以这些地址肯定可以使用。
我们知道,不分层的设备驱动程序总是在IRQL = PASSIVE_LEVEL下被用户模式调用,所以我们不需要关心用户缓冲区是否在内存中存在,即使它已经被交换出物理内存,内存管理器也会打理好一切的。
唯一的问题是:用户模式代码可能传过来一个错误的地址,或者在某处已经将缓冲区释放了–这在多线程的情况下完全可能发生。
我们必须预见到这种情况并且能正确地处理它,所以使用结构化异常处理(SEH)是很必要的(有关结构化异常处理,见《Windows环境下32位汇编语言程序设计》的第14章:异常处理,或者参考其他的相关资料),但要注意的是,内核模式的SEH和用户模式下使用的方法是一样的,所以你无法用它截获所有的异常,例如,就是安置了SEH后,除零错误还是会引发一个蓝屏死机画面。(使用了SEH的代码例子见\src\Article4-5\NtBuild)

5.8 IRP_MJ_DEVICE_CONTROL的分派过程

当驱动程序指定了IRP_MJ_DEVICE_CONTROL的分派过程后,I/O管理器收到用户模式代码对DeviceIoControl的调用后,就会把IRP传递给该分派过程。

and dwBytesReturned, 0

这句代码的意思是将I/O管理器将要拷贝的数据数量暂时设置为0。

mov esi, pIrp
assume esi:ptr _IRP

IoGetCurrentIrpStackLocation esi
mov edi, eax
assume edi:ptr IO_STACK_LOCATION

IoGetCurrentIrpStackLocation宏取出IRP的stack location的指针,也就是指向一个IO_STACK_LOCATION结构的指针,该结构中包含了一些常用的数据:

.if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_GET_PHYS_ADDRESS

判断一下I/O控制代码,我们不应该处理不认识的代码。

NUM_DATA_ENTRY equ 4
DATA_SIZE equ (sizeof DWORD) * NUM_DATA_ENTRY
IOCTL_GET_PHYS_ADDRESS equ CTL_CODE(FILE_DEVICE_UNKNOWN, 800h, METHOD_BUFFERED, FILE_READ_ACCESS + FILE_WRITE_ACCESS)

我们要处理的IOCTL_GET_PHYS_ADDRESS控制代码是在common.inc文件中作为常数定义的,这个include文件在驱动程序和主程序中都用到了。

.if ( [edi].Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE ) && ( [edi].Parameters.DeviceIoControl.InputBufferLength >= DATA_SIZE )

检查一下输入和输出缓冲区的长度,如果长度不够则停止处理。
IO_STACK_LOCATION结构的OutputBufferLength和InputBufferLength字段和DeviceIoControl函数指定的nOutBufferSize、nInBufferSize参数的取值是相符的。

mov edi, [esi].AssociatedIrp.SystemBuffer

我们可以从IRP的stack location取得指向系统缓冲区的指针,这个缓冲区中包含了用户模式代码传递给驱动的数据,本例中的数据是4个虚拟地址,驱动程序要将它们转换成物理地址。

assume edi:ptr DWORD

告诉编译器edi寄存器指向的是dword类型的值,否则的话我们每次使用edi的时候就一定要加上PTR DWORD了。

            xor ebx, ebx
            .while ebx < NUM_DATA_ENTRY
                invoke GetPhysicalAddress, [edi][ebx*(sizeof DWORD)]
                mov [edi][ebx*(sizeof DWORD)], eax
                inc ebx
            .endw

循环NUM_DATA_ENTRY次,每次从缓冲区中取出一个dword(也就是虚拟地址数据),然后调用GetPhysicalAddress以得到转换后的物理地址,并将它写回缓冲区的同样位置去。

mov dwBytesReturned, DATA_SIZE
mov status, STATUS_SUCCESS

所有工作完成后,将处理的总字节数放入dwBytesReturned,并把返回代码设置为成功。

        .else
            mov status, STATUS_BUFFER_TOO_SMALL
        .endif
    .else
        mov status, STATUS_INVALID_DEVICE_REQUEST
    .endif

遇到其他错误的话,返回对应的错误代码。

assume edi:nothing
push status
pop [esi].IoStatus.Status

完成IRP后,我们将当前status变量的值放入状态块的Status字段,这些状态值可以转换成Win32错误代码,对应如下:

Nt Status                      Win32 Error
=============================  ================
STATUS_SUCCESS                 NO_ERROR
STATUS_BUFFER_TOO_SMALL        ERROR_INSUFFICIENT_BUFFER
STATUS_INVALID_DEVICE_REQUEST  ERROR_INVALID_FUNCTION

Ntdll.dll中的RtlNtStatusToDosError函数可以将内核状态代码转换到Win32错误代码,用户模式应用程序调用GetLastError就可以得到这个代码。

push dwBytesReturned
pop [esi].IoStatus.Information

状态块的Information字段需要放置I/O管理器要拷贝到用户缓冲区的数据字节数,最后DeviceIoControl函数的调用者会在lpBytesReturned指向的变量中得到这个字节数数值。

assume esi:nothing
fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
mov eax, status
ret

调用IofCompleteRequest来结束对IRP的处理。
最后不用忘记,即使是收到了不认识的I/O控制码,我们也应该将I/O状态块设置合适的NTSTATUS值,并将Information字段设置为0,并用IO_NO_INCREMENT参数调用IofCompleteRequest来结束对IRP的处理。

5.9 内存地址转换

内核模式的代码可以将虚拟地址转换成物理地址,MmGetPhysicalAddress函数可以完成这个功能,例子中的GetPhysicalAddress子程序可以基本上完成相同的功能(当然少了一些扩展的功能),不幸的是我没有机会描述其中的工作细节,读者请自行参考”Inside Microsoft Windows 2000″一书(David Solomon和Mark Russinovich著),这一段的代码如下:

GetPhysicalAddress proc dwAddress:DWORD

    ; Converts virtual address in dwAddress to corresponding physical address

    mov eax, dwAddress
    mov ecx, eax

    shr eax, 22                               ; (Address >> 22) => Page Directory Index, PDI
    shl eax, 2                                ; * sizeof PDE = PDE offset

    mov eax, [0C0300000h][eax]                ; [Page Directory Base + PDE offset]

    .if ( eax & (mask pde4kValid) )           ; .if ( eax & 01y )
        ; PDE is valid
        .if !( eax & (mask pde4kLargePage) )  ; .if ( eax & 010000000y )
            ; small page (4kB)
            mov eax, ecx
            ; (Address >> 12) * sizeof PTE => PTE offset
            shr eax, 10
            and eax, 1111111111111111111100y
            add eax, 0C0000000h               ; add Page Table Array Base
            mov eax, [eax]                    ; fetch PTE

            .if eax & (mask pteValid)         ; .if ( eax & 01y )
                ; PTE is valid
                ; mask PFN   (and eax, 11111111111111111111000000000000y)
                and eax, mask ptePageFrameNumber

                ; We actually don't need these two lines
                ; because of module base is always page aligned
                and ecx, 00000000000000000000111111111111y  ; Byte Index
                add eax, ecx                  ; add byte offset to physical address
            .else
                xor eax, eax                  ; error
            .endif
        .else
            ; large page (4mB)
            ; mask PFN   (and eax, 11111111110000000000000000000000y)
            and eax, mask pde4mPageFrameNumber
            and ecx, 00000000001111111111111111111111y      ; Byte Index
            add eax, ecx                      ; add byte offset to physical address
        .endif
    .else
        xor eax, eax                          ; error
    .endif
    ret

GetPhysicalAddress endp

GetPhysicalAddress子程序将输入参数中的虚拟地址转换成物理地址后返回。

5.10 DriverUnload过程

DriverUnload过程非常直接了当,就是用于删除驱动程序创建的符号连接和设备对象。当用户模式代码以SERVICE_CONTROL_STOP参数调用ControlService函数时,该过程即被调用。

invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
mov eax, pDriverObject
invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject

DriverUnload过程做的工作和DriverEntry过程刚刚相反,它调用IoDeleteSymbolicLink函数来清除对象管理器命名空间中的符号连接,并调用IoDeleteDevice函数来删除设备对象本身。
前面也提到过,在内核模式下,你必须自己来释放所有申请的资源。
表5.1列出了驱动程序的各主要过程运行的进程上下文和IRQL,这些是你应该了解的,当然该表的内容仅对不分层或者分层驱动中的最高层有效。

表5.1:
User-mode           Kernel-mode                 Process context             IRQL
==================  ==========================  =========================== ============
StartService        DriverEntry                 System                      PASSIVE_LEVEL
CreateFile          IRP_MJ_CREATE               User-mode caller            PASSIVE_LEVEL
DeviceIoControl     IRP_MJ_DEVICE_CONTROL       User-mode caller            PASSIVE_LEVEL
ReadFile            IRP_MJ_READ                 User-mode caller            PASSIVE_LEVEL
WriteFile           IRP_MJ_WRITE                User-mode caller            PASSIVE_LEVEL
CloseHandle         IRP_MJ_CLEANUP,IRP_MJ_CLOSE User-mode caller            PASSIVE_LEVEL
ControlService(STP) DriverUnload                System                      PASSIVE_LEVEL

5.11 编译驱动程序的方法

:make
set drv=skeleton
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj rsrc.obj
del %drv%.obj
move %drv%.sys ..
echo.
pause

这些内容已经在第3节中解释过了,唯一新增的是多了个/ignore:4078选项,这是因为程序中有2个不同属性的INIT节区,所以链接器会报下面的警告(增加这个选项可以抑制该警告信息):

LINK : warning LNK4078: multiple “INIT” sections found with different attributes (E2000020)

5.12 添加资源

本例中我们还在驱动的资源中加上了版本信息,这可以用通常的资源脚本来完成(见rsrc.rc文件):

VS_VERSION_INFO VERSIONINFO
 FILEVERSION 1,0,0,0
 PRODUCTVERSION 1,0,0,0
 FILEFLAGSMASK 0x3fL
 FILEFLAGS 0x0L
 FILEOS 0x40004L
 FILETYPE 0x1L
 FILESUBTYPE 0x0L
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "040904E4"
        BEGIN
            VALUE "Comments", "Written by Four-F\0"
            VALUE "CompanyName", "Four-F Software\0"
            VALUE "FileDescription", "Kernel-Mode Driver VirtToPhys v1.00\0"
            VALUE "FileVersion", "1, 0, 0, 0\0"
            VALUE "InternalName", "VirtualToPhysical\0"
            VALUE "LegalCopyright", "Copyright ? 2003, Four-F\0"
            VALUE "OriginalFilename", "VirtToPhys.sys\0"
            VALUE "ProductName", "Kernel-Mode Driver Virtual To Physical Address Converter\0"
            VALUE "ProductVersion", "1, 0, 0, 0\0"
        END
    END
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x409, 1200
    END
END

这里没有什么特殊的东西,用常用的方法编译和链接就可以了。

5.13 关于调试

使用SoftIce中和驱动和设备有关的命令可以获取很多和驱动及其设备相关的有用信息,读者可以自行查看SoftIce的命令手册,在我的机器上,它的输出如下:

图5.2 driver VirtToPhys命令的输出

图5.3 device devVirtToPhys命令的输出

对你来说,SoftICE显示的信息应该是很好理解的,这些信息是从DRIVER_OBJECT以及 DEVICE_OBJECT结构中获取的,使用这些信息可以很容易在内存中找到这些对象并对它们的分派过程设置断点。

原文链接:http://211.90.241.130:22366/view.asp?file=324

☆版权☆

* 网站名称:obaby@mars
* 网址:https://oba.by/
* 个性:https://oba.by/
* 本文标题: 《驱动开发学习笔记(3-6)–Four-F的驱动开发教程-全功能的驱动程序分析》
* 本文链接:https://www.zhongxiaojie.cn/2009/09/291
* 短链接:https://oba.by/?p=291
* 转载文章请标明文章来源,原文标题以及原文链接。请遵从 《署名-非商业性使用-相同方式共享 2.5 中国大陆 (CC BY-NC-SA 2.5 CN) 》许可协议。


You may also like

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注