驱动开发学习笔记(3-5)–Four-F的驱动开发教程-I/O子系统

在这里下载本文的源代码

4. I/O子系统

※ 和本节内容相关的源代码见KmdKit\examples\simple\VirtToPhys

4.1 I/O管理器

在用户模式下,我们可以通过访问某个地址来直接调用dll中的函数,与此不同的是,从系统的稳定性考虑,在内核模式下这样做的话是很危险的。所以,系统提供了和内核模式通讯的媒介–I/O管理器,它是I/O子系统的部件之一。I/O管理器将应用程序、系统部件和设备连接起来,并定义了一个架构来支持设备驱动程序。
图4.1是I/O管理器如何在用户模式程序和驱动程序之间进行沟通的简单图解。

图4.1 I/O子系统的简单架构

从上图可以看到,所有用户模式程序到设备(包括设备驱动程序)的调用都必须通过I/O管理器来完成。
一般来说,用户模式的操作都被转换成了对具体硬件设备的I/O操作,仅对于某些设备,设备由驱动程序来创建和控制,这些设备就是虚拟设备。当然,创建这些设备并不意味着你创造了什么硬件(不然我每天创建n个显卡再卖掉,然后换BMW,呵呵~~~~),而仅仅是在内存中创建了一个新的对象而已。每个对象和一个物理设备或者逻辑设备对应,用于描述它们的特征。
创建设备后,驱动程序告诉I/O管理器:”这里有个我控制的设备,如果你收到了操作这个设备的I/O请求的话,直接发给我好了,剩下的由我来搞定!”。驱动程序知道如何对自己管理的设备进行I/O操作,I/O管理器唯一的职责在于创建I/O请求并把它发送给适当的设备驱动程序。用户模式的代码不知道(也不必知道)其中的细节,也不用知道究竟是哪个驱动程序在管理哪个设备。

4.2 VirToPhys驱动程序的控制程序

4.2.1 控制程序源代码

严格地说,这个代码包括了注册和启动驱动程序,以及作为客户端程序和设备进行通讯的代码。

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; VirtToPhys.asm - Driver Control Program for VirtToPhys driver
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              I N C L U D E   F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib
include \masm32\include\winioctl.inc
include \masm32\Macros\Strings.mac
include common.inc
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                        C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                      BigNumToString
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
BigNumToString proc uNum:UINT, pszBuf:LPSTR
 
; This function accepts a number and converts it to a
; string, inserting commas where appropriate.
local acNum[32]:CHAR
local nf:NUMBERFMT
 
    invoke wsprintf, addr acNum, $CTA0("%u"), uNum
    and nf.NumDigits, 0
    and nf.LeadingZero, FALSE
    mov nf.Grouping, 3
    mov nf.lpDecimalSep, $CTA0(".")
    mov nf.lpThousandSep, $CTA0(" ")
    and nf.NegativeOrder, 0
    invoke GetNumberFormat, LOCALE_USER_DEFAULT, 0, addr acNum, addr nf, pszBuf, 32
    ret
 
BigNumToString endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       start
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
start proc uses esi edi
 
local hSCManager:HANDLE
local hService:HANDLE
local acModulePath[MAX_PATH]:CHAR
local _ss:SERVICE_STATUS
local hDevice:HANDLE
 
local adwInBuffer[NUM_DATA_ENTRY]:DWORD
local adwOutBuffer[NUM_DATA_ENTRY]:DWORD
local dwBytesReturned:DWORD
 
local acBuffer[256+64]:CHAR
local acThis[64]:CHAR
local acKernel[64]:CHAR
local acUser[64]:CHAR
local acAdvapi[64]:CHAR
 
local acNumber[32]:CHAR
 
    invoke OpenSCManager, NULL, NULL, SC_MANAGER_ALL_ACCESS
    .if eax != NULL
        mov hSCManager, eax
        push eax
        invoke GetFullPathName, $CTA0("VirtToPhys.sys"), \
                                sizeof acModulePath, addr acModulePath, esp
        pop eax
 
        invoke CreateService, hSCManager, $CTA0("VirtToPhys"), \
                              $CTA0("Virtual To Physical Address Converter"), \
                              SERVICE_START + SERVICE_STOP + DELETE, SERVICE_KERNEL_DRIVER,\
                              SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, addr acModulePath,\
                              NULL, NULL, NULL, NULL, NULL
        .if eax != NULL
            mov hService, eax
            ; Driver's DriverEntry procedure will be called
            invoke StartService, hService, 0, NULL
            .if eax != 0
                ; Driver will receive I/O request packet (IRP) of type IRP_MJ_CREATE
                invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ+GENERIC_WRITE,\
                                0, NULL, OPEN_EXISTING, 0, NULL
                .if eax != INVALID_HANDLE_VALUE
                    mov hDevice, eax
 
                    lea esi, adwInBuffer
                    assume esi:ptr DWORD
                    invoke GetModuleHandle, NULL
                    mov [esi][0*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32)
                    mov [esi][1*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("user32.dll", szUser32)
                    mov [esi][2*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32)
                    mov [esi][3*(sizeof DWORD)], eax
 
                    lea edi, adwOutBuffer
                    assume edi:ptr DWORD
                    ; Driver will receive IRP of type IRP_MJ_DEVICE_CONTROL
                    invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \
                                            esi, sizeof adwInBuffer, \
                                            edi, sizeof adwOutBuffer, \
                                            addr dwBytesReturned, NULL
 
                    .if ( eax != 0 ) && ( dwBytesReturned != 0 )
                        invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \
                                                  addr acModulePath, sizeof acModulePath
                        lea ecx, acModulePath[eax-5]
                        .repeat
                            dec ecx
                            mov al, [ecx]
                        .until al == '\'
                        inc ecx
                        push ecx
 
                        CTA0 "%s \t%08Xh\t%08Xh   ( %s )\n", szFmtMod
 
                        invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber
                        pop ecx
                        invoke wsprintf, addr acThis, addr szFmtMod, ecx, \
                                         [esi][0*(sizeof DWORD)], \
                                         [edi][0*(sizeof DWORD)], addr acNumber
 
                        invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \
                                         [esi][1*(sizeof DWORD)], \
                                         [edi][1*(sizeof DWORD)], addr acNumber
 
                        invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \
                                         [esi][2*(sizeof DWORD)], \
                                         [edi][2*(sizeof DWORD)], addr acNumber
 
                        invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \
                                         [esi][3*(sizeof DWORD)], \
                                         [edi][3*(sizeof DWORD)], addr acNumber
 
                        invoke wsprintf, addr acBuffer,\
                                $CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \
                                addr acThis, addr acKernel, addr acUser, addr acAdvapi
 
                        assume esi:nothing
                        assume edi:nothing
                        invoke MessageBox,NULL,addr acBuffer,$CTA0("Modules Base Address"), \
                                           MB_OK + MB_ICONINFORMATION
                    .else
                        invoke MessageBox,NULL,$CTA0("Can't send control code to device."),\
                           NULL,MB_OK + MB_ICONSTOP
                    .endif
                    ; Driver will receive IRP of type IRP_MJ_CLOSE
                    invoke CloseHandle, hDevice
                .else
                    invoke MessageBox, NULL, $CTA0("Device is not present."), NULL, MB_ICONSTOP
                .endif
                ; DriverUnload proc in our driver will be called
                invoke ControlService, hService, SERVICE_CONTROL_STOP, addr _ss
            .else
                invoke MessageBox, NULL, $CTA0("Can't start driver."), NULL, MB_OK + MB_ICONSTOP
            .endif
            invoke DeleteService, hService
            invoke CloseServiceHandle, hService
        .else
            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_OK + MB_ICONSTOP
        .endif
        invoke CloseServiceHandle, hSCManager
    .else
        invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), NULL, \
                           MB_OK + MB_ICONSTOP
    .endif
    invoke ExitProcess, 0
start endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end start

代码将输入的数据发送给设备,并将设备返回的数据格式化并显示出来,这里只有少量新的东西,它们是3个调用:CreateFile、DeviceIoControl和CloseHandle,这些调用的参数都是设备(注意:不是驱动)的句柄。

4.2.2 设备对象

在被装载后,VirtToPhys驱动程序创建了一个名为”devVirtToPhys”的设备(dev前缀并不是必须的,但我还是打算把它加上–原因后面再解释)。
设备的名称放在对象管理器中,对象管理器是系统中负责创建、删除、保护和跟踪对象的组件,根据约定,设备对象存放在\Device目录下,应用程序用Win32 API是无法直接访问的。
要想研究对象管理器维护的对象名称列表的话,可以使用工具Windows Object Explorer (WinObjEx)(见http://www.wasm.ru/)或者Mark Russinovich编写的Object Viewer(见http://www.sysinternals.com/)。
要观察VirtToPhys程序在你的计算机上创建的对象,仅仅需要运行VirtToPhys.exe程序,但是不要关闭窗口。

图4.2 对象管理器中的devVirtToPhys设备对象

图4.3 devVirtToPhys对象的属性

4.2.3 驱动程序对象

VirtToPhys驱动程序对象(注意这里没有任何前缀)被放在\Driver目录中。

图4.4 对象管理器中的VirtToPhys驱动程序对象

4.2.4 符号连接对象

内部设备名称无法被Win32应用程序使用,因为除\BaseNamedObjects和\??外的所有目录对用户模式程序来说都是不可见的–而设备名称又必须放置于对象管理器的特定目录下才行。
“\??”目录中包括了对真实存在的设备名称的符号链表,设备驱动程序负责在这个目录中创建一个符号连接,这样设备就可以被Win32应用程序访问。
因此,如果我们的驱动程序对应的设备对象希望被用户模式的代码打开的话,就必须在”\??”目录中创建一个符号连接,指向”\Device”目录中的设备对象,然后,当调用者需要获取设备句柄时,I/O管理器就能够找到它。
顺便提一句,你也可以在用户模式下通过QueryDosDevice函数检测符号连接是否存在,还可以通过DefineDosDevice函数来修改它。
打开”\??”目录后,你可以看到大量的符号连接,早先在Windows NT 4系统中,这个目录的名称为\DosDevices,后来才更名为”\??”,这主要是基于性能上的考虑,因为这样目录名称按照字母表顺序会排在最前面。
为了向前兼容,对象管理器中的”\DosDevices”目录直接连到”\??”目录。
VirtToPhys驱动程序在”\??”目录中创建了指向”devVirtToPhys”设备的符号连接”slVirtToPhys”,真实设备的全名是”\Device\devVirtToPhys”,这里我用了”dev”前缀。

图4.5 对象管理器中的slVirtToPhys符号连接

图4.6 slVirtToPhys符号连接的对象属性

为各种对象的名称加上前缀是为了区别它们,最好不要将设备名、符号连接名称和驱动的名称混淆起来。另外,一个要点是符号连接名称必须定向到一个有效的设备名上面;另一个要点是,在同一个设备目录下不能有两个同名的对象,正像文件系统的一个目录下不能有两个同名文件一样。
这样,当StartService函数执行后,系统中就多了三个新的对象:”\Driver\VirtToPhys”驱动、”\Device\devVirtToPhys”设备和符号连接”\??\slVirtToPhys”。
不知道读者是否还记得,第二课”服务”中曾经提到过”\??”,那是在驱动的路径名中出现的,如”\??\C:\masm32\…”,实际上,”\??\C:”是一个符号连接,它指向名为”\Device\HarddiskVolume1″的内部设备,也就是系统中第一个物理硬盘上面的第一个分区。

4.2.5 文件对象

现在回头来看看源代码,当驱动被启动后,我们打算去调用它。为了完成这个功能,我们只需使用CreateFile函数来打开驱动,以此获得一个文件句柄。
文档中关于CreateFile的描述真是太多了,但是只有很少一部分是和设备驱动程序相关的。

CreateFile proto stdcall     lpFileName:LPCSTR,            dwDesiredAccess:DWORD, \
                             dwShareMode:DWORD,            lpSecurityAttributes:LPVOID, \
                             dwCreationDistribution:DWORD, dwFlagsAndAttributes:DWORD, \
                             hTemplateFile:HANDLE

这个函数可以创建或者打开一个已存在的对象,而不仅仅是文件(这似乎和它的名字不符,但是有很多Create打头的函数都有创建和打开的功能),微软其实可以干脆把它命名为CreateObject好了。
在这里,设备也可以作为对象来考虑。

函数的参数描述如下:
◎ lpFileName–指向以0结尾的表示设备名称的字符串,这里要用到指向设备对象的符号连接名
◎ dwDesiredAccess–指定访问设备的方式,可以有两个取值:GENERIC_READ表示写操作,允许将数据写到设备中;GENERIC_WRITE表示读操作,允许从设备读取数据,这两个值可以合并起来使用
◎ dwShareMode–指定设备是否可以被共享,0表示设备不能被共享,这样并发的访问会失败,直到句柄被关闭为止;要共享设备的话,可以指定下面的两个值:FILE_SHARE_READ表示可以并发地读取设备,FILE_SHARE_WRITE表示可以并发地写设备
◎ lpSecurityAttributes–指向SECURITY_ATTRIBUTES结构的指针,在此无用,所以可以指定为NULL
◎ dwCreationDistribution–指明当文件存在或不存在时函数采取的动作,对于设备来说,这个参数应该使用OPEN_EXISTING
◎ dwFlagsAndAttributes–文件属性,在这里总是使用0
◎ hTemplateFile–指定文件模板的句柄,在这里总是使用0

如果CreateFile函数成功地创建或者打开了指定的设备,那么返回值就是设备句柄,否则返回值是INVALID_HANDLE_VALUE。大部分需要返回句柄的Windows函数在失败时返回0,但是CreateFile函数返回的INVALID_HANDLE_ VALUE值的定义却是-1。
我们这样使用CreateFile函数:

      invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ + GENERIC_WRITE, \
               0, NULL, OPEN_EXISTING, 0, NULL

最后五个参数就不用解释了吧?第二个参数合并了GENERIC_READ以及GENERIC_WRITE,表示既要将数据写到设备,也要从设备读取处理的结果数据。
现在来看看第一个参数,这是指向符号连接名称的字符串指针,名称格式是”\\.\slVirtToPhys”,”\\.\”是Win32中定义本地计算机的方法,CreateFile函数实际上是NtCreateFile函数的封装(位于\%SystemRoot%\System32\ntdll.dll中),后者将访问定向到系统服务中去(注意不要和Win32服务进程混淆)。
系统服务是各种子系统环境到内核的入口点,系统服务的表现就是在x86处理器的机器上执行int 2eh(Windows NT/W2K) 或sysenter指令(Windows XP/2003),触发执行这些指令使线程切换到内核模式,并进入系统服务分派程序。
NtCreateFile将本地计算机的别名”\\.\”用”\??”代替(这样\\.\slVirtToPhys就变成了\?? \slVirtToPhys)并调用内核的ObOpenObjectByName函数,通过符号连接名称,ObOpenObjectByName函数找到\Device\devVirtToPhys对象并返回它的指针。NtCreateFile使用这个指针创建新的文件对象并返回句柄。
操作系统把所有的I/O操作请求抽象化成一个虚拟的文件,隐藏了目标设备的I/O操作可能在结构上并不等同于文件的事实,然后由驱动程序负责将对虚拟文件的请求转换成对具体硬件的请求,这种抽象可以推广到所有用户进程和设备之间的界面,所有对这种虚拟文件的读写操作都被当作是简单的流操作来处理。
在CreateFile返回前,I/O管理器创建了IRP_MJ_CREATE类型的IRP并将其传给驱动处理,驱动中负责响应这个IRP的子程序代码会在发起I/O请求(就是调用CreateFile函数的代码)的线程环境中执行,该线程的IRQL等于PASSIVE_LEVEL。如果驱动的子程序成功返回,那么对象管理器在进程的句柄表中为文件对象创建一个句柄,然后将其一层层地返回,直到返回到CreateFile函数。
新创建的文件对象是个实体对象,并不列在对象管理器中,读者可以用Mark Russinovich编写的Process Explorer工具来观察到它们(可以从http://www.sysinternals.com下载)。

图4.7 文件对象

图4.8 文件对象的属性

现在来总结一下,”\\.\slVirtToPhys”会被转换成符号连接”\??\slVirtToPhys”,最终用来找到需要的”\Device\devVirtToPhys”设备。然后可以从负责维护该设备的驱动程序中取得设备对象DEVICE_OBJECT,接下来I/O管理器将IRP_MJ_CREATE请求传递给驱动,驱动程序知道如何处理这个请求。如果驱动打算处理该请求,那么它返回成功代码,这样对象管理器创建对应这个设备的虚拟文件句柄并将它返回给用户模式代码。
句柄和符号连接为间接访问系统资源提供服务,这种”间接”方式将应用程序和系统的数据结构隔离开来。

4.2.6 和设备通讯

		.if eax != INVALID_HANDLE_VALUE
                    mov hDevice, eax

CreateFile函数返回有效的设备句柄后,我们将它保存在hDevice变量中,现在可以用ReadFile、WriteFile以及DeviceIoControl函数来和设备通讯了。DeviceIoControl函数是用来和设备通讯的通用函数,它的原型如下:

DeviceIoControl proto stdcall hDevice:HANDLE,         dwIoControlCode:DWORD, \
                              lpInBuffer:LPVOID,      nInBufferSize:DWORD, \
                              lpOutBuffer:LPVOID,     nOutBufferSize:DWORD, \
                              lpBytesReturned:LPVOID, lpOverlapped:LPVOID

DeviceIoControl函数的参数比CreateFile多,但用起来都很简单。
◎ hDevice–设备的句柄
◎ dwIoControlCode–控制代码,指出要进行什么操作,详细内容后面再做解释
◎ lpInBuffer–指向包含操作所需的数据的缓冲区指针,如果控制代码指明的操作并不需要输入数据的话,这里可以用NULL
◎ nInBufferSize–lpInBuffer参数指向的缓冲区的大小
◎ lpOutBuffer–指向用来接收输出数据的缓冲区,如果dwIoControlCode指明的操作不产生输出数据的话,这里可以用NULL
◎ nOutBufferSize–lpOutBuffer参数指向的缓冲区的大小
◎ lpBytesReturned–指向一个变量,用来返回放入lpOutBuffer缓冲区的数据的数量
◎ lpOverlapped–指向OVERLAPPED结构,这个参数仅在异步操作的时候才需要。我们的操作是同步的(就是在驱动的过程返回前DeviceIoControl函数也不返回),所以在这里使用NULL

4.2.7 I/O控制代码

设备驱动程序可以被当作内核模式函数包来看待,I/O控制代码就是用来指定访问其中的哪个函数的。DeviceIoControl函数的dwIoControlCode参数就是这个代码,它指出了我们需要进行的操作,以及如何进行操作。
控制代码是32位数字型常量,可以CTL_CODE宏来定义,它们定义在winioctl.inc和ntddk.inc文件中。

图4.9 I/O控制代码的定义

控制代码中各数据位字段的含义如下:
◎ DeviceType–设备类型(16bit)指出了设备的类型,微软保留了0-7FFFh的取值,剩下的8000h-0FFFFh供开发商定义新的内核模式驱动程序。我们可以在\include\w2k\ntddk.inc文件中找到一组FILE_DEVICE_XXX符号常量,这些值都是微软保留的值,我们可以使用其中的FILE_DEVICE_UNKNOWN。当然你也可以定义另外一个FILE_DEVICE_XXX值
◎ Access–存取代码(2bit)指明应用程序存取设备的方式,由于这个字段只有2位,所以只有4种可能性:
· FILE_ANY_ACCESS (0)–最大的存取权限,就是什么操作都可以
· FILE_READ_ACCESS (1)–读权限,设备将数据传递到指定的缓冲区
· FILE_WRITE_ACCESS (2)–写权限,可以从内存中向设备传递数据
· FILE_READ_ACCESS or FILE_WRITE_ACCESS (3)–读写权限,设备和内存缓冲区之间可以互相传递数据
◎ Function–功能代码(12bit)用来描述要进行的操作,我们可以用800h-0FFFh来定义自己的I/O控制代码,0-7FFh之间的值是被微软保留的,用来定义公用的I/O控制代码
◎ Method–缓冲模式(2bit)表示I/O管理器如何对输入和输出的数据进行缓冲,这个字段的长度是2位,所以有4种可能性:
· METHOD_BUFFERED (0)–对I/O进行缓冲
· METHOD_IN_DIRECT (1)–对输入不进行缓冲
· METHOD_OUT_DIRECT (2)–对输出不进行缓冲
· METHOD_NEITHER (3)–都不缓冲

缓冲模式的管理我们会在后面进行更详细的讨论,当前最重要的是,虽然进行缓冲会带来一些额外的内存开销,但却是最安全的,因为系统已经做好了相关的全部工作。在传输的数据小于一页(4Kb)的时候,驱动程序通常使用缓冲方式的I/O,因为对大量小块内存进行内存锁定带来的开销也是很大的。在VirtToPhys驱动程序中,我们使用带缓冲的方式。
读者可以手工去定义I/O控制代码,但是使用CTL_CODE宏会方便得多,它提供了创建IOCTL值的算法,具体如下:

CTL_CODE MACRO DeviceType:=<0>, Function:=<0>, Method:=<0>, Access:=<0>
    EXITM %(((DeviceType) SHL 16) OR ((Access) SHL 14) OR ((Function) SHL 2) OR (Method))
ENDM

前面我曾说过,CTL_CODE宏在winioctl.inc文件和ntddk.inc文件中都有定义。
在例子程序中,由于定义的NUM_DATA_ENTRY、DATA_SIZE常量和IOCTL_GET_PHYS_ADDRESS控制代码在服务控制程序以及驱动中都要被用到,所以我们将它放在一个单独的common.inc文件中,这样万一有所修改的话,就可以直接反映到两个代码中。

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)

4.2.8 交换数据

现在回到驱动的源代码中看看:

                    lea esi, adwInBuffer
                    assume esi:ptr DWORD
                    invoke GetModuleHandle, NULL
                    mov [esi][0*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32)
                    mov [esi][1*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("user32.dll", szUser32)
                    mov [esi][2*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32)
                    mov [esi][3*(sizeof DWORD)], eax

我们在adwInBuffer缓冲区中填写需要进行转换的虚拟地址。

                    lea edi, adwOutBuffer
                    assume edi:ptr DWORD
                    invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \
                                            esi, sizeof adwInBuffer, \
                                            edi, sizeof adwOutBuffer, \
                                            addr dwBytesReturned, NULL

调用DeviceIoControl的时候,我们将缓冲区传给驱动,这样它会将虚拟地址转换成物理地址。

                    .if ( eax != 0 ) && ( dwBytesReturned != 0 )
                        invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \
                                                  addr acModulePath, sizeof acModulePath
                        lea ecx, acModulePath[eax-5]
                        .repeat
                            dec ecx
                            mov al, [ecx]
                        .until al == '\'
                        inc ecx
                        push ecx
                        CTA0 "%s \t%08Xh\t%08Xh   ( %s )\n", szFmtMod
                        invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber
                        pop ecx
                        invoke wsprintf, addr acThis, addr szFmtMod, ecx, \
                                         [esi][0*(sizeof DWORD)], \
                                         [edi][0*(sizeof DWORD)], addr acNumber
 
                        invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \
                                         [esi][1*(sizeof DWORD)], \
                                         [edi][1*(sizeof DWORD)], addr acNumber
 
                        invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \
                                         [esi][2*(sizeof DWORD)], \
                                         [edi][2*(sizeof DWORD)], addr acNumber
 
                        invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \
                                         [esi][3*(sizeof DWORD)], \
                                         [edi][3*(sizeof DWORD)], addr acNumber
 
                        invoke wsprintf, addr acBuffer,\
                            $CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \
                            addr acThis, addr acKernel, addr acUser, addr acAdvapi
 
                        assume esi:nothing
                        assume edi:nothing
                        invoke MessageBox,NULL,addr acBuffer,$CTA0("Modules Base Address"),\
                                           MB_OK + MB_ICONINFORMATION
                    .else
                        invoke MessageBox,NULL,$CTA0("Can't send control code to device."),\
                                    NULL,MB_OK + MB_ICONSTOP
                    .endif

如果DeviceIoControl函数成功返回,那么dwBytesReturned中的数值就等于驱动程序在adwOutBuffer缓冲区中返回的数据的长度,现在我们的任务就简单了,只要把返回值格式化一下并显示出来即可。我想后面的代码对读者来说应该是很好理解的。

图4.10 VirtToPhys.exe程序的输出

4.2.9 扫尾工作

invoke CloseHandle, hDevice

最后要做的是关闭设备句柄,这时I/O管理器向设备驱动程序发送两个IRP,第一个是IRP_MJ_CLEANUP,它告诉驱动程序设备句柄将要被关闭了;然后是IRP_MJ_CLOSE,它告诉驱动程序设备句柄已经被关闭了。你可以在收到IRP_MJ_CLEANUP时返回一个错误代码,这样就可以阻止设备句柄被关闭。驱动程序的子程序在处理这些IRP时,代码都是在发出I/O请求的线程环境中执行的(也就是调用CloseHandle的线程),它们的IRQL = PASSIVE_LEVEL。
在以后的教程中,我们继续讲解如何在驱动程序中处理IRP。
如果要让驱动在老版本的Windows NT中运行的话,读者需要将代码中的”\??”改成”\DosDevices”并重新编译,因为前面我已经说过,以前的Windows NT 4版本中”\??”目录的名称是”\DosDevices”。

(注:本文中主要讲述控制程序部分,驱动部分VirtToPhys.sys的全部源代码和结构解释详见下一节《全功能的驱动程序》)

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

You may also like

发表评论

电子邮件地址不会被公开。 必填项已用*标注