驱动开发学习笔记(3-7)–Four-F的驱动开发教程-系统内存堆

在这里下载本文的源代码

6. 系统内存堆

本篇翻译:songsong <http://www.songsong.org>
源码位置:KmdKit\examples\basic\MemoryWorks\SystemModules

首先是罗云彬的废话:感谢刘松一起参与这个翻译项目,这样本教程的中文翻译才能这么快和大家见面,刘松是温哥华的帅哥作家,著有《GRE Yellow Bible》(《GRE词汇黄宝书》),文风幽默,看他的翻译,使大家看枯燥的驱动教程如同看泡妞教程,于轻松间掌握繁琐的东西。原本这也是本人梦想中的写作风格,可惜本人多年努力,除了外貌长得还是一如既往的幽默外,文字中还是幽默不起来,没办法哟~~~,好了,废话少说,下面请刘松出场。
(稀稀落落的掌声)……
……
我是温哥华的松松,被罗大哥抓来翻译文章,几个MM还等着我去酒吧,只好不去了……
如果各位帅哥美女已经看完了前面的基础知识,那就跟小弟来看看”一些”必要的底层技术。
为什么”一些”要加引号(一女同学提问)?那是因为驱动程序可以做许许多多的事情啊!
如果你不懂MM,你就别混了!(一男同学问:驱动程序和泡妞还有关系?)哦,我这里的MM不是指”美眉”,是指”内存管理(Memory Management)”。
好的,我们现在就开始学习MM……
内存管理器给用户进程提供了大量的用于MM的API。这些API可以分为三类:虚拟内存函数、内存映射文件函数和堆函数。内核的成员(包括驱动程序)有很多高级的工具。例如:驱动程序能够在物理地址空间里分配一个连续的内存。这类函数呢,前缀是”Mm”。另外呢,还有一种以”Ex”为前缀的函数,用于从系统内存池里(分页和不分页的)分配和释放内存,还可以操作后备列表(lookaside lists)。
后备列表是啥东东?我们下一节会讲,它可以提供更快的内存分配,却要使用预定义的固定的块大小。

6.1 系统内存堆

系统内存堆可跟用户堆不一样啊,它表现为系统地址空间的两个所谓的内存池。

◎ 不分页池–不分页池不会分页到交换文件(swap file),自然也不需要分页回来。它们总是老老实实在物理内存里活动,在你想访问它们的时候总能找到它们(任何IRQL等级),并且不会出现分页错误(废话,不分页如果还出现分页错误,就太没有天理了。)这也正是它的优点啊,任何访问都不会出现页面错误!页面错误可是往往导致系统瘫掉的哦(当IRQL >= DISPATCH_LEVEL)!
◎ 分页池–顾名思义,就是可以分页(分入和分出)的了。你只能使用(IRQL < DISPATCH_LEVEL)的内存。

以上两种池都位于系统地址空间,在进程上下文中可以使用它们。有一个函数集合叫ExAllocatePoolXXX,用于从系统内存池分配内存。函数ExFreePool用来释放。
在我们开始使用它们的时候,来看看基本要点:
前面提到,如果你访问已经被交换出去的内存时IRQL >= DISPATCH_LEVEL会怎么样?(一黑人女同学回答:瘫痪!)对,系统瘫痪!
但是事情并不绝对,也许它当时不死机,过一会才死呢!反正它迟早会死!啥时候死?就是当你的系统将内存交换了出去,并且你访问它的时候!
千万不要太钟爱不分页内存,太浪费资源啊!它总要占用物理内存啊!
大家还记得c语言里,malloc()和free()吗?分配的堆是要释放的。谁做的事谁要负责吗!尤其是男人一定要责任的哦!
这里也一样,你在系统池里分配了内存,无论你的驱动程序发生了什么事情,这些内存不会被回收,除非ExFreePool 被调用啊。如果你不用ExFreePool显式地释放内存,即使你的驱动程序卸载了,这些内存还驻留在那里。所以呢,你就乖乖地显式地释放内存吧!
系统内存池分配的内存不会被自动清零,最后的使用者可能会留下垃圾。所以呢,使用之前,最好统统置零。
你可以很容易地定义你需要的内存类型了,就两种:分页,不分页。如果你的代码要访问IRQL >= DISPATCH_LEVEL,不用说你也知道,你必须使用不分页类型。代码本身,和涉及的数据都要在不分页内存里。在缺省情况下,驱动程序以不分页内存加载,除非是INIT节区或者名称以”PAGE”开始的节区。假如你不做任何动作去改变驱动程序的内存属性(例如:别去调用MmPageEntireDriver使驱动程序的映像分页),你就不用关心驱动程序了,它总是在内存里。
先前的文章中我们讨论了常用的驱动函数(DriverEntry, DriverUnload, DispatchXxx)被调用时所处的IRQL等级。
DDK给了我们关于每一个函数被调用时的IRQL等级的相关信息。例如:在后面的文章中我们会使用IoInitializeTimer函数,该函数的描述这样说的:该函数执行时,时钟事件发生时的等级IRQL = DISPATCH_LEVEL 。这就意味着:这个函数访问的所有内存都必须是不分页的。
如果你不能确定到底是哪个IRQL,你写程序时候可以这样调用KeGetCurrentIrql:

1
2
3
4
5
6
invoke KeGetCurrentIrql
.if eax &lt; DISPATCH_LEVEL
    ; use any memory
.else
    ; use nonpaged memory only
.endif

6.2 从系统内存池里分配

各位GGJJDDMM,感谢大家看到这里,坚持,坚持你就是高手了!
让我们来看一个简单驱动程序例子SystemModules,该例子的主要动作集中在DriverEntry函数里。我们会分配分页内存(你应该记得DriverEntry运行在IRQL =PASSIVE_LEVEL等级,所以使用分页内存自然是没问题了),然后写进一些信息,再释放,并让系统卸载驱动程序。
下面是长长的代码,不过很简单,别害怕哦!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
;@echo off
;goto make
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;  SystemModules - Allocate memory from the system pool and use it
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.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\native.inc
include \masm32\include\w2k\ntoskrnl.inc
includelib \masm32\lib\w2k\ntoskrnl.lib
include \masm32\Macros\Strings.mac
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              D I S C A R D A B L E   C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code INIT
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DriverEntry proc uses esi edi ebx pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
 
local cb:DWORD
local p:PVOID
local dwNumModules:DWORD
local pMessage:LPSTR
local buffer[256+40]:CHAR
 
    invoke DbgPrint, $CTA0("\nSystemModules: Entering DriverEntry\n")
    and cb, 0
    invoke ZwQuerySystemInformation, SystemModuleInformation, addr p, 0, addr cb
    .if cb != 0
        invoke ExAllocatePool, PagedPool, cb
        .if eax != NULL
            mov p, eax
            invoke DbgPrint,\
               $CTA0("SystemModules: %u bytes of paged memory allocted at address %08X\n"),cb,p
            invoke ZwQuerySystemInformation, SystemModuleInformation, p, cb, addr cb
            .if eax == STATUS_SUCCESS
                mov esi, p
 
                push dword ptr [esi]
                pop dwNumModules
 
                mov cb, (sizeof SYSTEM_MODULE_INFORMATION.ImageName + 100)*2
 
                invoke ExAllocatePool, PagedPool, cb
                .if eax != NULL
                    mov pMessage, eax
 
                    invoke DbgPrint,\
                      $CTA0("SystemModules: %u bytes of paged memory allocted at address %08X\n"), \
                           cb, pMessage
                    invoke memset, pMessage, 0, cb
                    add esi, sizeof DWORD
                    assume esi:ptr SYSTEM_MODULE_INFORMATION
                    xor ebx, ebx
                    .while ebx &lt; dwNumModules
                        lea edi, [esi].ImageName
                        movzx ecx, [esi].ModuleNameOffset
                        add edi, ecx
                        invoke _strnicmp, edi, $CTA0("ntoskrnl.exe", szNtoskrnl, 4), sizeof szNtoskrnl - 1
                        push eax
                        invoke _strnicmp, edi, $CTA0("ntice.sys", szNtIce, 4), sizeof szNtIce - 1
                        pop ecx
 
                        and eax, ecx
                        .if ZERO?
                            invoke _snprintf, addr buffer, sizeof buffer, \
                                    $CTA0("SystemModules: Found %s base: %08X size: %08X\n", 4), \
                                    edi, [esi].Base, [esi]._Size
                            invoke strcat, pMessage, addr buffer
                        .endif
 
                        add esi, sizeof SYSTEM_MODULE_INFORMATION
                        inc ebx
                    .endw
                    assume esi:nothing
                    mov eax, pMessage
                    .if byte ptr [eax] != 0
                        invoke DbgPrint, pMessage
                    .else
                        invoke DbgPrint, \
                               $CTA0("SystemModules: Found neither ntoskrnl nor ntice.\n")
                    .endif
                    invoke ExFreePool, pMessage
                    invoke DbgPrint, $CTA0("SystemModules: Memory at address %08X released\n"), pMessage
                .endif
            .endif
            invoke ExFreePool, p
            invoke DbgPrint, $CTA0("SystemModules: Memory at address %08X released\n"), p
        .endif
    .endif
 
    invoke DbgPrint, $CTA0("SystemModules: Leaving DriverEntry\n")
    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret
 
DriverEntry endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end DriverEntry
 
:make
set drv=SystemModules
\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
del %drv%.obj
echo.
Pause

为了写点有用的信息,我们加载了一些模块到系统地址空间(包括以下系统模块:ntoskrnl.exe, hal.dll等,还有设备驱动程序),然后去找ntoskrnl.exe和ntice.sys。我们用SystemModuleInformation信息类作为参数调用ZwQuerySystemInformation来得到系统模块列表。读者可以到Garry Nebbett的书《winodwsNT/2000 内部API参考》去找这些函数的描述。顺便说一下,ZwQuerySystemInformation 是一个独特的函数,它返回大量的系统信息。
这个例子中没有提供驱动控制程序。你可以使用KmdKit package中的KmdManager 或者类似的工具,还可以使用DebugView ( http://www.sysinternals.com ) 或 SoftICE 控制台来查看调试信息。
现在我们来分析分析这段程序吧……

and cb, 0
invoke ZwQuerySystemInformation, SystemModuleInformation, addr p, 0, addr cb

首先我们要决定我们要使用多少空间。上面调用对ZwQuerySystemInformation的调用使我们获得STATUS_INFO_LENGTH_MISMATCH(这是正常,因为buffer的尺寸为零。),但是cb变量接收到了buffer尺寸(为零或非零)。于是我们就得到了需要的buffer尺寸。这里需要地址p是为了ZwQuerySystemInformation函数的正常执行。

1
2
.if cb != 0
    invoke ExAllocatePool, PagedPool, cb

ExAllocatePool从分页内存池分配需要数量的内存。如果是不分页内存呢,就把第一个参数PagedPool相应地改成NonPagedPool。ExAllocatePool比用户模式的HeapAlloc 简单一些,只有两个参数:第一个参数是内存池类型(分页、不分页),第二个参数是需要的内存尺寸。简单吧!

.if eax != NULL

如果ExAllocatePool 返回非零值,那么它就是一个指向分配buffer的指针。
检查调试信息会发现ExAllocatePool 分配的buffer地址是页尺寸大小的倍数。假如请求的内存的大小大于或等于(>=)页尺寸(我们这个例子中,是明显地大了),分配的内存会从页边界开始分配。

mov p, eax
invoke ZwQuerySystemInformation, SystemModuleInformation, p, cb, addr cb

我们再次调用ZwQuerySystemInformation,这次使用buffer的指针和尺寸作为参数。

.if eax == STATUS_SUCCESS
mov esi, p

如果返回的是STATUS_SUCCESS,那么buffer之中就包含了系统模块列表,数据以SYSTEM_MODULE_INFORMATION(在include\w2k\native.inc中定义)结构队列的形式存在。

1
2
3
4
5
6
7
8
9
10
11
SYSTEM_MODULE_INFORMATION STRUCT        ;Information Class 11
    Reserved            DWORD   2 dup(?)
    Base                PVOID   ?
    _Size               DWORD   ?
    Flags               DWORD   ?
    Index               WORD    ?
    Unknown             WORD    ?
    LoadCount           WORD    ?
    ModuleNameOffset    WORD    ?
    ImageName           CHAR 256 dup(?)
SYSTEM_MODULE_INFORMATION ENDS

cb变量接受实际返回的字节的数量,但是我们目前用不到它。
我假设在两次调用ZwQuerySystemInformation 之间没有其它新模块出现。这种可能性当然是很小的。我们这里只是为了学习目的嘛!你最好使用更安全的办法:在循环中反复调用ZwQuerySystemInformation来逐次增加buffer大小,直到该大小满足需求!

push dword ptr [esi]
pop dwNumModules

buffer中的第一个双字(double word)包含模块的数量,这些模块紧跟在SYSTEM_MODULE_INFORMATION队列的后面。然后,模块的数量会保存在dwNumModules中。

1
2
3
4
mov cb, (sizeof SYSTEM_MODULE_INFORMATION.ImageName + 100)*2
invoke ExAllocatePool, PagedPool, cb
.if eax != NULL
       mov pMessage, eax

我们需要另一个buffer来保存我们寻找的两个模块的名字和其它信息。我们假定这个尺寸((sizeof SYSTEM_MODULE_INFORMATION.ImageName + 100)*2)是足够的。
注意:这次的buffer地址不是页尺寸的倍数,是因为buffer尺寸小于一个页尺寸。

invoke memset, pMessage, 0, cb

用memset填充buffer为零,这是为了安全考虑,使字符串肯定以零结束。学习过c语言的同学对此函数应该很熟悉。

add esi, sizeof DWORD
assume esi:ptr SYSTEM_MODULE_INFORMATION

esi跳过DWORD(保存了模块数量)的大小,现在esi就指向了第一个SYSTEM_MODULE_INFORMATION结构。

xor ebx, ebx
.while ebx < dwNumModules

我们对结构队列循环dwNumModules次数,来寻找ntoskrnl.exe 和 ntice.sys。
在多处理器的系统ntoskrnl.exe模块的名字应该是ntkrnlmp.exe,如果你使用的是带PAE(物理地址扩展)的系统,那么系统会分别地支持ntkrnlpa.exe 和 ntkrpamp.exe。我这里当然假定你不会拥有那么牛的机器了。

lea edi, [esi].ImageName
movzx ecx, [esi].ModuleNameOffset
add edi, ecx

ImageName and ModuleNameOffset 域分别包含了模块的全路径和路径内模块名的相对偏移。

invoke _strnicmp, edi, $CTA0(“ntoskrnl.exe”, szNtoskrnl, 4), sizeof szNtoskrnl – 1
push eax
invoke _strnicmp, edi, $CTA0(“ntice.sys”, szNtIce, 4), sizeof szNtIce – 1
pop ecx

strnicmp 做不区分大小写的两ANSI标准字符串比较。第三个参数是比较的字符的数量。这里也许不必要使用__strnicmp,因为SYSTEM_MODULE_INFORMATION里模块名是零结束的,使用_stricmp就可以了。这里使用__strnicmp是为了更加安全。
顺便提一句,ntoskrnl.exe提供了许多基本的字符串函数,如strcmp、strcpy和strlen等。

1
2
3
4
5
6
7
8
9
10
11
12
13
               and eax, ecx
               .if ZERO?
               invoke _snprintf, addr buffer, sizeof buffer, \
                      $CTA0("SystemModules: Found %s base: %08X size: %08X\n", 4), \
                                edi, [esi].Base, [esi]._Size
                      invoke strcat, pMessage, addr buffer
               .endif
 
               add esi, sizeof SYSTEM_MODULE_INFORMATION
               inc ebx
 
               .endw
               assume esi:nothing

假如上文提及的模块被找到,我们就用_snprintf(内核提供)函数格式化字符串,字符串中包含了模块名、基地址和尺寸,然后将字符串加到buffer去。例子中对标号szNtoskrnl 和 szNtIce使用了sizeof操作符,在使用宏的时候,你可以换一下标号和对齐参数的排列顺序,宏会自动检测到,你可以只使用标号或者对齐参数(细节参见macros\Strings.mac)。

1
2
3
4
5
6
7
                    mov eax, pMessage
                    .if byte ptr [eax] != 0
                        invoke DbgPrint, pMessage
                    .else
                        invoke DbgPrint, \
                               $CTA0("SystemModules: Found neither ntoskrnl nor ntice.\n")
                    .endif

这里很容易看懂,由于我们前面把字符串都置了零,这里很容易判断我们是否找到了什么东西。

1
2
3
4
5
6
                    invoke ExFreePool, pMessage
                .endif
            .endif
            invoke ExFreePool, p
        .endif
    .endif

释放在系统内存池里分配的内存。

mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
ret

返回失败代码,这样强制系统将驱动程序从内存中卸载。
现在你清楚了吧,使用系统内存池比使用用户模式的堆简单多了。唯一需要注意的问题就是正确地定义内存池类型。
用户模式下的ntdll.dll提供了许多ZwXxx系列函数,他们是进入内核模式的大门。注意:他们的参数的数量和含义都是一样的。你可以省不少事儿了吧!
由于内核的错误会导致系统瘫痪,所以你可以在用户模式下调试,然后小小地改动(如果需要)后拷贝到你的驱动程序。例如:ntdll.dll的ZwQuerySystemInformation 调用返回同样的信息。使用这个技巧你就不用总是重新启动你的机器了。
哈哈,这一节到这里终于结束了,我去找对门的白人MM聊会儿天去了……

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

You may also like

发表评论

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