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 mrproper
和 make 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
编译后的内核会生成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提高解压性能。
__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寄存器的部分位域如下所示:
/*
* 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_data
在arch/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_el1
和vmpidr_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开始启动。
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)