ARM架构内核运行过程
2013-12-24 22:23
344 查看
虽然这里的Arm Linux kernel前面加上了Android,但实际上还是和普遍Arm linux kernel启动的过程一样的,这里只是结合一下Android的Makefile,讲一下bootimage生成的一个过程。这篇文档主要描述bootimage的构造,以及kernel真正执行前的解压过程。
在了解这些之前我们首先需要了解几个名词,这些名词定义在/Documentation/arm/Porting里面,这里首先提到其中的几个,其余几个会在后面kernel的执行过程中讲述:1)ZTEXTADDR boot.img运行时候zImage的起始地址,即kernel解压代码的地址。这里没有虚拟地址的概念,因为没有开启MMU,所以这个地址是物理内存的地址。解压代码不一定需要载入RAM才能运行,在FLASH或者其他可寻址的媒体上都可以运行。
2)ZBSSADDR 解压代码的BSS段的地址,这里也是物理地址。
3)ZRELADDR 这个是kernel解压以后存放的内存物理地址,解压代码执行完成以后会跳到这个地址执行kernel的启动,这个地址和后面kernel运行时候的虚拟地址满足:__virt_to_phys(TEXTADDR) = ZRELADDR。
4)INITRD_PHYS Initial Ram Disk存放在内存中的物理地址,这里就是我们的ramdisk.img。
5)INITRD_VIRT Initial Ram Disk运行时候虚拟地址。
6)PARAMS_PHYS 内核启动的初始化参数在内存上的物理地址。
下面我们首先来看看boot.img的构造,了解其中的内容对我们了解kernel的启动过程是很有帮助的。首先来看看Makefile是如何产生我们的boot.img的:
out/host/linux-x86/bin/mkbootimg-msm7627_ffa --kernel out/target/product/msm7627_ffa/kernel --ramdisk out/target/product/msm7627_ffa/ramdisk.img --cmdline "mem=203M console=ttyMSM2,115200n8 androidboot.hardware=qcom" --output out/target/product/msm7627_ffa/boot.img
根据上面的命令我们可以首先看看mkbootimg-msm7627ffa这个工具的源文件:system/core/mkbootimg.c。看完之后我们就能很清晰地看到boot.img的内部构造,它是由boot header /kernel /ramdisk /second stage构成的,其中前3项是必须的,最后一项是可选的。
[c-sharp]
view plaincopyprint?
/*
** +-----------------+
** | boot header | 1 page
** +-----------------+
** | kernel | n pages
** +-----------------+
** | ramdisk | m pages
** +-----------------+
** | second stage | o pages
** +-----------------+
**
** n = (kernel_size + page_size - 1) / page_size
** m = (ramdisk_size + page_size - 1) / page_size
** o = (second_size + page_size - 1) / page_size
**
** 0. all entities are page_size aligned in flash
** 1. kernel and ramdisk are required (size != 0)
** 2. second is optional (second_size == 0 -> no second)
** 3. load each element (kernel, ramdisk, second) at
** the specified physical address (kernel_addr, etc)
** 4. prepare tags at tag_addr. kernel_args[] is
** appended to the kernel commandline in the tags.
** 5. r0 = 0, r1 = MACHINE_TYPE, r2 = tags_addr
** 6. if second_size != 0: jump to second_addr
** else: jump to kernel_addr
*/
[c-sharp] view plaincopyprint? #define PHYSICAL_DRAM_BASE 0x00200000 #define KERNEL_ADDR (PHYSICAL_DRAM_BASE + 0x00008000) #define RAMDISK_ADDR (PHYSICAL_DRAM_BASE + 0x01000000) #define TAGS_ADDR (PHYSICAL_DRAM_BASE + 0x00000100) #define NEWTAGS_ADDR (PHYSICAL_DRAM_BASE + 0x00004000) #define PHYSICAL_DRAM_BASE 0x00200000 #define KERNEL_ADDR (PHYSICAL_DRAM_BASE + 0x00008000) #de 4000 fine RAMDISK_ADDR (PHYSICAL_DRAM_BASE + 0x01000000) #define TAGS_ADDR (PHYSICAL_DRAM_BASE + 0x00000100) #define NEWTAGS_ADDR (PHYSICAL_DRAM_BASE + 0x00004000)
上面这些值分别和我们开篇时候提到的那几个名词相对应,比如kernel_addr就是ZTEXTADDR,RAMDISK_ADDR就是INITRD_PHYS,而TAGS_ADDR就是PARAMS_PHYS。bootloader会从boot.img的分区中将kernel和ramdisk分别读入RAM上面定义的地址中,然后就会跳到ZTEXTADDR开始执行。
基本了解boot.img的内容之后我们来分别看看里面的ramdisk.img和kernel又是如何产生的,以及其包含的内容。从简单的说起,我们先看看ramdisk.img,这里首先要强调一下这个ramdisk.img在arm linux中的作用。它在kernel启动过程中充当着第一阶段的文件系统,是一个CPIO格式打成的包。通俗上来讲他就是我们将生成的root目录,用CPIO方式进行了打包,然后在kernel启动过程中会被mount作为文件系统,当kernel启动完成以后会执行init,然后将system.img再mount进来作为Android的文件系统。在这里稍微解释下这个mount的概念,所谓mount实际上就是告诉linux虚拟文件系统它的根目录在哪,就是说我这个虚拟文件系统需要操作的那块区域在哪,比如说ramdisk实际上是我们在内存中的一块区域,把它作为文件系统的意思实际上就是告诉虚拟文件系统你的根目录就在我这里,我的起始地址赋给你,你以后就能对我进行操作了。实际上我们也可以使用rom上的一块区域作为根文件系统,但是rom相对ram慢,所以这里使用ramdisk。然后我们在把system.img
mount到ramdisk的system目录,实际上就是将system.img的地址给了虚拟文件系统,然后虚拟文件系统访问system目录的时候会重新定位到对system.img的访问。我们可以看看makefile是如何生成它的:
out/host/linux-x86/bin/mkbootfs out/target/product/msm7627_ffa/root | out/host/linux-x86/bin/minigzip > out/target/product/msm7627_ffa/ramdisk.img
下面我们来看看kernel产生的过程,老方法,从Makefile开始/arch/arm/boot/Makefile ~
[c-sharp]
view plaincopyprint?
$(obj)/Image: vmlinux FORCE
$(call if_changed,objcopy)
@echo ' Kernel: $@ is ready'
$(obj)/compressed/vmlinux: $(obj)/Image FORCE
$(Q)$(MAKE) $(build)=$(obj)/compressed $@
$(obj)/zImage: $(obj)/compressed/vmlinux FORCE
$(call if_changed,objcopy)
@echo ' Kernel: $@ is ready'
[c-sharp] view plaincopyprint? $(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/$(HEAD) $(obj)/piggy.o / $(addprefix $(obj)/, $(OBJS)) FORCE $(call if_changed,ld) @: $(obj)/piggy.gz: $(obj)/../Image FORCE $(call if_changed,gzip) $(obj)/piggy.o: $(obj)/piggy.gz FORCE $(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/$(HEAD) $(obj)/piggy.o / $(addprefix $(obj)/, $(OBJS)) FORCE $(call if_changed,ld) @: $(obj)/piggy.gz: $(obj)/../Image FORCE $(call if_changed,gzip) $(obj)/piggy.o: $(obj)/piggy.gz FORCE
从这里我们就可以看出来实际上这个vmlinux就是将Image压缩以后根据vmlinux.lds与解压代码head.o和misc.o链接以后生成的一个elf,而且用readelf或者objdump可以很明显地看到解压代码是PIC的,所有的虚拟地址都是相对的,没有绝对地址。这里的vmlinx.lds可以对照着后面的head.s稍微看一下~得到压缩以后的vmlinx以后再将这个vmlinx经过objcopy以后就得到我们的zImage了,然后拷贝到out目录下就是我们的kernel了~~
在这里要强调几个地址,这些地址定义在arch/arm/mach-msm/makefile.boot里面,被arch/arm/boot/Makefile调用,其中zreladdr-y就是我们的kernel被解压以后要释放的地址了,解压代码跑完以后就会跳到这个地址来执行kernel的启动。不过这里还有其他两个PHYS,跟前面定义在boardconfig.h里面的值重复了,不知道这两个值在这里定义跟前面的值是一种什么关系???
好啦,讲到这里我们基本就知道boot.img的构成了,下面我们就从解压的代码开始看看arm linux kernel启动的一个过程,这个解压的source就是/arch/arm/boot/compressed/head.S。要看懂这个汇编需要了解GNU ASM以及ARM汇编指令,ARM指令就不说了,ARM RVCT里面的文档有得下,至于GNU ASM,不需要消息了解的话主要是看一下一些伪指令的含义(http://sources.redhat.com/binutils/docs-2.12/as.info/Pseudo-Ops.html#Pseudo%20Ops)
那么我们现在就开始分析这个解压的过程:
1)bootloader会传递2个参数过来,分别是r1=architecture ID, r2=atags pointer。head.S从哪部分开始执行呢,这个我们可以看看vmlinx.lds:
[c-sharp]
view plaincopyprint?
ENTRY(_start)
SECTIONS
{
. = 0;
_text = .;
.text : {
_start = .;
*(.start)
*(.text)
*(.text.*)
*(.fixup)
*(.gnu.warning)
*(.rodata)
*(.rodata.*)
*(.glue_7)
*(.glue_7t)
*(.piggydata)
. = ALIGN(4);
}
[c-sharp] view plaincopyprint? 1: mov r7, r1 @ save architecture ID mov r8, r2 @ save atags pointer #ifndef __ARM_ARCH_2__ /* * Booting from Angel - need to enter SVC mode and disable * FIQs/IRQs (numeric definitions from angel arm.h source). * We only do this if we were in user mode on entry. */ mrs r2, cpsr @ get current mode tst r2, #3 @ not user? bne 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 1: mov r7, r1 @ save architecture ID mov r8, r2 @ save atags pointer #ifndef __ARM_ARCH_2__ /* * Booting from Angel - need to enter SVC mode and disable * FIQs/IRQs (numeric definitions from angel arm.h source). * We only do this if we were in user mode on entry. */ mrs r2, cpsr @ get current mode tst r2, #3 @ not user? bne 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
上面首先保存r1和r2的值,然后进入超级用户模式,并关闭中断。
[c-sharp]
view plaincopyprint?
.text
adr r0, LC0
ldmia r0, {r1, r2, r3, r4, r5, r6, ip, sp}
subs r0, r0, r1 @ calculate the delta offset
@ if delta is zero, we are
beq not_relocated @ running at the address we
@ were linked at.
[c-sharp] view plaincopyprint? add r5, r5, r0 add r6, r6, r0 add ip, ip, r0 add r5, r5, r0 add r6, r6, r0 add ip, ip, r0
然后就是重定位了,即都加上一个偏移,经过重定位以后就都是绝对地址了。
[c-sharp]
view plaincopyprint?
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
/*
* The C runtime environment should now be setup
* sufficiently. Turn the cache on, set up some
* pointers, and start decompressing.
*/
bl cache_on
[c-sharp] view plaincopyprint? mov r1, sp @ malloc space above stack add r2, sp, #0x10000 @ 64k max mov r1, sp @ malloc space above stack add r2, sp, #0x10000 @ 64k max
解压的过程首先是在堆栈之上申请一个空间
[c-sharp]
view plaincopyprint?
/*
* Check to see if we will overwrite ourselves.
* r4 = final kernel address
* r5 = start of this image
* r2 = end of malloc space (and therefore this image)
* We basically want:
* r4 >= r2 -> OK
* r4 + image length <= r5 -> OK
*/
cmp r4, r2
bhs wont_overwrite
sub r3, sp, r5 @ > compressed kernel size
add r0, r4, r3, lsl #2 @ allow for 4x expansion
cmp r0, r5
bls wont_overwrite
mov r5, r2 @ decompress after malloc space
mov r0, r5
mov r3, r7
bl decompress_kernel
add r0, r0, #127 + 128 @ alignment + stack
bic r0, r0, #127 @ align the kernel length
[c-sharp] view plaincopyprint? * r0 = decompressed kernel length * r1-r3 = unused * r4 = kernel execution address * r5 = decompressed kernel start * r6 = processor ID * r7 = architecture ID * r8 = atags pointer * r9-r14 = corrupted */ add r1, r5, r0 @ end of decompressed kernel adr r2, reloc_start ldr r3, LC1 add r3, r2, r3 : ldmia r2!, {r9 - r14} @ copy relocation code stmia r1!, {r9 - r14} ldmia r2!, {r9 - r14} stmia r1!, {r9 - r14} cmp r2, r3 blo 1b add sp, r1, #128 @ relocate the stack bl cache_clean_flush add pc, r5, r0 @ call relocation code * r0 = decompressed kernel length * r1-r3 = unused * r4 = kernel execution address * r5 = decompressed kernel start * r6 = processor ID * r7 = architecture ID * r8 = atags pointer * r9-r14 = corrupted */ add r1, r5, r0 @ end of decompressed kernel adr r2, reloc_start ldr r3, LC1 add r3, r2, r3 1: ldmia r2!, {r9 - r14} @ copy relocation code stmia r1!, {r9 - r14} ldmia r2!, {r9 - r14} stmia r1!, {r9 - r14} cmp r2, r3 blo 1b add sp, r1, #128 @ relocate the stack bl cache_clean_flush add pc, r5, r0 @ call relocation code
因为没有将kernel解压在要求的地址,所以必须重定向,说穿了就是要将解压的kernel拷贝到正确的地址,因为正确的地址与zImage的地址是重合的,而要拷贝我们又要执行zImage的重定位代码,所以这里首先将重定位代码reloc_start拷贝到vmlinx上面,然后再将vmlinx拷贝到正确的地址并覆盖掉zImage。这里首先计算出解压后的vmlinux的高地址放在r1里面,r2存放着重定位代码的首地址,r3存放着重定位代码的size,这样通过拷贝就将reloc_start移动到vmlinx后面去了,然后跳转到重定位代码开始执行。
[c-sharp]
view plaincopyprint?
/*
* All code following this line is relocatable. It is relocated by
* the above code to the end of the decompressed kernel image and
* executed there. During this time, we have no stacks.
*
* r0 = decompressed kernel length
* r1-r3 = unused
* r4 = kernel execution address
* r5 = decompressed kernel start
* r6 = processor ID
* r7 = architecture ID
* r8 = atags pointer
* r9-r14 = corrupted
*/
.align 5
reloc_start: add r9, r5, r0
sub r9, r9, #128 @ do not copy the stack
debug_reloc_start
mov r1, r4
1:
.rept 4
ldmia r5!, {r0, r2, r3, r10 - r14} @ relocate kernel
stmia r1!, {r0, r2, r3, r10 - r14}
.endr
cmp r5, r9
blo 1b
add sp, r1, #128 @ relocate the stack
debug_reloc_end
call_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
mov pc, r4 @ call kernel
[c-sharp] view plaincopyprint? OUTPUT_ARCH(arm) ENTRY(stext) jiffies = jiffies_64; SECTIONS { . = 0x80000000 + 0x00008000; .text.head : { _stext = .; _sinittext = .; *(.text.h OUTPUT_ARCH(arm) ENTRY(stext) jiffies = jiffies_64; SECTIONS { . = 0x80000000 + 0x00008000; .text.head : { _stext = .; _sinittext = .; *(.text.h
很明显我们的vmlinx最开头的section是.text.head,这里我们不能看ENTRY的内容,以为这时候我们没有操作系统,根本不知道如何来解析这里的入口地址,我们只能来分析他的section(不过一般来说这里的ENTRY和我们从seciton分析的结果是一样的),这里的.text.head section我们很容易就能在arch/arm/kernel/head.S里面找到,而且它里面的第一个符号就是我们的stext:
[c-sharp]
view plaincopyprint?
.section ".text.head", "ax"
Y(stext)
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
@ and irqs disabled
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
[c-sharp] view plaincopyprint? #ifndef ENTRY #define ENTRY(name) / .globl name; / ALIGN; / name: #endif #ifndef WEAK #define WEAK(name) / .weak name; / name: #endif #ifndef END #define END(name) / .size name, .-name #endif /* If symbol 'name' is treated as a subroutine (gets called, and returns) * then please use ENDPROC to mark 'name' as STT_FUNC for the benefit of * static analysis tools such as stack depth analyzer. */ #ifndef ENDPROC #define ENDPROC(name) / .type name, @function; / END(name) #endif #ifndef ENTRY #define ENTRY(name) / .globl name; / ALIGN; / name: #endif #ifndef WEAK #define WEAK(name) / .weak name; / name: #endif #ifndef END #define END(name) / .size name, .-name #endif /* If symbol 'name' is treated as a subroutine (gets called, and returns) * then please use ENDPROC to mark 'name' as STT_FUNC for the benefit of * static analysis tools such as stack depth analyzer. */ #ifndef ENDPROC #define ENDPROC(name) / .type name, @function; / END(name) #endif
找到了vmlinux的起始代码我们就来进行分析了,先总体概括一下这部分代码所完成的功能,head.S会首先检查proc和arch以及atag的有效性,然后会建立初始化页表,并进行CPU必要的处理以后打开MMU,并跳转到start_kernel这个symbol开始执行后面的C代码。这里有很多变量都是我们进行kernel移植时需要特别注意的,下面会一一讲到。
在这里我们首先看看这段汇编开始跑的时候的寄存器信息,这里的寄存器内容实际上是同bootloader跳转到解压代码是一样的,就是r1=arch r2=atag addr。下面我们就具体来看看这个head.S跑的过程:
[c-sharp]
view plaincopyprint?
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
@ and irqs disabled
mrc p15, 0, r9, c0, c0 @ get processor id
[c-sharp] view plaincopyprint?
然后跳转到__lookup_processor_type,这个函数定义在head-common.S里面,这里的bl指令会保存当前的pc在lr里面,最后__lookup_processor_type会从这个函数返回,我们具体看看这个函数:
[c-sharp] view plaincopyprint?__lookup_processor_type:
adr r3, 3f
ldmda r3, {r5 - r7}
sub r3, r3, r7 @ get offset between virt&phys
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
1: ldmia r5, {r3, r4} @ value, mask
and r4, r4, r9 @ mask wanted bits
teq r3, r4
beq 2f
add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)
cmp r5, r6
blo 1b
mov r5, #0 @ unknown processor
2: mov pc, lr
ENDPROC(__lookup_processor_type)
[c-sharp] view plaincopyprint? .section ".proc.info.init", #alloc, #execinstr /* * Match any ARMv6 processor core. */ .type __v6_proc_info, #object _proc_info: .long 0x0007b000 .long 0x0007f000 .long PMD_TYPE_SECT | / PMD_SECT_BUFFERABLE | / PMD_SECT_CACHEABLE | / PMD_SECT_AP_WRITE | / PMD_SECT_AP_READ .long PMD_TYPE_SECT | / PMD_SECT_XN | / PMD_SECT_AP_WRITE | / PMD_SECT_AP_READ b __v6_setup .long cpu_arch_name .long cpu_elf_name .long HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP|HWCAP_JAVA .long cpu_v6_name .long v6_processor_functions .long v6wbi_tlb_fns .long v6_user_fns .long v6_cache_fns .size __v6_proc_info, . - __v6_proc_info .section ".proc.info.init", #alloc, #execinstr /* * Match any ARMv6 processor core. */ .type __v6_proc_info, #object __v6_proc_info: .long 0x0007b000 .long 0x0007f000 .long PMD_TYPE_SECT | / PMD_SECT_BUFFERABLE | / PMD_SECT_CACHEABLE | / PMD_SECT_AP_WRITE | / PMD_SECT_AP_READ .long PMD_TYPE_SECT | / PMD_SECT_XN | / PMD_SECT_AP_WRITE | / PMD_SECT_AP_READ b __v6_setup .long cpu_arch_name .long cpu_elf_name .long HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP|HWCAP_JAVA .long cpu_v6_name .long v6_processor_functions .long v6wbi_tlb_fns .long v6_user_fns .long v6_cache_fns .size __v6_proc_info, . - __v6_proc_info
对着.h我们就知道各个成员变量的含义了,他这里lookup的过程实际上是先求出这个proc_info_list的实际物理地址,并将其内容读出,然后将其中的mask也就是我们这里的0x007f000与寄存器与之后与0x007b00进行比较,如果一样的话呢就校验成功了,如果不一样呢就会读下一个proc_info的信息,因为proc一般都是只有一个的,所以这里一般不会循环,如果检测正确寄存器就会将正确的proc_info_list的物理地址赋给寄存器,如果检测不到就会将寄存器值赋0,然后通过LR返回。
[c-sharp]
view plaincopyprint?
bl __lookup_machine_type @ r5=machinfo
movs r8, r5 @ invalid machine (r5=0)?
beq __error_a @ yes, error 'a'
[c-sharp] view plaincopyprint? __lookup_machine_type: adr r3, 3b ldmia r3, {r4, r5, r6} sub r3, r3, r4 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space 1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type teq r3, r1 @ matches loader number? beq 2f @ found add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc cmp r5, r6 blo 1b mov r5, #0 @ unknown machine 2: mov pc, lr ENDPROC(__lookup_machine_type) __lookup_machine_type: adr r3, 3b ldmia r3, {r4, r5, r6} sub r3, r3, r4 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space 1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type teq r3, r1 @ matches loader number? beq 2f @ found add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc cmp r5, r6 blo 1b mov r5, #0 @ unknown machine 2: mov pc, lr ENDPROC(__lookup_machine_type)
这里的过程基本上是同proc的检查是一样的,这里主要检查芯片的类型,比如我们现在的芯片是MSM7X27FFA,这也是一个结构体,它的头文件在arch/arm/include/asm/arch/arch.h里面(machine_desc),它具体的实现根据你对芯片类型的选择而不同,这里我们使用的是高通的7x27,具体实现在arch/arm/mach-msm/board-msm7x27.c里面,这些结构体最后都会注册到_arch_info_begin和_arch_info_end段里面,具体的大家可以看看vmlinux.lds或者system.map,这里的lookup会根据bootloader传过来的nr来在__arch_info里面的相匹配的类型,没有的话就寻找下一个machin_desk结构体,直到找到相应的结构体,并会将结构体的地址赋值给寄存器,如果没有的话就会赋值为0的。一般来说这里的machine_type会有好几个,因为不同的芯片类型可能使用的都是同一个cpu架构。
对processor和machine的检查完以后就会检查atags parameter的有效性,关于这个atag具体的定义我们可以在./include/asm/setup.h里面看到,它实际是一个结构体和一个联合体构成的结合体,里面的size都是以字来计算的。这里的atags param是bootloader创建的,里面包含了ramdisk以及其他memory分配的一些信息,存储在boot.img头部结构体定义的地址中,具体的大家可以看咱以后对bootloader的分析~
[c-sharp]
view plaincopyprint?
__vet_atags:
tst r2, #0x3 @ aligned?
bne 1f
ldr r5, [r2, #0] @ is first tag ATAG_CORE?
cmp r5, #ATAG_CORE_SIZE
cmpne r5, #ATAG_CORE_SIZE_EMPTY
bne 1f
ldr r5, [r2, #4]
ldr r6, =ATAG_CORE
cmp r5, r6
bne 1f
mov pc, lr @ atag pointer is ok
1: mov r2, #0
mov pc, lr
ENDPROC(__vet_atags)
[c-sharp] view plaincopyprint? ldr r13, __switch_data @ address to jump to after @ mmu has been enabled adr lr, __enable_mmu @ return (PIC) address add pc, r10, #PROCINFO_INITFUNC ldr r13, __switch_data @ address to jump to after @ mmu has been enabled adr lr, __enable_mmu @ return (PIC) address add pc, r10, #PROCINFO_INITFUNC
这个__cpu_flush在这里就是我们proc-v6.S里面的__v6_setup函数了,具体它的实现我就不分析了,都是对arm控制寄存器的操作,这里转一下它对这部分操作的注释,看完之后就基本知道它完成的功能了。
/*
* __v6_setup
*
* Initialise TLB, Caches, and MMU state ready to switch the MMU
* on. Return in r0 the new CP15 C1 control register setting.
*
* We automatically detect if we have a Harvard cache, and use the
* Harvard cache control instructions insead of the unified cache
* control instructions.
*
* This should be able to cover all ARMv6 cores.
*
* It is assumed that:
* - cache type register is implemented
*/
完成这部分关于CPU的操作以后,下面就是打开MMU了,这部分内容也没什么好说的,也是对arm控制寄存器的操作,打开MMU以后我们就可以使用虚拟地址了,而不需要我们自己来进行地址的重定位,ARM硬件会完成这部分的工作。打开MMU以后,会将SP的值赋给PC,这样代码就会跳到__switch_data来运行,这个__switch_data是一个定义在head-common.S里面的结构体,我们实际上是跳到它地一个函数指针__mmap_switched处执行的。
这个switch的执行过程我们只是简单看一下,前面的copy data_loc段以及清空.bss段就不用说了,它后面会将proc的信息和machine的信息保存在__switch_data这个结构体里面,而这个结构体将来会在start_kernel的setup_arch里面被使用到。这个在后面的对start_kernel的详细分析中会讲到。另外这个switch还涉及到控制寄存器的一些操作,这里我不没仔细研究spec,不懂也就不说了~
好啦,switch操作完成以后就会b start_kernel了~ 这样就进入了c代码的运行了,下一篇文章仔细研究这个start_kernel的函数~~
了解完kernel启动以前的汇编之后我们来看看正式的c语言启动代码,也就是我们的start_kernel函数了。start_kernel相当大,里面每一个调用到的函数都足够我们伤脑筋了,我这里只是浅尝辄止的描述一下函数的功能,从而对kernel启动的过程有一个比较直观的了解。很多函数真正理解需要对linux相关体系有很深的了解,暂时没有时间深入,留待以后了。
说实话启动的代码看到现在唯一的感觉就是kernel的全局变量实在太多了,要了解一个过程跟踪一个变量的值的变化相当痛苦啊,不过耐心看下来,收获还是比较丰富的,对很多概念都有了一个比较直观的理解。闲话就不多说了,直接来上代码~~
smp_setup_processor_id();
//这个函数现在是空的;
lockdep_init();
//Runtime locking correctness validator, see Documentation/lockdep_design.txt
debug_objects_early_init();
cgroup_init_early();
//Control group, read Documentation/cgroup.txt
local_irq_disable();
//使用arm cpsid i指令来禁止IRQ
early_boot_irqs_off();
early_init_irq_lock_class();
/* 基本上面几个函数就是初始化lockdep和cgroup,然后禁止IRQ,为后续的运行创造条件 */
lock_kernel();
/* 看这个函数的之前我们首先来了解一段知识,linux kernel默认是支持preemption(抢占)的。在SMP环境下为了实现kernel的锁定,kernel使用了一个BKL(big kernel lock)的概念,在初始化的过程中先锁定这个BKL,然后再继续进行其他启动或者初始化过程,这样就可以防止启动过程中被中断,执行到res_init以后,kernel会释放这个锁,这样就确保了整个start_kernel过程都不会被抢占或者中断。由此我们可以看到这主要是针对多处理器而言的,实际上如果只有一个处理器的话它也不会被中断或者被抢占。
*/
/* 下面我们来看看这个函数的执行过程,在这里面能学到很多东西的:
int depth = current->lock_depth+1;
这个current实际上是一个宏,定义在arch/arm/include/asm/current.h里面,它实际是一个task_struct的结构体,这个结构体描述了这个task的一系列信息(task应该是linux进程调度的一个基本单位?)。linux下每个进程都有一个相对应的task_struct,这个task_struct有几个我们经常能看到的信息,一个就是PID,然后就是comm进程的名字,然后就是mm_struct,它定义了跟这个进程相关的所有申请的memory的管理。这个curren_thread_info()也是个很有意思的函数,直接读取SP来获得当前线程的结构体信息thread_info。thread_info和task_struct这两个结构体应该就代表了当前线程的所有信息。
初始化的lock_depth是-1,只有init task的lock_depth是0。
if (likely(!depth))
__lock_kernel();
这里判断是不是init task,如果是就会执行__lock_kernel();这个__lock_kernel首先禁止抢占,然后试着获得BKL,如果成功则直接返回,如果不成功首先判断是否禁止抢占成功了,如果成功了就用自旋锁去锁BKL。如果禁止抢占没有成功则在抢占有效的情况下去等待BKL,直到获得BKL。因为QC的片子不是SMP,所有这里第一次try的时候就直接成功了。
current->lock_depth = depth;
这个就没什么好说的了 */
/* 基本上来说这个lock_kernel就是禁止抢占,然后获得BKL,干了这么件事 */
tick_init();
//和时钟相关的初始化,好像是注册notify事件,没有仔细研究过
boot_cpu_init();
//这个实际上是在SMP环境下选择CPU,这里直接CPUID选择的是0号cpu
page_address_init();
//初始化high memory,在arm环境下实际上这个函数是空的,也就是说arm不支持high memory
printk(KERN_NOTICE);
printk(linux_banner);
//这里的KER_NOTICE是字符串<5>,不太明白它的意思。。。后面的linux_banner定义在kernel/init/version.c里面,这里的printk是门高深的学问,以后看console的时候会仔细分析
setup_arch(&command_line);
/* 这是一个重量级的函数了,会比较仔细地分析一下,主要完成了4个方面的工作,一个就是取得MACHINE和PROCESSOR的信息然或将他们赋值给kernel相应的全局变量,然后呢是对boot_command_line和tags接行解析,再然后呢就是memory、cach的初始化,最后是为kernel的后续运行请求资源。 */
/* 我们来仔细看看这个函数的实现:
setup_processor();
这个函数首先从arm寄存器里面取得cpu ID,然后调用lookup_processor_type来取得proc_info_list这个结构体。这个过程实际上和我们在head-common.S里面的过程是一样的,不知道这里为什么不直接去读switch_data里面已经保存好的数据反而又查询一遍是为什么?取得proc_info_list以后,将里面的内容一个个赋值给相应的全局变量,然后将CPU的信息打印出来。然后它会从arm寄存器里面获得cache的信息,并将cache的信息打印出来。最后它会调用cpu_proc_init()的函数,这个函数实际上定义在proc-v6.S里面,没有做任何事情。
mdesc = setup_machine(machine_arch_type);
首先这个machine_arch_type定义在生成的./include/asm-arm/mach-types.h里面,这个setup_macine实际上也是和上面的processor类似,都是调用head-common.S里面的函数,根据machine ID来获得Machine的信息,并将Machine name打印出来。
if (mdesc->soft_reboot)
reboot_setup("s");
设置reboot方式,默认是硬启动;
if (__atags_pointer)
tags = phys_to_virt(__atags_pointer);
else if (mdesc->boot_params)
tags = phys_to_virt(mdesc->boot_params);
这里首先判断head-common.S里面定义的__atags_pointer是不是为空,不为空的话说明bootloader设置了初始化参数,将参数的物理地址转化为虚拟地址,这里有一个覆盖,就是说可以在Machine desc里面对初始化参数的物理地址重新定位。
if (tags->hdr.tag != ATAG_CORE)
convert_to_tag_list(tags);
if (tags->hdr.tag != ATAG_CORE)
tags = (struct tag *)&init_tags;
首先判断是不是正确的atag格式,如果是以前老版本的param_struct格式会首先将其转换成tag格式,如果转换以后还是不对,则使用默认的init_tags,这里判断的过程都是根据结构体第一个值是不是ATAG_CORE.
if (mdesc->fixup)
mdesc->fixup(mdesc, tags, &from, &meminfo);
if (tags->hdr.tag == ATAG_CORE) {
if (meminfo.nr_banks != 0)
squash_mem_tags(tags);
save_atags(tags);
parse_tags(tags);
这里首先判断fixup函数指针,这里一般为空,如果不为空就会地用fixup来重新修改memory map,meminfo这个结构体定义在arch/arm/include/asm/setup.h里面,描述了内存块的信息,内存块的个数,每个内存块的起始地址和大小,如果修改了memory map则需要从atag参数里面去掉bootloader传过来的的memory map信息,然后是保存一下atag,这个保存函数在这里实际上是空的,没有做任何操作,最后是对atag参数进行解析。这里要说明一下这里的tags实际上是一个tag的数组或者说队列,里面有多个tag结构体,每一个结构体都是一个header加一个参数,具体的结构我们可以看看setup.h。对ATAG参数的解析全部定义在arch/arm/kernel/setup.c里面,首先在setup.c里面定义了一个类似于这样__tagtable(ATAG_CORE,
parse_tag_core)的宏,这个宏实际上是声明了一个放在__tagtable_begin和__tagtable_end段之间结构体,这个结构体定义了这个一个参数类型,和对这个参数类型进行解析的函数。所有的参数解析我们都可以从setup.c里面找到相对应的函数,比如说对boot_commad_line的解析,从config文件得到的default_commad_line就会被ATAG里面获得commad_line给替换掉;再比如ramdisk,就会将ATAG里面的ramdisk的信息赋值给rd_image_start,
rd_size等系统的全局变量。
init_mm.start_code = (unsigned long) _text;
init_mm.end_code = (unsigned long) _etext;
init_mm.end_data = (unsigned long) _edata;
init_mm.brk = (unsigned long) _end;
这就就是对init_mm结构体进行赋值,具体不了解这些东西咋用的,但是就是将text和data段给赋值了。
memcpy(boot_command_line, from, COMMAND_LINE_SIZE);
boot_command_line[COMMAND_LINE_SIZE-1] = '/0';
parse_cmdline(cmdline_p, from);
这里的boot_command_line来自于与config文件里面的CONFIG_CMDLINE,也有可能被ATAG里面的boot参数给覆盖,获得command_line以后对command_line进行解析。这个解析的过程也是在setup.c里面进行的,它首先查找.early_param.init段里面注册的结构体,通过__early_param将early_param结构体注册到这些段里面,实际这个early_param很简单一个就是类似于"initrd="的字符串,用来与command_line里面的字符进行匹配,如果匹配到了就执行early_param里面的那个函数,并将匹配到的字符作为参数传给函数。举个例子比如说我们现在的commadline里面有一句initrd=0x11000000,然后我们首先在early_param.init段里面搜索时候有没有early_param->arg和这个initrd=匹配,找到了就执行它里面的func然后将这个initrd=的值作为参数传进去。
paging_init(mdesc);
这个函数是个大函数,具体内容没有仔细看,需要对arm MMU了解比较深,这里只贴一下source里面关于这个函数的注释:
/*
* paging_init() sets up the page tables, initialises the zone memory
* maps, and sets up the zero page, bad page and bad page tables.
*/
request_standard_resources(&meminfo, mdesc);
这个函数用来申请一些应该是内存资源,具体的内容没有仔细研究,看不大懂。。
cpu_init();
初始化CPU,这里主要是对arm寄存器cpsr的操作
init_arch_irq = mdesc->init_irq;
system_timer = mdesc->timer;
init_machine = mdesc->init_machine;
这里将体系结构相关的几个函数,中断,初始化,定时器之类的赋值给kernel全局变量;
conswitchp = &vga_con;
这里设置了关于console的一个变量,具体不知道怎么用的,以后看console的时候再仔细分析
early_trap_init();
不知道这个函数具体做什么用的。。。 */
/* 基本上我们可以总结出setup_arch主要将一些体系结构的相关信息来赋值给kernel的全局变量,包括cpu啊,machine啊,memory,cahce啊,然后kernel再根据这些函数或者变量来做相应的工作,而且很明显地可以看出来这个setup_arch和前面的head.S,head-common.S,proc-v6.S,board-msm7x27.c是紧密联系在一起的 */
相关文章推荐
- ARMv6 架构下MMU虚拟地址到物理地址二级映射过程
- ARM内核和架构理解
- ARM内核和架构都是什么意思,内核和架构的关系是什么?
- arm-linux解压内核过程
- Android培训班(87)ARM内核生成过程
- Arm linux 内核移植及系统初始化过程分析
- arm内核移植过程
- ARM 架构下的 Linux 已可运行 x86 不再是唯一架构
- ARM内核目标系统中的代码运行时间测试
- Android培训班(87)ARM内核生成过程
- 5.分析内核中断运行过程,以及中断3大结构体:irq_desc、irq_chip、irqaction(详解)
- linux arm架构下tslib1.4 移植全过程
- 在ARM CORTEX-M内核上运行FreeRTOS
- 【android内核剖析笔记】APK程序的运行过程
- linux arm架构下tslib1.4 移植全过程2
- 如何在系统运行过程中修改内核参数
- arm linux 内核生成过程
- ARM big.LITTLE大小核架构在Linux和Android内核下多核调度算法
- ARM架构内核启动分析-head.S(1.1、vmlinux.lds 链接脚本分析)
- ARM Linux (S3C6410架构/2.6.35内核)的内存映射(五)