Kmd教程3-最简单的设备驱动程序

来源:互联网 发布:android与java的区别 编辑:程序博客网 时间:2024/05/18 05:44

转自:http://www.gomb.cn/?uid-2-action-viewspace-itemid-1450

在这里下载本文的源代码

3. 最简单的设备驱动程序

※ 和本章内容相关的源代码见:
KmdKit/examples/simple/Beeper
KmdKit/examples/simple/DateTime

3.1 如何编译和链接内核模式驱动程序

    我总是把驱动程序的汇编源代码放到批处理文件中,这样的文件从内容上看是.asm和.bat文件的混合体,但是扩展名是.bat(注:读者在实际使用的时候是不是这样做完全可以根据个人喜好而定)

;@echo off;goto make.386                      ; driver's code start  ;::::::::::::::::::::::::::::::::  ; the rest of the driver's code ;  ;::::::::::::::::::::::::::::::::end DriverEntry            ; driver's code end:makeset drv=drvname/masm32/bin/ml /nologo /c /coff %drv%.bat/masm32/bin/link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.objdel %drv%.objecho.pause

    如果你运行这个"自编译"的批处理文件的话,系统会做以下的事情:
    对于汇编编译器来说,前面两行的命令是被注释掉的,所以会被忽略;但是对于命令解释器来说,这两行会被执行,因为命令解释器会忽略前面的分号。这样一来,执行会跳到最后面的:make标号处,接下来下面的编译和链接命令就会被执行,跳转语句和:make标号中间的汇编源代码会被忽略。另外,对于汇编编译器来说,所有end标号后面的内容都会被忽略,所以后面的:make及其后的内容会被编译器忽略。
    这种方式用起来很方便,因为源代码和如何编译、链接程序的信息都被放在一起了,如果需要的话,读者也可以自己另外加些命令上去,在我编写的所有的驱动代码中,我都使用了这种方法。
    下面我来解释一下:make后面的一些命令的含义。

set drv=drvname

    这里定义一个环境变量,具体使用的时候用驱动的文件名来代替,下面的链接选项的含义是:

◎ /driver--告诉链接器创建Windows NT内核模式驱动程序,这个选项造成的最重要的影响是文件中会多出一个"INIT"节区(有关PE文件中节区等概念可以参考《Windows环境下32位汇编语言程序设计》一书的第17章:PE文件),另外还有".idata"节区,里面包含了一些IMAGE_IMPORT_DESCRIPTOR结构,指出了需要导入的函数和模块的名称。"INIT"节区的属性被标志为可丢弃,这样装载程序获取了相关的导入信息后,这个节区的内容即被丢弃
◎ /base:0x10000--将驱动映像的基地址设置为10000h
◎ /align:32--系统内存是很宝贵的,所以最好使用更有效的节区对齐数值
◎ /out:%dvr%.sys--链接器默认会创建以exe作为扩展名的文件,当指定了/DLL选项后,创建的是.dll文件,在这里,我们要强制让它创建以.sys为扩展名的文件
◎ /subsystem:native--在PE文件头中,有一个字段用来告诉映像装载程序使用哪个子系统:Win32、POSIX或者OS/2。我们需要为驱动指定合适的环境,当链接.exe或者.dll文件的时候,一般指定为Win32子系统。内核模式驱动程序不需要任何子系统,所以我们使用的参数是native

3.2 最简单的内核模式驱动程序

3.2.1 源代码

    这里是一个最简单的内核模式驱动程序的源代码
;@echo off;goto make;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::; simplest - Simplest possible kernel-mode driver;;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.386.model flat, stdcalloption casemap:none;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                I N C L U D E   F I L E S;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::include /masm32/include/w2k/ntstatus.incinclude /masm32/include/w2k/ntddk.inc;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                         C O D E;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.code;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                       DriverEntry;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR    retDriverEntry endp;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::end DriverEntry;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                              B U I L D I N G   D R I V E R;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::makeset drv=simplest/masm32/bin/ml /nologo /c /coff %drv%.bat/masm32/bin/link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.objdel %drv%.objecho.pause

3.2.2 DriverEntry过程

    就像其他的可执行程序一样,每个驱动程序也有一个入口点,这是当驱动被装载到内存中时首先被调用的,驱动的入口点是DriverEntry过程(注:过程也就是子程序),DriverEntry这个名称只是一个标记而已,你可以把它命名为其他任何名字--只要它是入口点就行了。DriverEntry过程用来对驱动程序的一些数据结构进行初始化,它的函数原型定义如下:

DriverEntry proto DriverObject:PDRIVER_OBJECT, RegistryPath:PUNICODE_STRING

    不幸的是,Charles Simonyi创造的著名的"匈牙利表示法"并没有在DDK中使用,但是我还是在任何可能的地方使用,所以我为DriverObject和RegistryPath参数都加上了前缀。
    PDRIVER_OBJECT和PUNICODE_STRING的定义可以分别在/include/w2k/ntddk.inc和/include/w2k/ntdef.inc中找到。

PDRIVER_OBJECT typedef PTR DRIVER_OBJECT
PUNICODE_STRING typedef PTR UNICODE_STRING

    当I/O管理器调用DriverEntry过程的时候,它会传过来两个指针类型的参数,说明如下:

◎ pDriverObject--指向用于描述当前驱动的对象(所谓对象,在内存中也就表现为一个结构而已),这个对象刚被系统初始化。由于Windows NT是一个面向对象的操作系统,因此,驱动也是被作为一个对象来描述的,当驱动被装载到内存中的时候,系统会创建一个对象来描述这个驱动,对象在内存中的表示方式就是一个DRIVER_OBJECT结构(在/include/w2k/ntddk.inc中定义),pDriverObject参数指向这个对象,以便让驱动有存取它的机会,但我们现在还没必要用到它
◎ pusRegistryPath--指向一个定长的Unicode字符串,内容是驱动的注册表键的路径,前面的章节中我们已经讨论过了驱动的注册表键。驱动程序可以用它来获取或者保存一些要用到的信息。如果在以后的执行中还要用到这个字符串,驱动程序应该保留一份该Unicode字符串的拷贝而不是仅仅保存这个指针,因为指针指向的内存在DriverEntry过程返回后即被释放掉了

    定长的Unicode字符串是用UNICODE_STRING结构来表示的,和用户模式代码不同,内核模式的代码往往采用用UNICODE_STRING结构定义的字符串,该结构在/include/w2k/ntdef.inc中定义如下:
UNICODE_STRING STRUCT    _Length         WORD    ?    MaximumLength   WORD    ?    Buffer          PWSTR   ?UNICODE_STRING ENDS

    结构中的各字段含义如下:

◎ _Length--字符串的长度,以字节表示(而不是以字符数量表示),这个长度不包括末尾的0字符,由于Length是汇编的保留字,所以我不得不在前面加了一个下划线
◎ MaximumLength--字符串缓冲区的长度,也是以字节数表示
◎ Buffer--指向Unicode字符串,不要想当然地认为这个字符串就是以0结尾的,很多时候尾部并没有0

    这种结构的优点在于它清楚地表现出了字符串的当前长度和最大的可能长度,这样就允许对它进行一些运算(比如在后面加上一些字符等)。
    前面举例的驱动程序是最最简单的,它仅仅可以被装载而已,但是即使被装载,它除了返回一个STATUS_DEVICE_CONFIGURATION_ERROR错误代码(全部代码的列表可以见/include/w2k/ntstatus.inc文件)外什么都不干;在这里如果返回的是STATUS_SUCCESS,那么驱动会保留在内存中,但是你却无法卸载它,因为程序中缺少了负责卸载的DriverUnload过程。
    读者可以用KmdManager工具来注册以及装载任何驱动程序。

3.3 Beeper驱动程序

3.3.1 源代码

    现在来看看Beeper驱动程序的源代码,在"服务"一节中我们已经看到过它的控制程序了。
;@echo off;goto make;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;  beeper - Kernel Mode Drive;  Makes beep thorough computer speaker;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.386.model flat, stdcalloption casemap:none;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                              I N C L U D E   F I L E S;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::include /masm32/include/w2k/ntstatus.incinclude /masm32/include/w2k/ntddk.incinclude /masm32/include/w2k/hal.incincludelib /masm32/lib/w2k/hal.lib;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                     E Q U A T E S;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::TIMER_FREQUENCY        equ 1193167                   ; 1,193,167 HzOCTAVE                 equ 2                         ; octave multiplierPITCH_C                equ 523                       ; C        -  523,25 HzPITCH_Cs               equ 554                       ; C#       -  554,37 HzPITCH_D                equ 587                       ; D        -  587,33 HzPITCH_Ds               equ 622                       ; D#       -  622,25 HzPITCH_E                equ 659                       ; E        -  659,25 HzPITCH_F                equ 698                       ; F        -  698,46 HzPITCH_Fs               equ 740                       ; F#       -  739,99 HzPITCH_G                equ 784                       ; G        -  783,99 HzPITCH_Gs               equ 831                       ; G#       -  830,61 HzPITCH_A                equ 880                       ; A        -  880,00 HzPITCH_As               equ 988                       ; B        -  987,77 HzPITCH_H                equ 1047                      ; H        - 1046,50 Hz; We are going to play c-major chordTONE_1                 equ TIMER_FREQUENCY/(PITCH_C*OCTAVE)TONE_2                 equ TIMER_FREQUENCY/(PITCH_E*OCTAVE)TONE_3                 equ (PITCH_G*OCTAVE)           ; for HalMakeBeepDELAY                  equ 1800000h                   ; for my ~800mHz box;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                         M A C R O S;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::DO_DELAY MACRO    mov eax, DELAY    .while eax        dec eax    .endwENDM;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                            C O D E;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.code;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                            MakeBeep1;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::MakeBeep1 proc dwPitch:DWORD    ; Direct hardware access    cli    mov al, 10110110y    out 43h, al    mov eax, dwPitch    out 42h, al    mov al, ah    out 42h, al    ; Turn speaker ON    in al, 61h    or  al, 11y    out 61h, al    sti    DO_DELAY    cli    ; Turn speaker OFF    in al, 61h    and al, 11111100y    out 61h, al    sti    retMakeBeep1 endp;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                            MakeBeep2;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::MakeBeep2 proc dwPitch:DWORD    ; Hardware access using WRITE_PORT_UCHAR and READ_PORT_UCHAR    ; functions from hal.dll    cli    invoke WRITE_PORT_UCHAR, 43h, 10110110y    mov eax, dwPitch    invoke WRITE_PORT_UCHAR, 42h, al    mov eax, dwPitch    invoke WRITE_PORT_UCHAR, 42h, ah    ; Turn speaker ON    invoke READ_PORT_UCHAR, 61h    or  al, 11y    invoke WRITE_PORT_UCHAR, 61h, al    sti    DO_DELAY    cli    ; Turn speaker OFF    invoke READ_PORT_UCHAR, 61h    and al, 11111100y    invoke WRITE_PORT_UCHAR, 61h, al    sti    retMakeBeep2 endp;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                       DriverEntry;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING    invoke MakeBeep1, TONE_1    invoke MakeBeep2, TONE_2    ; Hardware access using hal.dll HalMakeBeep function    invoke HalMakeBeep, TONE_3    DO_DELAY    invoke HalMakeBeep, 0    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR    retDriverEntry endp;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::end DriverEntry:makeset drv=beeper/masm32/bin/ml /nologo /c /coff %drv%.bat/masm32/bin/link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.objdel %drv%.objecho.pause

    这个驱动程序会使用主板上的扬声器来播放C大调的几个音符,为了实现这个功能,程序使用了IN和OUT指令来访问I/O端口。众所周知的是,Windows NT把I/O端口当作重要的资源来保护,任何用户模式的程序如果使用了IN或者OUT指令来访问端口的话,会被Windows立马枪毙掉。但事实上还是有办法绕过这个限制使用户模式的程序直接访问I/O端口,我们马上会谈到这一点。

3.3.2 控制系统定时器

    计算机中有三个定时器,分别是定时器0、1和2,由可编程定时芯片(Programmable Interval Timer/PIT)实现,其中定时器2用于发声,发声的频率取决于定时器计数器的初始设置值,定时器会将计数值从初始值开始递减到0,然后将计数值复原为初始值,以此循环。计数值的递减由频率为1,193,180 Hz的系统振荡器控制着,该频率值在所有的PC家族的机器中是固定的。振荡器每产生一个脉冲,计数值就减一,为了发出不同频率的声音,我们只需要设定不同的初始值即可,发声频率和初始值的关系是:声音频率=1193180/初始值。关于这方面更进一步的知识,读者可以在网上搜到更多的内容。
    这里还有个微妙的区别,kernel32.dll中的QueryPerformanceFrequency函数的允许最大值等于1193180,但是hal.dll里面的HalMakeBeep函数使用的数值却是1193167,在这里我将使用这个数值,我不知道这是不是误差补偿的结果,但这并不妨碍我们的程序发出声音来。
    好!现在我们用MakeBeep1子程序来发第一个C大调音符。
    mov al, 10110110y    out 43h, al

    首先,我们要设置定时器的控制寄存器,也就是将2进制值的10110110送到43h端口。
    mov eax, dwPitch    out 42h, al    mov al, ah    out 42h, al

    然后,我们用两个连续的操作将初始值的低位字节和高位字节送到42h端口。
    in al, 61h    or  al, 11y    out 61h, al

    现在要将扬声器打开,这可以通过将61h端口的位0和位1设置为1来完成,不出意外的话,现在应该能够听到声音了。
DO_DELAY MACRO    mov eax, DELAY    .while eax        dec eax    .endwENDM

    为了让声音延续一段时间,我们用DO_DELAY宏来进行一些延时,虽然这种延时方法有点过时,但还是很有效的。
    in al, 61h    and al, 11111100y    out 61h, al

    现在可以关闭扬声器了,千万别忘了扬声器是整个系统的资源哦,这只要将端口61h的位0和位1清零就好了。在程序中我们用了cli指令清除中断允许标志来关闭中断,这在多处理器的机器上会对其他程序有所影响的。
    接下来,我们用MakeBeep2子程序来发第2个音符--C大调的mi音,实现上的区别在于这次使用hal.dll中的WRITE_PORT_UCHAR和READ_PORT_UCHAR函数来代替in/out指令。HAL隐藏了和硬件相关的细节,如操作I/O端口的方法等,这样代码就可以做到和机器无关。
    第3个音符--C大调的so音符--则是用hal.dll中的HalMakeBeep函数来实现的,这个函数的参数不是定时器的初始值,而是发声的频率。
    在Beeper.bat文件的前面部分,读者可以发现12个频率定义值,程序中只用到了3个,读者可以自行用剩余的定义去写一个合成器,要关闭扬声器的话,只要用参数0来再次调用HalMakeBeep就行了。
    Beeper驱动程序的DriverEntry过程返回一个错误值,所以系统直接就把它从系统中清除掉了。当然,在以后的全功能驱动程序里面,这里应该返回STATUS_SUCCESS值。

3.3.3 自动启动驱动程序

    scp.exe程序安装驱动程序的时候使用的是手动启动模式,前面我们也讨论过其他的启动方式,现在来试试让它随系统的启动而自动启动,有很多中办法可以实现这个功能,最简单的就是将源代码中调用DeleteService的一行去掉,然后将 SERVICE_DEMAND_START参数改为SERVICE_AUTO_START,并将SERVICE_ERROR_IGNORE参数改为 SERVICE_ERROR_NORMAL,再重新编译即可。
    这样,运行scp.exe并退出后,注册表信息还会保留着。你可以尽管将它忘掉,但是每次系统启动的时候,beeper.sys会不厌其烦地通知你一下它还活着,而且在系统日志中,你也可以找到它的启动错误记录,在控制面板->管理工具->事件查看器中,选择系统日志,双击日志,就可以看到下面的信息:


图3.1 系统错误日志

    最好别忘了将注册表中的对应键值去掉,否则在每次启动的时候你都会听到声音的。

3.4 Giveio驱动程序例子

3.4.1 Giveio驱动程序的控制代码

    现在来写另一个SCP代码,以便用来控制一个新的例子--giveio.sys驱动程序:
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;  DateTime - Service Control Program for giveio driver;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.386.model flat, stdcalloption  casemap:none;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                              I N C L U D E   F I L E S;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::include /masm32/include/windows.incinclude /masm32/include/kernel32.incinclude /masm32/include/user32.incinclude /masm32/include/advapi32.incincludelib /masm32/lib/kernel32.libincludelib /masm32/lib/user32.libincludelib /masm32/lib/advapi32.libinclude /masm32/Macros/Strings.mac;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                         M A C R O S;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::CMOS MACRO by:REQ    mov al, by    out 70h, al    in al, 71h    mov ah, al    shr al, 4    add al, '0'    and ah, 0Fh    add ah, '0'    stoswENDM;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                           C O D E;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.code;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                          DateTime;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::DateTime proc uses edilocal acDate[16]:CHARlocal acTime[16]:CHARlocal acOut[64]:CHAR    ; See Ralf Brown's Interrupt List for details    ;:::::::::::::::::: Set data format  ::::::::::::::::::    mov al, 0Bh               ; status register B    out 70h, al    in al, 71h    push eax                  ; save old data format    and al, 11111011y         ; Bit 2: Data Mode - 0: BCD, 1: Binary    or al, 010y               ; Bit 1: 24/12 hour selection - 1 enables 24 hour mode    out 71h, al    ;:::::::::::::::::::: Lets' fetch current date  ::::::::::::::::::::    lea edi, acDate    CMOS 07h                  ; date of month    mov al, '.'    stosb    CMOS 08h                  ; month    mov al, '.'    stosb    CMOS 32h                  ; two most significant digit od year    CMOS 09h                  ; two least significant digit od year    xor eax, eax              ; terminate string with zero    stosb    ;:::::::::::::::::::: Lets' fetch current time :::::::::::::::::::    lea edi, acTime    CMOS 04h                  ; hours    mov al, ':'    stosb    CMOS 02h                  ; minutes    mov al, ':'    stosb    CMOS 0h                   ; seconds    xor eax, eax              ; terminate string with zero    stosb    ;:::::::::::::: restore old data format :::::::::::::    mov al, 0Bh    out 70h, al    pop eax    out 71h, al    ;::::::::::::::::: Show current date and time :::::::::::::::    invoke wsprintf, addr acOut, $CTA0("Date:/t%s/nTime:/t%s"), addr acDate, addr acTime    invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK    retDateTime endp;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                         start;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: start proclocal fOK:BOOLlocal hSCManager:HANDLElocal hService:HANDLElocal acDriverPath[MAX_PATH]:CHARlocal hKey:HANDLElocal dwProcessId:DWORD    and fOK, 0        ; assume an error    ; Open the SCM database    invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE    .if eax != NULL        mov hSCManager, eax        push eax        invoke GetFullPathName, $CTA0("giveio.sys"),sizeof acDriverPath,addr acDriverPath, esp        pop eax        ; Register driver in SCM active database        invoke CreateService,hSCManager,$CTA0("giveio"),$CTA0("Current Date and Time fetcher."), /                SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, /                SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL        .if eax != NULL            mov hService, eax            invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, /                                    $CTA0("SYSTEM//CurrentControlSet//Services//giveio"),/                                    0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey            .if eax == ERROR_SUCCESS                ; Add current process ID into the registry                invoke GetCurrentProcessId                mov dwProcessId, eax                invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId),NULL,REG_DWORD, /                                        addr dwProcessId, sizeof DWORD                .if eax == ERROR_SUCCESS                    ; Start driver                    invoke StartService, hService, 0, NULL                    inc fOK                ; Set OK flag                    invoke RegDeleteValue, hKey, addr szProcessId                .else                    invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), /                                        NULL, MB_ICONSTOP                .endif                invoke RegCloseKey, hKey            .else                invoke MessageBox, NULL, $CTA0("Can't open registry."), NULL, MB_ICONSTOP            .endif            ; Remove driver from SCM database            invoke DeleteService, hService            invoke CloseServiceHandle, hService        .else            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP        .endif        invoke CloseServiceHandle, hSCManager    .else        invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), /                           NULL, MB_ICONSTOP    .endif    ; If OK display current date and time to the user    .if fOK        invoke DateTime    .endif    invoke ExitProcess, 0start endp;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::end start

3.4.2 使用注册表向驱动程序传递信息

    这个例子中只有一点点新东西:
            invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, /                                    $CTA0("SYSTEM//CurrentControlSet//Services//giveio"), /                                    0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey            .if eax == ERROR_SUCCESS                invoke GetCurrentProcessId                mov dwProcessId, eax                invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId), NULL, REG_DWORD, /                                        addr dwProcessId, sizeof DWORD                .if eax == ERROR_SUCCESS                    invoke StartService, hService, 0, NULL

    在启动驱动前,我们在该驱动的注册表键中创建了一个名为ProcessId的键值,里面的数据是当前进程的进程ID,也就是SCP程序自己的进程ID,请注意我在这里使用$CTA0宏的方法,我使用了标号szProcessId,用这个标号指定了一个字符串"ProcessId",我们在后面还要用到这个标号,看来我的字符串宏真是太灵活了哇哈哈哈哈~~~(俄国人也会得意忘形呀!?)
    一旦注册表值成功创建,我们就可以启动驱动程序了,这个额外的注册表键是干什么的呢?谜底后面就会揭晓!
                    inc fOK                    invoke RegDeleteValue, hKey, addr szProcessId                .else                    invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), /                                        NULL, MB_ICONSTOP                .endif                invoke RegCloseKey, hKey

    当StartService函数返回后,我们可以认为驱动程序已经圆满地完成了任务,然后就将fOK标志设上,接下来调用RegDeleteValue的一句是可有可无的,因为下面调用DeleteService函数后,驱动注册表键下的所有键值都会被清除掉,但是从编程风格方面来说,明确地写上删除语句是一个很好的习惯。
    .if fOK        invoke DateTime    .endif

    在关闭所有SCM的句柄后,如果fOK标志位已经设置的话,程序就调用DateTime子程序。

3.4.3 存取CMOS

    计算机主板上有块小芯片是用来存放系统配置信息的,如硬盘参数、内存配置以及系统时间等等,这块芯片一般被叫做"CMOS"(CMOS是Complementary Metal Oxide Semiconductor的缩写)。CMOS芯片由电池供电,内部建有一个实时时钟(RTC),我们可以通过存取70h和71h号I/O端口来获取其中的数据,"Ralf Brown's Interrupt List"里面有其详细的格式说明,网址见http://www-2.cs.cmu.edu/afs/cs/user/ralf/pub/WWW/files.html(注:http://asm.yeah.net上面有已经整理成单个hlp文件的全部上述资料)。
    mov al, 0Bh               ; status register B    out 70h, al    in al, 71h    push eax                  ; save old data format    and al, 11111011y         ; Bit 2: Data Mode - 0: BCD, 1: Binary    or al, 010y               ; Bit 1: 24/12 hour selection - 1 enables 24 hour mode    out 71h, al

    首先,我们通过状态寄存器B来设置一个便于使用的数据格式。使用CMOS宏可以从CMOS获取数据并同时将它的转换成我们需要的格式。

    invoke wsprintf, addr acOut, $CTA0("Date:/t%s/nTime:/t%s"), addr acDate, addr acTime
    invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK

    当获取到所有的相关数据后,程序的输出如下:


图3.2 DateTime.exe程序的输出结果

    这个程序中最奇怪的事情就是我们竟然可以直接访问CMOS而不被系统阻止,前面已经说过,如果用户模式程序在Windows NT操作系统下使用IN或者OUT指令来存取I/O端口的话会被系统终止,但是我们却可以,这怎么可能呢?这是因为我运行程序前刚喝了脑x金!广告里面天天说,脑x金无所不能!~~~~~~呵呵,开个玩笑,这当然是因为有了Giveio驱动程序。

3.5 Giveio设备驱动程序

3.5.1 Giveio驱动程序的源代码
;@echo off;goto make;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;  giveio - Kernel Mode Driver;  Demonstrate direct port I/O access from a user mode;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.386.model flat, stdcalloption casemap:none;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                              I N C L U D E   F I L E S;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::include /masm32/include/w2k/ntstatus.incinclude /masm32/include/w2k/ntddk.incinclude /masm32/include/w2k/ntoskrnl.incincludelib /masm32/lib/w2k/ntoskrnl.libinclude /masm32/Macros/Strings.mac;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                     E Q U A T E S;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::IOPM_SIZE equ 2000h     ; sizeof I/O permission map;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                        C O D E;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.code;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::;                                       DriverEntry;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRINGlocal status:NTSTATUSlocal oa:OBJECT_ATTRIBUTESlocal hKey:HANDLElocal kvpi:KEY_VALUE_PARTIAL_INFORMATIONlocal pIopm:PVOIDlocal pProcess:LPVOID    invoke DbgPrint, $CTA0("giveio: Entering DriverEntry")    mov status, STATUS_DEVICE_CONFIGURATION_ERROR    lea ecx, oa    InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL    invoke ZwOpenKey, addr hKey, KEY_READ, ecx    .if eax == STATUS_SUCCESS        push eax        invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), /                               KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp        pop ecx        .if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) && ( ecx != 0 )            invoke DbgPrint, $CTA0("giveio: Process ID: %X"), /                                dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [kvpi]).Data            ; Allocate a buffer for the I/O permission map            invoke MmAllocateNonCachedMemory, IOPM_SIZE            .if eax != NULL                mov pIopm, eax                lea ecx, kvpi                invoke PsLookupProcessByProcessId, /                        dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess                .if eax == STATUS_SUCCESS                    invoke DbgPrint, $CTA0("giveio: PTR KPROCESS: %08X"), pProcess                    invoke Ke386QueryIoAccessMap, 0, pIopm                    .if al != 0                        ; I/O access for 70h port                        mov ecx, pIopm                        add ecx, 70h / 8                        mov eax, [ecx]                        btr eax, 70h MOD 8                        mov [ecx], eax                       ; I/O access for 71h port                        mov ecx, pIopm                        add ecx, 71h / 8                        mov eax, [ecx]                        btr eax, 71h MOD 8                        mov [ecx], eax                        invoke Ke386SetIoAccessMap, 1, pIopm                        .if al != 0                            invoke Ke386IoSetAccessProcess, pProcess, 1                            .if al != 0                                invoke DbgPrint, $CTA0("giveio: I/O permission is successfully given")                            .else                                invoke DbgPrint, $CTA0("giveio: I/O permission is failed")                                mov status, STATUS_IO_PRIVILEGE_FAILED                            .endif                        .else                            mov status, STATUS_IO_PRIVILEGE_FAILED                        .endif                    .else                        mov status, STATUS_IO_PRIVILEGE_FAILED                    .endif                    invoke ObDereferenceObject, pProcess                .else                    mov status, STATUS_OBJECT_TYPE_MISMATCH                .endif                invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE            .else                invoke DbgPrint, $CTA0("giveio: Call to MmAllocateNonCachedMemory failed")                mov status, STATUS_INSUFFICIENT_RESOURCES            .endif        .endif        invoke ZwClose, hKey    .endif    invoke DbgPrint, $CTA0("giveio: Leaving DriverEntry")    mov eax, status    retDriverEntry endp;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::end DriverEntry:makeset drv=giveio/masm32/bin/ml /nologo /c /coff %drv%.bat/masm32/bin/link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.objdel %drv%.objecho.pause

    这个驱动的代码是基于Dale Roberts写的一个著名的例子(giveio)改编的,我决定使用它是因为它在这篇教程中使用真是太合适了!

3.5.2 I/O许可位图

    谜底揭晓了:我们的驱动程序修改了I/O许可位图(I/O permission bit map,IOPM),这样进程就被允许自由地存取I/O端口,这方面详细资料见http://www.intel.com/design/intarch/techinfo/pentium/PDF/inout.pdf
    每个进程都有自己的I/O许可位图,每个单独的I/O端口的访问权限都可以对每个进程进行单独授权,如果相关的位被设置的话,对对应端口的访问就是被禁止的,如果相关的位被清除,那么进程就可以访问对应的端口。既然I/O地址空间由64K个可单独寻址的8位I/O端口组成,IOPM表的最大尺寸就是2000h字节(注:每个端口的权限用1个bit表示,64K个端口除以8得到的就是IOPM的字节数,也就是65536/8=8192字节=2000h字节)。
    TSS的设计意图是为了在任务切换的时候保存处理器状态,从执行效率的考虑出发,Windows NT并没有使用这个特征,它只维护一个TSS供多个进程共享,这就意味着IOPM也是共享的,因此某个进程改变了IOPM的话,造成的影响是系统范围的。
    ntoskrnl.exe中有些未公开的函数是用来维护IOPM的,它们是Ke386QueryIoAccessMap和Ke386SetIoAccessMap函数。

Ke386QueryIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID

    Ke386QueryIoAccessMap函数从TSS中拷贝2000h字节的当前IOPM到指定的内存缓冲区中,缓冲区指针由pIopm参数指定。
    各参数描述如下:
◎ dwFlag--0表示将全部缓冲区用0FFh填写,也就是所有的位都被设置,所有的端口都被禁止访问;1表示从TSS中将当前IOPM拷贝到缓冲区中
◎ pIopm--用来接收当前IOPM的缓冲区指针,注意缓冲区的大小不能小于2000h字节

    如果函数执行成功的话会在al中返回非0值(不是eax);如果执行失败会在al(不是eax)中返回零。

Ke386SetIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID

    Ke386SetIoAccessMap函数刚好相反,它从pIopm参数指定的缓冲区中拷贝2000h字节的IOPM到TSS中去。
    各参数描述如下:
◎ dwFlag--这个参数只能是1,其他任何值函数都会返回失败
◎ pIopm--指向包含IOPM数据的缓冲区,缓冲区的尺寸不能小于2000h字节

    如果函数执行成功的话会在al中返回非0值(不是eax);如果执行失败会在al(不是eax)中返回零。
    当IOPM拷贝到TSS后,IOPM的偏移指针必须被定位到新的数据中去,这可以通过Ke386IoSetAccessProcess函数来完成,这也是ntoskrnl.exe中的一个很有用的未公开函数。

Ke386IoSetAccessProcess proto stdcall pProcess:PTR KPROCESS, dwFlag:DWORD

    Ke386IoSetAccessProcess允许或者禁止对进程使用IOPM。其参数说明如下:
◎ pProcess--指向KPROCESS结构(后面会对这个结构进行解释)
◎ dwFlag--0表示禁止对I/O端口进行存取,将IOPM的偏移指针指到TSS段外面;1表示允许存取I/O端口,将IOPM的偏移指针指到TSS段的88h中

    如果函数执行成功的话会在al中返回非0值(不是eax);如果执行失败会在al(不是eax)中返回零。
    顺便提一下,ntoskrnl中的所有函数都有前缀,通过这个前缀你就可以辨别该函数属于系统功能中的哪一类。不同的前缀表示不同的功能--如i前缀表示内部使用(internal)、p表示私有函数(private)、f表示fastcall。再如,Ke表示内核函数(kernel),Psp表示内部进程支持函数(internal process support),Mm表示内存管理函数(Memory Manager)等等。
    Ke386IoSetAccessProcess函数的第一个参数指向进程对象,也就是KPROCESS结构(在/include/w2k/w2kundoc.inc中定义),我特地在文件名中加了个w2k前缀是因为在不同的Windows NT版本中,未公开的数据结构的定义可能会有所变动,所以将使用这个include文件后编出的驱动程序在XP中使用可能不是个好主意。Ke386IoSetAccessProcess会将KPROCESS结构中IopmOffset字段的值设置为合适的值。

3.5.3 从注册表中读取信息

    在调用Ke386IoSetAccessProcess的时候要用到进程对象的指针,有好几种办法可以获得该指针,我选择了最简单的办法--使用进程ID,这就是DateTime.exe程序首先获取当前进程的ID并把它保存在注册表中的原因。在这里我们使用注册表在用户模式的代码以及内核模式的驱动程序之间传递参数。由于DriverEntry过程是在系统进程环境中运行的,所以不这样做的话,我们无法得知它究竟是被哪个进程所启动的。
    DriverEntry过程的第二个参数--pusRegistryPath是指向注册表键路径的字符串,我们可以用它来获取保存的进程ID。
    现在来看看这一切是怎么实现的:

    lea ecx, oa
    InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL

    在调用ZwOpenKey函数前,我们必须先初始化OBJECT_ATTRIBUTES结构(在/include/w2k/ntdef.inc中定义),我用InitializeObjectAttributes宏来完成这个功能,但读者以后最好手工来完成这个工作,因为InitializeObjectAttributes宏可能并不像你预料的那样运行(注:宏中间可能用到很多寄存器,但是有些寄存器的值可能是你需要保存的,过多的使用宏以后,寄存器在哪里被修改了你都不知道),你也可以这样做:

lea ecx, oa
xor eax, eax
assume ecx:ptr OBJECT_ATTRIBUTES
mov [ecx].dwLength, sizeof OBJECT_ATTRIBUTES
mov [ecx].RootDirectory, eax ; NULL
push pusRegistryPath
pop [ecx].ObjectName
mov [ecx].Attributes, eax ; 0
mov [ecx].SecurityDescriptor, eax ; NULL
mov [ecx].SecurityQualityOfService, eax ; NULL
assume ecx:nothing

    ZwOpenKey函数的第一个参数指向一个变量,用来返回注册表键的句柄;第二个参数是存取注册表的权限,你应该注意到ecx中保存有指向已经初始化的该注册表键的对象属性结构的指针。
invoke ZwOpenKey, addr hKey, KEY_READ, ecx    .if eax == STATUS_SUCCESS        push eax        invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), /                               KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp        pop ecx

    ZwQueryValueKey返回注册表键的句柄,我们可以用它从注册表中得到保存的进程ID,该函数的第二个参数是要获取数据的注册表键的名称,例子程序中我使用了$CCOUNTED_UNICODE_STRING宏来定义UNICODE_STRING结构和里面的字符串数据,如果你不喜欢使用宏,那么可以用下面的常规方法:

usz dw 'U', 'n', 'i', 'c', 'o', 'd', 'e', ' ', 's', 't', 'r', 'i', 'n', 'g', 0
us UNICODE_STRING {sizeof usz - 2, sizeof usz, offset usz}

    但是我可不喜欢这么麻烦,所以我写了下面这些宏:COUNTED_UNICODE_STRING、$COUNTED_UNICODE_STRING、CCOUNTED_UNICODE_STRING和$CCOUNTED_UNICODE_STRING(定义在/Macros/Strings.mac中)。
    第三个参数指明要获取的数据类型,KeyValuePartialInformation是一个常量(在/include/w2k/ntddk.inc中定义);第四和第五个参数是指向KEY_VALUE_PARTIAL_INFORMATION结构的指针以及结构的长度,在结构的Data字段中我们会得到我们需要的进程ID;最后一个参数是指向返回数据字节数的指针,把它指向堆栈的话,就不必重新再分配一个变量了。

(注:原文作者这里用的一个小技巧可以学习一下,前面一句push eax,然后后面调用的最后一个参数是esp,在这里压入堆栈的堆栈指针正是指向push eax指令保留的堆栈空间,所以函数执行的时候在这里返回一个值,后面一句pop ecx实际上是弹出了函数返回在里面的数值,千万不要认为弹出的原始的eax值,这种方法可以免去定义一个临时使用的变量的麻烦,当然限制条件就是该临时变量的指针参数必须是函数的最后一个参数才行。这个技巧在整个教程中多次用到,请注意理解!)

3.5.4 让用户模式的进程可以存取I/O端口
.if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) && ( ecx != 0 )            invoke MmAllocateNonCachedMemory, IOPM_SIZE            .if eax != NULL                mov pIopm, eax

    调用ZwQueryValueKey成功后,我们使用MmAllocateNonCachedMemory 函数来申请一段不被cache的虚拟内存地址空间,供IOPM数据使用。
                lea ecx, kvpi                invoke PsLookupProcessByProcessId, /                        dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess                .if eax == STATUS_SUCCESS                    invoke Ke386QueryIoAccessMap, 0, pIopm

    然后将进程ID传递给PsLookupProcessByProcessId函数,这样就可以在pProcess中得到指向进程对象的指针,后面的Ke386QueryIoAccessMap的函数将IOPM拷贝到缓冲区中。
                    .if al != 0                        mov ecx, pIopm                        add ecx, 70h / 8                        mov eax, [ecx]                        btr eax, 70h MOD 8                        mov [ecx], eax                        mov ecx, pIopm                        add ecx, 71h / 8                        mov eax, [ecx]                        btr eax, 71h MOD 8                        mov [ecx], eax                        invoke Ke386SetIoAccessMap, 1, pIopm                        .if al != 0                            invoke Ke386IoSetAccessProcess, pProcess, 1                            .if al != 0                            .else                                mov status, STATUS_IO_PRIVILEGE_FAILED                            .endif                        .else                            mov status, STATUS_IO_PRIVILEGE_FAILED                        .endif                    .else                        mov status, STATUS_IO_PRIVILEGE_FAILED                    .endif

    现在将70h和71h号端口对应的数据位清除,并使用Ke386IoSetAccessProcess函数将修改后的IOPM写回去,以便存取这两个端口。
                    invoke ObDereferenceObject, pProcess                .else                    mov status, STATUS_OBJECT_TYPE_MISMATCH                .endif

    先前对PsLookupProcessByProcessId函数的调用会使进程对象的引用计数加1,每次当一个对象被引用的时候,对象管理器将对象的引用计数加1,并返回对象的指针;当内核模式程序完成对一个对象的操作后,应该再次调用对象管理器将对象的引用计数减1。同样道理,每次对象的句柄被获取后,引用计数也会被加1,当对象句柄被关闭后,引用计数减1,这是因为句柄也是用来访问对象的。所以,即使是一个对象的所有句柄都被关闭,如果系统还要用到这个对象的话,那么它的引用计数可能还是正数。只有当对象的引用计数减少到0的时候,对象管理器才将对象从内存中删除。
    调用ObDereferenceObject函数可以将对象的引用计数减1,它的返回值是对象的先前状态。
                invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE            .else                invoke DbgPrint, $CTA0("giveio: Call to MmAllocateNonCachedMemory failed")                mov status, STATUS_INSUFFICIENT_RESOURCES            .endif        .endif        invoke ZwClose, hKey    .endif

    接下来调用MmFreeNonCachedMemory来释放内存,并调用ZwClose函数关闭注册表句柄。
    到这里为止,所有的工作都完成了,虽然驱动还是返回一个错误代码,系统还是会将它直接从内存中清除,但是用户模式的进程已经可以直接访问2个I/O端口了。
    在这个例子中我们举例访问了CMOS,通过同样的办法改造前面的beeper.sys例子,我们也可以在用户模式进程中让系统扬声器发声。但是要记住的是:你还是无法直接在用户模式进程中使用类似于cli或者sti之类的特权指令,也不能直接调用hal.dll之类的dll中的函数,因为它们是在内核地址空间中运行的。你唯一能做到的就是用这种技巧访问所有的65535个I/O端口而已。
invoke MmAllocateNonCachedMemory, IOPM_SIZE.if eax != NULL    mov pIopm, eax    invoke RtlZeroMemory, pIopm, IOPM_SIZE    lea ecx, kvpi    invoke PsLookupProcessByProcessId, /                dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess    .if eax == STATUS_SUCCESS        invoke Ke386SetIoAccessMap, 1, pIopm        .if al != 0            invoke Ke386IoSetAccessProcess, pProcess, 1        .endif        invoke ObDereferenceObject, pProcess    .endif    invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE.else    mov status, STATUS_INSUFFICIENT_RESOURCES.endif

    还要注意的是:访问系统扬声器或者访问CMOS都是很安全的,但是存取其他的I/O端口可能是很危险的,因为在用户模式下,你无法保证和其他进程的访问保持同步状态。

3.6 关于驱动程序的调试

    现在可以更详细地讨论一下驱动的调试了,前面已经提到过,我们最好用SoftICE来调试驱动程序。
    为了产生一个断点,读者可以在驱动代码的某处直接放上一个"Int 3"指令,"Int 3"会产生一个软件断点,这样就可以被SoftICE一类的内核调试器捕捉到。在此之前,要确定INT 3的捕捉是被打开的。在SoftICE的命令控制窗口中打I3HERE命令可以做到这一点,更详细的说明读者可以参考SoftICE的命令手册。要注意的是:如果没有调试器来捕捉断点的话,断点指令引起的结果就是一个BSOD蓝屏死机画面,所以在启动驱动前不要忘了先敲"i3here on"命令。在最新的SoftICE版本中,内核模式地址空间中的int 3捕捉默认就是打开的。
    程序中还不时地调用了DbgPrint函数,这个函数会在调试器窗口中显示一个字符串,SoftICE可以很好地与之配合,你也可以用Mark Russinovich(www.sysinternals.com)写的DebugView软件来监视调试信息的输出。
原创粉丝点击