微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

内核如何知道物理内存基地址?

如何解决内核如何知道物理内存基地址?

我正在尝试了解 2 个密切相关的问题。

  1. 运行后引导加载程序和启用 MMU 之前的内核代码在物理/身份映射虚拟内存中运行。这段代码如何在不同的 cpu 之间移植可能具有不同物理地址范围的 DRAM?

  2. 对于管理页表的内核来说,它需要了解哪些物理内存资源可用,包括物理内存基地址和可用物理内存,因此它不会分配超出的物理地址DRAM 范围。

我想这在某种程度上取决于实现,但对不同架构如何处理这个问题的参考将不胜感激。到目前为止我的一些想法:

  1. 物理地址 DRAM 范围,或者至少是基地址,是在内核编译时被烘焙的。这意味着即使使用相同的 ISA,不同的 cpu 也需要重新编译。这是受到这个答案 here 的启发,如果我理解正确,它描述了内核基地址的相同解决方案。由于基地址在编译时是已知的,内核代码引用文字地址而不是 DRAM/内核基地址的偏移量。

  2. DRAM 信息与物理内存映射的其余部分一起从设备树中读取和学习。这至少是我对 Xilinx Zynq SoC 的印象,基于 this 等论坛帖子。虽然这个解决方案提供了更多的灵活性,并允许我们重新编译引导加载程序而不是整个内核来移植 cpu,但它确实让我想知道我的 X86 个人机器如何在运行时检测我安装了多少 DRAM。管理页表的代码仅引用 DRAM 基地址的偏移量,无需在具有不同 DRAM 物理地址范围的 cpu 之间重新编译即可移植。

解决方法

在启动时可用的整个物理内存 DIMM 可能不会并且通常不会映射到物理内存地址空间的单个连续范围,因此没有“基地址”。在硬重置时,在 CPU 固件完成执行后,将执行平台固件,通常是传统 BIOS 或 UEFI。给定的主板仅与有限的一组 CPU 集合兼容,这些 CPU 集合通常具有相同的方法来发现物理内存,包括 DIMM 和平台固件内存设备。平台固件的实现使用此方法构建内存描述条目表,其中每个条目描述物理内存地址范围。有关此处理器外观的更多信息,请参阅:How Does BIOS initialize DRAM?。该表存储在主内存 (DIMM) 中的一个地址处,已知该地址是为此目的保留的,并且应该由实际内存支持(系统可以在没有任何 DIMM 的情况下启动)。

自 90 年代中期以来 x86 PC BIOS 的大多数实现都提供实模式 INT 15h E820h 函数(15h 是中断号,E820h 是在 AX 寄存器中传递的参数)。这是特定于供应商的 BIOS 功能,首先在 PhoenixBIOS v4.0(1992-1994,我无法确定确切年份)中引入,后来被其他 BIOS 供应商采用。该接口由 1996 年发布的 ACPI 1.0 规范扩展,PhoenixBIOS 的后续修订版支持 ACPI。对应的 UEFI 接口是 GetMemoryMap(),它是 UEFI 启动时服务(意味着它只能在 UEFI 规范中定义的启动时调用)。内核可以使用这些接口之一来获取描述所有 NUMA 节点上的内存的地址映射。 x86 平台上的其他(旧)方法在 Detecting Memory (x86) 中讨论。 ACPI 规范都以 version 开头?和 UEFI 规范从版本开始?支持 DRAM DIMM 和 NVDIMM 内存范围类型。

例如,考虑 ACPI 兼容的 Linux 内核如何确定哪些物理地址范围在支持 x86 ACPI 的 BIOS 平台上可用(即由实际内存支持)和可用(即免费)。 BIOS 固件将引导加载程序从指定的可引导存储设备加载到专用于此目的的内存位置。固件完成执行后,它会跳转到引导加载程序,引导加载程序将在存储介质上找到内核映像,将其加载到内存中,并将控制权转移给内核。引导加载程序本身需要知道当前的内存映射并为其操作分配一些内存。它尝试通过调用 E820h 函数获取内存映射,如果不支持,它将求助于较旧的 PC BIOS 接口。 kernel boot protocol 定义引导加载程序可以使用哪些内存范围以及哪些内存范围必须留给内核使用。

引导加载程序本身不会修改内存映射或向内核提供映射。相反,当内核开始执行时,它会调用 E820h 函数并将一个 20 位的指针(在 ES:DI 中)传递给它,该指针指向内核知道在 x86 平台上空闲的缓冲区,根据启动协议。每次调用都会返回一个内存范围描述符,其大小至少为 20 字节。有关更多信息,请参阅 ACPI 规范的最新版本。大多数 BIOS 实现都支持 ACPI。

假设 Linux 内核具有上游默认引导参数,您可以使用命令 dmesg | grep 'BIOS-provided\|e820' 查看返回的内存范围描述符表。在我的系统上,它看起来像这样:

[    0.000000] BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x00000000000917ff] usable
[    0.000000] BIOS-e820: [mem 0x0000000000091800-0x000000000009ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000000e0000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000d2982fff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d2983000-0x00000000d2989fff] ACPI NVS
[    0.000000] BIOS-e820: [mem 0x00000000d298a000-0x00000000d2db9fff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d2dba000-0x00000000d323cfff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000d323d000-0x00000000d7eeafff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d7eeb000-0x00000000d7ffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000d8000000-0x00000000d875ffff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d8760000-0x00000000d87fffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000d8800000-0x00000000d8fadfff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d8fae000-0x00000000d8ffffff] ACPI data
[    0.000000] BIOS-e820: [mem 0x00000000d9000000-0x00000000da718fff] usable
[    0.000000] BIOS-e820: [mem 0x00000000da719000-0x00000000da7fffff] ACPI NVS
[    0.000000] BIOS-e820: [mem 0x00000000da800000-0x00000000dbe11fff] usable
[    0.000000] BIOS-e820: [mem 0x00000000dbe12000-0x00000000dbffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000dd000000-0x00000000df1fffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000f8000000-0x00000000fbffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fec00000-0x00000000fec00fff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fed00000-0x00000000fed03fff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fed1c000-0x00000000fed1ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fee00000-0x00000000fee00fff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000ff000000-0x00000000ffffffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000041edfffff] usable
[    0.002320] e820: update [mem 0x00000000-0x00000fff] usable ==> reserved
[    0.002321] e820: remove [mem 0x000a0000-0x000fffff] usable
[    0.002937] e820: update [mem 0xdd000000-0xffffffff] usable ==> reserved
[    0.169287] e820: reserve RAM buffer [mem 0x00091800-0x0009ffff]
[    0.169288] e820: reserve RAM buffer [mem 0xd2983000-0xd3ffffff]
[    0.169289] e820: reserve RAM buffer [mem 0xd2dba000-0xd3ffffff]
[    0.169289] e820: reserve RAM buffer [mem 0xd7eeb000-0xd7ffffff]
[    0.169289] e820: reserve RAM buffer [mem 0xd8760000-0xdbffffff]
[    0.169290] e820: reserve RAM buffer [mem 0xd8fae000-0xdbffffff]
[    0.169291] e820: reserve RAM buffer [mem 0xda719000-0xdbffffff]
[    0.169291] e820: reserve RAM buffer [mem 0xdbe12000-0xdbffffff]
[    0.169292] e820: reserve RAM buffer [mem 0x41ee00000-0x41fffffff]

该表中描述了以“BIOS-e820”开头的内存范围。第一行清楚地告诉您此信息的来源。此信息的确切格式取决于 Linux 内核版本。在任何情况下,您都会在每个条目中看到一个范围和一个类型。以“e820”开头的行(没有“BIOS-”部分)是内核本身对表所做的更改。 E820h 的实现可能有问题,或者不同条目中获得的范围之间可能存在重叠。内核执行必要的检查并相应地进行更改。标记为“可用”的范围大多可供内核免费使用,但 ACPI 规范中讨论的例外情况以及内核知道的情况除外。绝大多数 PC BIOS 实现最多返回 128 个内存范围描述符。旧版本的 Linux 内核最多只能处理 128 个内存范围,因此从 E820h 返回的任何超过 128 个的条目都将被忽略。从版本 ? 开始,此限制已放宽。有关详细信息,请参阅标题为“x86 启动:通过设置数据的链表传递超过 128 个 E820 内存映射条目”的内核补丁系列。

usableACPI data 类型的范围。 reserved 类型的范围由 DRAM DIMM 支持,或者由 CPU 或平台固件为 MMIO 砍掉。 ACPI NVS 类型的范围由固件内存支持。就固件而言,所有其他范围都不会由实际内存支持。请注意,固件可能会选择不映射所有已安装的 DRAM DIMM 或 NVDIMM。如果物理内存配置不受支持,或者固件由于 DIMM 中的问题而无法从已安装的 DIMM 获取信息,则可能会发生这种情况。

您可以计算固件为内核提供了多少已安装 DRAM DIMM 和 NVDIMM 的内存。在我的系统上,我安装了 16 GB 的 DRAM DIMM。因此,除非某些 DIMM 安装不正确、无法正常运行、固件中存在错误或平台或处理器不支持,否则内核可用的内存应该少于 16 GB。

所有 usable 范围加起来为 0x3FA42B800 字节。请注意,范围的最后一个地址是包含的,这意味着它指向作为范围一部分的字节位置。物理安装的 DIMM 总数为 16 GB 或 0x400000000 字节。因此,未为内核提供的已安装内存总量为 0x400000000 - 0x3FA42B800 或 16 GB 总内存中的约 92 MB。该内存被部分 reserved 范围和所有 ACPI data 范围占用。如果平台固件确定 DRAM DIMM 或 NVDIMM 中的某些位置不可靠,它们也将被切掉为 reserved

请注意,根据 ACPI 规范,E820 内存映射中未描述范围 0x000a0000-0x000fffff。这是 640KB-1MB 的上层存储区。内核会打印一条消息,指出它已从可用内存区域中删除了此范围以保持与旧系统的兼容性。

此时,大多数 PCIe 设备用作 MMIO 的内存尚未分配。我的处理器支持 39 位物理地址空间,这意味着 0 到 2^39 之间的地址可用于映射。到目前为止,只有这个空间的最底部 16.5 GB 已映射到某些内容。请注意,此范围内仍有未映射的间隙。内核可以使用这些间隙(几百 MB)和其余的物理地址空间(大约 495.5 GB)来为 IO 设备分配地址范围。内核最终会发现 PCIe 设备,并且对于每个设备,它会尝试加载兼容的驱动程序(如果可用)。然后驱动程序确定设备需要多少内存以及设备对内存地址施加的任何限制,并请求内核为设备分配内存并将其配置为设备拥有的 MMIO 内存。您可以使用 sudo cat /proc/iomem 命令查看最终的内存映射。

在某些情况下,您需要手动更改现有内存范围的内存类型(例如,用于测试)、创建新范围(例如 for emulating persistent memory on DRAM 或固件无法发现所有无论出于何种原因,可用内存的数量),减少内核可用的内存量(例如,防止裸机管理程序使用超出限制的内存并使剩余的内存可供来宾使用),甚至完全覆盖整个表从 E820h 返回。 memmemmap 内核参数可用于此类目的。当这些参数中的一个或多个被指定为有效值时,内核将首先读取 BIOS 提供的内存映射并进行相应的更改。内核将最终的内存映射打印为“用户定义的物理 RAM 映射”。在内核消息环形缓冲区中。您可以使用 dmesg | grep user: 查看这些消息(每个内存范围行以“user:”开头)。这些消息将在“BIOS-e820”消息之后打印。

在使用支持兼容性支持模块的 UEFI 固件启动的 x86 平台上(有关更多信息,请参阅 CSM 规范,该规范与 UEFI 分开),支持旧实模式 E820h 接口和 Linux默认情况下内核仍然使用它。如果内核在具有不支持 CSM 的 UEFI 的 x86 平台上运行,E820h 接口可能不提供所有或任何内存范围。在此类平台上可能需要使用 add_efi_memmap 内核参数。可以在 UEFI Memory V E820 Memory 找到示例。当 GetMemoryMap() 提供了一个或多个内存范围时,内核会将这些范围与来自 E820h 接口的范围合并。可以使用 dmesg | grep 'efi:' 查看生成的内存映射 另一个影响内存映射的 UEFI 相关内核参数是 efi_fake_mem

ACPI 规范(第 6.3 节)提供了通知机制,以在 IO 或 DIMM 设备以任何 S 状态插入系统或从系统中移除时通知内核。 (不过,我不知道是否有任何主板支持在任何 S 状态下移除 DIMM。这通常仅在 G3 状态下可能,也可能在 S4 和/或 S5 下才有可能)发生此类事件时,内核或固件会相应地更改内存映射。这些更改反映在 sudo cat /proc/iomem 中。

,

pc 相对寻址 是指一种编程技术,您的程序可以在任何地址上运行。由于重定位寄存器(例如段)已经过时,大多数与 pc 相关的编程是显式执行的。下面是一个通用类型的机器代码示例:

.text
entry:
    call reloc  /* call is pc relative */
reloc:
    pop %r0     /* r0 now contains physical address of reloc */
    sub $reloc,%r0,%r14  /* r14 contains difference between link address of reloc */
/* At this point,r14 is a relocation register.  A virtual address + r14 == the corresponding physical address. */
    add $proot,%r14,%r0  /* physical address of page table root */
    add $entry,%r1  /* entry is where we were loaded into ram */
    test $0xfff,%r1   /* someone is being funny and not page aligning us */
    jnz bad_alignment
    or   $0x7,%r1     /* put mythical page protection bits in r1 */
    mov $1024,%r2     /* number of pages in r2 */
loop:
    store %r1,(%r0)   /* store a page table entry */
    add $0x1000,%r1   /* setup next one 4096 bytes farther */
    add $4,%r0        /* point to next page table entry */
    sub $1,r2         /* are we done? */
    cmp %0,r2
    jne loop           /* nope,setup next entry */
    add $proot,%r0
    loadsysreg %r0,page_table_base_register
    mov $1,%r0
    mov $v_entry,%r1
    loadsysreg %r0,page_table_enabled
    jmp %r1
v_entry:
        /* now we are virtually addressed */
    call main
1:  jmp 1b   /* main shouldn't return. */


.data
.align 12   /* 4096 byte pages */
proot:
.zero 4096
.text

这个神话机器非常简单,只有一个平面页表,内核链接在地址 0,但可以从前 4M (1024 * 4096) 的任何地方运行。真机只是更详细的版本。 通常,在初始地址空间设置之前,您甚至不能信任 C 这样的系统语言。一旦它是,其中的代码可以构建更复杂的页表,并查询像设备树这样的数据库,甚至像 apic/uefi 这样的怪物以获取有关 ram 布局的更多信息等。

在内部节点与叶节点(例如 x86-classic)格式兼容的前向映射页表架构中,您可以递归地使用单个页表以允许更灵活的链接地址。 例如,如果您将 proot 中的最后一个条目(即 proot[1023])指向回 proot,那么您可以在 0xffffc000 处链接您的操作系统,并且此代码将正常工作(一旦转换为 x86)。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。