###cache_on之后
cache_on之后段代的代码大体可认为做了三件事:
- 检查内核是否需要移动,如果需要移动则将其移动并重定向.got的内容。
- 调用decompress_kernel解压内核。
- 调用call_kernel跳转到Image。
###vmlinux(小)的链接脚本:
//./arch/arm/boot/compressed/vmlinux.lds.in
//这个脚本是用来链接vmlinux(小)的,故zImage代码执行时
//内核各个段的分布也是在这个脚本中定义的(部分代码已省略)。
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = TEXT_START;
_text = .;
.text : {
_start = .;
*(.start)
*(.text)
*(.text.*)
*(.fixup)
*(.gnu.warning)
*(.glue_7t)
*(.glue_7)
}
.rodata : {
*(.rodata)
*(.rodata.*)
}
//这里是piggy.gz的位置
.piggydata : {
*(.piggydata)
}
. = ALIGN(4);
_etext = .;
.got.plt : { *(.got.plt) }
_got_start = .;
.got : { *(.got) }
_got_end = .;
.pad : { BYTE(0); . = ALIGN(8); }
_edata = .;
. = BSS_START;
__bss_start = .;
.bss : { *(.bss) }
_end = .;
. = ALIGN(8); /* the stack must be 64-bit aligned */
.stack : { *(.stack) }
.stab 0 : { *(.stab) }
.stabstr 0 : { *(.stabstr) }
.stab.excl 0 : { *(.stab.excl) }
.stab.exclstr 0 : { *(.stab.exclstr) }
.stab.index 0 : { *(.stab.index) }
.stab.indexstr 0 : { *(.stab.indexstr) }
.comment 0 : { *(.comment) }
}
###piggy.gzip.S
piggy.gzip.S是用来生成piggy.gzip.o的,其实际上相当于将一个piggy.gzip文件当
二进制打包到vmlinux(小)中了。
/./arch/arm/boot/compressed/piggy.gzip.S
.section .piggydata,#alloc
//global只是声明,并不分配空间
.globl input_data
input_data:
##INCBIN 指令在被汇编的文件内包含一个文件。 该文件按原样包含,没有进行汇编。
.incbin "arch/arm/boot/compressed/piggy.gzip"
.globl input_data_end
input_data_end:
//cache_on函数的实际作用是开启高速缓存,这样可以加快后续代码的执行速度
//在arm体系结构中,高速缓存TLB必须依赖于mmu,所以cache_on函数内部通过
//调用__setup_mmu来初始化页表(代码段的页表属性允许缓存)
//调用__common_mmu_cache_on来使能mmu
bl cache_on
//跳转到这里,mmu已经开启了,恒等映射!
/* ***************************************************************************************************
LC0的代码本来是在后面的,这里为了方便解释,先在前面列出了
各个代码段的分布见后(这些值都是编译地址!)。
LC0: .word LC0 @ r1
//bss段的开始位置
.word __bss_start @ r2
//bss的结束位置
.word _end @ r3
//数据段的结束位置
.word _edata @ r6
//pizzy.gz的结束位置
.word input_data_end - 4 @ r10 (inflated size location)
.word _got_start @ r11
.word _got_end @ r12 (ip)
//栈的结束位置
.word .L_user_stack_end @ sp
//这个应该是个伪指令,指定LC0大小的
.size LC0, . - LC0
**********************************************************************************************************/
//adr是相对于当前pc的相对寻址,一般用于获取指令的真是地址,而不是加载地址
restart: adr r0, LC0
//把上面LC0中的一堆变量载入到各个寄存器中
ldmia r0, {r1, r2, r3, r6, r10, r11, r12}
//设置临时栈, sp = L_user_stack_end,目前sp还指向栈底
ldr sp, [r0, #28]
/*********************
通过前面LC0地址表的内容可见,这里r0中的内容就是编译时决定的LC0的实际运行地址(特别注意不是链接地址),然后调用ldmia命令依次将LC0地址表处定义的各个地址加载到r1、r2、r3、r6、r10、r11、r12和SP寄存器中去。执行之后各个寄存器中保存内容的意义如下:
(1) r0:LC0标签处的运行地址
(2) r1:LC0标签处的链接地址
(3) r2:__bss_start处的链接地址
(4) r3:_end处的链接地址(即程序结束位置)
(5) r6:_edata处的链接地址(即数据段结束位置)
(6) r10:压缩后内核数据大小位置
(7) r11:GOT表的启示链接地址
(8) r12:GOT表的结束链接地址
(9) sp:栈空间结束地址
****************/
/***************************
在获取了LC0的链接地址和运行地址后,就可以通过计算这两者之间的差值来判断当前运行的地址是否就是编译时的链接地址。
zImage的加载地址未必是编译地址,所以这里需要将各个变量由编译地址修正为真实地址。
将运行地址和链接地址的偏移保存到r0寄存器中,然后更新r6和r10中的地址,将其转换为实际的运行地址
*******************************************/
//计算LC0的真实地址(既是物理地址,又是虚拟地址)和编译地址的差值。
sub r0, r0, r1 @ calculate the delta offset
//将_edata的编译地址转为真实地址
add r6, r6, r0 @ _edata
//将input_data_end的编译地址转为真实地址
add r10, r10, r0 @ inflated kernel size location
/**********************************************************************************
这里将镜像的结束地址保存到r10中去,我这里并没有定义ZBOOT_ROM(如果定义了ZBOOT_ROM则bss和stack是非可重定位的),这里将r10设置为sp结束地址上64kb处(这64kB空间是用来作为堆空间的)。
*************************************************************************************/
add sp, sp, r0
add r10, sp, #0x10000
/******************************************************************************
* The kernel build system appends the size of the
* decompressed kernel at the end of the compressed data
* in little-endian form.
r9是piggy.gzip这个gzip文件的最后四个字节,这四个字节记录的
是解压后的Image的大小存的.在我这里piggy.gzip最后: 30c1 b500 0a
Image大小为0xb5c130, 最后那个0a估计最后会去掉的。
这里用ldrb指令是考虑到这个存Image大小的位置可能不是4byte对齐的。
注释中说明了,内核编译系统在压缩内核时会在末尾处以小端模式附上未压缩的内核大小,这部分代码的作用就是将该值计算出来并保存到r9寄存器中去
*****************************************************************************/
ldrb r9, [r10, #0]
ldrb lr, [r10, #1]
orr r9, r9, lr, lsl #8
ldrb lr, [r10, #2]
ldrb r10, [r10, #3]
orr r9, r9, lr, lsl #16
orr r9, r9, r10, lsl #24
接下来内核如果配置为支持设备树(DTB)会做一些特别的工作,我这里没有配置(#ifdef CONFIG_ARM_APPENDED_DTB),所以先跳过。
/*****************************************************************************************
* 下面这一段代码是来检测是否需要复制自身的
* r4是最终内核要解压到的地址,是通过zreladdr来指定的
* r9是Image镜像的大小,是从piggy.gz文件末尾获取的
* r10是piggy.gz的结束位置 包含了代码段 bss段 堆 栈的大小;
* 往下执行需要满足两个条件之一:
* 1) 最终内核要解压到的地址(r4) - 16k页表 >= zImage的基本代码结束位置(r10)
* 2) 最终内核解压后的结束地址(r4 + r9(image length)) <= 后续执行的代码(wont_overwrite的地址)
0x00000000 -----------------------------------------------------------------------------
------ Image(zreladdr)
|
|
----- zImage起始地址 |
| |
| |
----- 当前代码(pc) ------ Image end 在此之上都可以
----- wont_overwrite的代码
|
----- zImage基本代码结束(piggy.gz末尾,r10)
----- Image(zreladdr)
;如果解压到这个位置往下都是可以的
|
|
----- Image end
0xffffffff -----------------------------------------------------------------------------
****************************************************************************************************/
/*****************************************************************************************
这部分代码用来分析当前代码是否会和最后的解压部分重叠,如果有重叠则需要执行代码搬移。首先比较内核解压地址r4-16Kb(这里是0x00004000,包括16KB的内核页表存放位置)和r10,如果r4 – 16kB >= r10,则无需搬移,否则继续计算解压后的内核末尾地址是否在当前运行地址之前,如果是则同样无需搬移,不然的话就需要进行搬移了。
总结一下可能的3种情况:
(1) 内核起始地址zreladdr– 16kB >= 当前镜像结束地址:无需搬移
(2) 内核结束地址 <= wont_overwrite运行地址:无需搬移
(3) 内核起始地址zreladdr– 16kB < 当前镜像结束地址 && 内核结束地址 > wont_overwrite运行地址:需要搬移
仔细分析一下,这里内核真正运行的地址是0x00004000,而现在代码的运行地址显然已经在该地址之后了反汇编发现wont_overwrite的运行地址是0x00008000+0x00000168),而且内核解压后的空间必然会覆盖掉这里(内核解压后的大小大于0x00000168),所以这里会执行代码搬移。
********************************************************************************************/
//这里r10 += 0x4000是预留给页表的
add r10, r10, #16384
cmp r4, r10
//如果r4 >= r10则满足条件1,Image要解压的地址在zImage后面,
//跳转到wont_overwrite,不需移动自身代码。
bhs wont_overwrite
//r10 = r4 + r9, r10为预计解压后的image的结束地址
add r10, r4, r9
//获取wont_overwrite的物理地址
adr r9, wont_overwrite
//如果否满足条件2,即image解压后没有覆盖wont_overwrite
//之后的代码,则也无需移动自身代码。
cmp r10, r9
bls wont_overwrite
/**********************************
* Relocate ourselves past the end of the decompressed kernel.
* r6 = _edata
* r10 = end of the decompressed kernel
* Because we always copy ahead, we need to do it from the end and go
* backward in case the source and destination overlap.
******************************************/
/***********************************
* Bump to the next 256-byte boundary with the size of
* the relocation code added. This avoids overwriting
* ourself when the offset is small.
***************************************************************/
//否则就需要移动代码的位置了,这里的逻辑是把从restart到zImage结束的代码
//移动到Image预计解压后的结束位置的后面,为什么从restart开始见下面。
//r10 为解压后内核的结束地址
add r10, r10, #((reloc_code_end - restart + 256) & ~255)
//这里的r10应该就是最终要移动到的地址了
bic r10, r10, #255
/* Get start of code we want to copy and align it down. */
//要复制的代码的起始地址
adr r5, restart
//清除末尾位
bic r5, r5, #31
/***************************
从这里开始会将镜像搬移到解压的内核地址之后,首先将解压后的内核结束地址进行扩展,扩展大小为代码段的大小(reloc_code_end定义在head.s的最后)保存到r10中,即搬运目的起始地址,然后r5保存了restart的起始地址,并进行对齐,即搬运的原起始地址。反汇编查看这里扩展的大小为0x800。
*************************/
//要复制的数据大小
sub r9, r6, r5 @ size to copy
add r9, r9, #31 @ rounded up to a multiple
bic r9, r9, #31 @ ... of 32 bytes
add r6, r9, r5
//要复制到的结束位置
add r9, r9, r10
//复制数据
1: ldmdb r6!, {r0 - r3, r10 - r12, lr}
cmp r6, r5
stmdb r9!, {r0 - r3, r10 - r12, lr}
bhi 1b
/* Preserve offset to relocated code. */
sub r6, r9, r6
//代码被移动过了,所以清缓存
bl cache_clean_flush
//跳转到restart重新执行,因为当前地址变了,所以LC0的当前地址和编译地址
//的差值就变了,前面的好多变量都是根据这个差值算出来的,所以这里跳到
//restart重新来过,所以前面检测的时候是检测wont_overwrite之前
//的代码是否被覆盖,而移动得要从restart的代码开始移动。
adr r0, BSYM(restart)
add r0, r0, r6
mov pc, r0
//到这里
/************************************************
这里首先计算出需要搬运的大小保存到r9中,搬运的原结束地址到r6中,搬运的目的结束地址到r9中。注意这里只搬运代码段和数据段,并不包含bss、栈和堆空间。
接下来开始执行代码搬移,这里是从后往前搬移,一直到r6 == r5结束,然后r6中保存了搬移前后的偏移,并重定向栈指针(cache_clean_flush可能会使用到栈)。
之后调用调用cache_clean_flush清楚缓存,然后将PC的值设置为搬运后restart的新地址,然后重新从restart开始执行。这次由于进行了代码搬移,所以会在检查自覆盖时进入wont_overwrite处执行。
*************************************************************/
wont_overwrite:
/*
* If delta is zero, we are running at the address we were linked at.
* r0 = delta (运行地址与链接地址的偏移量)
* r2 = BSS start
* r3 = BSS end
* r4 = kernel execution address
* r5 = appended dtb size (0 if not present)
* r7 = architecture ID
* r8 = atags pointer
* r11 = GOT start
* r12 = GOT end
* sp = stack pointer
这里的注释列出了现有所有寄存器值得含义,如果r0为0则说明当前运行的地址就是链接地址,无需进行重定位,跳转到not_relocated执行,但是这里运行的地址已经被移动到内核解压地址之后,显然不会是链接地址0x00000168(反汇编代码中得到),所以这里需要重新修改GOT表中的变量地址来实现重定位。
*/
//r5在一开始被初始化为0
orrs r1, r0, r5
//这里是如果运行地址与链接地址相等则跳转到not_relocated
//不执行got段的重定位
beq not_relocated
//否则将r11,r12存的GOT表的编译地址修改为当前地址
add r11, r11, r0
add r12, r12, r0
//修正bbs段的起始,结束地址
add r2, r2, r0
add r3, r3, r0
//对GOT表中的所有元素做重定位
1: ldr r1, [r11, #0] @ relocate entries in the GOT
add r1, r1, r0 @ This fixes up C references
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
blo 1b
/* bump our bss pointers too */
add r2, r2, r5
add r3, r3, r5
/*******************************************************
更新GOT表的运行起始地址到r11和结束地址到r12中去,然后同样更新BSS段的运行地址(需要修正BSS段的指针)。然后进入“1”标签中开始执行重定位。
通过r1获取GOT表中的一项,然后对这一项的地址进行修正,如果修正后的地址 < BSS段的起始地址,或者在BSS段之中则再加上DTB的大小(如果不支持DTB则r5的值为0),然后再将值写回GOT表中去。如此循环执行直到遍历完GOT表。
在重定位完成后,继续执行not_relocated部分代码,这里循环清零BSS段。
********************************************************/
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
/***********************************
* The C runtime environment should now be setup sufficiently.
* Set up some pointers, and start decompressing.
* r4 = kernel execution address
* r7 = architecture ID
* r8 = atags pointer
******************************************************/
mov r0, r4
mov r1, sp @ malloc space above stack
add r2, sp, #0x10000 @ 64k max
mov r3, r7
//这个即是将piggy.gz解压为Image的函数,这里是c代码
bl decompress_kernel
//解压后再次刷新缓存(个人理解应该是代码区域有代码变动就应该刷新缓存)
bl cache_clean_flush
bl cache_off
mov r0, #0 @ must be zero
mov r1, r7 @ restore architecture number
mov r2, r8 @ restore atags pointer
//r4是Image的入口地址,也是Image的解压地址,Image
//本身是个binary文件,第一个字节即为指令,这里跳转到Image
//即./arch/arm/kernel/head.s:stext
ARM( mov pc, r4 ) @ call kernel
/****************************************************
到此为止,C语言的执行环境已经准备就绪,设置一些指针就可以开始解压内核了(这里的内核解压部分是使用C代码写的)。
跳到 decompress_kernel 跳转到内核C代码运行。
这里r0~r3的4个寄存器是decompress_kernel()函数传参用的,r0传入内核解压后的目的地址,r1传入堆空间的起始地址,r2传入堆空间的结束地址,r3传入机器码,然后就开始调用decompress_clean_flush()函数执行内核
decompress_kernel()是用于解压内核
***************************************************************/
###decompress_kernel
decompress_kernel是用c函数完成的,其代码如下
//./kernel/arch/arm/boot/compressed/Misc.c
extern char input_data[];
void decompress_kernel(
unsigned long output_start, //zImage的解压地址(r4)
unsigned long free_mem_ptr_p, //临时空间,解压用(这里用的是栈)
unsigned long free_mem_ptr_end_p, //临时空间结尾
int arch_id) // 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();
//putstr是内核启动早期的打印函数,一般都是直接向IO端口写的数据
putstr("Uncompressing Linux...");
//内核解压函数,这里用的是gzip解压,这个input_data和input_data_end
//定义在piggy.gzip.S中,是piggy.gzip的起始和结束位置。
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");
}
这里要注意一点的是:decompress_kernel函数中就已经开始调用打印函数了,这一句
putstr("Uncompressing Linux...")
在goldfish启动的时候是可以打印出来的,在真实设备上(如MT6582),通过串口也是可以打出来的,其实现如下:
./kernel/arch/arm/boot/compressed/Misc.c
static void putstr(const char *ptr)
{
char c;
while ((c = *ptr++) != '\0') {
if (c == '\n')
putc('\r');
putc(c);
}
flush();
}
而这个putc,具体平台实现的方式不同,在goldfish上:
//./arch/arm/mach-goldfish/include/mach/uncompress.h
#define GOLDFISH_TTY_PUT_CHAR (*(volatile unsigned int *)0xff002000)
static void putc(int c)
{
//向IO端口0xff002000直接写入字符,这就是goldfish的uart端口
GOLDFISH_TTY_PUT_CHAR = c;
}
在MT6582上,也是通过一个地址写入的,如下:
//./mediatek/platform/mt6582/kernel/core/include/mach/uncompress.h
#define MT_UART_PHY_BASE 0x11002000
#define MT_UART_THR *(volatile unsigned char *)(MT_UART_PHY_BASE+0x0)
static inline void putc(int c)
{
//向IO端口0x11002000直接写入字符,这就是mt6582的uart端口
//这一句while循环是测试控制位的,具体位作用需参考板子的手册。
while (!(MT_UART_LSR & 0x20));
MT_UART_THR = c;
}
这种写入也就是在内核刚开始,有恒等映射的时候,后续page_init二次映射的时候这些地址都会映射到内核的内核空间的其他地方了(后续分析)。
##旧版废弃的内容,但应该是正确的
###part1
rm MMU 一级页表寻址:
arm协处理器CP15的C2寄存器记录着页基地址(又可以叫做一级页表基地址),对于一个地址addr, 在arm中第一级寻址算法为:C2[31:14] + addr[31:20] + 00,如图:
注1:
1) C2中记录的页表基地址,又可以叫做一级页表基地址。
2) 一级页表基地址中的每一项元素叫做一级页表项。
3) 一级页表项中的内容叫做一级页表描述符。
4) 一级页表描述符,描述的是否为一个二级页表,这个得看描述符的最后两位是什么。
注2: 一级寻址只与页表基地址的前18位(一级页表基地址),虚拟地址的前12位(一级页表项的页内偏移)有关 (+最后两位00),没有任何标记位,mmu通过将二者组合成一个32位物理地址,这个就是一级页表项的物理地址,从这个物理地址中读取到的内容就是一级页表描述符。
一级页表描述符:
arm 开启MMU后,当用户访问一个虚拟地址时,先根据C2和虚拟地址的前12位标记的物理地址中取出一个一级页表描述符,虚拟地址中剩下20位如何解析,是由这个一级页表描述符的低2位决定的。一共四种组合,对应四种不同的解析方式。在__setup_mmu中初始化的一级页表项的最后两位都是10,这里先介绍10。
一级页表描述符[1:0]为0b10,表示该一级描述符为段描述符。该一级描述符应该如下解析:
虚拟地址->物理地址的过程:
这个过程的总体描述如下:
- 用户访问一个虚拟地址addr。
- MMU取出addr的前12位,并根据C2定位一级页表描述符X。
- MMU 发现 X的最后两位为10,则取出x的前12位,拼接addr的后20位,组成物理地址。
- 访问此物理地址,获取数据返回给用户。
###part2
在zImage的反汇编代码中,input_data在_got_start段,值为0x4015,如下:
查看其内容:
查看piggy.gz:
可知整个zImage,实际上就是vmlinux(2.51MB那个)掐头去尾一点,vmlinux是个标准ELF文件,而zImage就是这个vmlinux去了ELF头尾组成的,其二进制差异不过1%20.
也就是说,实际上是vmlinux包含了piggy.gz,而将其裁剪一点,就形成了最终的zImage。
vmlinux(小的那个),是一个标准的elf文件,将其掐头去尾后就形成了zImage,这个就是最终的内核镜像。
zImage是最终的内核镜像,其内部包含了一个原封不动的piggy.gz,这是一个压缩后的内核。
piggy.gz是image通过gzip命令压缩来的。
Image文件是一个纯二进制文件,没有elf头,zImage中的最后一句 MOV PC, R4 ; call_kernel,这个R4就是之前kernel解压的地址,就是直接跳到了Image的相对偏移0位置的指令。
Image是从vmlinux(大)中抽取出来的,是objcopy -o binary vmlinux(大)来的。vmlinux(大)入口地址的第一条指令,就是Image这个二进制文件的第一条指令。
所以综上所述,zImage解压内核,跳转到Image偏移0处的指令,实际上相当于执行了vmlinux(大)的入口函数。而vmlinux(大)的入口函数为ENTRY(stext),定义在./arch/arm/kernel/head.S(注,zImage的入口函数定义在./arch/arm/boot/compressed/head.S),也就是说vmlinux(大)的入口函数,也是体系结构相关的!
————————————————
版权声明:本文为CSDN博主「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lidan113lidan/article/details/45043155