2009年01月31日

现在很多文章都将Android与Windows相提并论,我个人觉得,Android只能称为是又一个Linux发行版本,根本不能称为OS,它和Ubuntu, Fedora是一个级别的,而和Windows不是一个层次的概念。

不可否认,Google非常善于创造概念!

最近Intel也开测Moblin 2,同样,Moblin 2也只不过是Linux的又一个裁剪版本,针对ATOM处理器,得知AMD停止Geode的进一步研发,让我很是伤心。或许,AMD现在的确应该更专注一些。

2006年01月16日

使你的代码既能在32位windows也能在64位windows环境下运行是极度向往的。你需要尊从一些简单的规则,例如转换指针,以及在你的代码中使用新的数据类型。关于指针的维护规则如下所示。

1、不要将指针强制转换为int, long, ULONG或DWORD
如果你必须将指针强制转换以测试一些位,设置或清除一些位,或者维护它的内容,请使用UINT_PTR或INT_PTR类型。这些类型是内部类型,已经将指针的尺寸缩放到32和64位windows(例如, ULONG被用于32位windows,而_int64被用于64位Windows)。例如,假设你是这样写代码的:

ImageBase = (PVOID)((ULONG)ImageBase | 1);

作为对指针的处理,你要将代码改成如下所示:

ImageBase = (PVOID)((ULONG_PTR)ImageBase | 1);

合适的时候(如果你并不能确定到底哪儿需要它们,使用了也没什么坏处),要使用UINT_PTR和INT_PTR。不要将你的指针转换为ULONG,LONG, INT, UINT或DWORD

注意HANDLE被定义为void*, 那么将该类型转换为ULONG值,来测试、设置或清除低序2个位的时候,在64位Windows上是错误的。

2、使用PtrToLong和PtrToUlong来转换指针

3、小心使用OUT参数

4、

2005年12月20日

    有网友在论坛上发贴,要求我谈谈ReactOS是怎样实现系统调用的。另一方面,我上次已经谈到兼容内核应该如何实现Windows系统调用的问题,接着谈谈ReactOS怎样实现系统调用倒也顺理成章,所以这一次就来谈谈这个话题。不过这显然不属于“漫谈Wine”的范畴,也确实没有必要再来个“漫谈ReactOS”,因此决定把除Wine以外的话题都纳入“漫谈兼容内核”。
    ReactOS这个项目的目标是要开发出一个开源的Windows。不言而喻,它要实现的系统调用就是Windows的那一套系统调用,也就是要忠实地实现Windows系统调用界面。本文要说的不是Windows系统调用界面本身,而是ReactOS怎样实现这个界面,主要是说说用户空间的应用程序怎样进入/退出内核、即系统空间,怎样调用定义于这个界面的函数。实际上,ReactOS正是通过“int 0×2e”指令进入内核、实现系统调用的。虽然ReactOS并不是Windows,它的作者们也未必看到过Windows的源代码;但是我相信,ReactOS的代码、至少是这方面的代码,与“正本”Windows的代码应该非常接近,要有也只是细节上的差别。
    下面以系统调用NtReadFile()为例,按“自顶向下”的方式,一方面说明怎样阅读ReactOS的代码,一方面说明ReacOS是怎样实现系统调用的。

    首先,Windows应用程序应该通过Win32 API调用这个接口所定义的库函数,这些库函数基本上都是在“动态连接库”、即DLL中实现的。例如,ReadFile()就是在Win32 API中定义的一个库函数。实现这个库函数的可执行程序在Windows的“系统DLL”之一kernel32.dll中,有兴趣的读者可以在Windows上用一个工具depends.exe打开kernel32.dll,就可以看到这个DLL的导出函数表中有ReadFile()。另一方面,在微软的VC开发环境(Visual Studio)中、以及Win2k DDK中,都有个“头文件”winbase.h,里面有ReadFile()的接口定义:

WINBASEAPI
BOOL
WINAPI
ReadFile(
    IN HANDLE hFile,
    OUT LPVOID lpBuffer,
    IN DWORD nNumberOfBytesToRead,
    OUT LPDWORD lpNumberOfBytesRead,
    IN LPOVERLAPPED lpOverlapped
    );

    函数名前面的关键词WINAPI表示这是个定义于Win32 API的函数。
    在ReactOS的代码中同样也有winbase.h,这在目录reactos/w32api/include中:

[code]
BOOL WINAPI ReadFile(HANDLE, PVOID, DWORD, PDWORD, LPOVERLAPPED);

[/code]

    显然,这二者实际上是相同的(要不然就不兼容了)。当然,微软没有公开这个函数的代码,但是ReactOS为之提供了一个开源的实现,其代码在reactos/lib/kernel32/file/rw.c中。

BOOL STDCALL
ReadFile( HANDLE hFile,  LPVOID lpBuffer,  DWORD nNumberOfBytesToRead,
         LPDWORD lpNumberOfBytesRead,  LPOVERLAPPED lpOverLapped )
{
   ……

   errCode = NtReadFile(hFile,
       hEvent,
       NULL,
       NULL,
       IoStatusBlock,
       lpBuffer,
       nNumberOfBytesToRead,
       ptrOffset,
       NULL);

   ……
   return(TRUE);
}

我们在这里只关心NtReadFile(),所以略去了别的代码。
    如前所述,NtReadFile()是Windows的一个系统调用,内核中有个函数就叫NtReadFile(),它的实现在ntoskrnl.exe中(这是Windows内核的核心部分),这也可以用depends.exe打开ntoskrnl.exe察看。ReactOS代码中对内核函数NtReadFile()的定义在reactos/include/ntos/zw.h中,同样的定义也出现在reactos/w32api/include/ddk/winddk.h中:

NTSTATUS
STDCALL
NtReadFile(
   IN HANDLE FileHandle,
   IN HANDLE Event OPTIONAL,
   IN PIO_APC_ROUTINE UserApcRoutine OPTIONAL,
   IN PVOID UserApcContext OPTIONAL,
   OUT PIO_STATUS_BLOCK IoStatusBlock,
   OUT PVOID Buffer,
   IN ULONG BufferLength,
   IN PLARGE_INTEGER ByteOffset OPTIONAL,
   IN PULONG Key OPTIONAL
   );

而相应的实现则在reactos/ntoskrnl/io/rw.c中。
    表面上看这似乎挺正常,ReadFile()调用NtReadFile(),reactos/ntoskrnl/io/rw.c则为其提供了被调用的NtReadFile()。可是仔细一想就不对了。这ReadFile()是在用户空间运行的,而reactos/ntoskrnl/io/rw.c中的代码却是在内核中,是在系统空间。难道用户空间的程序竟能如此这般地直接调用内核中的函数吗?如果那样的话,那还要什么陷阱门、调用门这些机制呢?再说,编译的时候又怎样把它们连接起来呢?
    这么一想,就可以断定这里面另有奥妙。仔细一查,原来还另有一个NtReadFile(),在msvc6/iface/native/syscall/Debug/zw.c中:

__declspec(naked) __stdcall
NtReadFile(int dummy0, int dummy1, int dummy2)
{
    __asm {
    push ebp
    mov ebp, esp
    mov eax,152
    lea edx, 8[ebp]
    int 0×2E
    pop ebp
    ret 9
    }
}

原来,用户空间也有一个NtReadFile(),正是这个函数在执行自陷指令“int 0×2e”。我们看一下这段汇编代码。这里面的152就是NtReadFile()这个系统调用的调用号,所以当CPU自陷进入系统空间后寄存器eax持有具体的系统调用号。而寄存器edx,在执行了lea这条指令以后,则持有CPU在调用这个函数前夕的堆栈指针,实际上就是指向堆栈中调用参数的起点。在进行系统调用时如何传递参数这个问题上,Windows和Linux有着明显的差别。我们知道,Linux是通过寄存器传递参数的,好处是效率比较高,但是参数的个数受到了限制,所以Linux系统调用的参数都很少,真有大量参数需要传递时就把它们组装在数据结构中,而只传递数据结构指针。而Windows则通过堆栈传递参数。读者在上面看到,ReadFile()在调用NtReadFile()时有9个参数,这9个参数都被压入堆栈,而edx就指向堆栈中的这些参数的起点(地址最低处)。我们在这个函数中没有看到对通过堆栈传下来的参数有什么操作,也没有看到往堆栈里增加别的参数,所以传下来的9个参数被原封不动地传了下去(作为int 0×2e自陷的参数)。这样,当CPU自陷进入内核以后,edx仍指向用户空间堆栈中的这些参数。当然,CPU进入内核以后的堆栈是系统空间堆栈,而不是用户空间堆栈,所以需要用copy_from_user()一类的函数把这些参数从用户空间拷贝过来,此时edx的值就可用作源指针。至于寄存器ebp,则用作调用这个函数时的“堆栈框架”指针。
    当内核完成了具体系统调用的操作,CPU返回到用户空间时,下一条指令是“pop ebp”,即恢复上一层函数的堆栈框架指针。然后,指令“ret 9”使CPU返回到上一层函数,同时调整堆栈指针,使其跳过堆栈上的9个调用参数。在“正宗”的x86汇编语言中,用在ret指令中的数值以字节为单位,所以应该是“ret 24h”,而这里却是以4字节长字为单位,这显然是因为用了不同的汇编工具。
    子程序的调用者可以把参数压入堆栈,通过堆栈把参数传递给被调用者。可是,当CPU从子程序返回时,由谁负责从堆栈中清除这些参数呢?显然,要么就是由调用者负责,要么就是由被调用者负责,这里需要有个约定,使得调用者和被调用者取得一致。在上面NtReadFile()这个函数中,我们看到是由被调用者负起了这个责任、在调整堆栈指针。函数代码前面的__stdcall就说明了这一点。同样,在.h文件中对NtReadFile()的定义(申明)之前也加上了STDCALL,也是为了说明这个约定。“Undocumented Windows 2000 Secrets”这本书中(p51-53)对类似的各种约定有一大段说明,读者可以参考。另一方面,在上面这个函数的代码中,函数的调用参数是3个而不是9个。但是看一下代码就可以知道这些参数根本就没有被用到,而调用者、即前面的ReadFile()、也是按9个参数来调用NtReadFile()的。所以,这里的三个参数完全是虚设的,有没有、或者有几个、都无关紧要,难怪代码中称之为“dummy”。
    用户空间的这个NtReadFile()向上代表着内核函数NtReadFile(),向下则代表着想要调用内核函数NtReadFile()的那个函数,在这里是ReadFile();但是它本身并不提供什么附加的功能,这样的中间函数称为“stub”。
    当然,ReactOS的这种做法很容易把读者引入迷茫。相比之下,Linux的做法就比较清晰,例如应用程序调用的是库函数write(),而内核中与之对应的函数则是sys_write()。
    那么为什么ReactOS要这么干呢?我只能猜测:
    (1).Windows的源代码中就是这样,例如用depends.exe在ntdll.dll和ntoskrnl.exe中都可看到有名为NtReadFile()的函数,而ReactOS的人就依葫芦画瓢。
    (2).作为一条开发路线,ReactOS可能在初期不划分用户空间和系统空间,所有的代码全在同一个空间运行,所以应用程序可以直接调用内核中的函数。这样,例如对文件系统的开发就可以简单易行一些。然后,到一些主要的功能都开发出来以后,再来划分用户空间和系统空间,并且补上如何跨越空间这一层。从zw.c这个文件在native/syscall/Debug目录下这个迹象看,ReactOS似乎正处于走出这一步的过程中。
    (3).ReactOS的作者们可能有意让它也可以用于嵌入式系统。嵌入式系统往往不划分用户空间和系统空间,而把应用程序和内核连接在同一个可执行映像中。这样,如果需要把代码编译成一个嵌入式系统,就不使用stub;而若要把代码编译成一个桌面系统,则可以在用户空间加上stub并在内核中加上处理自陷指令“int 0×2e”的程序。

    在Windows中,stub函数NtReadFile()在ntdll.dll中。实际上,所有0×2e系统调用的stub函数都在这个DLL中。显然,所有系统调用的stub函数具有相同的样式,不同的只是系统调用号和参数的个数,所以ReactOS用一个工具来自动生成这些stub函数。这个工具的代码在msvc6/iface/native/genntdll.c中,下面是一个片断:

void write_syscall_stub(FILE* out, FILE* out3, char* name, char* name2,
   char* nr_args, unsigned int sys_call_idx)
{
int i;
int nArgBytes = atoi(nr_args);

#ifdef PARAMETERIZED_LIBS
  ……
#else
  fprintf(\"\\n\\t.global _%s\\n\\t\"\n",name);
  fprintf(out,"\".global _%s\\n\\t\"\n",name2);
  fprintf(out,"\"_%s:\\n\\t\"\n",name);
  fprintf(out,"\"_%s:\\n\\t\"\n",name2);
#endif
  fprintf(out,"\t\"pushl\t%%ebp\\n\\t\"\n");
  fprintf(out,"\t\"movl\t%%esp, %%ebp\\n\\t\"\n");
  fprintf(out,"\t\"mov\t$%d,%%eax\\n\\t\"\n",sys_call_idx);
  fprintf(out,"\t\"lea\t8(%%ebp),%%edx\\n\\t\"\n");
  fprintf(out,"\t\"int\t$0×2E\\n\\t\"\n");
  fprintf(out,"\t\"popl\t%%ebp\\n\\t\"\n");
  fprintf(out,"\t\"ret\t$%s\\n\\t\");\n\n",nr_args);
  ……
}

代码中的’\t’表示TAB字符,读者阅读这段代码应该没有什么问题。这段代码根据name、nr_args、sys_call_idx等参数为给定系统调用生成stub函数的汇编代码。那么这些参数从何而来呢?在ReactOS代码的reactos/tools/nci目录下有个文件sysfuncs.lst,下面是从这个文件中摘出来的几行:

NtAcceptConnectPort 6
NtAccessCheck 8
NtAccessCheckAndAuditAlarm 11
NtAddAtom 3
……
NtClose 1
……
NtReadFile 9
……

这里的NtAcceptConnectPort就是调用号为0的系统调用NtAcceptConnectPort(),它有6个参数。另一个系统调用NtClose()只有1个参数。而NtReadFile()有9个参数,并且正好是这个表中的第153行,所以调用号是152。

    用户空间的程序一执行int 0×2e,CPU就自陷进入了系统空间。其间的物理过程这里就不多说了,有需要的读者可参考“情景分析”或其它有关资料。我这里就从CPU怎样进入int 0×2e的自陷处理程序说起。
    像别的中断向量一样,ReactOS在其初始化程序KeInitExceptions()中设置了int 0×2e的向量,这个函数的代码在reactos/ntoskrnl/ke/i386/exp.c中:

VOID INIT_FUNCTION
KeInitExceptions(VOID)
/*
* FUNCTION: Initalize CPU exception handling
*/
{
   ……
   set_trap_gate(0, (ULONG)KiTrap0, 0);
   set_trap_gate(1, (ULONG)KiTrap1, 0);
   set_trap_gate(2, (ULONG)KiTrap2, 0);
   set_trap_gate(3, (ULONG)KiTrap3, 3);
   ……
   set_system_call_gate(0×2d,(int)interrupt_handler2d);
   set_system_call_gate(0×2e,(int)KiSystemService);
}

显然,int 0×2e的向量指向KiSystemService()。
    ReactOS在其内核函数的命名和定义上也力求与Windows一致,所以ReactOS内核中也有前缀为ke和ki的函数。前缀ke表示属于“内核”模块。注意Windows所谓的“内核(kernel)”模块只是内核的一部分,而不是整个内核,这一点我以后在“漫谈Wine”中还要讲到。而前缀ki,则是指内核中与中断响应和处理有关的函数。KiSystemService()是一段汇编程序,其作用相当于Linux内核中的system_call(),这段代码在reactos/ntoskrnl/ke/i386/syscall.S中。限于篇幅,我在这篇短文中就不详细讲解这个函数的全部代码了,而只是分段对一些要紧的关节作些说明。一般而言,能读懂Linux内核中system_call()那段代码的读者应该能至少大体上读懂这个函数。

_KiSystemService:

    /*
     * Construct a trap frame on the stack.
     * The following are already on the stack.
     */
    // SS                                                  + 0×0
    // ESP                                                 + 0×4
    // EFLAGS                                              + 0×8
    // CS                                                  + 0xC
    // EIP                                                 + 0×10
    pushl $0                                               // + 0×14
    pushl %ebp                                             // + 0×18
    pushl %ebx                                             // + 0×1C
    pushl %esi                                             // + 0×20
    pushl %edi                                             // + 0×24
    pushl %fs                                              // + 0×28
   
    /* Load PCR Selector into fs */
    movw $PCR_SELECTOR, %bx
    movw %bx, %fs
   
    /* Save the previous exception list */
    pushl %fs:KPCR_EXCEPTION_LIST                          // + 0×2C

    /* Set the exception handler chain terminator */
    movl $0xffffffff, %fs:KPCR_EXCEPTION_LIST

    /* Get a pointer to the current thread */
    movl %fs:KPCR_CURRENT_THREAD, %esi

前面的一些指令主要是在保存现场,类似于Linux内核中的宏操作SAVE_ALL。这里关键的一步是从%fs:KPCR_CURRENT_THREAD这个地址取得当前线程的指针并将其存放在寄存器%esi中。每个线程在内核中都有个KTHREAD数据结构,某种意义上相当于Linux内核中的“进程控制块”、即task_struct。Windows内核中也有“进程控制块”,但只是相当于把进程内各线程所共享的信息剥离了出来,而“线程控制块”则起着更重要的作用。所谓当前线程的指针,就是指向当前线程的KTHREAD数据结构的指针。当内核调度一个线程运行时,就将其KTHREAD数据结构的地址存放在%fs:KPCR_CURRENT_THREAD这个地址中,而(CPU在系统空间的)%fs的值则又固定存放在PCR_SELECTOR这个地址中(定义为0×30)。附带提一下,Win2k内核把%fs:0映射到线性地址0xffdff000(见“Secrets”一书p428)。
    总之,从现在起,寄存器%esi就指向了当前线程的KTHREAD数据结构。那么这一步对于系统调用为什么重要呢?我们看一下这个数据结构中的几个成分就可以明白:

typedef struct _KTHREAD
{
   /* For waiting on thread exit */
   DISPATCHER_HEADER    DispatcherHeader;      /* 00 */
   ……
   SSDT_ENTRY            *ServiceTable;        /* DC */
   ……
   UCHAR                  PreviousMode;        /* 137 */
   ……
} KTHREAD;

每个成分后面的注释说明这个成分在数据结构中以字节为单位的相对位移,例如指针ServiceTable的相对位移就是0xdc。事实上,这个指针正是我们此刻最为关注的,因为它直接与系统调用的函数跳转表有关。每个线程的这个指针都指向一个SSDT_ENTRY结构数组。既然每个线程都有这么个指针,就说明每个线程都可以有自己的ServiceTable。不过,实际上每个线程的ServiceTable通常都指向同一个结构数组,我们等一下再来看这个结构数组,现在先往下看代码。

    /* Save the old previous mode */
    pushl %ss:KTHREAD_PREVIOUS_MODE(%esi)                  // + 0×30
   
    /* Set the new previous mode based on the saved CS selector */
    movl 0×24(%esp), %ebx
    andl $1, %ebx
    movb %bl, %ss:KTHREAD_PREVIOUS_MODE(%esi)

    /* Save other registers */
    pushl %eax                                             // + 0×34
    pushl %ecx                                             // + 0×38
    pushl %edx                                             // + 0×3C
    pushl %ds                                              // + 0×40
    pushl %es                                              // + 0×44
    pushl %gs                                              // + 0×48
    sub $0×28, %esp                                        // + 0×70

#ifdef DBG
    ……
#else
    pushl 0×60(%esp) /* DebugEIP */                         // + 0×74
#endif
    pushl %ebp       /* DebugEBP */                         // + 0×78

    /* Load the segment registers */
    sti
    movw $KERNEL_DS, %bx
    movw %bx, %ds
    movw %bx, %es

    /* Save the old trap frame pointer where EDX would be saved */
    movl KTHREAD_TRAP_FRAME(%esi), %ebx
    movl %ebx, KTRAP_FRAME_EDX(%esp)

    /* Allocate new Kernel stack frame */
    movl %esp,%ebp

    /* Save a pointer to the trap frame in the TCB */
    movl %ebp, KTHREAD_TRAP_FRAME(%esi)

CheckValidCall:
   
#ifdef DBG
    ……
#endif
    /*
     * Find out which table offset to use. Converts 0×1124 into 0×10.
     * The offset is related to the Table Index as such: Offset = TableIndex x 10
     */
    movl %eax, %edi
    shrl $8, %edi
    andl $0×10, %edi
    movl %edi, %ecx
      
    /* Now add the thread’s base system table to the offset */
    addl KTHREAD_SERVICE_TABLE(%esi), %edi
  

这里我们关注的是最后这一小段。首先,KTHREAD_SERVICE_TABLE(%esi)就是当前线程的ServiceTable指针。常数KTHREAD_SERVICE_TABLE定义为0xdc:

#define KTHREAD_SERVICE_TABLE     0xDC

这跟前面KTHREAD数据结构的定义显然是一致的。
    上面讲过,实际上一般情况下所有线程的ServiceTable指针都指向同一个结构数组,那就是KeServiceDescriptorTable[ ]:

SSDT_ENTRY
__declspec(dllexport)
KeServiceDescriptorTable[SSDT_MAX_ENTRIES] = {
    { MainSSDT,  NULL,  NUMBER_OF_SYSCALLS,  MainSSPT },
    { NULL,     NULL,   0,   NULL   },
    { NULL,     NULL,   0,   NULL   },
    { NULL,     NULL,   0,   NULL   }
};

这个数组的大小一般是4,但是只用了前两个元素。这里只用了第一个元素,这就是常规Windows系统调用的跳转表。
    我以前曾经谈到,Windows在发展的过程中把许多原来实现于用户空间的功能(主要是图形界面操作)移到了内核中,成为一个内核模块win32k.sys,并相应地增加了一组“扩充系统调用”。这个数组的第二个元素就是为扩充系统调用准备的,但是在源代码中这个元素是空的,这是因为win32k.sys可以动态安装,安装了以后才把具体的数据结构指针填写进去。扩充系统调用与常规系统调用的区别是:前者的系统调用号均大于等于0×1000,而后者则小于0×1000。显然,内核需要根据具体的系统调用号来确定应该使用哪一个跳转表,或者说上述数组内的哪一个元素。每个元素的大小是16个字节,所以只要根据具体的系统调用号算出一个相对位移量,就起到了选择使用跳转表的作用。具体地,如果算得的位移量是0,那就是使用常规跳转表,而若是0×10就是使用扩充跳转表。
    上面的代码中正是这样做的。把系统调用号的副本(在%edi中)右移8位,再跟0×10相与,就起到了这个效果。于是,指令“addl KTHREAD_SERVICE_TABLE(%esi), %edi”就使寄存器%edi指向了应该使用的跳转表结构,即SSDT_ENTRY数据结构。代码的作者加了个注释,说是“把0×1124转换成0×10”,其意思实际上是:“如果系统调用号是0×1124,那么计算出来的相对位移是0×10”;后面一句说的是“相对位移 = 数组下标乘上0×10”。
    SSDT_ENTRY数据结构中的第三个成分,即相对位移为8之处是个整数,说明在函数跳转表中有几个指针,也即所允许的最大系统调用号。对于常规系统调用,这个整数是NUMBER_OF_SYSCALLS,在ReactOS的代码中定义为232,比Win2K略少一些。
    我们继续往下看代码:

    /* Get the true syscall ID and check it */
    movl %eax, %ebx
    andl $0×0FFF, %eax
    cmpl 8(%edi), %eax
   
    /* Invalid ID, try to load Win32K Table */
    jnb KiBBTUnexpectedRange

    /* Users’s current stack frame pointer is source */
    movl %edx, %esi
  
    /* Allocate room for argument list from kernel stack */
    movl 12(%edi), %ecx
    movb (%ecx, %eax), %cl
    movzx %cl, %ecx
   
    /* Allocate space on our stack */
    subl %ecx, %esp
   

正如代码中的注释所说,开始是检查系统调用号是否在合法范围之内,这里比较的对象显然就是NUMBER_OF_SYSCALLS。
    前面讲过,寄存器%edx指向用户空间堆栈上的函数调用框架,实际上就是指向所传递的参数,现在把这个指针复制到%esi中,这是在为从用户空间堆栈复制参数做准备。但是,光有复制的起点还不够,还需要有复制的长度(字节数)、即参数的个数乘4,所以需要知道具体的系统调用有几个参数。这个信息保存在一个以系统调用号为下标的无符号字节数组中(所以每个系统调用的参数总长度不能超过255字节),SSDT_ENTRY数据结构中的第三个成分(相对位移为12、或0xc)就是指向这个数组的指针。对于常规系统调用,这个数组是MainSSPT。可想而知,这个数组的内容也应来自sysfuncs.lst。代码中先让%ecx指向MainSSPT,再以%eax中的系统调用号与其相加,就使其指向了数组中的相应元素,而movb指令就把这个字节取了出来。所以,最后%ecx持有给定系统调用的参数复制长度。从%esp的内容中减去%ecx的内容,就在系统空间堆栈上保留了若干字节,其长度等于参数复制长度,这样就为把参数从用户空间堆栈复制到系统空间堆栈做好了准备。再往下看:

    /* Get pointer to function */
    movl (%edi), %edi
    movl (%edi, %eax, 4), %eax

    /* Copy the arguments from the user stack to our stack */
    shr $2, %ecx
    movl %esp, %edi
    cld
    rep movsd

    /* Do the System Call */
    call *%eax
    movl %eax, KTRAP_FRAME_EAX(%ebp)

    /* Deallocate the kernel stack frame  */
    movl %ebp, %esp

前面,寄存器%edi已经指向常规系统调用的SSDT_ENTRY数据结构,也就是指向了该数据结构中的第一个成分。SSDT_ENTRY数据结构的第一个成分是个指针,指向一个函数指针数组。对于常规系统调用,这就是MainSSDT。指令“movl (%edi), %edi”把%edi所指处的内容赋给了%edi,使原来指向这个指针的%edi现在指向了MainSSDT。这也是个以系统调用号为下标的数组,其定义为:

SSDT MainSSDT[] = {
  { (ULONG)NtAcceptConnectPort },
  { (ULONG)NtAccessCheck },
  { (ULONG)NtAccessCheckAndAuditAlarm },
  ……
  { (ULONG)NtReadFile },
  ……
}

在我们这个例子中,指令“movl (%edi, %eax, 4), %eax”,即“把%edi加相对位移为‘系统调用号乘4’之处的内容装入%eax”,使%eax指向了NtReadFile()。然后就是把参数从用户空间堆栈拷贝到系统空间堆栈,注意%ecx中的长度是以字节为单位的,所以要右移两位变成以长字为单位。
    最后,指令“call *%eax”就使CPU进入了内核里面的NtReadFile(),其代码在reactos/ntoskrnl/io/rw.c中。如果按Linux的规矩,这应该是sys_NtReadFile():

NTSTATUS STDCALL
NtReadFile (IN HANDLE FileHandle,
     IN HANDLE Event OPTIONAL,
     IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
     IN PVOID ApcContext OPTIONAL,
     OUT PIO_STATUS_BLOCK IoStatusBlock,
     OUT PVOID Buffer,
     IN ULONG Length,
     IN PLARGE_INTEGER ByteOffset OPTIONAL,
     IN PULONG Key OPTIONAL)
{
……
}

这个函数的调用界面与应用程序在用户空间进行这个系统调用时所遵循的界面完全相同,而应用程序压入用户空间堆栈的9个参数已经被拷贝到了系统空间堆栈中合适的位置上。于是,对于这个函数而言,就好像其调用者、在我们这个情景中是ReadFile()、就在系统空间中一样。

    回到上面的汇编代码中。当CPU从目标函数返回时,寄存器%eax持有该函数的返回值,这是要返回给用户空间的,所以把它保存在堆栈框架中。
    下面就是从内核返回到用户空间的过程,我把代码留给读者自己研究。不过需要给一点提示:
    (1).代码中的APC指“异步过程调用(Asynchronous Procedure Call)”,相当于Linux中的Signal。
    (2).Windows把内核的运行状态分成若干级别。最高的一些级别是不允许硬件中断(不允许级别更低的硬件中断);其次(2级和1级)是不允许进程调度(但是允许硬件中断),DPC(2级,相当于bh函数)和APC(1级,相当于signal)都应该在禁止调度的条件下执行;最低(0级)就是允许进程调度。
    (3).从内核中也可以通过_KiSystemService()进行系统调用(不过要经过一个内核版本的stub函数),所以代码中需要检测和区分CPU进入_KiSystemService()之前的运行模式,并且线程的KTHREAD数据结构中也有个成分PreviousMode,用来保存这个信息。而KTHREAD_PREVIOUS_MODE(%esi)就指向当前进程的PreviousMode。

KeReturnFromSystemCall:

    /* Get the Current Thread */
    movl %fs:KPCR_CURRENT_THREAD, %esi

    /* Restore the old trap frame pointer */
    movl KTRAP_FRAME_EDX(%esp), %ebx
    movl %ebx, KTHREAD_TRAP_FRAME(%esi)

_KiServiceExit:

    /* Get the Current Thread */
    cli
    movl %fs:KPCR_CURRENT_THREAD, %esi
   
    /* Deliver APCs only if we were called from user mode */
    testb $1, KTRAP_FRAME_CS(%esp)
    je KiRosTrapReturn
   
    /* And only if any are actually pending */
    cmpb $0, KTHREAD_PENDING_USER_APC(%esi)
    je KiRosTrapReturn
   
    /* Save pointer to Trap Frame */
    movl %esp, %ebx
   
    /* Raise IRQL to APC_LEVEL */
    movl $1, %ecx
    call @KfRaiseIrql@4
   
    /* Save old IRQL */
    pushl %eax
   
    /* Deliver APCs */
    sti
    pushl %ebx
    pushl $0
    pushl $UserMode
    call _KiDeliverApc@12
    cli
   
    /* Return to old IRQL */
    popl %ecx
    call @KfLowerIrql@4
   
KiRosTrapReturn:
   
    /* Skip debug information and unsaved registers */
    addl $0×30, %esp                                 // + 0×48
    popl %gs                                         // + 0×44
    popl %es                                         // + 0×40
    popl %ds                                         // + 0×3C
    popl %edx                                        // + 0×38
    popl %ecx                                        // + 0×34
    popl %eax                                        // + 0×30

    /* Restore the old previous mode */
    popl %ebx                                        // + 0×2C
    movb %bl, %ss:KTHREAD_PREVIOUS_MODE(%esi)

    /* Restore the old exception handler list */
    popl %fs:KPCR_EXCEPTION_LIST                     // + 0×28

    /* Restore final registers from trap frame */
    popl %fs                                         // + 0×24
    popl %edi                                        // + 0×20
    popl %esi                                        // + 0×1C
    popl %ebx                                        // + 0×18
    popl %ebp                                        // + 0×14
    add $4, %esp                                     // + 0×10

    /* Check if previous CS is from user-mode */
    testl $1, 4(%esp)
   
    /* It is, so use Fast Exit */
    jnz FastRet
   
    /*
     * Restore what the stub pushed, and return back to it.
     * Note that we were CALLed, so the first thing on our stack is the ret EIP!
     */
    pop %edx                                         // + 0×0C
    pop %ecx                                         // + 0×08
    popf                                             // + 0×04
    jmp *%edx
         
IntRet:
    iret

FastRet:

    /* Is SYSEXIT Supported/Wanted? */
    cmpl $0, %ss:_KiFastSystemCallDisable
jnz IntRet
……

熟悉Linux的读者知道CPU在返回用户空间之前应该调用有关进程(线程)调度的函数,因而会期待在这段代码中也看到这样的操作,然而却没有看到。但是实际上确实有这样的操作,只不过是深藏在函数KfLowerIrql()里面而已。
    搞懂了这个函数的读者现在应该知道我们将要怎样做了。不过,我们的目标不是把KiSystemService()与Linux的system_call()堆积、并列在一起,而是要把前者溶入到后者中去。再说,即使照搬了KiSystemService(),总不能因为这个程序调用了KfLowerIrql(),就又照搬KfLowerIrql()吧。如果按这样类推,那就势必要把整个ReactOS内核堆积到Linux内核中去了。由此可见,我们既要参考、借鉴ReactOS内核的实现,又要研究怎样把它融合、嫁接到Linux内核中去,这当然是一项富有挑战性的工作。

2005年12月13日

0.系统要求已安装
Visual C++6.0(安装时选上所有工具) ——-先装这个
DDK ——–再装这个
  
1.改造ddk\bin\setenv.bat
  把要求mstools的有关语句注释掉(若想在命令行环境开发驱动则还需加入call VC_DIR\VC98\Bin\Vcvars32.bat),以便能在命令行使用vc的相关工具;若只想在IDE环境开发就不必调用Vcvars32.bat,因为相关工具的路径信息可以在vc环境中设置.)

2.在该目录下创建一个批处理文件MakeDrvr.bat,内容如下:
 @echo off
 if "%1"=="" goto usage
 if "%3"=="" goto usage
 if not exist %1\bin\setenv.bat goto usage
 call %1\bin\setenv %1 %4
 %2

 cd %3
 build -b -w %5 %6 %7 %8 %9

 goto exit

 :usage
 echo usage MakeDrvr DDK_dir Driver_Drive Driver_Dir free/checked [build_options]
 echo eg MakeDrvr %%DDKROOT%% C: %%WDMBOOK%% free -cef
 :exit
  该批处理首先对传递的参数作一些检查,然后调用ddk的setenv命令设置环境变量,然后改变目录为源程序所在驱动器和目录,并最后调用build,-b保证显示完全的错误信息,-w保证在屏幕上输出警告,在vc ide里的output窗口中可以看到这些错误和警告。

3.建立一个空白工程
 选File的new菜单项,然后选project栏的makefile,然后输入路径,一路next下去即可,visual studio提供两种配置win32 debug和win32 release.

4.修改这两种配置
 =============================================================
 Free
 =============================================================
 build command line:
 c:\PCI9052Demo\MakeDrvr %DDKROOT% c: PCI9052Demo free
 
 rebuild all opinions:
 -nmake /a

 output filename:
 PCI9052Demo.sys

 browse info file name:
 objfre\i386\PCI9052Demo.bsc

 =============================================================
 Checked
 =============================================================
 build command line:
 c:\PCI9052Demo\MakeDrvr %DDKROOT% c: PCI9052Demo checked
 
 rebuild all opinions:
 -nmake /a

 output filename:
 PCI9052Demo.sys

 browse info file name:
 objfre\i386\PCI9052Demo.bsc

5.添加源文件到工程
 可以新建,也可以添加,这和普通的win32开发一样。

6.添加资源文件
 选INSERT的RESOURCE菜单项即可

7.把文件makefile放入源程序目录,其内容总是
 #
 # DO NOT EDIT THIS FILE!!! Edit .\sources. if you want to add a new source
 # file to this component. This file merely indirects to the real make file
 # that is shared by all the driver components of the Windows NT DDK
 #

 !INCLUDE $(NTMAKEENV)\makefile.def

8.把文件Sources放入源程序目录,内容为
  TARGETNAME=RamDrive//这是要生成的驱动程序.sys文件的名字
  TARGETPATH=obj //.sys文件所在目录的上层目录,(由于ddk的bug)应手工在obj目录下创建checked和free目录,以作为.sys的最终存放目录
  TARGETTYPE=DRIVER //驱动程序的类型,一般不变
  INCLUDES=$(BASEDIR)\inc //ddk包含文件路径,一般不变
  SOURCES=RamDrive.cpp RamDrive.rc //源文件(不要头文件),资源文件
  BROWSER_INFO = 1 //若想要浏览信息,则要有本行;否则可无

9.因为MakeDrvr.bat在DriverEnv目录,所以应把该目录添加到vc的Executable files里选tools的options菜单项,然后选directories页,在show directories for一栏选择Executable files,然后添加即可.

  至此,环境设置完毕,你可以按F7, build你的驱动程序了。


WDM驱动程序设计之设计开发篇


通过安装DDK和相应的开发软件,我们构造好了WDM驱动程序的开发环境。接着,我们就要深入进行设计与开发工作了。

WDM 驱动程序的运作流程

WDM本身的PNP管理器被抽象地提升到了ROOT的地位。PNP管理器负责所有的总线驱动程序的加载。总线驱动程序则负责遍历所有位于总线上的设备,并且为每个设备创建相应的设备对象。当PNP管理器发现一个设备对象,就查找该对象对应的Driver。并调用该Driver的ADD DEVICE例程。如果Driver不在内存中,就先加载,然后调用ADD DEVICE例程。

当然,总线本身并没有发出任何信号告诉PNP管理器自己的存在,所以,总线Driver是在NT的安装时设定的。而ISA设备并没有规范,因为需要KMD自己检查硬件存在及状态,所以它是老式KMD存在的惟一理由。这也是微软极力在新规范里取消ISA总线的理由之一。WDM支持PNP协议和PM协议,而且实现时仅仅需要在MAJOR FUNCTION里加入一些对PNP和PM事件响应的例程即可。

驱动程序设计

设计一个设备驱动程序,应该支持和其他相同类型设备的NT驱动程序相同的IRP_MJ_XXX和IOCTL请求代码。如果设计一个中间层NT驱动程序,应该首先确认下层驱动程序所管理的设备,因为一个高层的驱动程序必须具有低层驱动程序绝大多数IRP_MJ_XXX例程入口。高层驱动程序在接到I/O请求时,在确定自身IRP当前堆栈单元参数有效的前提下 ,设置好IRP中下一个低层驱动程序的堆栈单元,然后再调用IoCallDriver将请求传递给下层驱动程序处理。一旦决定好了驱动程序应该处理哪些IRP_MJ_XXX,就可以开始确定驱动程序应该有多少个Dispatch例程。当然也可以考虑把某些RP_MJ_XXX处理的例程合并为同一例程处理。例如在ChangerDisk和VDisk里,对IRP_MJ_CREATE和IRP_MJ_CLOSE处理的例程就是同一函数。

一个驱动程序必须为它所管理的每个可能成为I/O请求的目标的物理和逻辑设备创建一个Device对象。一些低层的驱动程序还可能要创建一些不确定数目的Device对象。例如一个硬盘驱动程序必须为每一个物理硬盘创建一个Device对象,同时还必须为每个物理磁盘上的每个逻辑分区创建一个Device对象。

一个高层驱动程序必须为它所代表的虚拟设备创建一个Device对象,这样更高层的驱动程序才能连接它们的Device对象到这个驱动程序的Device对象。另外,一个高层驱动程序通常为它低层驱动程序所创建的Device对象创建一系列的虚拟或逻辑Device对象。

尽管可以分阶段来设计驱动程序,从而使一个处在开发阶段的驱动程序不必一开始就创建出所有它将要处理的所有Device对象,但从一开始就确定好最终要创建的所有Device对象将有助于设计者所要解决的任何同步问题。另外,确定所要创建的Device对象还有助于定义Device对象的Device Extension的内容和数据结构。

驱动程序开发

驱动程序的开发是一个从粗到细逐步求精的过程。NT DDK的src\目录下有一个庞大的模板代码,几乎覆盖了所有类型的设备驱动程序、高层驱动程序和过滤器驱动程序。在开始开发驱动程序之前,应该先在这个样板库下面寻找是否有和所要开发的类似类型的例程。

例如若要开发光盘塔驱动程序,虽然DDK对光盘塔没有任何描述,但光盘塔是符合SCSI-Ⅱ规范的SCSI设备,可以在src\storage\class目录中发现很多和SCSI设备有关的驱动程序,例如SCSI Tape、SCSI Disk、SCSI CDROM等驱动程序开发时,可以参考类似驱动程序,从而减化开发难度。

下面笔者将进一步介绍开发驱动程序的基本步骤:

l.编写驱动程序框架

(1)首先编写一个DriverEntry例程,并在该例程里调用IoCreateDevice来创建一个Device对象。

(2)写一个处理IRP_MJ_CREATE请求的Dispatch例程的基本框架。如果驱动程序创建了多于一个的Device对象,则必须为IRP_MJ_CLOSE请求写一个例程,该例程通常情况下可以和DispatchCreate共用一个例程。

(3) 编译连接驱动程序。

2.测试驱动程序

(1)首先在系统中安装好驱动程序,具体编译安装驱动程序请见下版的《编译安装篇》。

(2)为NT逻辑设备名称和目标Device对象名称之间建立起符号连接,在前面已经知道Device对象名称对Win32用户模式是不可见的,是不能直接通过API来访问的,Win32 API只能访问NT逻辑设备名称。可以通过修改注册表来建立这两种名称之间的符号连接。运行Regedt32.exe在\HKEY_LOCAL_MACHINE\ System\ CurrentControlSet\ Control\ Session Manager\ DOS Devices下建立起符号连接,这种符号连接也可以在驱动程序里调用函数IoCreateSymbolicLink来创建。

(3)完成以上所有的设置并检查无误后,我们必须重新启动Windows系统。

(4)编写一个简单的测试程序调用Win32 API中的CreateFile函数,并以刚才命名的NT逻辑设备名打开这个设备。如果打开成功,则成功地写出了一个最简单的驱动程序了。支持更多的设备I/O请求,例如驱动程序可能需要对IRP_MJ_READ请求做出响应(完成后可用ReadFile 函数进行测试)。如果驱动程序需要能够手工卸载,那么还必须对IRP_MJ_CLOSE做出响应。为所需要处理的IRP_MJ_XXX写好处理例程,并在DriverEntry里面初始化好这些例程入口。一个低层的驱动程序需要一个StartIo、ISR和DpcForIsr例程,可能还需要一个SynchCritSection例程,如果设备使用了DMA,那么可能还需要一个AdapterControl例程。

对于高层驱动程序可能需要一个或多个IoCompletion例程,最起码完成检查I/O状态块然后调用IoCompleteRequest的工作。如果需要,还要对Device Extension数据结构和内容做些修改。有一点必须很清楚的,就是代码运行级别的问题,即IRQL,最常见的级别是PASSIVE_LEVEL、APC_LEVEL、DISPATCH_LEVEL和DIRQL。

在看NT DDK HELP中的函数说明的时候,要注意函数的可运行级别,比如有的函数只能在PASSIVE_LEVEL下运行,有的函数则可以在DISPATCH_LEVEL以下级别运行,级别越高的时候,对代码的要求就越严格,比如在DISPATCH_LEVEL的时候,就不能使用分页内存。通常情况下应该尽可能让代码在低运行级别如PASSIVE_LEVEL下运行,在高级别下运行过长时间将导致系统效率降低、影响系统响应的实时性。但有时候自己无法控制运行的级别,例如在调用低层Driver时使用IoCallDriver,低层Driver响应完毕后会执行completion例程,该例程运行的级别就是由低层Driver来决定。因此在编写completion例程时,应尽量将这个函数设计成能在DISPATCH_LEVEL级别运行。

依照以上开发步骤,我们可以设计出全新的WDM设备驱动程序。


 

WDM驱动程序设计之编译安装篇

设计开发好自己的WDM驱动程序后,为了运行该驱动程序,我们必须编译和安装它们。

编译设备驱动程序的方法

安装DDK后,在DDK程序组下有Check和Free两个编译环境,Check环境用于编译带调试信息的驱动程序,Free则是编译正式发布版本的环境。通常情况下设备驱动程序的编译采用命令行的方式。通过一定的设置可以在VC ++的集成环境下编译。

一般来说,成功编译一个最基本的设备驱动程序需要四个文件,第一个是驱动程序,即C语言源程序文件(例如vdisk.c,注意下面所有的例子都是以vdisk来说明);第二个是RC文件(例如vdisk.rc);第三个是sources文件;第四个文件是makefile.rc文件。sources文件和make文件类似,用来指定需要编译的文件以及需要连接的库文件。这三个辅助文件都很简单,在DDK samples的每个例程里都有三个这样的文件,依样画瓢就能理解它们的结构和意义。

1.举例分析

以下以vdisk程序为例,设vdisk.rc代码为:

/vdisk.rc/

#i nclude

#i nclude

#define VER_FILETYPE  VFT_DRV

#define VER_FILESUBTYPE VFT2_DRV_SYSTEM

#define VER_FILEDESCRIPTION_STR "SCSI VDisk Driver"

#define VER_INTERNALNAME_STR "vdisk.sys"

#define VER_ORIGINALFILENAME_STR "vdisk.sys"

#i nclude "common.ver"

/end of vdisk.rc/

设备驱动程序一般都使用Build实用程序来进行,Build只是NMAKE外面的一个外包装程序。Build本身其实相当简单,编译的大部分工作实际上由Build传递给NMAKE来进行。

/SOURCES/

TARGETNAME=vdisk

TARGETTYPE=DRIVER

TARGETPATH=$(BASEDIR)\lib

TARGETLIBS=$(BASEDIR)\lib\\$(DDKBUILDENV)\scsiport.lib

INCLUDES=..\..\inc

SOURCES=vdisk.c vdisk.rc

/end of SOURCES/

注意SOURCES的文件名没有任何扩展名。

# makefile

#

# DO NOT EDIT THIS FILE!!! Edit .\sources. if you want to add a new source

# file to this component. This file merely indirects to the real make file

# that is shared by all the driver components of the Windows NT DDK

#

!INCLUDE $(NTMAKEENV)\makefile.def

# end of makefile

对所有驱动程序而言,makefile都是一样的,Microsoft也警告不要编辑这个文件,如果需要,可以编辑修改sources文件达到同样的效果。对于设备驱动程序,所使用的C编译器基本上无一例外地选用VC++。

2.编译的基本步骤

(1)首先进入check或free编译环境,初始化DDK编译环境。

(2)运行VC安装目录下bin目录下的vcvars32.bat,初始化VC++编译环境。

(3)运行Build.exe进行编译。

设备驱动程序的安装和启动

1.添加注册表中的键值

Windows NT在引导的时候,通过扫描注册表构造驱动程序列表。这个列表既包括自启动的驱动程序,也包括需要手工启动的驱动程序。这个列表其实就是控制面板中设备Applet所列出来的所有设备。所有的设备驱动程序应该在注册表的HKEY_LOCAL_MACHINE\System\CurrentControl-

Set\Services\下有相应的键值。下面以vdisk为例来说明如何添加键值:

首先在HKEY_LOCAL_MACHINE\ System\ Current ControlSet\Services\下添加一个子项vdisk,注意这里的名称应该和你的驱动程序名称一致。例如驱动程序名称是vdisk.sys,那么这里的子项名称就是vdisk。然后在vdisk下添加以下键值:

名称 数据类型 说明

Type REG_DWORD 驱动程序的种类

Start REG_DWORD 驱动程序的起始启动时间

ErrorControl REG_DWORD 驱动装入失败的错误处理

Group REG_SZ 驱动程序的组名

DependOnGroup REG_MULTI_SZ 所依赖的其他驱动程序

Tag REG_BINARY 同组内驱动程序装入顺序

Parameters (key) 驱动程序特定的参数键

Type值为1表示内核模式驱动程序;为2表示文件系统驱动程序。

ErrorControl值为0表示日志记录错误并忽略;值为1表示日志记录错误并显示一个对话框;值为2表示日志记录错误,并用最后的正确配置重新启动;值为3表示日志记录错误,如果已经使用过正确配置,返回失败。

在任何一个设备驱动程序中,上表中的前三项参数都是必需的。

2.控制驱动程序的装入次序

有时候控制多个驱动程序的装入次序是必要的。例如一套驱动程序中包括三个驱动程序,分别是jbChanger.sys,changerDisk.sys和vdisk.sys。jbChanger和changerDisk是两个SCSI类驱动程序,它们都依赖SCSI小端口(mini port驱动程序),同时changerDisk必须在jbChanger启动之后启动。vdisk是虚拟的磁盘驱动程序,它必须在jbChanger和changerDisk都启动之后才能启动成功。

3.驱动程序的Start值

上面注册表中驱动程序的Start值控制驱动程序在系统启动的时间。目前,Start可以取以下值,此外为该值留有扩展余地,以适用于新的要求:

(l)0×0 (SERVICE_BOOT_START):这个值指定本驱动程序应该由操作系统装入程序启动。一般的驱动程序不会采用本值,因为系统在这个时候几乎还没有启动,大部分系统尚不可用。

(2)0×1 (SERVICE_SYSTEM_START):该值表示在操作系统装入后但同时初始化它自己时启动驱动程序。

(3)0×2 (SERVICE_AUTO_START):该值表示在整个系统启动并运行后由服务控制管理器装入。

(4)0×3 (SERVICE_DEMAND_START):该值表示该驱动程序必须手工启动。可以通过控制面板的设备applet或者使用WIN32 API编程来启动。

(5)0×4 (SERVICE_DISABLED):表示本驱动程序被禁用。

注意在调试驱动程序的时候,最好将Start值设置为3来手工启动,这是因为如果设置为自动启动,而驱动程序在启动的过程中又发生了异常错误的话,可能导致系统不能启动。

如果没有紧急恢复盘,首先可以尝试在启动的时候选择用已知的配置来启动系统,看是否能启动成功。如果失败,可以用DOS启动后到\%SystemRoot%\System32\Drivers目录下将出现问题的驱动程序删除,然后系统就可以启动了。

不过如果NT安装在NTFS分区,DOS启动后将看不到这个分区,这样就必须将硬盘挂到另一NT系统上来删除这个文件了。通过设置Start可以控制驱动程序在不同的时候启动。但如果要解决依赖性问题,则需要使用Group和DependOnGroup值。

首先要确定自己的驱动程序使用的Group名,系统有一些定义好的组名,对于当前系统存在的组名,可以观察注册表的\HKEY_LOCAL_MACHINE\System\CurrentControl-

Set\Control\ServiceGroupOrder\List的键值。例如该值可以设置为:

SCSI miniport

port

Primary disk

SCSI class

SCSI CDROM class

filter


boot file system

这里每一行都是一个Group名,一般来说某个驱动程序都属于某一个Group。系统启动时按照该List下组的顺序依次启动各组里的驱动程序。例如jbChanger和changerDisk都属于SCSI Class组。如果你觉得该表中的组名都不合适,可以在该List的适当位置中添加新的组名。

DependOnGroup值控制本驱动程序启动的时候必须先启动另一组的驱动程序,例如jbChanger和changerDisk的启动就依赖于SCSI miniport组。因此jbChanger和changerDisk的DependOnGroup值都为SCSI miniport。

4.修改注册表的方法

在注册表里这些值可以手工修改,也可以自己编程利用WIN32 API进行添加,同时也可以用ini文件的方式来添加。下面是一个ini(文件名为vdisk.ini)文件的例子。

\Registry\Machine\System\CurrentControl

Set\Services\VDisk

Type=REG_DWORD 0×00000001

Start=REG_DWORD 0×00000003

ErrorControl=REG_DWORD 0×00000001


Group=SCSI Class

Parameters

DriveLetter=N:

然后以vdisk.ini为参数运行REGINI.EXE。就会自动在注册表里添加相应的项。

在注册表里添加好这些项后,必须重新启动系统,这样所添加的设备驱动程序才能在控制面板的设备applet中列出来,再进行其他操作。

5.启动设备驱动程序

在添加修改好注册表后,重新启动系统,如果选择的Start值是0、1、2,如果一切正常,驱动程序就应该已经启动起来了。可以观察控制面板的设备applet中的设备列表。如果Start选择的是3,则可以直接启动。

6.调试工具

目前NT驱动程序的调试工具只有WINDBG和SOFTICE,WINDBG的使用需要双机环境,强力推荐使用SOFTICE。注意目前国内FTP服务器上的SOFTICE 3.2 FOR NT的Setup.ins文件是错误的,它将导致安装程序不认识你的NT,可以用3.0的setup.ins文件替代3.2的setup.ins,这样就可以安装成功。


WDM驱动程序可以在Windows NT上与现有的Windows NT驱动程序共存,也可以在Windows 95上与现有的Windows 95驱动程序共存。现有的Windows NT 和Windows 95驱动程序将继续被支持,但是却不能使用WDM的先进优点。由微软提供的可扩展的WDM类驱动程序是支持新设备的最好选择。在开始开发一个新的WDM类驱动程序之前,硬件开发者应当请教微软公司以取得对特定设备类的支持信息。一旦有可能,就采用仅编写一次类驱动程序,然后通过使用WDM的微型驱动程序来将其扩展成针对特定硬件接口的驱动程序的方法。

这篇文章JIN认为是比较好的,希望大家能喜欢!

jinsanlen 发表于 >2005-6-16 8:56:28  [全文] [评论] [引用] [推荐] [档案] [推给好友] 
 

2005-6-15
驱动的一点体会

一.驱动程序介绍
Windows 环境下,应用程序访问硬件设备需要通过设备驱动程序。
在Windows 95/98下,驱动程序通常是一个Vxd文件;而在Windows NT下,
驱动程序则是一个Sys文件。虽然这两个操作系统都是出自微软门下,
但其使用的驱动程序结构是完全不同的,因此当一个硬件产品需要在以
上两种操作系统下都能运行时,就需要分别编写其特定环境下的驱驱
动程序。驱动程序的编写是一件相当复杂的工作。编程者首先要学习
95/98/NT操作系统的内部工作机制,当对其内部工作机制有了相当了
解后,接下来就要学习如何编写设备驱动程序,当真正要开始编驱动
程序的时候,还要学会如何在Windows内核模式下调试程序,在这里有
一点要说明的是Windows的内核模式可不比用户模式那么简单,在内核
模式下调试程序需要借助SoftICE之类的调试工具。经过不懈努力编写
好驱动程序后,就可以编写应用软件了,这些应用软件就是我们现在
看得见摸得着的东西了,比如Windows 右下角的那个音量控制图标,
它就是一个应用软件,在它下面其实还要经过声卡的驱动程序才能控
制音量的大小。总地来说编写驱动程序是一件复杂的工作,需要编写
者耐心;而学习编写驱动程序也会有一个曲折的过程,需要学习者有
恒心和毅力,在此我也要鼓励那此刚开始学驱动程序编程的初学者。
二.NT和WDM设备驱动程序开发书评
1. The Windows NT Device Driver Book(Art Baker)
- 中译本 :Windows NT设备驱动程序设计指南(机械工业出版社)
这是所出版的第一本关于NT driver开发的书,它只讨论了NT4.0的设备驱动程序开发,Ba
ker先生现在正从事此书第二版的写作.本书只覆盖了部份NT driver(如基本PIO和DMA)的
开发,且叙述得不够深入.如果你想写PCI Drivers的话,你从本书得不到帮助.
本书的最大缺点是技术错误太多!,寄希望于它的第二版吧!
2.Windows NT Device Driver Development by Peter Viscarola and W.Anthony Maso
n(Macmillan出版社)
-中译本:无
该书于1998年10月出版,它同样是只讨论了NT4.0的设备驱动程序开发,本书提供了许多N
T OS和I/O子系统的系统结构细节.另外,本书的两位作者基于自己在实际开发过程中所积
累的经验,在书中提出了很多开发drivers的技巧和建议.此书没有告诉读者"你为什么要
这么作",而只是解释了如何作以及用什么工具去实现.该书相当深入地阐述了各种不同的
drivers的开发,如DMA和PIO.本书从技术的角度讲,是写得比较精确的!
3.Developing Windows NT Devices Drivers byEdwardN.Dekker and Joseph M.Newcom
er(Addison出版社)
-中译本:无
本书是一本Drivers开发的百科全书,值得买!,主要还是讨论NT4.0的设备驱动程序开发.
此书提供了大量硬件的技术细节,如PCI/ISA总线.本书在教你如何写drivers的同时,还提
供了许多示范代码.
本书比较精确.读本书可分享作者丰富的drivers开发经验,很明显,作者很精通drivers的
开发,也知道如何解释这些问题.另外,本书的排版和写作风格也相当不错!
4.Writng Windows WDM Decvice Drivers by Chris Cant(R&D Books)
-中译本:WDM编程指南,刚出版.
不明白为什么国内要选这本书来翻译出版?,本书主要讨论WDM的开发.如果你想看到设备
/硬件信息或OS的系统结构,还是另找它书吧!我想本书的书名取为"WDM入门教程"也许更
为合适.本书只花了两页的功夫来讨论DMA.有时用词也不是那么恰当和精确. 如果你是W
in32程序员或技术经理,或许本书有点用.如果你想知道如何去写一个真正的drivers的话
,不必买这本书.
5.Programming The Microsoft Windows Driver Model by Walter Oney(微软出版社)
本书主要讨论WDM的开发.它详细地讨论了WDM drivers的结构以及如何构造它们. 本书深
入清晰地解释了PIO/DMA及其类型drivers的开发.本书"is clear written",它所提供的
信息是精确和可靠的!
结论
如果你拥有Windows NT Device Driver Development(Peter Viscarola and W.Anthony
Mason), Developing Windows NT Devices Drivers(EdwardN.Dekker and Joseph M.N
ewcomer)和 Programming The Microsoft Windows DriverModel(Walter Oney)这三本书
,你就会明白地知道如何写WDM和NT4的drivers了!
三.怎样学习DDK
学DDK的第一步准备功夫是把英文阅读能力练好,你别指望书商会出中译本.因这类书的
卖相太差,比不上VB,Delphi,JAVA这类较大众化的书.再者要找到够格的译者很难.要译好
是要花相当时间,那还到不如去写些轻松的书,稿费也赚得多.
接下来就要练基础工夫,如同张无忌花了6年时间练好九阳真经,等到练乾昆大挪移时只花
数个时辰就OK.要如何练底层基础呢?勤看书,勤coding及Trace别人写得sample code而已
.即使是天才型的Programmer也是要看书,因Window不是他设计的,必须了解Window才有办
法下手.而非像写Algorithm方面的论文般,自己定assumption, Lemma,导出Theme,下con
clusion就完成.所以一些刚入社会的研究生最好先调整自己的心态.
以往我是读一些大师级的书,如:
"Window Programing"的CharlePetzold,
"Undocument Window"系列的Andrew Scullman,
"Win95 system Programming Secrets"的MattPietrek,
"AdvancedWindows"的JefferyRichter.
期刊的话是:
Microsoft System Journal,
Doctor Dobb’s Journal,
Window Developer Journal这本期刊是我认为学DDK的人必要订阅的!几乎过个1,2期就会
刊登Window Device Driver相关的文章,而且里面有位Paula女士主持的NT专栏,写的很深
入,不是市面一些标榜NT"大剖析"之类的书籍所能比拟.在Andrew Scrullman的"Undocum
ent NT"尚未问世之前,它是我觉得最有深度的专栏.
接下来就谈与Driver有直接关系的资料:
Device Driver的书籍,我从Win31开始说起:
"Writing Windows Device Driver and VxD",Karen Hazzen,第1,2版是最适合写Win31
Driver的参考书籍.也有一本白皮的"Writing Window Device Driver",我认为它的参考
性很低,因它光抄Win31 DDK Function Description就花了50~60几页,有A钱之嫌.
Win95:
那当然是首推"System Programing for Win95",Walter Oney.
这本巨作.我曾因翻Chapter11~13翻到书页掉落,而重新再买一本.Walter Oney既出,谁与
争锋.有这本就够了!也没有人有胆来挑战他.
WinNT:
唯一的一本:"The NT Device Driver Book",Art Baker.很有系统的一步一步介绍如何写
NT Kernal mode driver.先看这本书然后再看MS的NT DDK on Help会让你较容易了解.当
初没这本书时,我刚开始看DDK Help是看得满头雾水.最好搭配"Inside Windows NT",He
len Custer一起看,因为NT底层已经导入Object Oriented观念(WDM是将NT Kernel mode
driver 加装Plug&Play及Bus handle等新功能),与Win95的Virtual Machine观念相差甚
远.这本书虽是1992年出版,但有对NT的核心运作加以介绍,也是一本难得的好书.
NewGroup and WebSite:
1.Win95是comp.os.ms-windows.programmer.vxd
2.NT的话是comp.os.ms-windows.programmer.nt.kernel-mode
3.Window Device Driver Web site:
http://www.albany.net/~danorton/ddk 
http://www.vireo.com 
发展工具:
1. 如何要让你日子好过,一定要有套SoftIce for Win95/NT.千万不要用MS的WinDebug,
它根本是无山晓路用.SoftIce可让你用source code level debuging,对有assembly恐惧
症的人是一大福音.(但我还是劝有心学DDK的人还是要摸assembly).
2.VToolsD(for Win31/95),DriverWork(for NT/WDM)
Vireo公司出的DDK Tool让你完全用C来写Driver,我只能说写Driver时,我已经不能没有
它.由其它还免费附Class Library source code,让我Trace的很高兴.
领域知识(Domain Knowledge)
写Driver需了解你要控制的硬件,如Driver有用到DMA/Interrupt,建议你看一下8259,82
37A的资料.在这方面,"微电脑接口技术与应用"是本不错的参考书. Programmer总有一天
会coding不动的时候.但人老成精,只要你的Domain Knowledge够强的话, 还是会有许多
公司请你去当技术经理, 而这也是你能克死年轻力壮Programmer最大的本钱. 多多看些
其他杂志, 接触一些非Programming方面的知识来培养你的Domain Knowledge吧!
四.DDK常用函数列表
Support Function and Data Structure Reference
ASSERT
ASSERTMSG
CM_FULL_RESOURCE_DESCRIPTOR
CM_PARTIAL_RESOURCE_DESCRIPTOR
CM_PARTIAL_RESOURCE_LIST
CM_RESOURCE_LIST
CONFIGURATION_INFORMATION
CONTAINING_RECORD
CONTROLLER_OBJECT
DbgBreakPoint
DbgPrint
DEVICE_DESCRIPTION
DEVICE_OBJECT
DriverEntry
DRIVER_OBJECT
ExAcquireFastMutex
ExAcquireFastMutexUnsafe
ExAcquireResourceExclusiveLite
ExAcquireResourceSharedLite
ExAcquireSharedStarveExclusive
ExAcquireSharedWaitForExclusive
ExAllocateFromNPagedLookasideList
ExAllocateFromPagedLookasideList
ExAllocatePool
ExAllocatePoolWithQuota
ExAllocatePoolWithQuotaTag
ExAllocatePoolWithTag
ExConvertExclusiveToSharedLite
ExDeleteNPagedLookasideList
ExDeletePagedLookasideList
ExDeleteResourceLite
ExFreePool
ExFreeToNPagedLookasideList
ExFreeToPagedLookasideList
ExGetCurrentResourceThread
ExInitializeFastMutex
ExInitializeNPagedLookasideList
ExInitializePagedLookasideList
ExInitializeResourceLite
ExInitializeWorkItem
ExInterlockedInsertHeadList
ExInterlockedInsertTailList
ExInterlockedRemoveHeadList
ExIsResourceAcquiredExclusiveLite
ExIsResourceAcquiredSharedLite
ExQueueWorkItem
ExReleaseFastMutex
ExReleaseFastMutexUnsafe
ExReleaseResourceForThreadLite
ExTryToAcquireFastMutex
ExTryToAcquireResourceExclusiveLite
HalAssignSlotResources
HalGetAdapter
HalGetBusData
HalGetBusDataByOffset
HalGetInterruptVector
HalSetBusData
HalSetBusDataByOffset
HalTranslateBusAddress
HKEY_LOCAL_MACHINE
InitializeListHead
InitializeObjectAttributes
InsertHeadList
InsertTailList
INTERFACE_TYPE
InterlockedDecrement
InterlockedExchange
InterlockedExchangeAdd
InterlockedIncrement
IoAcquireCancelSpinLock
IoAllocateAdapterChannel
IoAllocateController
IoAllocateErrorLogEntry
IoAllocateIrp
IoAllocateMdl
IoAssignResources
IoAttachDevice
IoAttachDeviceToDeviceStack
IoBuildAsynchronousFsdRequest
IoBuildDeviceIoControlRequest
IoBuildPartialMdl
IoBuildSynchronousFSDRequest
IoCallDriver
IoCancelIrp
IoCompleteRequest
IoConnectInterrupt
IoCopyCurrentIrpStackLocationToNext
IoCreateController
IoCreateDevice
IoCreateNotificationEvent
IoCreateSymbolicLink
IoCreateSynchronizationEvent
IoDeleteController
IoDeleteDevice
IoDeleteSymbolicLink
IoDetachDevice
IoDisconnectInterrupt
IO_ERROR_LOG_PACKET
IoFlushAdapterBuffers
IoFreeAdapterChannel
IoFreeController
IoFreeIrp
IoFreeMapRegisters
IoFreeMdl
IoGetConfiguationInformation
IoGetCurrentIrpStackLocation
IoGetCurrentProcess
IoGetDeviceObjectPointer
IoGetNextIrpStackLocation
IoInitializeDpcRequest
IoInitializeIrp
IoInitializeTimer
IoMakeAssociatedIrp
IoMapTransfer
IoMarkIrpPending
IoRegisterShutdownNotification
IoReleaseCancelSpinLock
IoRequestDpc
IO_RESOURCE_DESCRIPTOR
IO_RESOURCE_LIST
IO_RESOURCE_REQUIREMENTS_LIST
IoSetCancelRoutine
IoSetCompletionRoutine
IoSetNextIrpStackLocation
IoSizeOfIrp
IoSkipCurrentIrpStackLocation
IO_STACK_LOCATION
IoStartNextPacket
IoStartNextPacketByKey
IoStartPacket
IoStartTimer
IO_STATUS_BLOCK
IoStopTimer
IoUnregisterShutdownNotification
IoWriteErrorLogEntry
IsListEmpty
IRP
KdPrint
KeAcquireSpinLock
KeAcquireSpinLockAtDpcLevel
KeBugCheck
KeBugCheckEx
KeCancelTimer
KeClearEvent
KeDelayExecutionThread
KeDeregisterBugCheckCallback
KeFlushIoBuffers
KeGetCurrentIrql
KeGetCurrentProcessorNumber
KeGetDcacheFillSize
KeInitializeCallbackRecord
KeInitializeDeviceQueue
KeInitializeDpc
KeInitializeEvent
KeInitializeMutex
KeInitializeSemaphore
KeInitializeSpinLock
KeInitializeTimer
KeInitializeTimerEx
KeInsertByKeyDeviceQueue
KeInsertDeviceQueue
KeInsertQueueDpc
KeLowerIrql
KeNumberProcessors
KeQueryPerformanceCounter
KeQuerySystemTime
KeQueryTickCount
KeQueryTimeIncrement
KeRaiseIrql
KeReadStateEvent
KeReadStateMutex
KeReadStateSemaphore
KeReadStateTimer
KeRegisterBugCheckCallback
KeReleaseMutex
KeReleaseSemaphore
KeReleaseSpinLock
KeReleaseSpinLockFromDpcLevel
KeRemoveByKeyDeviceQueue
KeRemoveDeviceQueue
KeRemoveEntryDeviceQueue
KeRemoveQueueDpc
KeResetEvent
KeSetEvent
KeSetPriorityThread
KeSetTimer
KeSetTimerEx
KeStallExecutionProcessor
KeSynchronizeExecution
KeWaitForMultipleObjects
KeWaitForMutexObject
KeWaitForSingleObject
KEY_BASIC_INFORMATION
KEY_FULL_INFORMATION
KEY_NODE_INFORMATION
KEY_VALUE_BASIC_INFORMATION
KEY_VALUE_FULL_INFORMATION
KEY_VALUE_PARTIAL_INFORMATION
KIRQL
KSYNCHRONIZE_ROUTINE
LARGE_INTEGER
MmAllocateContiguousMemory
MmAllocateNonCachedMemory
MmCreateMdl
MmFreeContiguousMemory
MmFreeNonCachedMemory
MmGetMdlByteCount
MmGetMdlByteOffset
MmGetMdlVirtualAddress
MmGetPhysicalAddress
MmGetSystemAddressForMdl
MmInitializeMdl
MmIsAddressValid
MmMapIoSpace
MmMapLockedPages
MmPrepareMdlForReuse
MmProbeAndLockPages
MmQuerySystemSize
MmSizeOfMdl
MmUnlockPages
MmUnlockPagableImageSection
MmUnmapIoSpace
MmUnmapLockedPages
NTSTATUS
ObDereferenceObject
ObReferenceObjectByHandle
ObReferenceObjectByPointer
PCI_COMMON_CONFIG
PCI_SLOT_NUMBER
PDRIVER_CONTROL
PIO_DPC_ROUTINE
PIO_TIMER_ROUTINE
PKDEFERRED_ROUTINE
PKSTART_ROUTINE
PsCreateSystemThread
PsGetCurrentProcess
PsGetCurrentThread
PsTerminateSystemThread
READ_PORT_BUFFER_type
READ_PORT_type
READ_REGISTER_BUFFER_type
READ_REGISTER_type
RemoveEntryList
RemoveHeadList
RemoveTailList
RtlInitUnicodeString
RtlMoveMemory
RTL_QUERY_REGISTRY_ROUTINE
RTL_QUERY_REGISTRY_TABLE
RtlQueryRegistryValues
RtlUnicodeStringToAnsiString
RtlZeroMemory
UNICODE_STRING
_URB_CONTROL_DESCRIPTOR_REQUEST
_URB_HEADER
UsbBuildGetDescriptorRequest
WRITE_PORT_BUFFER_type
WRITE_PORT_type
WRITE_REGISTER_BUFFER_type
WRITE_REGISTER_type
ZwClose
ZwCreateFile
ZwCreateKey
ZwDeleteKey
ZwEnumerateKey
ZwEnumerateValueKey
ZwFlushKey
ZwMapViewOfSection
ZwOpenKey
ZwOpenSection
ZwQueryKey
ZwQueryValueKey
ZwSetInformationThread
ZwSetValueKey
ZwUnmapViewOfSection 
 

2005年12月06日

内存管理是操作系统最重要的一部分,其决定了操作系统的性能。Intel X86采用分段、分页的内存机制,Windows NT/2000充分利用了此项先进的机制。段内IA-32体系使用页目录(Page Directory)与页表(Page Table,SoftICE中Page命令可以显示出页目录与页表等)形成对4G地址的寻址能力。文中未特别说明,均讨论运行平台为Intel 32位处理器的Windows NT/2000,所提及的Windows也仅指Windows NT/2000。Windows中不管用户态与核心态的代码段、数据段与堆栈段基址均为0,文中提到的逻辑地址(由段基址与偏移量两部分组成)与线性地址值是相等的。由于对于用户来说线性地址是其可见的,若我未特别指出物理地址,所说的地址也仅指线性地址。

页目录(PDE)由1024项组成,每项均指向一页表(PTE),每一页表也由1024个页组成,IA-32体系每页大小为4K,所以可寻址范围为4G(1024*1024*4K)。Windows中每个进程都拥有其各自的进程地址空间,即拥有其各自的页目录与页表。每个进程均使用线性地址C0300000H指向其特定的页目录所在的地址,而页目录中每项(即页表)均依次排列在线性地址C0000000H处,每个页表均占用4K(1024*4)字节,如第一个页表位于C0000000H处,而第二个页表位于C0000000+1000H(4K),即C0001000中,依次类推,计算公式即为C0000000H+页目录偏移值(线性地址的高10位)*1000H,在下面我将利用此公式。当然以上描述的前提是每个页表均位于物理内存中(由页目录中每项中的P位指定),这也是为什么IA-32使用两级页表的原因,否则每个进程除其代码与数据等外还额外需要4M(4*1024*1024)的存储器。

上面的机制即实现了物理地址寻址,也就实现了在Windows NT/2000中物理地址与线性地址的相互转换(虽然CPU在对内存操作时只需要线性地址转换成物理地址,但我们在分析程序代码等仍需要物理地址转换成线性地址)。照例先看看SoftICE的分析吧:

// addr explorer命令后以下操作将只在进程explorer的私有进程空间进行
:addr explorer

// 显示explorer进程页目录所在物理地址,即进程切换至explorer后PDBR(CR3)中的值
:addr
CR3 LDT Base:Limit KPEB Addr PID Name
.
.
.
00C10000 FF9FC920 036C explorer
.
.
.

/*

线性地址的格式:0-11位对应1页(4096字节)的偏移量
12-21位对当前页表中1024项中寻址
22-31位对页目录进行寻址
高20位(12-31位)又称为桢
根据上面提及的公式,即可以得到物理地址高20位的值,再加上线性地址页表偏移(作为物理
地址的低12位),即实现了线性地址转化为物理地址,用公式表示为:

@(C0000000h+PDE*1000h+PTE*4)&0fffff000h+PO
=@(C0000000h+4*(PDE*400h+PTE))&0fffff000h+PO
=@(C0000000h+(PDE>>10d+PTE)<<2d)&0fffff000h+PO
=@(C0000000h+(LA>>12d)<<2d)&0fffff000h+PO
=@(C0000000h+(LA&0xFFFFF000)>>10d)&0fffff000h+PO

上式中用PDE与PTE分别代表Page Directory与Page Table的偏移值,用LA代表给定的线性
地址,用PO代表LA的低12位,用h与d分别代表16/10进制,@后表示取后地址指针中的内容。
如此分析后,线性地址C0300000所对应的页目录为300h,页表为300h,偏移量为0
则 C0000000h+PD*1000h+PT*4+PO=C0000000+300h*1000h+300h*4+0
*/

:dd c0000000+300*1000+300*4 l 4
0010:C0300C00 00C10063 01A31063 00000000 0141F163 c…c…….c.A.
|
|_低12位(0-11)063为属性位、Intel保留位与系统(OS)使用位

// 显示C0300000的物理地址(00C10000)
:? dword(@(c0000000+300*1000+300*4))&fffff000+C0300000&00000fff
00C10000

//用SoftICE验证
:phys dword(@(c0000000+300*1000+300*4))&fffff000+C0300000&00000fff
C0300000

:page C0300000
Linear Physical Attributes
C0300000 00C10000 P D A S RW

其实上面最后一条命令就可以实现所有其它指令的功能,下面我列出实现的代码段:

// 线性地址->物理地址
// SoftICE中Page命令可以实现此功能
// 一个线性地址对应唯一的物理地址
// 此函数若返回0说明此线性地址未对应物理地址

ULONG LinearAddressToPhysicalAddress(ULONG lAddress)
{
unsigned int *pAddr;
unsigned int *PageDirectoryEntry=(unsigned int *)0xC0300000;
unsigned int *PageTableEntry=(unsigned int *)0xC0000000;

//判断页目录是否有效,第0位(P)为存在位,请参阅相关书籍
if((!(PageDirectoryEntry[lAddress>>22]&0xFFFFF000))
&&(!(PageDirectoryEntry[lAddress>>22]&0×00000001)))
return 0;

//@(C0000000h+(LA&0xFFFFF000)>>10d)&0fffff000h+PO 见上叙述
pAddr=(int *)((int)PageTableEntry+((lAddress&0xFFFFF000)>>10));
if((*pAddr)&1)
return ((*pAddr) &0xFFFFF000) |(lAddress&0×00000FFF);
return 0;
}

那么又如何逆向实现物理地址转换成线性地址呢?虽然其间没有任何关系,但因为知道页目录与页表的具体位置,可使用直接在这个范围中搜索的方法。理论上这个范围最大为1024*1024*4(4M),但由于很多页目录项当前均不存在于物理内存中,所以实际上搜索范围小得多。这也导致一个问题,有可能导致蓝屏(内核态访问不存在的地址)。所以以下给出的代码我检查了页目录中每一项的P位。

// 物理地址->线性地址
// 相当于SoftICE中Phys命令
// 搜索所有有效的页表寻找指定物理地址
// 有可能多个线性地址同时指向同一个物理地址
// 此函数若未输出任何结果表明当前还没有线性地址映射至此物理地址中

void PhysicalAddressToLinearAddress(ULONG pAddress)
{
unsigned int *pAddr;
unsigned int *PageDirectoryEntry=(unsigned int *)0xC0300000;
unsigned int *PageTableEntry=(unsigned int *)0xC0000000;
int i,j;
DbgPrint("\n");
for(i=0;i<1024;i++)
if((PageDirectoryEntry[i]&0xFFFFF000)&&(PageDirectoryEntry[i]&0×00000001))
for(j=0;j<1024;j++){
pAddr=(int *)((int)PageTableEntry+i*4096+j*4);
if((*pAddr)&0×00000001)
if(((*pAddr)&0xFFFFF000)==(pAddress&0xFFFFF000))
DbgPrint("%08X\n",
((i*4*1024*1024+j*4*1024)&0xFFFFF000)|(pAddress&0×00000FFF));
}
}

上面两个程序段涉及到2G-4G范围(线性地址C0000000以上)的内存访问,普通用户态的程序无法实现。在Windows中请使用设备驱动程序以使其在核心态正确执行。

那么Windows又是如何利用分页机制以高效、合理的利用好有限的物理内存呢?Jeffrey Richter的经典著作<<Programming Applications for Microsoft Windows,Fourth Edition>>全面阐述了Windows的内存管理机制,相关原理请具体参阅此书!下面我列出同一程序(mspaint.exe)的两个运行实例的线性地址与物理地址的对应关系,以说明Windows的内存分页机制。

// 下面是mspaint.exe装入内存后各段的地址(两个同时运行的mspaint.exe进程的映射地址一致)
:map32 mspaint
Owner Obj Name Obj# Address Size Type
mspaint .text 0001 001B:01001000 0003A500 CODE RO
mspaint .data 0002 0023:0103C000 00002670 IDATA RW
mspaint .rsrc 0003 0023:0103F000 000116C8 IDATA RO
————-
|
|_逻辑地址

// mspaint.exe第一个运行实例的线性地址与物理地址的对应关系

Linear Address Range Physical Address Range Attributes
——————– ———————- ———-
00010000 – 00010FFF 03A8B000 – 03A8BFFF 047
00020000 – 00020FFF 03BCC000 – 03BCCFFF 047
0006D000 – 0006DFFF 018BC000 – 018BCFFF 047
. . .
. . .
. . .
//mspaint.exe第一个实例的.text段
01001000 – 01001FFF 00596000 – 00596FFF 005
01002000 – 01002FFF 03F97000 – 03F97FFF 005
01003000 – 01003FFF 03D58000 – 03D58FFF 005
. . .
. . .
. . .
//mspaint.exe第一个实例的.data段
0103C000 – 0103CFFF 0225F000 – 0225FFFF 047
0103D000 – 0103DFFF 03620000 – 03620FFF 047
0103E000 – 0103EFFF 03C1E000 – 03C1EFFF 047
. . .
. . .
. . .
//mspaint.exe第一个实例的.rsrc段
0103F000 – 0103FFFF 01652000 – 01652FFF 025
01040000 – 01040FFF 02653000 – 02653FFF 005
01041000 – 01041FFF 003D4000 – 003D4FFF 005
. . .
. . .
. . .
//mspaint.exe第一个实例的页目录的表
C0300000 – C0300FFF 030FD000 – 030FDFFF 063
C0301000 – C0301FFF 017FE000 – 017FEFFF 063
C0303000 – C0303FFF 0141F000 – 0141FFFF 163
. . .
. . .
. . .
FFD0F000 – FFD0FFFF 000FF000 – 000FFFFF 023
FFDF0000 – FFDF0FFF 0026A000 – 0026AFFF 163
FFDFF000 – FFDFFFFF 00269000 – 00269FFF 163

// mspaint.exe第二个运行实例的线性地址与物理地址的对应关系

Linear Address Range Physical Address Range Attributes
——————– ———————- ———-
00010000 – 00010FFF 03A6A000 – 03A6AFFF 047
00020000 – 00020FFF 0352B000 – 0352BFFF 067
0006D000 – 0006DFFF 03413000 – 03413FFF 047
. . .
. . .
. . .
//mspaint.exe第二个实例的.text段
01001000 – 01001FFF 00596000 – 00596FFF 005
01002000 – 01002FFF 03F97000 – 03F97FFF 005
01003000 – 01003FFF 03D58000 – 03D58FFF 005
. . .
. . .
. . .
//mspaint.exe第二个实例的.data段
0103C000 – 0103CFFF 030DF000 – 030DFFFF 047
0103D000 – 0103DFFF 009A0000 – 009A0FFF 047
0103E000 – 0103EFFF 02089000 – 02089FFF 047
. . .
. . .
. . .
//mspaint.exe第二个实例的.rsrc段
0103F000 – 0103FFFF 01652000 – 01652FFF 005
01040000 – 01040FFF 02653000 – 02653FFF 005
01041000 – 01041FFF 003D4000 – 003D4FFF 005
. . .
. . .
. . .
//mspaint.exe第二个实例的页目录的表
C0300000 – C0300FFF 037C9000 – 037C9FFF 063
C0301000 – C0301FFF 02F8A000 – 02F8AFFF 063
C0303000 – C0303FFF 0141F000 – 0141FFFF 163
. . .
. . .
. . .
FFD0F000 – FFD0FFFF 000FF000 – 000FFFFF 023
FFDF0000 – FFDF0FFF 0026A000 – 0026AFFF 163
FFDFF000 – FFDFFFFF 00269000 – 00269FFF 163

上面列表直接从mspaint.exe的两个同时运行的实例的页目录与页表中取得。实际上你只要理解物理地址与线性地址的相互转换,稍微修改一下上面所给的两个代码段便能得到上面的信息(32位x86平台Windows 2000 Server Build 2195的某一时刻所取,我的机器物理内存为64M,即040000000H字节)。

Windows中每个进程均有其私有的线性地址,在线性地址的前2G(用户空间,Windows 2000 Server的ntoskrnl.exe的MmHighestUserAddress指出用户空间最大值7FFEFFFFH),上例中列出的两个实例同时运行的mspaint.exe的.text段与.rsrc段均指向同一物理地址空间,而.data段指向不同的物理空间。这是由于不同段的作用与性质决定,不难理解。在后2G(内核空间,Windows 2000 Server的ntoskrnl.exe的MmSystemRangeStart指出其线性地址的开始位置),大部分物理空间均由两个实例共享,实际上不同程序所有运行的例程均共享这2G,当然页目录(C0300000h)与页表(C0000000h)等其它比较特殊的操作除外,上例中基本可以看出这个规则。当然页目录与页表也有项目是指向同一物理区域,这样才能实现进程间共享物理内存,如上面mspaint.exe的两个实例的页目录中C0303000-C0303FFF所映射的线性地址00800000-00BFFFFF(4M)指向同一物理区域。

这只是讨论通常情况下Windows如何在程序间高效的使用内存,实际上Windows赋予页很多机制,如Copy On Write等,使.text段等需要时也可以指向不同的物理地址,典型情况是使用用户态的调试器(Microsoft Visual C++所附的IDE调试环境等)对应用程序进行调试。当然Windows也提供让数据共享同一物理内存区域的方法,即使用Microsoft连接器(link)的section开关,赋予特定的段共享(S)属性。

在<<浅析Windows NT/2000环境切换>>(Nsfocus Magazine 12)中我曾详细的介绍了Windows NT/2000环境切换后页目录基址(CR3)切换代码,那么Windows NT/2000如何为应用程序从头分配页目录与页表呢?因为只有在建立新进程时才牵涉到页目录与页表的分配,所以还是让我们看看Kernel32.dll中的CreateProcessW代码吧(CreateProcessA间接调用CreateProcessW).

可以简单的将流程用代码如下显示:

KERNEL32!CreateProcessW
.
.(一些错误例程如进程文件是否存在,内核对象安全性检查等过程)
.
;打开文件
001B:77E7DDD2 CALL [ntdll!NtOpenFile]
.
.(主要是一些参数的压栈代码)
.

;为可执行文件分配虚拟地址
001B:77E7DE0A CALL [ntdll!NtCreateSection]
.
.
.
;关闭文件
001B:77E7DE1E CALL [ntdll!NtClose]
.
.
.
;调用NtCreateProcess创建进程
001B:77E7DF83 CALL [ntdll!NtCreateProcess]
.
.
.

其实上面ntdll.dll的四个过程在Windows 2000 Server Build 2195中分别是Service ID为64h、2bh、18h与29h的System Service。关于System Service可参阅<<再谈Windows NT/2000内部数据结构>>(Nsfocus Magazine 11)。

我们继续看看NtCreateProcess处理流程:

:u ntdll!NtCreateProcess //用户态,就是常说的Native API
ntdll!NtCreateProcess
001B:77F92D2C MOV EAX,00000029
001B:77F92D31 LEA EDX,[ESP+04]
001B:77F92D35 INT 2E //使用中断门进入核心态
001B:77F92D37 RET 0020

:ntcall
Service table address: 804704D8 Number of services:000000F8
.
.
.
0029 0008:804AD948 params=08 ntoskrnl!SeUnlockSubjectContext+0514
|
|_ID为29h的System Service(NtCreateProcess)的入口地址
.
.
.
:u 8:804ad948
0008:804AD948 55 PUSH EBP
0008:804AD949 8BEC MOV EBP,ESP
0008:804AD94B 6AFF PUSH FF
0008:804AD94D 6890354080 PUSH 80403590
0008:804AD952 682CCC4580 PUSH ntoskrnl!_except_handler3
0008:804AD957 64A100000000 MOV EAX,FS:[00000000]
0008:804AD95D 50 PUSH EAX
0008:804AD95E 64892500000000 MOV FS:[00000000],ESP
.
.
.
;EBP-30中此时存放新建进程的KPEB,以下几句实现对KPEB后的0A2H*4(648)字节清零
0008:804ADAF5 B9A2000000 MOV ECX,000000A2
0008:804ADAFA 33C0 XOR EAX,EAX
0008:804ADAFC 8B7DD0 MOV EDI,[EBP-30]
0008:804ADAFF F3AB REPZ STOSD
.
.
.
0008:804AD5E7 55 PUSH EBP
0008:804AD5E8 8BEC MOV EBP,ESP
;KPEB与进程Context分别作为这个过程的第一与第四个参数传入(ebp+8与ebp+14h)
0008:804AD5EA 8B4508 MOV EAX,[EBP+08]
0008:804AD5ED 8D4808 LEA ECX,[EAX+08]
0008:804AD5F0 C60003 MOV BYTE PTR [EAX],03
0008:804AD5F3 89480C MOV [EAX+0C],ECX
0008:804AD5F6 C640021B MOV BYTE PTR [EAX+02],1B
0008:804AD5FA 8909 MOV [ECX],ECX
0008:804AD5FC 8A4D0C MOV CL,[EBP+0C]
0008:804AD5FF 884862 MOV [EAX+62],CL
0008:804AD602 8B4D10 MOV ECX,[EBP+10]
0008:804AD605 89485C MOV [EAX+5C],ECX
0008:804AD608 8A4D18 MOV CL,[EBP+18]
0008:804AD60B 884864 MOV [EAX+64],CL
0008:804AD60E 8B4D14 MOV ECX,[EBP+14]
0008:804AD611 8B11 MOV EDX,[ECX]
;EDX存放进程Context(即页目录的物理地址)
;至于进程Context的算法,由于不仅与ntoskrnl.exe中的几个变量有关,还与执行环境息息相关,感兴趣的自己用SoftICE跟跟
;将进程Context放入新建的KPEB中
0008:804AD613 895018 MOV [EAX+18],EDX ;18H是进程Context相对KPEB的偏移
.
.
.
;以下实现将新建的进程的KPEB插入系统KPEB的双向链表
0008:804ADD22 A184A14680 MOV EAX,[8046A184]

/*
8046A180用以下两条SoftICE命令输出结果就可以很容易理解
:? (@8046a180)-a0
FE4E1D60 4266532192 (-28435104) "﨨`"
:? @PsInitialSystemProcess //显示System进程的KPEB
FE4E1D60 4266532192 (-28435104) "﨨`"
即实现将新KPEB链插入已有链表尾
在插入KPEB后,系统就可以根据上面提供的页目录对进程进行调度(即新进程拥有新的私有的进程空间)
*/

0008:804ADD27 8B4DD0 MOV ECX,[EBP-30]
0008:804ADD2A C781A000000080A14680 MOV DWORD PTR [ECX+000000A0],8046A180
0008:804ADD34 8B4DD0 MOV ECX,[EBP-30]
0008:804ADD37 8981A4000000 MOV [ECX+000000A4],EAX
0008:804ADD3D 8B4DD0 MOV ECX,[EBP-30]
0008:804ADD40 81C1A0000000 ADD ECX,000000A0 ;是不是可以找出链状结构相对KPEB的偏移呢?
0008:804ADD46 8908 MOV [EAX],ECX
0008:804ADD48 8B45D0 MOV EAX,[EBP-30]
0008:804ADD4B 05A0000000 ADD EAX,000000A0
0008:804ADD50 A384A14680 MOV [8046A184],EAX


为了更直观,上面的代码我只是按照系统执行流程列出(相对于实际的磁盘存放顺序).其实系统在创建进程初,首先用ObCreateObject建立section内核对象(section对象并不分配物理内存,Windows 2000 DDK Documentation中有详细介绍),然后才有牵涉到页目录与页表,自己在分析代码时要特别注意,还有可以使用IDA对ntoskrnl.exe进程分析,毕竟IDA对代码流程提供的比较清晰。至于以上所提的进程Context,系统KPEB双向链表等请参阅<<浅析Windows NT/2000环境切换>>,在那我已经进行了比较详细的说明。

在深入分析Windows的有关内存操作的API(如VirtualAllocEx,CreateFileMapping,HeapAlloc等等)后,还可以发现很多其它方面重要的信息。如跟踪ntoskrnl!NtCreateSection(Windows在装入可执行文件与CreateFileMapping等都最终调用此函数)可发现Copy On Write等机制是如何实现的等等.这些都留着你自己去找找了。Windows 2000支持多个页面文件(命名为Pagefile.sys),这牵涉到原型PTE(Sofice中用ProtoPTE表示),分页文件使用的PTE等等很多的机制,要分析之,还要对Windows 2000的分层驱动程序中的FSD有很好的理解.简单的说即文中只讨论到PTE中的P位为1的情况。

内存管理可以说是操作系统实现中最重要的环节,也是最为复杂的一环节。对于相对贫乏的内存资源,内存共享也成了一个很重要的有效手段。Windows 2000/XP在此方面的实现借助于一个称为原型PTE(Prototype PTE,PPTE)的软件机制。在《小议Windows NT/2000分页机制》中我详细的介绍了Intel X86实现分段、分页的硬件PTE工作方式。我们来回顾一下这种机制:

    假设我们的一个进程映射了从虚拟地址0xXXXXXXXX(假设位于分配粒度上)开始的4M空间,而这4M空间当前都相应的映射了实际的物理内存(鉴于Lazy evaluation等的先进思想,这种情况在Windows 2000/XP中比较少见)。我们将这4M空间分成1000块的4K(PAGE_SIZE,X86处理器决定),对于第n个4K(0<=n<1000),其虚拟地址(0xXXXXXXXX+n*4K),我们都有一个对应的硬件PTE,指出目前这4K驻留于物理内存的位置。通过由PDBR(CR3寄存器)与虚拟地址可定位这个硬件PTE(具体请参阅《小议Windows NT/2000分页机制》)。

    现在让我们来考虑这样一种情况,我们有一个文件其大小也为4M,我们知道通常我们要使用这个文件都要将它读入内存。试想同时有两个或更多的进程需读写这个文件,这就需要解决内存共享的问题。实际上就算当前只有一个进程访问这个文件,对于这种潜在的需要共享的文件,Windows 2000/XP均会事先考虑共享情况。她通过一个称为Section的内核对象来实现这样的目的。仔细想想,这种情况下内存共享决不仅仅是内存资源的充分利用,就算我们可以为每个进程各分配4M空间,但是这将导致各个进程某种时刻可能得不到这个文件的最新内容。这是非常糟糕的情况。在内部Windows 2000/XP利用原型PTE来解决这样的情况。基于硬件PTE相同的原理,对于这样一个4M的文件,在映射这个文件时,Windows 2000/XP同样的将这个文件分成1000块,每块4K(PAGE_SIZE)大小。然后从页交换区分配1000个DWORD,每个DWORD值都是原型PTE,它们组成原型PTE表。对于这个文件的第n个4K(0<=n<1000),如果当前其驻留在物理内存中的话,其对应的PPTE的Valid位(bit 0,与硬件PTE一致)为1,然后这个PPTE的Page Frame Number(PPTE的高20位)用于指示物理内存。如果当前其仍然在磁盘中的话,Valid位为0。针对这种情况,通过PPTE的高20位(PFN Entry),查找Page Frame Datbase(由MmPfnDatabase定位),通过PFN Entry的Subsection PTE(windbg中称为restore pte,《Inside Windows 2000》中称为original pte,Windows XP内部称为Subsection PTE),定位Subsection,然后通过Subsection指向的Control Area的FILE_OBJECT,与PPTE在PPTE表的偏移n,通过公式:

    PFN Entry Subsection PTE->Subsection->Control Area->FileObject + n * 4K

定位所要访问的文件偏移,这样Windows 2000/XP使用页面调入IO读入这页内容,更新PPTE表的这个PPTE。以上的这一系列定位转换算法,如Subsection PTE如何定位Subsection,我将另行介绍。上面的描述解决了一个非常重要的问题,我们不需要更新所有引用这一页面的进程的硬件PTE,因为此时所有进程的PTE均指向PPTE,我们只要更新PPTE就能达到目的。至于进程PTE如何指向PPTE,下面我会涉及到这个内容。这儿你只要有一个概念,进程的PTE为了指向PPTE,肯定是一个Invalid PTE,即bit 0为0,而且其bit 10为1(PPTE标志,具体请看我在《探寻Windows NT/2000 Copy On Write机制》列出的HARDWARE_PTE_X86结构)。

    对于PPTE,因为X86处理器没有提供这样一种方式,像处理硬件PTE一样,由CPU直接进行地址转换。Windows 2000/XP内存管理器在处理Page Fault时,通过软件机制来模拟这种实现,这可以说是硬件PTE与PPTE的一个本质区别。

    应该重点提出的是PPTE存在于页交换区(由MmPagedPoolStart与MmPagedPoolEnd指定的位置,从虚拟地址0xE1000000开始),其本身也有可能被Page Out,Windows 2000/XP通过MiCheckProtoPtePageState判断是否被Page Out,还有页交换区的起始地址0xE10000000将用于从无效PTE转化成原型PTE所在的地址,这等一下我会介绍到的。

    照例我们用SoftICE来验证一下我们前面的描述:

    :bpint e

    只要我们截获这个硬件中断,我们就知道肯定发生了Page Fault,但是我们并不能确定这都是由于指向PPTE的无效PTE导致的。事实上Copy On Write等等其他机制,均会发生Page Fault(《探寻Windows NT/2000 Copy On Write机制》有详细讨论)。但是正如我们前面提及的PPTE的bit 10为1,我们还是很容易的判定一个Page Fault是不是由于指向PPTE的无效PTE导致的。由于发生Page Fault的虚拟地址由CR2寄存器指定,经过几次尝试以后,我们继续以下的讨论:

    Break due to BPINT 0E   (ET=2.23 Seconds)
    :cpu

    Processor 00 Registers
    ———————-
    CS:EIP=0008:801648A4  SS:ESP=0010:FCBEADC8
    EAX=C002100B  EBX=77E74A02  ECX=00000102  EDX=00000000
    ESI=00085108  EDI=000493E0  EBP=0140FF74  EFL=00000006
    DS=0023  ES=0023  FS=0038  GS=0000

    CR0=8000003B PE MP TS ET NE PG
    CR2=77D3BB26   //发生Page Fault的虚拟地址。
       .
       .
       .

    :page 77d3bb26
    Linear     Physical   Attributes
    77D3BB26   NP 01A714F6

    从PTE值01A714F6的bit 10为1我们知道这是一个指向PPTE的无效PTE。通过query命令我们可以找到CR2指定的地址,位于模块rpcrt4.dll中。从下面可以看到:

    :query 77d30000
    Context   Address Range      Flags     MMCI      PTE       Name
    explorer  77D20000-77D8E000  07100001  FF8D1328  E169C580  rpcrt4.dll

    结合我文章开始的介绍,通过以下的计算:

    :? (77d3bb26-77d20000)/1000*4+e169c580
    unsigned long = 0xE169C5EC, -513161748, "\xE1i\xC5\xEC"

    我们可以得到其实PTE 01A714F6应该指向0xE169C5EC位置。这时候由MMCI指向的Control Area,根据我上面提到的计算公式,即可以读出rpcrt4.dll偏移(0xE169C5EC-0XE169C580)/4*1000处,即0×1B000处的4K字节,读入虚拟地址77D3B000中((0xE169C5EC-0XE169C580)/4*1000+77D20000),而CR2指定的地址77D3BB26肯定在这4K之中。

    其实这样我们已经描述了MmAccessFault处理指向PPTE的无效PTE的一个典型过程。这里只是演示了原型PTE指向的页面未驻留在物理内存的情况,试想如果我们的页面已经在物理内存了,我们还有必要去费时的查找VAD吗?这就要涉及到无效的PTE如何定位原型PTE,所以我一直使用指向PPTE的无效PTE的叫法。《Inside Windows 2000》中指出指向PPTE的无效PTE的具体格式,但我发现其描述的不尽正确,我一直深信像作者那样能触及Windows 2000代码的人肯定不会有什么问题,所以我在理解PPTE时一直卡在此处。后来通过反汇编实现时发现实际上通过下面的方式来计算PPTE的位置:

   (PTE>>2) & 0×3FFFFE00 + (PTE & 0×000000FF) << 1 + 0xE1000000

    其中PTE为指向PPTE的无效PTE,0xE10000000是页交换区的起始地址。同样我们使用上面的例子来演示这个算法:

    上面的无效PTE为01A714F6,有了这个值,我们可以得到:

       PPTE Address = (0×01A714F6 >> 2) & 0×3FFFFE00 + (0×01A714F6 & 0×000000FF) << 1 + 0xE1000000
                    = 0×0069C53D & 0×3FFFFE00 + 0xF6 << 1 + 0xE1000000
                    = 0×69C400 + 0×1EC + 0xE1000000
                    = 0xE169C5EC

    与我们通过VAD查找到的PPTE位置0xE169C5EC一致。

    为了更好的理解PPTE,我们再来看一个例子。我们知道在Windows 2000/XP中ntdll.dll是个非常重要的dll,只要操作系统正常启动,ntdll肯定会被多个进程共享。我们用SoftICE作如下分析:

    :query -x 77f50000
    Context   Address Range      Flags     MMCI      PTE       Name
    smss      77F50000-77FF8000  07100005  80E6FA50  E131F9E8  ntdll.dll
        .
        .
        .
    explorer  77F50000-77FF8000  07100005  80E6FA50  E131F9E8  ntdll.dll
        .
        .
        .

    :addr smss
    :mod ntdll
    hMod Base     PEHeader Module Name      File Name
         77F50000 77F500E8 ntdll            \WINDOWS\system32\ntdll.dll

    根据ntdll的基地址77F50000,我们查看其硬件PTE:

    :dd 1df*1000+350*4+c0000000 l 4 //详细请参考《小议Windows NT/2000分页机制》
    0010:C01DFD40 02267027  02F2E005  02F2F005  00C7E4FA      ’p&………….

    从smss进程的这些页表,我们很容易知道ntdll.dll第1至3个4K均驻留于物理内存地址中,因为它们都是有效的硬件PTE,而第四个PTE(00C7E4FA),虽然其是一个无效PTE(bit 0为0),但由于其是一个指向PPTE的PTE(bit 10为1),所以我们不能仅凭此PTE是个无效PTE,就断定ntdll.dll的第4个4K就不在物理内存中。我们要进一步的分析这个PTE,找出指向的PPTE判断这第4个4K是不是真的就是在磁盘中。OK,通过上面提及的算法,我们很容易的算出PPTE Address为E131F9F4,我们来看看这个PPTE的值:

    :dd e131f9f4 l 4 
    0010:E131F9F4 02F30121  02F31121  02F32121  02F33121      !…!…!!..!1..

    从值02F30121我们这时就可以判定这第4个4K也存在于物理地址中,位于Page Frame Number为02F30的物理内存中,剩下的就是查PFN Database了。

    我们也可以来查看查看explorer进程的ntdll.dll映射情况,来验证一下这种情况:

    :addr explorer
    :dd 1df*1000+350*4+c0000000 l 4
    0010:C01DFD40 02267025  02F2E025  02F2F025  02F30025      %p&.%…%…%…

    这回清楚了吧。文章开头我提及:“我们不需要更新所有引用这一页面的进程的硬件PTE,因为此时所有进程的PTE均指向PPTE,我们只要更新PPTE就能达到目的了”。从中我们也可以看到ntdll.dll的第4个4K实际上位于物理内存中,但Windows 2000/XP并没有更新每个引用此页面的PTE,就正如smss进程一样。而PPTE却已经指向其实际地址了。当smss进程首次访问这个区域时,内存管理器才将02F30025(假设属性与explorer进程使用这页的属性一样且为考虑访问位标志)这个有效的硬件PTE更新上面的00C7E4FA,现在一切都明朗了吧。

    本文虽然着重点在于介绍PPTE,但实际上我已将Section对象的内部机制说得非常清楚。这也是我原先将文章标题定为剖析Section之类的。关于PPTE,我的理解也经历了较多时间,主要是目前这部分资料实在是没有,仅有的《Inside Windows 2000》在没深入介绍的同时其指向PPTE的无效PTE格式未明确指出(特别是加上0xE1000000,这让我吃尽了苦头),本文介绍的这个格式我已经在Windows 2000及XP上测试过,实际上本文的两个例子一个是在Windows 2000 Server Build 2195,另一个在XP专业版Build 2600上演示的。

    在这次介绍PPTE后,我们来回顾一下内存管理器内部的几个千丝万缕的联系:

    FILEOBJECT的SECTION_OBJECT_POINTERS->DataSectionObject或SECTION_OBJECT_POINTERS->ImageSectionObject(决定于Section对象映射的文件的打开方式)指向Control Area,同时进程描述这文件映射的虚拟地址的VAD的MMCI成员(SoftICE叫法)也指向这个Control Area,Control Area底下存在一至多个SubSection,SubSection指向PPTE,PPTE table一般位于Control Area指向的Segment结构的底部。Section对象指向Segment;进程Page Table指向PPTE;这一切现在已描述的比较清楚了。还有一个主要的联系,即PFN Entry的Restore PTE(Original PTE)指向Subsection。

在设计和使用PCI设备时,经常要在PC机的软件中访问和控制硬件设备,但Windows操作系统(Windows NT、Windows2000、WindowsXP)为了保证系统的安全性、稳定性和可移植性,对应用程序访问硬件资源加以限制,这就要求设计设备驱动程序以实现PC机的软件对PCI设备的访问。
内核(Kernel)模式的驱动程序可以应用于WINDOWS NT和WINDOWS 2000的操作系统中。它区别于WDM(Win32 Driver Model)模型,主要不支持即插即用,但对于编程的思想二者基本上一致,对于本文所述的DMA编程的方法在WDM模式的驱动程序中一样适合。
本文通过现有最常见的AMCC公司生产的AMCC S5933 PCI 控制芯片为例说明在WINDOWS NT平台下如何编写设备驱动程序以实现DMA传输方式。本文对基本的驱动程序设计技术不作详细的说明,重点介绍PCI设备驱动程序开发的相关技术与实现方法,以及用户接口程序的设计与实现技术。
1 NT平台驱动程序模式及开发工具
设备驱动程序是指管理某个外围设备的一段代码,驱动程序不会独立地存在,而是操作系统的一部分。通过设备驱动程序,多个进程可以同时使用这些资源,从而可以实现多进程并行运行。在本文中,将调用设备驱动程序的PC机程序称为用户程序。
Intel 80386以上的微处理器有4个优先级别:0级、1级、2级和3级。Windows NT使用了一个简化的模型描述硬件特权级,然后这个模型映射到指定CPU上可用的特权检查机制,即内核模式和用户模式。内核模式对应于Intel系统的0级,可以执行特权级指令,对任何I/O设备有全部的访问权,还能够访问任何虚地址和控制虚拟内存硬件。用户模式对应于Intel系统的3级环,用户程序在该模式下运行,对硬件的访问操作受到系统的限制。
开发设备驱动采用的工具包括微软为驱动开发提供的设备驱动开发包(Device Driver Kit,DDK)。DDK开发包提供了设备开发的帮助文档,编译需要的头文件和库文件,调试工具和范例程序。基于NT DDK使用C语言编写,因此我们使用VC++6.0作为编辑与编译工具。内核调试工具选用了Numega公司的产品SoftICE。
2 PCI设备驱动开发的特点
2.1PCI配置空间
每个PCI设备都有自己的配置空间,用于支持即插即用,使之满足现行的系统配置结构。下面对PCI配置空间做一下简要介绍。
配置空间是一容量为256字节并具有特定结构的地址空间。这个空间又分为头标区和设备有关区两部分。头标区的长度是64字节,每个设备都必须配置该区的寄存器。该区中的各个字段用来唯一地识别设备。其余的192字节因设备而异。配置空间的头标区64个字节的使用情况如图1示。
为了实现即插即用,系统可根据硬件资源的使用情况,为PCI设备分配新的资源。因此编写设备驱动程序重点是获得基址寄存器(Base Address)和中断干线寄存器的内容。配置空间共有六个基址寄存器和一个中断干线寄存器,具体用法如下:
PCI Base Address 0寄存器:系统利用此寄存器为PCI接口芯片的配置寄存器分配一段PCI地址空间,通过这段地址我们可以以内存映射的形式访问PCI接口芯片的配置寄存器。
PCI Base Address 1寄存器:系统利用此寄存器为PCI接口芯片的配置寄存器分配一段PCI地址空间,通过这段地址我们可以以I/O的形式访问PCI接口芯片的配置寄存器。
PCI Base Address 2、3、4、5寄存器:系统BIOS利用这些寄存器分配PCI地址空间以支持PCI接口芯片的局部配置寄存器0、1、2、3的访问。
在所有基址寄存器中,第0位均为只读位,表示这段地址映射到存储器空间还是I/O空间,如果是“1”表示映射到I/O空间,如果是“0”则表示映射到存储器空间。
中断干线寄存器(Interrupt Line):用于说明中断线的连接情况,这个寄存器的值与标准8259的IRQ编号(0~15)对应。
Byte3
Byte2
Byte1
Byte0
Device ID
Vendor ID
PCI Status
PCI Command
Class Code
Revision ID
Built-In Self Test
Header Type
Latency Timer
CacheLine Size
Base Address Register 0 ~ 5
Reserved Space
Reserved Space
Expansion ROM Base Address
Reserved Space
Reserved Space
Max. Latency
Min. Grant
Interrupt Pin
Interrupt Line
表1 PCI配置空间
 
 
2.2设备初始化
PCI设备驱动程序要完成识别PCI器件、寻找PCI硬件的资源和对PCI器件中断的服务。在驱动程序初始化过程中,使用HalGetBusData()函数完成寻找PCI设备的工作。在初始化过程中,使用器件识别号(Device ID)和厂商识别号(Vendor ID),通过遍历总线上的所有设备,寻找到指定的PCI设备,并获取设备的总线号,器件号与功能号。通过这些配置信息,可以在系统中寻址该设备的资源配置列表。
在此之后,驱动程序需要从配置空间获取硬件的参数。PCI设备的中断号、端口地址的范围(I/O)方式、存储器的地址与映射方式等,都可以从硬件资源列表数据结构中获取。在Windows NT中,调用HalAssignSlotResources()函数来获得指定设备的资源列表数据结构指针,然后通过遍历该列表中的所有资源描述符,获取该设备的I/O端口基地址与长度,中断的中断级、中断向量与模式,存储器基地址与长度等硬件资源数据。我们设计的DMA通信采用总线主控方式进行通信,在设备初始化时需要对DMA适配器进行初始化,使用HalGetAdapter()获得操作系统分配的适配器对象指针。
示例代码如下:
// 遍历总线,获得指定设备的总线号,器件号与功能号
for ( busNumber = 0; busNumber < MAX_PCI_BUSES; busNumber++ )
{
for ( deviceNumber = 0;deviceNumber < PCI_MAX_DEVICES;deviceNumber++ )
    {
      slotNumber.u.bits.DeviceNumber = deviceNumber;
      for ( functionNumber = 0; functionNumber < PCI_MAX_FUNCTION; functionNumber++ )
      {
          slotNumber.u.bits.FunctionNumber = functionNumber;
          if (!HalGetBusData(PCIConfiguration,
                          busNumber,
                          slotNumber.u.AsULONG,
                          &pciData,
                          sizeof(ULONG)
                          ) )
          {
               deviceNumber = PCI_MAX_DEVICES;
                break;
          }
          if (pciData.VendorID == PCI_INVALID_VENDORID )
          {  
              continue;
          }
          if ( ( VendorId != PCI_INVALID_VENDORID ) &&
             ( pciData.VendorID != VendorId || pciData.DeviceID != DeviceId ))
          {
               continue;
          }
          pPciDeviceLocation->BusNumber = busNumber;
          pPciDeviceLocation->SlotNumber = slotNumber;
          pPciDeviceLocation = &PciDeviceList->List[++count];
          status = STATUS_SUCCESS;
     }  
   }  
}  
 
    // 获取设备的资源列表数据指针
    status = HalAssignSlotResources(RegistryPath,
                                    &pDevExt->ClassUnicodeString,
                                    DriverObject,
                                    DeviceObject,
                                    pDevExt->InterfaceType,
                                    pDevExt->BusNumber,
                                    pDevExt->SlotNumber,
                                    &pCmResourceList
                                    );
  
2.3 I/O端口访问
在PC机上,I/O寻址方式与内存寻址方式不同,所以处理方法也不同。I/O空间是一个64K字节的寻址空间,I/O寻址没有实模式与保护模式之分,在各种模式下寻址方式相同。在Windows NT下,系统不允许处于Ring3级的用户程序和用户模式驱动程序直接使用I/O指令,对I/O端口进行访问,任何对I/O的操作都需要借助内核模式驱动来完成。在访问I/O端口时,使用READ_PORT_XXX与WRITE_PORT_XXX函数来进行读写。I/O端口基地址使用从配置空间基址寄存器PCI Base Address 1中返回的I/O端口基地址。
示例代码如下:
RegValue = READ_PORT_ULONG(pBaseAddr+RegOffSet);
WRITE_PORT_ULONG(pBaseAddr+ RegOffset, RegValue);
2.4设备内存访问
Winsows工作在32位保护模式下,保护模式与实模式的根本区别在于CPU寻址方式上的不同,这也是Windows驱动程序设计中需要着重解决的问题。Windows采用了分段、分页机制,使得一个程序可以很容易地在物理内存容量不一样的、配置范围差别很大的计算机上运行,编程人员使用虚拟存储器可以写出比任何实际配置的物理存储器都大得多的程序。每个虚拟地址由16位的段选择字和32位段偏移量组成。通过分段机制,系统由虚拟地址产生线性地址。再通过分页机制,由线性地址产生物理地址。线性地址被分割成页目录(Page Directory)、页表(Page Table)和页偏移(Offset)三个部分。当建立一个新的Win32进程时,操作系统会为它分配一块内存,并建立它自己的页目录、页表,页目录的地址也同时放入进程的现场信息中。当计算一个地址时,系统首先从CPU控制器CR3中读出页目录所在的地址,然后根据页目录得到页表所在的地址,再根据页表得到实际代码/数据页的页帧,最后再根据页偏移访问特定的单元。硬件设备读写的是物理内存,但应用程序读写的是虚拟地址,所以存在着将物理内存地址映射到用户程序线性地址的问题。
从物理内存到线性地址的转换是驱动程序需要完成的工作,可以在初始化驱动程序的进行。在已经获得设备的存储器基地址后,首先调用HalTranslateBusAddress()函数将总线相关的内存地址转换成系统的物理地址,然后调用MmMapIoSpace()函数将系统的物理地址映射到线性地址空间。在需要访问设备内存时,调用READ_REGISTER_XXX()与WRITE_REGISTER_XXX ()函数来进行,基地址使用前面映射后的线性地址。在设备卸载时,调用MmUnmapIoSpace()断开设备内存与线性地址空间的映射。
示例代码如下:
HalTranslateBusAddress(InterfaceType,
                     BusNumber,
                     BaseAddress->RangeStart,
                     &addressSpace,         
                     &cardAddress
                     ))
BaseAddress->MappedRangeStart = MmMapIoSpace(cardAddress,
                                            BaseAddress->RangeLength,
                                            MmCached
                                            );
……
RegValue = READ_REGISTER_ULONG(pRegister);
WRITE_REGISTER_ULONG(pRegister, pInBuf->RegValue);
……
MmUnmapIoSpace(pBaseAddress->MappedRangeStart, pBaseAddress->RangeLength );
2.5中断处理
中断的设置、响应与调用在驱动程序中完成。设置中断应该在设备创建时完成,使用从CmResourceTypeInterrupt描述符中提取的参数,先调用HalGetInterruptVector()将与总线有关的中断向量参数转换为系统的中断向量,然后调用IoConnectInterrupt()指定中断服务,注册中断服务函数ISR(Interrupt Service Routine)的函数指针。当硬件设备产生中断时,系统会自动调用ISR函数来响应中断。ISR函数运行的中断请求级较高,主要完成对硬件设备中断的清除,不适合执行过多的代码。在传输大块数据时,需要使用延迟过程调用(Delay Process Call,DPC)机制。例如,使用PCI设备进行DMA通信时,在ISR函数中完成对指定设备中断的判断以及清除中断,在退出ISR前,调用DPC函数;在DPC函数中,完成DMA通信的过程,并将数据返回给用户程序。
示例代码如下:
DeviceExtension->InterruptLevel = partialData->u.Interrupt.Level;
DeviceExtension->InterruptVector = partialData->u.Interrupt.Vector;
DeviceExtension->InterruptAffinity = partialData->u.Interrupt.Affinity;
if (partialData->Flags & CM_RESOURCE_INTERRUPT_LATCHED)
{
DeviceExtension->InterruptMode = Latched;
}
else
{               
DeviceExtension->InterruptMode = LevelSensitive;
}               
……
vector = HalGetInterruptVector(pDevExt->InterfaceType,
                          pDevExt->BusNumber,
                          pDevExt->InterruptLevel,
                          pDevExt->InterruptVector,
                          &irql,
                          &affinity
                          );
status = IoConnectInterrupt(&pDevExt->InterruptObject,
                      (PKSERVICE_ROUTINE)PciDmaISR,
                      DeviceObject, 
                      NULL,
                      vector,
                      irql,
                      irql,
                      pDevExt->InterruptMode,
                      TRUE,
                      affinity,
                      FALSE
                      );
 
 
2.6DMA通信过程
DMA通信在驱动程序中实现,需要多个例程才能完成一次DMA通信。
1)  DriverEntry例程
构造DEVICE_DESCRIPTION结构,并调用HalGetAdapter,找到与设备关联的Adapter对象,并将返回的Adapter对象的地址和映射寄存器的数目保存在设备扩展的数据结构中。
示例代码:
   // 申请DMA的适配器对象
deviceDescription.Version = DEVICE_DESCRIPTION_VERSION;
    deviceDescription.Master = TRUE;
    deviceDescription.ScatterGather = pDevExt->ScatterGather;
    deviceDescription.DemandMode = FALSE;
    deviceDescription.AutoInitialize = FALSE;
    deviceDescription.Dma32BitAddresses = TRUE;
    deviceDescription.BusNumber = pDevExt->BusNumber;
    deviceDescription.InterfaceType = pDevExt->InterfaceType;
    deviceDescription.MaximumLength = pDevExt->MaxTransferLength;
 
    pDevExt->AdapterObject = HalGetAdapter(&deviceDescription,
                                        &numberOfMapRegisters
                                        );
    ……
2)Start I/O例程
该例程请求Adapter对象的拥有权,然后把其余的工作留给AdapterControl回调例程。
a)         调用KeFlushIoBuffers从CPU的Cache把数据清到物理内存,然后计算映射寄存器的数目和用户缓冲区的大小,及在第一次设备操作中传输的字节数。
b)        调用MmGetMdlVirtualAddress,从MDL中恢复用户缓冲区的虚地址,并存入设备扩展数据结构中。
c)        调用IoAllocateAdapterChannel请求Adapter对象的拥有权。如果调用成功,其余的设置工作由AdapterControl例程去做;如果失败了,则完成本次IRP包处理,开始处理下一个IRP。
3) AdapterControl例程
该例程完成初始化DMA控制器,并启动设备的工作。
a)       调用IoMapTransfer,装入Adapter对象的映射寄存器。
b)      向设备发送合适的命令开始传输操作。
c)      返回值KeepObject保留Adapter对象的拥有权。
4)中断服务(ISR)例程
在设备中断时,由系统调用。
a)       向硬件设备发出中断响应的指令。
b)      调用IoRequestDpc在驱动程序的DpcForIsr中继续处理该请求。
c)      返回TRUE,表示已经服务了本次中断。
5)DpcForIsr例程
由ISR在每个部分数据传输操作的结束时触发,完成当前IRP请求。
a) 调用IoFlushAdapterBuffers,清除Adapter对象的Cache中的任何剩余数据。
b) 调用IoFreeMapRegisters,释放所使用的映射寄存器。
c) 检查有未传完的剩余数据,如果有,则计算下次设备操作中需要传输的字节数,调用IoMapTransfer重设映射寄存器,并启动设备;如果没有剩余数据,则完成当前IRP请求,并开始下一个请求。
2005年12月05日

物理地址扩展 (PAE) X86 概述
物理地址扩展 (PAE) X86 允许软件使用地址窗口扩展 (AWE) API 集并在具有 Intel Pentium Pro 或更高版本处理器的计算机上运行,而 4 GB 以上物理内存允许将更多物理内存映射为应用程序的虚拟地址空间。

不使用 AWE API 集的应用程序也可以从 PAE X86 获益,因为操作系统使用更大的物理内存来减小页面,从而提高了性能。同样,这还使驻留多个应用程序的合并服务器受益。

通过在内存中而不是在磁盘上保存数据,可提高操纵大量数据的应用程序的性能。例如,PAE X86 可显著提高以下类型应用程序的性能:

• 数据库,如 Microsoft SQL/E 7.0 或更高版本。
 
• 科学和工程应用程序,如计算流体动力学的应用程序。
 
• 执行大量数据采集的统计分析应用程序。
 

更新到 PAE X86
下列更新已被执行,从而支持增加数据执行保护 (DEP)(也称为非执行页面保护):

• 在采用支持非执行页面保护功能的处理器的计算机上启用 DEP 时,在运行装有 Service Pack 1 的 Windows Server 2003 和装有 Service Pack 2 的 Windows XP 的计算机上将自动启用 PAE。
 
• 在装有 SP1 的标准版 Windows Server 2003 和装有 SP2 的 Windows XP 上启用 PAE 模式时,物理地址空间将限制在 4 GB。将物理地址空间限制在 4 GB 有助于防止 PAE 模式出现驱动程序兼容性问题。
 

只有特定硬件才支持 PAE X86,因此在最初安装该操作系统时不启用此功能。有关哪些硬件支持 PAE X86 的详细信息,可以通过单击支持资源中的相应链接,查询有关 Windows Server 2003 家族中产品的硬件兼容性信息。有关如何启用 PAE X86 的详细信息,请参阅启用物理地址扩展 (PAE)。

1.

在根文件夹(如 C:)下查找 Boot.ini 文件并删除它的只读属性。

2.

打开 Boot.ini 文件,然后将 /PAE 参数添加到 ARC 路径中,如以下 Windows Server 2003 Datacenter Edition 示例所示:

multi(0)disk(0)rdisk(0)partition(2)\%systemroot%="Windows Server 2003, Datacenter Edition" /PAE

3.

在“文件”菜单上,单击“保存”。

4.

还原 Boot.ini 文件的只读属性。

5.

为使更改生效,请重新启动计算机。

如果要禁用PAE,把C:\Boot.ini中的/noexecute改为/execute就好了!!

使用PAE的分页机制的36位物理寻址

PAE分页机制以及对36bit物理寻址的支持,是在IA32架构的奔腾pro处理器中采用的。在IA32处理器中实现这个特性是通过CPUID指令的特性标志PAE(当CPUID指令的源操作数是2时,EDX寄存器的bit6就是这个特性标志)。CR4中的物理地址扩展(PAE)标志可以开启PAE机制,将物理地址从32bit扩展至36bit。处理器提供额外的4个地址线引脚来容纳这额外的地址位。为了能使用这个选项,必须设置如下的标志:
 CR0寄存器中的PG标志(bit 31)-开启分页
 CR4寄存器中的PAE标志(bit5)置位,开启PAE分页机制。
当开启PAE分页机制时,处理器支持两种尺寸的页:4KB和2MB。当使用32bit寻址时,这两种尺寸的页都能够使用同一个页表集来寻址(也就是说,一个页目录项可以指向一个2MB的页,也可以指向一个页表,这个页表的表项指向4KB的页)。要支持36bit的物理地址,分页的数据结构需要做如下的变化:
 页表项将变为64bit以适应36bit物理地址。每个4KB页的页目录和页表也就可以有最多512个表项了。
 线性地址变换的层次中,一个叫做页目录指针表的新表将被加入。这个表有4个64bit的表项。在线性变换的层次中,这个表在页目录之上。随着物理地址扩展机制的开启,处理器支持4个页目录。
 寄存器CR3(PDPR)中20bit的页目录基地址被27bit 的页目录指针表基地址所替代(见图3-17)(此时,寄存器CR3叫做PDPTR)。这个域给出了页目录指针表基地址的高27位,这就迫使页目录指针表的地址是32byte对齐的。
 线性地址变换允许将32bit的线性地址映射到更大的物理地址空间中。

 
3.8.1.开启PAE时的线性地址变换(4KB页)
图3-18显示了当启用PAE分页机制进行线性地址到4KB页映射时,页目录指针表,页目录和页表的层次结构。这种分页方法可以寻址高达220个页,线性地址空间达232byte(4GB)。
为了选择各种表项,线性地址被分为3部分:
 页目录指针表项-bit30到bit31,给出了该页目录指针表项在页目录指针表中的偏移量。被选中的表项给出了一个页目录的基地址。
 页目录项-bit21到bit29,给出了在被选中的页目录中的偏移量。被选择的目录项给出了一个页表的基地址。
 页表项-bit12到bit20,给出了在被选中的页表中的偏移量。被选中的页表项给出了一个页在内存中的物理基地址。
 页偏移量-bit0到bit11,给出了在被选中的页中的偏移量。
 
3.8.2启用PAE的线性地址变换(2MB 页)
图3-19显示了当启用PAE分页机制时,如何使用页目录指针表和页目录将线性地址映射到2MB的页。这种分页方法可以将2048个页(4个页目录指针表项乘上512个页目录项)映射到4GB的线性地址空间上。

当启用PAE时,通过设置页目录项中的页尺寸(PS)标志(见图3-14)。(如表3-3中所示,当启用PAE时,CR4寄存器中的PSE标志将对页的尺寸不起作用)。一旦PS标志被置位,线性地址被分为3部分:
 页目录指针表项-bit30到bit31,给出了一个页目录指针表项在页目录指针表中的偏移量。该页目录指针表项给出了一个页目录的基地址。
 页目录项-bit21到bit29,给出了一个页目录项在页目录中的偏移量。该页目录项给出了一个2MB页的基地址。
 页偏移量-bit0到bit20,给出了该地址在页中的偏移量。
 
3.8.3使用扩展的页表结构来访问完全扩展的物理地址空间
The page-table structure described in the previous two sections allows up to 4 GBytes of the 64 GByte extended physical address space to be addressed at one time. Additional 4-GByte sections of physical memory can be addressed in either of two way:
 将寄存器CR3中的指针改为指向另外一个页目录指针表,这个指针表又指向另外一个页目录和页表集合。
 改变页目录指针表的表项,使其指向另外一个页目录,这个页目录又会指向另外一个页表集合。
3.8.4.启用扩展寻址后的页目录项和页表项
图3-20显示了当使用4KB页,使用了36bit扩展物理地址时,页目录指针表项,页目录项和页表项的格式。图3-21当使用2MB和36bit扩展物理地址时,页目录指针表项和页目录项的格式。这些表项中的标志功能与3.7.6节 “页目录项和页表项”中描述的功能是一样的,其中主要的不同之处如下:
 增加的页目录指针表项
 表项的大小从32bit增加到了64bit
 页目录和页表的最多项数为512个
 每个项中,物理基地址域扩展到了24bit

注意
现行的实现了PAE机制的IA-32处理器在装载页目录指针表项时,使用非缓存访问。这种行为是模式特定行为,而非架构特定行为。未来的IA-32处理器也许会缓存页目录指针表项
 
 
根据表项的不同,表项中的物理基地址的说明如下:
 在页目录指针表项中,基地址是一个4KB页目录的第一个byte的物理地址
 在页目录项中,基地址是一个4KB页表或2MB页的第一个byte的物理地址
 在页表项中,基地址是一个4KB页的第一个byte的物理地址
在所有的表项中(除了指向2MB页的页目录项),基地址都被视为36bit物理地址的高24位,这就迫使页表和页都是4KB对齐的(这样,36bit物理地址的低12位都为0)。当页目录项指向一个2MB的页时,基地址被视为36bit物理地址的高15位,这就迫使2MB的页都是2MB对齐的(这样,36位物理地址的低21位为0)。

页目录指针表项的存在标志位,可以为0,也可以为1。如果存在标志被清零,页目录指针表的余下bits可以为操作系统所用。如果存在标志被置位(1),那么页目录指针表项就如图3-20(4KB页)和图3-21(2MB)所示。

页目录项中的页尺寸标志(bit7)可以判断该表项指向一个页表还是指向一个2MB的页。当该标志被清零时,表项指向一个页表;当该标志被置位时,表项指向一个2MB的页。这个标志使得4KB和2MB的页在一个页表集合中混用。

访问标志(A)(bit5)和脏标志(D)(bit6)供指向页的表项使用。

所有物理地址扩展表项的bit9,10和11都可为软件所用。(当“存在位”为0时,bit1到bit63都可以为软件所用)图3-14中所有被标为“保留”或“0”的bit都应当被置为0并且不能被软件访问。当控制寄存器CR4中的PSE和PAE标志被置位,而页目录项和页表项中的保留bit没有被置为0,处理器产生一个页错误(#PF);如果页目录指针表项中的保留bit没有被置为0,处理器会产生一个一般错误(#GP)。

  inline 关键字用来定义一个类的内联函数,引入它的主要原因是用它替代C中表达式形式的宏定义。表达式形式的宏定义一例:

   #define ExpressionName(Var1,Var2) (Var1+Var2)*(Var1-Var2)

为什么要取代这种形式呢?
  1. 首先谈一下在C中使用这种形式宏定义的原因,C语言是一个效率很高的语言,这种宏定义在形式及使用上像一个函数,但它使用预处理器实现,没有了参数压栈,代码生成等一系列的操作,因此,效率很高,这是它在C中被使用的一个主要原因。
  2. 这种宏定义在形式上类似于一个函数,但在使用它时,仅仅只是做预处理器符号表中的简单替换,因此它不能进行参数有效性的检测,也就不能享受C++编译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用就存在着一系列的隐患和局限性。
     3. inline 推出的目的,也正是为了取代这种表达式形式的宏定义,它消除了它的缺点,同时又很好地继承了它的优点。

为什么inline能很好地取代表达式形式的预定义呢?

对应于上面的1-3点,阐述如下:
  1. inline 定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换,(像宏一样展开),没有了调用的开销,效率也很高。 
  2. 很明显,类的内联函数也是一个真正的函数,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。 
  3. inline 可以作为某个类的成员函数,当然就可以在其中使用所在类的保护成员及私有成员。 

在何时使用inline函数:
  首先,你可以使用inline函数完全取代表达式形式的宏定义。另外要注意,内联函数一般只会用在函数内容非常简单的时候,这是因为,内联函数的代码会在任何调用它的地方展开,如果函数太复杂,代码膨胀带来的恶果很可能会大于效率的提高带来的益处。