ARM Linux 启动时的自解压过程 | Linux 内核

2021-08-26 17:48:49 浏览数 (1)

大家好,我是工具人老吴。

最近业余时间都在学习 Linux 内核和英语,或者是陪家人玩耍,没有投入太多的时间在文章。

今天起得比较早,就给大家翻译一篇 linus 的文章吧,大家可以感受一下大神的写作能力。

水平有限,建议搭配原文阅读。

另外,由于最近英语提升了不少,以后争取多翻译一些干货给大家。

OK,Let's go.


ARM Linux 一般都使用压缩的内核,例如 zImage。

这样做有两个主要原因:

1、节省存放内核的闪存或其他存储介质的空间。

例如,在我工作的平台上,vmlinux 未压缩的内核是 11.8 MB,而压缩后的 zImage 只有4.8MB,节省了 50% 以上的空间。

2、加载速度更快。

通常情况下,解压消耗的时间比从存储介质传输未压缩镜像的时间要短。

例如从 NAND Flash 加载内核,就是一种很典型的情况。

本文将对 ARM Linux 的自解压过程进行一个简单介绍。arch/arm/* 下的大多数机器都会使用压缩的内核,其自解压过程是一样的。

Bootloader

Bootloader,无论是 RedBoot、U-Boot 还是 EFI,都将内核映像放置在物理内存中的某个位置,并通过寄存器传递一些参数来执行它。

2002 年,Russell King 就在 Booting ARM Linux 文档中定义了 Booloader 引导 Linux 内核的 ABI。

Bootloader 将 0 放入寄存器 r0,将 Machine ID 放入寄存器 r1,并将指向 ATAG 的指针放入寄存器 r2。

ATAG 包含物理内存的位置和大小,内核被放置在该内存中的某个位置。只要确保有足够解压内核的空间,它就能从任何地址执行。

然后 Bootloader 会在管理模式下跳转到内核,此时,所有中断、MMU 和缓存都是 disabled。

在现代设备树内核中,r2 被重新用作指向物理内存中设备树 (DTB) 的指针。在这种情况下,r1 被忽略。DTB 也可以附加到内核映像后,并且可以选择使用来自 r2 的 ATAG 进行修改。

zImage 的解压

如果使用的是压缩内核,则执行开始于 arch/arm/boot/compressed/head.S 中的 start 符号。出于遗留原因,它以 8 或 7 个 NOP 指令开头。它跳过一些幻数并保存指向 ATAG 的 指针 (r2)。现在,内核解压代码 (The decompression code) 将从它被加载的物理内存的物理地址开始执行。

代码语言:javascript复制
arch/arm/boot/compressed/head.S

start:
  [...]
  .word _magic_sig @ Magic numbers to help the loader
  .word _magic_start @ absolute load/run zImage address
  .word _magic_end @ zImage end address
  .word 0x04030201 @ endianness flag

  __EFI_HEADER
  [...]
  mov r7, r1   @ save architecture ID
  mov r8, r2   @ save atags pointer

解压代码首先会确定物理内存的起始位置。在大多数现代平台上,这是通过 Kconfig 选择 AUTO_ZRELADDR 完成的,使能这个配置后,内核会通过将 PC 寄存器进行 128MB 的对齐的方式来获得物理内存的起始地址,内核总是假设它是在物理内存的第一块的第一部分加载和执行。

代码语言:javascript复制
arch/arm/boot/compressed/head.S

#ifdef CONFIG_AUTO_ZRELADDR
  /*
   * Find the start of physical memory.  As we are executing
   * without the MMU on, we are in the physical address space.
   * We just need to get rid of any offset by aligning the
   * address.
   *
   * This alignment is a balance between the requirements of
   * different platforms - we have chosen 128MB to allow
   * platforms which align the start of their physical memory
   * to 128MB to use this feature, while allowing the zImage
   * to be placed within the first 128MB of memory on other
   * platforms.  Increasing the alignment means we place
   * stricter alignment requirements on the start of physical
   * memory, but relaxing it means that we break people who
   * are already placing their zImage in (eg) the top 64MB
   * of this range.
   */
  mov r4, pc
  and r4, r4, #0xf8000000
  /* Determine final kernel image address. */
  add r4, r4, #TEXT_OFFSET
#else
  ldr r4, =zreladdr
#endif

然后会计算物理内存起始地址 TEXT_OFFSET 。

TEXT_OFFSET,顾名思义,这是内核 .text 段应位于的位置。.text 段包含可执行代码,因此这就是解压缩后内核的实际起始地址。TEXT_OFFSET 通常为 0x8000,因此解压后的内核将位于物理内存起始地址 TEXT_OFFSET 。TEXT_OFFSET 在 arch/arm/Makefile 中定义。

0x8000 (32KB) 偏移量是一个惯例,因为通常有一些固定的架构相关的数据放置在 0x00000000 处,例如中断向量,许多旧系统将 ATAG 放置在 0x00000100 处。另外还需要额外的空间,是因为当内核最终启动时,它将从该地址中减去 0x4000(或 LPAE 的 0x5000),并将初始内核页表 (initial kernel page table) 存储在那里。

对于某些特定平台,TEXT_OFFSET 将在内存中向后扩展,特别是一些高通平台会将其扩展到 0x00208000,因为物理内存的第一个 0x00200000 (2 MB) 用于与 modern CPU 的共享内存通信。

接下来,如果能映射解压前的内核和解压后的内核所在的区域的话,解压代码会设置一个页表,。这个页表不是为了使用虚拟内存,而是为了解压前能使能 cache,从而获得更快的解压速度。

接下来,内核设置一个局部的栈指针和 malloc() 区域,以便我们可以处理子例程调用和进行小内存分配,简单地说,就是可以执行 C 代码了。

appended dtb 的内核

接下来,我们检查由 ARM_APPENDED_DTB 符号启用的附加 DTB 。这是在编译期间添加到 zImage 的 DTB,其实就是 cat foo.dtb >> zImage。DTB 使用幻数 0xD00DFEED 进行标识。

如果找到附加的 DTB,并设置了 CONFIG_ARM_ATAG_DTB_COMPAT,我们首先将 DTB 扩展 50% 并调用 atagstofdt,它将使用来自 ATAG 的信息(例如内存块和内存大小)来扩充 DTB。

然后,DTB 指针(开始时由 r2 传入的)被指向附加 DTB 的指针覆盖,DTB 的大小也会被保存,并且更新内核映像的末端地址为 kernel image end dtb size,以便附加 DTB(可选) 使用 ATAG 修改)包含在压缩内核的总大小中。同时,还会上调栈和 malloc() 的位置,以避免 DTB 被破坏。

注意:如果在 r2 中传入了设备树指针,并且还提供了附加的 DTB,则系统使用附加的 DTB。偶尔会使用这个技巧来覆盖 Bootloader 传递的默认 DTB。

注意:如果在 r2 中传入 ATAG,则肯定没有通过该寄存器传入的 DTB。如果你不想替换掉老版本的 bootloader,你几乎总是需要 CONFIG_ARM_ATAG_DTB_COMPAT 符号,因为 ATAG 正确定义了旧平台上的内存。另外,确实可以可以在设备树中定义内存,但通常情况下,人们都不会这么做,而是并依靠 bootloader 来提供内存信息:一种方式是 bootloader 修改 DTB,另一种方式是 ATAG 和 DTB 在启动时一起协同工作。

解压后的内核可能与压缩的内核重叠

接下来,我们检查解压后的内核是否会覆盖压缩内核。如果发生这种情况,则需要先确定解压后的内核将在内存中的哪个位置结束,然后内核会将自己(压缩内核)复制到该位置。

然后代码会巧妙地跳回到一个叫做 restart 的标签的重定位地址:这是设置栈指针和 malloc() 区域的代码的起始位置,但现在在新的物理地址处执行。

这意味着将再次设置栈和 malloc() 区域并查找附加的 DTB,一切看起来就像内核第一次加载到此位置一样开始。(但有一个区别:我们已经用 ATAG 扩充了 DTB,因此不会再重复次步骤了。),但是这次解压出来的内核将不会覆盖压缩内核。

move the compressed kernel down so the decompressed kernel can fit.

这些操作不会检查内存是否用完,即我们是否会碰巧将内核复制到物理内存的末尾。如果发生这种情况,结果是不可预测的。如果内存为 8MB 或更少,就会发生这种情况,在这些情况下: 不要使用压缩内核。

The compressed kernel is moved below the decompressed kernel.

现在我们知道内核可以解压缩到压缩镜像下方的内存中,并且它们在解压缩过程中不会发生重叠,现在可以开始执行wont_overwrite 处的代码了。

代码语言:javascript复制
arch/arm/boot/compressed/head.S

wont_overwrite:
/*
 * If delta is zero, we are running at the address we were linked at.
 *   r0  = delta
 *   r2  = BSS start
 *   r3  = BSS end
 *   r4  = kernel execution address (possibly with LSB set)
 *   r5  = appended dtb size (0 if not present)
 *   r7  = architecture ID
 *   r8  = atags pointer
 *   r11 = GOT start
 *   r12 = GOT end
 *   sp  = stack pointer
 */
 # 省略
 [...]

 /*
 * The C runtime environment should now be setup sufficiently.
 * Set up some pointers, and start decompressing.
 *   r4  = kernel execution address
 *   r7  = architecture ID
 *   r8  = atags pointer
 */
 mov r0, r4
 mov r1, sp   @ malloc space above stack
 add r2, sp, #0x10000 @ 64k max
 mov r3, r7
 bl decompress_kernel

接着,我们检查解压器是否有被链接,并可能更改一些指针表,为执行解压缩器准备好 C 运行时环境。

确保 cache 已打开。

清除 BSS 区域(因此所有未初始化的变量都将为 0),也是在准备 C 运行时环境。

接下来调用 boot/compressed/misc.c 中的 decompress_kernel() ,它依次调用 do_decompress() ,后者调用 __decompress() ,并执行实际的解压缩。

代码语言:javascript复制
arch/arm/boot/compressed/decompress.c

#ifdef CONFIG_KERNEL_GZIP
#include "../../../../lib/decompress_inflate.c"
#endif

#ifdef CONFIG_KERNEL_LZO
#include "../../../../lib/decompress_unlzo.c"
#endif

#ifdef CONFIG_KERNEL_LZMA
#include "../../../../lib/decompress_unlzma.c"
#endif

#ifdef CONFIG_KERNEL_XZ
#define memmove memmove
#define memcpy memcpy
#include "../../../../lib/decompress_unxz.c"
#endif

#ifdef CONFIG_KERNEL_LZ4
#include "../../../../lib/decompress_unlz4.c"
#endif

int do_decompress(u8 *input, int len, u8 *output, void (*error)(char *x))
{
 return __decompress(input, len, NULL, NULL, output, 0, NULL, error);
}

这是在 C 中实现的,解压类型因 Kconfig 选项而异:与编译内核时选择的解压器将链接到映像并从物理内存执行。

所有架构共享同一个解压库。调用的 _decompress() 函数将取决于链接到图像的 lib/decompress*.c 中的哪个解压缩器。解压器的选择发生在 arch/arm/boot/compressed/decompress.c 中,只需将整个解压器包含到文件中即可。

在调用解压器之前,解压器需要的所有关于压缩内核位置变量都设置在寄存器中了。

0 人点赞