5.2 解压内核

根据构建内核时的分析,我们知道,内核的保护模式部分包括非压缩部分以及压缩部分,压缩部分才是内核正常运转时的部分,而非压缩部分只是一个过客,其主要作用是解压内核的压缩部分,解压完成后,非压缩部分也将退出历史舞台。

内核的解压缩过程几经演进,现在的解压过程不再是首先将内核解压到另外的位置,然后再合并到最终的目的地址。而是采用了所谓的就地解压(in-place decompression)方法,内核解压时并不需要解压到另外的位置,从而避免覆盖其他部分的数据。

以不可重定位的内核的解压过程为例,其解压过程如图5-7所示。

5.2 解压内核 - 图1

图 5-7 不可重定位内核的就地解压过程

对于不可重定位内核,最终解压后的的内核的起始位置是内核编译时设定的加载地址,即LOAD_PHYSICAL_ADDR。虽然解压的方式是就地解压,但是为了安全起见,解压过程所需要的内存空间并不完全等于解压后内核占据的空间,而是还预留有那么一点点安全空间。所以这个解压所需的空间,即图中标明的"In-place decompress buffer"的长度是解压后内核的长度z_output_len加上这个预留的安全空间。

为了确保在解压时,读取的位置永远在写入的位置的前面。内核首先移动到这个解压空间的最后。那么内核如何才能确保移动到这个空间的最后呢?内核只需从LOAD_PHYSICAL_ADDR向后移动z_extract_offset,就确保了内核映像移动到了这个解压空间的最后。

那么z_extract_offset以及图5-7中的几个数据,包括解压后内核的长度z_output_len等数据,都是从哪里获取的呢?这些数据当然是压缩内核的时候最清楚了,因此这些早已在内核编译时,进行压缩时就已经计算好了,定义在内核映像中:

5.2 解压内核 - 图2

piggy.S中定义的解压内核时需要的变量包括:

1)z_input_len,压缩内核的长度,即vmlinux.bin.gz的长度。

2)z_output_len,内核解压缩后的长度。

3)z_extract_offset,进行就地解压前,相对于解压后的位置,内核映像需要向后移动一段距离,为解压留出空间,避免解压的内核覆盖了压缩的内核,z_extract_offset就是这个偏移的大小。

4)z_extract_offset_negative,这个是z_extract_offset的负数,是为了编程方便定义的。

5)input_data,标识内核映像中,压缩部分的起始位置。

在解压缩后,非压缩部分根据需要可能还要对内核进行重定位符号,然后跳转到解压后的内核的入口startup_32。接下来,我们就具体讨论一下这个过程。

5.2.1 移动内核映像

1.确定源地址

前面讨论GRUB时,我们看到虽然GRUB按照引导协议的规定,将内核保护模式部分加载的地址,即code32_start写入了引导参数中,但是只有内核的“真正”部分投入运行时,内核才复制GRUB保存在低端内存的引导参数。也就是说,这个临时的负责解压部分,并没有复制GRUB保存的引导参数,因此,内核还需要自己计算映像被加载的地址。内核使用下面的代码获得当前被Bootloader加载的地址:

5.2 解压内核 - 图3

这里首先解释一下代码片段中的1f。1后面的f表示的是forward,即以该条指令为参照,继续向前来寻找1这个标号;如果1的后缀是b,则意义正好与此相反。

call指令执行时,首先会将该调用返回后执行的下一条指令的地址压栈,这里就是标号1标识的指令运行时的地址。执行了call指令后,程序跳转到标号1所在的代码行处执行,标号1所在行的代码将栈顶的内容弹出到寄存器ebp中。而此时栈顶的内容恰恰是执行call调用前CPU压入的标号1处的指令的地址,也就是说,寄存器ebp中保存的就是标号1标识的指令在运行时所在的地址。接下来,减去标号1这行代码相对于程序开头处的偏移,即$1b,就获得了函数startup_32的运行时地址。而函数startup_32就是内核开头,换句话说,就是获得了内核保护模式部分被GRUB实际加载的地址。

这段代码执行后,寄存器ebp中保存的就是内核的加载地址。

2.确定目的地址

内核映像移动的目的地址针对内核是否支持重定位需要区别计算。

(1)内核被编译为可重定位

如果内核被编译为可重定位,理论上内核映像被加载的地址就可以作为最终的解压目的地址。但是出于效率角度的考虑,所以还要进一步检查Bootloader加载的地址是否符合内核的对齐要求。如果不符合要求,那么还要按照内核的对齐要求修正一下这个地址,然后再将其作为最终的内核解压的目的地址。代码如下所示:

5.2 解压内核 - 图4

5.2 解压内核 - 图5

在上面代码中,#if代码块的目的就是进行对齐修订。修订后的地址,也就是解压内核的目的地址,保存在寄存器ebx中。

然后,将内核解压后的地址再加上偏移z_extract_offset,最后寄存器ebx中保存的即是内核映像在解压前应该移动到的位置。

如果需要配置一个可重定位的内核,则可按如下步骤进行:

1)执行make menuconfig,出现如图5-8所示的界面。

5.2 解压内核 - 图6

图 5-8 配置可重定位内核(1)

2)在图5-8中,选择菜单项"Processor type and features",出现如图5-9所示的界面。

5.2 解压内核 - 图7

图 5-9 配置可重定位内核(2)

3)在图5-9中,选择"Build a relocatable kernel"。

在内核3.7.4版本中默认对齐为0x1000000,当然也可以通过配置内核进行修改。

(2)内核不支持重定位

如果内核没有被编译为可重定位,那么表明内核不允许将其加载到其他位置,必须要加载到LOAD_PHYSICAL_ADDR,因此内核的解压目的地址就是LOAD_PHYSICAL_ADDR。代码如下:

5.2 解压内核 - 图8

然后,将解压的目的地址偏移z_extract_offset,最后,寄存器ebx中保存的即是内核映像在解压前应该移动到的位置。

变量LOAD_PHYSICAL_ADDR是内核编译时指定的加载地址。其定义如下:

5.2 解压内核 - 图9

可见,LOAD_PHYSICAL_ADDR就是内核配置选项CONFIG_PHYSICAL_START按照内核的对齐要求修订后的值。

由这里可见,即使内核不允许重定位,那么事实上最后内核解压后的地址也是符合内核的对齐要求的,因为这里已经对编译时指定的加载地址进行了对齐处理。

内核3.7.4版本的默认加载地址是0x1000000,用户可以通过配置指定内核加载的物理地址,步骤如下:

1)执行make menuconfig,出现如图5-10所示的界面。

5.2 解压内核 - 图10

图 5-10 更改内核加载地址(1)

2)在图5-10中,选择菜单项"Processor type and features",出现如图5-11所示的界面。

5.2 解压内核 - 图11

图 5-11 更改内核加载地址(2)

3)设置内核加载地址依赖于CONFIG_EXPERT或者CONFIG_CRASH_DUMP,所以如果要修改内核依赖地址,必须选择这两项中的一项。可以在图5-11中选中"kernel crash dumps",即CONFIG_CRASH_DUMP,在该配置项的下面即可出现修改内核加载地址的配置项"Physical address where the kernel is loaded",修改这一项的值即可达到修改内核加载地址的目的。

3.移动内核映像

源地址和目的地址确定后,内核映像就开始了移动过程,代码如下:

5.2 解压内核 - 图12

5.2 解压内核 - 图13

在上述代码中,符号_bss在链接vmlinux.bin时定义在bss段的开头。BSS段被链接在vmlinux.bin的最后,而它在内核映像文件中并不占据空间,因此,符号_bss的地址就是内核保护模式的末尾。

寄存器esi是保存移动指令的源地址的,第2行代码就是设置这个寄存器的值,表达式:

5.2 解压内核 - 图14

展开后如下:

5.2 解压内核 - 图15

而寄存器ebp中保存的值是内核加载的地址,所以"%ebp+_bss"即为内核的末尾地址,-4的目的当然是为第一次复制留出4字节的空间。

类似的,第3行代码是设置保存移动指令的目的地址的寄存器edi。因为寄存器ebx中保存移动后的内核地址,所以edi中的值最后设置为移动后的内核的末尾地址,并为第一次复制留出4字节的空间。

同理,读者回忆一下vmlinu.bin的构建,链接脚本指定将函数startup_32链接在vmlinux.bin的最开头,因此,在第4行代码中,"_bss-startup_32"就是内核以字节为单位的长度了。因为,一次复制4字节,所以代表移动次数的寄存器ecx需要右移两位(即除以4)。

第6行代码中,指令std的目的是表示每移动一次,esi和edi分别减一(4字节),也就是说复制是从内核尾部向着头部的方向复制。

内核移动结束后,显然需要重新装载指令指针EIP的值,跳转到移动后的内核中继续执行。这里是通过jmp指令修改指令指针的,即第12行代码,这条语句的目的是跳转到移动后的内核映像中标号relocated处继续执行。