Linux对于Arm64架构,其编译出来的内核默认是不支持压缩的;而对于Arm32版本来说,默认支持内核解压的操作(代码位于arch/arm/boot/compressed目录下,可是arm64目录下没有对应的代码)。如果实在想压缩内核,也可以在bootloader里面解压好后放到内存中指定的位置。

每个编译好的内核image文件都有一个头,用来说明这个image所包含的一些信息,头里面所有数据字段都是小端(little-endian)字节序。至于这个头里面的内容,可以通过下面的代码了解到(代码位于arch/arm64/kernel/head.S):

__HEAD
_head:
	/*
	 * DO NOT MODIFY. Image header expected by Linux boot-loaders.
	 */
#ifdef CONFIG_EFI
	/*
	 * This add instruction has no meaningful effect except that
	 * its opcode forms the magic "MZ" signature required by UEFI.
	 */
	add	x13, x18, #0x16
	b	stext
#else
	b	stext				// branch to kernel start, magic
	.long	0				// reserved
#endif
	le64sym	_kernel_offset_le		// Image load offset from start of RAM, little-endian
	le64sym	_kernel_size_le			// Effective size of kernel image, little-endian
	le64sym	_kernel_flags_le		// Informative flags, little-endian
	.quad	0				// reserved
	.quad	0				// reserved
	.quad	0				// reserved
	.ascii	ARM64_IMAGE_MAGIC		// Magic number
#ifdef CONFIG_EFI
	.long	pe_header - _head		// Offset to the PE header.

pe_header:
	__EFI_PE_HEADER
#else
	.long	0				// reserved
#endif

文件头的前两个字段长度都是4字节,里面的数据其实是两条指令。bootloader在加载完内核后,都会直接会跳转到image的头部,将控制权交给内核。无论是否打开了编译选项CONFIG_EFI,内核其实最终都会跳转到stext指明的段开始的代码处。

文件头开始的第三个字段长度是8字节,是一个偏移量TEXT_OFFSET,指明了对于内核实际加载的位置相对于指定被加载到的位置的偏移。无论是物理地址还是虚拟地址都会有这个偏移量。这个偏移量在中定义:

ifeq ($(CONFIG_ARM64_RANDOMIZE_TEXT_OFFSET), y)
TEXT_OFFSET := $(shell awk "BEGIN {srand(); printf \"0x%06x\n\", \
		 int(2 * 1024 * 1024 / (2 ^ $(CONFIG_ARM64_PAGE_SHIFT)) * \
		 rand()) * (2 ^ $(CONFIG_ARM64_PAGE_SHIFT))}")
else
TEXT_OFFSET := 0x00080000
endif

一般情况下TEXT_OFFSET的值为0x00080000。但是,如果加上了编译选项CONFIG_ARM64_RANDOMIZE_TEXT_OFFSET,则内核会随机生成一个偏移量。但这个随机偏移量有一些限制,通过以下代码可以看出(代码位于arch/arm64/kernel/head.S):

#if (TEXT_OFFSET & 0xfff) != 0
#error TEXT_OFFSET must be at least 4KB aligned
#elif (PAGE_OFFSET & 0x1fffff) != 0
#error PAGE_OFFSET must be at least 2MB aligned
#elif TEXT_OFFSET > 0x1fffff
#error TEXT_OFFSET must be less than 2MB
#endif

可以看出,TEXT_OFFSET必须是4K对齐的,而且不能大于2M。所以,这个TEXT_OFFSET并不是为了安全的目的(区别于内核地址空间布局随机,KASLR),因为其范围实在太小了。加上这个TEXT_OFFSET的目的主要还是用来查错,因为很多bootloader可能都不会读image头的这个字段,将image加载到内存的指定位置,而是想当然的就把其放到默认偏移0x00080000上去。如果使用这个随机偏移后,内核加载执行后就会出错,从而暴露出问题。

文件头开始的第四个字段长度是8字节,内容是内核的大小。

文件头开始的第五个字段长度是8字节,是一个flag字段,指明了image的一些信息,虽然一共保留了64位,但目前只使用了最后4位。其中Bit 0指出内核image本身用的是什么字节序,0表示小端字节序,1表示大端字节序。值得注意的是,无论image采用什么字节序,image的头一定是小端字节序的。Bit 1~2表示内核所使用的内存页大小,1是4K,2是16K,3是64K,0表示未指定。Bit 3指明bootloader应该加载image到物理内存的什么位置,0表示应该尽量加载在物理内存的最低端,1表示可以加载到物理内存的任何位置,但都要保证是2M对齐的。

紧接着的三个字段长度都是8字节,并且全都是保留字段,全部设置成0。

文件头开始的第九个字段长度4字节,是一个魔数,其定义如下(代码位于arch/arm64/include/asm/image.h):

#define ARM64_IMAGE_MAGIC	"ARM\x64"

文件头的最后一个4字节也是一个偏移,用来指出EFI对应的PE头相对image头的偏移。如果不支持EFI的话,那这个字段就设置成0。从前面的代码中可以看出,如果配置成支持EFI的话,PE的头是直接跟在image头后面的,而image头一共长64字节,所以这个偏移会固定设置成0x40。

下面我们来看看一个实际的例子,笔者编译了一个树莓派5.4内核(代码仓库https://github.com/raspberrypi/linux.git,分支rpi-5.4.y),其头二进制形式如下:

可以看出,其TEXT_OFFSET是0x80000;image的大小是0xFE0000;flag是0x0A,最低位是0,表示image本身是小端字节序,中间两位是01,表示内存页大小是4K,第4位是1,表示bootloader可以把内核镜像加载在物理内存的任何位置。

Logo

更多推荐