下面的讨论基于32位ARM Linux,内核源代码版本为3.9,
如果你通过设置初始页表(稍后将被函数覆盖paging_init
)和转向的过程来解决你所有的问题在 MMU 上。
当内核首次由引导加载程序启动时,汇编函数stext
(在 arch\arm\kernel\head.s 中)是第一个运行的函数。请注意,此时 MMU 尚未打开。
除其他外,此函数完成的两个导入工作stext
是:
- 创建初始页面表(稍后将被函数覆盖
paging_init
)
- 打开 MMU
- 跳转到内核初始化代码的C部分并继续
在深入研究您的问题之前,了解以下内容是有益的:
- 在MMU开启之前,CPU下发的每一个地址都是物理地址
- MMU开启后,CPU下发的每一个地址都是虚拟地址
- 在打开 MMU 之前应该设置一个正确的页表,否则你的代码将简单地“被吹走”
- 按照惯例,Linux 内核使用较高的 1GB 虚拟地址部分,而用户空间使用较低的 3GB 部分
现在是棘手的部分:
第一个技巧:使用与位置无关的代码。汇编函数stext链接到地址“ PAGE_OFFSET + TEXT_OFFSET
”(0xCxxxxxxx),这是一个虚拟地址,但是由于MMU还没有开启,所以汇编函数stext实际运行的地址是“ PHYS_OFFSET + TEXT_OFFSET
”(实际值取决于你的实际硬件),这是一个物理地址。
所以,事情是这样的:函数程序stext
“认为”它运行在像 0xCxxxxxxx 这样的地址,但实际上它运行在地址 (0x00000000 + some_offeset) 中(假设你的硬件将 0x00000000 配置为 RAM 的起点)。所以在打开 MMU 之前,需要非常仔细地编写汇编代码,以确保在执行过程中不会出错。事实上,使用了一种称为位置无关代码 (PIC) 的技术。
为了进一步解释上述内容,我提取了几个汇编代码片段:
ldr r13, =__mmap_switched @ address to jump to after MMU has been enabled
b __enable_mmu @ jump to function "__enable_mmu" to turn on MMU
请注意,上面的“ldr”指令是一条伪指令,意思是“获取函数__mmap_switched的(虚拟)地址并将其放入r13”
函数 __enable_mmu 依次调用函数 __turn_mmu_on:(请注意,我从函数 __turn_mmu_on 中删除了几条指令,这些指令是函数的基本指令,但不符合我们的兴趣)
ENTRY(__turn_mmu_on)
mcr p15, 0, r0, c1, c0, 0 @ write control reg to enable MMU====> This is where MMU is turned on, after this instruction, every address issued by CPU is "virtual address" which will be translated by MMU
mov r3, r13 @ r13 stores the (virtual) address to jump to after MMU has been enabled, which is (0xC0000000 + some_offset)
mov pc, r3 @ a long jump
ENDPROC(__turn_mmu_on)
第二个技巧:在打开 MMU 之前设置初始页表时的相同映射。更具体地说,内核代码运行的同一地址范围被映射两次。
- 正如预期的那样,第一个映射将地址范围 0x00000000(同样,此地址取决于硬件配置)到(0x00000000 + 偏移量)映射到 0xCxxxxxxx 到(0xCxxxxxxx + 偏移量)
- 有趣的是,第二个映射将地址范围 0x00000000 到(0x00000000 + 偏移量)映射到自身(即:0x00000000 -->(0x00000000 + 偏移量))
为什么这样做?请记住,在 MMU 开启之前,CPU 发出的每个地址都是物理地址(从 0x00000000 开始),而在 MMU 开启之后,CPU 发出的每个地址都是虚拟地址(从 0xC0000000 开始)。
因为ARM是流水线结构,所以在MMU开启的那一刻,ARM的管道中仍有指令在使用MMU开启前CPU产生的(物理)地址!为了避免这些指令被炸毁,必须设置一个相同的映射来满足它们。
现在回到你的问题:
- 这时候打开页表后内核空间还是1GB(从0xC0000000-0xFFFFFFFF)?
A:我猜你的意思是打开MMU。答案是肯定的,内核空间是1GB(实际上它也占用了0xC0000000以下的几兆字节,但这不是我们感兴趣的)
- 并且在内核进程的页表中,只映射了 0xC0000000 - 0xFFFFFFFF 范围内的页表条目(PTE)?超出此范围的 PTE 将不会被映射,因为内核代码永远不会跳转到那里?
答:虽然这个问题的答案相当复杂,因为它涉及到很多关于特定内核配置的细节。
要完全回答这个问题,您需要阅读内核源代码中设置初始页表的部分(汇编函数__create_page_tables
)和设置最终页表的函数(C 函数 paging_init)。
简单来说,ARM中有两级页表,第一级页表是PGD,占用16KB。内核在初始化过程中首先将该 PGD 清零,并在汇编函数中进行初始映射__create_page_tables
。在 function__create_page_tables
中,只有很小一部分的地址空间被映射。
之后,在函数中建立最终的页表paging_init
,并且在这个函数中,映射了相当大一部分的地址空间。假设您只有 512M RAM,对于大多数常见配置,这 512M-RAM 将由内核代码逐段映射(1 段为 1MB)。如果您的 RAM 非常大(例如 2GB),则只有一部分 RAM 会被直接映射。(我将在这里停止,因为关于问题 2 的细节太多了)
- 打开页表前后的映射地址是一样的吗?
A:我想我在“第二招:在打开MMU之前设置初始页表时的相同映射”的解释中已经回答了这个问题。
4. 内核空间中的页表是全局的,将在系统中的所有进程(包括用户进程)之间共享?
答:是的,不是的。是的,因为所有进程共享内核页表的相同副本(内容)(较高的 1GB 部分)。不,因为每个进程都使用自己的 16KB 内存来存储内核页表(尽管更高 1GB 部分的页表内容对于每个进程都是相同的)。
5. 这种机制在 x86 32bit 和 ARM 中是否相同?
不同的架构使用不同的机制