对于很多架构来说,都会使用【启动加载程序】将linux内核镜像加载到内存中。有的启动加载程序还会对内核镜像进行校验和检查,还有的会解压linux内核镜像并重新部署linux内核镜像。
对于linux内核来说,【启动加载程序】可以看做是引导加载程序与linux内核镜像之间的粘合剂,启动加载程序负责提供合适的上下文环境让linux内核运行于其中。
本文将描述启动加载程序的组成结构和其一些重要功能。
(注)本文章内容基于linux内核版本:4.1.15
本小节以ARM架构为例。
在linux编译构建的最后阶段,启动加载程序会与vmlinux
拼接成一个合成的、最终的linux内核镜像。在ARM架构下,启动加载程序放置于(/arch/arm/boot/compressed)目录下。其中承载启动加载核心功能的是:head.S
、head-xxx.S
、misc.c
、piggy.xxx.S
等文件。下文将逐一描述这些文件。
在/arch/arm/boot/compressed/目录中有piggy.gzip.S、piggy.lz4.S、piggy.lzma.S、piggy.lzo.S四个文件用于指定如何包含压缩后的linux内核镜像。本小节假设linux内核以lzo
方式压缩,其piggy.lzo.S
文件中的内容如下(/arch/arm/boot/compressed/piggly.lzo.S):
.section .piggydata,#alloc
.globl input_data
input_data:
.incbin "arch/arm/boot/compressed/piggy.lzo"
.globl input_data_end
input_data_end:
在编译构建过程中,汇编器将汇编这个文件并生成一个piggy.lzo镜像,该镜像包含一个名为.piggydata
的段,piggly.lzo.S汇编文件的作用是将压缩后的二进制内核镜像(piggy.lzo)放到这个段中。压缩后内核镜像(piggy.lzo)是通过汇编器的预处理指令.incbin
将piggy.lzo包含进来(.incbin
类似于C语言中的#include文件指令)。input_data
和input_data_end
标签用于:在启动加载程序中确定包含的内核镜像的边界。
注意:这个文件与linux内核的入口(head.o)不同,虽然两者名称一样,但是其汇编代码和功能不一样。在启动加载程序中的head.o
文件用于在底层用汇编语言实现处理器的初始化,具体的初始化操作有:支持处理器内部指令和数据缓存、禁止中断并建立C语言运行环境等。
misc.c
文件用于解压和重新部署镜像。
从启动加载程序向linux内核的跳转是由汇编语句:b __enter_kernel
完成。
__enter_kernel
是启动加载程序进入linux内核的入口点,汇编代码如下:
__enter_kernel:
mov r0, #0 @ must be 0
ARM( mov pc, r4 ) @ call kernel
M_CLASS( add r4, r4, #1 ) @ enter in Thumb mode for M class
THUMB( bx r4 ) @ entry point is always ARM for A/R classes
由于R4=内核的执行地址,上述代码使用mov pc, r4
将pc设置为R4的值,从而实现启动加载程序向linux内核的跳转。
首先解压linux内核镜像的入口是decompress_kernel()
函数,该函数在(/arch/arm/compressed/head.S)文件中由汇编代码调用,调用点位于:
mov r0, r4
mov r1, sp @ malloc space above stack
add r2, sp, #0x10000 @ 64k max
mov r3, r7
bl decompress_kernel
bl cache_clean_flush
bl cache_off
mov r1, r7 @ restore architecture number
mov r2, r8 @ restore atags pointer
#ifdef CONFIG_ARM_VIRT_EXT
mrs r0, spsr @ Get saved CPU boot mode
and r0, r0, #MODE_MASK
cmp r0, #HYP_MODE @ if not booted in HYP mode...
bne __enter_kernel @ boot kernel directly
adr r12, .L__hyp_reentry_vectors_offset
ldr r0, [r12]
add r0, r0, r12
bl __hyp_set_vectors
__HVC(0) @ otherwise bounce to hyp mode
b . @ should never be reached
.align 2
.L__hyp_reentry_vectors_offset: .long __hyp_reentry_vectors - .
#else
b __enter_kernel
#endif
上述bl decompress_kernel
代码调用decompress_kernel函数执行镜像解压。该函数定义如下:
extern int do_decompress(u8 *input, int len, u8 *output, void (*error)(char *x));
void decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,
unsigned long free_mem_ptr_end_p,
int arch_id)
{
int ret;
__stack_chk_guard_setup();
output_data = (unsigned char *)output_start;
free_mem_ptr = free_mem_ptr_p;
free_mem_end_ptr = free_mem_ptr_end_p;
__machine_arch_type = arch_id;
arch_decomp_setup();
putstr("Uncompressing Linux...");
ret = do_decompress(input_data, input_data_end - input_data,
output_data, error);
if (ret)
error("decompressor returned an error");
else
putstr(" done, booting the kernel.\n");
}
在上述代码中,将调用do_decompress()
函数,该函数定义在(/arch/arm/boot/compressed/decompress.c)文件中,其将调用__decompress()函数,该函数由配置宏定义管控并分别定义在/lib/decompress_unlzma.c、/lib/decompress_unlzo.c、/lib/decompress_unxz.c、/lib/decompress_unlz4.c文件中。
对于linux内核来说,启动加载程序并不是单独的运行程序,它是与vmlinux内核镜像拼接在一起,完成其使命的。
在linux源码构建过程的最后阶段,会打印出以下信息:
LD vmlinux
SORTEX vmlinux
SYSMAP System.map
OBJCOPY arch/arm/boot/Image
Building modules, stage 2.
MODPOST 18 modules
Kernel: arch/arm/boot/Image is ready
LDS arch/arm/boot/compressed/vmlinux.lds
AS arch/arm/boot/compressed/head.o
LZO arch/arm/boot/compressed/piggy.lzo
CC arch/arm/boot/compressed/misc.o
CC arch/arm/boot/compressed/decompress.o
CC arch/arm/boot/compressed/string.o
AS arch/arm/boot/compressed/piggy.lzo.o
LD arch/arm/boot/compressed/vmlinux
OBJCOPY arch/arm/boot/zImage
Kernel: arch/arm/boot/zImage is ready
从以上信息分析可知,编译过程中先构建出ELF格式的内核主体vmlinux
(包含符号、注释、调试信息和与架构相关的部分)。该内核主体由/arch/arm/kernel/vmlinux.lds.S链接脚本负责链接。
然后汇编出piggy.lzo.o(压缩后的内核镜像),最后会汇编出合成后的linux内核镜像。
本文分析了启动加载程序的大致结构,以及在汇编代码中的调用点和入口点等。
由于小生精力和知识有限,如遇文章存在有不妥之处,请多多批评或与我讨论,谢谢啦!