基于S3C2410-ARM Linux启动过程分析

时间:2022-09-30 04:46:09
本文着重分析FS2410平台linux-2.6内核启动的详细过程,主要包括:zImage解压缩阶段、vmlinux启动汇编阶段、startkernel到创建第一个进程阶段三个部分,一般将其称为linux内核启动一、二、三阶段,本文也将采用这种表达方式。对于zImage之前的启动过程,本文不做表述,可参考作者 “u-boot-1.3.1启动过程分析”一文。
-------------------------------------------------------------------------------------------------------
硬件平台:优龙FS2410开发板
CPU: S3C2410(Arm920T); NOR Flash: SST39VF1601(2MB);
SDRAM: K4S561632D-TC/L75 (32M)x2);Nand Flash: K9K2G08U0M-YCB0(64MB);
Net card: CS8900A)。
软件平台:u-boot-1.3.1、linux-2.6
-------------------------------------------------------------------------------------------------------
本文中涉及到的术语约定如下:
基本内核映像:即内核编译过程中最终在内核源代码根目录下生成的vmlinux映像文件,并不包含任何内核解压缩和重定位代码;
zImage 内核映像:包含了内核及压缩和重定位代码,以及基本内核映像vmlinux的压缩挡piggy.gz的一个映像文件,通常是目标板bootloader加载的对象;
zImage 下载地址:即bootloader将zImage 下载到目标板内存的某个地址或者nand read 将zImage 读到内存的某个地址;
zImage 加载地址:由Linux的bootloader完成的将zImage 搬移到目标板内存的某个位置所对应的地址值,默认值0x30008000。
1. 基本内核映像的生成
基本内核映像是真正的linux内核映像,从下面的链接过程,我们可以看出其生成过程,这对于理解内核的启动过程是很有帮助的。该链接过程最值得关注的就是这一阶段的链接脚本arch/arm/kernel/vmlinux.lds。
arm-linux-gnu-ld -EL -p --no-undefined -X -o vmlinux -T arch/arm/kernel/vmlinux.lds arch/arm/kernel/head.o arch/arm/kernel/init_task.o init/built-in.o --start-group usr/built-in.o arch/arm/kernel/built-in.o arch/arm/mm/built-in.o arch/arm/common/built-in.o arch/arm/mach-s3c2410/built-in.o arch/arm/nwfpe/built-in.o kernel/built-in.o mm/built-in.o fs/built-in.o ipc/built-in.o security/built-in.o crypto/built-in.o lib/lib.a arch/arm/lib/lib.a lib/built-in.o arch/arm/lib/built-in.o drivers/built-in.o sound/built-in.o net/built-in.o --end-group .tmp_kallsyms2.o
2. zImage 压缩内核映像的生成
这是一个最终或者固化到Flash上,或者直接加载到内存中运行的内核映像,通常包括了压缩版的基本内核映像,解压缩代码和重定位代码,其具体的生成过程如下:
图一 zImage内核生成过程( linuxforum中无法贴出来 完整文章请参看: http://www.docin.com/sz_farsight

这里值得关注的也有一个链接脚本文件。-T arch/arm/boot/compressed/vmlinux.lds。

Arm linux zImage 生成过程文字详解:
(1). 将上面的基本linux内核映像vmlinux去除调试信息、注释、符号表等内容,生成arch/arm/boot/Image,这是不带多余信息的linux内核,Image的大小约3.2MB;
命令:arm-linux-gnu-objcopy -O binary -R .note -R .comment -S vmlinux arch/arm/boot/Image
(2). 将 arch/arm/boot/Image 用gzip -9 压缩生成arch/arm/boot/compressed/piggy.gz大小约1.5MB;
命令:gzip -f -9 < arch/arm/boot/compressed/../Image > arch/arm/boot/compressed/piggy.gz
(3). 编译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;
命令:arm-linux-gnu-gcc -Wp,-MD,arch/arm/boot/compressed/.piggy.o.d -nostdinc -isystem /home/justin/crosstool/gcc-3.4.5-glibc-2.3.6/arm-linux-gnu/lib/gcc/arm-linux-gnu/3.4.5/include -D__KERNEL__ -Iinclude -mlittle-endian -D__ASSEMBLY__ -Wa,-L -gdwarf-2 -mapcs-32 -mno-thumb-interwork -D__LINUX_ARM_ARCH__=4 -march=armv4 -mtune=arm9tdmi -msoft-float -c -o arch/arm/boot/compressed/piggy.o arch/arm/boot/compressed/piggy.S
(4). 依据arch/arm/boot/compressed/vmlinux.lds 将arch/arm/boot/compressed/目录下的文件head.o 、piggy.o 、misc.o链接生成 arch/arm/boot/compressed/vmlinux,这个vmlinux是经过压缩且含有自解压代码的内核,大小约1.5MB;
命令:arm-linux-gnu-ld -EL --defsym zreladdr=0x30008000 --defsym params_phys=0x30000100 -p --no-undefined -X /home/justin/crosstool/gcc-3.4.5-glibc-2.3.6/arm-linux-gnu/lib/gcc/arm-linux-gnu/3.4.5/libgcc.a -T arch/arm/boot/compressed/vmlinux.lds arch/arm/boot/compressed/head.o arch/arm/boot/compressed/piggy.o arch/arm/boot/compressed/misc.o -o arch/arm/boot/compressed/vmlinux
(5). 将arch/arm/boot/compressed/vmlinux去除调试信息、注释、符号表等内容,生成arch/arm/boot/zImage大小约1.5MB;这已经是一个可以使用的linux内核映像文件了;
3. Linux内核启动第一阶段:内核解压缩和重定位
该阶段是从u-boot引导进入内核执行的第一阶段,我们知道u-boot引导内核启动的最后一步是:通过一个函数指针thekernel()带三个参数跳转到内核(zImage)入口点开始执行,此时,u-boot的任务已经完成,控制权完全交给内核(zImage)。
稍作解释,在u-boot的文件lib_arm/armlinux.c(u-boot-1.3.1)或者lib_arm/bootm.c (u-boot-1.3.4)中定义了thekernel,并在do_bootm_linux的最后执行thekernel,如下:
void (*theKernel)(int zero, int arch, uint params);
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);
//hdr->ih_ep----Entry Point Address uImage 中指定的内核入口点,这里是0x30008000。
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);其中第二个参数为机器ID,第三参数为u-boot传递给内核参数存放在内存中的首地址,此处是0x30000100。
由上述zImage的生成过程我们可以知道,第一阶段运行的内核映像实际就是arch/arm/boot/compressed/vmlinux,而这一阶段所涉及的文件也只有三个:
arch/arm/boot/compressed/vmlinux.lds
arch/arm/boot/compressed/head.S
arch/arm/boot/compressed/misc.c
图二 内核启动第一步的物理内存分布( linuxforum中无法贴出来 完整文章请参看: http://www.docin.com/sz_farsight
我们的分析集中在arch/arm/boot/compressed/head.S,适当参考vmlinux.lds。
从arch/arm/boot/compressed/vmlinux 的反汇编代码可一看出,内核执行的第一个代码段位start,
*****start
vmlinux: file format elf32-littlearm
Disassembly of section .text:
00000000 <start>:
0: e1a00000 nop (mov r0,r0)
…. …. …..
1c: e1a00000 nop (mov r0,r0)
20: ea000002 b 30 <.text+0x30>
…. …. …..
*****保存参数
30: e1a07001 mov r7, r1
u-boot向内传递参数分析
//由thekernel 传递的三个参数分别保存在r0,r1,r2中。
//将机器ID保存在r7中
34: e3a08000 mov r8, #0 ; 0x0
//保存r0,这里似乎没有太大的意义,
这里没有保存r2,也就是u-boot传递给内核参数的首地址0x30000100,看来linux-2.6.14启动时是不需要传递该参数的而是通过struct machine_desc(include/asm-arm/mach/arch.h)来确定,但是这个文件只是该结构的定义,真正的参数赋值在哪呢?实际上,这就是在内核移植是需要做的工作了,内核移植最主要的一个文件就是arch/arm/mach-s3c2410/mach-fs2410.c,通过下面的宏来实现对machine_desc结构体的赋值,并且在该文件中对所涉及到的函数进行了具体的代码实现,这是内核移植方面的内容,与我们的主题无关,这里不再多说。
MACHINE_START(SMDK2410, "SMDK2410")
/* @TODO: request a new identifier and switch to SMDK2410 */
.phys_ram = S3C2410_SDRAM_PA,
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
//指定u-boot传递给内核的参数存放的位置
.map_io = smdk2410_map_io,
.init_irq = smdk2410_init_irq,
.init_machine = sdmk2410_init,
.timer = &s3c24xx_timer,
MACHINE_END
下面我们采用汇编代码来进行分析:arch/arm/boot/compressed/head.S
.align
start: //u-boot first jump to this execute
.type start,#function
.rept 8
mov r0, r0
.endr

b 1f
.word 0x016f2818 @ Magic numbers to help the loader
.word start @ absolute load/run zImage address
.word _edata @ zImage end address
1: mov r7, r1 @ save architecture ID
mov r8, #0 @ save r0
*****判定是否是超级用户模式
mrs r2, cpsr @ get current mode
tst r2, #3 @ not user? //判断当前是否为超级用户权限模式
bne not_angel //如果是超级用户权限模式,jump to not_angel
mov r0, #0x17 @ angel_SWIreason_EnterSVC 如果是普通用户模式,则通过软中断进入超级用户权限模式
swi 0x123456 @ angel_SWI_ARM
*****关中断
not_angel:
mrs r2, cpsr @ turn off interrupts to //关中断
orr r2, r2, #0xc0 @ prevent angel from running
msr cpsr_c, r2
*****将编译时指定的一些变量加载到相应的寄存器中
/* some architecture specific code can be inserted by the linker here, but it should preserve r7 and r8. zImage 的连接首地址为0x0 zImage的运行时首地址一般为0x30008000,当然可以不同 */
.text
adr r0, LC0 //读取LC0的当前运行时地址,应当为zImage的运行时起始地址+(LC0到zImage链接地址的首地址(0x0)的偏移)
ldmia r0, {r1, r2, r3, r4, r5, r6, ip, sp}//将LC0中的变量值加载到r1, r2, r3, r4, r5, r6, ip, sp
subs r0, r0, r1 @ calculate the delta offset //计算当前运行地址与链接地址的偏移
beq not_relocated @ if delta is zero, we are running at the address we were linked at.
//如果运行地址等于链接地址,则跳过重定位部分代码,否则继续执行relocate
/* .type LC0, #object
LC0: .word LC0 @ r1
.word __bss_start @ r2
.word _end @ r3
.word zreladdr @ r4
.word _start @ r5
.word _got_start @ r6
.word _got_end @ ip
.word user_stack+4096 @ sp
LC1: .word reloc_end - reloc_start
.size LC0, . - LC0 */
/* We're running at a different address. We need to fix up various pointers:
* r5 - zImage base address
* r6 - GOT start
* ip - GOT end
GOT(global offset table)
GOT是一个数组,存在ELF image的数据段中,他们是一些指向objects的指针(通常
是数据objects).动态连接器将重新修改那些编译时还没有确定下来地址的符号的
GOT入口。所以说GOT在i386动态连接中扮演着重要的角色。*/
*****将上面的变量进行重定位,转换为当前的运行时地址
add r5, r5, r0 //zImage 的链接时首地址重定位为运行时首地址
add r6, r6, r0 //GOT的链接时首地址重定位为运行时首地址
add ip, ip, r0
#ifndef CONFIG_ZBOOT_ROM
/* If we're running fully PIC === CONFIG_ZBOOT_ROM = n,
* we need to fix up pointers into the BSS region.
* r2 - BSS start
* r3 - BSS end
* sp - stack pointer */
add r2, r2, r0 //__bss_start的链接时首地址重定位为运行时首地址
add r3, r3, r0 //_end的链接时地址重定位为运行时地址
add sp, sp, r0 //user_stack+4096的链接时地址重定位为运行时地址
/* Relocate all entries in the GOT table.
重定位GOT中的所有链接地址为当前运行时地址 */
1: ldr r1, [r6, #0] @ relocate entries in the GOT
add r1, r1, r0 @ table. This fixes up the
str r1, [r6], #4 @ C references.
cmp r6, ip
blo 1b
#else
/* Relocate entries in the GOT table. We only relocate
* the entries that are outside the (relocated) BSS region.
重定位GOT中的所有链接地址为当前运行时地址但是不重定位
BSS_START到BSS_END部分 */
1: ldr r1, [r6, #0] @ relocate entries in the GOT
cmp r1, r2 @ entry < bss_start ||
cmphs r3, r1 @ _end < entry
addlo r1, r1, r0 @ table. This fixes up the
str r1, [r6], #4 @ C references.
cmp r6, ip
blo 1b
#endif
*****重定位已经完成,清零BSS段
not_relocated: mov r0, #0
1: str r0, [r2], #4 @ clear bss
str r0, [r2], #4
str r0, [r2], #4
str r0, [r2], #4
cmp r2, r3
blo 1b
*****准备进入C 程序的相关设置,开启cache,设置一些指针
/* The C runtime environment should now be setup sufficiently. Turn the cache on, set up some pointers, and start decompressing. */
cache on 是一个相当复杂的过程,这里简单描述其流程,如有兴趣可参考“ Arm linux 启动第一阶段cache on 分析”
bl cache_on -------〉call_cache_fn-------〉
通过查表proc_types调用 __armv4_cache_on
---------------------------〉__setup_mmu
__setup_mmu: sub r3, r4, #16384 //4k ,r3=0x30004000
@ Page directory size //16384=16kB=0x4000
bic r3, r3, #0xff @ Align the pointer
bic r3, r3, #0x3f00
/* Initialise the page tables, turning on the cacheable and bufferable bits for the RAM area only. */
mov r0, r3
mov r8, r0, lsr #18 r8=0x30004000>>18=0xc00
mov r8, r8, lsl #18 @ start of RAM,
//r8=0xc00<<18=0x30000000
add r9, r8, #0x10000000@ a reasonable RAM size
//r9=0x40000000
mov r1, #0x12
orr r1, r1, #3 << 10 //r1=0xc00|0x12=0xc12
add r2, r3, #16384 //r2=0x30004000+0x4000=0x30008000
1: cmp r1, r8 @ if virt > start of RAM
orrhs r1, r1, #0x0c @ set cacheable, bufferable
cmp r1, r9 @ if virt > end of RAM
bichs r1, r1, #0x0c @ clear cacheable, bufferable
str r1, [r0], #4 @ 1:1 mapping,
//r0=0x300040000 ,r1=0xc12
//0x12表明这是一个段描述符即bit4和bit1为1。
//0xc00即bit11和bit10为11,即AP=11,允许所有读写访问
add r1, r1, #1048576 //1048576=0x100000
teq r0, r2
bne 1b
//上面代码段实现0x00000000 ~0x40000000(1GB)地址空间也表的创建,映射的目的地址也是0x00000000~0x40000000,所以没有实际意义,也许是为了打开cache以及设置访问属性。
mov r0, #0
mcr p15, 0, r0, c7, c10, 4 @ drain write buffer 清零
mcr p15, 0, r0, c8, c7, 0 @ flush I,D TLBs 清零
mrc p15, 0, r0, c1, c0, 0 @ read control reg 清零
orr r0, r0, #0x5000 @ I-cache enable, RR cache replacement
orr r0, r0, #0x0030
bl __common_cache_on------->
__common_cache_on:
#ifndef DEBUG
orr r0, r0, #0x000d @ Write buffer, mmu
#endif
mov r1, #-1
mcr p15, 0, r3, c2, c0, 0 @ load page table pointer
//将页表基址写入页表基址寄存器cp15 c2
mcr p15, 0, r1, c3, c0, 0
//设置域访问控制寄存器,写入0xffffffff ,与控制位为11,也就是允许权限检查,可以访问。
mcr p15, 0, r0, c1, c0, 0 @ load control register
mov pc, lr
mov r0, #0
mcr p15, 0, r0, c8, c7, 0 @ flush I,D TLBs
mov pc, r12
返回,cache on 结束。
//这里的r1,r2之间的空间为解压缩内核程序所使用,也是传递给decompress_kernel的第二和第三的参数
mov r1, sp @ malloc space above stack //将SP的运行时地址存入r0
add r2, sp, #0x10000 @ 64k max //r2=sp+0x10000
*****解压缩内核,分三种情况,下面一一解释:
说明:这一段分析中所提到的vmlinux指的是基本内核映像vmlinux
/*检查当前地址间的相互关系, 防止解压缩过程中出现地址重叠或者说地址冲突
* r4 = final kernel address //最终解压后的内核首地址
* r5 = start of this image zImage的运行时首地址,一般为0x30008000,当然也可以不同,
* r2 = end of malloc space (and therefore this image)
* We basically want:
* r4 >= r2 -> OK
* r4 + image length <= r5 -> OK
decompress_kernel(ulg output_start, ulg free_mem_ptr_p, ulg free_mem_ptr_end_p,
int arch_id */
第一种情况:
理论上讲,这种情况下vmlinux的起始地址大于zImage运行时所需的最大地址(r2),那么直接将zImage解压到vmlinux的目标地址也是没有问题的,但是实际上有点意外,看下面的分析。
cmp r4, r2
bhs wont_overwrite //r4大于r2不会发生地址冲突
分析:依据链接脚本arch/arm/boot/compressed/vmlinux.lds分析
.data : { *(.data) }
_edata = .;
. = ALIGN(4);
__bss_start = .;
.bss: { *(.bss) }
_end = .;
.stack (NOLOAD): { *(.stack) }
Sp的装载地址在zImage映像上面的bss段之上,那么从上面的代码知道r2=sp+0x10000,
那么我们得出结论:第一种情况不可能发生:因为r4=0x30008000,内存的起始地址为0x30000000,zImage 的大小最小约1MB,即0x100000,所以r4一定小于r2。
第二种情况:
这种情况下zImage的起始地址大于vmlinux的目标起始地址加上vmlinux大小(4M)的地址,所以将zImage直接解压到vmlinux的目标地址是没有问题的。
add r0, r4, #4096*1024 @ 4MB largest kernel size
cmp r0, r5
bls wont_overwrite //r4+zImage size <=r5 不会发生地址冲突


分析如下:假如r5>r4+0x400000,其根本意思就是u-boot将zImage重定位到r4+0x400000(我们这里就是0x30008000+0x400000=0x30408000的位置),这一方案是可行的。其内存分布图如下所示:
图三, 第二种解压缩情况内存分布图( linuxforum中无法贴出来 完整文章请参看: http://www.docin.com/sz_farsight

第三种情况:也是我们最常使用的方式,将作详细解释
这种情况下vmlinux的目标位置刚好和zImage的当前位置重合,所以解决方案就是先将zImage解压到zImage的上面,再将其重定位或者说搬移到目标位置。当然这其中就有个问题,vmlinux的搬移同样会覆盖掉正在运行的重定位及其后面的第一阶段启动代码,所以在搬移vmlinux到目标位置之前我们需要先将重定位代码段(事实上包含了重定位代码和后面的内核启动第一阶段的代码)搬移到vmlinux的上面,这样就避免了地址的冲突。
mov r5, r2 @ decompress after malloc space
mov r0, r5 //将zImage解压到r2(就是sp+0x10000)起始的地址
mov r3, r7 //Archtecture ID-->r3
bl decompress_kernel
add r0, r0, #127
bic r0, r0, #127 @ align the kernel length
/* r0 = decompressed kernel length
* r1-r3 = unused
* r4 = kernel execution address
* r5 = decompressed kernel start
* r6 = processor ID
* r7 = architecture ID
* r8-r14 = unused */
//将内核重定位代码段搬移到解压后的内核的上面
add r1, r5, r0 @ end of decompressed kernel
adr r2, reloc_start //reloc_start代码段的起始地址
ldr r3, LC1 //reloc_start段代码的大小
add r3, r2, r3
1: ldmia r2!, {r8 - r13} @ copy relocation code
stmia r1!, {r8 - r13}
ldmia r2!, {r8 - r13}
stmia r1!, {r8 - r13}
cmp r2, r3
blo 1b
bl cache_clean_flush //清cache
add pc, r5, r0 @ call relocation code //跳转到新的reloc_start代码位置执行解压后内核的重定位, 将解压后的内核搬移到r4=0x30008000位置
图四, 第三种解压缩情况内存分布图( linuxforum中无法贴出来 完整文章请参看: http://www.docin.com/sz_farsight
/* * We're not in danger of overwriting ourselves. Do this the simple way.
* r4 = kernel execution address
* r7 = architecture ID
解压缩过程中不需要传递压缩映像的起始地址,这些时在编译链接时
就已经由arch/arm/boot/compressed/piggy.S决定好了。 */
第一、二种情况下的解压代调用部分
wont_overwrite: mov r0, r4 //设置解压缩目的起始地址,也就是内核入口地址
//在arm linux中这一地址一般为0x30008000
mov r3, r7 //architecture ID 解压缩时需要该参数
bl decompress_kernel //调用解压缩c程序
b call_kernel
第三种情况下后面这部分代码的位置已经不是zImage最初装载的位置了,而是在搬移重定位代码段时,被搬移到vmlinux上面了,否则就会出错,因为vmlinux搬移之后将会彻底覆盖zImage最初装载的位置。
debug_reloc_end //relocate 代码段紧接着就是call_kernel
call_kernel: bl cache_clean_flush //清cache
bl cache_off //关cache
mov r0, #0
mov r1, r7 @ restore architecture number
mov pc, r4 @ call kernel 跳转到内核启动第二阶段开始执行。

至此内核解压缩过程完毕,接下来就是进入真正的内核vmlinux了(待续)