更多内容可关注微信公众号![]()
arm-linux内核启动学习笔记(二)(废弃)
#已合并到arm-linux内核启动学习笔记(一)
###Part2:(cache_on,call_kernel]
####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之后
cache_on之后段代的代码大体可认为做了三件事:
- 检查内核是否需要移动,如果需要移动则将其移动并重定向.got的内容。
- 调用decompress_kernel解压内核。
- 调用call_kernel跳转到Image。
//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]
/*
zImage的加载地址未必是编译地址,所以这里需要
将各个变量由编译地址修正为真实地址。
*/
//计算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
//将sp的编译地址修正为真实地址,并向上增加64K,作为栈顶
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对齐的。
*/
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
/*
* 这一段代码是来检测是否需要复制自身的
* r4是最终内核要解压到的地址,是通过zreladdr来指定的
* r9是Image镜像的大小,是从piggy.gz文件末尾获取的
* r10是piggy.gz的结束位置
* 往下执行需要满足两个条件之一:
* 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 -----------------------------------------------------------------------------
*/
//这里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开始见下面。
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
//要复制的数据大小
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
//到这里
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
*/
//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
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
###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");
}
这里的这一句”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();
}
###旧版废弃的内容,但是正确的
在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(大)的入口函数,也是体系结构相关的!