这是以前玩Arm的时候写的~
板子:朗成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大小的页
引导启动中只使用了第一层分节,未使用第二层分页,下图描述了分节的取址