Linux kernel启动流程第一阶段
head.S:kernel第一阶段入口
来自:http://blog.csdn.net/ooonebook/article/details/52710290
1、safe_svcmode_maskallr9;关闭普通中断、快速中断,使能SVC模式
实现代码:arch/arm/include/asm/assembler.h
注解:来自http://blog.csdn.net/crosskernel/article/details/21091819
从Hyper态返回SVC态
//reg—暂存寄存器
.macro safe_svcmode_maskall reg:req
#if __LINUX_ARM_ARCH__ >= 6
//读取cpsr到暂存寄存器reg
mrs \reg , cpsr
/*以下两条指令区分当前cpsr是否处在HYP_MODE,
*若处在HYP_MODE模式,标志位清零
*/
eor \reg, \reg, #HYP_MODE
tst \reg, #MODE_MASK
bic \reg , \reg , #MODE_MASK
//在暂存寄存器里存放SVC控制位
orr \reg , \reg , #PSR_I_BIT | PSR_F_BIT | SVC_MODE
THUMB( orr \reg , \reg , #PSR_T_BIT )
/*若当前寄存器处于hyper模式,返回SVC模式走一个杜撰的HVC异常返回。*/
bne 1f
orr \reg, \reg, #PSR_A_BIT
/*设置返回到SVC态的地址是标号2,即该宏调用的后一条指令。*/
adr lr, BSYM(2f)
//退出hyper后的cpsr寄存器
msr spsr_cxsf, \reg
//放到ELR_hyp中
__MSR_ELR_HYP(14)
//返回到SVC态
__ERET
//当前处理器不在hyper状态,强制切换到SVC态
1: msr cpsr_c, \reg
2:
2、proccessor info的获取
cpu id和procinfo是一一对应的关系,所以可以通过cpu id来获取到对应的procinfo结构体。
procinfo使用proc_info_list结构体,用来说明一个cpu的信息,包括这个cpu的ID号,对应的内核数据映射区的MMU标识等等。
注意: - proc_info_list结构体中存在MMU标识,也就是我们需要在打开MMU之前需要先获取procinfo的原因,因为打开MMU之前需要配置临时内核页表,而配置临时内核页表需要这里的MMU标识来进行设置。
对应代码:
1)mrc p15, 0, r9, c0, c0 @ get processor id
解释:arm体系将CPUID(处理器标识符,主标识符)存放在协处理器cp15的c0寄存器中。
2)bl__lookup_processor_type @r5 = procinfor9 = cupid
arch/arm/kernel/head-common.S 通过比较各个CPU的procinfo中的cpu id的值来查找对应的procinfo结构体。若不支持当前CPU,则r5=0
3)判断r5,若为0,则打印错误。
3、地址的一次转换
@ 在调用__enable_mmu前使用的都是物理地址,而内核却是以虚拟地址连接的,这里进行一次转换
#ifndef CONFIG_XIP_KERNEL
adr r3,2f @ r3= 第124行代码的物理地址
ldmia r3, {r4,r8} @r4= 第124行代码的虚似地址,r8=PAGE_OFFSET
sub r4, r3,r4 @ (PHYS_OFFSET - PAGE_OFFSET)即物理地址与虚似地址差值
add r8, r8,r4 @ PHYS_OFFSET r8=PAGE_OFFSET对应的物理地址
#else
ldr r8,=PLAT_PHYS_OFFSET @ RAM的起始物理地址,值为0x30000000
L124 2: .long . @ "."号表示当前这行代码编译连接后的虚似地址
L125 .long PAGE_OFFSET
4、验证dtb bl __vet_atags
实现代码:arch/arm/kernel/head-common.S
TIPS:
1、dtb里面存放了各种硬件信息,如果dtb有问题,会导致后续开机过程中读取的设备信息有问题而导致无法开机。
2、在生成dtb的时候会在头部上添加一个幻数magic,而验证dtb是否合法主要也就是看这个dtb的magic是否和预期的值一致。其中magic是固定值:0xd00dfeed(大端)或者0xedfe0dd0(小端)。我们只要提取待验证dtb的地址(Uboot传入的r2)上的数据的前四个字节,与0xd00dfeed(大端)或者0xedfe0dd0(小端)进行比较,如果匹配的话,就说明对应待验证dtb就是一个合法的dtb。
代码注解:
Ldr r5, [r2, #0] @获取前4个字节放在r5中
Ldrr6, =OF_DT_MAGIC @获取magic放在r6
5、bl__create_page_tables(创建临时内核页表====>打开MMU)
实现代码:arch/arm/kernel/head.S
TIPS:
1、MMU(Memory ManageUnion):
将线性地址(虚拟地址)映射为物理地址(RAM地址);
提供硬件机制的内存访问授权;
根据页表找到对应关系以及权限;
2、为了打开MMU,内核需要创建一个临时内核页表,用于kenrel启动过程中的打开MMU的过渡阶段。 并且,使用的是段式管理的方法。
代码注解:
1)获取内核页表的起始地址
pgtbl r4, r8 @ page table address
pgtbl 宏用于通过DRAM物理地址来获取页表的物理地址。
前面我们已经知道r8用于存放DRAM的起始物理地址,r4则是要存放计算得到的页表物理地址。
pgtbl 宏如下:
arch/arm/kernel/head.S
.macro pgtbl, rd, phys
add \rd, \phys,
#TEXT_OFFSET
sub \rd, \rd,
#PG_DIR_SIZE
.endm
kernel在放在DRAM上偏移TEXT_OFFSET的位置上。 而linux规定将TEXT_OFFSET之前的PG_DIR_SIZE大小的空间用作临时页表。
所以计算方式如下:
kernel起始地址=DRAM起始物理地址+TEXT_OFFSET=0x20008000
内核页表地址=kernel起始地址-PG_DIR_SIZE=0x20004000 =>PG_DIR_SIZE=0x4000(16K)
所以代码换算成如下计算:
\rd(r4) = phys(r8) +TEXT_OFFSET
\rd(r4) = \rd(r4) -PG_DIR_SIZE
2)清零页表
str r3, [r0], #4
@ 从r0(临时内核页表物理地址)指向的寄存器上开始写入0值,每16个字节一个循环
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
每16个字节一循环可以减少teq的频率,同时不会出现清零越界(因:页表大小为16K)
3)设置MMU的标识并存放到r7寄存器
ldr r7,[r10, #PROCINFO_MM_MMUFLAGS]@ mm_mmuflags
PROCINFO_MM_MMUFLAGS对应如下
DEFINE(PROCINFO_MM_MMUFLAGS, offsetof(struct proc_info_list, __cpu_mm_mmu_flags));
4)创建映射表
未懂
6、b __enable_mmu(调用平台特定的__CPU_flush函数(在结构体proc_info_list中),使能MMU)
代码:
ldr r13, =__mmap_switched @ address to jump to after
@ mmu has been enabled
@ 把__mmap_switched函数的地址放在寄存器
@ __mmap_switched实现了打开MMU之后跳转到start_kernel。
adr lr, BSYM(1f) @ return (PIC) address
@简单说,就是存储了调用子程序后返回的指令地址
movr8, r4 @ set TTBR1 to swapper_pg_dir
@ 把临时内核页表的地址放在r8寄存器中
ldrr12, [r10, #PROCINFO_INITFUNC]
@ 把cpu对应procinfo中的__cpu_flush存放到r12寄存器中
@ __cpu_flush成员存放的是cpu对应架构的setup函数的地址,在结构体proc_info_list中
@ 对于s5pv210来说,这个值就是__v7_setup的连接地址。
addr12, r12, r10
retr12
@ 这里实现为跳转到__v7_setup的物理地址上,也就是调用__v7_setup
1: b __enable_mmu
@ 跳转到__enable_mmu
__enable_mmu:
· 需要先将页表物理地址写入到cp15的c2寄存器中
· 需要在cp15的c3寄存器中写入位域相应的权限
· 配置cp15的c1寄存器,用来控制MMU的相应功能
· 跳转到__turn_mmu_on
__turn_mmu_on:真正的打开MMU的函数,打开MMU后CPU会把所有地址都当做虚拟地址处理。
注:__turn_mmu_on中会执行命令:
mov r3, r13
ret r3
之前在head.S中有指令“ldr r13, =__mmap_switched”,因此会直接跳转去执行__mmap_switched,该函数负责跳转至内核。
7、跳转至内核
准备阶段:
· 数据段的准备
· 堆栈段的准备
· 一些后续会访问到的变量的设置(CPU ID、machine id、&dtb、当前进程堆栈指针)
· 当前进程堆栈指针的设置
代码:
__mmap_switched_data结构体:
__mmap_switched_data:
.long __data_loc @ r4
.long _sdata @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ r4
.long __machine_arch_type @ r5
.long __atags_pointer @ r6
#ifdefCONFIG_CPU_CP15
.long cr_alignment @ r7
#else
.long 0 @ r7
#endif
.long init_thread_union +THREAD_START_SP @ sp
.size __mmap_switched_data, . - __mmap_switched_data
__mmap_switched:
adrr3, __mmap_switched_data
@ 将__mmap_switched_data的地址加载到r3中
ldmia r3!, {r4, r5, r6, r7}
@ 将__mmap_switched_data(r3)上的值分别加载到r4、r5、r6、r7寄存器中,__mmap_switched_data前面说明了
@ 经过上述动作,r4、r5、r6、r7寄存器分别存放了如下值
@ r4 -> __data_loc:数据段存储地址
@ r5 -> _sdata:数据段起始地址
@ r6 -> __bss_start:堆栈段起始地址
@ r7 -> _end:堆栈段结束地址
cmpr4, r5 @ Copy data segment if needed
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
@ 判断数据段存储地址(r4)和数据段起始地址(r5)
@ 如果不一样的话需要搬移到数据段起始地址(r5)上。
movfp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
@ 清空堆栈段。
@ 从堆栈段起始地址(r6)开始写入0,一直写到地址为堆栈段结束地址(r7)
ARM( ldmia r3, {r4, r5, r6, r7, sp})
THUMB(ldmia r3, {r4, r5, r6, r7} )
THUMB(ldr sp, [r3, #16] )
@ 继续将__mmap_switched_data(r3)上的值分别加载到r4、r5、r6、r7、sp寄存器中,注意是前面r3已经加载过一部分了,地址和__mmap_switched_data已经不一样了。
@ 经过上述动作,r4、r5、r6、r7寄存器分别存放了如下值
@ r4 -> processor_id变量地址:其内容是cpu处理器ID
@ r5 -> __machine_arch_type变量地址:其内容是machine id
@ r6 -> __atags_pointer变量地址:其内容是dtb的地址
@ r7 -> cr_alignment变量地址:其内容是cp15的c1的寄存器的值
@ sp-> init_thread_union + THREAD_START_SP,设置了当前进程的堆栈
strr9, [r4] @ Save processor ID
@ 把cpu处理器id(r9)放到processor_id变量中([r4])
strr1, [r5] @ Save machine type
@ 把mechine id(r1)存放到__machine_arch_type变量中([r5])
strr2, [r6] @ Save atags pointer
@ 把dtb的地址指针(r2)存放到__atags_pointer变量中([r6])
cmpr7, #0
strne r0, [r7] @ Save control register values
@ 把cp15的c1的寄存器的值(r0)存放到cr_alignment变量中([r7])
b start_kernel
@ 跳转到start_kernel中,也就是启动流程的第二阶段。
ENDPROC(__mmap_switched)
Kernel启动流程中的Tips:
1、 Kernel一般会存在于存储设备上,比如FLASH\EMMC\SDCARD. 因此,需要先将kernel镜像加载到RAM的位置上,CPU才可以去访问到kernel。
但是注意,加载的位置是有要求的,一般是加载到物理RAM偏移0x8000的位置,也就是要在前面预留出32K的RAM。kernel会从加载的位置上开始解压,而kernel前面的32K空闲RAM中,16K作为boot params,16K作为临时页表
2、 Arch/arm/kernel/head.S(kernel的入口函数)
3、 bootloader需要通过设置PC指针到kernel的入口代码处(也就是kernel的加载位置)来实现kernel的跳转。
Kernel的硬件要求如下(解释了bootloader为何去那样初始化硬件):
* MMU =off
MMU用来处理物理地址到虚拟内存地址的映射,因此需要软件上需要先配置其映射表(也就是后续文章会说明的页表)。MMU关闭的情况下,CPU寻址的地址都是物理地址,也就是不需要经过转化直接访问相应的硬件。一旦打开之后,CPU寻址的所有地址都是虚拟地址,都会经过MMU映射到真正的物理地址上,即使你在代码中访问的是一个物理地址,也会被当作虚拟内存地址使用。
而映射表是由kernel自己创建的,因此,在创建映射表之前kernel访问的地址都是物理地址,所以必须保证MMU是关闭状态。
D-cache= off
CACHE是CPU和内存之间的高速缓冲存储器,又分成数据缓冲器D-cache和指令缓冲器I-cache。
数据Cache一定要关闭,否则可能kernel刚启动的过程中,去取数据的时候,从Cache里面取,而这时候RAM中数据还没有Cache过来,导致数据预取异常。
博主的理解是,假设打开MMU之前,cache上存了一个项“地址0x20000000、数据0xffff0000”,打开MMU之后,读取0x20000000地址上(虚拟地址)数据,但是此时会直接从cache中读到项“地址0x20000000、数据0xffff0000”,但实际上对应物理地址上的数据并不是这个,所以会导致读取的数据错误。
宏 |
位置 |
默认值 |
说明 |
KERNEL_RAM_ADDR |
arch/arm/kernel/head.S +26 |
0xc0008000 |
kernel在RAM中的虚拟地址 |
PAGE_OFFSET |
include/asm-arm/memeory.h +50 |
0xc0000000 |
内核空间的起始虚拟地址 |
TEXT_OFFSET |
arch/arm/Makefile +131 |
0x00008000 |
内核在RAM中起始位置相对于 RAM起始地址的偏移 |
TEXTADDR |
arch/arm/kernel/head.S +49 |
0xc0008000 |
kernel的起始虚拟地址 |
PHYS_OFFSET |
include/asm-arm/arch- *** /memory.h |
平台相关 |
RAM的起始物理地址,对于s3c2410来说在include/asm-arm/arch-s3c2410/memory.h下定义,值为0x30000000(ram接在片选6上) |