查看“Detecting Memory (x86)”的源代码
←
Detecting Memory (x86)
跳到导航
跳到搜索
因为以下原因,您没有权限编辑本页:
您请求的操作仅限属于该用户组的用户执行:
用户
您可以查看和复制此页面的源代码。
{{Bias}} 操作系统初始化自身所需的最重要信息之一是对于机器上可用RAM的映射。(译者注:本文讨论了操作系统如何检测计算机有多少实际物理内存,并找到它们的访问地址的实现,建议同时参考阅读[[Memory_Map_(x86)|x86内存映射]]。本文分成了独立的全部原理讲解和全部代码示例两部分,如果你只关心一种做法,可以跳过一部分,前后结合着读。) 从根本上说,操作系统获取该信息的最佳方式是使用BIOS。 可能有一些罕见的机器,你别无选择,只能尝试自己检测内存 --然而,在任何其他情况下这样做都是不明智的。 好像应该完全有理由对自己说,“BIOS怎么检测RAM?我就怎么做。” 不幸的是,答案令人失望:<br> 大多数BIOS在检测到安装的RAM的类型、检测每个内存模块的大小、然后配置芯片组以使用检测到的RAM之前,不能使用任何RAM。 所有这些都取决于芯片组特定的方法,并且通常记录在内存控制器 (北桥) 的数据表中。 在此过程中,RAM不能用于运行程序。 BIOS最初是从ROM运行的,因此它可以使用RAM芯片玩它自己必要的把戏。 但是从任何其他程序内部完全不可能做到这一点。 我们也合理的希望能回收从0xA0000到0xFFFFF的内存,并让RAM地址连续(译者注:这段区域是BIOS开机会分配的一段内存,详见[[Memory_Map_(x86)|x86内存映射]])。 答案再次令人失望:<br> 忘了它吧。 这段内存其中一些SMM或ACPI可能要经常使用。 其中一些可能会多次需要,即使在机器引导之后也是如此。 收回其中的一部分将需要大量的主板或芯片组特定的控制。 <i>是</i>可以编写一个“芯片组驱动程序”,允许你收回一点。 然而,几乎可以肯定的是,将其全部收回是不可能的。 总的来说,这点微小的结果不值得付出努力。 可能需要注意的是,所有PC机都需要一个低于4GB地址的内存孔(memory hole),用于额外的内存映射硬件(包括实际的BIOS ROM)。 因此,对于RAM>3G的机器,主板/芯片组/BIOS可能会将某些RAM(会与映射的硬件重叠)映射到4G以上-- 可以使用 [[PAE]] 或 [[Long Mode|长模式]] 访问它。 有关内存的一般布局,请参见[[Memory Map (x86)|内存映射(x86)]]。 ==检测下层内存(Low Memory)== “Low Memory” 是1MB以下的可用RAM,通常在640KB以下。 有两个[[Memory Map (x86)#BIOS功能|BIOS功能]]可以获得它的大小。 INT 0x12:INT 0x12调用将返回AX = 总KB数。 AX值从0到EBDA的底部 (当然,你可能也不应该使用空间的前0x500字节- 也就是IVT或BDA部分)。 用法: <source lang="asm"> ; 清除进位标志(carry flag) clc ; 切换到BIOS(=请求low memory大小) int 0x12 ; 如果失败,则设置进位标志 jc .Error ; AX = 从0开始的以KB为单位的连续内存量。 </source> 注意:此功能应始终存在,并且可能不会修改进位标志。 如果仿真器不支持它,进位标志将被设置1,指示错误。 或者,你可以只要使用INT 0x15,EAX = 0xE820 (请参见下文)。 ==检测上层内存(Upper Memory)== =BIOS功能:INT 0x15, EAX = 0xE820= 另请参阅: [http://www.uruk.org/orig-grub/mem64mb.html] 到目前为止,检测PC内存的最佳方法是使用INT 0x15,EAX = 0xE820命令。 此功能适用于2002年以后生产的所有PC,以及在此之前的大多数现有PC。 它是唯一可以检测4G以上内存区域的BIOS功能。 它是BIOS的终极内存检测功能。 实际上,此功能返回一个未排序的列表,该列表可能包含未使用的条目,并且(在极少或不可靠的情况下)可能返回重叠区域。 每个列表条目都存储在ES:DI处的内存中,并且DI等待你去<b>递增</b>。 在20字节版本的条目格式中为2个uint64_t和1个uint32_t, 在ACPI 3.0版,24字节版本的条目格式中增加了一个uint32_t(但没人见过24字节的)。 最好始终将列表条目存储为24字节数量 -- 保留uint64_t对齐,如果没有别的。(确保在每次调用之前将最后一个uint64_t设置为1,以使你的映射与ACPI兼容)。 * 第一个uint64_t = 基地址 *第二个uint64_t = “区域”的长度(如果该值为0,则忽略该条目) * 下一个uint32_t = 区域“类型” ** 类型1: 可用 (普通) RAM **类型2:保留-不可用 ** 类型3:ACPI可回收内存(reclaimable memory) ** 类型4: ACPI NVS(non-volatile非易失)内存 ** 类型5:包含坏内存的区域 * 下一个uint32_t = ACPI 3.0扩展属性位域(如果是24字节版本) ** 扩展属性的位0指示是否应忽略整个条目 (如果该位是清除0的)。 这将是一个巨大的兼容性问题,因为大多数当前的操作系统不会读取这一位,也不会忽略条目。 ** 扩展属性的位1指示条目是否是非易失性的(如果该位已设置1)。 该标准规定,“报告为非易失性存储器可能需要表征以确定其是否适合用作常规RAM。” ** 扩展属性的剩余30位目前尚未定义。 基本用法:<br> 对于该功能的第一次调用,将ES:DI指向列表的目标缓冲区。 清除EBX。 将EDX设置为固定数字0x534D4150。 将EAX设置为0xE820(注意,EAX的上16位应设置为0)。 将ECX设置为24。执行INT 0x15。 如果对该功能的第一次调用成功,EAX将被设置为0x534d4150,并且进位标志(Carry flag)将被清除。 EBX将被设置为一些非零值,必须为下一次功能调用保留这些值。CL将包含实际存储在ES:DI(可能是前面的20)的字节数。 对于功能的后续调用:按列表条目大小递增DI,将EAX重置为0xE820,将ECX重置为24。 当你到达列表末尾时,EBX可能会重置为0。 如果在EBX = 0时再次调用该功能,列表将重新开始。 如果EBX未重置为0,则当你尝试访问最后一个有效条目之后的条目时,该功能将返回进位设置(Carry set 1)。 (有关实现该算法的详细ASM示例,请参见下面的 [[#获取E820内存映射|代码示例]]。) 注意: * 获得列表后,可能需要:对列表进行排序,合并相同类型的相邻范围,将任何重叠区域更改为重叠双方中限制最严格的类型,并将任何无法识别的“类型”值更改为类型2。 * 类型3 “ACPI reclaimable memory(可回收内存)” 区域可以像正常的 “可用RAM” 区域一样使用 (并与之组合),只要你已经使用过存储在那里的ACPI表 (即可以“回收”)。 * 类型2、4、5(保留、ACPI非易失性、坏)标记在分配物理内存时应避免的区域。 * 将未列出的区域视为类型2 -- 保留。 * 你的代码必须能够处理不以任何“页面边界(page boundary)”开始或结束的区域。 在Bochs中调用INT 15h, EAX=E820的典型输出: <pre> 基址 | 长度 | 类型 0x0000000000000000 | 0x000000000009FC00 | Free Memory (1) 0x000000000009FC00 | 0x0000000000000400 | Reserved Memory (2) 0x00000000000E8000 | 0x0000000000018000 | Reserved Memory (2) 0x0000000000100000 | 0x0000000001F00000 | Free Memory (1) 0x00000000FFFC0000 | 0x0000000000040000 | Reserved Memory (2) </pre> ===其他方法=== ====PnP==== 使用即插即用(PnP)调用可以获得相当好的内存映射。{这里缺少描述和代码。} ====SMBIOS==== SMBIOS旨在允许 “管理员” 评估硬件升级选项或维护公司当前正在使用的硬件的目录 (即,它提供信息供人类使用,而不是供软件使用)。 它在许多计算机上可能不会给出可靠的结果 -参见: [http://www.pcpitstop.com/faq/smbios.asp]. SMBIOS将尝试告诉你安装的内存条数量及其大小(MB)。可以从保护模式调用SMBIOS。 然而,一些制造商并没有使他们的系统完全兼容。 例如HP Itanium符合DIG64规范,因此他们的SMBIOS不会返回所有必需的设备类型。 使用这些功能检测内存完全忽略了内存孔/内存映射设备/保留区域的概念。 ====BIOS功能:INT 0x15,AX=0xE881==== 这个功能在实模式下不起作用。 相反,它应该从32位保护模式调用。 它返回与功能E801相同的信息,但使用扩展寄存器(EAX/EBX/ECX/EDX)。 所有1994年以后的设备都应该可用。 有关如何调用它的信息:http://www.ctyme.com/intr/rb-1742.html ==== BIOS功能:INT 0x15, AX = 0xE801==== 此功能自大约1994年以来一直存在,因此从那时到现在的所有系统都应该具有此功能。 它是用来处理15M内存孔的,但会停在下一个孔/内存映射设备/上面的保留区域。 也就是说,它只设计用于处理16M以上的连续内存。 典型输出: <br> AX = CX = 1M到16M之间的扩展内存,单位为K(最大3C00h = 15MB) BX = DX = 16M以上的扩展内存,以64K块为单位 有一些BIOS总是以AX = BX = 0返回。 在这种情况下,请使用CX/DX对。 其他一些BIOS将返回CX = DX = 0。 Linux在INT操作之前将CX/DX对初始化为0,然后使用CX/DX,除非它们仍然为0 (这是它将使用AX/BX)。 在任何情况下,在信任结果之前,最好先对所使用的寄存器中的值进行健全性检查。 (GRUB只信任AX/BX -- 这不太好。) Linux用法: <source lang="asm"> XOR CX, CX XOR DX, DX MOV AX, 0xE801 INT 0x15 ; 请求upper memory大小 JC SHORT .ERR CMP AH, 0x86 ;不支持的功能 JE SHORT .ERR CMP AH, 0x80 ; 无效命令 JE SHORT .ERR JCXZ .USEAX ; CX结果是否无效? MOV AX, CX MOV BX, DX .USEAX: ; AX = 连续KB数,1M到16M ; BX = 16M以上连续64Kb页 </source> ====BIOS功能:INT 0x15, AX = 0xDA88==== 此功能返回可用RAM的连续KiB数,从0x00100000开始,单位为CL:BX,单位为KiB。 这与 “INT 0x15, AX = 0x8A” 非常相似 -如果此功能表示0x00100000处有14个MiB的RAM,那么你不能假设0x01000000处没有更多的RAM,因此你应该从0x01000000开始探测任何额外的内存。 如果不支持此功能,它将返回“Carry=Set”。 ==== BIOS功能:INT 0x15, AH = 0x88 ==== 注意:即使BIOS检测到内存超过15M,该功能也可能会将自身限制为报告15M(出于传统原因)。 它还可能报告高达64M。 它只报告连续 (可用) RAM。 在某些BIOS中,可能无法清除成功时的CF标志。 用法: <source lang="asm"> CLC ; CF bug变通方法 MOV AH, 0x88 INT 0x15 ; 请求upper memory大小 JC SHORT .ERR TEST AX, AX ; size = 0是一个错误 JE SHORT .ERR ; AX = 1M以上的连续KB数 </source> ==== BIOS功能:INT 0x15, AH = 0x8A ==== 此功能返回扩展内存大小(以DX:AX为单位,以KiB为单位),或者更具体地说,它返回从0x00100000开始的可用RAM的连续KiB数。 这也是事情开始变得棘手的地方。 如果存在ISA memory hole(这是ISA设备用于内存映射I/O的1 MiB 孔,从0x00F00000到0x00FFFFFF-例如ISA视频卡的线性帧缓冲区),则此功能可能不会报告所有可用RAM。 例如,它可能报告从0x00100000到0x00F00000的RAM,并且不能报告超过0x01000000的任何RAM(如果存在)。 基本上,如果此功能表示在0x00100000处有14 MiB的RAM,那么你不能假设在0x01000000处没有更多的RAM。 在这种情况下,其他方法可能都无法告诉你更多信息,因此你需要从0x01000000开始探测任何额外内存。 如果不支持此功能,它将返回“Carry=Set”。(译者注:芯片位定时,set指为1,clear指为0) 至少有一些BIOS有在没有相应pop的情况下推送BX,导致返回到IP:BX,并且标志仍在堆栈中的错误。 前面提到的BIOS也支持E820,所以最简单的解决方法是在E820工作时避免此功能。 ====BIOS功能:INT 0x15, AH = 0xC7==== 尽管没有得到广泛支持,但IBM定义的这个功能提供了一个不错的内存映射 (尽管不如0xE820好)。 DS:SI指向以下内存映射表: <pre> 大小 偏移 描述 2 00h 返回数据的有效字节数 (不包括此uint16_t) 4 02h 1-16MB之间的本地内存量,以1KB块为单位 4 06h 16MB到4 GB之间的本地内存量,以1KB数据块为单位 4 0Ah 1-16MB之间的系统内存量,以1KB块为单位 4 0Eh 16MB和4GB之间的系统内存量,以1KB块为单位 4 12h 1-16MB之间的可缓存内存量,以1KB数据块为单位 4 16h 16MB至4GB之间的可缓存内存量,以1KB块为单位 4 1Ah 1-16MB之间的非系统内存启动前的1KB块数 4 1Eh 非系统内存启动前的1KB块数,介于16MB和4GB之间 2 22h 0C000h和0D000h段中最大可用内存块的起始段 2 24h 由偏移量22h定义的大量空闲内存块 </pre> 第一个uint16_t可以返回的最小数字是66字节。 以下是内存类型的定义方式: * 系统板上的本地内存或无法从通道访问的内存。 它可以是系统内存,也可以是非系统内存。 * 适配器上的通道内存。 它可以是系统内存,也可以是非系统内存。 * 由主操作系统管理和分配的系统内存。 如果启用了缓存,则会缓存此内存。 * 主操作系统未管理或分配的非系统内存。 该内存包括内存映射的I/O设备;适配器上的可由适配器直接修改的内存; 以及可在其地址空间内重定位的内存,例如内存切换(bank-switched)和扩展存储器规范(EMS-expanded-memory-specifications)内存。 此内存未缓存。 ====CMOS==== CMOS存储器大小信息可以忽略15M处的标准memory hole。 如果使用CMOS大小,则可能需要简单地假设此内存孔存在。 当然,它也没有关于任何其他保留区域的信息。 用法: <source lang="c"> unsigned short total; unsigned char lowmem, highmem; outportb(0x70, 0x30); lowmem = inportb(0x71); outportb(0x70, 0x31); highmem = inportb(0x71); total = lowmem | highmem << 8; return total; </source> ==== E820h ==== 还有一些其他BIOS功能声称可以为你提供内存信息。 然而,它们是大多不受支持,以至于甚至不可能找到机器来测试代码。 <b>所有</b> 当前计算机都支持E820 (见上文)。 如果某个用户碰巧发现了一台恐龙机器,以至于它的BIOS不支持任何标准的内存检测功能 --他们不会抱怨你的现代操作系统无法支持这台机器。 你只要给出一个错误信息就行了。 ====手动探测==== {{Warning|这可能会损坏你的计算机。}} 尽量使用BIOS获取内存映射,或者使用 [[GRUB]] (它为你调用BIOS)。 内存探测可能会产生'''本质上不可预测'''的结果,因为供应商不支持内存探测。 =====理论介绍===== 直接内存探测仅对具有错误和/或不可更新的BIOS非常旧的系统有用,或者可能针对具有修改后的硬件的系统不再与固件匹配的系统。 所以,如果你不打算支持这种计算机,你就不需要内存探测。 就这样。 即使你需要,你也可以考虑为了运行起来更安全,内存将少于实际可用内存。 无论如何,不要认为这会使你省去了解如何调用复杂的BIOS API的工作。 在启动探测之前,你将始终需要检测'''判断'''运行操作系统的计算机确实需要它,并且确实需要探测'''哪个'''内存区域 (因为对于其他内存区域,你应该总是将BIOS提供的信息视为权威的)。 你还需要考虑适当的内存孔(memory hole)和/或内存映射设备(memory mapped devices),它们可能因系统而异。 当完美实现时,直接探测内存可能允许你检测BIOS无法提供适当支持系统上的扩展内存。 然而,该算法始终需要考虑系统内存中的潜在漏洞或之前检测到的内存映射设备,如帧缓冲SVGA卡等。 也许你只想探测在特定计算机型号上已知无法检测到的特定内存范围。 但是,BIOS是计算机的一部分,并且可能知道你忽略掉的内存,主板和PCI设备上的事情 (请参阅 [[#内存探测的实际障碍]])。 探测内存映射的PCI设备可能会产生'''不可预测的结果'''。 结果本质上是不可预测的,因为供应商不支持内存探测。 最有可能的结果是使你的计算机崩溃,但你甚至可以'''永久损坏系统''',例如清除固件芯片或设置不安全的设备操作参数。 别忘了曾经出现过的[https://en.wikipedia.org/wiki/CIH__(computer_virus) 切尔诺贝利]计算机病毒。 注意:尝试读/写不存在的内存时不会出现错误 -了解这一点很重要: 你不会得到有效的结果,但也不会出现错误。 =====内存探测的实际障碍===== 下面列出了内存探测中涉及的一些技术困难,如果你最终不得不这样做,这些困难可能有助于实现这种算法: * BIOS在RAM中留下的重要数据结构(例如ACPI表)可能会被你丢弃。 这些结构可能在任何地方,知道其地址的唯一方法是 “询问” BIOS。 * 内存映射设备的大小可以从15 MB到16 MB(通常是“VESA本地总线”视频卡,或者不限于视频的旧ISA卡)。 * 也可能在0x00080000处出现(极其罕见)的“内存孔”,用于与远古设备卡的某种兼容性. * 在现代系统上,也可能存在INT 0x15, eax = 0xE820表示不使用的错误RAM,它可以在任何地方使用 (前1 MB除外)。 * 也可能有大的任意内存孔。(例如,具有高达0x1FFFFFF的RAM的NUMA系统,从0x20000000到0x3FFFFFFF的孔,然后从0x40000000到0x5FFFFF的更多RAM。) * 物理地址可能会以各种方式截断。 例如,较旧的芯片组通常具有比处理器所支持的更少的地址线,并且通常在使用这种芯片组的主板上,额外的地址线根本不连接。 例如,955X之前的英特尔桌面芯片组只有32条地址线,尽管它们通常与至少支持PAE的处理器一起使用。 额外的地址线(A32-A35)简单地不连接在主板上,并且如果处理器试图使用超过32位的物理地址来访问存储器, 由于主板上没有连接额外的地址线,物理地址被截断。 * 内存映射设备(PCI视频卡、HPET、PCI express配置空间、APIC等)的地址必须避免。 * 也有(通常较旧的)主板,你可以将值写入“任意值”,然后由于总线电容而能读回相同的值; 在主板中,你可以将值写入缓存并从缓存中读取相同的值,即使该地址上没有RAM。 * 有一些(较旧的,主要是80386)主板将选项ROM和BIOS下面的RAM重新映射到RAM的末尾。 (例如,如果安装了4 MB RAM,你会得到从0x00000000到0x000A0000的RAM,以及从0x00100000到0x00460000的更多RAM,如果你测试每个MB的RAM,则会出现问题,因为你得到的答案是错误的 -- 计数不足0x00400000的RAM,或超过0x00500000的RAM)。 * 如果你正确地编写代码(即,尽可能避免许多问题),那么它的速度会非常慢。 * 最后,测试RAM(如果它实际工作正常)只会告诉你RAM在哪里-它不会给你一个完整的物理地址空间映射。 你不会知道你可以安全地把内存映射的PCI设备放在哪里,因为你不会知道哪些区域是为芯片组的东西保留的 (例如SMM,ROM) 等。 与此形成对比的是,使用BIOS功能并不太难,更可靠,提供了完整的信息,而且速度非常快。 == 通过GRUB的内存映射 == [[GRUB]]或任何实现[http://www.gnu.org/software/grub/manual/multiboot/multiboot.html Multiboot规范]的Bootloader提供了一种检测机器内存量的便捷方法。 不需要重新发明轮子,你可以利用multiboot_info结构利用其他人所做的艰苦工作。 GRUB运行时,它将此结构加载到内存中,并将此结构的地址留在EBX寄存器中。 你还可以在GRUB命令行中使用GRUB命令<tt>displaymem</tt>和GRUB 2命令<tt>lsmmap</tt>查看此结构。 它使用的方法是: * 尝试BIOS Int 0x15, eax = 0xE820 * 如果不起作用,请尝试BIOS Int 0x15, AX = 0xE801和BIOS Int 0x12 * 如果不起作用,请尝试BIOS Int 0x15, AH = 0x88和BIOS Int 0x12 但是,它没有考虑任何已知会影响某些BIOS的错误 (请参阅 [[RBIL]] 中的条目)。 它不检查带进位设置返回的“E801”和/或“88”。 要利用GRUB传递给你的信息,首先在内核的主文件中包含文件[https://www.gnu.org/software/grub/manual/multiboot/html_node/multiboot_002eh.html multiboot.h]。 然后,确保从汇编加载器加载 _main函数时,将EAX和EBX推到堆栈上。 在调用任何其他初始化函数(如[[Calling_Global_Constructors|Global constructor initialization]])之前,请确保执行此操作,否则初始化函数可能会抢夺寄存器。 然后,按如下方式定义你的启动函数: _main (multiboot_info_t* mbd, unsigned int magic) {...} 内存检测的关键在于multiboot_info结构体。 要确定连续内存大小,只需检查<tt>mbd->flags</tt>以验证位0是否已设置,然后就可以安全地参考传统内存的<tt>mbd->mem_lower</tt>(例如,0到640KB之间的物理地址) 和<tt>mbd->mem_upp</tt>用于high内存(例如,从1MB开始)。 两者都以kibibytes给出,即每个1024字节的块。 要获得完整的内存映射,请检查<tt>mbd->flags</tt>的第6位,并使用<tt>mbd->mmap_addr</tt>访问BIOS提供的内存映射。 引用于[http://www.gnu.org/software/grub/manual/multiboot/html_node/Boot-information-format.html#Boot%20information%20format 规范], {{Quotation | 如果设置了标志uint16_t中的位6,则mmap_* 字段有效,并指示包含BIOS提供的计算机内存映射的缓冲区的地址和长度。map_addr是地址,mmap_length是缓冲区的总大小。 缓冲区由一个或多个大小/结构对(size/structure pairs)组成(大小实际上用于跳到下一对):“” }} 考虑到这一点,我们的示例代码将如下所示。 请注意,如果你更喜欢不需要从上面的链接下载的multiboot.h标头的版本,则本文的 “代码示例” 部分中列出了另一个版本。 <source lang="c"> #include "multiboot.h" void _main(multiboot_info_t* mbd, uint32_t magic) { /*确保magic number与内存映射匹配*/ if(magic != MULTIBOOT_BOOTLOADER_MAGIC) { panic("invalid magic number!"); } /* 检查第6位以查看是否有有效的内存映射 */ if(!(mbd->flags >> 6 & 0x1)) { panic("invalid memory map given by GRUB bootloader"); } /* 在内存映射中循环并显示值 */ int i; for(i = 0; i < mbd->mmap_length; i += sizeof(multiboot_memory_map_t)) { multiboot_memory_map_t* mmmt = (multiboot_memory_map_t*) (mbd->mmap_addr + i); printf("Start Addr: %x | Length: %x | Size: %x | Type: %d\n", mmmt->addr, mmmt->len, mmmt->size, mmmt->type); if(mmmt->type == MULTIBOOT_MEMORY_AVAILABLE) { /* * 用这个内存块做点什么! * 请注意,显示为可用的某些内存实际上 * 正在被内核积极使用!你需要把它拿走 * into account before writing to memory! */ } } } </source> '''警告:''' 如果你从gnu.org(上面链接)下载了multiboot头文件,你可能会得到一个版本,该版本将基址和长度字段分别定义为一个64位无符号整数,而不是两个32位无符号整数。 帖子[https://forum.osdev.org/viewtopic.php?t=30318 这可能会导致gcc错误地包装结构]中说,当你尝试读取它时,可能会导致荒谬的值。 * 链接论坛帖子指责GCC没有正确打包multiboot结构,然而,真正的错误是printf的实现/使用。 使用类型uint64_t时,必须指定%lx(而不是%x),以便printf将所有64位作为一个参数读取,而不是将高-32作为一个参数读取,将低-32作为下一个参数读取。 或者,你可以将multiboot标头 (特别是multiboot_mmap_entry struct) 修改为以下内容,以获取正确的值: <source lang="c"> struct multiboot_mmap_entry { multiboot_uint32_t size; multiboot_uint32_t addr_low; multiboot_uint32_t addr_high; multiboot_uint32_t len_low; multiboot_uint32_t len_high; #define MULTIBOOT_MEMORY_AVAILABLE 1 #define MULTIBOOT_MEMORY_RESERVED 2 #define MULTIBOOT_MEMORY_ACPI_RECLAIMABLE 3 #define MULTIBOOT_MEMORY_NVS 4 #define MULTIBOOT_MEMORY_BADRAM 5 multiboot_uint32_t type; } __attribute__((packed)); typedef struct multiboot_mmap_entry multiboot_memory_map_t; </source> 每个multiboot mmap条目存储如下: :{| {{wikitable}} |- ! 0 | size |- ! 4 | base_addr_low |- ! 8 | base_addr_high |- ! 12 | length_low |- ! 16 | length_high |- ! 20 | type |- |} *“size”是关联结构的大小,单位为字节,可以大于最小的20个字节。 base_addr_low是起始地址的下32位,而base_addr_high是上32位,总共是64位的起始地址。 length_low是以字节为单位的内存区域大小的低32位,length_high是高32位,总计为64位长度。 type(类型)是表示的各种地址范围,其中值1表示可用RAM,所有其他值当前表示保留区域。 * GRUB只是使用INT 15h, EAX=E820来获取详细的内存映射,并且不验证该映射的 “健全性”。 它也不会对条目进行排序,检索任何可用的ACPI 3.0扩展uint32_t(使用“忽略此条目”位),或以任何其他方式清理表。 * 你必须处理的问题之一是,根据多重引导(Multiboot)规范,理论上GRUB可以将其多重引导信息及其引用的所有表(ELF部分、mmap和模块)放置在内存中的任何位置。 实际上,在当前的GRUB legacy中,它们被分配为GRUB程序本身的一部分,低于1MB,但不能保证保持不变。 因此,在开始使用特定内存之前,你应该尝试保护这些表。 (你可以扫描表格,以确保它们的地址都在1M以下。) * 另一个问题是,“类型” 字段定义为 “1 = 可用的RAM” 和 “其他任何东西都不可用”。 不管多重引导规范怎么说,很多人都认为类型字段直接取自INT 15h, EAX=E820 (在旧版本的GRUB中也是如此)。 但是,GRUB 2支持从UEFI/EFI (和其他来源) 启动,并且假定类型字段直接从INT 15h获取的代码,EAX = E820将被破坏。 这意味着(在发布新的多引导规范之前),你不应该对类型进行假设,也不能做诸如回收“ACPI可回收”区域或支持S4/hibernate states之类的事情 (因为操作系统需要保存/恢复标记为“ACPI NVS”的区域才能做到这一点)。 幸运的是,新版本的多重引导规范应该会尽快发布,希望可以解决此问题 (但不幸的是,你无法判断自制操作系统是从“GRUB legacy”还是“GRUB 2”启动的,除非它采用了新的多引导头并与GRUB legacy不兼容)。 ==模拟器中的内存检测== 当你告诉仿真器你想要仿真多少内存时,这个概念有点 “模糊”,因为在1M以下的RAM的仿真缺失位(emulated missing bits)。 如果你让模拟器模拟32M,这是否意味着你的地址空间肯定会从0到32M-1,带有丢失位? 不一定。 仿真器可能会假设你的意思是在1M以上的 <i>连续</i> 内存为32M,因此它可能会在33M -1处结束。 或者它可能会假设你的意思是32M的总可用内存,从0到32M + 384K - 1。(译者注:见[[Memory_Map_(x86)]]中的实模式第一个Mib地址空间表) 因此,如果你看到“检测到的内存大小”与你的预期不完全匹配,请不要感到惊讶。 == 在UEFI上呢? == 在UEFI上,有“BootServices->GetMemoryMap”。 此功能类似于E820,是新的UEFI机器上的唯一解决方案。 基本上,要使用,首先你调用它一次以获取内存映射的大小。 然后分配一个大小相同的缓冲区,然后再次调用以获取映射本身。 当心,通过分配内存,你可能会增加内存映射的大小。 考虑到新的分配可以将一个空闲内存区域一分为二,你应该为2个额外的内存描述符添加空间。 它返回一个EFI_MEMORY_DESCRIPTOR。 它们具有以下格式(取自GNU EFI): <source lang="c"> typedef struct { UINT32 Type; // EFI_MEMORY_TYPE, Field size is 32 bits followed by 32 bit pad UINT32 Pad; EFI_PHYSICAL_ADDRESS PhysicalStart; // Field size is 64 bits EFI_VIRTUAL_ADDRESS VirtualStart; // Field size is 64 bits UINT64 NumberOfPages; // Field size is 64 bits UINT64 Attribute; // Field size is 64 bits } EFI_MEMORY_DESCRIPTOR; </source> 要遍历它们,可以使用NEXT_MEMORY_DESCRITOR宏。 内存类型与E820代码不同。为了转化, 请参阅[https://github.com/tianocore/edk2/blob/70d5086c3274b1a5b099d642d546581070374e6e/OvmfPkg/Csm/LegacyBiosDxe/LegacyBootSupport.c#L1601 CSM E820兼容性]。 <source lang="c"> typedef enum { EfiReservedMemoryType, EfiLoaderCode, EfiLoaderData, EfiBootServicesCode, EfiBootServicesData, EfiRuntimeServicesCode, EfiRuntimeServicesData, EfiConventionalMemory, EfiUnusableMemory, EfiACPIReclaimMemory, EfiACPIMemoryNVS, EfiMemoryMappedIO, EfiMemoryMappedIOPortSpace, EfiPalCode, EfiPersistentMemory, EfiMaxMemoryType } EFI_MEMORY_TYPE; </source> == 代码示例 == ===获取GRUB内存映射=== 声明适当的结构体,获取指向第一个实例的指针,获取所需的任何地址和长度信息, 最后跳到下一个内存映射实例,方法是将size+sizeof(mmap->size) 添加到指针, 因为mmap->size不考虑自身,而且GRUB在结构中将base_addr_low处理为偏移量0。 你还必须使用mmap_length来确保不会超出整个缓冲区。 <source lang="c"> typedef struct multiboot_memory_map { unsigned int size; unsigned int base_addr_low,base_addr_high; // 你也可以使用: unsigned long int base_addr; 如果支持。 unsigned int length_low,length_high; //还可以使用:unsigned long int length;如果支持的话。 unsigned int type; } multiboot_memory_map_t; //这实际上是一个条目,不是整个地图。 typedef multiboot_memory_map_t mmap_entry_t; int main(multiboot_info* mbt, unsigned int magic) { ... mmap_entry_t* entry = mbt->mmap_addr; while(entry < mbt->mmap_addr + mbt->mmap_length) { // 对条目做点什么 entry = (mmap_entry_t*) ((unsigned int) entry + entry->size + sizeof(entry->size)); } ... } </source> ===获取E820内存映射=== <source lang="asm"> ; 使用INT 0x15, eax=0xE820 BIOS函数获取内存映射 ; 注意: 最初di为0,请确保将其设置为一个值,以使BIOS代码不会被覆盖。 ; 覆盖BIOS代码的后果将导致诸如陷入'int 0x15'等问题` ; inputs:ES:DI->24字节条目的目标缓冲区 ; outputs: bp = 条目计数,破坏除esi以外的所有寄存器 mmap_ent equ 0x8000 ; 条目数将存储在0x8000 do_e820: mov di, 0x8004 ; 将di设置为0x8004。否则,在获取某些条目后,此代码将停滞在`int 0x15`中 xor ebx, ebx ; ebx必须为0才能开始 xor bp, bp ; 在bp中保留一个条目计数 mov edx, 0x0534D4150 ; 将“SMAP”放入edX mov eax, 0xe820 mov [es:di + 20], dword 1 ; 强制有效的ACPI 3.X条目 mov ecx, 24 ; 请求24字节 int 0x15 jc short .failed ; 第一次调用时设置的进位表示“不支持的功能” mov edx, 0x0534D4150 ; 一些BIOS显然破坏了这个寄存器? cmp eax, edx ; 如果成功,eax必须重置为“SMAP” jne short .failed test ebx, ebx ; EBX=0表示列表只有1个条目长(毫无价值) je short .failed jmp short .jmpin .e820lp: mov eax, 0xe820 ; eax,ecx在每个int 0x15通话中都会被丢弃 mov [es:di + 20], dword 1 ; 强制使用有效的ACPI 3.X条目 mov ecx, 24 ; 再次请求24字节 int 0x15 jc short .e820f ; 进位集表示 “已经到达的列表结束” mov edx, 0x0534D4150 ; 修复可能损坏的寄存器 .jmpin: jcxz .skipent ; 跳过任何0长度条目 cmp cl, 20 ; 得到24字节ACPI 3.X响应? jbe short .notext test byte [es:di + 20], 1 ; 如果是:“忽略此数据”位是否清晰? je short .skipent .notext: mov ecx, [es:di + 8] ; 获取较低的uint32_t内存区长度 or ecx, [es:di + 12] ; “ 或 ”它与uint32_t上测试为零 jz .skipent ; 如果长度uint64_t为0,则跳过输入 inc bp ; 获得一个好的条目:++count,移到下一个储存点 add di, 24 .skipent: test ebx, ebx ; 如果ebx重置为0,则列表已完成 jne short .e820lp .e820f: mov [mmap_ent], bp ; 存储条目计数 clc ; 到目前为止,列表末尾有“JC”,因此必须清除进位 ret .failed: stc ; “ 不支持功能 ”错误退出 ret </source> C中的示例(假设我们处于引导加载程序环境中,实数模式,DS和CS = 0000): <source lang="c"> //在实模式下运行可能需要: __asm__(".code16gcc\n"); // SMAP条目结构 #include <stdint.h> typedef struct SMAP_entry { uint32_t BaseL; // base address uint64_t uint32_t BaseH; uint32_t LengthL; // length uint64_t uint32_t LengthH; uint32_t Type; // entry Type uint32_t ACPI; // extended }__attribute__((packed)) SMAP_entry_t; // 将内存映射加载到缓冲区-注意:regparm(3)避免了实数模式下gcc的堆栈问题 int __attribute__((noinline)) __attribute__((regparm(3))) detectMemory(SMAP_entry_t* buffer, int maxentries) { uint32_t contID = 0; int entries = 0, signature, bytes; do { __asm__ __volatile__ ("int $0x15" : "=a"(signature), "=c"(bytes), "=b"(contID) : "a"(0xE820), "b"(contID), "c"(24), "d"(0x534D4150), "D"(buffer)); if (signature != 0x534D4150) return -1; //错误 if (bytes > 20 && (buffer->ACPI & 0x0001) == 0) { // 忽略此条目 } else { buffer++; entries++; } } while (contID != 0 && entries < maxentries); return entries; } // 例如,在主例程中,内存映射存储在0000:1000-0000:2FFF中 [...] { [...] SMAP_entry_t* smap = (SMAP_entry_t*) 0x1000; const int smap_size = 0x2000; int entry_count = detectMemory(smap, smap_size / sizeof(SMAP_entry_t)); if (entry_count == -1) { // ERROR-暂停系统和/或显示错误消息 [...] } else { // 进程内存映射 [...] } } </source> ===获取UEFI内存映射=== <source lang="c"> EFI_STATUS Status; EFI_MEMORY_DESCRIPTOR *EfiMemoryMap; UINTN EfiMemoryMapSize; UINTN EfiMapKey; UINTN EfiDescriptorSize; UINT32 EfiDescriptorVersion; // // 获取EFI内存映射 // EfiMemoryMapSize = 0; EfiMemoryMap = NULL; Status = gBS->GetMemoryMap ( &EfiMemoryMapSize, EfiMemoryMap, &EfiMapKey, &EfiDescriptorSize, &EfiDescriptorVersion ); ASSERT (Status == EFI_BUFFER_TOO_SMALL); // // 使用为分配池返回的大小。 // EfiMemoryMap = (EFI_MEMORY_DESCRIPTOR *) AllocatePool (EfiMemoryMapSize + 2 * EfiDescriptorSize); ASSERT (EfiMemoryMap != NULL); Status = gBS->GetMemoryMap ( &EfiMemoryMapSize, EfiMemoryMap, &EfiMapKey, &EfiDescriptorSize, &EfiDescriptorVersion ); if (EFI_ERROR (Status)) { FreePool (EfiMemoryMap); } // // 获取描述符 // EFI_MEMORY_DESCRIPTOR *EfiEntry = EfiMemoryMap; do { // ...对EfiEntry做点什么。 EfiEntry = NEXT_MEMORY_DESCRIPTOR (EfiEntry, EfiDescriptorSize); } while((UINT8*)EfiEntry < (UINT8*)EfiMemoryMap + EfiMemoryMapSize); </source> ===Manual Probing in C=== 注: *中断禁用和缓存失效使内存保持一致。 * 遵循此示例的汇编语言手册探查代码更好 <source lang="c"> /* * void count_memory (void) * * probes memory above 1mb * * last mod : 05sep98 - stuart george * 08dec98 - "" "" * 21feb99 - removed dummy calls * */ void count_memory(void) { register ULONG *mem; ULONG mem_count, a; USHORT memkb; UCHAR irq1, irq2; ULONG cr0; /* save IRQ's */ irq1=inb(0x21); irq2=inb(0xA1); /* kill all irq's */ outb(0x21, 0xFF); outb(0xA1, 0xFF); mem_count=0; memkb=0; // 存储CR0的副本 __asm__ __volatile("movl %%cr0, %%eax":"=a"(cr0))::"eax"); // 使缓存无效 // 回写并使缓存失效 __asm__ __volatile__ ("wbinvd"); // 仅使用PE/CD/NW插入cr0 // cache disable(486+), no-writeback(486+), 32bit mode(386+) __asm__ __volatile__("movl %%eax, %%cr0", :: "a" (cr0 | 0x00000001 | 0x40000000 | 0x20000000) : "eax"); do { memkb++; mem_count += 1024*1024; mem= (ULONG*) mem_count; a= *mem; *mem= 0x55AA55AA; //空的asm调用告诉gcc不要依赖其寄存器中的内容 //作为保存的变量(避免GCC优化) asm("":::"memory"); if (*mem!=0x55AA55AA) mem_count=0; else { *mem=0xAA55AA55; asm("":::"memory"); if(*mem!=0xAA55AA55) mem_count=0; } asm("":::"memory"); *mem=a; } while (memkb<4096 && mem_count!=0); __asm__ __volatile__("movl %%eax, %%cr0", :: "a" (cr0) : "eax"); mem_end = memkb<<20; mem = (ULONG*) 0x413; bse_end= (*mem & 0xFFFF) <<6; outb(0x21, irq1); outb(0xA1, irq2); } </source> === 在ASM中手动探测 === 这是内存探测的“最不安全”算法。 它对内存内容是“非破坏性的”,通常比上面的C代码要好。 注: * <b>除非绝对必须</b>,否则不要使用手动探测。 这意味着手动探测代码仅用于不可靠的旧计算机,并且用于手动探测的代码需要为不可靠的旧计算机设计 (对于现代计算机来说,这是 “好的” 假设,例如 “不太可能存在ISA视频卡”,不适用)。 * 尽量减少手动探测的次数。 例如,如果BIOS支持“Int 0x12”(它们都支持),则使用它来避免探测1MB以下的RAM。 如果 “Int 0x15, AH = 0x88” 说0x00100000处有0xFFFF KB,你认为还有更多 (因为如果有的话,16位的值不能告诉你还有更多)然后从已知RAM的末尾(而不是从0x00100000)进行探测。 * 不要假设对“非RAM”的写入不会被缓存 (测试后,使用wbinbd或CLFLUSH刷新缓存,以确保你正在测试物理地址而不是缓存)。 * 由于总线电容存在,对“非RAM”的写入也会被保留 (在不同的地址使用虚拟写入来避免这种情况,因此如果该地址没有RAM,则读回伪值,而不是测试值)。 * 不要将设置的值写入地址并将其读回以测试RAM (例如,"mov [address],0x12345678; mov [dummy],0x0; wbinvd; cmp [address],0x12345678") 因为你可能不走运,发现一个ROM包含与你正在使用的值相同的值。 相反,尝试修改已经存在的内容。 * 测试每个块的最后一个字节,而不是每个块的第一个字节,并确保每个块的大小小于16 KB。 这是因为一些较旧的主板将ROM区域下的RAM重新定位到内存顶部 (例如,具有2 MB RAM的计算机可能具有从0x000E0000 0x000FFFFF的128 KB ROM和从0x00100000到0x0020FFFF的RAM。 * 不要对“内存上限”做任何假设。 仅仅因为RAM的最后一个字节在0x0020FFFF并不意味着安装了2176KB的RAM, 仅仅因为安装了2 MB的RAM并不意味着RAM的最后一个字节将位于0x001FFFFF。 * 假设memory holes存在(并冒跳过一些RAM的风险)比假设内存漏洞不存在(并冒崩溃的风险)要好。 这意味着假设从0x00F00000到0x00FFFFFF的区域不能使用,并且根本不探测该区域 (可能在此区域中存在某种ISA设备,并且对该区域的任何写入都可能导致问题)。 <source lang="asm"> ;探测某个地址是否有RAM ; ; 注:“dummy”->没有任何重要内容的已知良好的内存地址 ; ;Input ; edx Maximum number of bytes to test ; esi Starting address ; ;Output ; ecx Number of bytes of RAM found ; esi Address of RAM probeRAM: push eax push ebx push edx push ebp mov ebp,esi ;ebp = starting address add esi,0x00000FFF ;round esi up to block boundary and esi, ~0x00000FFF ;truncate to block boundary push esi ;Save corrected starting address for later mov eax, esi ;eax = corrected starting address sub eax, ebp ;eax = bytes to skip from original starting address, due to rounding xor ecx,ecx ;ecx = number of bytes of RAM found so far (none) sub edx,eax ;edx = number of bytes left to test jc .done ; all done if nothing left after rounding or esi,0x00000FFC ;esi = address of last uint32_t in first block shr edx,12 ;edx = number of blocks to test (rounded down) je .done ; Is there anything left after rounding? .testAddress: mov eax,[esi] ;eax = original value mov ebx,eax ;ebx = original value not eax ;eax = reversed value mov [esi],eax ;Modify value at address mov [dummy],ebx ;Do dummy write (that's guaranteed to be a different value) wbinvd ;Flush the cache mov ebp,[esi] ;ebp = new value mov [esi],ebx ;Restore the original value (even if it's not RAM, in case it's a memory mapped device or something) cmp ebp,eax ;Was the value changed? jne .done ; no, definitely not RAM -- exit to avoid damage ; yes, assume we've found some RAM add ecx,0x00001000 ;ecx = new number of bytes of RAM found add esi,0x00001000 ;esi = new address to test dec edx ;edx = new number of blocks remaining jne .testAddress ;more blocks remaining? ;If not, we're done .done: pop esi ;esi = corrected starting address (rounded up) pop ebp pop edx pop ebx pop eax ret </source> 进一步说明: * 根据它的使用方式,可以跳过一些初始代码 (例如,如果你知道起始地址始终在4KB边界上对齐)。 * Wbinwd指令严重影响性能,因为它使所有缓存中的所有数据无效 (TLB除外)。 最好使用CLFLUSH,这样你只会使需要失效的缓存线失效,但旧的CPU不支持CLFLUSH (此代码适用于较旧的计算机)。 对于较旧的计算机,它不应该太慢,因为缓存和RAM之间的速度差异并不大,并且通常只有少量的RAM (例如64 MB或更少)。 现代计算机要测试的内存要多得多,而且更依赖缓存。 例如,内存为32MB的80486可能需要1秒,而内存为2 GB的奔腾4可能需要30秒或更长时间。 * 增加块大小 (例如,每16 KB测试一次,而不是每4 KB测试一次) 将提高性能 (并增加风险)。 16KB的块可能是安全的,而较大的块大小则不安全。 非常大的块(例如,每1MB测试一次)可能可以在现代计算机上使用(但在现代计算机上根本不需要探测), 任何大于1 MB的东西都保证会定期给出错误的结果。 * 80386及更早版本的计算机不支持WBINVD。 这意味着对于80386和更早的版本,你不能刷新缓存,但这应该无关紧要 (对于80386和较旧的内存,其运行速度与CPU相同,因为这里没有缓存)。 不过,你需要在较新的CPU上刷新缓存。 拥有一个使用WBINVD的例程和另一个不使用WBINVD的例程可能比在循环中间执行“if (CPU_is_80486_or_newer) { WBINVD }”要好。 ==另见== ===论坛主题=== * [http://www.osdev.org/phpBB2/viewtopic.php?t=11391 Grub's multiboot memory map], 以GRUB/BIOS报告内存映射的真实示例为特色。 * [https://forum.osdev.org/viewtopic.php?t=30318 QEMU Multiboot Invalid Memory Map], 用GRUB检测内存时的一个潜在陷阱 [[Category:X86]]
本页使用的模板:
模板:Bias
(
查看源代码
)
模板:If
(
查看源代码
)
模板:Quotation
(
查看源代码
)
模板:Show1
(
查看源代码
)
模板:Warning
(
查看源代码
)
模板:Wikitable
(
查看源代码
)
返回至“
Detecting Memory (x86)
”。
导航菜单
个人工具
登录
命名空间
页面
讨论
变体
已展开
已折叠
查看
阅读
查看源代码
查看历史
更多
已展开
已折叠
搜索
导航
首页
最近更改
随机页面
MediaWiki帮助
工具
链入页面
相关更改
特殊页面
页面信息