这是以前玩Arm的时候写的~
主要参考了xpl的arm linux kernel 从入口到start_kernel 的代码分析
板子:朗成AT2440EVB
内核:2.6.18-2
BootLoader在引导启动内核的时候需要设置3个寄存器
R0 – 0
R1 – 板子的ID号
R2 – 内核的参数链表地址,也就是TAG链表
内核在编译之后会进行再连接,连接的脚本在/arch/arm/kernel/vmlinux.lds.S中
SECTIONS { #ifdef CONFIG_XIP_KERNEL . = XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR); #else . = PAGE_OFFSET + TEXT_OFFSET; #endif .init : { /* Init code and data */ _stext = .; _sinittext = .; *(.init.text) _einittext = .; ................................................. } |
PAGE_OFFSET为0xC000 0000 是内核空间的虚拟地址起始处
TEXT_OFFSET 为0x8000 是相对于内核空间的代码段起始处偏移值
这里PAGE_OFFSET + TEXT_OFFSET也就是内核代码段起始处的虚拟地址,为0xC000 8000
而在这个地址的代码为_stext
_stext在/arch/arm/kernel/head.S中
__INIT .type stext, %function ENTRY(stext) //SVC模式,禁止中断和快速中断 msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE // ensure svc mode // and irqs disabled //MRC p15,0,Rd,c0,c0,0 ; returns ID register //用R9保存处理器的ID号 mrc p15, 0, r9, c0, c0 // get processor id //跳转到__lookup_processor_type //并将下一条指令的地址赋给LR寄存器 bl __lookup_processor_type // r5=procinfo r9=cpuid //将R5的值赋给R10,同时检测R5的值是否为0 movs r10, r5 // invalid processor (r5=0)? //为0则跳转到出错处理 beq __error_p // yes, error 'p' //不为0则跳转到__lookup_machine_type //并将下一条指令的地址赋给LR寄存器 bl __lookup_machine_type // r5=machinfo //将R5的值赋给R8,同时检测R5的值是否为0 movs r8, r5 // invalid machine (r5=0)? //为0则跳转到出错处理 beq __error_a // yes, error 'a' //不为0则跳转到__create_page_tables //并将下一条指令的地址赋给LR寄存器 bl __create_page_tables /* * The following calls CPU specific code in a position independent * manner. See arch/arm/mm/proc-*.S for details. r10 = base of * xxx_proc_info structure selected by __lookup_machine_type * above. On return, the CPU will be ready for the MMU to be * turned on, and r0 will hold the CPU control register value. */ //将__switch_data处的地址赋给R13 ldr r13, __switch_data // address to jump to after // mmu has been enabled //将__enable_mmu处的地址赋给LR寄存器 adr lr, __enable_mmu // return (PIC) address //将proc_info_list结构中的__cpu_flush成员的值赋给pc //也就是跳转到__cpu_flush中执行 add pc, r10, #PROCINFO_INITFUNC |
首先是__lookup_processor_type,它负责寻找处理器ID号对应的proc_info_list结构
__lookup_processor_type在arch/arm/kernel/head-common.S中
.type __lookup_processor_type, %function __lookup_processor_type: //读取下面标号3处的地址到R3中 adr r3, 3f //将标号3处地址的内容装载到R5-R7中 //R7 - . //R6 - __proc_info_end //R5 - __proc_info_begin 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 //读取proc_info_list结构中的内容到R3和R4 //R3 -cpu_val //R4 -cpu_mask 1: ldmia r5, {r3, r4} // value, mask //用R4与上R9,只关注需要的位 and r4, r4, r9 // mask wanted bits //比较R3和R4是否相等 teq r3, r4 //相等则跳转到下面标号2处 beq 2f //不等则取得下一个proc_info_list结构 add r5, r5, #PROC_INFO_SZ // sizeof(proc_info_list) //测试R5和R6是否相等,相等则说明proc_info_list结构历遍完毕 cmp r5, r6 //R5和R6不等则跳转到上面的标号1处 blo 1b //R5和R6相等则将R5设置为0 mov r5, #0 // unknown processor //将LR寄存器中的值赋给PC 2: mov pc, lr .long __proc_info_begin .long __proc_info_end 3: .long . .long __arch_info_begin .long __arch_info_end |
由于刚进入引导程序,这个时候MMU还有没开启,所以需要手工计算虚拟地址和物理地址之间的差值
__proc_info_begin和__proc_info_end在/arch/arm/kernel/vmlinux.lds.S的连接脚本中,用于标注proc_info_list结构的起始和结束地址
这里处理器为Arm920T,所以对应的proc_info_list结构在/arch/arm/mm/proc-arm920.S中
执行完毕后回到stext,来到__lookup_machine_type,它负责寻找板子ID号对应的machine_desc结构
__lookup_machine_type在arch/arm/kernel/head-common.S中
.long __proc_info_begin .long __proc_info_end 3: .long . .long __arch_info_begin .long __arch_info_end .type __lookup_machine_type, %function __lookup_machine_type: //将上面标号3处的地址赋给R3 adr r3, 3b //读取R3中的内容到R4-R6 //R4 - . //R5 - __arch_info_begin //R6 - __arch_info_end 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 //读取R5所指的machine_desc结构中的machinfo_type成员到R3中 1: ldr r3, [r5, #MACHINFO_TYPE] // get machine type //比较R3和R1是否相等 teq r3, r1 // matches loader number? //相等则跳转到下面的标号2处 beq 2f // found //不等则将R5指向下一个machine_desc结构 add r5, r5, #SIZEOF_MACHINE_DESC // next machine_desc //检测R5和R6是否相等 cmp r5, r6 //不等则跳转到上面的标号1处 blo 1b //相等则将R5赋为0 mov r5, #0 // unknown machine //将LR寄存器中的值赋给PC 2: mov pc, lr |
__arch_info_begin和__arch_info_end在/arch/arm/kernel/vmlinux.lds.S的连接脚本中,用于标注machine_desc结构的起始和结束地址
这里板子ID号对应的machine_desc结构在/arch/arm/mach-s3c2410/mach-sbz2440.c中
MACHINE_START(SBZ2440, "SBZ2440") .phys_io = S3C2410_PA_UART, .io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc, .boot_params = S3C2410_SDRAM_PA + 0x100,
.init_irq = s3c24xx_init_irq, .map_io = smdk2440_map_io, .init_machine = smdk2440_machine_init, .timer = &s3c24xx_timer, MACHINE_END |
MACHINE_START和MACHINE_END都是宏,在/include/asm/mach/arch.h中
#define MACHINE_START(_type,_name) \ static const struct machine_desc __mach_desc_##_type \ __attribute_used__ \ __attribute__((__section__(".arch.info.init"))) = { \ .nr = MACH_TYPE_##_type, \ .name = _name,
#define MACHINE_END \ }; |
这里machine_desc结构所在的文件是由用户自己编写的,朗成自己改了一个mach-sbz2440.c给AT2440EVB使用
执行完毕后就回到stext,来到__create_page_tables,它负责执行第一阶段,也就是内核引导阶段所要使用的分页初始化
__create_page_tables在/arch/arm/kernel/head.S中
.type __create_page_tables, %function __create_page_tables: // .macro pgtbl, rd // ldr \rd, =(__virt_to_phys(KERNEL_RAM_ADDR - 0x4000)) // .endm //#define __virt_to_phys(x) ((x) - PAGE_OFFSET + PHYS_OFFSET) //PAGE_OFFSET = 0xC000 0000 //PHYS_OFFSET = 0x3000 0000 //#define KERNEL_RAM_ADDR (PAGE_OFFSET + TEXT_OFFSET) //PAGE_OFFSET = 0xC000 0000 //TEXT_OFFSET = 0x8000 //R4 = 0x3000 4000 pgtbl r4 // page table address /* * Clear the 16K level 1 swapper page table */ //将R4的值赋给R0 mov r0, r4 //将R3设为0 mov r3, #0 //R6 = R0 + 0x4000 //R6 = 0x3000 8000 add r6, r0, #0x4000 //将0x30004000 - 0x30008000区域的值清零 //将R3的值赋给R0所指的地址,并且R0的值自加4 1: str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 //当R0 = 0x30008000时初始化完毕 teq r0, r6 //R0未到达0x30008000时则返回上面的标号1处继续初始化 bne 1b //读取proc_info_list结构中的__cpu_mm_mmu_flags成员到R7中 //这个值为0xC1D ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] // mm_mmuflags /* * Create identity mapping for first MB of kernel to * cater for the MMU enable. This identity mapping * will be removed by paging_init(). We use our current program * counter to determine corresponding section base address. */ //将PC寄存器的值向右移20位,取得高12位赋给R6 //这里R6为0x300,因为将内核解压到了物理地址0x3000 8000,则PC的最高12位为0x300 mov r6, pc, lsr #20 // start of kernel section //将R6的值向左移20位后或上R7保存在R3中 orr r3, r7, r6, lsl #20 // flags + kernel base //[0x3000 4000] = 0x3000 0C1D str r3, [r4, r6, lsl #2] // identity mapping /* * Now setup the pagetables for our kernel direct * mapped region. We round TEXTADDR down to the * nearest megabyte boundary. It is assumed that * the kernel fits within 4 contigous 1MB sections. */ //PAGE_OFFSET = 0xC000 0000 //TEXT_OFFSET = 0x8000 //#define KERNEL_RAM_ADDR (PAGE_OFFSET + TEXT_OFFSET) //#define TEXTADDR KERNEL_RAM_ADDR add r0, r4, #(TEXTADDR & 0xff000000) >> 18 // start of kernel //[0x3000 7000] = 0x3000 0C1D str r3, [r0, #(TEXTADDR & 0x00f00000) >> 18]! add r3, r3, #1 << 20 //[0x3000 7004] = 0x3010 0C1D str r3, [r0, #4]! // KERNEL + 1MB add r3, r3, #1 << 20 //[0x3000 7008] = 0x3020 0C1D str r3, [r0, #4]! // KERNEL + 2MB add r3, r3, #1 << 20 //[0x3000 700C] = 0x3030 0C1D str r3, [r0, #4] // KERNEL + 3MB /* * Then map first 1MB of ram in case it contains our boot params. */ //R0 = 0x3000 4000 + 0x3000 add r0, r4, #PAGE_OFFSET >> 18 //R6 = R7 | 0x3000 0000 orr r6, r7, #PHYS_OFFSET //[0x3000 7000] = 0x3000 0C1D str r6, [r0] mov pc, lr |
上面代码中还有一部分宏判断语句,因为这里不会执行,我就不贴出来了
PAGE_OFFSET为0xC000 0000 是内核空间的虚拟地址起始处
TEXT_OFFSET 为0x8000 是相对于内核空间的代码段起始处偏移值
PHYS_OFFSET 为0x3000 0000 是RAM所在的BANK物理地址的起始处
这是我的板子上的设置,因为RAM是接在了BANK6上,而BANK6的起始地址为0x3000 0000,所以PHYS_OFFSET 为0x3000 0000
小结一下,这里将物理地址0x3000 4000 – 0x3000 8000处的内容全部清0
然后设置了以下地址的描述符
[0x3000 4000] = 0x3000 0C1D
[0x3000 7000] = 0x3000 0C1D
[0x3000 7004] = 0x3010 0C1D
[0x3000 7008] = 0x3020 0C1D
[0x3000 700C] = 0x3030 0C1D
__create_page_tables执行完后回到stext中,接下来是以下3步
//将__switch_data处的地址赋给R13
ldr r13, __switch_data
//将__enable_mmu处的地址赋给LR寄存器
adr lr, __enable_mmu
//将proc_info_list结构中的__cpu_flush成员的值赋给pc
//也就是跳转到__cpu_flush中执行
add pc, r10, #PROCINFO_INITFUNC
__enable_mmu和__switch_data等用到的时候再说
现在先来看看add pc, r10, #PROCINFO_INITFUNC
R10在之前指向了ARM920所对应的proc_info_list结构
这个结构在/arch/arm/mm/proc-arm920.S中
结构如下:
__arm920_proc_info: //cpu_val .long 0x41009200 //cpu_mask .long 0xff00fff0 //__cpu_mm_mmu_flags .long PMD_TYPE_SECT | \ PMD_SECT_BUFFERABLE | \ PMD_SECT_CACHEABLE | \ PMD_BIT4 | \ PMD_SECT_AP_WRITE | \ PMD_SECT_AP_READ //__cpu_io_mmu_flags .long PMD_TYPE_SECT | \ PMD_BIT4 | \ PMD_SECT_AP_WRITE | \ PMD_SECT_AP_READ //__cpu_flush b __arm920_setup //arch_name .long cpu_arch_name //elf_name .long cpu_elf_name //elf_hwcap .long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB //cpu_name .long cpu_arm920_name //proc .long arm920_processor_functions //tlb .long v4wbi_tlb_fns //user .long v4wb_user_fns //cache #ifndef CONFIG_CPU_DCACHE_WRITETHROUGH .long arm920_cache_fns #else .long v4wt_cache_fns |
对应的结构声明在/include/asm-arm/procinfo.h中
这里PROCINFO_INITFUNC取的是__cpu_flush,也就是__arm920_setup
__arm920_setup在/arch/arm/mm/proc-arm920.S中,如下:
__INIT .type __arm920_setup, #function __arm920_setup: mov r0, #0 //Invalidate ICache and DCache SBZ MCR p15,0,Rd,c7,c7,0 mcr p15, 0, r0, c7, c7 // invalidate I,D caches on v4 //Drain write buffer SBZ MCR p15,0,Rd,c7,c10,4 //Stops execution until the write buffer has drained. mcr p15, 0, r0, c7, c10, 4 // drain write buffer on v4 #ifdef CONFIG_MMU //Invalidate TLB(s) SBZ MCR p15,0,Rd,c8,c7,0 mcr p15, 0, r0, c8, c7 // invalidate I,D TLBs on v4 #endif //加载下面标号为arm920_crval的地址 adr r5, arm920_crval //加载R5所指的地址内容到R5和R6中 //R5 - clear //当CONFIG_MMU为真时 //R6 - mmuset //当CONFIG_MMU为假时 //R6 - ucset ldmia r5, {r5, r6} //MRC p15, 0, Rd, c1, c0, 0 ; read control register //读取控制寄存器信息到R0中 mrc p15, 0, r0, c1, c0 // get control register v4 //清除不需要的位 bic r0, r0, r5 //置需要的位为真 orr r0, r0, r6 mov pc, lr .size __arm920_setup, . - __arm920_setup /* * R * .RVI ZFRS BLDP WCAM * ..11 0001 ..11 0101 * */ .type arm920_crval, #object arm920_crval: // .macro crval, clear, mmuset, ucset //#ifdef CONFIG_MMU // .word \clear // .word \mmuset //#else // .word \clear // .word \ucset //#endif // .endm crval clear=0x00003f3f, mmuset=0x00003135, ucset=0x00001130 |
SBZ的意思为0,这里也就是需要的参数为0,所以需要先把R0置0
crval是一个宏
.macro crval, clear, mmuset, ucset
#ifdef CONFIG_MMU
.word \clear
.word \mmuset
#else
.word \clear
.word \ucset
#endif
.endm
当CONFIG_MMU为真时则
arm920_crval:
.word 0x00003f3f
.word 0x00003135
为假时则
arm920_crval:
.word 0x00003f3f
.word 0x00001130
最后执行mov pc, lr
在之前内核将LR设为了__enable_mmu
__enable_mmu在/arch/arm/kernel/head.S中,如下
.type __enable_mmu, %function __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 //#define domain_val(dom,type) ((type) << (2*(dom))) // #define DOMAIN_KERNEL 2 //#define DOMAIN_TABLE 2 //#define DOMAIN_USER 1 //#define DOMAIN_IO 0 //#define DOMAIN_MANAGER 3 //#define DOMAIN_CLIENT 1 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, Rd, c3, c0, 0 ; write domain 15:0 access permissions mcr p15, 0, r5, c3, c0, 0 // load domain access register //MCR p15, 0, Rd, c2, c0, 0 ; write TTB register //填写基地址 //R4在之前设置为了0x3000 4000 mcr p15, 0, r4, c2, c0, 0 // load page table pointer b __turn_mmu_on |
主要就是填写了节基地址寄存器,设置基地址为0x3000 4000
然后转到__turn_mmu_on
__turn_mmu_on也在/arch/arm/kernel/head.S中,如下
__turn_mmu_on: mov r0, r0 //MCR p15, 0, Rd, c1, c0, 0 ; write control register mcr p15, 0, r0, c1, c0, 0 // write control reg //MRC p15,0,Rd,c0,c0,0 ; returns ID register mrc p15, 0, r3, c0, c0, 0 // read id reg mov r3, r3 mov r3, r3 mov pc, r13 |
ARM9是5级流水线,分别为
1. 取指
2. 译码
3. 执行
4. 缓冲
5. 回写
第一步mov r0, r0和之前的b __turn_mmu_on一起考虑
在之前的mcr p15, 0, r4, c2, c0, 0 的指令中会装载节基地址,但是这个时候只是取指,到执行还需要2个指令周期, b __turn_mmu_on是第一个指令周期,所以还需要mov r0, r0做第二个指令周期来让mcr p15, 0, r4, c2, c0, 0得以真正的执行
下面的mov r3, r3同理
最后mov pc, r13
R13在之前设置为__switch_data
__switch_data在/arch/arm/kernel/head-common.S中,如下:
.type __switch_data, %object __switch_data: .long __mmap_switched .long __data_loc // r4 .long __data_start // r5 .long __bss_start // r6 .long _end // r7 .long processor_id // r4 .long __machine_arch_type // r5 .long cr_alignment // r6 .long init_thread_union + THREAD_START_SP // sp |
R13中就是__mmap_switched的地址, mov pc, r13等于去执行__mmap_switched所指的指令
__mmap_switched在/arch/arm/kernel/head-common.S中,如下:
.type __mmap_switched, %function __mmap_switched: //加载__switch_data+4处的地址给R3 //也就是__data_loc的地址 adr r3, __switch_data + 4 //加载R3处的内容给R4-R7 //并且将地址回写到R3,最后R3指向processor_id //R4 - __data_loc 数据存放的位置 //R5 - __data_start 数据开始的位置 //R6 - __bss_start BSS段开始的位置 //R7 - _end BSS段结束位位置,也是内核结束的位置 ldmia {r4, r5, r6, r7} //检测__data_loc和__data_start是否相等 cmp r4, r5 // Copy data segment if needed //不等则执行拷贝 //将__data_loc开始处的内容拷贝到__data_start开始的位置 1: cmpne r5, r6 ldrne fp, [r4], #4 strne fp, [r5], #4 bne 1b //将FP指针置0 mov fp, #0 // Clear BSS (and zero fp) //将__bss_start到_end中的内容清0 1: cmp r6, r7 strcc fp, [r6],#4 bcc 1b //加载R3处的内容给R4-R6,SP //R4 - processor_id //R5 - __machine_arch_type //R6 - cr_alignment //SP - init_thread_union + THREAD_START_SP ldmia r3, {r4, r5, r6, sp} //将R9中的值保存到processor_id //也就是保存处理器ID号 str r9, [r4] // Save processor ID //将R1中的值保存到__machine_arch_type //也就是保存板子的ID号 str r1, [r5] // Save machine type //清除R0中的A位后保存到R4中 bic r4, r0, #CR_A // Clear 'A' bit //将R0和R4中的值保存到R6所指的地址 //R6所指的地址在arch/arm/kernel/entry-armv.S // .globl cr_alignment // .globl cr_no_alignment //cr_alignment: // .space 4 //cr_no_alignment: // .space 4 //cr_alignment <-R0 //cr_no_alignment <-R4 stmia r6, {r0, r4} // Save control register values //进入到start_kernel b start_kernel |
注释都有了~ 最后就是跳转到start_kernel,进行第二阶段,也就是内核的初始化
下面对ARM的分页进行一下介绍
ARM的分页分为两层,第一层为必选,称为分节,将内存分为每个1MB的区域,第二层为可选,是将第一层中的1MB区域再进行划分成1KB,4KB或者64KB大小的页
引导启动中只使用了第一层分节,未使用第二层分页,下图描述了分节的取址
上图中的RS为系统使用的属性~ 我这里就不介绍了~
分节取址主要分成了2步,我这里以虚拟地址0xC000 8000介绍之前分页初始化进行的设置:
1. 取得节描述符,使用节基地址寄存器中的节基地址与虚拟地址中的节索引进行组合,这里节基地址为0x3000 4000 它的31-14位为0011 0000 0000 0000 01 ,虚拟地址为0xC000 8000,所以节索引为1100 0000 0000 ,组合得 0011 0000 0000 0000 0111 0000 0000 0000 ,也就是0x3000 7000,取物理地址0x3000 7000处的节描述符
2. 取得物理地址,使用节描述符中的节物理基地址和虚拟地址中的节偏移进行组合,这里0x3000 7000处的节描述符为0x3000 0C1D,则其节物理基地址为0011 0000 0000,虚拟地址为0xC000 8000,所以节偏移为0000 1000 0000 0000 0000,与节物理基地址进行组合,得0011 0000 0000 0000 1000 0000 0000,也就是0x3000 8000
虚拟地址0xC000 8000也就是物理地址0x3000 8000