Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
carloscn opened this issue Jul 5, 2022 · 0 comments
Open

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

carloscn opened this issue Jul 5, 2022 · 0 comments

Comments

@carloscn
Copy link
Owner

carloscn commented Jul 5, 2022

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

carloscn added a commit that referenced this issue Jul 5, 2022
* [01_Linux内核的启动(一)之启动前准备](#64
@carloscn carloscn changed the title 01_Linux内核的启动(一)之启动前准备 01_LinuxKernel_内核的启动(一)之启动前准备 Jul 8, 2022
@carloscn carloscn changed the title 01_LinuxKernel_内核的启动(一)之启动前准备 0x01_LinuxKernel_内核的启动(一)之启动前准备 Jul 27, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant