1. Linux内核启动第二阶段:
内核启动第二阶段主要完成的工作有,cpuID检查,machineID(也就是开发板ID)检查,创建初始化页表,设置C代码运行环境,跳转到内核第一个真正的C函数startkernel开始执行。
这一阶段涉及到两个重要的结构体:一个是structproc_info_list主要描述CPU相关的信息,定义在文件include/asm-arm/procinfo.h中,与其相关的函数及变量在文件arch/arm/mm/proc_arm920.S中被定义和赋值。另外一个更重要的结构体就是描述开发板或者说机器信息的结构体structmachine_desc,定义在include/asm-arm/mach/arch.h文件中,其函数的定义和变量的赋值在板极相关文件arch/arm/mach-s3c2410/mach-smdk2410.c中实现,这也是内核移植非常重要的一个文件。
Kernelstartup entry point.
该阶段一般由前面的解压缩代码调用
进入该阶段要求:MMU= off, D-cache = off, I-cache = dont care,
r0= 0, r1 = machine nr.
*This code is mostly position independent, so if you link the kernelat
*0xc0008000, you call this at __pa(0xc0008000).
所有的机器ID列表保存在arch/arm/tools/mach-types文件中,在编译时会将这些机器ID按照统一的格式链接到基本内核映像文件vmlinux的__arch_info_begin和__arch_info_end之间的段中。存储格式定义在include/asm-arm/mach/arch.h文件中的结构体structmachine_desc{}。这两个结构体的内容最终会被连接到基本内核映像vmlinux中的两个段内,分别是*(.proc.info.init)和*(.arch.info.init),可以参考下面的连接脚本。
链接脚本:arch/arm/kernel/vmlinux.lds
SECTIONS
{
. = TEXTADDR;
.init : { /*初始化代码段*/
_stext= .;
_sinittext = .;
*(.init.text)
_einittext = .;
__proc_info_begin= .;
*(.proc.info.init)
__proc_info_end= .;
__arch_info_begin= .;
*(.arch.info.init)
__arch_info_end= .;
__tagtable_begin= .;
*(.taglist.init)
__tagtable_end= .;
. = ALIGN(16);
__setup_start= .;
*(.init.setup)
__setup_end= .;
__early_begin= .;
*(.early_param.init)
__early_end= .;
__initcall_start= .;
*(.initcall1.init)
*(.initcall2.init)
*(.initcall3.init)
*(.initcall4.init)
*(.initcall5.init)
*(.initcall6.init)
*(.initcall7.init)
__initcall_end= .;
__con_initcall_start= .;
*(.con_initcall.init)
__con_initcall_end= .;
__security_initcall_start= .;
*(.security_initcall.init)
__security_initcall_end= .;
.= ALIGN(32);
__initramfs_start= .;
usr/built-in.o(.init.ramfs)
__initramfs_end= .;
.= ALIGN(64);
__per_cpu_start= .;
*(.data.percpu)
__per_cpu_end= .;
#ifndef CONFIG_XIP_KERNEL
__init_begin= _stext;
*(.init.data)
.= ALIGN(4096);
__init_end= .;
#endif
}
__INIT //与链接脚本中的初始化代码段标识同义,表示该代码段为初始化代码段
.type stext, %function
ENTRY(stext)
msrcpsr_c, #PSR_F_BIT | PSR_I_BIT | MODE_SVC
//进入超级权限模式,关中断
bl __lookup_processor_type @ r5=procinfo r9=cupid
//进行CPUID检查,并将CPUI相关的procinfo结构在物理地址空间的首地址保存在r10,
*********************************************************
/*从协处理器CP15,C0读取CPUID,然后在__proc_info_begin开始的段中进行查找,如果找到,则返回对应处理器相关结构体在物理地址空间的首地址到r5,最后保存在r10中。
由于此时还没有开启MMU,这里读取到的一些地址信息不是物理地址而是虚拟地址所以要进行相关的地址转换。
*Returns:
* r3, r4, r6 corrupted
* r5 = proc_info pointer in physical address space
* r9 = cpuid */
.type __lookup_processor_type, %function
__lookup_processor_type:
adr r3, 3f
//读取标号3位置的当前运行时地址,adr为相对寻址所以这里读取的是运行时地址
ldmda r3, {r5, r6, r9}
//加载对应标号的链接时地址到r5,r6,r9,因为r9和r3读取的位置相同,所以二者之间的差就是当前运行时地址与链接地址的真正偏移,然后给r5,r6加上这个偏移值,就实现了将链接时地址转换为当前运行时的地址
sub r3, r3,r9 @ get offset between virt&phys
add r5, r5,r3 @ convert virt addresses to
add r6, r6,r3 @ physical address space
//前面是读取相关段的地址,并将其转换为当前可用的运行时地址
mrc p15, 0, r9, c0, c0 //读取处理器ID
1: ldmia r5, {r3, r4}
//从procinfo中读取前两个变量的值,并与处理器ID进行比较,可参考下面的结构体
and r4, r4,r9 @ mask wanted bits
teq r3, r4
beq 2f //如果正确则返回,不正确则偏移到下一个procinfo结构进行查找
add r5, r5,#PROC_INFO_SZ @ sizeof(proc_info_list)
cmp r5, r6
blo 1b
mov r5,#0 @ unknown processor
2: mov pc, lr
/*include/asm-arm/procinfo.h
#define PROC_INFO_SZ 48
struct proc_info_list {
unsignedint cpu_val;
unsignedint cpu_mask;
后面内容省略
}; */
//这是一段C语言调用该函数的代码,将会在start_kernel()---àsetup_arch()
--àarch/arm/kernel/setuo.c文件的setup_processor()函数中调用
ENTRY(lookup_processor_type)
stmfd sp!, {r4 - r6, r9, lr}
bl __lookup_processor_type
mov r0, r5
ldmfd sp!, {r4 - r6, r9, pc}
/* Look ininclude/asm-arm/procinfo.h and arch/arm/kernel/arch.h for
* moreinformation about the __proc_info and __arch_info structures. */
.long __proc_info_begin -----àr5
.long __proc_info_end ----àr6
3: .long ----àr9
.long __arch_info_begin
.long __arch_info_end
**********************************************************
movs r10, r5 @ invalid processor (r5=0)?
beq __error_p @ yes, error 'p'
bl __lookup_machine_type @ r5=machinfo
//进行机器ID检查,并将其对应machine_desc结构的首地址保存在r8中。
*****************************************************************
/*机器ID是由u-boot引导内核是通过thekernel第二个参数传递进来的,现在保存在r1中,在__arch_info_begin开始的段中进行查找,如果找到,则返回machine对应相关结构体在物理地址空间的首地址到r5,最后保存在r8中。
由于此时还没有开启MMU,这里读取到的一些地址信息不是物理地址而是虚拟地址所以要进行相关的地址转换。
* linux/include/asm-arm/mach/arch.h
struct machine_desc {
unsignedint nr; architecture number
无关内容已删除
};
//从下面这个宏定义可以看出,machine_desc相关的内容被链接到段.arch.info.init中
#defineMACHINE_START(_type,_name) /
conststruct machine_desc __mach_desc_##_type /
__attribute__((__section__(".arch.info.init")))= { /
.nr =MACH_TYPE_##_type, /
.name = _name,
#defineMACHINE_END /
};
* 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
//读取标号3位置的当前运行时地址,adr为相对寻址所以这里读取的是运行时地址
ldmia r3, {r4, r5, r6}
//加载对应标号的链接时地址到r4,r5,r6,因为r4和r3读取的位置相同,所以二者之间的差就是当前运行时地址与链接地址的真正偏移,然后给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] //#defineMACHINFO_TYPE 0
//从arch_info中读取第一个变量的值,并与u-boot传递的机器ID进行比较,可参考上面的结构体
teq r3,r1 @ matches loader number?
beq 2f @ found
add r5, r5,#SIZEOF_MACHINE_DESC @ next machine_desc
// include/asm-arm/asm-offset.h
//#defineSIZEOF_MACHINE_DESC 56 /* sizeof(struct machine_desc) */
cmp r5, r6
blo 1b
mov r5,#0 @ unknown machine
2: mov pc, lr
//这是一段C语言调用该函数的代码,将会在start_kernel()---àsetup_arch()
--àarch/arm/kernel/setuo.c文件的setup_machine()函数中调用
ENTRY(lookup_machine_type)
stmfd sp!, {r4 - r6, lr}
mov r1, r0
bl __lookup_machine_type
mov r0, r5
ldmfd sp!, {r4 - r6, pc}
*****************************************************************
movs r8, r5 @ invalid machine (r5=0)?
beq __error_a @ yes, error 'a'
bl __create_page_tables //创建内核初始化页表
*****************************************************
创建内核初始化页表部分代码分析:
/* Setup the initial pagetables. We only setup the barest
*amount which are required to get the kernel running, which
*generally means mapping in the kernel code.
* r8 = machinfo
* r9 = cpuid
* r10= procinfo
*Returns:
* r0, r3, r5, r6, r7 corrupted
* r4 = physical page table address */
.type __create_page_tables, %function
__create_page_tables:
ldr r5, [r8,#MACHINFO_PHYSRAM] //#define MACHINFO_PHYSRAM 4
//r5=S3C2410_SDRAM_PA,,物理内存起始地址
pgtbl r4, r5 //r4=stext-0x4000=30008000-4000=0x30004000
/* .macro pgtbl, rd, phys
adr /rd, stext //stext即就是第二阶段的起始地址,因为这里采用伪指令相对寻址所以stext的地址为0x30008000.
sub /rd, /rd, #0x4000
.endm */
/*#define PROCINFO_MMUFLAGS 8
#define PROCINFO_INITFUNC 12
#define MACHINFO_TYPE 0
#defineMACHINFO_PHYSRAM 4
#defineMACHINFO_PHYSIO 8
#define MACHINFO_PGOFFIO 12
#defineMACHINFO_NAME 16 */
创建16KB一级交换页表
//清零0x30004000------0x30008000
mov r0, r4
mov r3, #0
add r6, r0, #0x4000 //r6为0x30008000也就是交换页表的上限
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
//一共映射4MB空间,足够内核启动使用即可,因为在内核启动第三阶段这些也表将被函数page_init删除。
//从proc_info段加载MMU标记到r7
ldr r7, [r10,#PROCINFO_MMUFLAGS] // #define PROCINFO_MMUFLAGS 8
mov r6, pc, lsr#20 //vmlinux起始段地址,pc的最高12位。这里为0x300
orr r3, r7, r6, lsl#20 //0x300<<20|MMU_flags-àr3即,r3为第一个页表描述符
str r3, [r4, r6,lsl #2] //将r3保存到0x3004c00=r6<<2=0x300<<2=0xc00+r4(页表起始地址),
/*设置kernel直接映射区域TEXTADDR=0xc0008000 */
add r0, r4, #(TEXTADDR & 0xff000000) >> 18 //0x30004000+((0xc0008000&0xff000000)>>18)=0x30004000+0x3000=0x30007000=r0
str r3, [r0,#(TEXTADDR & 0x00f00000) >> 18]!
//将r3存储到r0+((0xc0008000&0x00f00000)>>18)=0x30007000+0x3000=0x3000a000
add r3, r3, #1 <<20 //r3=r3+0x100000
str r3, [r0,#4]! // KERNEL + 1MB
add r3, r3, #1 <<20
str r3, [r0,#4]! @ KERNEL + 2MB
add r3, r3, #1 <<20
str r3, [r0,#4] @ KERNEL + 3MB
/*映射物理内存起始地址第一MB的内容,因为其中包括了u-boot传递给内核的参数*/
add r0, r4,#VIRT_OFFSET >> 18
orr r6, r5, r7//r5=物理内存地址,r7=MMU标记
str r6, [r0]
#ifdef CONFIG_XIP_KERNEL
无用代码,已删除
#endif
#ifdef CONFIG_DEBUG_LL //调试部分代码,此处不做分析
无用代码,已删除
#ifdefined(CONFIG_ARCH_NETWINDER) || defined(CONFIG_ARCH_CATS)
无用代码,已删除
#endif
#ifdef CONFIG_ARCH_RPC
无用代码,已删除
#endif
#endif
mov pc,lr
.ltorg
*****************************************************
-----------------------------------------------------------------------------------------------------------------
/*下面这段时CPU专用的代码,以位置无关的方式访问
r10= 由__lookup_machine_type找到的对应CPU的xxx_proc_info结构体的首地址
返回之后CPU将准备打开MMU
r0保存CPU控制寄存器的值。 */
/*include/asm-arm/procinfo.h
#define PROC_INFO_SZ 48
struct proc_info_list {
unsignedint cpu_val;
unsignedint cpu_mask;
unsignedlong __cpu_mmu_flags;
unsignedlong __cpu_flush;
constchar *arch_name;
constchar *elf_name;
unsignedint elf_hwcap;
constchar *cpu_name;
structprocessor *proc;
structcpu_tlb_fns *tlb;
structcpu_user_fns *user;
structcpu_cache_fns *cache;
};
//上述proc_info_list结构体对应于ARM920TCPU的实现部分,这里只是一部分,详细请参考文件: arch/arm/mm/proc_arm920.S
.align
.section ".proc.info.init",#alloc, #execinstr
.type __arm920_proc_info,#object
__arm920_proc_info:
.long 0x41009200
.long 0xff00fff0
.long PMD_TYPE_SECT | /
PMD_SECT_BUFFERABLE| /
PMD_SECT_CACHEABLE| /
PMD_BIT4| /
PMD_SECT_AP_WRITE| /
PMD_SECT_AP_READ
b __arm920_setup
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB
.long cpu_arm920_name
.long arm920_processor_functions
.long v4wbi_tlb_fns
.long v4wb_user_fns
#ifndefCONFIG_CPU_DCACHE_WRITETHROUGH
.long arm920_cache_fns
#else
.long v4wt_cache_fns
#endif
.size __arm920_proc_info, . - __arm920_proc_info
*/
ldr r13, __switch_data //设置MMU打开之后跳转到的地址
adr lr, __enable_mmu //
add pc, r10, #PROCINFO_INITFUNC //PROCINFO_INTFUNC=12
//跳转到对应CPU结构体的__cpu_flush函数执行实际上是执行arch/arm/mm/proc_arm920.S文件中的.proc_info_init段的基址+12位置的函数,也就是执行b __arm920_setup,如下:
*****************************************************************************
__INIT
.type __arm920_setup, #function
__arm920_setup:
mov r0, #0
mcr p15, 0, r0, c7,c7 @ invalidateI,D caches on v4
mcr p15, 0, r0, c7, c10,4 @drain write buffer on v4
mcr p15, 0, r0, c8,c7 @ invalidateI,D TLBs on v4
mrc p15, 0, r0, c1,c0 @ getcontrol register v4
ldr r5,arm920_cr1_clear
bic r0, r0, r5
ldr r5,arm920_cr1_set
orr r0, r0, r5
mov pc,lr //因为前面设置了lr=__enable_mmu ,所以开始执行启动MMU的代码。
.size __arm920_setup, . - __arm920_setup
.type arm920_cr1_clear, #object
.type arm920_cr1_set, #object
arm920_cr1_clear:
.word 0x3f3f
arm920_cr1_set:
.word 0x3135
*************************************************************************
使能MMU
使能MMU之前设置一些普通bit,装载页表地址以及域访问寄存器
.type __enable_mmu, %function
__enable_mmu:
#ifdef CONFIG_ALIGNMENT_TRAP
orr r0, r0, #CR_A //执行该段代码,其余的条件编译条件均未定义
#else
bic r0, r0, #CR_A
#endif
#ifdefCONFIG_CPU_DCACHE_DISABLE
bic r0, r0, #CR_C
#endif
#ifdefCONFIG_CPU_BPREDICT_DISABLE
bic r0, r0, #CR_Z
#endif
#ifdefCONFIG_CPU_ICACHE_DISABLE
bic r0, r0, #CR_I
#endif
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))
mcrp15, 0, r5, c3, c0, 0 //设置域访问寄存器c3
mcrp15, 0, r4, c2, c0, 0 //设置页表地址c2
b __turn_mmu_on
/*使能MMU,这将完全改变可见的内存空间,不能跟踪执行。
r0 = cp#15 control register
r13= *virtual* address to jump to 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 //前面设置的r13=__switch_data,跳转到__switch_data执行。如下面所示,__switch_data首地址存放的是__mmap_switched的地址,所以实际上是跳转到__mmap_switched执行,注意这里MMU已经开启,无需再进行虚拟地址和物理地址的手工转换,完全由MMU来完成。
-----------------------------------------------------------------------------------------------------------------
.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
下面这段代码执行时,MMU是开启的,是开启MMU之后执行的第一段代码,使用绝对地址访问方式,而且这段代码时位置相关的。
* r0 = cp#15 control register
* r1 = machine ID
* r9 = processor ID
.type __mmap_switched, %function
__mmap_switched:
adr r3, __switch_data + 4
ldmia r3!, {r4, r5, r6, r7}
//将r4位置的数据段搬移到r5开始的位置,实际上是将__data_loc数据段搬移到__data_start位置。
cmp r4,r5
1: cmpne r5, r6
ldrne fp, [r4], #4 //fp即就是寄存器r11àargumentpointer
strne fp, [r5], #4
bne 1b
//清零BSS段
movfp, #0
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ldmia r3, {r4, r5, r6, sp} //r3指向的是.long processor_id @ r4
str r9, [r4] //保存process_id到r4指向的位置
str r1, [r5] //保存__machine_arch_type到r5指向的位置,
bic r4, r0, #CR_A @ Clear 'A' bit
stmia r6, {r0, r4} @ Save control register values
b start_kernel //跳转到start_kernel函数开始执行内核启动第三阶段