(一)kernel是怎样起来的-----head.S篇

一、前言

    由于本人知识能力有限,其实本篇文章大部分内容都是参照linuxer大神《ARM64的启动过程之(一):内核第一个脚印》系列文章码出来的,具体出处链接:http://www.wowotech.net/armv8a_arch/arm64_initialize_1.html,有兴趣的同学可以直接翻阅大神的系列文章,里面有更多更充分的细节表述。本人写这篇文章的目的,纯粹是为了给自己一次学习的总结,以免时间长了,将知识点忘记得一干二净。本篇文章基于linux kernel source 4.15-5版本的源码。相关的手册可以在我的下载资源中找到。

二、正题

    我们知道,在进入内核之前,有一个bootloader的过程,其最大的作用莫过于准备好各方面的CPU外围芯片,尤其是RAM,其外,也要将我们的内核镜像解压至(可选)内存中使之运行。更具体地,bootloader所要完成的事情,至少要包含以下几点:

        1、初始化系统中的RAM,并将RAM的相关信息传递给kernel,比如RAM的大小和分布。

        2、准备好device tree blob,完成某些节点的解析,并将DTB首地址传递给内核。

        3、解压内核。

       4、移交控制权给内核,对于ARM64 Linux来说,依据ARM64 boot protocol,在内核运行前,bootloader需要保证以下的几个条件:

                MMU = off, D-cache = off, I-cache = on or off 

                x0 = physical address to the FDT blob

    也就是说,在刚进入kernel的时候,MMU,D-cache是关闭的,这也能很好的解释在实际项目中所遇到的,在bootloader完成某些数据读写/加载功能时,其速度远远不如在BSP中执行的时候。而对于PPC处理器,bootloader执行相关功能实现的速度,明显比ARM中快,其MMU在CPU上电启动时默认开启了,大概是PPC的bootloader不需要在开启MMU之后,又将其关闭吧?留个疑问,需要确认一下。

        回到主题,根据vmlinux.lds.S文件的描述,内核执行的第一行代码是sections .text 中的_stext,即:

ENTRY(stext)
	bl	preserve_boot_args
	bl	el2_setup			// Drop to EL1, w0=cpu_boot_mode
	adrp	x23, __PHYS_OFFSET
	and	x23, x23, MIN_KIMG_ALIGN - 1	// KASLR offset, defaults to 0
	bl	set_cpu_boot_mode_flag
	bl	__create_page_tables
	/*
	 * The following calls CPU setup code, see arch/arm64/mm/proc.S for
	 * details.
	 * On return, the CPU will be ready for the MMU to be turned on and
	 * the TCR will have been set.
	 */
	bl	__cpu_setup			// initialise processor
	b	__primary_switch
ENDPROC(stext)

       这几个函数即为head.s的主要函数,依次执行,最后通过__primary_switch进入start_kernel函数开始C语言代码的执行。

    在细致分析之前,首先要明确,整个以上函数的主要目的,或者说任务是什么?由前面可以得知,此时MMU和D-cache均是关闭状态的,I-cache的状态则是依据具体的bootloader而定,那么,答案就比较明确了,即开启MMU和cache。如何开启MMU?很简单,不过是往CPU的某个寄存器写1罢了。但是,在这之前,我们需要做好充分的准备工作,让内核代码,能够顺畅地、欢快地在MMU on/off切换前后执行,也就是我们需要建立-----内核内存映射表,或者说页表。当然,该页表应该不是完整意义上我们在内核启动后所使用的那个页表,事实上,在我们进入c语言代码,也就是start_kernel函数之后,我们会再次进行页表项的建立,那个时候所使用的才是真正完整的。

(一)

preserve_boot_args
/*
 * Preserve the arguments passed by the bootloader in x0 .. x3
 */
preserve_boot_args:
	mov	x21, x0				// x21=FDT

	adr_l	x0, boot_args			// record the contents of
	stp	x21, x1, [x0]			// x0 .. x3 at kernel entry
	stp	x2, x3, [x0, #16]

	dmb	sy				// needed before dc ivac with
						// MMU off

	mov	x1, #0x20			// 4 x 8 bytes
	b	__inval_dcache_area		// tail call
ENDPROC(preserve_boot_args)

     该函数很简单,凭注释也很容易读懂,由于bootloader把设备树的首地址赋值给了通用寄存器x0,因此,x21现在保存着FDT(物理地址),用于后面的调用,同时也将x0寄存器腾了出来,以便后面的代码可以将x0作为通用寄存器使用。随后,分别将x21,x1,x2,x3的值保存到boot_args标签所代表的地址空间中,并使用dmb sy设置指令屏障。随后,调用__inval_dcache_area将该片内存中的cache使无效。为什么这么做?在bootloader中,很有可能已经对该片内存执行过一些操作了,而bootloader是会开启cache的,因此,在各层级的cache中可能会包含无效的数据,因此,很有必要将其invalue掉。此时x0保存着boot_args的首地址,x1保存着boot_args的大小,作为__inval_dcache_area的参数使用。

     此处直接黏贴大神的原话:为何要保存x0~x3这四个寄存器呢?因为ARM64 boot protocol对启动时候的x0~x3这四个寄存器有严格的限制:x0是dtb的物理地址,x1~x3必须是0(非零值是保留将来使用)。在后续setup_arch函数执行的时候会访问boot_args并进行校验。

/*
 * The recorded values of x0 .. x3 upon kernel entry.
 */
u64 __cacheline_aligned boot_args[4];

    解释一下adr_l这个宏,该宏的含义是将boot_args标签的物理地址赋值给了x0,即x0 = __pa(boot_args);adr_l宏最终会调用adrp指令,该指令的作用就是将符号地址变为运行时地址,由于此时MMU和cache都是关闭的,而boot_args的标签是虚拟地址(为什么是虚拟地址?事实上vmlinux.lds.S所定义的标签地址都是虚拟地址,因此这个时候,head.s里面的变量/标签所代表的均是虚拟地址),运行时地址,在目前的情况下也就是物理地址,因为MMU还没有打开。

    在MMU没有打开的情况下,我们不能直接访问虚拟地址,需要adrp这个指令来获取标签的物理地址。而adrp指令是page对齐的,显然boot_args无法保证这一条件(事实上,它应当是cache_line对齐的),因此ard_l的作用就体现出来了,它并不需要page对齐。

    至于dmb sy, ARM文档中,有关于数据访问指令和 data cache指令之间操作顺序的约定,原文如下:

All data cache instructions, other than DC ZVA, that specify an address can execute in any order relative to loads or stores that access any address with the Device memory attribute,or with Normal memory with Inner Non-cacheable attribute unless a DMB or DSB is executed between the instructions.

    即,在Inner Non-cacheable 的情况下,所有的data cache instructions执行之前, 除了DC ZVA(cache zeros by Virtual Address. This zeros a block of memory within the cache)之外,都要先执行DMB或者DSB,来保证stp等数据加载指令已经执行完毕。因此,在Non-cacheable的情况下,必须要使用DMB来保证stp指令在dc ivac指令之前执行完成。

(二) 

bl	el2_setup			// Drop to EL1, w0=cpu_boot_mode

    ARMv8中有exception level的概念,即EL0~EL3一共4个level。这个概念代替了以往的普通模式、特权模式的定义,也大致延续了ARMv7中的PL0、PL1、PL2的概念,各个level所代表的具体意义可以参见下图(盗图):

    即用户态所使用的Application处于特权的最低等级EL0,内

  • 2
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值