http://blog.chinaunix.net/uid-20672257-id-2891129.html
内核编译完成后会生成zImage内核镜像文件。关于bootloader加载zImage到内核,并且跳转到zImage开始地址运行zImage的过程,相信大家都很容易理解。但对于zImage是如何解压的过程,就不是那么好理解了。本文将结合部分关键代码,讲解zImage的解压过程。
先看看zImage的组成吧。在内核编译完成后会在arch/arm/boot/下生成zImage
在arch/arm/boot/Makefile中:
56 $(obj)/zImage: $(obj)/compressed/vmlinux FORCE
57 $(call if_changed,objcopy)
58 @echo ' Kernel: $@ is ready'
由此可见,zImage的是elf格式的arch/arm/boot/compressed/vmlinux二进制化得到的
在arch/arm/boot/compressed/Makefile中:
104 $(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/$(HEAD) $(obj)/piggy.$(suffix_y).o \
105 $(addprefix $(obj)/, $(OBJS)) $(lib1funcs) FORCE
106 $(call if_changed,ld)
107 @:
108
109 $(obj)/piggy.$(suffix_y): $(obj)/../Image FORCE
110 $(call if_changed,$(suffix_y))
111
112 $(obj)/piggy.$(suffix_y).o: $(obj)/piggy.$(suffix_y) FORCE
其中Image是由内核顶层目录下的vmlinux二进制化后得到的。注意:arch/arm/boot/compressed/vmlinux是位置无关的,这个有助于理解后面的代码。链接选项中有个 -fpic参数:
79 EXTRA_CFLAGS := -fpic -fno-builtin
在说-fpic参数前先说一下位置无关代码,位置无关代码主要是在访问全局变量和全局函数的时候采用了位置无关的重定位方法,既依赖GOT和PLT来重定位.普通的重定位方法需要修改代码段,比如偏移地址0x100处需要重定位,loader就修改代码段的0x100处的内容,通过查找重定位信息得到具体的值.这种方法需要修改代码段的内容,对于动态连接库来说,其初衷是让多个进程共享代码段,若对其进行写操作,就回引起COW,从而失去共享.
-fPIC选项告诉编绎器使用GOT和PLT的方法重定位,这两个都是数据段,因此避免了COW,真正实现了共享.如果不用-fPIC,动态连接库依然可以使用,但其重定位方法为一般方法,必然会引起COW.但也无所谓,除了性能在COW时稍微受些影响,其他也没啥
总结一下zImage的组成,它是由一个压缩后的内核piggy.o,连接上一段初始化及解压功能的代码(head.o misc.o),组成的。
下面就要看内核的启动了,那么内核是从什么地方开始运行的呢?这个当然要看lds文件啦。zImage的生成经历了两次大的链接过程:一次是顶层vmlinux的生成,由arch/arm/kernel/vmlinux.lds(这个lds文件是由arch/arm/kernel/vmlinux.lds.S生成的)决定;另一次是arch/arm/boot/compressed/vmlinux的生成,是由arch/arm/boot/compressed/vmlinux.lds(这个lds文件是由arch/arm/boot/compressed/vmlinux.lds.in生成的)决定。zImage的入口点应该由arch/arm/boot/compressed/vmlinux.lds决定。从中可以看出入口点为‘_start’,在分析lds文件前建议先看看前面的 Linux下的lds链接脚本基础一文
10 OUTPUT_ARCH(arm)
11 ENTRY(_start)
12 SECTIONS
13 {
14 /DISCARD/ : {
15 *(.ARM.exidx*)
16 *(.ARM.extab*)
17 /*
18 * Discard any r/w data - this produces a link error if we have any,
19 * which is required for PIC decompression. Local data generates
20 * GOTOFF relocations, which prevents it being relocated independently
21 * of the text/got segments.
22 */
23 *(.data)
24 }
25
26 . = 0;
27 _text = .;
28
29 .text : {
30 _start = .;
31 *(.start)
。。。。。。。。。。。
69 }
在arch/arm/boot/compressed/head.S中找到入口点.。也就是说文件arch/arm/boot/compressed/head.S是linux内核启动过程执行的第一个文件。
启动zImage内核时,u-boot调用boot_zImage函数(前面部分略过,从boot_zImage函数讲起),该函数完成以下工作:
1. 设置内核由nand flash复制到sdram中的地址:0x30008000;
2. 调用copy_kernel_img 函数复制内核到sdram;
3. 设置Image magic number;
4. 设置传递给内核的参数地址为0x30001000;
5. 设置机器码为193;
6. 最后调用call_linux函数,将控制权彻底交给内核。
当完成了上述工作后,内核开始启动,zImage内核的入口程序为:arch/arm/boot/compressed/head.S
我们现在来分析一下这个文件
123 .section ".start", #alloc, #execinstr
124 /*
125 * sort out different calling conventions
126 */
127 .align
128 start:
129 .type start,#function //type指定start这个符号是函数类型
130 .rept 8 //重复8次 mov r0, r0,
131 mov r0, r0 //空操作,让前面所取指令得以执行。
132 .endr
133
134 b 1f //跳转
/*
魔数0x016f2818是在bootloader中用于判断zImage的存在,
而zImage的判别的magic number为0x016f2818,这个也是内核和bootloader约定好的。
*/
135 .word 0x016f2818 @ Magic numbers to help the loader
136 .word start @ absolute load/run zImage address
137 .word _edata @ zImage end address
//r1和r2中分别存放着由bootloader传递过来的architecture ID和指向标记列表的指针。
138 1: mov r7, r1 @ save architecture ID
139 mov r8, r2 @ save atags pointer
140
/*
这也标志着u-boot将系统完全的交给了OS,bootloader生命终止。之后会读取
cpsr并判断是否处理器处于supervisor模式——从u-boot进入kernel,系统已经处于SVC32模式;
而利用angel进入则处于user模式,还需要额外两条指令。之后是再次确认中断关闭,并完成cpsr写入
*/
141 #ifndef __ARM_ARCH_2__
142 /*
143 * Booting from Angel - need to enter SVC mode and disable
144 * FIQs/IRQs (numeric definitions from angel arm.h source).
145 * We only do this if we were in user mode on entry.
146 */
147 mrs r2, cpsr @ get current mode
148 tst r2, #3 @ not user?
149 bne not_angel
150 mov r0, #0x17 @ angel_SWIreason_EnterSVC//0x17是angel_SWIreason_EnterSVC半主机操作
151 ARM( swi 0x123456 ) @ angel_SWI_ARM //0x123456是arm指令集的半主机操作编号
152 THUMB( svc 0xab ) @ angel_SWI_THUMB
153 not_angel:
154 mrs r2, cpsr @ turn off interrupts to
155 orr r2, r2, #0xc0 @ prevent angel from runnin g
156 msr cpsr_c, r2 //这里将cpsr中I、F位分别置“1”,关闭IRQ和FIQ
157 #else
158 teqp pc, #0x0c000003 @ turn off interrupts
159 #endif
160
161 /*
162 * Note that some cache flushing and other stuff may
163 * be needed here - is there an Angel SWI call for this?
164 */
165
166 /*
167 * some architecture specific code can be inserted
168 * by the linker here, but it should preserve r7, r8, and r 9.
169 */
170
/*
LC0表是链接文件arch/arm/boot/compressed/vmlinux.lds(这个lds文件是由arch/arm/boot/compressed/vmlinux.lds.in生成的)的各段入口。
10 OUTPUT_ARCH(arm)
11 ENTRY(_start)
12 SECTIONS
13 {
14 /DISCARD/ : {
15 *(.ARM.exidx*)
16 *(.ARM.extab*)
17 /*
18 * Discard any r/w data - this produces a link error if we have any,
19 * which is required for PIC decompression. Local data generates
20 * GOTOFF relocations, which prevents it being relocated independently
21 * of the text/got segments.
22 */
23 *(.data)
24 }
25
26 . = 0;
27 _text = .;
28
29 .text : {
30 _start = .;
31 *(.start)
32 *(.text)
33 *(.text.*)
34 *(.fixup)
35 *(.gnu.warning)
36 *(.rodata)
37 *(.rodata.*)
38 *(.glue_7)
39 *(.glue_7t)
40 *(.piggydata)
41 . = ALIGN(4);
42 }
43
44 _etext = .;
45
46 /* Assume size of decompressed image is 4x the compressed image */
47 _image_size = (_etext - _text) * 4;
48
49 _got_start = .;
50 .got : { *(.got) }
51 _got_end = .;
52 .got.plt : { *(.got.plt) }
53 _edata = .;
54
55 . = ALIGN(4);
56 __bss_start = .;
57 .bss : { *(.bss) }
58 _end = .;
59
60 .stack (NOLOAD) : { *(.stack) }
61
62 .stab 0 : { *(.stab) }
63 .stabstr 0 : { *(.stabstr) }
64 .stab.excl 0 : { *(.stab.excl) }
65 .stab.exclstr 0 : { *(.stab.exclstr) }
66 .stab.index 0 : { *(.stab.index) }
67 .stab.indexstr 0 : { *(.stab.indexstr) }
68 .comment 0 : { *(.comment) }
69 }
展开如下表:
zImage在内存中的初始地址为0x30008000,那么这个地址是怎么来的?
查看2410的 datasheet ,发现内存映射的基址是0x3000 0000 ,那么0x30008000又是如何来的呢?
在内核文档 Document/arm/Booting 文件中有:
5. Calling the kernel image
---------------------------
Existing boot loaders: MANDATORY
New boot loaders: MANDATORY
There are two options for calling the kernel zImage. If the zImage
is stored in flash, and is linked correctly to be run from flash,
then it is legal for the boot loader to call the zImage in flash
directly.
The zImage may also be placed in system RAM (at any location) and
called there. Note that the kernel uses 16K of RAM below the image
to store page tables. The recommended placement is 32KiB into RAM.
看来在 image 下面用了32K(0x8000)的空间存放内核页表,
0x30008000就是2410的内核在 RAM 中的启动地址,这个地址就是这么来的。所以后面zreladdr(内核运行地址)及bootloader中都定义相关的宏来定位到这个地址
1、初始状态
链接文件arch/arm/boot/compressed/vmlinux.lds中的连接地址都是位置无关的,即都是以0地址为偏移的。而此时内核已被bootloader搬移到了SDRAM中。链接地址应该加上这个偏移。
*/
171 .text
172 adr r0, LC0 //指令adr是基于PC的值来获取标号LC0的地址的,LC0在后面第315行定义,由于内核已被搬移,标号LC0的地址不是以地址0为偏移的,这中间就存在一个固定的地址偏移,在s3c2410中是0x30008000.
173 ldmia r0, {r1, r2, r3, r5, r6, r11, ip}
174 ldr sp, [r0, #28]
175 #ifdef CONFIG_AUTO_ZRELADDR
176 @ determine final kernel image address
177 and r4, pc, #0xf8000000
178 add r4, r4, #TEXT_OFFSET
179 #else
180 ldr r4, =zreladdr
/*zreladdr内核运行地址,相关定义如下:
arch/arm/boot/Makefile
24 ZRELADDR := $(zreladdr-y)
25 PARAMS_PHYS := $(params_phys-y)
arch/arm/mach-s3c2410/Makefile.boot
1 ifeq ($(CONFIG_PM_H1940),y)
2 zreladdr-y := 0x30108000
3 params_phys-y := 0x30100100
4 else
5 zreladdr-y := 0x30008000
6 params_phys-y := 0x30000100
7 endif
*/
181 #endif
182 subs r0, r0, r1 @ calculate the delta offse t
//这里获得当前运行地址与链接地址的偏移量,存入r0中为0x30008000.如果不需要重定位,即内核没有进行过搬移,就跳转。如果内核代码是存于NANDflash中的是需要搬移的。
183
184 @ if delta is zero, we are
185 beq not_relocated @ running at the address we
186 @ were linked at.
187
188 /*
189 * We're running at a different address. We need to fix
190 * up various pointers:
191 * r5 - zImage base address (_start)
192 * r6 - size of decompressed image
193 * r11 - GOT start
194 * ip - GOT end
195 */
196 add r5, r5, r0 //修改内核映像基地址此时r5=0x30008000
197 add r11, r11, r0 //修改got表的起始和结束位置
198 add ip, ip, r0
199
200 #ifndef CONFIG_ZBOOT_ROM
201 /*
202 * If we're running fully PIC === CONFIG_ZBOOT_ROM = n,
203 * we need to fix up pointers into the BSS region.
204 * r2 - BSS start
205 * r3 - BSS end
206 * sp - stack pointer
207 */
//S3C2410平台是需要一下调整的,将bss段以及堆栈的地址都进行调整
208 add r2, r2, r0
209 add r3, r3, r0
210 add sp, sp, r0
211
212 /*
213 * Relocate all entries in the GOT table.
214 *///修改GOT(全局偏移表)表。根据当前的运行地址,修正该表
215 1: ldr r1, [r11, #0] @ relocate entries in the G OT
216 add r1, r1, r0 @ table. This fixes up the
217 str r1, [r11], #4 @ C references.
218 cmp r11, ip
219 blo 1b
220 #else
221
222 /*
223 * Relocate entries in the GOT table. We only relocate
224 * the entries that are outside the (relocated) BSS region.
225 *///S3C2410平台不会进入该分支,只对got表中在bss段以外的符号进行重定位
226 1: ldr r1, [r11, #0] @ relocate entries in the G OT
227 cmp r1, r2 @ entry < bss_start ||
228 cmphs r3, r1 @ _end < entry
229 addlo r1, r1, r0 @ table. This fixes up the
230 str r1, [r11], #4 @ C references.
231 cmp r11, ip
232 blo 1b
233 #endif
234
//下面的代码,如果运行当前运行地址和链接地址相等,则不需进行重定位。直接清除bss段
235 not_relocated: mov r0, #0
236 1: str r0, [r2], #4 //清BSS段,所有的arm程序都需要做这些的
237 str r0, [r2], #4
238 str r0, [r2], #4
239 str r0, [r2], #4
240 cmp r2, r3
241 blo 1b
242
243 /*
244 * The C runtime environment should now be setup
245 * sufficiently. Turn the cache on, set up some
246 * pointers, and start decompressing.
247 */
248 bl cache_on //打开cache
249
250 mov r1, sp @ malloc space above stack
251 add r2, sp, #0x10000 @ 64k max 分配一段解压函数需要的内存缓冲,参见下图。
252
253 /*
254 * Check to see if we will overwrite ourselves.
255 * r4 = final kernel address
256 * r5 = start of this image
257 * r6 = size of decompressed image
258 * r2 = end of malloc space (and therefore this image)
259 * We basically want:
260 * r4 >= r2 -> OK
261 * r4 + image length <= r5 -> OK
262 */
263 cmp r4, r2 //r4为内核执行地址,此时为0X30008000,r2此时为用户栈定,即解压函数所需内存缓冲的开始处,显然r4 < r2所以不会跳转。
264 bhs wont_overwrite
265 add r0, r4, r6
266 cmp r0, r5
//r5是内核映像的开始地址0X30008000,r6为内核映像大小,r4为解压后内核开始地址,此时为0X30008000,r5为解压前映存放的开始位置,此时也为0X30008000。下面的判断是,看解压后的内核是不是会覆盖未解压的映像。显然是覆盖的,所以是不会跳转的。注意:内核映像解压后不会超过解压前的4倍大小。
267 bls wont_overwrite
268
269 mov r5, r2 @ decompress after malloc space//此时r2为解压函数缓冲区的尾部地址。
270 mov r0, r5 //r0为映像解压后存放的起始地址,解压后的内核就紧接着存放在解压函数缓冲区的尾部。
271 mov r3, r7 //r7中存放的是architecture ID,对于SMDK2410这个值为193;
/*
解压函数是用C语言实现的在文件arch/arm/boot/compressed/misc.c中。
解压函数的是个参数是有r0~r3传入的。r0~r3的值在上面已初始化,再对照上面表格就很清楚了。
decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,
unsigned long free_mem_ptr_end_p,
int arch_id)
output_start:指解压后内核输出的起始位置,此时它的值参考上面的图表,紧接在解压缓冲区后;
free_mem_ptr_p:解压函数需要的内存缓冲开始地址;
free_mem_ptr_end_p:解压函数需要的内存缓冲结束地址,共64K;
arch_id :architecture ID,对于SMDK2410这个值为193;
*/
272 bl decompress_kernel
/*下图是head.S调用misc.c中的decompress_kernel刚解压完内核后,还没有进行重定位时的情况
解压完毕后,内核长度返回值存放在r0寄存器里。在内核末尾空出128字节的栈空间,并且使其长度128字节对齐。*/
273
274 add r0, r0, #127 + 128 @ alignment + stack
275 bic r0, r0, #127 @ align the kernel length
276 /*
277 * r0 = 解压后内核的长度
278 * r1-r3 = 没使用
279 * r4 = 内核执行地址
280 * r5 = decompressed kernel start解压后内核的起始地址,如上面初始化 mov r5, r2
281 * r7 = architecture ID 处理器ID
282 * r8 = atags pointer 标记列表地址
283 * r9-r12,r14 = corrupted
284 */
/*
上面只是将内核临时解压到了一个位置,下面还要将它重定位到0X30008000处。
标号reloc_start下面有一段重定位内核的程序。为了在内核重定位的过程中不至于将这段用于
重定位的代码给覆盖了,就先将这段用于内核重定位的代码搬到另一个地方,如下表。
*/
285 add r1, r5, r0 //r1就是解压后内核代码的结束位置,下面就是将这段重定位代码搬移到r1地址处。
286 adr r2, reloc_start //重定位代码起始地址
287 ldr r3, LC1 //用于内核重定位的代码的长度
288 add r3, r2, r3 //重定位代码的结束地址
289 1: ldmia r2!, {r9 - r12, r14} @ copy relocation code//将这段重定位代码搬移到r1地址处,如上表。
290 stmia r1!, {r9 - r12, r14}
291 ldmia r2!, {r9 - r12, r14}
292 stmia r1!, {r9 - r12, r14}
293 cmp r2, r3
294 blo 1b
295 mov sp, r1
296 add sp, sp, #128 @ relocate the stack//改变堆栈指针位置。
297
298 bl cache_clean_flush //搬移完成后刷新cache,因为代码地址变化了不能让cache再命中被内核覆盖的老地址。
299 ARM( add pc, r5, r0 ) @ call relocation code//r0 + r5 就是被搬移后的内核重定位代码的开始位置,reloc_start。下面将会讲到。
300 THUMB( add r12, r5, r0 )
301 THUMB( mov pc, r12 ) @ call relocation code
302
303 /*
304 * We're not in danger of overwriting ourselves. Do this the simple way.
305 *
306 * r4 = kernel execution address
307 * r7 = architecture ID
308 */
//如果内核映像没有被bootloader搬移过,上面程序就会跳到此处。
309 wont_overwrite: mov r0, r4
310 mov r3, r7
311 bl decompress_kernel
312 b call_kernel
313
314 .align 2
315 .type LC0, #object
//这个表与文件arch/arm/kernel/vmlinux.lds.S(这个lds文件是由arch/arm/boot/compressed/vmlinux.lds.in生成的)
316 LC0: .word LC0 @ r1
317 .word __bss_start @ r2
318 .word _end @ r3
319 .word _start @ r5
320 .word _image_size @ r6
321 .word _got_start @ r11
322 .word _got_end @ ip
323 .word user_stack_end @ sp
324 LC1: .word reloc_end - reloc_start
325 .size LC0, . - LC0
下面代码是将解压后的内核代码重定位。过程见下图
//下面代码就是实现将解压后的内核代码搬到0X30008000处
546 /*
547 * All code following this line is relocatable. It is relocated by
548 * the above code to the end of the decompressed kernel image and
549 * executed there. During this time, we have no stacks.
550 *
551 * r0 = decompressed kernel length
552 * r1-r3 = unused
553 * r4 = kernel execution address
554 * r5 = decompressed kernel start
555 * r7 = architecture ID
556 * r8 = atags pointer
557 * r9-r12,r14 = corrupted
558 */
559 .align 5
560 reloc_start: add r9, r5, r0 // r0 + r5就是解压后内核代码的结束位置加128字节栈空间。
561 sub r9, r9, #128 @ do not copy the stack
562 debug_reloc_start
563 mov r1, r4 //r4为内核执行地址,即为0X30008000。
564 1:
565 .rept 4 //将解压后的内核搬到r1处,即0X30008000处。
566 ldmia r5!, {r0, r2, r3, r10 - r12, r14} @ relocate kernel
567 stmia r1!, {r0, r2, r3, r10 - r12, r14}
568 .endr
569
570 cmp r5, r9
571 blo 1b
572 mov sp, r1
573 add sp, sp, #128 @ relocate the stack
574 debug_reloc_end
575
//清除并关闭cache,清零r0,将machine ID存入r1,atags指针存入r2,再跳入0x30008000执行真正的内核Image
576 call_kernel: bl cache_clean_flush
577 bl cache_off
578 mov r0, #0 @ must be zero
579 mov r1, r7 @ restore architecture numb er
580 mov r2, r8 @ restore atags pointer
581 mov pc, r4 @ call kernel
582
此时内核解压已经完成。内核启动要执行的第二个文件就是arch/arm/kernel/head.S文件。
总结一下head.S会做些什么样的工作:
1、对于各种Arm CPU的DEBUG输出设定,通过定义宏来统一操作;
2、设置kernel开始和结束地址,保存architecture ID;
3、如果在ARM2以上的CPU中,用的是普通用户模式,则升到超级用户模式,然后关中断
4、分析LC0结构delta offset,判断是否需要重载内核地址(r0存入偏移量,判断r0是否为零)。
5、需要重载内核地址,将r0的偏移量加到BSS region和GOT table中的每一项。对于位置无关的代码,程序是通过GOT表访问全局数据目标的,也就是说GOT表中中记录的是全局数据目标的绝对地址,所以其中的每一项也需要重载。
6、清空bss堆栈空间r2-r3l,建立C程序运行需要的缓存
7、这时r2是缓存的结束地址,r4是kernel的最后执行地址,r5是kernel境象文件的开始地址
8、用文件misc.c的函数decompress_kernel(),解压内核于缓存结束的地方(r2地址之后)。
为了更清楚的了解解压的动态过程。我们用图表的方式描述下代码的搬运解压过程。然后再针对中间的一些关键过程阐述。
zImage在内存中的初始地址为0x30008000(这个地址由bootloader决定,位置不固定)u-boot会将zImage镜像copy到sdram的0x30008000位置处。此时为初始状态,这里称为状态1。
1、初始状态
|.text | 0x30008000开始,包含piggydata段(即压缩的内核段)
|. got |
|. data |
|.bss |
|.stack | 4K大小
2、 head.S调用misc.c中的decompress_kernel刚解压完内核后,内存中的各段位置如下,状态2
.text | 0x30008000开始,包含piggydata段(即压缩的内核段)
. got |
. data |
.bss |
.stack |4K大小
解压函数所需缓冲区 | 64K大小
解压后的内核代码 | 小于4M
3、当如果head.S中有代码搬运工作时,即出现overwrite时,内存中的各段位置如下,此时会将head.S中的部分代码重定位,状态3
.text |0x30008000开始,包含piggydata段(即压缩的内核段)
. got |
. data |
.bss |
.stack | 4K大小
解压函数所需缓冲区 | 64K大小
解压后的内核代码 | 小于4M
head.S中的部分重定位代码代码 | reloc_start至reloc_end
4、跳转到重定位后的reloc_start处,由reloc_start至reloc_end的代码复制解压后的内核代码到0x30008000处,并调用call_kernel跳转到0x30008000处执行。
解压后的内核 | 0x30008000开始
在通过head.S了解了动态过程后,我们可能会有几个问题:
问题1:zImage是如何知道自己最后的运行地址是0x30008000的?
问题2:调用decompress_kernel函数时,其4个参数是什么值及物理含义?
问题3:解压函数是如何确定代码中压缩内核位置的?
先回答第1个问题
zImage在内存中的初始地址为0x30008000,那么这个地址是怎么来的?
查看2410的 datasheet ,发现内存映射的基址是0x3000 0000 ,那么0x30008000又是如何来的呢?
在内核文档 Document/arm/Booting 文件中有:
5. Calling the kernel image
---------------------------
Existing boot loaders: MANDATORY
New boot loaders: MANDATORY
There are two options for calling the kernel zImage. If the zImage
is stored in flash, and is linked correctly to be run from flash,
then it is legal for the boot loader to call the zImage in flash
directly.
The zImage may also be placed in system RAM (at any location) and
called there. Note that the kernel uses 16K of RAM below the image
to store page tables. The recommended placement is 32KiB into RAM.
看来在 image 下面用了32K(0x8000)的空间存放内核页表,
再来先看看TEXT_OFFSET( 内核 在RAM中的 起始位置相对于 RAM起始地址偏移。值为0x00008000)它在
./arch/arm/Makefile中定义
118 textofs-y := 0x00008000
222 TEXT_OFFSET := $(textofs-y)
此处,定义了Image的偏移地址。
0x30008000就是2410的内核在 RAM 中的启动地址,这个地址就是这么来的。所以后面zreladdr(内核运行地址)及bootloader中都定义相关的宏来定位到这个地址
在arch/arm/boot/Makefile文件中 ZRELADDR := $(zreladdr-y)
arch/arm/boot/Makefile
24 ZRELADDR := $(zreladdr-y)
25 PARAMS_PHYS := $(params_phys-y)
在arch/arm/mach-s3c2410/Makefile.boot中zreladdr-y := 0x30008000这个就是zImage的运行地址了,
arch/arm/mach-s3c2410/Makefile.boot
1 ifeq ($(CONFIG_PM_H1940),y)
2 zreladdr-y := 0x30108000
3 params_phys-y := 0x30100100
4 else
5 zreladdr-y := 0x30008000
6 params_phys-y := 0x30000100
7 endif
在arch/arm/boot/compressed/head.S中有
180 ldr r4, =zreladdr 内核就是用这种方式让代码知道最终运行的位置的
此处定义了内核的物理地址和 u-boot 传递参数的地址,也就是加载地址。此处要和 u-boot 一致。
接下来再回答第2个问题
decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,
unsigned long free_mem_ptr_end_p,
int arch_id)
output_start:指解压后内核输出的起始位置,此时它的值参考上面的图表,紧接在解压缓冲区后;
free_mem_ptr_p:解压函数需要的内存缓冲开始地址;
ulg free_mem_ptr_end_p:解压函数需要的内存缓冲结束地址,共64K;
arch_id :architecture ID,对于SMDK2410这个值为193;
最后回答第3个问题
首先看看piggy.o是如何生成的,
打开arch/arm/boot/Makefile文件:
49 $(obj)/Image: vmlinux FORCE
50 $(call if_changed,objcopy)
51 @echo ' Kernel: $@ is ready'
56 $(obj)/zImage: $(obj)/compressed/vmlinux FORCE
57 $(call if_changed,objcopy)
58 @echo ' Kernel: $@ is ready'
可以看出,zImage是vmlinux通过objcopy后生成的;打开arch/arm/boot/compressed/Makefie文件:
63 suffix_$(CONFIG_KERNEL_GZIP) = gzip
64 suffix_$(CONFIG_KERNEL_LZO) = lzo
65 suffix_$(CONFIG_KERNEL_LZMA) = lzma
67 targets := vmlinux vmlinux.lds \
68 piggy.$(suffix_y) piggy.$(suffix_y).o \
69 font.o font.c head.o misc.o $(OBJS)
./arch/arm/boot/Makefile:30:targets := Image zImage xipImage bootpImage uImage
104 $(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/$(HEAD) $(obj)/piggy.$(suffix_y).o \
105 $(addprefix $(obj)/, $(OBJS)) $(lib1funcs) FORCE
106 $(call if_changed,ld)
107 @:
109 $(obj)/piggy.$(suffix_y): $(obj)/../Image FORCE
110 $(call if_changed,$(suffix_y))
111
112 $(obj)/piggy.$(suffix_y).o: $(obj)/piggy.$(suffix_y) FORCE
可以看出,当配置内核为GZIP方式时,zImage的压缩方式使用gzip。现在可以清楚的明白zImage和vmlinux的关系了,一句话总结一下:zImage就是vmlinux通过objcopy、gzip压缩后,得到的内核,其头部是由head.S misc.c组成的自解压代码。
而u-boot所一直追捧的uImage格式,只是在zImage的前面加上了40Byte的内核头部信息,由于本文主要讲解zImage,此处暂不对uImage进行过多的分析。这里内核默认采用gzip方式,所以第112行也就是
piggy.gzip
$(obj)/piggy.gzip.o: $(obj)/piggy.gzip FORCE
piggy.gzip.o是由piggy.S生成的,咱们看看piggy.gzip.S的内容:
.section .piggydata,#alloc
.globl input_data
input_data:
.incbin "arch/arm/boot/compressed/piggy.gzip"
.globl input_data_end
input_data_end:
再看看misc.c中decompress_kernel函数吧,它将调用
do_decompress(input_data, input_data_end - input_data,
output_data, error);
解压内核。发现什么没?这里的input_data不正是piggy.S里的input_data吗?这个时候应该明白内核是怎样确定piggy.gz在zImage中的位置了吧。
最后说明一点的是do_decompress()在arch/arm/boot/compressed/decompresse.c中定义,它将调用decompress,进而调用bunzip2()或unlzma()来获取压缩内核代码。具体解压过程以后有时间再分析。
unsigned long
decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,
unsigned long free_mem_ptr_end_p,
int arch_id)
{
unsigned char *tmp;
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();
tmp = (unsigned char *) (((unsigned long)input_data_end) - 4);
output_ptr = get_unaligned_le32(tmp);
putstr("Uncompressing Linux...");
do_decompress(input_data, input_data_end - input_data,
output_data, error);
putstr(" done, booting the kernel.\n");
return output_ptr;
}
decompress_kernel()先调用arch_decomp_setup()进行设置,初始化,实现的功能是检测CPU型号、使能看门狗(如果在配置内核时配置的前提下)和串口。然后使用在打印出信息“Uncompressing Linux...”后,之后调用do_decompress函数进行解压,do_decompress函数是在arch/arm/boot/compressed/Decompress.c文件中的。将内核放于指定的位置。最后打印出信息" done, booting the kernel." do_decompress函数其代码如下。
#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
void do_decompress(u8 *input, int len, u8 *output, void (*error)(char *x))
{
decompress(input, len, NULL, NULL, output, NULL, error);
}
当配置内核时选中GZIP方式,就会#include “../../../../lib/decompress_inflate.c”。文件最后一行为:
#define decompress gunzip
即:最后调用 gunzip函数对zImage进行解压。
当完成所有解压任务后,又将跳转回head.S文件中,执行call_kernel,将启动真正的Image.
arch/arm/plat-samsung/include/plat/uncompress.h
typedef unsigned int upf_t; /* cannot include linux/serial_core.h */
/* uart setup */
static unsigned int fifo_mask;
static unsigned int fifo_max;
/* forward declerations */
static void arch_detect_cpu(void);
/* defines for UART registers */
#include <plat/regs-serial.h>
#include <plat/regs-watchdog.h>
/* working in physical space... */
#undef S3C2410_WDOGREG
#define S3C2410_WDOGREG(x) ((S3C24XX_PA_WATCHDOG + (x)))
/* how many bytes we allow into the FIFO at a time in FIFO mode */
#define FIFO_MAX (14)
#define uart_base S3C_PA_UART + (S3C_UART_OFFSET * CONFIG_S3C_LOWLEVEL_UART_PORT)
static __inline__ void
uart_wr(unsigned int reg, unsigned int val)
{
volatile unsigned int *ptr;
ptr = (volatile unsigned int *)(reg + uart_base);
*ptr = val;
}
static __inline__ unsigned int
uart_rd(unsigned int reg)
{
volatile unsigned int *ptr;
ptr = (volatile unsigned int *)(reg + uart_base);
return *ptr;
}
/* we can deal with the case the UARTs are being run
* in FIFO mode, so that we don't hold up our execution
* waiting for tx to happen...
*/
static void putc(int ch)
{
if (uart_rd(S3C2410_UFCON) & S3C2410_UFCON_FIFOMODE) {
int level;
while (1) {
level = uart_rd(S3C2410_UFSTAT);
level &= fifo_mask;
if (level < fifo_max)
break;
}
} else {
/* not using fifos */
while ((uart_rd(S3C2410_UTRSTAT) & S3C2410_UTRSTAT_TXE) != S3C2410_UTRSTAT_TXE)
barrier();
}
/* write byte to transmission register */
uart_wr(S3C2410_UTXH, ch);
}
static inline void flush(void)
{
}
#define __raw_writel(d, ad) \
do { \
*((volatile unsigned int __force *)(ad)) = (d); \
} while (0)
/* CONFIG_S3C_BOOT_WATCHDOG
*
* Simple boot-time watchdog setup, to reboot the system if there is
* any problem with the boot process
*/
#ifdef CONFIG_S3C_BOOT_WATCHDOG
#define WDOG_COUNT (0xff00)
static inline void arch_decomp_wdog(void)
{
__raw_writel(WDOG_COUNT, S3C2410_WTCNT);
}
static void arch_decomp_wdog_start(void)
{
__raw_writel(WDOG_COUNT, S3C2410_WTDAT);
__raw_writel(WDOG_COUNT, S3C2410_WTCNT);
__raw_writel(S3C2410_WTCON_ENABLE | S3C2410_WTCON_DIV128 | S3C2410_WTCON_RSTEN | S3C2410_WTCON_PRESCALE(0x80), S3C2410_WTCON);
}
#else
#define arch_decomp_wdog_start()
#define arch_decomp_wdog()
#endif
#ifdef CONFIG_S3C_BOOT_ERROR_RESET
static void arch_decomp_error(const char *x)
{
putstr("\n\n");
putstr(x);
putstr("\n\n -- System resetting\n");
__raw_writel(0x4000, S3C2410_WTDAT);
__raw_writel(0x4000, S3C2410_WTCNT);
__raw_writel(S3C2410_WTCON_ENABLE | S3C2410_WTCON_DIV128 | S3C2410_WTCON_RSTEN | S3C2410_WTCON_PRESCALE(0x40), S3C2410_WTCON);
while(1);
}
#define arch_error arch_decomp_error
#endif
#ifdef CONFIG_S3C_BOOT_UART_FORCE_FIFO
static inline void arch_enable_uart_fifo(void)
{
u32 fifocon = uart_rd(S3C2410_UFCON);
if (!(fifocon & S3C2410_UFCON_FIFOMODE)) {
fifocon |= S3C2410_UFCON_RESETBOTH;
uart_wr(S3C2410_UFCON, fifocon);
/* wait for fifo reset to complete */
while (1) {
fifocon = uart_rd(S3C2410_UFCON);
if (!(fifocon & S3C2410_UFCON_RESETBOTH))
break;
}
}
}
#else
#define arch_enable_uart_fifo() do { } while(0)
#endif
static void
arch_decomp_setup(void)
{
/* we may need to setup the uart(s) here if we are not running
* on an BAST... the BAST will have left the uarts configured
* after calling linux.
*/
arch_detect_cpu();
arch_decomp_wdog_start();
/* Enable the UART FIFOs if they where not enabled and our
* configuration says we should turn them on.
*/
arch_enable_uart_fifo();
}
arch/arm/mach-s3c2410/include/mach/uncompress.h
static inline int is_arm926(void)
{
unsigned int cpuid;
asm volatile ("mrc p15, 0, %0, c1, c0, 0" : "=r" (cpuid));
return ((cpuid & 0xff0) == 0x260);
}
static void arch_detect_cpu(void)
{
unsigned int cpuid;
cpuid = *((volatile unsigned int *)S3C2410_GSTATUS1);
cpuid &= S3C2410_GSTATUS1_IDMASK;
if (is_arm926() || cpuid == S3C2410_GSTATUS1_2440 ||
cpuid == S3C2410_GSTATUS1_2442 ||
cpuid == S3C2410_GSTATUS1_2416 ||
cpuid == S3C2410_GSTATUS1_2450) {
fifo_mask = S3C2440_UFSTAT_TXMASK;
fifo_max = 63 << S3C2440_UFSTAT_TXSHIFT;
} else {
fifo_mask = S3C2410_UFSTAT_TXMASK;
fifo_max = 15 << S3C2410_UFSTAT_TXSHIFT;
}
}
先看看zImage的组成吧。在内核编译完成后会在arch/arm/boot/下生成zImage
在arch/arm/boot/Makefile中:
56 $(obj)/zImage: $(obj)/compressed/vmlinux FORCE
57 $(call if_changed,objcopy)
58 @echo ' Kernel: $@ is ready'
由此可见,zImage的是elf格式的arch/arm/boot/compressed/vmlinux二进制化得到的
在arch/arm/boot/compressed/Makefile中:
104 $(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/$(HEAD) $(obj)/piggy.$(suffix_y).o \
105 $(addprefix $(obj)/, $(OBJS)) $(lib1funcs) FORCE
106 $(call if_changed,ld)
107 @:
108
109 $(obj)/piggy.$(suffix_y): $(obj)/../Image FORCE
110 $(call if_changed,$(suffix_y))
111
112 $(obj)/piggy.$(suffix_y).o: $(obj)/piggy.$(suffix_y) FORCE
其中Image是由内核顶层目录下的vmlinux二进制化后得到的。注意:arch/arm/boot/compressed/vmlinux是位置无关的,这个有助于理解后面的代码。链接选项中有个 -fpic参数:
79 EXTRA_CFLAGS := -fpic -fno-builtin
在说-fpic参数前先说一下位置无关代码,位置无关代码主要是在访问全局变量和全局函数的时候采用了位置无关的重定位方法,既依赖GOT和PLT来重定位.普通的重定位方法需要修改代码段,比如偏移地址0x100处需要重定位,loader就修改代码段的0x100处的内容,通过查找重定位信息得到具体的值.这种方法需要修改代码段的内容,对于动态连接库来说,其初衷是让多个进程共享代码段,若对其进行写操作,就回引起COW,从而失去共享.
-fPIC选项告诉编绎器使用GOT和PLT的方法重定位,这两个都是数据段,因此避免了COW,真正实现了共享.如果不用-fPIC,动态连接库依然可以使用,但其重定位方法为一般方法,必然会引起COW.但也无所谓,除了性能在COW时稍微受些影响,其他也没啥
总结一下zImage的组成,它是由一个压缩后的内核piggy.o,连接上一段初始化及解压功能的代码(head.o misc.o),组成的。
下面就要看内核的启动了,那么内核是从什么地方开始运行的呢?这个当然要看lds文件啦。zImage的生成经历了两次大的链接过程:一次是顶层vmlinux的生成,由arch/arm/kernel/vmlinux.lds(这个lds文件是由arch/arm/kernel/vmlinux.lds.S生成的)决定;另一次是arch/arm/boot/compressed/vmlinux的生成,是由arch/arm/boot/compressed/vmlinux.lds(这个lds文件是由arch/arm/boot/compressed/vmlinux.lds.in生成的)决定。zImage的入口点应该由arch/arm/boot/compressed/vmlinux.lds决定。从中可以看出入口点为‘_start’,在分析lds文件前建议先看看前面的 Linux下的lds链接脚本基础一文
10 OUTPUT_ARCH(arm)
11 ENTRY(_start)
12 SECTIONS
13 {
14 /DISCARD/ : {
15 *(.ARM.exidx*)
16 *(.ARM.extab*)
17 /*
18 * Discard any r/w data - this produces a link error if we have any,
19 * which is required for PIC decompression. Local data generates
20 * GOTOFF relocations, which prevents it being relocated independently
21 * of the text/got segments.
22 */
23 *(.data)
24 }
25
26 . = 0;
27 _text = .;
28
29 .text : {
30 _start = .;
31 *(.start)
。。。。。。。。。。。
69 }
在arch/arm/boot/compressed/head.S中找到入口点.。也就是说文件arch/arm/boot/compressed/head.S是linux内核启动过程执行的第一个文件。
启动zImage内核时,u-boot调用boot_zImage函数(前面部分略过,从boot_zImage函数讲起),该函数完成以下工作:
1. 设置内核由nand flash复制到sdram中的地址:0x30008000;
2. 调用copy_kernel_img 函数复制内核到sdram;
3. 设置Image magic number;
4. 设置传递给内核的参数地址为0x30001000;
5. 设置机器码为193;
6. 最后调用call_linux函数,将控制权彻底交给内核。
当完成了上述工作后,内核开始启动,zImage内核的入口程序为:arch/arm/boot/compressed/head.S
我们现在来分析一下这个文件
123 .section ".start", #alloc, #execinstr
124 /*
125 * sort out different calling conventions
126 */
127 .align
128 start:
129 .type start,#function //type指定start这个符号是函数类型
130 .rept 8 //重复8次 mov r0, r0,
131 mov r0, r0 //空操作,让前面所取指令得以执行。
132 .endr
133
134 b 1f //跳转
/*
魔数0x016f2818是在bootloader中用于判断zImage的存在,
而zImage的判别的magic number为0x016f2818,这个也是内核和bootloader约定好的。
*/
135 .word 0x016f2818 @ Magic numbers to help the loader
136 .word start @ absolute load/run zImage address
137 .word _edata @ zImage end address
//r1和r2中分别存放着由bootloader传递过来的architecture ID和指向标记列表的指针。
138 1: mov r7, r1 @ save architecture ID
139 mov r8, r2 @ save atags pointer
140
/*
这也标志着u-boot将系统完全的交给了OS,bootloader生命终止。之后会读取
cpsr并判断是否处理器处于supervisor模式——从u-boot进入kernel,系统已经处于SVC32模式;
而利用angel进入则处于user模式,还需要额外两条指令。之后是再次确认中断关闭,并完成cpsr写入
*/
141 #ifndef __ARM_ARCH_2__
142 /*
143 * Booting from Angel - need to enter SVC mode and disable
144 * FIQs/IRQs (numeric definitions from angel arm.h source).
145 * We only do this if we were in user mode on entry.
146 */
147 mrs r2, cpsr @ get current mode
148 tst r2, #3 @ not user?
149 bne not_angel
150 mov r0, #0x17 @ angel_SWIreason_EnterSVC//0x17是angel_SWIreason_EnterSVC半主机操作
151 ARM( swi 0x123456 ) @ angel_SWI_ARM //0x123456是arm指令集的半主机操作编号
152 THUMB( svc 0xab ) @ angel_SWI_THUMB
153 not_angel:
154 mrs r2, cpsr @ turn off interrupts to
155 orr r2, r2, #0xc0 @ prevent angel from runnin g
156 msr cpsr_c, r2 //这里将cpsr中I、F位分别置“1”,关闭IRQ和FIQ
157 #else
158 teqp pc, #0x0c000003 @ turn off interrupts
159 #endif
160
161 /*
162 * Note that some cache flushing and other stuff may
163 * be needed here - is there an Angel SWI call for this?
164 */
165
166 /*
167 * some architecture specific code can be inserted
168 * by the linker here, but it should preserve r7, r8, and r 9.
169 */
170
/*
LC0表是链接文件arch/arm/boot/compressed/vmlinux.lds(这个lds文件是由arch/arm/boot/compressed/vmlinux.lds.in生成的)的各段入口。
10 OUTPUT_ARCH(arm)
11 ENTRY(_start)
12 SECTIONS
13 {
14 /DISCARD/ : {
15 *(.ARM.exidx*)
16 *(.ARM.extab*)
17 /*
18 * Discard any r/w data - this produces a link error if we have any,
19 * which is required for PIC decompression. Local data generates
20 * GOTOFF relocations, which prevents it being relocated independently
21 * of the text/got segments.
22 */
23 *(.data)
24 }
25
26 . = 0;
27 _text = .;
28
29 .text : {
30 _start = .;
31 *(.start)
32 *(.text)
33 *(.text.*)
34 *(.fixup)
35 *(.gnu.warning)
36 *(.rodata)
37 *(.rodata.*)
38 *(.glue_7)
39 *(.glue_7t)
40 *(.piggydata)
41 . = ALIGN(4);
42 }
43
44 _etext = .;
45
46 /* Assume size of decompressed image is 4x the compressed image */
47 _image_size = (_etext - _text) * 4;
48
49 _got_start = .;
50 .got : { *(.got) }
51 _got_end = .;
52 .got.plt : { *(.got.plt) }
53 _edata = .;
54
55 . = ALIGN(4);
56 __bss_start = .;
57 .bss : { *(.bss) }
58 _end = .;
59
60 .stack (NOLOAD) : { *(.stack) }
61
62 .stab 0 : { *(.stab) }
63 .stabstr 0 : { *(.stabstr) }
64 .stab.excl 0 : { *(.stab.excl) }
65 .stab.exclstr 0 : { *(.stab.exclstr) }
66 .stab.index 0 : { *(.stab.index) }
67 .stab.indexstr 0 : { *(.stab.indexstr) }
68 .comment 0 : { *(.comment) }
69 }
展开如下表:
zImage在内存中的初始地址为0x30008000,那么这个地址是怎么来的?
查看2410的 datasheet ,发现内存映射的基址是0x3000 0000 ,那么0x30008000又是如何来的呢?
在内核文档 Document/arm/Booting 文件中有:
5. Calling the kernel image
---------------------------
Existing boot loaders: MANDATORY
New boot loaders: MANDATORY
There are two options for calling the kernel zImage. If the zImage
is stored in flash, and is linked correctly to be run from flash,
then it is legal for the boot loader to call the zImage in flash
directly.
The zImage may also be placed in system RAM (at any location) and
called there. Note that the kernel uses 16K of RAM below the image
to store page tables. The recommended placement is 32KiB into RAM.
看来在 image 下面用了32K(0x8000)的空间存放内核页表,
0x30008000就是2410的内核在 RAM 中的启动地址,这个地址就是这么来的。所以后面zreladdr(内核运行地址)及bootloader中都定义相关的宏来定位到这个地址
1、初始状态
![](http://img.ph.126.net/3mWyVuDtlYArkMI1BX8cbA==/3223451433291935338.jpg)
链接文件arch/arm/boot/compressed/vmlinux.lds中的连接地址都是位置无关的,即都是以0地址为偏移的。而此时内核已被bootloader搬移到了SDRAM中。链接地址应该加上这个偏移。
*/
171 .text
172 adr r0, LC0 //指令adr是基于PC的值来获取标号LC0的地址的,LC0在后面第315行定义,由于内核已被搬移,标号LC0的地址不是以地址0为偏移的,这中间就存在一个固定的地址偏移,在s3c2410中是0x30008000.
173 ldmia r0, {r1, r2, r3, r5, r6, r11, ip}
174 ldr sp, [r0, #28]
175 #ifdef CONFIG_AUTO_ZRELADDR
176 @ determine final kernel image address
177 and r4, pc, #0xf8000000
178 add r4, r4, #TEXT_OFFSET
179 #else
180 ldr r4, =zreladdr
/*zreladdr内核运行地址,相关定义如下:
arch/arm/boot/Makefile
24 ZRELADDR := $(zreladdr-y)
25 PARAMS_PHYS := $(params_phys-y)
arch/arm/mach-s3c2410/Makefile.boot
1 ifeq ($(CONFIG_PM_H1940),y)
2 zreladdr-y := 0x30108000
3 params_phys-y := 0x30100100
4 else
5 zreladdr-y := 0x30008000
6 params_phys-y := 0x30000100
7 endif
*/
181 #endif
182 subs r0, r0, r1 @ calculate the delta offse t
//这里获得当前运行地址与链接地址的偏移量,存入r0中为0x30008000.如果不需要重定位,即内核没有进行过搬移,就跳转。如果内核代码是存于NANDflash中的是需要搬移的。
183
184 @ if delta is zero, we are
185 beq not_relocated @ running at the address we
186 @ were linked at.
187
188 /*
189 * We're running at a different address. We need to fix
190 * up various pointers:
191 * r5 - zImage base address (_start)
192 * r6 - size of decompressed image
193 * r11 - GOT start
194 * ip - GOT end
195 */
196 add r5, r5, r0 //修改内核映像基地址此时r5=0x30008000
197 add r11, r11, r0 //修改got表的起始和结束位置
198 add ip, ip, r0
199
200 #ifndef CONFIG_ZBOOT_ROM
201 /*
202 * If we're running fully PIC === CONFIG_ZBOOT_ROM = n,
203 * we need to fix up pointers into the BSS region.
204 * r2 - BSS start
205 * r3 - BSS end
206 * sp - stack pointer
207 */
//S3C2410平台是需要一下调整的,将bss段以及堆栈的地址都进行调整
208 add r2, r2, r0
209 add r3, r3, r0
210 add sp, sp, r0
211
212 /*
213 * Relocate all entries in the GOT table.
214 *///修改GOT(全局偏移表)表。根据当前的运行地址,修正该表
215 1: ldr r1, [r11, #0] @ relocate entries in the G OT
216 add r1, r1, r0 @ table. This fixes up the
217 str r1, [r11], #4 @ C references.
218 cmp r11, ip
219 blo 1b
220 #else
221
222 /*
223 * Relocate entries in the GOT table. We only relocate
224 * the entries that are outside the (relocated) BSS region.
225 *///S3C2410平台不会进入该分支,只对got表中在bss段以外的符号进行重定位
226 1: ldr r1, [r11, #0] @ relocate entries in the G OT
227 cmp r1, r2 @ entry < bss_start ||
228 cmphs r3, r1 @ _end < entry
229 addlo r1, r1, r0 @ table. This fixes up the
230 str r1, [r11], #4 @ C references.
231 cmp r11, ip
232 blo 1b
233 #endif
234
//下面的代码,如果运行当前运行地址和链接地址相等,则不需进行重定位。直接清除bss段
235 not_relocated: mov r0, #0
236 1: str r0, [r2], #4 //清BSS段,所有的arm程序都需要做这些的
237 str r0, [r2], #4
238 str r0, [r2], #4
239 str r0, [r2], #4
240 cmp r2, r3
241 blo 1b
242
243 /*
244 * The C runtime environment should now be setup
245 * sufficiently. Turn the cache on, set up some
246 * pointers, and start decompressing.
247 */
248 bl cache_on //打开cache
249
250 mov r1, sp @ malloc space above stack
251 add r2, sp, #0x10000 @ 64k max 分配一段解压函数需要的内存缓冲,参见下图。
252
253 /*
254 * Check to see if we will overwrite ourselves.
255 * r4 = final kernel address
256 * r5 = start of this image
257 * r6 = size of decompressed image
258 * r2 = end of malloc space (and therefore this image)
259 * We basically want:
260 * r4 >= r2 -> OK
261 * r4 + image length <= r5 -> OK
262 */
263 cmp r4, r2 //r4为内核执行地址,此时为0X30008000,r2此时为用户栈定,即解压函数所需内存缓冲的开始处,显然r4 < r2所以不会跳转。
264 bhs wont_overwrite
265 add r0, r4, r6
266 cmp r0, r5
//r5是内核映像的开始地址0X30008000,r6为内核映像大小,r4为解压后内核开始地址,此时为0X30008000,r5为解压前映存放的开始位置,此时也为0X30008000。下面的判断是,看解压后的内核是不是会覆盖未解压的映像。显然是覆盖的,所以是不会跳转的。注意:内核映像解压后不会超过解压前的4倍大小。
267 bls wont_overwrite
268
269 mov r5, r2 @ decompress after malloc space//此时r2为解压函数缓冲区的尾部地址。
270 mov r0, r5 //r0为映像解压后存放的起始地址,解压后的内核就紧接着存放在解压函数缓冲区的尾部。
271 mov r3, r7 //r7中存放的是architecture ID,对于SMDK2410这个值为193;
/*
解压函数是用C语言实现的在文件arch/arm/boot/compressed/misc.c中。
解压函数的是个参数是有r0~r3传入的。r0~r3的值在上面已初始化,再对照上面表格就很清楚了。
decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,
unsigned long free_mem_ptr_end_p,
int arch_id)
output_start:指解压后内核输出的起始位置,此时它的值参考上面的图表,紧接在解压缓冲区后;
free_mem_ptr_p:解压函数需要的内存缓冲开始地址;
free_mem_ptr_end_p:解压函数需要的内存缓冲结束地址,共64K;
arch_id :architecture ID,对于SMDK2410这个值为193;
*/
272 bl decompress_kernel
/*下图是head.S调用misc.c中的decompress_kernel刚解压完内核后,还没有进行重定位时的情况
![](http://img.ph.126.net/E9YuZXD9E8dMQVwD0beApg==/3368692521273957424.jpg)
解压完毕后,内核长度返回值存放在r0寄存器里。在内核末尾空出128字节的栈空间,并且使其长度128字节对齐。*/
273
274 add r0, r0, #127 + 128 @ alignment + stack
275 bic r0, r0, #127 @ align the kernel length
276 /*
277 * r0 = 解压后内核的长度
278 * r1-r3 = 没使用
279 * r4 = 内核执行地址
280 * r5 = decompressed kernel start解压后内核的起始地址,如上面初始化 mov r5, r2
281 * r7 = architecture ID 处理器ID
282 * r8 = atags pointer 标记列表地址
283 * r9-r12,r14 = corrupted
284 */
/*
上面只是将内核临时解压到了一个位置,下面还要将它重定位到0X30008000处。
标号reloc_start下面有一段重定位内核的程序。为了在内核重定位的过程中不至于将这段用于
重定位的代码给覆盖了,就先将这段用于内核重定位的代码搬到另一个地方,如下表。
![](http://img.ph.126.net/DP_KispkBUdUtaGA7RPy7w==/3227110607989164171.jpg)
*/
285 add r1, r5, r0 //r1就是解压后内核代码的结束位置,下面就是将这段重定位代码搬移到r1地址处。
286 adr r2, reloc_start //重定位代码起始地址
287 ldr r3, LC1 //用于内核重定位的代码的长度
288 add r3, r2, r3 //重定位代码的结束地址
289 1: ldmia r2!, {r9 - r12, r14} @ copy relocation code//将这段重定位代码搬移到r1地址处,如上表。
290 stmia r1!, {r9 - r12, r14}
291 ldmia r2!, {r9 - r12, r14}
292 stmia r1!, {r9 - r12, r14}
293 cmp r2, r3
294 blo 1b
295 mov sp, r1
296 add sp, sp, #128 @ relocate the stack//改变堆栈指针位置。
297
298 bl cache_clean_flush //搬移完成后刷新cache,因为代码地址变化了不能让cache再命中被内核覆盖的老地址。
299 ARM( add pc, r5, r0 ) @ call relocation code//r0 + r5 就是被搬移后的内核重定位代码的开始位置,reloc_start。下面将会讲到。
300 THUMB( add r12, r5, r0 )
301 THUMB( mov pc, r12 ) @ call relocation code
302
303 /*
304 * We're not in danger of overwriting ourselves. Do this the simple way.
305 *
306 * r4 = kernel execution address
307 * r7 = architecture ID
308 */
//如果内核映像没有被bootloader搬移过,上面程序就会跳到此处。
309 wont_overwrite: mov r0, r4
310 mov r3, r7
311 bl decompress_kernel
312 b call_kernel
313
314 .align 2
315 .type LC0, #object
//这个表与文件arch/arm/kernel/vmlinux.lds.S(这个lds文件是由arch/arm/boot/compressed/vmlinux.lds.in生成的)
316 LC0: .word LC0 @ r1
317 .word __bss_start @ r2
318 .word _end @ r3
319 .word _start @ r5
320 .word _image_size @ r6
321 .word _got_start @ r11
322 .word _got_end @ ip
323 .word user_stack_end @ sp
324 LC1: .word reloc_end - reloc_start
325 .size LC0, . - LC0
下面代码是将解压后的内核代码重定位。过程见下图
![](http://img.ph.126.net/y5_cp1gQ3g0UMxzyzslFIQ==/2307250384097654490.jpg)
//下面代码就是实现将解压后的内核代码搬到0X30008000处
546 /*
547 * All code following this line is relocatable. It is relocated by
548 * the above code to the end of the decompressed kernel image and
549 * executed there. During this time, we have no stacks.
550 *
551 * r0 = decompressed kernel length
552 * r1-r3 = unused
553 * r4 = kernel execution address
554 * r5 = decompressed kernel start
555 * r7 = architecture ID
556 * r8 = atags pointer
557 * r9-r12,r14 = corrupted
558 */
559 .align 5
560 reloc_start: add r9, r5, r0 // r0 + r5就是解压后内核代码的结束位置加128字节栈空间。
561 sub r9, r9, #128 @ do not copy the stack
562 debug_reloc_start
563 mov r1, r4 //r4为内核执行地址,即为0X30008000。
564 1:
565 .rept 4 //将解压后的内核搬到r1处,即0X30008000处。
566 ldmia r5!, {r0, r2, r3, r10 - r12, r14} @ relocate kernel
567 stmia r1!, {r0, r2, r3, r10 - r12, r14}
568 .endr
569
570 cmp r5, r9
571 blo 1b
572 mov sp, r1
573 add sp, sp, #128 @ relocate the stack
574 debug_reloc_end
575
//清除并关闭cache,清零r0,将machine ID存入r1,atags指针存入r2,再跳入0x30008000执行真正的内核Image
576 call_kernel: bl cache_clean_flush
577 bl cache_off
578 mov r0, #0 @ must be zero
579 mov r1, r7 @ restore architecture numb er
580 mov r2, r8 @ restore atags pointer
581 mov pc, r4 @ call kernel
582
此时内核解压已经完成。内核启动要执行的第二个文件就是arch/arm/kernel/head.S文件。
总结一下head.S会做些什么样的工作:
1、对于各种Arm CPU的DEBUG输出设定,通过定义宏来统一操作;
2、设置kernel开始和结束地址,保存architecture ID;
3、如果在ARM2以上的CPU中,用的是普通用户模式,则升到超级用户模式,然后关中断
4、分析LC0结构delta offset,判断是否需要重载内核地址(r0存入偏移量,判断r0是否为零)。
5、需要重载内核地址,将r0的偏移量加到BSS region和GOT table中的每一项。对于位置无关的代码,程序是通过GOT表访问全局数据目标的,也就是说GOT表中中记录的是全局数据目标的绝对地址,所以其中的每一项也需要重载。
6、清空bss堆栈空间r2-r3l,建立C程序运行需要的缓存
7、这时r2是缓存的结束地址,r4是kernel的最后执行地址,r5是kernel境象文件的开始地址
8、用文件misc.c的函数decompress_kernel(),解压内核于缓存结束的地方(r2地址之后)。
为了更清楚的了解解压的动态过程。我们用图表的方式描述下代码的搬运解压过程。然后再针对中间的一些关键过程阐述。
zImage在内存中的初始地址为0x30008000(这个地址由bootloader决定,位置不固定)u-boot会将zImage镜像copy到sdram的0x30008000位置处。此时为初始状态,这里称为状态1。
1、初始状态
|.text | 0x30008000开始,包含piggydata段(即压缩的内核段)
|. got |
|. data |
|.bss |
|.stack | 4K大小
2、 head.S调用misc.c中的decompress_kernel刚解压完内核后,内存中的各段位置如下,状态2
.text | 0x30008000开始,包含piggydata段(即压缩的内核段)
. got |
. data |
.bss |
.stack |4K大小
解压函数所需缓冲区 | 64K大小
解压后的内核代码 | 小于4M
3、当如果head.S中有代码搬运工作时,即出现overwrite时,内存中的各段位置如下,此时会将head.S中的部分代码重定位,状态3
.text |0x30008000开始,包含piggydata段(即压缩的内核段)
. got |
. data |
.bss |
.stack | 4K大小
解压函数所需缓冲区 | 64K大小
解压后的内核代码 | 小于4M
head.S中的部分重定位代码代码 | reloc_start至reloc_end
4、跳转到重定位后的reloc_start处,由reloc_start至reloc_end的代码复制解压后的内核代码到0x30008000处,并调用call_kernel跳转到0x30008000处执行。
解压后的内核 | 0x30008000开始
在通过head.S了解了动态过程后,我们可能会有几个问题:
问题1:zImage是如何知道自己最后的运行地址是0x30008000的?
问题2:调用decompress_kernel函数时,其4个参数是什么值及物理含义?
问题3:解压函数是如何确定代码中压缩内核位置的?
先回答第1个问题
zImage在内存中的初始地址为0x30008000,那么这个地址是怎么来的?
查看2410的 datasheet ,发现内存映射的基址是0x3000 0000 ,那么0x30008000又是如何来的呢?
在内核文档 Document/arm/Booting 文件中有:
5. Calling the kernel image
---------------------------
Existing boot loaders: MANDATORY
New boot loaders: MANDATORY
There are two options for calling the kernel zImage. If the zImage
is stored in flash, and is linked correctly to be run from flash,
then it is legal for the boot loader to call the zImage in flash
directly.
The zImage may also be placed in system RAM (at any location) and
called there. Note that the kernel uses 16K of RAM below the image
to store page tables. The recommended placement is 32KiB into RAM.
看来在 image 下面用了32K(0x8000)的空间存放内核页表,
再来先看看TEXT_OFFSET( 内核 在RAM中的 起始位置相对于 RAM起始地址偏移。值为0x00008000)它在
./arch/arm/Makefile中定义
118 textofs-y := 0x00008000
222 TEXT_OFFSET := $(textofs-y)
此处,定义了Image的偏移地址。
0x30008000就是2410的内核在 RAM 中的启动地址,这个地址就是这么来的。所以后面zreladdr(内核运行地址)及bootloader中都定义相关的宏来定位到这个地址
在arch/arm/boot/Makefile文件中 ZRELADDR := $(zreladdr-y)
arch/arm/boot/Makefile
24 ZRELADDR := $(zreladdr-y)
25 PARAMS_PHYS := $(params_phys-y)
在arch/arm/mach-s3c2410/Makefile.boot中zreladdr-y := 0x30008000这个就是zImage的运行地址了,
arch/arm/mach-s3c2410/Makefile.boot
1 ifeq ($(CONFIG_PM_H1940),y)
2 zreladdr-y := 0x30108000
3 params_phys-y := 0x30100100
4 else
5 zreladdr-y := 0x30008000
6 params_phys-y := 0x30000100
7 endif
在arch/arm/boot/compressed/head.S中有
180 ldr r4, =zreladdr 内核就是用这种方式让代码知道最终运行的位置的
此处定义了内核的物理地址和 u-boot 传递参数的地址,也就是加载地址。此处要和 u-boot 一致。
接下来再回答第2个问题
decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,
unsigned long free_mem_ptr_end_p,
int arch_id)
output_start:指解压后内核输出的起始位置,此时它的值参考上面的图表,紧接在解压缓冲区后;
free_mem_ptr_p:解压函数需要的内存缓冲开始地址;
ulg free_mem_ptr_end_p:解压函数需要的内存缓冲结束地址,共64K;
arch_id :architecture ID,对于SMDK2410这个值为193;
最后回答第3个问题
首先看看piggy.o是如何生成的,
打开arch/arm/boot/Makefile文件:
49 $(obj)/Image: vmlinux FORCE
50 $(call if_changed,objcopy)
51 @echo ' Kernel: $@ is ready'
56 $(obj)/zImage: $(obj)/compressed/vmlinux FORCE
57 $(call if_changed,objcopy)
58 @echo ' Kernel: $@ is ready'
可以看出,zImage是vmlinux通过objcopy后生成的;打开arch/arm/boot/compressed/Makefie文件:
63 suffix_$(CONFIG_KERNEL_GZIP) = gzip
64 suffix_$(CONFIG_KERNEL_LZO) = lzo
65 suffix_$(CONFIG_KERNEL_LZMA) = lzma
67 targets := vmlinux vmlinux.lds \
68 piggy.$(suffix_y) piggy.$(suffix_y).o \
69 font.o font.c head.o misc.o $(OBJS)
./arch/arm/boot/Makefile:30:targets := Image zImage xipImage bootpImage uImage
104 $(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/$(HEAD) $(obj)/piggy.$(suffix_y).o \
105 $(addprefix $(obj)/, $(OBJS)) $(lib1funcs) FORCE
106 $(call if_changed,ld)
107 @:
109 $(obj)/piggy.$(suffix_y): $(obj)/../Image FORCE
110 $(call if_changed,$(suffix_y))
111
112 $(obj)/piggy.$(suffix_y).o: $(obj)/piggy.$(suffix_y) FORCE
可以看出,当配置内核为GZIP方式时,zImage的压缩方式使用gzip。现在可以清楚的明白zImage和vmlinux的关系了,一句话总结一下:zImage就是vmlinux通过objcopy、gzip压缩后,得到的内核,其头部是由head.S misc.c组成的自解压代码。
而u-boot所一直追捧的uImage格式,只是在zImage的前面加上了40Byte的内核头部信息,由于本文主要讲解zImage,此处暂不对uImage进行过多的分析。这里内核默认采用gzip方式,所以第112行也就是
piggy.gzip
$(obj)/piggy.gzip.o: $(obj)/piggy.gzip FORCE
piggy.gzip.o是由piggy.S生成的,咱们看看piggy.gzip.S的内容:
.section .piggydata,#alloc
.globl input_data
input_data:
.incbin "arch/arm/boot/compressed/piggy.gzip"
.globl input_data_end
input_data_end:
再看看misc.c中decompress_kernel函数吧,它将调用
do_decompress(input_data, input_data_end - input_data,
output_data, error);
解压内核。发现什么没?这里的input_data不正是piggy.S里的input_data吗?这个时候应该明白内核是怎样确定piggy.gz在zImage中的位置了吧。
最后说明一点的是do_decompress()在arch/arm/boot/compressed/decompresse.c中定义,它将调用decompress,进而调用bunzip2()或unlzma()来获取压缩内核代码。具体解压过程以后有时间再分析。
unsigned long
decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,
unsigned long free_mem_ptr_end_p,
int arch_id)
{
unsigned char *tmp;
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();
tmp = (unsigned char *) (((unsigned long)input_data_end) - 4);
output_ptr = get_unaligned_le32(tmp);
putstr("Uncompressing Linux...");
do_decompress(input_data, input_data_end - input_data,
output_data, error);
putstr(" done, booting the kernel.\n");
return output_ptr;
}
decompress_kernel()先调用arch_decomp_setup()进行设置,初始化,实现的功能是检测CPU型号、使能看门狗(如果在配置内核时配置的前提下)和串口。然后使用在打印出信息“Uncompressing Linux...”后,之后调用do_decompress函数进行解压,do_decompress函数是在arch/arm/boot/compressed/Decompress.c文件中的。将内核放于指定的位置。最后打印出信息" done, booting the kernel." do_decompress函数其代码如下。
#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
void do_decompress(u8 *input, int len, u8 *output, void (*error)(char *x))
{
decompress(input, len, NULL, NULL, output, NULL, error);
}
当配置内核时选中GZIP方式,就会#include “../../../../lib/decompress_inflate.c”。文件最后一行为:
#define decompress gunzip
即:最后调用 gunzip函数对zImage进行解压。
当完成所有解压任务后,又将跳转回head.S文件中,执行call_kernel,将启动真正的Image.
arch/arm/plat-samsung/include/plat/uncompress.h
typedef unsigned int upf_t; /* cannot include linux/serial_core.h */
/* uart setup */
static unsigned int fifo_mask;
static unsigned int fifo_max;
/* forward declerations */
static void arch_detect_cpu(void);
/* defines for UART registers */
#include <plat/regs-serial.h>
#include <plat/regs-watchdog.h>
/* working in physical space... */
#undef S3C2410_WDOGREG
#define S3C2410_WDOGREG(x) ((S3C24XX_PA_WATCHDOG + (x)))
/* how many bytes we allow into the FIFO at a time in FIFO mode */
#define FIFO_MAX (14)
#define uart_base S3C_PA_UART + (S3C_UART_OFFSET * CONFIG_S3C_LOWLEVEL_UART_PORT)
static __inline__ void
uart_wr(unsigned int reg, unsigned int val)
{
volatile unsigned int *ptr;
ptr = (volatile unsigned int *)(reg + uart_base);
*ptr = val;
}
static __inline__ unsigned int
uart_rd(unsigned int reg)
{
volatile unsigned int *ptr;
ptr = (volatile unsigned int *)(reg + uart_base);
return *ptr;
}
/* we can deal with the case the UARTs are being run
* in FIFO mode, so that we don't hold up our execution
* waiting for tx to happen...
*/
static void putc(int ch)
{
if (uart_rd(S3C2410_UFCON) & S3C2410_UFCON_FIFOMODE) {
int level;
while (1) {
level = uart_rd(S3C2410_UFSTAT);
level &= fifo_mask;
if (level < fifo_max)
break;
}
} else {
/* not using fifos */
while ((uart_rd(S3C2410_UTRSTAT) & S3C2410_UTRSTAT_TXE) != S3C2410_UTRSTAT_TXE)
barrier();
}
/* write byte to transmission register */
uart_wr(S3C2410_UTXH, ch);
}
static inline void flush(void)
{
}
#define __raw_writel(d, ad) \
do { \
*((volatile unsigned int __force *)(ad)) = (d); \
} while (0)
/* CONFIG_S3C_BOOT_WATCHDOG
*
* Simple boot-time watchdog setup, to reboot the system if there is
* any problem with the boot process
*/
#ifdef CONFIG_S3C_BOOT_WATCHDOG
#define WDOG_COUNT (0xff00)
static inline void arch_decomp_wdog(void)
{
__raw_writel(WDOG_COUNT, S3C2410_WTCNT);
}
static void arch_decomp_wdog_start(void)
{
__raw_writel(WDOG_COUNT, S3C2410_WTDAT);
__raw_writel(WDOG_COUNT, S3C2410_WTCNT);
__raw_writel(S3C2410_WTCON_ENABLE | S3C2410_WTCON_DIV128 | S3C2410_WTCON_RSTEN | S3C2410_WTCON_PRESCALE(0x80), S3C2410_WTCON);
}
#else
#define arch_decomp_wdog_start()
#define arch_decomp_wdog()
#endif
#ifdef CONFIG_S3C_BOOT_ERROR_RESET
static void arch_decomp_error(const char *x)
{
putstr("\n\n");
putstr(x);
putstr("\n\n -- System resetting\n");
__raw_writel(0x4000, S3C2410_WTDAT);
__raw_writel(0x4000, S3C2410_WTCNT);
__raw_writel(S3C2410_WTCON_ENABLE | S3C2410_WTCON_DIV128 | S3C2410_WTCON_RSTEN | S3C2410_WTCON_PRESCALE(0x40), S3C2410_WTCON);
while(1);
}
#define arch_error arch_decomp_error
#endif
#ifdef CONFIG_S3C_BOOT_UART_FORCE_FIFO
static inline void arch_enable_uart_fifo(void)
{
u32 fifocon = uart_rd(S3C2410_UFCON);
if (!(fifocon & S3C2410_UFCON_FIFOMODE)) {
fifocon |= S3C2410_UFCON_RESETBOTH;
uart_wr(S3C2410_UFCON, fifocon);
/* wait for fifo reset to complete */
while (1) {
fifocon = uart_rd(S3C2410_UFCON);
if (!(fifocon & S3C2410_UFCON_RESETBOTH))
break;
}
}
}
#else
#define arch_enable_uart_fifo() do { } while(0)
#endif
static void
arch_decomp_setup(void)
{
/* we may need to setup the uart(s) here if we are not running
* on an BAST... the BAST will have left the uarts configured
* after calling linux.
*/
arch_detect_cpu();
arch_decomp_wdog_start();
/* Enable the UART FIFOs if they where not enabled and our
* configuration says we should turn them on.
*/
arch_enable_uart_fifo();
}
arch/arm/mach-s3c2410/include/mach/uncompress.h
static inline int is_arm926(void)
{
unsigned int cpuid;
asm volatile ("mrc p15, 0, %0, c1, c0, 0" : "=r" (cpuid));
return ((cpuid & 0xff0) == 0x260);
}
static void arch_detect_cpu(void)
{
unsigned int cpuid;
cpuid = *((volatile unsigned int *)S3C2410_GSTATUS1);
cpuid &= S3C2410_GSTATUS1_IDMASK;
if (is_arm926() || cpuid == S3C2410_GSTATUS1_2440 ||
cpuid == S3C2410_GSTATUS1_2442 ||
cpuid == S3C2410_GSTATUS1_2416 ||
cpuid == S3C2410_GSTATUS1_2450) {
fifo_mask = S3C2440_UFSTAT_TXMASK;
fifo_max = 63 << S3C2440_UFSTAT_TXSHIFT;
} else {
fifo_mask = S3C2410_UFSTAT_TXMASK;
fifo_max = 15 << S3C2410_UFSTAT_TXSHIFT;
}
}