welfear 2009年08月26日 星期三 18:42 | 2765次浏览 | 4条评论
Windows物理内存的故事
文档名称:Windows物理内存的故事
文档维护:welfear
创建时间:2009年6月2日
我想我还没有完全搞清楚事情的全部经过,但至此我应该可以满足我的好奇心。
WinHex无法全部读取系统中的全部物理内存是十分正常的现象。在这之前,我们先把问题转换一下,
WinHex是使用\Device\PhysicalMemory来读取系统物理内存的(这一点不难证明,即使WinHex没有开源),
所以问题就变成了通过内核对象\Device\PhysicalMemory无法访问全部物理内存。
所以我想还是先从这个内核对象入手吧。
首先要弄明白这个内核对象是怎么回事。在MmInitSystem中,也就内存子系统初始化时会调用
函数MiSectionInitialization。在MiSectionInitialization中,系统会创建Section Object Type。
在建立Section对象后,第一件事就是创建\Device\PhysicalMemory对象。这个对象就是用来映射物理内存的。
而它的ControlArea和其它Section对象是有区别的,体现在
ControlArea->u.Flags.PhysicalMemory = 1;
这句上。当打开这个对象时,PhysicalMemory是不为零的。这样接下来再使用NtMapViewOfSection来映射内存时,
在NtMapViewOfSection->MmMapViewOfSection中就会通过ControlArea->u.Flags.PhysicalMemory是否为零来
选择调用MiMapViewOfPhysicalSection或是MiMapViewOfImageSection。显然,在映射物理内存时会调用
MiMapViewOfPhysicalSection。在此之前还有一处检测ControlArea->u.Flags.PhysicalMemory的代码:
if (Section->Segment->ControlArea->u.Flags.PhysicalMemory) {
HighestPhysicalAddressInPfnDatabase = (ULONGLONG)MmHighestPhysicalPage << PAGE_SHIFT;
CapturedOffset.LowPart = CapturedOffset.LowPart & ~(PAGE_SIZE - 1);
if (PreviousMode != KernelMode) {
if ((ULONGLONG)(CapturedOffset.QuadPart + CapturedViewSize) > HighestPhysicalAddressInPfnDatabase) {
Status = STATUS_INVALID_PARAMETER_6;
goto ErrorReturn;
}
}
}
显然,当用户态请求的映射范围超出了MmHighestPhysicalPage指示的系统内存页时就会发生参数错误而返回。
至此,我们的注意力又转向全局变量MmHighestPhysicalPage的初始化上。在研究这个问题之前我们简单描述下
整个系统的初始化过程。
绕过远古时期的MBR、BOOT Sector之流,直接来到ntldr。ntldr由16位的ntldr.com和32位的osloader.exe两部分组成。
在16位的引导代码中,系统会通过系统BIOS收集一些硬件数据为以后做准备。与Linux引导类似,Windows NT也是通过
BIOS中断int 15h来收集系统内存的位图信息。下面是freeloader中的代码:
00144 static ULONG
00145 PcMemGetBiosMemoryMap(PBIOS_MEMORY_MAP BiosMemoryMap, ULONG MaxMemoryMapSize)
00146 {
00147 REGS Regs;
00148 ULONG MapCount;
00152 /* Int 15h AX=E820h
00153 * Newer BIOSes - GET SYSTEM MEMORY MAP
00154 *
00155 * AX = E820h
00156 * EAX = 0000E820h
00157 * EDX = 534D4150h ('SMAP')
00158 * EBX = continuation value or 00000000h to start at beginning of map
00159 * ECX = size of buffer for result, in bytes (should be >= 20 bytes)
00160 * ES:DI -> buffer for result
00161 * Return:
00162 * CF clear if successful
00163 * EAX = 534D4150h ('SMAP')
00164 * ES:DI buffer filled
00165 * EBX = next offset from which to copy or 00000000h if all done
00166 * ECX = actual length returned in bytes
00167 * CF set on error
00168 * AH = error code (86h)
00169 */
00170 Regs.x.eax = 0x0000E820;
00171 Regs.x.edx = 0x534D4150; /* ('SMAP') */
00172 Regs.x.ebx = 0x00000000;
00173 Regs.x.ecx = sizeof(BIOS_MEMORY_MAP);
00174 Regs.w.es = BIOSCALLBUFSEGMENT;
00175 Regs.w.di = BIOSCALLBUFOFFSET;
00176 for (MapCount = 0; MapCount < MaxMemoryMapSize; MapCount++)
00177 {
00178 Int386(0x15, &Regs, &Regs);
00187 /* If the BIOS didn't return 'SMAP' in EAX then
00188 * it doesn't support this call */
00189 if (Regs.x.eax != 0x534D4150)
00190 {
00191 break;
00192 }
00194 /* Copy data to caller's buffer */
00195 RtlCopyMemory(&BiosMemoryMap[MapCount], (PVOID)BIOSCALLBUFFER, Regs.x.ecx);
00203 /* If the continuation value is zero or the
00204 * carry flag is set then this was
00205 * the last entry so we're done */
00206 if (Regs.x.ebx == 0x00000000 || !INT386_SUCCESS(Regs))
00207 {
00208 MapCount++;
00209 DPRINTM(DPRINT_MEMORY, "End Of System Memory Map!\n\n");
00210 break;
00211 }
00213 /* Setup the registers for the next call */
00214 Regs.x.eax = 0x0000E820;
00215 Regs.x.edx = 0x534D4150; /* ('SMAP') */
00216 /* Regs.x.ebx = 0x00000001; Continuation value already set by the BIOS */
00217 Regs.x.ecx = sizeof(BIOS_MEMORY_MAP);
00218 Regs.w.es = BIOSCALLBUFSEGMENT;
00219 Regs.w.di = BIOSCALLBUFOFFSET;
00220 }
00222 return MapCount;
00223 }
代码中的注释已经可以说明通过int 15h获得E820位图信息的方法了,代码中还有作者的实践经验:-)。
在获得系统内存状态之后,在osloader.exe获得控制权时会获得引导参数指针。也就是在跳入
NtProcessStartup时,之后调用KiRosBuildOsMemoryMap转换引导参数收集的系统E820内存位图为
MemoryDescriptorList的形式。相关代码在reatcos\ntoskrnl\ke\freeldr.c中。
经过osloader.exe的引导后,Windows NT内核获得了控制权。在跳入KiSystemStartup时同样也
传入了前一模块的引导信息参数指针。在轮到内存子系统初始化时,系统调用了MiInitMachineDependent。
在MmInitSystem->MiInitMachineDependent中:
if ((MemoryDescriptor->MemoryType != LoaderFirmwarePermanent) &&
(MemoryDescriptor->MemoryType != LoaderBBTMemory) &&
(MemoryDescriptor->MemoryType != LoaderSpecialMemory)) {
if (MemoryDescriptor->MemoryType != LoaderBad) {
MmNumberOfPhysicalPages += MemoryDescriptor->PageCount;
}
if (MemoryDescriptor->BasePage < MmLowestPhysicalPage) {
MmLowestPhysicalPage = MemoryDescriptor->BasePage;
}
if ((MemoryDescriptor->BasePage + MemoryDescriptor->PageCount) >
MmHighestPhysicalPage) {
MmHighestPhysicalPage =
MemoryDescriptor->BasePage + MemoryDescriptor->PageCount - 1;
}
}
这样MmHighestPhysicalPage就被初始化为非Firmware相关的最大内存数。
这就是通过WinHex使用的用户态请求的\Device\PhysicalMemory内存映射就这样无法访问全部内存的原因。
此外通过MmMapIoSpace是可以访问全部内存的,不然怎么读取ACPI Table呢?
当然如果当初我仔细读过毛老师的《Linux内核情景分析》中关于Linux启动的代码分析,那也就不会产生如此
愚蠢的问题了。
Zeuux © 2024
京ICP备05028076号
回复 電波系山寨文化科学家 2009年08月31日 星期一 20:38
回复 welfear 2009年08月31日 星期一 22:16