Detecting Memory (x86)

来自osdev
Zhang3讨论 | 贡献2022年3月22日 (二) 09:05的版本 (创建页面,内容为“{{Bias}} 操作系统初始化自身所需的最重要信息之一是对于机器上可用RAM的映射。(译者注:本文讨论了操作系统如何检测计算机有多少实际物理内存,并找到它们的访问地址的实现,建议同时参考阅读x86内存映射。本文分成了独立的全部原理讲解和全部代码示例两部分,如果你只关心一种做法,可以跳过一部分,前后结合着读。) 从…”)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索

本文可能存在 可能对某些观点来说不平衡。请通过添加有关被忽视的观点信息[1]

操作系统初始化自身所需的最重要信息之一是对于机器上可用RAM的映射。(译者注:本文讨论了操作系统如何检测计算机有多少实际物理内存,并找到它们的访问地址的实现,建议同时参考阅读x86内存映射。本文分成了独立的全部原理讲解和全部代码示例两部分,如果你只关心一种做法,可以跳过一部分,前后结合着读。) 从根本上说,操作系统获取该信息的最佳方式是使用BIOS。 可能有一些罕见的机器,你别无选择,只能尝试自己检测内存 --然而,在任何其他情况下这样做都是不明智的。

好像应该完全有理由对自己说,“BIOS怎么检测RAM?我就怎么做。” 不幸的是,答案令人失望:
大多数BIOS在检测到安装的RAM的类型、检测每个内存模块的大小、然后配置芯片组以使用检测到的RAM之前,不能使用任何RAM。 所有这些都取决于芯片组特定的方法,并且通常记录在内存控制器 (北桥) 的数据表中。 在此过程中,RAM不能用于运行程序。 BIOS最初是从ROM运行的,因此它可以使用RAM芯片玩它自己必要的把戏。 但是从任何其他程序内部完全不可能做到这一点。

我们也合理的希望能回收从0xA0000到0xFFFFF的内存,并让RAM地址连续(译者注:这段区域是BIOS开机会分配的一段内存,详见x86内存映射)。 答案再次令人失望:
忘了它吧。 这段内存其中一些SMM或ACPI可能要经常使用。 其中一些可能会多次需要,即使在机器引导之后也是如此。 收回其中的一部分将需要大量的主板或芯片组特定的控制。 可以编写一个“芯片组驱动程序”,允许你收回一点。 然而,几乎可以肯定的是,将其全部收回是不可能的。 总的来说,这点微小的结果不值得付出努力。

可能需要注意的是,所有PC机都需要一个低于4GB地址的内存孔(memory hole),用于额外的内存映射硬件(包括实际的BIOS ROM)。 因此,对于RAM>3G的机器,主板/芯片组/BIOS可能会将某些RAM(会与映射的硬件重叠)映射到4G以上-- 可以使用 PAE长模式 访问它。

有关内存的一般布局,请参见内存映射(x86)

检测下层内存(Low Memory)

“Low Memory” 是1MB以下的可用RAM,通常在640KB以下。 有两个BIOS功能可以获得它的大小。

INT 0x12:INT 0x12调用将返回AX = 总KB数。 AX值从0到EBDA的底部 (当然,你可能也不应该使用空间的前0x500字节- 也就是IVT或BDA部分)。

用法:

    ; 清除进位标志(carry flag)
    clc

    ; 切换到BIOS(=请求low memory大小)
    int 0x12

    ; 如果失败,则设置进位标志
    jc .Error

    ; AX = 从0开始的以KB为单位的连续内存量。

注意:此功能应始终存在,并且可能不会修改进位标志。 如果仿真器不支持它,进位标志将被设置1,指示错误。

或者,你可以只要使用INT 0x15,EAX = 0xE820 (请参见下文)。

检测上层内存(Upper Memory)

BIOS功能:INT 0x15, EAX = 0xE820

另请参阅: [2]

到目前为止,检测PC内存的最佳方法是使用INT 0x15,EAX = 0xE820命令。 此功能适用于2002年以后生产的所有PC,以及在此之前的大多数现有PC。 它是唯一可以检测4G以上内存区域的BIOS功能。 它是BIOS的终极内存检测功能。

实际上,此功能返回一个未排序的列表,该列表可能包含未使用的条目,并且(在极少或不可靠的情况下)可能返回重叠区域。 每个列表条目都存储在ES:DI处的内存中,并且DI等待你去递增。 在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位目前尚未定义。

基本用法:
对于该功能的第一次调用,将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示例,请参见下面的 代码示例。)

注意:

  • 获得列表后,可能需要:对列表进行排序,合并相同类型的相邻范围,将任何重叠区域更改为重叠双方中限制最严格的类型,并将任何无法识别的“类型”值更改为类型2。
  • 类型3 “ACPI reclaimable memory(可回收内存)” 区域可以像正常的 “可用RAM” 区域一样使用 (并与之组合),只要你已经使用过存储在那里的ACPI表 (即可以“回收”)。
  • 类型2、4、5(保留、ACPI非易失性、坏)标记在分配物理内存时应避免的区域。
  • 将未列出的区域视为类型2 -- 保留。
  • 你的代码必须能够处理不以任何“页面边界(page boundary)”开始或结束的区域。

在Bochs中调用INT 15h, EAX=E820的典型输出:

  基址               | 长度                | 类型
  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)

其他方法

PnP

使用即插即用(PnP)调用可以获得相当好的内存映射。{这里缺少描述和代码。}

SMBIOS

SMBIOS旨在允许 “管理员” 评估硬件升级选项或维护公司当前正在使用的硬件的目录 (即,它提供信息供人类使用,而不是供软件使用)。 它在许多计算机上可能不会给出可靠的结果 -参见: [3].

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以上的连续内存。

典型输出:
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用法:

	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页

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标志。

用法:

	CLC			; CF bug变通方法
	MOV AH, 0x88
	INT 0x15		; 请求upper memory大小
	JC SHORT .ERR
	TEST AX, AX		; size = 0是一个错误
	JE SHORT .ERR
	; AX = 1M以上的连续KB数

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指向以下内存映射表:

大小   偏移  描述                                  

 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定义的大量空闲内存块

第一个uint16_t可以返回的最小数字是66字节。 以下是内存类型的定义方式:

  • 系统板上的本地内存或无法从通道访问的内存。 它可以是系统内存,也可以是非系统内存。
  • 适配器上的通道内存。 它可以是系统内存,也可以是非系统内存。
  • 由主操作系统管理和分配的系统内存。 如果启用了缓存,则会缓存此内存。
  • 主操作系统未管理或分配的非系统内存。 该内存包括内存映射的I/O设备;适配器上的可由适配器直接修改的内存; 以及可在其地址空间内重定位的内存,例如内存切换(bank-switched)和扩展存储器规范(EMS-expanded-memory-specifications)内存。 此内存未缓存。

CMOS

CMOS存储器大小信息可以忽略15M处的标准memory hole。 如果使用CMOS大小,则可能需要简单地假设此内存孔存在。 当然,它也没有关于任何其他保留区域的信息。

用法:

    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;

E820h

还有一些其他BIOS功能声称可以为你提供内存信息。 然而,它们是大多不受支持,以至于甚至不可能找到机器来测试代码。 所有 当前计算机都支持E820 (见上文)。 如果某个用户碰巧发现了一台恐龙机器,以至于它的BIOS不支持任何标准的内存检测功能 --他们不会抱怨你的现代操作系统无法支持这台机器。 你只要给出一个错误信息就行了。

手动探测

警告: 这可能会损坏你的计算机。

尽量使用BIOS获取内存映射,或者使用 GRUB (它为你调用BIOS)。 内存探测可能会产生本质上不可预测的结果,因为供应商不支持内存探测。

理论介绍

直接内存探测仅对具有错误和/或不可更新的BIOS非常旧的系统有用,或者可能针对具有修改后的硬件的系统不再与固件匹配的系统。 所以,如果你不打算支持这种计算机,你就不需要内存探测。 就这样。 即使你需要,你也可以考虑为了运行起来更安全,内存将少于实际可用内存。

无论如何,不要认为这会使你省去了解如何调用复杂的BIOS API的工作。 在启动探测之前,你将始终需要检测判断运行操作系统的计算机确实需要它,并且确实需要探测哪个内存区域 (因为对于其他内存区域,你应该总是将BIOS提供的信息视为权威的)。 你还需要考虑适当的内存孔(memory hole)和/或内存映射设备(memory mapped devices),它们可能因系统而异。

当完美实现时,直接探测内存可能允许你检测BIOS无法提供适当支持系统上的扩展内存。 然而,该算法始终需要考虑系统内存中的潜在漏洞或之前检测到的内存映射设备,如帧缓冲SVGA卡等。 也许你只想探测在特定计算机型号上已知无法检测到的特定内存范围。

但是,BIOS是计算机的一部分,并且可能知道你忽略掉的内存,主板和PCI设备上的事情 (请参阅 #内存探测的实际障碍)。 探测内存映射的PCI设备可能会产生不可预测的结果。 结果本质上是不可预测的,因为供应商不支持内存探测。 最有可能的结果是使你的计算机崩溃,但你甚至可以永久损坏系统,例如清除固件芯片或设置不安全的设备操作参数。 别忘了曾经出现过的切尔诺贝利计算机病毒。

注意:尝试读/写不存在的内存时不会出现错误 -了解这一点很重要: 你不会得到有效的结果,但也不会出现错误。

内存探测的实际障碍

下面列出了内存探测中涉及的一些技术困难,如果你最终不得不这样做,这些困难可能有助于实现这种算法:

  • 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或任何实现Multiboot规范的Bootloader提供了一种检测机器内存量的便捷方法。 不需要重新发明轮子,你可以利用multiboot_info结构利用其他人所做的艰苦工作。 GRUB运行时,它将此结构加载到内存中,并将此结构的地址留在EBX寄存器中。 你还可以在GRUB命令行中使用GRUB命令displaymem和GRUB 2命令lsmmap查看此结构。

它使用的方法是:

  • 尝试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传递给你的信息,首先在内核的主文件中包含文件multiboot.h。 然后,确保从汇编加载器加载 _main函数时,将EAX和EBX推到堆栈上。 在调用任何其他初始化函数(如Global constructor initialization)之前,请确保执行此操作,否则初始化函数可能会抢夺寄存器。 然后,按如下方式定义你的启动函数:

_main (multiboot_info_t* mbd, unsigned int magic) {...}

内存检测的关键在于multiboot_info结构体。 要确定连续内存大小,只需检查mbd->flags以验证位0是否已设置,然后就可以安全地参考传统内存的mbd->mem_lower(例如,0到640KB之间的物理地址) 和mbd->mem_upp用于high内存(例如,从1MB开始)。 两者都以kibibytes给出,即每个1024字节的块。

要获得完整的内存映射,请检查mbd->flags的第6位,并使用mbd->mmap_addr访问BIOS提供的内存映射。 引用于规范

如果设置了标志uint16_t中的位6,则mmap_* 字段有效,并指示包含BIOS提供的计算机内存映射的缓冲区的地址和长度。map_addr是地址,mmap_length是缓冲区的总大小。 缓冲区由一个或多个大小/结构对(size/structure pairs)组成(大小实际上用于跳到下一对):“”


考虑到这一点,我们的示例代码将如下所示。 请注意,如果你更喜欢不需要从上面的链接下载的multiboot.h标头的版本,则本文的 “代码示例” 部分中列出了另一个版本。

#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!
             */
        }
    }
}

警告: 如果你从gnu.org(上面链接)下载了multiboot头文件,你可能会得到一个版本,该版本将基址和长度字段分别定义为一个64位无符号整数,而不是两个32位无符号整数。 帖子这可能会导致gcc错误地包装结构中说,当你尝试读取它时,可能会导致荒谬的值。

  • 链接论坛帖子指责GCC没有正确打包multiboot结构,然而,真正的错误是printf的实现/使用。 使用类型uint64_t时,必须指定%lx(而不是%x),以便printf将所有64位作为一个参数读取,而不是将高-32作为一个参数读取,将低-32作为下一个参数读取。

或者,你可以将multiboot标头 (特别是multiboot_mmap_entry struct) 修改为以下内容,以获取正确的值:

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;

每个multiboot mmap条目存储如下:

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以上的 连续 内存为32M,因此它可能会在33M -1处结束。 或者它可能会假设你的意思是32M的总可用内存,从0到32M + 384K - 1。(译者注:见Memory_Map_(x86)中的实模式第一个Mib地址空间表) 因此,如果你看到“检测到的内存大小”与你的预期不完全匹配,请不要感到惊讶。

在UEFI上呢?

在UEFI上,有“BootServices->GetMemoryMap”。 此功能类似于E820,是新的UEFI机器上的唯一解决方案。 基本上,要使用,首先你调用它一次以获取内存映射的大小。 然后分配一个大小相同的缓冲区,然后再次调用以获取映射本身。 当心,通过分配内存,你可能会增加内存映射的大小。 考虑到新的分配可以将一个空闲内存区域一分为二,你应该为2个额外的内存描述符添加空间。 它返回一个EFI_MEMORY_DESCRIPTOR。 它们具有以下格式(取自GNU EFI):

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;

要遍历它们,可以使用NEXT_MEMORY_DESCRITOR宏。

内存类型与E820代码不同。为了转化, 请参阅CSM E820兼容性

typedef enum {
  EfiReservedMemoryType,
  EfiLoaderCode,
  EfiLoaderData,
  EfiBootServicesCode,
  EfiBootServicesData,
  EfiRuntimeServicesCode,
  EfiRuntimeServicesData,
  EfiConventionalMemory,
  EfiUnusableMemory,
  EfiACPIReclaimMemory,
  EfiACPIMemoryNVS,
  EfiMemoryMappedIO,
  EfiMemoryMappedIOPortSpace,
  EfiPalCode,
  EfiPersistentMemory,
  EfiMaxMemoryType
} EFI_MEMORY_TYPE;

代码示例

获取GRUB内存映射

声明适当的结构体,获取指向第一个实例的指针,获取所需的任何地址和长度信息, 最后跳到下一个内存映射实例,方法是将size+sizeof(mmap->size) 添加到指针, 因为mmap->size不考虑自身,而且GRUB在结构中将base_addr_low处理为偏移量0。 你还必须使用mmap_length来确保不会超出整个缓冲区。

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));
	}
	...
}

获取E820内存映射

; 使用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

C中的示例(假设我们处于引导加载程序环境中,实数模式,DS和CS = 0000):

//在实模式下运行可能需要:
__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 {
		// 进程内存映射
		[...]
	}
}

获取UEFI内存映射

  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);

Manual Probing in 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);
}

在ASM中手动探测

这是内存探测的“最不安全”算法。 它对内存内容是“非破坏性的”,通常比上面的C代码要好。

注:

  • 除非绝对必须,否则不要使用手动探测。 这意味着手动探测代码仅用于不可靠的旧计算机,并且用于手动探测的代码需要为不可靠的旧计算机设计 (对于现代计算机来说,这是 “好的” 假设,例如 “不太可能存在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设备,并且对该区域的任何写入都可能导致问题)。
;探测某个地址是否有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

进一步说明:

  • 根据它的使用方式,可以跳过一些初始代码 (例如,如果你知道起始地址始终在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 }”要好。

另见

论坛主题