查找对应arm架构下的arch/arm/kernel/vmlinux.lds.S, 找到入口点ENTRY(stext),这个stext在arch/arm/kernel/head.S中定义,定义如下:
.section ".text.head", "ax"
.type stext, %function
ENTRY(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
movs r10, r5 @ invalid processor (r5=0)?
beq __error_p @ yes, error 'p'
bl __lookup_machine_type @ r5=machinfo
movs r8, r5 @ invalid machine (r5=0)?
beq __error_a @ yes, error 'a'
bl __vet_atags
bl __create_page_tables
在进入到linux kernel时,需要确认在svc模式下,而且irq和fiq都是disable状态。
1.processor id
mrc p15, 0, r9, c0, c0 @ get processor id
arm的协处理器指令,通过对协处理器15 c0 c0操作获取processor id,存放到r9
.type __lookup_processor_type, %function
__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
/*
* This provides a C-API version of the above function.
*/
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}
/*
* Look in include/asm-arm/procinfo.h and arch/arm/kernel/arch.[ch] for
* more information about the __proc_info and __arch_info structures.
*/
.long __proc_info_begin
.long __proc_info_end
3: .long .
.long __arch_info_begin
.long __arch_info_end
adr r3, 3f
将前面标号为3位置的地址存放到r3中,这条指令获取的地址是基于pc的偏移地址,也就是运行时的地址,属于位置无关码。
ldmda r3, {r5 - r7}
r3的地址确定,所以指令运行结束后
r5存的是符号__proc_info_begin的地址;
r6存放的是符号__proc_info_end的地址;
r7存放的是符号3f的地址,这里需要注意运行地址和链接地址的区别,r7中的存放的地址是链接时确定的标号地址,r3中存放的是运行时的地址。
__proc_info_begin和__proc_info_end是在arch/arm/kernel/vmlinux.lds.S中的init代码段中:
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
linux kernel中使用struct proc_info_list 描述processor type,
在include/asm-arm/procinfo.h定义
struct proc_info_list {
unsigned int cpu_val;
unsigned int cpu_mask;
unsigned long __cpu_mm_mmu_flags; /* used by head.S */
unsigned long __cpu_io_mmu_flags; /* used by head.S */
unsigned long __cpu_flush; /* used by head.S */
const char *arch_name;
const char *elf_name;
unsigned int elf_hwcap;
const char *cpu_name;
struct processor *proc;
struct cpu_tlb_fns *tlb;
struct cpu_user_fns *user;
struct cpu_cache_fns *cache;
};
以arm-926为例,可以在arch/arm/mm/proc-arm926.S中找到定义。
.section ".proc.info.init", #alloc, #execinstr
.type __arm926_proc_info,#object
__arm926_proc_info:
.long 0x41069260 @ ARM926EJ-S (v5TEJ)
.long 0xff0ffff0
.long PMD_TYPE_SECT | \
PMD_SECT_BUFFERABLE | \
PMD_SECT_CACHEABLE | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
.long PMD_TYPE_SECT | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
b __arm926_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_arm926_name
.long arm926_processor_functions
.long v4wbi_tlb_fns
.long v4wb_user_fns
.long arm926_cache_fns
.size __arm926_proc_info, . - __arm926_proc_info
从上述代码中可以看到__arm926_proc_info被放在“.proc.info.init”段当中。
继续分析__lookup_processor_type,
sub r3, r3, r7
上面的分析我们可以知道r3中存储的是3f处的物理地址,而r7存储的是3f处的虚拟地址,这一行是计算当前程序运行的物理地址和虚拟地址的差值,将其保存到r3中。
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
将r5,r6存储的虚拟地址转换成物理地址,
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
对照struct proc_info_list结构体定义,可以知道r3,r4分别保存__arm926_proc_info中的cpu_val和cpu_mask;
r9中存储了processor id,与r4的cpu_mask进行逻辑与得到我们需要的值,然后和r3的cpu_val进行比较,如果相等则找到对应的processor id,然后返回。
如果没有找到,则继续寻找比较下一个proc_info,直到__proc_info_end结束,然后设置r5为0并返回,
movs r10, r5 @ invalid processor (r5=0)?
beq __error_p @ yes, error 'p'
如果r5为0,则跳转至__error_p错误处理。
2. machinfo
看完processor id,我们再来看看machinfo,linux kernel使用struct machine_desc结构体描述machine type,通过MACHINE_START来定义,例如:
MACHINE_START(SMDK2410, "SMDK2410") /* @TODO: request a new identifier and switch
* to SMDK2410 */
/* Maintainer: Jonas Dietsche */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = smdk2410_map_io,
.init_irq = s3c24xx_init_irq,
.init_machine = smdk2410_init,
.timer = &s3c24xx_timer,
MACHINE_END
__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
/*
* Lookup machine architecture in the linker-build list of architectures.
* Note that we can't use the absolute addresses for the __arch_info
* lists since we aren't running with the MMU on (and therefore, we are
* not in the correct address space). We have to calculate the offset.
*
* r1 = machine architecture number
* Returns:
* r3, r4, r6 corrupted
* r5 = mach_info pointer in physical address space
*/
.type __lookup_machine_type, %function
__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
和分析processor type类似,
adr r3, 3b
把3b处的地址存入r3中,此处是物理地址;
ldmia r3, {r4, r5, r6}
把3b处开始的连续地址即3b处的地址,__arch_info_begin,__arch_info_end依次存入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,r6的虚拟地址转换为物理地址
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
获取machine_desc结构成员nr,并和r1进行比较,其中r1是uboot调用kernel入口地址时传入的参数。如果找到匹配直接返回,如果在“arch.info.init”段中都找不到,那就设置r5为0,然后返回进入错误处理。
3. vet_atags
检测bootloader传入参数链表atags的合法性。
.type __vet_atags, %function
__vet_atags:
tst r2, #0x3 @ aligned?
bne 1f
ldr r5, [r2, #0] @ is first tag ATAG_CORE?
subs r5, r5, #ATAG_CORE_SIZE
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
首先检测参数链表指针是否对齐,然后检测第一个tag长度是否合法,是不是ATAG_CORE,如果正常直接返回,如果有其中某一项不正常,则将参数设置为0然后返回。
相关结构定义如下:
struct tag {
struct tag_header hdr;
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;
};
struct tag_header {
u32 size;
u32 tag;
};
size=(sizeof(tag->tag_header)+sizeof(tag->u.core))>>2
#define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
4.__create_page_tables
首先来看ARM MMU所支持的虚实地址转换机制,下图所示虚地址VA的[20-31]位和CP15 CR2的[14-31]位共同构成一个地址,这个虚拟地址里存放的是一级页表项,表项中section base address对应virtual address的table index,由此找到虚拟地址对应的物理地址。
下面看下代码是如何实现创建页表的,函数定义如下:
.macro pgtbl, rd
ldr \rd, =(KERNEL_RAM_PADDR - 0x4000)
.endm
.type __create_page_tables, %function
__create_page_tables:
pgtbl r4 @ page table address
/*
* Clear the 16K level 1 swapper page table
*/
mov r0, r4
mov r3, #0
add r6, r0, #0x4000
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
为页表存放预留16K区域并清零,后面需要通过协处理器指令进行设置。
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
获得proc_info_list的__cpu_mm_mmu_flags的值,并存储到 r7中。
mov r6, pc, lsr #20 @ start of kernel section,
orr r3, r7, r6, lsl #20 @ flags + kernel base
str r3, [r4, r6, lsl #2] @ identity mapping
通过PC值高12位得到kernel的section基地址,
r3 = r7 | (r6 << 20) @ kernel base + mmu flag
设置页表项,并将该页表项数据存放对应的页表中,具体是 *(r4 + r6 << 2) = r3
/*
* Now setup the pagetables for our kernel direct
* mapped region.
*/
add r0, r4, #(KERNEL_START & 0xff000000) >> 18
str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
r0 存放转换表的起始位置
ldr r6, =(KERNEL_END - 1)
r6 存放kernel_end虚拟地址
add r0, r0, #4
此后依次递增
add r6, r4, r6, lsr #18
计算最后一条存放的地址,存放在r6中
1: cmp r0, r6
add r3, r3, #1 << 20
strls r3, [r0], #4
bls 1b
从开始位置到结束,依次填充页表项,一个页表项代表了1MB空间的映射关系。XIP宏相关的映射我们直接跳过。
/*
* Then map first 1MB of ram in case it contains our boot params.
*/
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
.ltorg
设置RAM的第一MB虚拟地址的页表,映射完成之后如下图所示
4.调用平台特定的 __cpu_flush 函数
mmu页表配置完成后,在开启mmu前还需要完成很多操作,比如清除ICache,DCache,wrtiebuffer,TLB等,这些都可以通过CP15来完成。
代码如下:
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
分别设置sp,lr,pc,注意r10存储的是proc info的基地址, PROCINFO_INITFUNC 宏指xiang的是proc_info_list结构体中的成员函数__cpu_flush,这里也就是 b __arm926_setup函数
.type __arm926_setup, #function
__arm926_setup:
mov r0, #0
mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4
mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4
#ifdef CONFIG_MMU
mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4
#endif
adr r5, arm926_crval
ldmia r5, {r5, r6}
mrc p15, 0, r0, c1, c0 @ get control register v4
bic r0, r0, r5
orr r0, r0, r6
mov pc, lr
.size __arm926_setup, . - __arm926_setup
.type arm926_crval, #object
arm926_crval:
crval clear=0x00007f3f, mmuset=0x00003135, ucset=0x00001134
使数据cache,指令cache无效;使write buffer无效;使数据TLB,指令TLB无效;
获取arm926_crval地址,并存入r5,存入该地址连续8字节数据分别到r5, r6,
r5 = 0x00007f3f, r6 = 0x00003135.
通过cp15获取操作寄存器的值,并对mmu进行设置,然后返回进入__enable_mmu.
.type __enable_mmu, %function
__enable_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
/*
* Enable the MMU. This completely changes the structure of the visible
* memory space. You will not be able to trace execution through this.
* If you have an enquiry about this, *please* check the linux-arm-kernel
* mailing list archives BEFORE sending another post to the list.
*
* r0 = cp#15 control register
* r13 = *virtual* address to jump to upon completion
*
* other registers depend on the function called upon completion
*/
.align 5
.type __turn_mmu_on, %function
__turn_mmu_on:
mov r0, r0
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
mov pc, r13
mmu的具体操作我们不在关心,这里主要看下最后一条指令
mov pc,r13
由此进入__switch_data,看下__switch_data的定义,
.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 __atags_pointer @ r6
.long cr_alignment @ r7
.long init_thread_union + THREAD_START_SP @ sp
明显可以看出会调用到__mmap_switched.
.type __mmap_switched, %function
__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
b start_kernel
这里检测data段存储位置和数据开始位置是否不相等,是否需要搬运数据。清除bss段,并保存相关参数,最后进入start_kernel.