共享 DLL 中的代码页的实验(摘自Undocument NT)

    技术2022-05-20  38

    Listing 4-1: SHOWDIR.C /* Should be compiled in release mode to run properly */#include <windows.h>#include <string.h>#include <stdio.h>#include "gate.h"/* Global array to hold the page directory */DWORD PageDirectory[1024];这是 SHOWDIR.C 文件的开头部分,除了头文件的包含,还包括了用于保存页表的全局变量数组的定义。 所包含的头文件 GATE.H 是很有意思的。这个头文件里有使用调用门机制的函数的原型。使用调用门机制就可以在内核模式下执行自己的代码,而不用写新的驱动程序。我们在这个样例程序程序中使用此机制是因为页表不能从用户模式代码下访问。现在只要知道此机制使得一般可执行文件中的函数能在内核模式下执行就足够了。转到页表的定义上来,我们已经讲到了每个页表项的大小是4字节,一个页目录有1024个表项。因此,PageDirectory 是一个 1024 个 DWORDs的数组。数组中的每一个 DWORD 都代表着相应的目录项。/* C function called from the assembly stub */void _stdcall CFuncGetPageDirectory(){    DWORD *PageDir = (DWORD *)0xC0300000;    int i = 0;    for (i = 0; i < 1024; i++) {        PageDirectory[i] = PageDir[i];    }}CfuncGetPageDirectory() 是通过调用门机制在内核模式下执行的函数。此函数仅仅在用户模式内存区中拷贝一分页目录,这样程序中其它的用户模式的代码就能访问了。页目录被映射在每个进程的虚地址 0xC0300000 中。这个地址是无法在用户模式访问的。CFuncGetPageDirectory() 函数从地址 0xC0300000 处拷贝 1024 个 DWORDs 到全局变量 PageDirectory 中,这个变量可由程序中的用户模式代码访问。/* Displays the contents of page directory. Starting* virtual address represented by the page directory* entry is shown followed by the physical page* address of the page table*/void DisplayPageDirectory(){    int i;    int ctr = 0;    printf("Page directory for the process, pid=%x/n", GetCurrentProcessId());    for (i = 0; i<1024; i++) {        if (PageDirectory[i] & 0x01) {            if ((ctr % 3) == 0) {                printf("/n");            }            printf("x:x ", i << 22, PageDirectory[i] & 0xFFFFF000);            ctr++;        }    }    printf("/n");}DisplayPageDirectory() 函数在用户模式下运行并打印出由 CfuncGetPageDirectory() 初始化的 PageDirectory 数组。函数检查每一个表项的最低位(Least Significant Bit,LSB)。仅当最低位置位时页表项才有效。对于无效表项函数跳过不打印。 函数每行打印3个页表项,换句话说,每三个表项后打印一个换行字符。对每个目录项都打印其逻辑地址,所打印的相应的页表地址则是从页目录中取得的。如前所述,逻辑地址的前10位(或者说最高10位)用作页目录的索引。换句话说,索引值为 i 的页表项代表着前10位为 i 的逻辑地址。对于每一个目录项,函数打印逻辑地址范围的基址。这个基地址(即范围中最低的地址)的低22位(或叫 22 LSBs)为 0。此基地址是由索引值 i 左移至前10位得到的。对应于逻辑地址的页表的地址保存在页目录项的前20位(或叫 20 MSBs)中。12 LSBs 为页目录项的标志。函数通过掩闭标志位来计算页表地址。main(){WORD CallGateSelector;int rc;static short farcall[3];/* Assembly stub that is called through callgate */extern void GetPageDirectory(void);/* Creates a callgate to read the page directory* from Ring 3 */rc = CreateCallGate(GetPageDirectory, 0, &CallGateSelector);if (rc == SUCCESS) {farcall[2] = CallGateSelector;_asm {call fword ptr [farcall]}DisplayPageDirectory();getchar();/* Releases the callgate */rc=FreeCallGate(CallGateSelector);if (rc!=SUCCESS) {printf("FreeCallGate failed, ""CallGateSelector=%x, rc=%x/n",CallGateSelector, rc);}} else {printf("CreateCallGate failed, rc=%x/n", rc);}return 0;}main() 开始先创建调用门,使得 GetPageDirectory() 函数在内核模式下执行。GetPageDirectory() 用汇编语言编写,是 RING0.ASM 的一部分。CreateCallGate() 函数被程序用来创建调用门,它是由 CALLGATE.DLL 提供的。函数返回一个调用门描述符。

    我们将几个要点在这里快速地过一下。由 CreateCallGate() 返回的调用门描述符是所给函数的一个段选择子:这个函数就是 GetPageDirectory()。 为了调用由调用门选择子指向的函数,需要使用一个远程 call 指令。这个远程调用指令需要一个16位的段选择子和一个32位的段内偏移。当通过调用门调用时,偏移并不起作用。处理器总是跳转到由调用门指向的函数的起始点。因此,程序只初始化 farcall 数组的第3个成员,该成员对应着段选择子。通过调用门发出调用就使得执行控制传到了 GetPageDirectory() 函数。此函数调用 CfuncGetPageDirectory(),将页目录拷贝到 PageDirectory 数组。调用门调用返回后,程序调用 DisplayPageDirectory() 打印拷贝到 PageDirectory 中的页目录。程序释放调用门后退出。Listing 4-2: RING0.ASM .386.model small.codeinclude ../include/undocnt.incpublic _GetPageDirectoryextrn _CFuncGetPageDirectory@0:near;Assembly stub called from callgate_GetPageDirectory procRing0Prologcall _CFuncGetPageDirectory@0Ring0Epilogretf_GetPageDirectory endpEND通过调用门调用的函数需用汇编语言编写,这其中有几个原因。第一,函数需要执行一个 prolog 和一个 epilog,这两个都是汇编语言的宏,用于启用内核模式的分页。第二,函数在最后需要一个远程返回。函数将剩下的工作交给用 C 语言写的 CFuncGetPageDirectory() 函数。如果比较 showdir 程序对两个不同进程的输出,会发现两进程页表目录的高半部除两个表项外几乎完全相同。 换句话说,对应与这两个表项的内核地址空间并未被两进程共享。

    我们来一次一步地分析,为什么这两个 entries 会有所不同。页表本身需要被映射到某个线性地址。当 Windows NT 需要访问页标时,就使用这一线性地址区。为了表达 4GB 内存分成了每页 4KB 的 1M 页,就需要 1K 个页表,每个页表有 1K 个页表项。为了映射这 1K 个页表,Windows NT 在每一个进程中都保留了 4MB 的线性地址范围。正如前面所讲,每一个进程都有一组不同的页表,无论是那个进程,Windows NT 都要把页表映射到范围从 0xC0000000 至 0xC03FFFFF 的线性地址中。 我们将这个线性地址范围称为页表地址范围。换句话说,不同进程的页表地址范围映射入了不同的页表——即映射到不同的物理页。也许已经注意到,页表的地址范围位于内核地址空间的范围内。Windows NT 不能将这个关键的系统数据映射进用户地址空间让用户进程操纵内存。 最后的结果就是进程不能共享页表地址范围中的页,尽管这些地址都在内核地址空间里。准确地讲,一个页表需要映射 4MB 的地址空间是因为每个页表有 1K 个页表项而每个页表项又对应着一个 4KB 的页。结果,Windows NT 不能共享对应于页表地址范围的页表。这就解释了页表目录中两个神秘表项中的一个。 然而,这个表项的神秘性并没有就此终结。有这个表项所指定的物理页与页表目录的物理地址一致。显然的结论就是对于页表地址范围,页表目录也扮演着页表的角色。 这是可以做到的,因为在 80386 上页表目录表项的格式与页表项 PTE 是一样的。


    最新回复(0)