文章目录
- 解压过程
- Makefile
- include/asm/unified.h
- compressed/efi-header.S
- compressed/fdt_check_mem_start.c 从FDT中检查内存是否可用,返回可用内存起始地址
- compressed/piggy.S 设置piggy_data的内容与piggy_data_end的地址
- compressed/misc.c 杂项
- compressed/decompress.c
- compressed/head.S
-
- start 入口函数
- LC0 保存着内核的基地址和数据段结束地址
- LC1 存放着内核的栈顶地址和数据段结束地址的位置无关偏移量
- Lheadroom 内核代码大小+16kb(页表)+DTB预留大小(1mb)
- 1 使用位置无关执行重定向栈顶地址,并开启缓存页表
- cache_on 开启缓存
- call_cache_fn 获取处理器ID
- reloc_code_end 记录重定向代码的结束地址
- dbgkc 打印调试内核信息
- cache_clean_flush 清除缓存并刷新
- restart 重定向并获取解压后的内核大小
- Linflated_image_size_offset 解压后的内核大小偏移量
- get_inflated_image_size 获取解压后的内核大小
- wont_overwrite 修正LC0存储的内容,BSS区域的起始和结束地址,GOT表的起始和结束地址
- not_relocated 重定向已完成执行
- __enter_kernel 进入内核

https://github.com/wdfk-prog/linux-study
解压过程
- 依据arch/arm/kernel/vmlinux.lds 生成linux内核源码根目录下的vmlinux,这个vmlinux属于未压缩,带调试信息、符号表的最初的内核,大小约23MB;
- 将上面的vmlinux去除调试信息、注释、符号表等内容,生成arch/arm/boot/Image,这是不带多余信息的linux内核,Image的大小约3.2MB
- 将 arch/arm/boot/Image 用gzip -9 压缩生成arch/arm/boot/compressed/piggy.gz大小约1.5MB;
- 编译arch/arm/boot/compressed/piggy.S 生成arch/arm/boot/compressed/piggy.o大小约1.5MB,这里实际上是将piggy.gz通过piggy.S编译进piggy.o文件中。而piggy.S文件仅有6行,只是包含了文件piggy.gz;
- 依据arch/arm/boot/compressed/vmlinux.lds 将arch/arm/boot/compressed/目录下的文件head.o 、piggy.o 、misc.o链接生成 arch/arm/boot/compressed/vmlinux,这个vmlinux是经过压缩且含有自解压代码的内核,大小约1.5MB;
- 将arch/arm/boot/compressed/vmlinux去除调试信息、注释、符号表等内容,生成arch/arm/boot/zImage大小约1.5MB;这已经是一个可以使用的linux内核映像文件了;
- 将arch/arm/boot/zImage添加64Bytes的相关信息打包为arch/arm/boot/uImage大小约1.5MB;
Makefile
TEXT_OFFSET 内核映像的字节偏移量 0x00008000
# 文本偏移量。此列表按地址进行数字排序,以便
# 提供一种避免/解决多架构内核冲突的方法。
# 注意:该值以下的 32kB 是预留给内核使用的
# 在引导期间,这个偏移量对于kexec-tools 的
textofs-y := 0x00008000
# TRAM 中内核映像的字节偏移量从 RAM 开始。
TEXT_OFFSET := $(textofs-y)
piggy_data
- 生成vmlinux时一同生成piggy_data
- piggy_data包含了内核的压缩数据,压缩算法由
CONFIG_KERNEL_GZIP
等决定 - piggy_data.o,使用piggy_data的内容生成.o文件
include/asm/unified.h
AR_CLASS M_CLASS 选择使用的架构指令
#ifdef CONFIG_CPU_V7M
#define AR_CLASS(x...)
#define M_CLASS(x...) x
#else
#define AR_CLASS(x...) x
#define M_CLASS(x...)
#endif
compressed/efi-header.S
__nop 空指令
.macro __nop
AR_CLASS( mov r0, r0 )
M_CLASS( nop.w )
.endm
__initial_nops 执行2个__nop
.macro __initial_nops
#ifdef CONFIG_EFI_STUB
.inst MZ_MAGIC | (0xe225 << 16) @ eor r5, r5, 0x4d000
eor r5, r5, 0x4d000 @ undo previous insn
#else
__nop
__nop
#endif
.endm
compressed/fdt_check_mem_start.c 从FDT中检查内存是否可用,返回可用内存起始地址
- 检查传入的
R1
,即mov r8, r2 @ save atags pointer
传入的FDT指针是否为空 - 32位地址空间,检查
#address-cells
和#size-cells
是否大于2 - 检查
/chosen
节点下是否存在linux,usable-memory-range
属性,是否有可用内存限制 - 检查
device_type
为memory
的节点,是否存在linux,usable-memory
或reg
属性 - 获取内存的起始地址和大小,检查是否在可用内存范围内
- 如果在可用内存范围内,则返回传入的
mem_start
;否则更新可用内存的起始地址使用最小的可用内存 - 如果没有找到可用内存,则使用传入的
mem_start
作为返回值 - 返回值:要使用的物理内存起始地址
uint32_t fdt_check_mem_start(uint32_t mem_start, const void *fdt)
{
uint32_t addr_cells, size_cells, usable_base, base;
uint32_t fdt_mem_start = 0xffffffff;
const fdt32_t *usable, *reg, *endp;
uint64_t size, usable_end, end;
const char *type;
int offset, len;
if (!fdt)
return mem_start;
if (fdt_magic(fdt) != FDT_MAGIC)
return mem_start;
/* There may be multiple cells on LPAE platforms */
addr_cells = get_cells(fdt, "#address-cells");
size_cells = get_cells(fdt, "#size-cells");
if (addr_cells > 2 || size_cells > 2)
return mem_start;
/*
* 崩溃转储内核时的可用内存
* 此属性描述限制:此范围内的内存为
* 仅在通过其他机制进行描述时有效
*/
usable = get_prop(fdt, "/chosen", "linux,usable-memory-range",
(addr_cells + size_cells) * sizeof(fdt32_t));
if (usable) {
size = get_val(usable + addr_cells, size_cells);
if (!size)
return mem_start;
if (addr_cells > 1 && fdt32_ld(usable)) {
/* Outside 32-bit address space */
return mem_start;
}
usable_base = fdt32_ld(usable + addr_cells - 1);
usable_end = usable_base + size;
}
/* 遍历所有内存节点和区域 */
for (offset = fdt_next_node(fdt, -1, NULL); offset >= 0;
offset = fdt_next_node(fdt, offset, NULL)) {
type = fdt_getprop(fdt, offset, "device_type", NULL);
if (!type || strcmp(type, "memory"))
continue;
reg = fdt_getprop(fdt, offset, "linux,usable-memory", &len);
if (!reg)
reg = fdt_getprop(fdt, offset, "reg", &len);
if (!reg)
continue;
for (endp = reg + (len / sizeof(fdt32_t)); //获取当前节点的reg属性
endp - reg >= addr_cells + size_cells; //reg属性的长度大于一对参数
reg += addr_cells + size_cells) { //获取下一个reg属性
size = get_val(reg + addr_cells, size_cells); //获取当前节点的size
if (!size)
continue;
if (addr_cells > 1 && fdt32_ld(reg)) {
/* Outside 32-bit address space, skipping */
continue;
}
base = fdt32_ld(reg + addr_cells - 1);
end = base + size;
if (usable) {
/*
* Clip to usable range, which takes precedence
* over mem_start
*/
if (base < usable_base)
base = usable_base;
if (end > usable_end)
end = usable_end;
if (end <= base)
continue;
} else if (mem_start >= base && mem_start < end) {
/* 计算的地址有效,使用它*/
return mem_start;
}
//更新可用内存的起始地址,使用最小的可用内存
if (base < fdt_mem_start)
fdt_mem_start = base;
}
}
if (fdt_mem_start == 0xffffffff) {
/* 未找到可用内存,正在回退到默认值*/
return mem_start;
}
/*
*计算出的地址不可用,或者被
* “linux,usable-memory-range” 属性。
* 改用 DTB 中可用的最低物理内存地址,
* 并确保这是 2 MiB 的倍数,以便进行 phys/virt 修补。
*/
return round_up(fdt_mem_start, SZ_2M);
}
compressed/piggy.S 设置piggy_data的内容与piggy_data_end的地址
//只读属性
.section .piggydata, "a"
//全局符号
.globl input_data
input_data:
//指定文件 piggy_data 的内容直接包含到当前的汇编文件中
.incbin "arch/arm/boot/compressed/piggy_data"
.globl input_data_end
input_data_end:
compressed/misc.c 杂项
decompress_kernel 调用解压内核函数,并打印日志
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;
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(); //arch/arm/include/debug/uncompress.h为空函数不执行
putstr("Uncompressing Linux..."); //putstr最终调用puts,在debug.s中实现
ret = do_decompress(input_data, input_data_end - input_data,
output_data, error);
if (ret)
error("decompressor returned an error"); //输出错误后,while(1)死循环
else
putstr(" done, booting the kernel.\n");
}
arch/arm/boot/compressed/decompress.c 不同解压的中间层
- makefile和konconfig中进行了选择配置需要的解压算法
#ifdef CONFIG_KERNEL_GZIP
#include "../../../../lib/decompress_inflate.c"
#endif
#ifdef CONFIG_KERNEL_LZO
#include "../../../../lib/decompress_unlzo.c"
#endif
#ifdef CONFIG_KERNEL_LZMA
#include "../../../../lib/decompress_unlzma.c"
#endif
#ifdef CONFIG_KERNEL_XZ
/* Prevent KASAN override of string helpers in decompressor */
#undef memmove
#define memmove memmove
#undef memcpy
#define memcpy memcpy
#include "../../../../lib/decompress_unxz.c"
#endif
#ifdef CONFIG_KERNEL_LZ4
#include "../../../../lib/decompress_unlz4.c"
#endif
int do_decompress(u8 *input, int len, u8 *output, void (*error)(char *x))
{
return __decompress(input, len, NULL, NULL, output, 0, NULL, error);
}
compressed/decompress.c
- 根据所选的解压算法,包含不同的解压算法.同时只能选择一个解压算法
- 注意定义了
STATIC
和STATIC_RW_DATA
,并且decompress_inflate.c
是**include
**的.分析时需要注意包括这两个宏
#define STATIC static
#define STATIC_RW_DATA /* non-static please */
#ifdef CONFIG_KERNEL_GZIP
#include "../../../../lib/decompress_inflate.c"
#endif
#ifdef CONFIG_KERNEL_LZO
#include "../../../../lib/decompress_unlzo.c"
#endif
#ifdef CONFIG_KERNEL_LZMA
#include "../../../../lib/decompress_unlzma.c"
#endif
#ifdef CONFIG_KERNEL_XZ
#include "../../../../lib/decompress_unxz.c"
#endif
#ifdef CONFIG_KERNEL_LZ4
#include "../../../../lib/decompress_unlz4.c"
#endif
int do_decompress(u8 *input, int len, u8 *output, void (*error)(char *x))
{
return __decompress(input, len, NULL, NULL, output, 0, NULL, error);
}
compressed/head.S
- 使用__nop指令填充32字节,跳过a.out头部.
- 其中
__initial_nops
跳过2 - 循环5个
M_CLASS( nop.w )
跳过一个
- 其中
- 跳转到1,重定向栈顶地址,开启缓存页表
- cache_on开启缓存,call_cache_fn获取处理器ID.(V7M直接返回)
restart
函数重定向并获取解压后的内核大小,判断页表是否会覆盖内核映像,如果会则拷贝内核到新的内存区域,重定向zimage的地址,并跳转到重定向后的restart的地址进行执行.不会则跳转到wont_overwrite
函数wont_overwrite
修正LC0存储的内容,BSS区域的起始和结束地址,GOT表的起始和结束地址,顺序执行到not_relocated
标签
start 入口函数
.section ".start", "ax"
.align
AR_CLASS( .arm )
start:
.type start,#function
/*
* 这 7 个 nop 以及下面的 1 个 nop 为
* !THUMB2 形成 8 个 nop,使压缩的内核可启动
* 在假定内核在 a.out 中的传统 ARM 系统上
* 二进制格式。这些系统上的引导加载程序将
* 将 32 字节跳转到图像中以跳过 a.out 标头。
* 这 8 个 nop 正好填满了 32 个字节,一切仍然如此
* 在这些旧系统上按预期工作。Thumb2 模式保持
* 7 个 nop,因为事实证明一些引导加载程序
* 正在修补内核的初始指令,即
* 已开始利用此 “补丁区域”。
*/
__initial_nops
.rept 5
__nop
.endr
#ifndef CONFIG_THUMB2_KERNEL
__nop
#else
AR_CLASS( sub pc, pc, #3 ) @ A/R: switch to Thumb2 mode
M_CLASS( nop.w ) @ M: already in Thumb2 mode
.thumb @使用thumb指令集
#endif
W(b) 1f @跳转到1
@r1和r2中分别存放着由bootloader传递过来的architecture ID和指向标记列表的指针。
mov r7, r1 @ save architecture ID
mov r8, r2 @ save atags pointer
LC0 保存着内核的基地址和数据段结束地址
- got:全局偏移表,存储动态链接的函数的地址
.align 2
.type LC0, #object
LC0: .word LC0 @ r1
.word __bss_start @ r2
.word _end @ r3
.word _got_start @ r11
.word _got_end @ ip
.size LC0, . - LC0
LC1 存放着内核的栈顶地址和数据段结束地址的位置无关偏移量
- 在
LC1
中存放着内核的栈顶地址和数据段结束地址 - 通过存储栈顶地址-
LC1
的地址来获取位置无关的地址
.type LC1, #object
LC1: .word .L_user_stack_end - LC1 @ sp
.word _edata - LC1 @ r6 image_end
.size LC1, . - LC1 @确定LC1占用的字节数
.L_user_stack: .space 4096
.L_user_stack_end:
Lheadroom 内核代码大小+16kb(页表)+DTB预留大小(1mb)
.Lheadroom:
.word _end - restart + 16384 + 1024*1024
1 使用位置无关执行重定向栈顶地址,并开启缓存页表
- 对齐128MB
- 修正sp栈顶指针
- 通过
fdt_check_mem_start
检查内存是否可用,并返回可用内存起始地址 - 确定最终的内核映像地址
- 判断页表是否会覆盖内核映像.(在内核映像地址范围下的16kb页表与1mb的DTB)
- 设置之后的页表超过了内核映像,则跳过cache_on不进行设置.r4的LSB位表示
- 如果r4的LSB位为1,则跳过cache_on
- 如果r4的LSB位为0,则调用cache_on开启缓存
- 否则调用
cache_on
开启缓存
.text
#ifdef CONFIG_AUTO_ZRELADDR
/*
* 查找物理内存的起点。 当我们执行时如果不开启 MMU,我们将处于物理地址空间。
* 我们只需要通过对齐地址。
*
* 这种一致性是不同的平台 - 我们选择了 128MB 来允许对齐其物理内存开始的平台
* 设置为 128MB 以使用此功能,同时允许 zImage放置在其他平台。 增加对齐意味着我们将
* 对物理开始时的对齐要求更严格 记忆,但放松它意味着我们打破了那些 已经将他们的 zImage 放在(例如)前 64MB 中
* 的
*/
mov r0, pc
and r0, r0, #0xf8000000 @对齐128MB
#ifdef CONFIG_USE_OF
@LC1 存放着内核的栈顶地址和数据段结束地址
adr r1, LC1 @r1 存储LC1的内存地址
ldr sp, [r1] @ get stack location
@ 这里需要加上LC1的地址偏移量,修正存放在LC1处减去的偏移量
add sp, sp, r1 @ apply relocation
/*根据通过的 DTB 验证计算的开始时间 */
mov r1, r8 @r8 save atags pointer
bl fdt_check_mem_start
1:
#endif /* CONFIG_USE_OF */
/*确定最终的内核映像地址. */
add r4, r0, #TEXT_OFFSET
#endif
/*
* 仅当页表不会覆盖我们自己时,才设置页表。
* 这意味着 r4 < pc || r4 - 16k 页目录 > &_end。
* 鉴于 r4 > &_end 最不频繁,我们添加了一个粗略的
* 额外 1MB 的空间用于可能的附加 DTB。
*/
mov r0, pc
/*
if(pc <= 确定最终的内核映像地址) {
r0 = headroom;
r0 += pc;
}
//设置之后的页表超过了内核映像,则跳过cache_on不进行设置
if(确定最终的内核映像地址 <= r0) {
r4 |= 1;
//跳过cache_on
}
*/
cmp r0, r4
ldrcc r0, .Lheadroom
addcc r0, r0, pc
cmpcc r4, r0
orrcc r4, r4, #1 @ 记住我们跳过了cache_on ,用R4的LSB位表示
blcs cache_on @上次第一个操作数大于或等于第二个操作数,调用cache_on
cache_on 开启缓存
- 32字节对齐,传入8,调用
call_cache_fn
,然后返回 - 这段代码的目的是开启缓存(cache)。为了开启指令缓存(I cache)和数据缓存(D cache),需要设置一些页表。页表被放置在内核执行地址向下16KB的位置,希望这个位置没有被其他东西使用。如果被使用了,可能会导致程序崩溃。
- 在进入这个例程时:
- r4 寄存器包含内核执行地址
- r7 寄存器包含架构编号
- r8 寄存器包含 ATAGs 指针
- 在退出这个例程时,以下寄存器的值可能会被破坏:
- r0, r1, r2, r3, r9, r10, r12
- 这个例程必须保留 r4, r7, r8 的值。
.align 5
cache_on: mov r3, #8 @ cache_on function
b call_cache_fn
call_cache_fn 获取处理器ID
- 将
r12
设置为proc_types
的地址 - 获取处理器ID
- 根据处理器ID跳转到不同的函数
CONFIG_CPU_V7M
则在其他地方实现,这里直接返回
/*
* 以下是
* 各种处理器。 这是一个通用的钩子,用于定位
* 输入并跳转到指定偏移量处的指令
* 从区块的开头开始。 请注意,这是所有位置
* 独立代码。
*
* r1 = 损坏
* r2 = 损坏
* r3 = 块偏移
* r9 = 损坏
* r12 = 损坏
*/
call_cache_fn: adr r12, proc_types
#ifdef CONFIG_CPU_CP15
mrc p15, 0, r9, c0, c0 @ get processor ID
#elif defined(CONFIG_CPU_V7M)
/*
* 在 v7-M 上,处理器 ID 位于 V7M_SCB_CPUID
* register,但由于缓存处理是在
* v7-M(如果存在的话)我们只是提前返回这里。
* 如果使用了 V7M_SCB_CPUID 则 cpu ID 函数(即
* __armv7_mmu_cache_{on,off,flush}) 将被选中,该
* 使用未在 v7-M 上实现的 CP15 寄存器。
*/
bx lr
#else
ldr r9, =CONFIG_PROCESSOR_ID
#endif
reloc_code_end 记录重定向代码的结束地址
restart: //331
reloc_code_end: //1437
dbgkc 打印调试内核信息
.macro kputc,val
mov r0, \val
bl putc
.endm
/*
* Debug kernel copy by printing the memory addresses involved
*/
.macro dbgkc, begin, end, cbegin, cend
#ifdef DEBUG
kputc #'C'
kputc #':'
kputc #'0'
kputc #'x'
kphex \begin, 8 /* Start of compressed kernel */
kputc #'-'
kputc #'0'
kputc #'x'
kphex \end, 8 /* End of compressed kernel */
kputc #'-'
kputc #'>'
kputc #'0'
kputc #'x'
kphex \cbegin, 8 /* Start of kernel copy */
kputc #'-'
kputc #'0'
kputc #'x'
kphex \cend, 8 /* End of kernel copy */
kputc #'\n'
#endif
.endm
cache_clean_flush 清除缓存并刷新
/*
* 清理并刷新缓存以保持一致性。
*
* 入境时,
* r0 = 起始地址
* r1 = 结束地址(不包括)
* 退出时,
* R1、R2、R3、R9、R10、R11、R12 损坏
* 此例程必须保留:
* R4、R6、R7、R8
*/
.align 5
cache_clean_flush:
mov r3, #16
mov r11, r1
b call_cache_fn
restart 重定向并获取解压后的内核大小
- 重定向栈顶地址
- 重定向数据段结束地址
- 调用
get_inflated_image_size
获取解压后的内核大小 - 分配64k的内存空间用于malloc
#arch/arm/boot/compressed/Makefile MALLOC_SIZE := 65536 AFLAGS_head.o += -DTEXT_OFFSET=$(TEXT_OFFSET) -DMALLOC_SIZE=$(MALLOC_SIZE)
- 检查页表是否会覆盖内核映像
- 不会则跳转到
wont_overwrite
函数 - 会则修正内核映像地址,拷贝内核到新的内存区域,重定向zimage的地址,并跳转到重定向后的restart的地址进行执行
restart: adr r0, LC1 //获取LC1的地址
ldr sp, [r0] //获取栈顶地址
ldr r6, [r0, #4] //获取数据段结束地址
add sp, sp, r0 //修正栈顶地址
add r6, r6, r0 //修正数据段结束地址
get_inflated_image_size r9, r10, lr
#ifndef CONFIG_ZBOOT_ROM //ROM/flash 中的压缩引导加载程序
/* malloc 空间位于重新定位的堆栈上方(最大 64K) */
add r10, sp, #MALLOC_SIZE
#else
/*
* 如果 BSS/堆栈ZBOOT_ROM不可重定位,
* 但是有人仍然可以从 RAM 运行此代码,
* 在这种情况下,我们的参考是 _edata。
*/
mov r10, r6
#endif
mov r5, #0 @ init dtb size 设置为 0
// 使用附加的设备树blob到zImage(实验)
#ifdef CONFIG_ARM_APPENDED_DTB
#endif
/*
* 检查我们是否会覆盖自己。
* r4 = 最终内核地址(可能设置了 LSB)
* r9 = 解压缩图像的大小
* r10 = 此映像的结尾,包括 bss/stack/malloc 空间(如果非 XIP)
* 我们基本上想要:
* r4 - 16k 页面目录 >= r10 -> OK
* r4 + 图像长度 <= wont_overwrite 的地址 -> OK
* 注意:r4 中可能的 LSB 在这里是无害的。
*/
add r10, r10, #16384 //页表大小
cmp r4, r10 //是否会覆盖自己
bhs wont_overwrite //if(r4 >= r10 + 16k)跳转到wont_overwrite
add r10, r4, r9 //r10 = 解压缩图像的结束地址
adr r9, wont_overwrite //将 wont_overwrite 标签的地址加载到 r9 寄存器中。
cmp r10, r9
bls wont_overwrite //if(解压缩图像的结束地址 <= wont_overwrite 的地址)跳转到wont_overwrite
// 没有跳转,需要修复
/*
* 重新定位到解压缩的内核末尾之后。
* r6 = _edata(image_end)
* r10 = 解压内核的结束地址
* 因为我们总是提前复制,所以我们需要从头开始做.如果源和目标重叠,则向后移动。
*/
/*
* 移动到已添加重定位代码的下一个 256 字节边界.
* 这样可以避免当偏移量较小时覆盖我们自己。
*/
add r10, r10, #((reloc_code_end - restart + 256) & ~255)
bic r10, r10, #255 @对齐到256字节
/* 获取我们想要复制的代码的开头并将其对齐。 */
adr r5, restart
bic r5, r5, #31 @对齐到32字节
/* Relocate the hyp vector base if necessary */
#ifdef CONFIG_ARM_VIRT_EXT //无使用
#endif
//R9 = _edata(image_end) + restart的地址
sub r9, r6, r5 @ size to copy
add r9, r9, #31
bic r9, r9, #31 @ 确保 r9 的值是32字节对齐的。
add r6, r9, r5 //r6 = restart + size to copy
add r9, r9, r10 //r9 = size to copy + 解压内核的结束地址
#ifdef DEBUG //打印内核信息
sub r10, r6, r5
sub r10, r9, r10
/*
* 我们即将将内核复制到新的内存区域。
* 新内存区域的边界可以在 R10 和 R9,而 R5 和 R6 包含边界
* 的 MEMORY 中。致电 dbgkc 将有助于打印此内容信息。
*/
dbgkc r5, r6, r10, r9
/* Start of compressed kernel
End of compressed kernel
Start of kernel copy
End of kernel copy
C:0xC0080060-0xC02151C0->0xC02E3000-0xC0478160
*/
#endif
// 将image_end的数据拷贝到新的内存区域(size to copy + 解压内核的结束地址)
// r6 image_end
1: ldmdb r6!, {r0 - r3, r10 - r12, lr}
cmp r6, r5
stmdb r9!, {r0 - r3, r10 - r12, lr}
bhi 1b //if(r6 > restart的地址)跳转到1继续拷贝
/*保留 offset 以重新定位的代码。*/
sub r6, r9, r6
mov r0, r9 @ 重新定位的 zImage 的开始
add r1, sp, r6 @ 重新定位的 zImage 结束
bl cache_clean_flush
badr r0, restart //将 restart 标签的地址加载到 r0 寄存器中
add r0, r0, r6 //重定位后的代码的起始地址
mov pc, r0 //跳转到重定位后的代码的起始地址 restart处
Linflated_image_size_offset 解压后的内核大小偏移量
- 减去 4 的目的是为了获取解压后内核镜像大小的地址,因为这个大小通常存储在 input_data_end 之前的4个字节中。减去当前地址
.
是为了得到一个相对偏移量,这样在运行时可以正确地访问解压后内核镜像大小的位置。
.long (input_data_end - 4) - .
get_inflated_image_size 获取解压后的内核大小
/*
* The kernel build system appends the size of the
* decompressed kernel at the end of the compressed data
* in little-endian form.
* 执行之后res = 解压后的内核大小
*/
.macro get_inflated_image_size, res:req, tmp1:req, tmp2:req
//获取解压后的内核大小偏移量的地址-当前地址
adr \res, .Linflated_image_size_offset
ldr \tmp1, [\res] //获取解压后的内核大小偏移量到\tmp1
add \tmp1, \tmp1, \res @ 解压的图像大小地址 = 偏移量 + 当前地址
//通过一系列的 ldrb 和 orr 指令以小端格式读取解压后镜像大小的四个字节。
ldrb \res, [\tmp1] @ get_unaligned_le32
ldrb \tmp2, [\tmp1, #1]
orr \res, \res, \tmp2, lsl #8
ldrb \tmp2, [\tmp1, #2]
ldrb \tmp1, [\tmp1, #3]
orr \res, \res, \tmp2, lsl #16
orr \res, \res, \tmp1, lsl #24
.endm
- 验证如下:
Image未压缩的,去除调试信息的内核大小为2992000(0x2DA780)字节
-rwxrwxr-x 1 embedsky embedsky 2992000 Mar 2 14:49 Image*
piggy_data的最后四字节转换成小端也为2992000(0x2DA780)字节
hexdump -C arch/arm/boot/compressed/piggy_data | tail -n 2
00190730 10 ca f1 73 80 a7 2d 00
wont_overwrite 修正LC0存储的内容,BSS区域的起始和结束地址,GOT表的起始和结束地址
wont_overwrite:
adr r0, LC0
ldmia r0, {r1, r2, r3, r11, r12}
sub r0, r0, r1 @ 偏移量 = 存储的LC0的地址 - 当前LC0地址
/*
* 如果 delta 为零,我们正在运行链接地址。
* r0 = delta
* r2 = BSS 起始地址
* r3 = BSS 结束地址
* r4 = 内核执行地址(可能设置了 LSB)
* r5 = 附加的 dtb 大小(如果不存在则为 0)
* r7 = 架构 ID
* r8 = atags 指针
* r11 = GOT 起始地址
* r12 = GOT 结束地址
* sp = 堆栈指针
*/
orrs r1, r0, r5 //delta 不为0 或者 附加的 dtb 大小不为0,则不会跳转到 not_relocated
beq not_relocated
add r11, r11, r0
add r12, r12, r0
#ifndef CONFIG_ZBOOT_ROM
/*
* 如果我们完全运行 PIC === CONFIG_ZBOOT_ROM = n,
* 我们需要修正指向 BSS 区域的指针。
* 请注意,堆栈指针已经被修正。
*/
add r2, r2, r0
add r3, r3, r0
/*
* 重新定位GOT表中的所有条目。将bss条目提升到_edata + dtb大小
*/
1: ldr r1, [r11, #0] @ 重新定位 GOT 中的条目
add r1, r1, r0 @ 这修复了 C 引用
cmp r1, r2 @ if entry >= bss_start &&
cmphs r3, r1 @ bss_end > entry
addhi r1, r1, r5 @ entry += dtb size
str r1, [r11], #4 @ next entry
cmp r11, r12 @ if not end of GOT
blo 1b @ 跳到1处继续循环
/* 也提升我们的 BSS 指针*/
add r2, r2, r5 @调整bss指针到dtb 大小后
add r3, r3, r5 @调整bss结束指针到dtb 大小后
#endif
not_relocated 重定向已完成执行
- 循环清除 BSS 区域
- 判断是否跳过缓存设置,如果跳过则直接调用cache_on开启缓存
- 传入参数进行解压内核
- r0 = 内核执行地址
- r1 = 堆栈上方的 malloc 空间
- r2 = 表示动态内存分配空间的最大地址
- r3 = 架构 ID
not_relocated: mov r0, #0
// 循环清除 BSS 区域
1: str r0, [r2], #4 @ clear bss
str r0, [r2], #4
str r0, [r2], #4
str r0, [r2], #4
cmp r2, r3
blo 1b
/*
* 我们之前跳过了缓存设置吗?
* 这由 r4 中的 LSB 表示。
* 如果是这样,现在就做。
*/
//这`1 使用位置无关执行重定向栈顶地址,并开启缓存页表`中进行判断
//设置之后的页表超过了内核映像,则跳过cache_on不进行设置,否则直接进行cache_on
tst r4, #1 //将 r4 寄存器的值与立即数1进行按位与操作,并根据结果更新条件标志
bic r4, r4, #1 //清除最低有效位
blne cache_on //如果 tst 指令的结果表明 r4 的最低有效位为1(即之前跳过了缓存设置)
/*
* 现在应该已经充分设置了 C 运行环境。
* 设置一些指针,然后开始解压。
* r4 = 内核执行地址
* r7 = 架构 ID
* r8 = atags 指针
*/
mov r0, r4
mov r1, sp @ 堆栈上方的 malloc 空间
add r2, sp, #MALLOC_SIZE @ 64k max
mov r3, r7
/*
* r0 = 内核执行地址
* r1 = 堆栈上方的 malloc 空间
* r2 = 表示动态内存分配空间的最大地址
* r3 = 架构 ID
*/
bl decompress_kernel
/*
进入这个宏之后
r1 = 解压后的镜像大小
*/
get_inflated_image_size r1, r2, r3
mov r0, r4 @ 解压镜像的开始地址
add r1, r1, r0 @ 解压镜像的结束地址
bl cache_clean_flush
bl cache_off
mov r0, r4 @ start of inflated image
add r1, r1, r0 @ end of inflated image
bl cache_clean_flush
bl cache_off
#ifdef CONFIG_ARM_VIRT_EXT //ARM 虚拟化
#else
b __enter_kernel //跳转进入内核
#endif
__enter_kernel 进入内核
V7M
不执行ARM
,执行THUMB
和M_CLASS
r4
为内核的入口地址
mov r0, #0 @ must be 0
mov r1, r7 @ 恢复架构编号
mov r2, r8 @ 恢复 Atags 指针
ARM( mov pc, r4 ) @ call kernel
M_CLASS( add r4, r4, #1 ) @ 在 M 类的 Thumb 模式下进入
THUMB( bx r4 ) @ 对于 A/R 类,入口点始终为 ARM