carloscn/blog

0x01_LinuxKernel_内核的启动(一)之启动前准备

carloscn opened this issue · 0 comments

0x01_Linux内核的启动(一)之启动前准备

在广州市图书馆偶然间看到尹锡训 -《ARM Linux内核源码剖析》(코드로 알아보는 ARM 리눅스 커널)1,这个书我觉得非常适合我现在看。本书涉及Linux内核启动和处理,紧密和ARM架构机制相结合,一方面可以加深对ARM硬件机制的理解,另一方面能够科普Linux内核内设机制,作为Linux内核机制的入门,相当于从ARM迈入Linux的桥梁。本书需要结合ARM手册和Linux内核两本书同时参考。

1. 内核构建系统

操作系统必然是一个非常庞大和复杂的系统2。由调度程序、文件系统、内存管理、网络系统等诸多子系统组成。这种内核十分庞大,但是其生成只需要一个二进制启动文件(zImage/bzImage)。可以将zImage文件就视为内核elf文件

1.1 内核初始化和配置

内核初始化状态是从kernel.org下载的tar.gz内核源程序压缩包,解压之后为内核源码的代码树,这种状态叫做内核的初始状态

使用make mrpropermake distclean等指令可以内核源码恢复到内核初始状态。注意:

  • 执行mrproper编译指令,只清除.config文件在内的为内核编译级链接而生成的诸多设置文件。
  • 执行distclean编译指令,清除内核编译后生成的所有对象文件、备份文件。

内核在初始状态不可以直接编译,虽然可以生成vmlinux(没有压缩的内核文件3),但大部分情况会引起内核严重错误(kernel panic)。因此,需要执行最重要、最需要谨慎处理的**内核配置(kernel configuration)**的过程,也是生成编译必要.config文件的过程。注意:

  • xconfig:基于Qt的前端的配置
  • menuconfig: 基于终端的menu配置
  • gconfig:基于GTK的前端的配置

kernel的配置的单位是 SoC级的

1.2 内核的构建和安装

生成.config文件之后,就可以开始构建内核(kernel building),相当于从kernel源代码 -> zImage的过程。如果從kernel編譯過程來看vmlinux是如何組成的話4

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j4
(....)
  LD      vmlinux
  SORTEX  vmlinux
  SYSMAP  System.map
  OBJCOPY arch/arm/boot/Image
  Kernel: arch/arm/boot/Image is ready
  Kernel: arch/arm/boot/Image is ready
  LDS     arch/arm/boot/compressed/vmlinux.lds
  AS      arch/arm/boot/compressed/head.o
  GZIP    arch/arm/boot/compressed/piggy.gzip
  CC      arch/arm/boot/compressed/misc.o
  CC      arch/arm/boot/compressed/decompress.o
  CC      arch/arm/boot/compressed/string.o
  AS      arch/arm/boot/compressed/lib1funcs.o
  AS      arch/arm/boot/compressed/ashldi3.o
  AS      arch/arm/boot/compressed/bswapsdi2.o
  AS      arch/arm/boot/compressed/piggy.gzip.o
  LD      arch/arm/boot/compressed/vmlinux
  OBJCOPY arch/arm/boot/zImage
  Kernel: arch/arm/boot/zImage is ready

這裡列出比較重要的幾個:

檔案 功能 RPi source
vmlinux vmlinux是ELF格式binary檔案,為最原始也未壓縮的kernel鏡像。 ./vmlinux
System.map 在符號名稱與它們的記憶體位置間的查詢表格 ./System.map
Image vmlinux經過objcopy處理,把代碼從中抽出(去除註解或debugging symbols)以用於形成可執行的機器碼。不過Image此時還不能直接執行,需加入metadata資訊。 ./arch/arm/boot/Image
head.o ARM特有的code,用來接受從bootloader送來的系統控制權,source code head.S是用組語(arm-assembly)寫成。 ./arch/arm/boot/compressed/head.S
piggy.gzip 被gzip壓縮的Image ./arch/arm/boot/compressed/piggy.gzip
piggy.gzip.o 用組語寫成,可被用來連結到別的物件,例如piggy.gzip ./arch/arm/boot/compressed/piggy.gzip.S
misc.o 用來解壓縮。 ./arch/arm/boot/compressed/misc.c
compressed/vmlinux 結合System.map等檔案並產生鏡像檔,意義跟一開始的vmlinux不太一樣。 ./arch/arm/boot/compressed/vmlinux
zImage 最後產生的鏡像檔,已經被壓縮過。 ./arch/arm/boot/zImage

vmlinux是ELF格式的二进制映像,由文件系统、网络、设备驱动及调度程序组成,是zimage之前的原始文件,构建内核过程均需要各个部件通过linker组合成一个名为vmlinux的较大的ELF格式对象文件。图源于链接5

image-20220705123358771

编译后的内核会生成ELF格式的二进制文件vmlinux,具备内核的所有要素。vmlinux通过gzip压缩成为piggy.o和head.o, misc.o执行链接,最后生成zImage二进制文件。内核的内存加载及执行都能通过zImage提高速度,特别是head.o和misc.o相当于引导程序加载项。

内核最后安装生成与/boot文件夹下

2. 解压内核 decompressed kernel (ARMv7 only)

2.1 ARMv8

在ARMv8中已经drop掉了关于对Linux内核解压的部分,实际上可以在arch/arm64/boot/的下面看不到了compressed/head.S的身影了。这部分原因我们可以在booting arch64里面可以看到说明6

3. Decompress the kernel image
------------------------------

Requirement: OPTIONAL

The AArch64 kernel does not currently provide a decompressor and
therefore requires decompression (gzip etc.) to be performed by the boot
loader if a compressed Image target (e.g. Image.gz) is used.  For
bootloaders that do not implement this requirement, the uncompressed
Image target is available instead.

基本的意思是在arm64内核的下面不再默认提供解压工具,如果有需要需要在boot组件中实现。那么相应的,在AArch64 kernel image decompression的一封信件中,可以找到uboot的确是对aarch64这块重新做了支持7。更多的工程师也倾向于拿掉内核的压缩和解压部分,甚至对uboot的uImage压缩提出了挑战设计。笔者认为,在arch64这样的高性能架构,已经无需在对vmlinux的大小做太多的考虑,可能只有在arch64一些size sensitive的情境下面才需要进行压缩。

而且,在linux kernel里面存在两个head.S也十分让人费解。在ARMv7的手册里还对这部分做了解释8,arch/arm/boot/compressed/head.S和arch/arm/kernel/head.S,前者主要是对在early-boot阶段对zImage的解压,对vmlinux的还原。的确,去掉更好,更有助于消除内核歧义。

2.2 ARMv7

内核实际的启动节点是start_kernel()函数,在这个调用中再去调用100多个子函数执行linux的启动。但是调用start_kernel之前,也有一部分工作要处理:

  • 必须对zImage进行解压
  • 完成页目录构建(处理器列表、MMU目录建立、激活MMU)
  • 检查处理器信息(atag信息有消息)

本章以ARMv7和ARMv8架构为例子,研究Linux内核启动前准备。

2.2.1 zImage pre-decompressed

zImage在开始前,需要对处理器进行一些初始化操作,为解压做一些准备。这个过程包含:中断禁止、分配动态内存、初始化BSS区、初始化页目录、打开cache等任务。然后后面会为解压数据进行空间数据(页目录空间)。

2.2.1.1 start标签

通过启动加载项完成对软硬件的默认初始化任务之后,最先执行的是arch/arm/boot/compressed/head.S,代码如下arch/arm/boot/compressed/head.S 。start标签是第一个执行的代码,在start标签中,从启动加载项接手ID和atags的信息。此外,还会禁用中断及初始化寄存器,并跳转到not_relocated标签(用于初始化bss)。

LCO -> r1
__bss_start -> r2
_end -> r3
zreladdr -> r4
_start -> r5
_got_start ->r6
_got_end -> ip
user_stack + 4096 -> sp

2.2.1.2 bss系统域初始化- not_relocated标签

not_relocated:	mov	r0, #0
1:		str	r0, [r2], #4		@ clear bss
		str	r0, [r2], #4
		str	r0, [r2], #4
		str	r0, [r2], #4
		cmp	r2, r3
		blo	1b

		/*
		 * Did we skip the cache setup earlier?
		 * That is indicated by the LSB in r4.
		 * Do it now if so.
		 */
		tst	r4, #1
		bic	r4, r4, #1
		blne	cache_on @将用于内核的动态内存空间起始地址

2.2.1.3 激活缓存-cache_on

如果已经完成了bss区域的初始化和动态区域的设置,解压内核的最后准备就是执行cache_on。Linux中的cacheon的实现方式根据arm的机构版本不同是不同的。在cache_on标签中查找并调用当前系统ARM结构版本相符程序的子程序,另外,调用__setup_mmu子程序对页目录进行初始化。

/*
 * Turn on the cache.  We need to setup some page tables so that we
 * can have both the I and D caches on.
 *
 * We place the page tables 16k down from the kernel execution address,
 * and we hope that nothing else is using it.  If we're using it, we
 * will go pop!
 *
 * On entry,
 *  r4 = kernel execution address
 *  r7 = architecture number
 *  r8 = atags pointer
 * On exit,
 *  r0, r1, r2, r3, r9, r10, r12 corrupted
 * This routine must preserve:
 *  r4, r7, r8
 */
		.align	5
cache_on:	mov	r3, #8			@ cache_on function
		b	call_cache_fn

/*
 * Initialize the highest priority protection region, PR7
 * to cover all 32bit address and cacheable and bufferable.
 */
__armv4_mpu_cache_on:
		mov	r0, #0x3f		@ 4G, the whole
		mcr	p15, 0, r0, c6, c7, 0	@ PR7 Area Setting
		mcr 	p15, 0, r0, c6, c7, 1

		mov	r0, #0x80		@ PR7
		mcr	p15, 0, r0, c2, c0, 0	@ D-cache on
		mcr	p15, 0, r0, c2, c0, 1	@ I-cache on
		mcr	p15, 0, r0, c3, c0, 0	@ write-buffer on

		mov	r0, #0xc000
		mcr	p15, 0, r0, c5, c0, 1	@ I-access permission
		mcr	p15, 0, r0, c5, c0, 0	@ D-access permission

		mov	r0, #0
		mcr	p15, 0, r0, c7, c10, 4	@ drain write buffer
		mcr	p15, 0, r0, c7, c5, 0	@ flush(inval) I-Cache
		mcr	p15, 0, r0, c7, c6, 0	@ flush(inval) D-Cache
		mrc	p15, 0, r0, c1, c0, 0	@ read control reg
						@ ...I .... ..D. WC.M
		orr	r0, r0, #0x002d		@ .... .... ..1. 11.1
		orr	r0, r0, #0x1000		@ ...1 .... .... ....

		mcr	p15, 0, r0, c1, c0, 0	@ write control reg

		mov	r0, #0
		mcr	p15, 0, r0, c7, c5, 0	@ flush(inval) I-Cache
		mcr	p15, 0, r0, c7, c6, 0	@ flush(inval) D-Cache
		mov	pc, lr

__armv3_mpu_cache_on:
		mov	r0, #0x3f		@ 4G, the whole
		mcr	p15, 0, r0, c6, c7, 0	@ PR7 Area Setting

		mov	r0, #0x80		@ PR7
		mcr	p15, 0, r0, c2, c0, 0	@ cache on
		mcr	p15, 0, r0, c3, c0, 0	@ write-buffer on

		mov	r0, #0xc000
		mcr	p15, 0, r0, c5, c0, 0	@ access permission

		mov	r0, #0
		mcr	p15, 0, r0, c7, c0, 0	@ invalidate whole cache v3
		/*
		 * ?? ARMv3 MMU does not allow reading the control register,
		 * does this really work on ARMv3 MPU?
		 */
		mrc	p15, 0, r0, c1, c0, 0	@ read control reg
						@ .... .... .... WC.M
		orr	r0, r0, #0x000d		@ .... .... .... 11.1
		/* ?? this overwrites the value constructed above? */
		mov	r0, #0
		mcr	p15, 0, r0, c1, c0, 0	@ write control reg

		/* ?? invalidate for the second time? */
		mcr	p15, 0, r0, c7, c0, 0	@ invalidate whole cache v3
		mov	pc, lr

#ifdef CONFIG_CPU_DCACHE_WRITETHROUGH
#define CB_BITS 0x08
#else
#define CB_BITS 0x0c
#endif

2.2.1.4 页表项初始化 - __setup_mmu

__setup_mmu标签在cache_on标签内部调用,用于初始化解压内核的页表项。特别是对内存的256MB区域设置cacheable, bufferable,这是因为解压内核的时候,使用cache和writebuffer提高解压性能。

image-20220705150241288

__setup_mmu:	sub	r3, r4, #16384		@ Page directory size
		bic	r3, r3, #0xff		@ Align the pointer
		bic	r3, r3, #0x3f00
/*
 * Initialise the page tables, turning on the cacheable and bufferable
 * bits for the RAM area only.
 */
		mov	r0, r3
		mov	r9, r0, lsr #18
		mov	r9, r9, lsl #18		@ start of RAM
		add	r10, r9, #0x10000000	@ a reasonable RAM size
		mov	r1, #0x12		@ XN|U + section mapping
		orr	r1, r1, #3 << 10	@ AP=11
		add	r2, r3, #16384
1:		cmp	r1, r9			@ if virt > start of RAM
		cmphs	r10, r1			@   && end of RAM > virt
		bic	r1, r1, #0x1c		@ clear XN|U + C + B
		orrlo	r1, r1, #0x10		@ Set XN|U for non-RAM
		orrhs	r1, r1, r6		@ set RAM section settings
		str	r1, [r0], #4		@ 1:1 mapping
		add	r1, r1, #1048576
		teq	r0, r2
		bne	1b
/*
 * If ever we are running from Flash, then we surely want the cache
 * to be enabled also for our execution instance...  We map 2MB of it
 * so there is no map overlap problem for up to 1 MB compressed kernel.
 * If the execution is in RAM then we would only be duplicating the above.
 */
		orr	r1, r6, #0x04		@ ensure B is set for this
		orr	r1, r1, #3 << 10
		mov	r2, pc
		mov	r2, r2, lsr #20
		orr	r1, r1, r2, lsl #20
		add	r0, r3, r2, lsl #2
		str	r1, [r0], #4
		add	r1, r1, #1048576
		str	r1, [r0]
		mov	pc, lr
ENDPROC(__setup_mmu)

@ Enable unaligned access on v6, to allow better code generation
@ for the decompressor C code:
__armv6_mmu_cache_on:
		mrc	p15, 0, r0, c1, c0, 0	@ read SCTLR
		bic	r0, r0, #2		@ A (no unaligned access fault)
		orr	r0, r0, #1 << 22	@ U (v6 unaligned access model)
		mcr	p15, 0, r0, c1, c0, 0	@ write SCTLR
		b	__armv4_mmu_cache_on

__arm926ejs_mmu_cache_on:
#ifdef CONFIG_CPU_DCACHE_WRITETHROUGH
		mov	r0, #4			@ put dcache in WT mode
		mcr	p15, 7, r0, c15, c0, 0
#endif

__armv4_mmu_cache_on:
		mov	r12, lr
#ifdef CONFIG_MMU
		mov	r6, #CB_BITS | 0x12	@ U
		bl	__setup_mmu
		mov	r0, #0
		mcr	p15, 0, r0, c7, c10, 4	@ drain write buffer
		mcr	p15, 0, r0, c8, c7, 0	@ flush I,D TLBs
		mrc	p15, 0, r0, c1, c0, 0	@ read control reg
		orr	r0, r0, #0x5000		@ I-cache enable, RR cache replacement
		orr	r0, r0, #0x0030
 ARM_BE8(	orr	r0, r0, #1 << 25 )	@ big-endian page tables
		bl	__common_mmu_cache_on
		mov	r0, #0
		mcr	p15, 0, r0, c8, c7, 0	@ flush I,D TLBs
#endif
		mov	pc, r12

__armv7_mmu_cache_on:
		enable_cp15_barriers	r11
		mov	r12, lr
#ifdef CONFIG_MMU
		mrc	p15, 0, r11, c0, c1, 4	@ read ID_MMFR0
		tst	r11, #0xf		@ VMSA
		movne	r6, #CB_BITS | 0x02	@ !XN
		blne	__setup_mmu
		mov	r0, #0
		mcr	p15, 0, r0, c7, c10, 4	@ drain write buffer
		tst	r11, #0xf		@ VMSA
		mcrne	p15, 0, r0, c8, c7, 0	@ flush I,D TLBs
#endif
		mrc	p15, 0, r0, c1, c0, 0	@ read control reg
		bic	r0, r0, #1 << 28	@ clear SCTLR.TRE
		orr	r0, r0, #0x5000		@ I-cache enable, RR cache replacement
		orr	r0, r0, #0x003c		@ write buffer
		bic	r0, r0, #2		@ A (no unaligned access fault)
		orr	r0, r0, #1 << 22	@ U (v6 unaligned access model)
						@ (needed for ARM1176)
#ifdef CONFIG_MMU
 ARM_BE8(	orr	r0, r0, #1 << 25 )	@ big-endian page tables
		mrcne   p15, 0, r6, c2, c0, 2   @ read ttb control reg
		orrne	r0, r0, #1		@ MMU enabled
		movne	r1, #0xfffffffd		@ domain 0 = client
		bic     r6, r6, #1 << 31        @ 32-bit translation system
		bic     r6, r6, #(7 << 0) | (1 << 4)	@ use only ttbr0
		mcrne	p15, 0, r3, c2, c0, 0	@ load page table pointer
		mcrne	p15, 0, r1, c3, c0, 0	@ load domain access control
		mcrne   p15, 0, r6, c2, c0, 2   @ load ttb control
#endif
		mcr	p15, 0, r0, c7, c5, 4	@ ISB
		mcr	p15, 0, r0, c1, c0, 0	@ load control register
		mrc	p15, 0, r0, c1, c0, 0	@ and read it back
		mov	r0, #0
		mcr	p15, 0, r0, c7, c5, 4	@ ISB
		mov	pc, r12

__fa526_cache_on:
		mov	r12, lr
		mov	r6, #CB_BITS | 0x12	@ U
		bl	__setup_mmu
		mov	r0, #0
		mcr	p15, 0, r0, c7, c7, 0	@ Invalidate whole cache
		mcr	p15, 0, r0, c7, c10, 4	@ drain write buffer
		mcr	p15, 0, r0, c8, c7, 0	@ flush UTLB
		mrc	p15, 0, r0, c1, c0, 0	@ read control reg
		orr	r0, r0, #0x1000		@ I-cache enable
		bl	__common_mmu_cache_on
		mov	r0, #0
		mcr	p15, 0, r0, c8, c7, 0	@ flush UTLB
		mov	pc, r12

__common_mmu_cache_on:
#ifndef CONFIG_THUMB2_KERNEL
#ifndef DEBUG
		orr	r0, r0, #0x000d		@ Write buffer, mmu
#endif
		mov	r1, #-1
		mcr	p15, 0, r3, c2, c0, 0	@ load page table pointer
		mcr	p15, 0, r1, c3, c0, 0	@ load domain access control
		b	1f
		.align	5			@ cache line aligned
1:		mcr	p15, 0, r0, c1, c0, 0	@ load control register
		mrc	p15, 0, r0, c1, c0, 0	@ and read it back to
		sub	pc, lr, r0, lsr #32	@ properly flush pipeline
#endif

3. 从zImage还原vmlinux(ARMv7 only)

Linux内核之前通过not_relocated和cache_on做好解压之前的准备工作,并且malloc了空间,做好了对vmlinux还原的准备。本节通过gunzip对压缩后的内核zImage执行解压,并调用与当前处理器的ARM结构对应的cache函数。最终将PC移动到zImage解压的位置,使解压之后的内核能够执行。

Note, gunzip位于内核源码arch/arm/boot/compressed/misc.c中。

3.1 wont_overwrite

wont_overwrite在zImage准备解压的最后一项工作,在这个步骤里面,上一个阶段的需要传递参数过来。这里面有个知识点,CONFIG_ZBOOT_ROM9。这项配置指示压缩的bootloader是不是在ROM/flash当中,根据文献10,分为了两种模式,一种是PIC模式和ROM模式,PIC模式只有在RAM里面有相应的程序,load内核之后可能会把这部分程序覆盖;而ROM模式,将RAM和ROM地址分开,load内核之后不会将这部分程序覆盖。使用PIC模式直接CONFIG_ZBOOT_ROM=n即可。

Compressed boot loader in ROM/flash11

configname: CONFIG_ZBOOT_ROM

Linux Kernel Configuration

└─> Boot options

└─> Compressed boot loader in ROM/flash

Say Y here if you intend to execute your compressed kernel image
(zImage) directly from ROM or flash. If unsure, say N.

wont_overwrite:
/*
 * If delta is zero, we are running at the address we were linked at.
 *   r0  = delta
 *   r2  = BSS start
 *   r3  = BSS end
 *   r4  = kernel execution address (possibly with LSB set)
 *   r5  = appended dtb size (0 if not present)
 *   r7  = architecture ID
 *   r8  = atags pointer
 *   r11 = GOT start
 *   r12 = GOT end
 *   sp  = stack pointer
 */
		orrs	r1, r0, r5
		beq	not_relocated

		add	r11, r11, r0
		add	r12, r12, r0

#ifndef CONFIG_ZBOOT_ROM
		/*
		 * If we're running fully PIC === CONFIG_ZBOOT_ROM = n,
		 * we need to fix up pointers into the BSS region.
		 * Note that the stack pointer has already been fixed up.
		 */
		add	r2, r2, r0
		add	r3, r3, r0

		/*
		 * Relocate all entries in the GOT table.
		 * Bump bss entries to _edata + dtb size
		 */
1:		ldr	r1, [r11, #0]		@ relocate entries in the GOT
		add	r1, r1, r0		@ This fixes up C references
		cmp	r1, r2			@ if entry >= bss_start &&
		cmphs	r3, r1			@       bss_end > entry
		addhi	r1, r1, r5		@    entry += dtb size
		str	r1, [r11], #4		@ next entry
		cmp	r11, r12
		blo	1b

		/* bump our bss pointers too */
		add	r2, r2, r5
		add	r3, r3, r5

#else

		/*
		 * Relocate entries in the GOT table.  We only relocate
		 * the entries that are outside the (relocated) BSS region.
		 */
1:		ldr	r1, [r11, #0]		@ relocate entries in the GOT
		cmp	r1, r2			@ entry < bss_start ||
		cmphs	r3, r1			@ _end < entry
		addlo	r1, r1, r0		@ table.  This fixes up the
		str	r1, [r11], #4		@ C references.
		cmp	r11, r12
		blo	1b
#endif

要注意到,这部分程序的时候对GOT全局表进行了展开。

decompress_kernel是解压zImage的子程序,这样引导加载项(bootstrap loader)的所有操作基本结束,跳转到call kernel标签执行内核代码初始化操作。查看代码:https://elixir.bootlin.com/linux/v2.6.30.4/source/arch/arm/boot/compressed/head.S#L537

call_kernel:	
        bl	cache_clean_flush
		bl	cache_off
		mov	r0, #0			@ must be zero
		mov	r1, r7			@ restore architecture number
		mov	r2, r8			@ restore atags pointer
		mov	pc, r4			@ call kernel

这部分已经在linux2.6.30+中去掉了。banch target cache是流水线中的指令cache,清除btc实际上是刷流水线。

第一步,是cache失效并刷回到ram里面;

第二步,关闭开始mmu,使cache和tlb失效,清除btc(branch target cache,分支目标缓存)。

第三部**,控制权移动到arch/arm/kernel/head.S中,正式执行内核初始化,并且把结构ID和atags指针作为传递参数。**

对于__cache_off,在linux这样处理:

__armv4_mmu_cache_off:
		mrc	p15, 0, r0, c1, c0  @ 将control reg复制到r0
		bic	r0, r0, #0x000d     @ 清空0x000d -> 1101 0,2,3 bit位 
		mcr	p15, 0, r0, c1, c0	@ turn MMU and cache off
		mov	r0, #0
		mcr	p15, 0, r0, c7, c7	@ invalidate whole cache v4
		mcr	p15, 0, r0, c8, c7	@ invalidate whole TLB v4
		mov	pc, lr

真正意义的cache off还需要invalidate所有的cache和TLB。

4. 调用start_kernel(ARMv7/v8)

这个部分是调用start_kernel()函数的最后阶段。正式启动内核之前,先检查自身处理器的信息和机器信息的结构体的位置,确认从启动加载项接受的atag信息是不是有效。ARMv7和ARMv8有着不同的流程,我们需要在这里开始做出ARMv7和ARMv8两个处理器的分支划分。

4.1 ARMv7

如果自身处理器信息和机器有误,程序就立即结束。在检查完所有信息合法性的基础上,设置页表的MMU标识并激活,最终调用C代码编成start_kernel()内核起始函数。

4.1.1 初始化指向 --- stext标签

通过引导加载项(该程序负责将压缩的内核加载到内存,并执行解压等内核启动所需的操作)加载内核之后,首先执行的部分就是stext。执行该标签时要求如下状态。

  • MMU = off
  • D-Cache = off
  • r0 = 0
  • r1 = machine number
  • r2 = atags pointer

在进入stext标签时候,首先转换armv7需要转换到SVC模式(SVC_MODE),并禁止IRQ。然后调用合法性检查程序,主要针对于CPU和平台信息,并检查atag信息,追加设置页表之后启动MMU。

/*
 * Kernel startup entry point.
 * ---------------------------
 *
 * This is normally called from the decompressor code.  The requirements
 * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
 * r1 = machine nr, r2 = atags pointer.
 *
 * This code is mostly position independent, so if you link the kernel at
 * 0xc0008000, you call this at __pa(0xc0008000).
 *
 * See linux/arch/arm/tools/mach-types for the complete list of machine
 * numbers for r1.
 *
 * We're trying to keep crap to a minimum; DO NOT add any machine specific
 * crap here - that's what the boot loader (or in extreme, well justified
 * circumstances, zImage) is for.
 */
	.section ".text.head", "ax"
ENTRY(stext)
	msr	cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
						@ and irqs disabled
	mrc	p15, 0, r9, c0, c0		@ get processor id
	bl	__lookup_processor_type		@ r5=procinfo r9=cpuid
	movs	r10, r5				@ invalid processor (r5=0)?
	beq	__error_p			@ yes, error 'p'
	bl	__lookup_machine_type		@ r5=machinfo
	movs	r8, r5				@ invalid machine (r5=0)?
	beq	__error_a			@ yes, error 'a'
	bl	__vet_atags
	bl	__create_page_tables

	/*
	 * The following calls CPU specific code in a position independent
	 * manner.  See arch/arm/mm/proc-*.S for details.  r10 = base of
	 * xxx_proc_info structure selected by __lookup_machine_type
	 * above.  On return, the CPU will be ready for the MMU to be
	 * turned on, and r0 will hold the CPU control register value.
	 */
	ldr	r13, __switch_data		@ address to jump to after
						@ mmu has been enabled
	adr	lr, __enable_mmu		@ return (PIC) address
	add	pc, r10, #PROCINFO_INITFUNC
ENDPROC(stext)

切换SVC模式,armv7通过下面的指令进行。

msr	cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
					@ and irqs disabled

4.1.2 processor / machine and atags

关键点:虚拟地址转物理地址

如何搜寻的过程,这里暂时不整理,这部分可以参考《ARMLinux内核源码剖析》的第73页。这里有比较重要的处理是在禁用MMU的情况下,虚拟地址转化为物理地址的一个方法。程序走到这里的时候,由于禁用了MMU,因此无法通过虚拟地址访问内存。但是,保存处理器信息的区域地址__proc_info_begin和 _proc_info_end是编译的时候指定的,所以都是虚拟地址。因此,只有将这些虚拟地址转换为物理地址,才能正确的访问处理器新的的proc_info_list的结构体。

这里的做法是做偏移处理,将偏移的量放在一个寄存器里面,然后作为数值运算:

virtual_address(__proc_info_begin) + offset = r5 + offset
											= physical_address(__proc_info_begin)

关键点:atags

Linux内核从启动加载项接受三个参数与。在ARM中循序AAPCS(procedure call stardard for the ARM architecture)标准的时候,少于4个的参数被分配在r0-r3之中,对于5个以上的参数,其前四个参数在r0-r3志宏,而之后的参数则进入栈。tagged list由struct tag数组组成,包含内存、视频、serial、initrd、revision、cmdline等信息。

tagged list不得被内核解压器(decompresssor)或者bootp程序覆写(overwrite),因此主要位于RAM的第一个16KB。在uboot中可以查看bootm.c的setup_start_tag函数,已经setup_end_tag中设置的ATAG_CORE/NONE代码12

struct tag_header {
		u32 size;	//结构体的大小
		u32 tag;	//结构体的类型
	};
struct tag {
       struct tag_header hdr;
		union { 	//此枚举体包含了uboot传给内核参数的所有类型
				struct tag_core         core;
				struct tag_mem32        mem;
				struct tag_videotext    videotext;
				struct tag_ramdisk      ramdisk;
				struct tag_initrd       initrd;
				struct tag_serialnr     serialnr;
				struct tag_revision     revision;
				struct tag_videolfb     videolfb;
				struct tag_cmdline      cmdline;
				/*
				* Acorn specific
				*/
				struct tag_acorn        acorn;
				/*
				 * DC21285 specific
				 */
				struct tag_memclk       memclk;
				struct tag_mtdpart      mtdpart_info;
		} u;
};

4.1.3 __create_page_tables

在打开MMU之前,Linux内核需要为MMU建立好页表。虚拟内存的作用我们在ARMv8的MMU管理中已经把整个历史go-through了一遍,在这里我们可以简单的再去复习一下。在32位操作系统中,处理器具有4G的虚拟内存,ARMv7架构是32位的处理器架构,在不开large memory的情况下最高也只能支持4GB的虚拟内存访问,这里的4GB虚拟内存指的并不是所有进程合在一起的内存为4G,而是每一个进程看到的都是4GB的虚拟地址空间。

这就是MMU的作用,MMU让每一个进程都有独占了所有内存的幻象。而这种技术的底层支持,也是来源于现代内存管理的方式——分页管理及换入换出技术。例如图中两个进程,被MMU映射到不同的物理地址页帧上面,两个进程无法感知对方的映射。

而且这样的分页技术提供了一个好处,就是进程和进程之间的内存共享。通过MMU映射相同的物理页帧就可以达到这样的效果。__create_page_tables 中,需要创建一个16K大小的页表项(包含4096个虚拟地址和物理地址的映射关系),这个页表项也是在ram上面开辟的。

__create_page_tables中,KERNEL_RAM_PADDR相聚0x4000的位置(KERNEL_RAM_PADDR-0x4000)到KERNEL_RAM_PADDR所有页表项执行循环,并初始化为0。对相当于内核区域的项设置节区基址和cacheable,bufferable值。执行__create_page_tables之后的页表如图所示:

/*
 * Setup the initial page tables.  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, r6, r7 corrupted
 *  r4 = physical page table address
 */
__create_page_tables:
	pgtbl	r4				@ page table address

	/*
	 * Clear the 16K level 1 swapper page table
	 */
	mov	r0, r4
	mov	r3, #0
	add	r6, r0, #0x4000
1:	str	r3, [r0], #4
	str	r3, [r0], #4
	str	r3, [r0], #4
	str	r3, [r0], #4
	teq	r0, r6
	bne	1b

	ldr	r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags

	/*
	 * Create identity mapping for first MB of kernel to
	 * cater for the MMU enable.  This identity mapping
	 * will be removed by paging_init().  We use our current program
	 * counter to determine corresponding section base address.
	 */
	mov	r6, pc, lsr #20			@ start of kernel section
	orr	r3, r7, r6, lsl #20		@ flags + kernel base
	str	r3, [r4, r6, lsl #2]		@ identity mapping

	/*
	 * Now setup the pagetables for our kernel direct
	 * mapped region.
	 */
	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

#ifdef CONFIG_XIP_KERNEL
	/*
	 * Map some ram to cover our .data and .bss areas.
	 */
	orr	r3, r7, #(KERNEL_RAM_PADDR & 0xff000000)
	.if	(KERNEL_RAM_PADDR & 0x00f00000)
	orr	r3, r3, #(KERNEL_RAM_PADDR & 0x00f00000)
	.endif
	add	r0, r4,  #(KERNEL_RAM_VADDR & 0xff000000) >> 18
	str	r3, [r0, #(KERNEL_RAM_VADDR & 0x00f00000) >> 18]!
	ldr	r6, =(_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
#endif

	/*
	 * 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]

#ifdef CONFIG_DEBUG_LL
	ldr	r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
	/*
	 * Map in IO space for serial debugging.
	 * This allows debug messages to be output
	 * via a serial console before paging_init.
	 */
	ldr	r3, [r8, #MACHINFO_PGOFFIO]
	add	r0, r4, r3
	rsb	r3, r3, #0x4000			@ PTRS_PER_PGD*sizeof(long)
	cmp	r3, #0x0800			@ limit to 512MB
	movhi	r3, #0x0800
	add	r6, r0, r3
	ldr	r3, [r8, #MACHINFO_PHYSIO]
	orr	r3, r3, r7
1:	str	r3, [r0], #4
	add	r3, r3, #1 << 20
	teq	r0, r6
	bne	1b
#if defined(CONFIG_ARCH_NETWINDER) || defined(CONFIG_ARCH_CATS)
	/*
	 * If we're using the NetWinder or CATS, we also need to map
	 * in the 16550-type serial port for the debug messages
	 */
	add	r0, r4, #0xff000000 >> 18
	orr	r3, r7, #0x7c000000
	str	r3, [r0]
#endif
#ifdef CONFIG_ARCH_RPC
	/*
	 * Map in screen at 0x02000000 & SCREEN2_BASE
	 * Similar reasons here - for debug.  This is
	 * only for Acorn RiscPC architectures.
	 */
	add	r0, r4, #0x02000000 >> 18
	orr	r3, r7, #0x02000000
	str	r3, [r0]
	add	r0, r4, #0xd8000000 >> 18
	str	r3, [r0]
#endif
#endif
	mov	pc, lr
ENDPROC(__create_page_tables)
	.ltorg

4.1.4 设置core(__v6_setup)标签

完成__create_page_tables之后,运行 __v6_setup程序设置当前处理器。根据不同的ARM架构,处理器初始化函数执行过程也是不一致的。vx的时候就调用vx_setup,调用之后就开始执行初始化任务。

setup的流程是: config_smp -> config_mmu

4.1.4.1 config SMP

在ARM里面引入了cluster的概念,一个cluster内有多个相同处理器,在setup程序中会将SMP模式激活,并启动SCU(snoop control unit)(如果有的话)用于保持多个core之间的cache一致性。

除此之外,需要对cache机制进行使失效操作。哈佛体系结构和混合结构都可以执行初始化。清理所有的cache失效之后,清空写缓冲。清空操作中使用的协处理器命令是数据同步屏障(data synchronized barrier)。

使TLB失效之后,在寄存器TTB1中保存页表起始地址。最后,讲MMU、cache、分离预测这些功能设置值保存到寄存器r0中,并跳转到__enable_mmu

4.1.4.2 enable mmu

为了控制MMU的运行,使用协处理器15(CP15)的各个寄存器,控制CPc1寄存器将集成MMU控制系统中默认寄存器的作用。c1寄存器的部分位域如下所示:

image-20220706150418298

/*
 * Setup common bits before finally enabling the MMU.  Essentially
 * this is just loading the page table pointer and domain access
 * registers.
 */
__enable_mmu:
#ifdef CONFIG_ALIGNMENT_TRAP
	orr	r0, r0, #CR_A
#else
	bic	r0, r0, #CR_A
#endif
#ifdef CONFIG_CPU_DCACHE_DISABLE
	bic	r0, r0, #CR_C
#endif
#ifdef CONFIG_CPU_BPREDICT_DISABLE
	bic	r0, r0, #CR_Z
#endif
#ifdef CONFIG_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))
	mcr	p15, 0, r5, c3, c0, 0		@ load domain access register
	mcr	p15, 0, r4, c2, c0, 0		@ load page table pointer
	b	__turn_mmu_on
ENDPROC(__enable_mmu)

/*
 * Enable the MMU.  This completely changes the structure of the visible
 * memory space.  You will not be able to trace execution through this.
 * If you have an enquiry about this, *please* check the linux-arm-kernel
 * mailing list archives BEFORE sending another post to the list.
 *
 *  r0  = cp#15 control register
 *  r13 = *virtual* address to jump to upon completion
 *
 * other registers depend on the function called upon completion
 */
	.align	5
__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
ENDPROC(__turn_mmu_on)

在enable mmu的过程中,设置所需要的位激活MMU硬件,将访问寄存器和页表指针值加载到ARM处理器的寄存器。为寄存器r0设置与MMU相关的精简为,激活MMU硬件,使虚拟内存可用。

4.1.5 跳转start_kernel

__mmap_switched标签开始,MMU处于激活装填,代码通过非PIC的绝对地址执行。从start_kernel开始,代码由C语言编写而成,因此需要__switch_data标签中设置的data\bss\stack值,并调用start_kernel后。_switch_dataarch/arm/kernel/head-common.S中。

	.type	__switch_data, %object
__switch_data:
	.long	__mmap_switched
	.long	__data_loc			@ r4
	.long	_data				@ r5
	.long	__bss_start			@ r6
	.long	_end				@ r7
	.long	processor_id			@ r4
	.long	__machine_arch_type		@ r5
	.long	__atags_pointer			@ r6
	.long	cr_alignment			@ r7
	.long	init_thread_union + THREAD_START_SP @ sp
/*
 * The following fragment of code is executed with the MMU on in MMU mode,
 * and uses absolute addresses; this is not position independent.
 *
 *  r0  = cp#15 control register
 *  r1  = machine ID
 *  r2  = atags pointer
 *  r9  = processor ID
 */
__mmap_switched:
	adr	r3, __switch_data + 4

	ldmia	r3!, {r4, r5, r6, r7} @ AAAAAAAAAA_flag
	cmp	r4, r5				@ Copy data segment if needed
1:	cmpne	r5, r6           @ BBBBBBBBBBB_flag
	ldrne	fp, [r4], #4
	strne	fp, [r5], #4
	bne	1b

	mov	fp, #0				@ Clear BSS (and zero fp)
1:	cmp	r6, r7
	strcc	fp, [r6],#4
	bcc	1b

	ldmia	r3, {r4, r5, r6, r7, sp}
	str	r9, [r4]			@ Save processor ID
	str	r1, [r5]			@ Save machine type
	str	r2, [r6]			@ Save atags pointer
	bic	r4, r0, #CR_A			@ Clear 'A' bit
	stmia	r7, {r0, r4}			@ Save control register values
	b	start_kernel
ENDPROC(__mmap_switched)
  • AAAAAAAAA_flag位置,是将data_loc和data和bss_start,end变量保存的地址存入寄存器r4/r5/r6/r7
  • BBBBBBBBBBB_flag位置,是将data_loc和data值比较,如果不一致,则复制数据。data_loc指向二进制文件中初始化数据区域的起始位置,data指向内存中初始化数据区域其实位置。由于内核在ROM中通过XIP执行无法修改数据,因此将数据区域复制到RAM,使其能够修改。

4.2 ARMv8

ARMv8的aarch32执行状态等效于ARMv7,这里ARMv8限定为aarch64执行状态。ARMv8相对来说比较复杂,因为ARMv8相比于ARMv7包含了多个异常等级(EL0-EL3),而EL0和EL1的counterpart有secure和non-secure(EL2是hypervisor的层级,只有non-secure的概念;而EL3是安全等级最高的层级,只有secure的概念)。对于ARMv8 aarch64而言,因此在kernel start的时候要做更多的更充分的准备。

对于aarch64而言,linux kernel在调用kernel start之前,需要boot loader(boot loader和boot中的bootloader意义不一致,这个广义性的包含启动内核之前所有的boot程序,而在bootloader中,仅仅指的是boot stage的第一阶段)至少要做完以下工作:

  • 初始化RAM(必须)
  • 设定设备树(必须)
  • 解压内核映像(可选)
  • 调用kernel start(必须)

初始化RAM

kernel对于启动加载项的一个要求就是必须是找到并且初始化所有未来kernel内核将会用到的存储在系统上的volatile数据。这项初始化要求要根据不同的硬件平台(启动加载项可能是使用一些内部的算法自动的定位到所有的ram,亦或是使用平台级的初始化程序,这要看boot的设计者如何设计)

设定设备树

设备树blob (dtb)必须要被放置在内核映像的开始512Mbytes内的8字节边界的位置,并且不可以横跨2MBytes的界限。这个操作允许内核使用一个在初始状态页表内单独的数据段去映射dtb。

解压内核镜像

AArch64不再提供解压程序(gzip),如果一定要加载压缩的内核镜像,那么这部分工作需要在boot loader中执行解压操作。

调用内核的接口

在解压之后的内核镜像中,必须包含一个64byte的头文件:

  u32 code0;			/* Executable code */
  u32 code1;			/* Executable code */
  u64 text_offset;		/* Image load offset, little endian */
  u64 image_size;		/* Effective Image size, little endian */
  u64 flags;			/* kernel flags, little endian */
  u64 res2	= 0;		/* reserved */
  u64 res3	= 0;		/* reserved */
  u64 res4	= 0;		/* reserved */
  u32 magic	= 0x644d5241;	/* Magic number, little endian, "ARM\x64" */
  u32 res5;      		/* reserved (used for PE COFF offset) */

这里引用linux kernel document下面的对于aarch64的一些建议13

在跳转到内核之前,下面的状况必须满足:

  • Quiesce(静默)所有的DMA设备,是因为无效的网络数据包和硬盘数据会干扰到你,这样会节约你的时间。
  • 初始化CPU的通用寄存器设定
    • x0 = 在系统RAM中的dtb的物理地址
    • x1、x2、x3 = 0,留着以后用
  • CPU mode
    • 所有的中断必须被屏蔽掉(PSTATE.DAIF),这里包括debug,serror,IRQ,FIQ
    • CPU必须在EL1或者EL2,更推荐在EL2,因为可以访问一些虚拟化特性。
  • Caches,MMU
    • MMU必须关闭
    • I-Cache 关闭开启都可以
    • 加载的kernel image的相关地址范围,必须在PoC的范围内进行清理工作。在系统中出现的cache或者相关的保持一致性的机制,这些都要求使用虚拟地址来进行cache的维护而不是set/way的操作。
    • 架构系统的维护所有cache,必须使能使用VA的方式来操作。
    • 非架构系统维护的cache,不推荐使用VA方式来操作,这个要被配置或者要被禁用。
  • Arch timers
    • CNTFRQ是要被配置的。如果以el2进入内核,的CNTHCTL_EL2必须要设定EL1PCTEN。
  • Coherency - 所有将要被启动的CPU,在进入内核之前,全部都要进行一致性同步,确保他们被划归到同一个一致性管理范围。这个可能需要IMPLEMENTATION DEFINE初始化去使能每个CPU接收维护操作的能力。
  • system registers - 所有可写的系统寄存器的需要在更高一层的level去写,为了防止unkown状态。
  • gicv3
    • 如果当前是el3,ICC_SRE_EL3.Enable必须要被初始化为0, .Sre必须被初始化为0
    • 如果当前是el2,ICC_SRE_EL2.Enable必须要被初始化为0,.SRE必须被初始化为0

与armv7架构实际上是一致的,boot loader和kernel之间需要交互,以及kernel需要保存boot loader的参数并对参数进行校验,最后将控制权转换给kernel,并在此之前做一些SoC上的初始化工作。

根据在boot.txt文档中提到的概念,控制权移交给kernel至少在硬件上要follow以下的配置:

  • MMU = off
  • D-Cache = off
  • I-Cache = on / off (PoC range)
  • x0 = physical address of the FDT blob.

MMU和cache实际上有些关联的。在ARM64中,SCTLR,System control register 用来控制MMU的caches,虽然这几个控制bit是分开的,但是不意味着的mmu,dcache,icache的开关是彼此独立的。一般而言,MMU和d-cache是相互绑定的,这个在arm里面我们已经讨论过,启动d-cache是需要mmu做内存保护(比如内存属性、共享属性)的合法性检查都在MMU的页表中保存着,如果没有MMU对页表的翻译,那么dcache根本是无头苍蝇,因此MMU和d-cache在arm64里面成对出现。这样的设定也并非强制,但必须保证内存的非device,是non-shareable的的。

4.2.1 preserve_boot_args

保存boot loader传递过来的参数,根据文档要求,需要初始化CPU的通用寄存器设定:

  • x0 = 在系统RAM中的dtb的物理地址
  • x1、x2、x3 = 0,留着以后用
/*
 * Preserve the arguments passed by the bootloader in x0 .. x3
 */
preserve_boot_args:
	mov	x21, x0				// x21=FDT @保存dtb地址到x21寄存器

	adr_l	x0, boot_args			// record the contents of @x0保存boot_args变量地址
	stp	x21, x1, [x0]			// x0 .. x3 at kernel entry @
	stp	x2, x3, [x0, #16]

	dmb	sy				// needed before dc ivac with
						// MMU off @ memory barrier

	mov	x1, #0x20			// 4 x 8 bytes @ 传递给inval_dcache_area参数
	b	__inval_dcache_area		// tail call
ENDPROC(preserve_boot_args)

这里面有几个比较好的点,从文献14,提到的以下几点值得注意:

MMU禁止后虚拟地址的处理

这部分在armv7也遇到过,在没有开MMU的情况下,访问的变量是虚拟地址的,armv7给的处理方式通过计算offset的方式来得到物理地址。armv8有着完全不同的处理方法,我们boot_args变量在内存中是虚拟地址,但是MMU 和 d-cache都是关闭状态,因此写入boot_args并非直接写入到cache中,而是直接写入RAM。访问这个变量是通过adr_l这个宏完成,内部是通过adrp指令来做的,adrp是和pc为相对位置的指令,这样就避开了armv7使用offset的方式来获取。

4.2.2 el2_setup

在kernel启动的时候,根据boot.txt的要求,SoC状态可能是EL1也可能是EL2,如果在EL2我们就会拥有更大的权限并且有更高的责任(要保证EL2的机制完善)。如果是在EL1和传统的armv7架构的setup流程没有什么太大的差别,而在EL2场景就稍微复杂一点,还需要兼顾虚拟化状态的基本设定,然后将CPU降低到EL1进行设定。

https://github.com/carloscn/imx-linux-4.1.15/blob/master/arch/arm64/kernel/head.S#L504

/*
 * If we're fortunate enough to boot at EL2, ensure that the world is
 * sane before dropping to EL1.
 *
 * Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in x20 if
 * booted in EL1 or EL2 respectively.
 */
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
ENTRY(el2_setup)
	mrs	x0, CurrentEL  @ 从CurrentEL读取数据到x0寄存器
	cmp	x0, #CurrentEL_EL2 @ 判断是不是EL2
	b.ne	1f @ 不是的话 跳到1 forward处
	mrs	x0, sctlr_el2
CPU_BE(	orr	x0, x0, #(1 << 25)	)	// Set the EE bit for EL2
CPU_LE(	bic	x0, x0, #(1 << 25)	)	// Clear the EE bit for EL2
	msr	sctlr_el2, x0
	b	2f
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
1:	mrs	x0, sctlr_el1
CPU_BE(	orr	x0, x0, #(3 << 24)	)	// Set the EE and E0E bits for EL1
CPU_LE(	bic	x0, x0, #(3 << 24)	)	// Clear the EE and E0E bits for EL1
	msr	sctlr_el1, x0
	mov	w20, #BOOT_CPU_MODE_EL1		// This cpu booted in EL1
	isb
	ret
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
	/* Hyp configuration. */
2:	mov	x0, #(1 << 31)			// 64-bit EL1
	msr	hcr_el2, x0

	/* Generic timers. */
	mrs	x0, cnthctl_el2
	orr	x0, x0, #3			// Enable EL1 physical timers
	msr	cnthctl_el2, x0
	msr	cntvoff_el2, xzr		// Clear virtual offset
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
#ifdef CONFIG_ARM_GIC_V3
	/* GICv3 system register access */
	mrs	x0, id_aa64pfr0_el1
	ubfx	x0, x0, #24, #4
	cmp	x0, #1
	b.ne	3f

	mrs_s	x0, ICC_SRE_EL2
	orr	x0, x0, #ICC_SRE_EL2_SRE	// Set ICC_SRE_EL2.SRE==1
	orr	x0, x0, #ICC_SRE_EL2_ENABLE	// Set ICC_SRE_EL2.Enable==1
	msr_s	ICC_SRE_EL2, x0
	isb					// Make sure SRE is now set
	msr_s	ICH_HCR_EL2, xzr		// Reset ICC_HCR_EL2 to defaults
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
3:
#endif
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
	/* Populate ID registers. */
	mrs	x0, midr_el1
	mrs	x1, mpidr_el1
	msr	vpidr_el2, x0
	msr	vmpidr_el2, x1
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
	/* sctlr_el1 */
	mov	x0, #0x0800			// Set/clear RES{1,0} bits
CPU_BE(	movk	x0, #0x33d0, lsl #16	)	// Set EE and E0E on BE systems
CPU_LE(	movk	x0, #0x30d0, lsl #16	)	// Clear EE and E0E on LE systems
	msr	sctlr_el1, x0
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
	/* Coprocessor traps. */
	mov	x0, #0x33ff
	msr	cptr_el2, x0			// Disable copro. traps to EL2

#ifdef CONFIG_COMPAT
	msr	hstr_el2, xzr			// Disable CP15 traps to EL2
#endif
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
	/* EL2 debug */
	mrs	x0, pmcr_el0			// Disable debug access traps
	ubfx	x0, x0, #11, #5			// to EL2 and allow access to
	msr	mdcr_el2, x0			// all PMU counters from EL1
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
	/* Stage-2 translation */
	msr	vttbr_el2, xzr
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
	/* Hypervisor stub */
	adrp	x0, __hyp_stub_vectors
	add	x0, x0, #:lo12:__hyp_stub_vectors
	msr	vbar_el2, x0
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
	/* spsr */
	mov	x0, #(PSR_F_BIT | PSR_I_BIT | PSR_A_BIT | PSR_D_BIT |\
		      PSR_MODE_EL1h)
	msr	spsr_el2, x0
	msr	elr_el2, lr
	mov	w20, #BOOT_CPU_MODE_EL2		// This CPU booted in EL2
	eret
ENDPROC(el2_setup)

从上述代码中可以得到el2_setup的流程可以表述为:

  • currentEL中获取当前EL等级所处的位置
  • 使用sctlr_el2配置在EL2时候地址翻译的大小端endian(如果是el1模式,那么就访问sctlr_el1
  • 配置hcr_el2寄存器
  • 配置通用定时器generic timers
  • 配置gicv3
  • midr_el1vmpidr_el2中读取arch信息,processor id信息
  • 把be或者le写入sctlr_el1寄存器
  • 配置PMCR_EL0婿debug操作
  • 设定SPSR_EL2和 ELR的初始值,eret指令会在发生异常的时候填充这些寄存器值
  • eret指令模拟一次异常返回,用于填充这些寄存器的值

为什么无法从el3开始setup? 14

当一个SoC设计支持了EL3的支持,理所应当的应该从EL3逐级开始进行初始化,而为什么限定最高为EL2等级。我们从secure boot的综述文章中可以拿到以下的图,可以看出在ARMv8之前由于trustzone的支持,optee要进行boot,此时optee作为secure platform firmware,它会进行硬件平台的初始化,然后将控制权限交给uboot次级引导,而并不是直接由linux kernel进行cpu的控制。对于linux kernel而言,它无法感知secure world。因此 linux kernel并不是el3开始启动。

image-20220708141238938

4.2.3 set_cpu_boot_mode_flag

顾名思义,这个步骤是设定cpu启动模式。w20寄存器保存cpu启动时候的异常等级。

https://github.com/carloscn/imx-linux-4.1.15/blob/master/arch/arm64/kernel/head.S#L594

/*
 * Sets the __boot_cpu_mode flag depending on the CPU boot mode passed
 * in x20. See arch/arm64/include/asm/virt.h for more info.
 */
ENTRY(set_cpu_boot_mode_flag)
	adr_l	x1, __boot_cpu_mode
	cmp	w20, #BOOT_CPU_MODE_EL2
	b.ne	1f
	add	x1, x1, #4
1:	str	w20, [x1]			// This CPU has booted in EL1
	dmb	sy   @ x1还有别的CPU在用,需要保证一致性,确保x1被刷进来
	dc	ivac, x1			// Invalidate potentially stale cache line
	ret
ENDPROC(set_cpu_boot_mode_flag)

本质上我们希望所有的cpu在初始化 的时候都处于同样的mode,要么是EL2,要么都是EL1。所有的cpu core在启动的时候都处于EL2 mode表示支持虚拟化,只有在这种情况下,kvm模块才能被顺利启动。这个函数会在每一个cpu上面执行14

4.2.4 __vet_fdt

x21寄存器会被保存boot_args中传递进来的fdt的物理地址,x24被定义为_PHYS_OFFSET

#define __PHYS_OFFSET (KERNEL_START - TEXT_OFFSET)

#define KERNEL_START _text 为kernel开始运行的虚拟地址,在https://github.com/carloscn/imx-linux-4.1.15/blob/master/arch/arm64/kernel/vmlinux.lds.S#L84 被设定为:

. = PAGE_OFFSET + TEXT_OFFSET;
.head.text : {
		_text = .;
		HEAD_TEXT
}

因此,_PHYS_OFFSET为kernel image的首地址。__vet_fdt主要是对fdt参数进行校验

/*
 * Determine validity of the x21 FDT pointer.
 * The dtb must be 8-byte aligned and live in the first 512M of memory.
 */
__vet_fdt:
	tst	x21, #0x7  @是不是8byte对齐
	b.ne	1f
	cmp	x21, x24  @ 是否小于kernel space的首地址
	b.lt	1f
	mov	x0, #(1 << 29)
	add	x0, x0, x24
	cmp	x21, x0  @ 是否大于kernel space的首地址 + 512M
	b.ge	1f
	ret
1:
	mov	x21, #M @ 传递fdt地址有误。
	ret
ENDPROC(__vet_fdt)

附录

IMG_7654

IMG_7651

Ref

Footnotes

  1. ARM Linux内核源码剖析

  2. Linux Kernel Map

  3. 几种linux内核文件的区别(vmlinux、zImage、bzImage、uImage、vmlinuz、initrd )

  4. Chapter 5 : Kernel Initialization

  5. Decompressed vmlinux: linux kernel initialization from page table configuration perspective

  6. Booting AArch64 Linux

  7. AArch64 kernel image decompression

  8. ARM Cortex-A Series Programmer's Guide for ARMv7-A - Kernel entry

  9. CONFIG_ZBOOT_ROM: Compressed boot loader in ROM/flash

  10. [PATCH] Clean up ARM compressed loader

  11. Linux Config Help - Compressed boot loader in ROM/flash

  12. uboot以tag方式给内核传参

  13. booting.txt

  14. ARM64的启动过程之(一):内核第一个脚印 2 3