Linux启动之/arch/arm/kernel/head.S
当解压缩部分的head.S执行完后,就开始执行kernel/目录下真正的linux内核代码。在内核连接文件/kernel/vmlinux/lds里定义了这部分开始所处的段空间为.text.head,也即内核代码段的头
关键代码如下:
mrc p15, 0, r9, c0, c0 @ get processor id//读出CPUid
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
大致流程为,寻找CPU类型查找机器信息,解析内核参数列表,创建内存分页机制
__lookup_processor_type,__lookup_machine_type,__vet_atags函数都在kernel/head-comm.S内,这个文件实际上是被包含在head.S内
Linux之所以把搜索机器类型和CPU类型独立出来,就是为了让内核尽可能的和bootload独立,增强移植性,把不同CPU的差异性处理减到最小。比如不同ARM架构的CPU处理中断的,打开MMU,cach操作是不同的,因此,在内核开始执行前需要定位CPU架构,比如高通利用的ARM11,Ti用的cortex-8架构
__lookup_machine_type 寻找的机器类型结构定义在arch/arm/include/asm/mach.h中
查询方法比较简单,利用bootloa传进来的参数依次查询上述结构表项
这个表项是在编译阶段将#define MACHINE_START(_type,_name)宏定义的结构体struct machine_desc 连接到
__arch_info段,那么结构体开始和结束地址用__arch_info_begin和__arch_info_end符号引用
3: .long .
.long __arch_info_begin
.long __arch_info_end
//r1 = 机器架构代码 number,由bootload最后阶段传进来
.type __lookup_machine_type, %function
__lookup_machine_type:
adr r3, 3b
ldmia r3, {r4, r5, r6}
sub r3, r3, r4 @ 此时没有开MMU,因此需要确定放置__arch_info_begin的实际物理地址
add r5, r5, r3 @ 调整地址,找到__arch_info的实际地址(连接地址和物理地址不一定一样,因此需要调整)
add r6, r6, r3 @
1: ldr r3, [r5, #MACHINFO_TYPE] @ MACHINFO_TYPE=机器类型域的偏移量
teq r3, r1 @ 是否和bootload传进来的参数相同?
beq 2f @ 找到则跳出循环
add r5, r5, #SIZEOF_MACHINE_DESC @ 地址偏移至下个__arch_inf表项
cmp r5, r6
blo 1b
mov r5, #0 @ 未知的类型
2: mov pc, lr//返回
__lookup_processor_type的查询的结构为struct proc_info_list
机器类型确定后即开始解析(__vet_atags)内核参数列表,判断第一个参数类型是不是ATAG_CORE。
内核参数列表一般放在内核前面16K地址空间处。列表的表项由struct tag构成,每个struct tag有常见的以下类型:
:ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。
这些类型是宏定义,比如#define ATAG_CORE 0x54410001
arch/arm/include/asm/setup.h
struct tag_header {
__u32 size;
__u32 tag;
};
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;//命令行
} u;
};
接下来就是创建页表,因为要使能MMU进行虚拟内存管理,因此必须创建映射用的页表。页表就像一个函数发生器,保证访问虚拟地址时能从物理地址里取到正确代码
pgtbl r4 @ page table address
//页表放置的位置可由下面的宏确定,即在内核所在空间的前16K处
.macro pgtbl, rd
ldr /rd, =(KERNEL_RAM_PADDR - 0x4000)
.endm
mov r0, r4
mov r3, #0
add r6, r0, #0x4000//16K的空间,r6即是页表结束处
1: str r3, [r0], #4//清空页表项,页表项共有16K/4项
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS]
//从从差得的proc_info_list结构PROCINFO_MM_MMUFLAGS处获取MMU的信息
/*
为内核创建1M的映射空间,这里是按照1:1一致映射,即代码的基地址(高12bit)对应相同的物理块地址。这种映射关系只是在启动阶段,在跳进start_kernel后会被paging_init().移除。这种映射可以直接利用当前地址的高12bit作为基地址,这种方式很巧妙,因为当前的PC(加颜色处的地址)依然在1M空间内,因此,高12bit(段基地址)在1M空间内都是相同的。
*/
mov r6, pc, lsr #20 @ 内核映像的基地址
orr r3, r7, r6, lsl #20 @ 基地址偏移后再加上标示符,即可得一个页表项的值
str r3, [r4, r6, lsl #2] @将此表项按照页表项的索引存入对应的表项中。比如,若//基地址是0xc0001000,那么存入页表的第0xc00项中
//目前的映射依然是1:1的映射
//然后移到下个段基地址处,开始映射此KERNEL_START对应的空间
//这个空间映射的物理地址与上面的相同,也就是两个虚拟地址映射到了同一个物理地址空间
//r0+基地址组成//在第一级页表中索引到相关的项
add r0, r4, #(KERNEL_START & 0xff000000) >> 18
str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
ldr r6, =(KERNEL_END - 1)
add r0, r0, #4//移到下个表项
add r6, r4, r6, lsr #18//结束的基地址
1: cmp r0, r6
add r3, r3, #1 << 20//下个1M物理地址空间
strls r3, [r0], #4//建立映射表项,开始创建所有的内核空间页表项
bls 1b//
#ifdef CONFIG_XIP_KERNEL
/*
* Map some ram to cover our .data and .bss areas.
*/
orr r3, r7, #(KERNEL_RAM_PADDR & 0xff000000)
.if (KERNEL_RAM_PADDR & 0x00f00000)
orr r3, r3, #(KERNEL_RAM_PADDR & 0x00f00000)
.endif
add r0, r4, #(KERNEL_RAM_VADDR & 0xff000000) >> 18
str r3, [r0, #(KERNEL_RAM_VADDR & 0x00f00000) >> 18]!
ldr r6, =(_end - 1)
add r0, r0, #4
add r6, r4, r6, lsr #18
1: cmp r0, r6
add r3, r3, #1 << 20
strls r3, [r0], #4
bls 1b
#endif
/*
* Then map first 1MB of ram in case it contains our boot params.
*/
//虚拟ram地址的第一个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//页表建立完成,返回
页表创建后,具体的映射空间如下图:
执行完上述页表创建,开始执行内核跳转:
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
__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
语句“add pc, r10, #PROCINFO_INITFUNC”通过查表调用proc-v7.s中__v7_setup函数,该函数末尾通过将lr寄存器赋给pc,导致对__enable_mmu的调用,完成使能mmu的操作,之后将r13寄存器值赋给pc,调用__switch_data数据结构中的第一个函数__mmap_switched,
.type __mmap_switched, %function
__mmap_switched:
adr r3, __switch_data + 4
ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5 @ 拷贝数据段
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
mov fp, #0 @ 清除BSS段
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ldmia r3, {r4, r5, r6, r7, sp}//然后调整指针到processor_id 域
str r9, [r4] @ 保存CPU ID
str r1, [r5] @保存机器类型
str r2, [r6] @ 保存参数列表指针
bic r4, r0, #CR_A @ Clear 'A' bit
stmia r7, {r0, r4} @ 保存控制信息
b start_kernel
最终调用init/main.c文件中的start_kernel函数。
这个start_kernel正是kernel/init/main.c的内核起始函数