Linux kernel启动流程第一阶段

 

head.S:kernel第一阶段入口

来自:http://blog.csdn.net/ooonebook/article/details/52710290

1safe_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:

 

2proccessor info的获取
cpu idprocinfo是一一对应的关系,所以可以通过cpu id来获取到对应的procinfo结构体。 
procinfo使用proc_info_list结构体,用来说明一个cpu的信息,包括这个cpuID号,对应的内核数据映射区的MMU标识等等。
注意: 
- proc_info_list结构体中存在MMU标识,也就是我们需要在打开MMU之前需要先获取procinfo的原因,因为打开MMU之前需要配置临时内核页表,而配置临时内核页表需要这里的MMU标识来进行设置。
 
对应代码:
1mrc p15, 0, r9, c0, c0   @ get processor id

解释:arm体系将CPUID(处理器标识符,主标识符)存放在协处理器cp15c0寄存器中。

2bl__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:

1dtb里面存放了各种硬件信息,如果dtb有问题,会导致后续开机过程中读取的设备信息有问题而导致无法开机。

2、在生成dtb的时候会在头部上添加一个幻数magic,而验证dtb是否合法主要也就是看这个dtbmagic是否和预期的值一致。其中magic是固定值:0xd00dfeed(大端)或者0xedfe0dd0(小端)我们只要提取待验证dtb的地址(Uboot传入的r2)上的数据的前四个字节,与0xd00dfeed(大端)或者0xedfe0dd0(小端)进行比较,如果匹配的话,就说明对应待验证dtb就是一个合法的dtb

代码注解:

Ldr r5, [r2, #0] @获取前4个字节放在r5

Ldrr6, =OF_DT_MAGIC @获取magic放在r6

5bl__create_page_tables(创建临时内核页表====>打开MMU)

实现代码:arch/arm/kernel/head.S

TIPS:

1MMUMemory 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)创建映射表

未懂

6b __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:

· 需要先将页表物理地址写入到cp15c2寄存器中

· 需要在cp15c3寄存器中写入位域相应的权限

· 配置cp15c1寄存器,用来控制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 IDmachine 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)上的值分别加载到r4r5r6r7寄存器中,__mmap_switched_data前面说明了

@ 经过上述动作,r4r5r6r7寄存器分别存放了如下值

@ 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)上的值分别加载到r4r5r6r7sp寄存器中,注意是前面r3已经加载过一部分了,地址和__mmap_switched_data已经不一样了

@ 经过上述动作,r4r5r6r7寄存器分别存放了如下值

@ r4 -> processor_id变量地址:其内容是cpu处理器ID

@ r5 -> __machine_arch_type变量地址:其内容是machine id

@ r6 -> __atags_pointer变量地址:其内容是dtb的地址

@ r7 -> cr_alignment变量地址:其内容是cp15c1的寄存器的值

@ 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

@ cp15c1的寄存器的值(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的位置,也就是要在前面预留出32KRAMkernel会从加载的位置上开始解压,而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
CACHECPU和内存之间的高速缓冲存储器,又分成数据缓冲器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上)