IMX6Solo启动流程-Linux 内核启动 二

释放双眼,带上耳机,听听看~!

写在前头

*.版权声明:本篇文章为原创,可随意转载,转载请注明出处,谢谢!另我创建一个QQ群82642304,欢迎加入!
*.目的:整理一下RIotBoard开发板的启动流程,对自己的所学做一个整理总结,本系列内核代码基于linux-3.0.35-imx。
*.备注:整个系列只是对我所学进行总结,记录我认为是关键的点,另我能力有限,难免出现疏漏错误,如果读者有发现请多指正,以免我误导他人!


内核自解压

上篇我们说到uImage的生成流程,内核真正的入口地址是在arch/arm/boot/compressed中,所以我们首先看下该目录下面的vmlinux.lds。


.text : {
_start = .;
*(.start)
*(.text)
(.text.)
*(.fixup)
*(.gnu.warning)
*(.rodata)
(.rodata.)
*(.glue_7)
*(.glue_7t)
*(.piggydata)
. = ALIGN(4);
}

入口地址就是*(.start)段,定义在head.S中。在这里要注意一下一个段是*(.piggydata),它的定义在piggy.gzip.S中。
piggy.gzip.S文件内容:


1
2
3
4
5
6
7
1    .section .piggydata,#alloc
2    .globl  input_data
3input_data:
4    .incbin "arch/arm/boot/compressed/piggy.gzip"
5    .globl  input_data_end
6input_data_end:
7

可以看出被压缩后的内核文件piggy.gzip被放在段.piggydata中,然后被链接到vmlinux。
PS:内核被压缩之后的长度在piggy.gzip文件的最后四个字节。


自解压代码的入口

被压缩后的内核以一个段数据的形式链接进来,所以自解压程序首先找到这个段数据的地址,然后解压缩到指定的地址。
入口在head.S中的.start段,如果把所有代码都贴出来会让文章显得很长,所有我会尽量少地贴代码,各位读者请参考head.S来看。
具体的执行流程为:

  1. 将r1、r2寄存器保存到r7、r8寄存器中,根据AAPCS对函数调用中寄存器的责任规定,r0、r1、r2寄存器分别保存的是前三个参数,所以实际上此时r1和r2的值就是Uboot中theKernel的第二个(机器ID)和第三个参数(内核参数地址)。
  2. 进入SVC模式,关闭中断。
  3. ldr r4, =zreladdr;将zreladdr赋值给r4寄存器。zreladdr的定义在同目录下的Makefile中:

ifneq (
$(CONFIG_AUTO_ZRELADDR),y)
LDFLAGS_vmlinux += –defsym zreladdr=
$(ZRELADDR)
endif

即宏ZERLADDR的值,而ZERLADDR又在arch/arm/boot/Makefile中定义为

ZRELADDR :=
$(zreladdr-y)

$(zreladdr-y)的定义在arch/arm/mach-xxx/Makefile.boot中,每个板子的定义各不相同,实际上zreladdr就是内核解压缩到的地址。解压之后的内核被放在以zreladdr开始的内存上。也是解压之后内核入口地址。
4. bl cache_on;开启I/D缓存,为了加速CPU执行速度,略过。
5. 加载LC0表,LC0的定义为:

LC0: .word LC0 @ r1
.word __bss_start @ r2
.word _end @ r3
.word _edata @ r6
.word input_data_end – 4 @ r10 (inflated size location)
.word _got_start @ r11
.word _got_end @ ip
.word .L_user_stack_end @ sp
.size LC0, . – LC0

里面的定义可以结合vmlinux.lds一起看。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1restart:    
2        adr r0, LC0;相当于add r0, pc, LC0
3        ldmia   r0, {r1, r2, r3, r6, r10, r11, r12};加载表
4        ldr sp, [r0, #28];加载sp指针,即.L_user_stack_end,定义在同一文件,一段4K的堆栈
5        /*
6         * We might be running at a different address.  We need
7         * to fix up various pointers.
8         * 由于linux加载地址可能跟链接地址不一致,所以重新计算_edata和input_data_end - 4的值
9         */
10        sub r0, r0, r1      @ calculate the delta offset
11        add r6, r6, r0      @ _edata
12        add r10, r10, r0        @ inflated kernel size location
13
14        /*
15         * The kernel build system appends the size of the
16         * decompressed kernel at the end of the compressed data
17         * in little-endian form.
18         * 计算出被压缩内核的长度,保存到r9
19         */
20        ldrb    r9, [r10, #0]
21        ldrb    lr, [r10, #1]
22        orr r9, r9, lr, lsl #8
23        ldrb    lr, [r10, #2]
24        ldrb    r10, [r10, #3]
25        orr r9, r9, lr, lsl #16
26        orr r9, r9, r10, lsl #24
27        /* malloc space is above the relocated stack (64k max) */
28        add sp, sp, r0;重新计算堆栈的地址
29        add r10, sp, #0x10000;再申请64K
30

在这里先稍微说一下链接地址和执行地址。链接地址就是链接程序给执行代码,变量符号等指定的地址。例如长跳转调用一个子函数,编译器会给这个子函数指定一个固定的链接地址xxxxxxxx,跳转的时候就是b xxxxxxxx;。链接之后的程序中,这些地址都已固定。
而运行地址就是实际上程序被加载到运行的地址。我们写的应用程序被加载的时候运行地址和链接地址是一致的,所以长跳转的时候不会有问题。如果链接地址和运行地址不一致,就会出现问题,比如长跳转到一个子函数地址为xxxxxxxx,但是由于该子函数的代码不是被加载到xxxxxxxx而是被加载到yyyyyyyy的地址,此时就会出错,同理变量也是。
在上面那段汇编代码中有个重新计算地址的就是出于这个考虑,如果链接地址和运行地址不一致,那么_edata和input_data_end所指向的地址就有问题。
计算的原理是:adr r0, LC0后r0里面保存的就是LC0的真实地址,然后加载LC0表,注意第一个数据就是定义LC0的链接地址,然后加载到r1寄存器中。sub r0, r0, r1就计算出运行地址和链接地址的偏移量,保存到寄存器r0中。接着就重新计算_edata和input_data_end的值。


总结

为了避免一片文章过长,先讲到这里。
看懂vmlinux.lds之后,就会对镜像文件的布局了解,结合该文件看代码,就不会有多少疑惑,还有一点就是要弄清楚运行地址和链接地址。

参考

暂无

给TA打赏
共{{data.count}}人
人已打赏
安全运维

WordPress网站专用docker容器环境带Waf

2020-7-18 20:04:44

安全运维

运维安全-Gitlab管理员权限安全思考

2021-9-19 9:16:14

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索