以下讨论基于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 配置为起点内存)。所以在打开 MMU 之前,需要非常仔细地编写汇编代码,以确保在执行过程中不会出错。实际上使用了一种称为位置无关代码(PIC)的技术。
为了进一步解释上面,我提取了几个汇编代码sn-ps:
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 中进行初始映射。在函数__create_page_tables 中,只映射了很小一部分的地址空间。
之后,在函数paging_init中建立了最终的页表,并且在这个函数中,映射了相当大一部分的地址空间。假设您只有 512M RAM,对于大多数常见配置,这 512M-RAM 将由内核代码逐段映射(1 段为 1MB)。如果您的 RAM 非常大(例如 2GB),则只有一部分 RAM 会被直接映射。
(我将在这里停止,因为关于问题2的细节太多了)
- 开启页表前后映射地址相同?
答:我想我已经在我对“第二个技巧:在打开 MMU 之前设置初始页表时的相同映射”的解释中回答了这个问题。
4 .内核空间中的页表是全局的,将被共享
系统中的所有进程,包括用户进程?
A:是的,不是的。是的,因为所有进程共享内核页表的相同副本(内容)(较高的 1GB 部分)。不,因为每个进程都使用自己的 16KB 内存来存储内核页表(尽管更高 1GB 部分的页表内容对于每个进程都是相同的)。
5 .这个机制在 x86 32bit 和 ARM 中是一样的吗?
不同的架构使用不同的机制