5.2 解压内核
根据构建内核时的分析,我们知道,内核的保护模式部分包括非压缩部分以及压缩部分,压缩部分才是内核正常运转时的部分,而非压缩部分只是一个过客,其主要作用是解压内核的压缩部分,解压完成后,非压缩部分也将退出历史舞台。
内核的解压缩过程几经演进,现在的解压过程不再是首先将内核解压到另外的位置,然后再合并到最终的目的地址。而是采用了所谓的就地解压(in-place decompression)方法,内核解压时并不需要解压到另外的位置,从而避免覆盖其他部分的数据。
以不可重定位的内核的解压过程为例,其解压过程如图5-7所示。
图 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等数据,都是从哪里获取的呢?这些数据当然是压缩内核的时候最清楚了,因此这些早已在内核编译时,进行压缩时就已经计算好了,定义在内核映像中:
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加载的地址:
这里首先解释一下代码片段中的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加载的地址是否符合内核的对齐要求。如果不符合要求,那么还要按照内核的对齐要求修正一下这个地址,然后再将其作为最终的内核解压的目的地址。代码如下所示:
在上面代码中,#if代码块的目的就是进行对齐修订。修订后的地址,也就是解压内核的目的地址,保存在寄存器ebx中。
然后,将内核解压后的地址再加上偏移z_extract_offset,最后寄存器ebx中保存的即是内核映像在解压前应该移动到的位置。
如果需要配置一个可重定位的内核,则可按如下步骤进行:
1)执行make menuconfig,出现如图5-8所示的界面。
图 5-8 配置可重定位内核(1)
2)在图5-8中,选择菜单项"Processor type and features",出现如图5-9所示的界面。
图 5-9 配置可重定位内核(2)
3)在图5-9中,选择"Build a relocatable kernel"。
在内核3.7.4版本中默认对齐为0x1000000,当然也可以通过配置内核进行修改。
(2)内核不支持重定位
如果内核没有被编译为可重定位,那么表明内核不允许将其加载到其他位置,必须要加载到LOAD_PHYSICAL_ADDR,因此内核的解压目的地址就是LOAD_PHYSICAL_ADDR。代码如下:
然后,将解压的目的地址偏移z_extract_offset,最后,寄存器ebx中保存的即是内核映像在解压前应该移动到的位置。
变量LOAD_PHYSICAL_ADDR是内核编译时指定的加载地址。其定义如下:
可见,LOAD_PHYSICAL_ADDR就是内核配置选项CONFIG_PHYSICAL_START按照内核的对齐要求修订后的值。
由这里可见,即使内核不允许重定位,那么事实上最后内核解压后的地址也是符合内核的对齐要求的,因为这里已经对编译时指定的加载地址进行了对齐处理。
内核3.7.4版本的默认加载地址是0x1000000,用户可以通过配置指定内核加载的物理地址,步骤如下:
1)执行make menuconfig,出现如图5-10所示的界面。
图 5-10 更改内核加载地址(1)
2)在图5-10中,选择菜单项"Processor type and features",出现如图5-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.移动内核映像
源地址和目的地址确定后,内核映像就开始了移动过程,代码如下:
在上述代码中,符号_bss在链接vmlinux.bin时定义在bss段的开头。BSS段被链接在vmlinux.bin的最后,而它在内核映像文件中并不占据空间,因此,符号_bss的地址就是内核保护模式的末尾。
寄存器esi是保存移动指令的源地址的,第2行代码就是设置这个寄存器的值,表达式:
展开后如下:
而寄存器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处继续执行。