kernel之启动流程head.S

通过上一篇Makefile我们分析到了,编译出vmlinux的第一个原材料是head.o。.

先回顾下uboot是怎么启动kernel的。uboot将kernel从flash中拷贝到sdram后,设置tag进行工作交接,然后启动内核。
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
r0 : 0
r1 : bd->bi_arch_number 机器ID
r2 : bd->bi_boot_params tag

1.分析内存va和pa映射

我们的s3c2440的sdram起始地址是0x3000,0000。make完成后,我们分析下vmlinux.lds中的链接地址如下:

 . = (0xc0000000) + 0x00008000;

显而易见这不可能是物理地址,因为我们的物理地址空间没有这么大。

ARM Linux kernel将SDRAM的开始地址定义为PHYS_OFFSET。经bootloader加载kernel并由自解压部分代码运行后,最终kernel 被放置到KERNEL_RAM_PADDR(=PHYS_OFFSET + TEXT_OFFSET,即0x30008000)地址上的一段内存,经此放置后,kernel代码以后均不会被移动。

#define PHYS_OFFSET	UL(0x30000000)

在进入 kernel代码前,即bootloader和自解压缩阶段,ARM未开启MMU功能。因此kernel启动代码一个重要功能是设置好相应的页表,并开启 MMU功能。为了支持MMU功能,kernel镜像中的所有符号,包括代码段和数据段的符号,在链接时都生成了它在开启MMU时,所在物理内存地址映射到 的虚拟内存地址。

以arm kernel第一个符号(函数)stext为例,在编译链接,它生成的虚拟地址是0xc0008000,而放置它的物理地址为0x30008000(PHYS_OFFSET+TEXT_OFFSET)。实际上这个变换可以利用简单的公式进行表示:va = pa – PHYS_OFFSET + PAGE_OFFSET。Arm linux最终的kernel空间的页表,就是按照这个关系来建立。

2.uboot和kernel的机器ID进行匹配

开始分析head.S


bl	__lookup_machine_type		#查找支持的机器ID(uboot传进来的)
//跳转到 head_common.S分析这个函数
3:	.long	.
	.long	__arch_info_begin
	.long	__arch_info_end
__lookup_machine_type:
	adr	r3, 3b                //取上面3中 ‘.’的物理地址
	ldmia	r3, {r4, r5, r6}  //r4 = __arch_info_begin,r5 = __arch_info_end都是va
	sub	r3, r3, r4			  //得到物理地址和虚拟地址的偏差
	add	r5, r5, r3			  //得到__arch_info_begin物理地址
	add	r6, r6, r3			  //得到__arch_info_end物理地址

在执行1之前穿插下,得到__arch_info_begin和__arch_info_end物理地址里面到底有什么东东呢?
在arch/arm/kernel/vmlinux.lds中可以找到这两条。(注意vmlinux.lds里面的链接地址是虚拟地址

. = (0xc0000000) + 0x00008000; #lds中的是虚拟地址
 __arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;

搜索 arch.info.init,得到下面的内容:

__attribute的用法:https://my.oschina.net/u/180497/blog/177206

__attribute__((section("section_name")))
//其作用是将作用的函数或数据放入指定名为"section_name"输入段。
//利用 GCC 的 __attribute__ 属性的 section 选项来控制数据区的基地址。


define MACHINE_START(_type,_name)			\
static const struct machine_desc __mach_desc_##_type	\  //##在C/C++中具有连接字符串的作用
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	\  
	.nr		= MACH_TYPE_##_type,		\
	.name		= _name,

#define MACHINE_END				};

继续搜索MACHINE_START:就以2410为例:

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
//解析这个宏得到下面结构体:
static const struct machine_desc __mach_desc_SMDK2410	
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	
	.nr		= MACH_TYPE_SMDK2410,		/* 机器ID,#define MACH_TYPE_SMDK2410  193*/
	.name		= "SMDK2410",
        .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,
	};

看到这里再回去看head.S就明白了,我们首先找到__arch_info_begin和__arch_info_end的物理地址(lds中),这段地址里面存放了linux所支持的板卡的机器ID,name,中断初始化等函数。下面必然要做的就是 uboot传入的机器ID和vmlinux.lds中__arch_info_begin段所支持的板卡类型的机器ID进行match。我们继续往下看head_common.S:

1:	ldr	r3, [r5, #MACHINFO_TYPE]	@ get machine type
	teq	r3, r1				//r3是段开始地址,R1是uboot传入的机器ID,开始比对
	beq	2f				   //机器ID相等,蹦到2
	add	r5, r5, #SIZEOF_MACHINE_DESC	//没有找到,蹦到下一个,继续查找
	cmp	r5, r6
	blo	1b
	mov	r5, #0				@ unknown machine
2:	mov	pc, lr             // 把lr赋给PC,跳出去。

3.创建临时页表

          kernel里面的所有符号在链接时,都使用了虚拟地址值。在完成基本的初始化后,kernel代码将跳到第一个C语言函数start_kernl来 执行,在哪个时候,这些虚拟地址必须能够对它所存放在真正内存位置,否则运行将为出错。为此,CPU必须开启MMU,但在开启MMU前,必须为虚拟地址到 物理地址的映射建立相应的面表。在开启MMU后,kernel指并不马上将PC值指向start_kernl,而是要做一些C语言运行期的设置,如堆栈, 重定义等工作后才跳到start_kernel去执行。在此过程中,PC值还是物理地址,因此还需要为这段内存空间建立va = pa的内存映射关系。当然,本函数建立的所有页表都会在将来paging_init销毁再重建,这是临时过度性的映射关系和页表。

前几篇文章讲了MMU工作机制。我们需要在sdram中建立页表,然后把页表的地址告诉给CP15的C2寄存器地址转换表基地址)。

bl	__create_page_tables
//跳转  __create_page_tables 函数执行
__create_page_tables:
	pgtbl	r4				//页表基地址
/*分析下 pgtbl 的由来
.macro	pgtbl, rd
ldr	\rd, =(KERNEL_RAM_PADDR - 0x4000)
.endm
*/

/*第一步*******************0x4000大小为16Kb,将此16K 清零***************************/
	mov	r0, r4
	mov	r3, #0
	add	r6, r0, #0x4000
1:	str	r3, [r0], #4  //将r3中的值存到r0所指定的地址中, 同时r0=r0+4
	str	r3, [r0], #4
	str	r3, [r0], #4
	str	r3, [r0], #4
	teq	r0, r6        //比较是否到了16kB
	bne	1b            //16KB没有清0结束,继续清


/*第二步*******************建立恒等映射********************************/
ldr	r7, [r10, #PROCINFO_MM_MMUFLAGS]   //r10指向开发板相应的proc_info元素,读出mmuflags到r7
// pc 表示当前代码运行的物理地址。以1M的方式对齐。当前映射的1M空间位为va == pa
/************************************************                                
*|31|.............|20|..................... |1|0*
                    1 0 0 0 0 0 0 0... 0 0 0 0 0
                    |           sector         |
*************************************************/
mov	r6, pc, lsr #20			// r6 = (pc >> 20).start of kernel section 
orr	r3, r7, r6, lsl #20		// r3 = r7 |(r6 <<< 20).flags + kernel base 1M空间清零
str	r3, [r4, r6, lsl #2]    // R3-> (r4+(r6<<2)) ,将目录内容写入到对应页目录中
/*

/*第3步*******************kernel空间的映射***************************/
	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
	strls	r3, [r0], #4
	bls	1b


/*第4步*******************进行boot传入的1MB的params映射***************************/
	/*
	 * 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]

4.开启MMU

将页表的基地址传给协处理器cp15的C2寄存器,然后设置一些cache,开MMU操作。

__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

5.__switch_data

__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


__mmap_switched:
	adr	r3, __switch_data + 4     //地址+4

	ldmia	r3!, {r4, r5, r6, r7}  //将r4->r3地址中(__data_loc),r5->r3+4,r6->r3+8.
	cmp	r4, r5				//比较__data_start和__data_loc是否相等,是否需要重定位
1:	cmpne	r5, r6         //不相等开始copy
	ldrne	fp, [r4], #4
	strne	fp, [r5], #4
	bne	1b

	mov	fp, #0				//Clear BSS (and zero fp).BSS段清0.
1:	cmp	r6, r7
	strcc	fp, [r6],#4
	bcc	1b
 
//设置 ID,栈指针。
	ldmia	r3, {r4, r5, r6, sp}
	str	r9, [r4]			@ Save processor ID
	str	r1, [r5]			@ Save machine type
	bic	r4, r0, #CR_A			@ Clear 'A' bit
	stmia	r6, {r0, r4}			@ Save control register values
//跳转执行 c函数
	b	start_kernel

部分参考:

https://blog.csdn.net/sfrysh/article/details/7226912#

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值