在哪里执行?

我们继续看 arch/arm/kernel/head.S 处的符号 stext()

下一步就是处理在未知内存地址处运行的问题。

内核可以加载到任何地方(只要是合理的偶数地址即可)并执行,所以我们要处理这一点。注意内核代码不是位置无关的,内核经过编译和连接后,必须在特定的地址执行。但我们还不知道这个地址。

内核首先要检查一些特殊特性,如:虚拟化扩展、LPAE(大型物理地址扩展),然后进行以下操作:

    adr        r3, 2f                @将下面的 标号2 的地址(此地址是连接时指定的地址)传递给r3
    ldmia      r3, {r4, r8}            @将标号2处的地址传送给r4, PAGE_OFFSET值传递给r8
    sub        r4, r3, r4            @ (PHYS_OFFSET - PAGE_OFFSET)
    add        r8, r8, r4            @ PHYS_OFFSET

    (...)

2:  .long      .                    @在实际运行中 . 可以获取到当前的运行地址
    .long      PAGE_OFFSET

上面代码中.long .的作用:在连接时赋值为标签 2: 处的地址,所以 . 会解析为 标号 2: 实际被连接到的地址,连接器认为该地址会被定位到内存中。该地址将位于内核指定的某块虚拟内存中,即通常位于 0xC0000000 上方的某个地方。

之后就是编译好的常量PAGE_OFFSET,我们已经知道它的值大概为 0xC0000000。

我们将 2: 在编译时生成的地址加载到 r4 中,将常量 PAGE_OFFSET 加载到 r8 中。然后从r3中减去 2: 的真实地址r4。记住ARM汇编的参数顺序就像计算器一样,sub ra, rb, rc 相当于 ra = rb - rc。

这样在 r4 中得到的结果就是内核在编译时的运行地址和实际运行时的运行地址之间的偏移量。所以这里的注释 @ (PHYS_OFFSET - PAGE_OFFSET) 表明我们获得了该偏移量。如果内核符号 2: 在编译时的执行地址是虚拟内存中的 0xC0001234,但实际上在 0x10001234处执行,那么 r4 的值就是 0x10001234 - 0xC0001234 = 0x50000000。这个值的实际含义是“-0xB0000000”,因为这里的算术是可交换的:0xC0001234 + 0x50000000 = 0x10001234。证明完毕。

下面,将这个偏移量加到编译时确定的 PAGE_OFFSET 上。我们已知后者类似于 0xC0000000。使用循环算术,如果内核执行的实际地址还是 0x10001234,我们将得到 0xC0000000 + 0x50000000 = 0x10000000 并保存在 r8 中,这就是内核执行时的基址的物理地址。所以注释写的是 @PHYS_OFFSET 。r8中保存的这个值就是我们要使用的值。

旧的ARM内核中有一个叫做 PLAT_PHYS_OFFSET 的符号,其中包含的正是这个偏移量(如0x10000000),不过是在编译时确定的。现在已经不这样做了,而是在执行时动态确定。如果你的操作系统比Linux简单,那很可能会发现,开发人员通常会做出类似于“物理偏移量是常量”的假设进行简化。Linux发展到今天这种做法,是因为它需要在各种内存布局中引导同一个内核。

03_执行 - 图1


图:本文示例中的物理内存到虚拟内存的映射。

关于 PHYS_OFFSET 有一些规则:它需要满足一些基本的对齐要求。在确定第一个解压后的代码中的第一个物理内存块的位置时,我们执行 PHYS = pc & 0xF8000000,意思是物理RAM必须从偶数的128MB边界上开始。例如从 0x00000000 开始就很好。

这段代码考虑了XIP(execute in place,原地执行)的一些特殊情况,例如内核直接从ROM中执行,不过这里不再讨论,因为这种情况更罕见,甚至比不使用虚拟内存的情况还罕见。

还有另一点需要注意。如果你尝试过加载一个解压后的内核并引导,就会发现它对于加载位置非常挑剔——必须放在类似于 0x00008000 或 0x10008000 (假设你的 TEXT_OFFSET 为 0x8000)之类的地址上。而使用压缩后的内核就没有这个问题,因为解压缩程序会将内核解压到合适的位置上(大多数情况下为 0x00008000),所以这个问题解决了。这就是人们常常觉得压缩后的内核“更好用”的原因。