ARM32 Linux内核如何解压缩

2020-08-14 00:09:05

它节省了闪存或保存内核的其他存储介质上的空间,而内存就是金钱。例如,对于我工作的Gemini平台,vmlinux未压缩内核是11.8MB,而压缩的zImage只有4.8MB,我们节省了50%以上。

加载速度更快,因为运行解压缩所需的时间比从存储介质(如闪存)传输未压缩图像所需的时间要短。对于NAND闪存控制器,很容易出现这种情况。

本文旨在全面介绍Linux内核如何在ARM 32位遗留系统上自解压。如果使用压缩内核引导,则Arch/arm/*下的所有机器都使用此方法,并且大多数机器都使用压缩内核。

引导加载程序(无论是RedBoot、U-Boot还是EFI)将内核映像放在物理内存中的某个位置,并通过传递较低寄存器中的一些参数来执行它。

Russell King在2002年的Boting arm Linux文档中定义了用于从Bootloader引导Linux内核的ABI。引导加载程序将0放入寄存器R0,将架构ID放入寄存器R1,并将指针放入寄存器R2中的ATAG。ATAG将包含物理内存的位置和大小。内核会放在这个内存中的某个地方。只要解压缩的内核合适,它就可以从任何地址执行。然后,引导加载程序在管理程序模式下跳转到内核,禁用所有中断、MMU和缓存。

在当代设备树内核中,R2被重新用作指向物理内存中的设备树BLOB(DTB)的指针。(在本例中,将忽略R1。)。还可以将DTB附加到内核映像,并可选择使用R2中的ATAG进行修改。我们将在下面更多地讨论这个问题。

如果内核是压缩的,则执行开始于位于文件下面一点的符号start:中的arch/arm/boot/combedded/head.S中。(这一点并不是立竿见影的。)。由于传统原因,它以8或7个NOP说明开始。它跳过一些魔术数字并保存指向ATAG的指针。因此,现在内核解压缩代码是从加载它的物理内存的物理地址执行的。

然后,解压缩代码定位物理内存的起始位置。在大多数现代平台上,这是通过KCONFIG选择的代码AUTO_ZRELADDR来完成的,这意味着程序计数器与0xf8000000之间的逻辑与。这意味着内核很容易假设它已经在物理内存的第一块的第一部分中加载和执行。

正在制作的补丁程序会尝试从设备树中获取此信息。

然后,将TEXT_OFFSET添加到指向物理内存开始的指针。顾名思义,这就是内核.text段(作为编译器的输出)应该位于的位置。Text段包含可执行代码,因此这是解压缩后内核的实际起始地址。TEXT_OFFSET通常为0x8000,因此内核将位于物理内存中的0x8000字节。这在ARCH/ARM/Makefile中定义。

0x8000(32KB)偏移量是一种惯例,因为通常有一些固定架构特定的数据放置在0x00000000处,例如中断向量,而许多较老的系统将ATAG放置在0x00000100处。还必须有一些空间,因为当内核最终引导时,它将从该地址减去0x4000(对于LPAE,减去0x5000),并将初始内核页表存储在那里。

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

下一步,解压缩代码建立一个页表,如果可能的话,将一个页表放在整个未压缩+压缩的内核映像上。页表不是用于虚拟内存,而是用于启用缓存,然后打开缓存。由于自然原因,如果我们可以使用缓存,解压缩将会快得多。

接下来,内核设置一个本地堆栈指针和malloc()区域,这样我们就可以继续处理子例程调用和小内存分配,执行用C编写的代码。这被设置为正好指向内核映像结束之后。

接下来,我们检查由ARM_APPENDED_DTB符号启用的附加DTB blob。这是在构建期间添加到zImage的DTB,通常使用简单的cat foo.dtb>;>;zImage。DTB使用幻数0xD00DFEED标识。

如果找到附加的DTB,并且设置了CONFIG_ARM_ATAG_DTB_COMPAT,我们首先将DTB扩展50%,并调用atags tofdt,这将使用ATAG中的信息(如内存块和大小)来增加DTB。

下一个。Dtb指针(在开始时作为r2传入)被指向附加的dtb的指针覆盖,我们还保存了dtb的大小,并将内核映像的末尾设置在dtb之后,以便将附加的dtb(可选地使用ATAG修改)包括在压缩内核的总大小中。如果找到附加的DTB,我们还会转移堆栈和malloc()位置,这样就不会破坏DTB。

注意:如果在R2中传入了设备树指针,并且还提供了附加的DTB,则附加的DTB将“获胜”,并且是系统将使用的。这有时可用于覆盖引导加载程序传递的默认DTB。

注意:如果在R2中传入ATAG,则肯定没有DTB通过该寄存器传入。如果您使用不想替换的旧引导加载程序,您几乎总是需要CONFIG_ARM_ATAG_DTB_COMPAT符号,因为ATAG正确地定义了旧平台上的内存。可以在设备树中定义内存,但通常情况下,人们会跳过这一步,而依赖引导加载程序以某种方式(引导加载程序更改DTB)或另一种方式(ATAG在引导时增加附加的DTB)来提供此功能。

接下来,我们检查是否要用未压缩内核覆盖压缩内核。那将是不幸的。如果发生这种情况,我们会检查内存中未压缩内核的结束位置,然后将自己(压缩内核)复制到该位置。

然后,代码简单地跳回到名为Restart的标签的重新定位地址:这是设置堆栈指针和malloc()区域的代码的开始,但现在在新的物理地址执行。

这意味着它将再次设置堆栈和malloc()区域,并查找附加的dtb,一切看起来就像内核从一开始就加载到这个位置一样。(但有一个不同之处:我们已经用ATAG增强了DTB,所以不会再这样做了。)。这一次,未压缩的内核不会覆盖压缩的内核。

不会检查内存是否用完,也就是说,我们是否碰巧将内核复制到物理内存的末尾之外。如果发生这种情况,结果是不可预测的。如果内存为8MB或更小,在以下情况下可能会发生这种情况:请勿使用压缩内核。

现在我们知道内核可以解压缩到压缩映像下面的内存中,并且它们在解压缩过程中不会发生冲突,我们在标签WUNT_OVERWRITE:处执行。

我们检查是否在解压缩器所链接的地址上执行,并可能更改一些指针表。这是用于执行解压缩器的C运行时环境。

我们确保缓存已打开。(不一定有空间容纳页表。)。

我们清除BSS区域(因此所有未初始化的变量都将为0),这也适用于C运行时环境。

接下来,我们调用boot/combedded/misc.c中的decpress_kernel()符号,该符号依次调用do_decumpress(),后者调用将执行实际解压缩的__decumpress()。

这是用C语言实现的,解压缩类型根据Kconfig选项的不同而不同:与构建内核时选择的压缩相同的解压缩器将链接到映像中,并从物理内存执行。所有架构共享相同的解压缩库。所调用的__解压缩()函数将取决于lib/despress_*.c中链接到映像的解压缩器。只需将整个解压缩器包含到文件中,就可以在Arc/arm/boot/combedded/decpress.c中选择解压缩器。

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

解压缩后,解压缩的内核位于TEXT_OFFSET,附加的dtb(如果有的话)保留在压缩内核所在的位置。

解压缩之后,我们调用get_flumated_image_size()来获得最终的解压缩内核的大小。然后,我们再次刷新并关闭缓存。

然后,我们跳到符号__enter_kernel,它将R0、R1和R2设置为引导加载程序应该离开它们,除非我们连接了设备树BLOB,在这种情况下,R2现在指向该DTB。然后,我们将程序计数器设置为内核的开始,即物理内存的开始加上TEXT_OFFSET,在非常传统的系统上通常是0x00008000,在一些QUALCOMM系统上可能是0x20008000。

我们现在处于相同的位置,就好像我们已经将一个未压缩的内核(vmlinux文件)加载到内存中的text_offset处,传递(通常)R2中的一个设备树。

未压缩的内核在符号stext()、文本段的开始处开始执行。可以在arch/arm/kernel/head.S中找到此代码。

这是另一个讨论的主题。但是,请注意,此处的代码并不查找附加的设备树!如果应该使用附加的设备树,则必须使用压缩内核。这同样适用于使用ATAG扩充任何设备树。它还必须使用压缩内核映像,因为执行此操作的代码是引导压缩内核的程序集的一部分。

首先,您需要启用CONFIG_DEBUG_LL,它使您能够在UART控制台上敲打出字符,而不需要任何更高级打印机制的干预。它所做的一切就是向UART提供物理地址,并提供例程来轮询推送字符。它设置DEBUG_UART_PHYS,以便内核知道物理UART I/O区域的位置。确保这些定义是正确的。

首先启用名为CONFIG_DEBUG_UNCOMPRESS的KCONFIG选项。所有这些操作都是打印短消息“解压缩linux…”。在解压缩内核之前,并且在解压缩之后“完成,引导内核”。这是一个很好的冒烟测试,可以显示CONFIG_DEBUG_LL已设置,DEBUG_UART_PHYS正确,解压正在工作,但仅此而已。这不提供任何低级调试。

可以通过启用ARCH/ARM/BOOT/COMPRESSED/head.S中定义的调试来调试和检查实际的解压缩内核解压缩,最简单的方法是将ON-DDEBUG标记为ARCH/ARM/BOOT/COMPRESSED/Makefile中head.S的AFLAGS(汇编程序标志),如下所示:

这意味着在我们引导时,我将内核加载到0x40300000,这将与未压缩的内核发生冲突。因此,内核被复制到0x41801D00,这是未压缩内核的结束位置。添加一些进一步的调试打印,我们可以看到附加的dtb首先在0x40DEBA68处找到,向下移动内核后在0x422E56A8处找到,这是内核引导时保留的位置。