随笔之GoldFish Kernel启动过程中arm汇编分析一 分析电子版下载地址:进入我的资源就能找到 http://download.csdn.net/detail/innost/4834459 本节介绍Kernel启动。此时Piggy已经将vimlinux解压,BL将执行权限传给了Kernel。 代码在arch/arm/kernel/head.S中。相关代码如下: //将采用C/C++注释语句 /* .section是GNU ASM的语法。格式如下: .section name[,"flags"[,@type]] 其中,name是必须的,flags是可选。 "ax"表示:a为section is allocatable,x为executable。 */ .section ".text.head", "ax"
//这个ENTRY(stext)有相当的含义。在kernel/vmlinux.ld.S中,也定义了一个ENTRY。在ld //语法中,ENTRY是一个command,用来定义入口点。所以,这里就是kernel执行的入口点函数。 ENTRY(stext) /* MSR:是ARM汇编指令,用来将数据copy到status register寄存器中。cpsr_c表示要操作 CPSR寄存器的Control标志。 */
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode @ and irqs disabled 1.1 MSR设置I/F和CPU ModeCPSR全称是Current Process Status Register,用来表示当前CPU的状态,也可用于控制。相关控制位如图1所示: 图1 CPSR控制位 由图1可知: q N/Z/C/V控制位用来表示负/零/进位/溢出,属于User Flags,即可在UserMode下操作。A q I/F表示Interrupt和Fast Interrupt使能位。 q Mode用来控制CPU当前的模式。ARM CPU一共有7种模式。 根据上面的代码,首先将禁止I/F中断,并进入Supervisor模式,也就是OS运行的模式。图2为ARM CPU支持的CPU模式。 图2 ARM CPU支持的运行模式 另外,MSR指令操作的格式如下: 图3 MSR指令格式 其中最重要的是fields,目前支持: q c:设置control bit。对应位为16。 q x:设置extension bit。对应位为17。 q s:设置status bit。对应位为18。 q f:设置flags field。对应位为19。 图4 MSR二进制格式 直接看上面的解释,还不是很清楚,因为设置的是MSR指令本身的内容,具体对应到CPSR呢,则可通过下面的伪语句得到: 图5 MSR 设置说明 从代码可知: msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE //上面代码将设置CPSR的0到第7位,刚好是控制I/F和设置CPU模式的。 1.2 ARM CP15协处理器控制设置好CPU模式后,下面的工作就是获取CPU的信息。在ARM中,协处理(coprocessor)15中用于管理CPU信息和MMU相关的工作。CP15也是ARM中最重要的处理器,以后会经常碰到。 先看下面这条语句: mrc p15, 0, r9, c0, c0 @ get processor id MRC是ARM指令,用来从协处理对应的寄存器读取信息到CPU的寄存器,对应写协处理寄存器的指令是MCR。二者的语法格式(注意,是操作CP15的时候)如图6所示: 图6 MRC操作CP15的格式说明 q Rd:本指令用得是R9,也就是协处理的信息会保存到R9中。 q CRn:MRC中协处理器的主要寄存器。此处用得是C0。标准写法是C0,C1一直到C15。 q CRm:附属信息。如果没有附属信息,则使用C0。 q opcode2,类似附属信息。根据CRn来决定是否需要。如果不指定,则使用0。 CP15有很重要的作用,可通过操作CP15的寄存器来控制它。如图7所示: 图7 CP15各个寄存器的作用 先来看此处操作的C0寄存器。 opcode2在指令中默认是0,所以将取出Main ID register的信息。 得到的结果将怎么使用呢?来看下一句指令: bl __lookup_processor_type @ r5=procinfo r9=cpuid BL是ARM中的跳转指令,相当于调用函数吧。__lookup_processor_type用来得到CPU信息。注意,这个函数调用的参数是R9,R9的值是从CP15 C0寄存器读取出来的,而是是Main ID。下面看看此函数如何处理R9。 1.3 __lookup_procesoor_type分析该函数在head-Common.S中定义。下面逐行分析它,这里会碰到几个重要的指令及其用法。 __lookup_processor_type: //adr是一条伪指令,其作用是将3f标签的地址赋给R3。这个伪指令其实是可拆分成多条指令 //由于后面的3f是相对当前PC位置而言,所以R3实际上存储的是3f的物理地址。 adr r3, 3f//f是forward之意。标志3在此代码之后声明 /* ldm是load multiple register的意思,它的作用是将[r3]对应的内存内容存储到r5,r6, r7寄存器中。DA是Decrease After的意思。ARM汇编在这里有4种模式,DA,IA,DB,IB等 此处的ldmda,将把3F所在的内容依序传递给R7,R6,R5。每传递一次,R3递减4个字节。 */ ldmda r3, {r5 - r7} 上面语句执行完后: q R5=__proc_info_begin,这个值是虚地址。 q R6=__proc_info_end。 q R7=.。 以上几个值都是虚地址。__proc_info_begin/end是ld在链接时候指定的信息。 图8 arc/arm/kernel/vmlinx.lds.S文件 从中可以看出,__proc_info_begin/end包含了代码中定义在.proc.info.init段的内容。如图9所示。 图9 proc-V7.s定义的proc.info.init的内容 为什么是proc -v7.S文件呢,因为goldfish编译的就是这个文件。从图9可以看出,其实也就是定义了一个数据结构罢了。 接着来看代码 //r3指向3f的物理地址,r7指向虚拟地址,而现在只能访问物理地址,所以需要找到一个offset sub r3, r3, r7 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space 经过上面的换算,r5,r6现在都指向__proc_info_begin/end的物理地址了。 //ldmia将[r5]的内存信息存储到r3,r4中,每完成一次传输,r5自动加4. 1: ldmia r5, {r3, r4} @ value, mask //下面将测试R9和mask之后的值是否是我们想要的r3的值。根据图9。应该是0x000f0000。 //在Main ID register中,这表明[16-19]位是都是1. and r4, r4, r9 @ mask wanted bits teq r3, r4 beq 2f //如果是我们想要的数据,则跳转到2f //否则跳过一个PROC_INFO_SIZE,继续找,一般只有一个PROC_INFO结构体。 add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) cmp r5, r6 blo 1b //如果没找到,则设置R5寄存器为0 mov r5, #0 @ unknown processor 2: mov pc, lr //从函数返回 ENDPROC(__lookup_processor_type)
/* * 提供一个C接口的lookup_process_type函数 */ ENTRY(lookup_processor_type) stmfd sp!, {r4 - r7, r9, lr} mov r9, r0 bl __lookup_processor_type mov r0, r5 ldmfd sp!, {r4 - r7, r9, pc} ENDPROC(lookup_processor_type)
.long __proc_info_begin .long __proc_info_end 3: .long . .long __arch_info_begin .long __arch_info_end lookup_process_type其实比较简单,这里就不再多说。但图9的内容以后还要回过头来继续介绍。那里将初始化CPU MMU相关的内容。 //如果r5为空,则表示CPU信息获取是否,调用__error_p,退出整个启动 movs r10, r5 @ invalid processor (r5=0)? beq __error_p @ yes, error 'p'
否则,将调用__lookup_machine_type获取机器信息。 1.4 __lookup_machine_type分析该函数也是在head-comm.S中定义的。 __lookup_machine_type: adr r3, 3b //b是backward的意思。标志3在此代码之前声明。 //r4,r5,r6分别指向 label 3,__arch_info_begin和__arch_info_end ldmia r3, {r4, r5, r6} sub r3, r3, r4 add r5, r5, r3 add r6, r6, r3 //以上将得到__arch_info_begin/end的物理地址
1: ldr r3, [r5, #MACHINFO_TYPE] //比较r1和MACHINFO_TYPE是不是一致。注意,r1的值是BL传递给它的 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) 这里涉及到另一个关键数据结构,也就是定义在.arch.info.init段中的。如图10所示: 图10 .arch.info.init段 从图10可知,这个段其实对应了一个数据结构,即machine_desc.在我们的goldfish平台中,它是这么定义的: [arch/arm/mach-goldfish/board-goldfish.c] //还需要加上: nr = MACH_TYPE_GOLDFISH name = "Goldfish" MACHINE_START(GOLDFISH, "Goldfish") .phys_io = IO_START, .io_pg_offst = ((IO_BASE) >> 18) & 0xfffc, .boot_params = 0x00000100, .map_io = goldfish_map_io, .init_irq = goldfish_init_irq, .init_machine = goldfish_init, .timer = &goldfish_timer, MACHINE_END 完整的machine_desc定义如图11所示: 图11 machine_desc定义 在Goldfish中,nr为1441。详情可参考arch/arm/tools/machine-types.h。 另外,在BootLoader调用kernel之前,传递参数情况如图12所示: 图12 arch/arm/boot/head.S调用kernel前传递参数 从图12可知: q r1保存的是machine nr。 这部分代码属于BootLoader,相当复杂。以后再细说。 假设__lookup_machine_type一切正常 bl __lookup_machine_type @ r5=machinfo movs r8, r5 @ invalid machine (r5=0)? beq __error_a @ yes, error 'a' 1.5 __vet_atags分析接下来的任务就是Kernel校验BL传递的启动参数了。这部分内容和BootLoader有较大关系。 bl __vet_atags 此处的核心概念就是ATAG_CORE/END之类的,由BootLoader往Kernel传递参数,主要是tag结构体 在arch/arm/include/asm/setup.h中。BL传递的是struct tag的链表,该链表以ATAG_CORE开头,以ATAG_NONE结尾。 #define ATAG_CORE 0x54410001 #define ATAG_NONE 0x00000000 struct tag_header { __u32 size; __u32 tag; }; struct tag { struct tag_header hdr; //首先是一个头,根据头部的tag来判断下面的union是哪个 union { struct tag_core core; struct tag_mem32 mem; struct tag_videotext videotext; struct tag_ramdisk ramdisk; struct tag_initrd initrd; struct tag_serialnr serialnr; struct tag_revision revision; struct tag_videolfb videolfb; struct tag_cmdline cmdline; struct tag_acorn acorn; struct tag_memclk memclk; } u; }; 你可以根据上面的信息自行分析__vet_atags函数。 1.6 __create_page_tables分析下面的任务就是调用__create_page_tables创建page table。 bl __create_page_tables //调用__create_page_tables函数 此函数就在head.S中定义,代码如下: __create_page_tables: /* pgtbl是head.S中定义的一个宏,见下面的分析 */ pgtbl r4 pgtbl定义了一个宏,相关代码如下: //TEXT_OFFSET是kernel镜像在内存中的偏移量。一般定义为0X8000,即32KB处 //PHYS_OFFSET:是内核镜像在内存中的起始物理地址。上面二者之和就是内核镜像在机器上的 //物理地址。Goldfish平台中,PHYS_OFFSET为0。 //PAGE_OFFSET是Kernel镜像在虚拟内存的起始地址,一般是3G处 #define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET) #define KERNEL_RAM_PADDR (PHYS_OFFSET + TEXT_OFFSET) .macro pgtbl, rd //此宏调用完毕后,r4的值就是0x4000,即16KB ldr \rd, =(KERNEL_RAM_PADDR - 0x4000) .endm 接着看代码。 mov r0, r4 mov r3, #0 add r6, r0, #0x4000 //STR将寄存器的值往内存中传送。r3为0,故内存的值被设置为0.每调用一次str,r0递增4 //r0是base address,其值可自动增减。由arm address mode格式控制 1: str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 teq r0, r6 bne 1b //此循环调用完毕后,0x4000-0x8000的内存都被设置为0。此时r0=32KB
//r10存储的是图9中proc_info的第三个long,也就是mmuflas,用于设置MMU参数 ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags 1.6.1 ARM MMU设置介绍虽然上面最后一条语句是一个简单的ldr,但背后的内容却相当丰富,不把它搞清楚,后面的内容将解释不清。来看proc-V7中mm_mmuflags对应的值是什么 .long PMD_TYPE_SECT | \ // #define PMD_TYPE_SECT (2 << 0) PMD_SECT_BUFFERABLE | \ //#define PMD_SECT_BUFFERABLE (1 << 2) PMD_SECT_CACHEABLE | \//#define PMD_SECT_CACHEABLE (1 << 3) PMD_SECT_AP_WRITE | \//#define PMD_SECT_AP_WRITE (1 << 10) PMD_SECT_AP_READ //#define PMD_SECT_AP_READ (1 << 11) 上面代码中把对应PMD_SECT_XXX的值显示出来,可知它无非是定义了一个32位的常量,某些位置的值为1,某些位置的值为0。为什么要怎么做呢?先来看ARM MMU所支持的虚实地址转换机制。 图13 ARM MMU虚实地址转换 由图13可知: q 虚地址VA的[20-31]位和CP15 CR2的[14-31]位共同构成First Level地址。 q 从First Level地址将得到一个First Level Descripter,也就是图13中标明memory access的内容。 q FLD中不同字段表明其内容是段寻址还是页寻址。主要是根据前2位来判断。如果前2位是0b10则是段寻址。 结合图13和前面的代码: q PMD_TYPE_SECT = 2<<0,刚好就是0b10 q C|B控制Cachable和Buffable的,对应为[2,3]位 q AP对应为Access Point,对应为[10,11]位。 另外,Domain是ARM CPU的一个重要概念,主要和权限有关。以后碰到再说。 至此,当ldr r7 xx执行完后,r7的值包含了section base address对应的[0-12]位的值。而section base address本身却还没有赋值。 接下来的代码就是为了构造一个FLD的值。根据图13,section base address应该是[20-31]位 //r6的值为当前PC值右移20位 mov r6, pc, lsr #20 orr r3, r7, r6, lsl #20 @ flags + kernel base //此时,r3的值就是一个基于段寻址的FLD。把它存起来。位置是r4+r6<<2 str r3, [r4, r6, lsl #2] 现在r6存储的是段寻址的基地址,需要把这个值存储到对应表的位置,由于在表中,每一项是4个字节,所以这里需要乘以4,也就是lsl #2。 稍微解释下这里左移4的原因: 1 r4存储的是表的起始地址 2 r6存储的是offset 3 r3存储的是往r4[offset]的值 4 由于1个offset实际上是4个字节,所以真实存储的位置就是r4[4*offset] = r3 1.6.2 设置页表当理解上面代码后,下面就是把kernel虚拟地址的位置存储到r4表中了 继续看代码 //立即数的计算比较难理解,网上也没有相关说法。不过,只要知道下面这段代码就是存储kernel //虚拟地址到对应页表位置即可 add r0, r4, #(KERNEL_START & 0xff000000) >> 18 str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! ldr r6, =(KERNEL_END - 1) add r0, r0, #4 //r0 = ro+4 add r6, r4, r6, lsr #18 //r6=r4+r6>>18 1: cmp r0, r6 add r3, r3, #1 << 20 //r3 += 1<<20,每次递增1M //ls是condition code,表示小于等于,即只要r0<=r6,strls就会执行 strls r3, [r0], #4 bls 1b
//map物理地址前1M到对应位置 add r0, r4, #PAGE_OFFSET >> 18 orr r6, r7, #(PHYS_OFFSET & 0xff000000) .if (PHYS_OFFSET & 0x00f00000) orr r6, r6, #(PHYS_OFFSET & 0x00f00000) .endif str r6, [r0]
mov pc, lr ENDPROC(__create_page_tables) .ltorg 1.6.3 总结建议大家仔细体会create_page_tables这段内容。虽然以后不太可能会使用它们,但把这段代码搞清楚还是一个比较有意思的过程。 1.7 剩余工作回到head.S,最后还剩下几句代码: //将__switch_data的位置存储到r13 ldr r13, __switch_data //获取__enable_mmu标签的地址,并保存到lr中 adr lr, __enable_mmu //r10存储的是__v7_proc_info的地址,#PROCINFO_INITFUNC是一个偏移量 //执行完下条语句后,pc指向__v7_proc_info的b __v7_setup,故下面这条语句就是 //执行__v7_setup函数 add pc, r10, #PROCINFO_INITFUNC ENDPROC(stext) 1.7.1 __switch_data说明__switch_data标签如下,主要存储了一些数据。 [head-common.S] .type __switch_data, %object __switch_data: .long __mmap_switched .long __data_loc @ r4 .long _data @ r5 .long __bss_start @ r6 .long _end @ r7 .long processor_id @ r4 .long __machine_arch_type @ r5 .long __atags_pointer @ r6 .long cr_alignment @ r7 .long init_thread_union + THREAD_START_SP @ sp 以后再讨论具体作用。 1.7.2 __v7_setup先来看 add pc, r10, #PROCINFO_INITFUNC 实际上就是执行__v7_setup函数。代码在mm/proc-v7.S中。 adr r12, __v7_setup_stack @ the local stack stmia r12, {r0-r5, r7, r9, r11, lr} bl v7_flush_dcache_all ldmia r12, {r0-r5, r7, r9, r11, lr} mov r10, #0 dsb #ifdef CONFIG_MMU //goldfish定义了这个配置项 mcr p15, 0, r10, c8, c7, 0 @ invalidate I + D TLBs mcr p15, 0, r10, c2, c0, 2 @ TTB control register orr r4, r4, #TTB_FLAGS mcr p15, 0, r4, c2, c0, 1 @ load TTB1 mov r10, #0x1f @ domains 0, 1 = manager mcr p15, 0, r10, c3, c0, 0 @ load domain access register #endif ldr r5, =0xff0aa1a8 ldr r6, =0x40e040e0 mcr p15, 0, r5, c10, c2, 0 @ write PRRR mcr p15, 0, r6, c10, c2, 1 @ write NMRR adr r5, v7_crval ldmia r5, {r5, r6} mrc p15, 0, r0, c1, c0, 0 @ read control register bic r0, r0, r5 @ clear bits them orr r0, r0, r6 @ set them //最后一句,将lr赋值给pc。执行完后,将跳到__enable_mmu函数。 mov pc, lr @ return to head.S:__ret ENDPROC(__v7_setup) 上面代码大多是执行ARM v7 CPU的MMU相关设置的,而其中的汇编语句到比较简单。这也是ARM MMU设置的核心内容。下面我们将结合ARM CPU Rerference简单介绍下这些设置的内容。 请务必从ARM官方网页上下载下面两个文档: q DDI0344D_cortex_a8_r2p1_trm.pdf:介绍CORTEX A8相关内容 q DDI0406B_arm_architecture_reference_manual_errata_markup_10_0:最新的ARM架构参考手册 1. 如何看懂MMU设置并掌握理论知识以下面这个设置为例: mcr p15, 0, r10, c8, c7, 0 打开参考文档DDI0344D_cortex_a8_r2p1_trm.pdf的第112页。从这一页开始,C15协处理器的各个寄存器的配置都有详细的说明。如图14所示 图14 C8寄存器的设置 上图中,左边空白区域对应的是C8。可知,c8,c7,0的组合对应的是Invalidate unified TLB unlocked entries.详细说明在page3-99。 如果在此文档中碰到有不理解的内容,就需要参考DDI0406B_arm_architecture_reference_manual_errata_markup_10_0。该文档会介绍一些理论知识。 篇幅原因,我就不在这里啰嗦。已经告诉大家如何钓鱼了,请大家自己尝试! 1.7.3 __enable_mmu介绍__v7_setup最后已经的mov pc, lr将使得CPU跳转到__enable_mmu处,其代码如下所示: __enable_mmu: #ifdef CONFIG_ALIGNMENT_TRAP orr r0, r0, #CR_A #else bic r0, r0, #CR_A #endif #ifdef CONFIG_CPU_DCACHE_DISABLE bic r0, r0, #CR_C #endif #ifdef CONFIG_CPU_BPREDICT_DISABLE bic r0, r0, #CR_Z #endif #ifdef CONFIG_CPU_ICACHE_DISABLE bic r0, r0, #CR_I #endif //设置domain的权限,请参考前面的书籍了解DOMAIN在ARM MMU中的意义 mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \ domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \ domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \ domain_val(DOMAIN_IO, DOMAIN_CLIENT)) //请参考前面的方法,了解下面这两条语句的实际作用 mcr p15, 0, r5, c3, c0, 0 @ load domain access register mcr p15, 0, r4, c2, c0, 0 @ load page table pointer b __turn_mmu_on //跳转到__turn_mmu_on ENDPROC(__enable_mmu) 简单看看__turn_mmu_on: __turn_mmu_on: mov r0, r0 //类似nop的空指令,浪费一点CPU时间,怕引起race condition发生 //c1,c0这两个控制MMU的设置 mcr p15, 0, r0, c1, c0, 0 @ write control reg mrc p15, 0, r3, c0, c0, 0 @ read id reg mov r3, r3 mov r3, r3 //此时,MMU就正式启动了 mov pc, r13 //r13指向__switch_data ENDPROC(__turn_mmu_on) MMU启动后,我们也无需管什么物理地址还是虚拟地址,直接去看对应地址的代码即可。如果您非对这个转换过程很感兴趣,建议您把那两个参考书好好瞅瞅。 1.7.4 __mmaped_switched介绍__switch_data第一个定义的就是__mmaped_switched,PC将执行这里的指令: __mmap_switched: adr r3, __switch_data + 4
ldmia r3!, {r4, r5, r6, r7} cmp r4, r5 @ Copy data segment if needed 1: cmpne r5, r6 ldrne fp, [r4], #4 strne fp, [r5], #4 bne 1b
mov fp, #0 @ Clear BSS (and zero fp) 1: cmp r6, r7 strcc fp, [r6],#4 bcc 1b
ldmia r3, {r4, r5, r6, r7, sp} str r9, [r4] @ Save processor ID str r1, [r5] @ Save machine type str r2, [r6] @ Save atags pointer bic r4, r0, #CR_A @ Clear 'A' bit stmia r7, {r0, r4} @ Save control register values //上面我就懒得废话了,下面这句代码相信各位都很了解。执行start_kernel函数。 b start_kernel ENDPROC(__mmap_switched)
二总结我觉得需要说明下为什么写这篇文章: 早在2010年7月的时候,我就看了那本鼎鼎大名的《ARM体系结构与编程》,这应该是第一本系统介绍ARM体系结构和编程的书。但是没看懂,全是枯燥的ARM CPU设置,纯教科书。 最近因为工作的原因,想把ARM这块重新捡起来,想起2年的痛苦,觉得应该换个思路。ARM也好,汇编也好,我们应该关注它的目的,而不是具体它是怎么实现的。即了解What to do比了解How to do更重要(仅我个人目的而言,前者重要。不过在某些追求细节的时候,后者重要。需要你自己去判断)。根据这个思路,我选择以Linux Kernel启动为分析对象,大致研究流程如下: q 先花几天时间了解下ARM汇编的大概语句。 q 直接上代码分析。不过你得对Kernel启动的流程稍有了解。还好我在《深入理解Android卷I》写完后,花了点时间把这块整理了下。请参考http://blog.csdn.net/innost/article/details/6693731 q 碰到不懂的汇编语句,就查参考手册。这些还只是针对一些没有背景知识的语句。 q 当碰到类似CP15操作的语句时,其背后往往包含了较多的CPU相关的知识,这时候就需要查阅前面提到的两本参考书籍,去真真正正了解ARM CPU运行的相关原理。 大概经过2周先痛苦挣扎,到后面豁然开朗的过程,后续的研究就非常非常流畅了。 |
|