查看原文
其他

Zircon - Fuchsia 内核分析 - 启动(平台初始化)

坑大 看雪学院 2019-05-25

简介


Zircon 是 Google 新操作系统 Fuchsia 的内核,基于 LK - Little Kernel 演变而来。而 Little Kernel 前面一直作为 Android 系统的 Bootloader 的核心而存在。Zircon 在此基础上增加了 MMU,System Call 等功能。


Zircon 目前支持 X86/X64 和 ARM 两种 CPU 平台,下面我将以 ARM64 为例,一行行分析 Zircon 内核的早期启动过程,看一下 Zircon 和 ARM64 是如何完成平台初始化的,这部分由汇编实现。


需要事先声明的是,本人平时从事的是 Android 开发,对于 ARM 了解有限,此次源码阅读也会参考一些其他资料,其中难免会有一些错误,望广大读者谅解。

 

带注释的 Zircon 内核源码(未完成):

https://github.com/ganyao114/zircon/tree/doc



ARM64


首先需要简单过一下涉及到的 ARM64 背景,以前虽有简单接触过嵌入式的 ARM,但相比之下 ARM64 确实要复杂很多很多。



特权模式/异常等级


在 ARM32 中,我们使用 SVC 等 7 种特权模式来区分 CPU 的工作模式,操作系统等底层程序会运行在高特权模式,而普通用户程序则运行在低特权的用户模式。


而在 ARM64 中,其实也类似,不过在 ARM64 中统一成了 4 个异常等级 EL-0/1/2/3。


EL 架构:


  • 特权等级 EL3 > EL2 > EL1 > EL0 , EL0 为非特权执行等级;

  • EL2 无 Secure State,有 None-Secure State。EL3 只有 Secure-State,并且控制 EL0和EL1 在两种模式间切换;

  • EL0 和 EL1 必须实现,EL2 和 EL3 是可选的。



关于 4 个特权等级在系统软件中的实际使用:


| EL |  用途 |
|------|------|
| EL0 | 运行用户程序 |
| EL1 | 操作系统内核 |
| EL2 | Hypervisor (可以理解为上面跑多个虚拟内核) |
| EL3 | Secure Monitor(ARM Trusted Firmware) |

 

Secure State 的影响:



多核心


在多核心处理器下,ID = 0 的 CPU 内核为 prime 核心,或者被称为 BSP 引导处理器 - bootstrap processor,其他处理器则为 non - prime 核心,或者 AP 核心 - Application Processor,开机和内核初始化由 prime 核心完成,AP 核心只要完成自身的配置就可以了。

 

ARM MP 架构图:

 


  • CPU 内核间通过核间中断 - IPI 彼此通讯。

  • 每个 CPU 内核都能看到同样的内存总线和数据,一般 L1 缓存每个内核独享,L2/L3 为所有内核共享。

  • 所有 CPU 内核共享同一个 I/O 周边与中断控制器,中断控制器会根据配置将中断分发到合适的 CPU 内核。


寄存器


仅说明本文所涉及的



内核代码中的通用范例


有了上文对 ARM64 的简单介绍,我们就可以看懂代码中的一些代码了
以下是比较通用的代码。


判断是否是 prime CPU 内核:

mrs     cpuid, mpidr_el1

ubfx    cpuid, cpuid, #0, #15 /* mask Aff0 and Aff1 fields */ //aff0 记录 cpu ID,aff1 记录是否支持超线程

cbnz    cpuid, .Lno_save_bootinfo //如果不是 prim 核心(0 号核心),则不需要启动内核,也就不需要准备内核启动参数,直接执行核心初始化工作


前两行取出 mpidr_el1 的 AFF01 放入 cpuid。


第三行如果 cpuid = 0 则代表是 prime cpu 内核,并且也是第一个线程,虽然现在超线程没有实现就是了。


取标签/数据地址


这里需要解释一下,因为 kernel 在链接的时候是根据虚拟地址来的。而在内核引导的早期阶段,也就是本文所介绍的这个过程中,MMU 是处于关闭状态的,这段时间内核实际是跑在物理地址上的。


那么,这段代码就必须是 PIC 位置无关代码,除了尽量使用寄存器,在不得不访问内存时,这段代码还不能依赖链接器所给的地址,那么如果在这段代码中需要取到内存中的地址只能使用指令计算数据/Label的实际地址。

 

Zircon 将这一操作简化成了一个宏:

.macro adr_global reg, symbol

//得到包含 symbol 4K 内存页的基地址

adrp \reg, \symbol

//第一个全局变量的地址

add \reg, \reg, #:lo12:\symbol

.endm


第一行得到得到包含 symbol 4K 内存页的基地址。


第二行在基地址上加上偏移就是 symbol 的实际地址。


判断当前所在的 EL

mrs x9, CurrentEL

cmp x9, #(0b01 << 2)

//不等于 0 时,说明不是在异常级别 1,跳转到 notEL1 代码

bne .notEL1


循环遍历

str     xzr, [page_table1, tmp, lsl #3]

add     tmp, tmp, #1

cmp     tmp, #MMU_KERNEL_PAGE_TABLE_ENTRIES_TOP

bne     .Lclear_top_page_table_loop


等价于

for(int tmp = 0;tmp < MMU_KERNEL_PAGE_TABLE_ENTRIES_TOP;tmp++) {

    page_table1[tmp] = 0;

}



启动过程概述


启动早期,即内核在进入 C++ 世界之前,主要分为以下几步:


  • 初始化各个 EL1 - EL3 下的异常配置

  • 创建启动阶段页表

  • 为打开 MMU 做准备

  • 打开 MMU

  • 配置栈准备进入 C 世界



启动时序与代码


在多核处理器架构中,很多初始化代码仅需要由 prime 处理器完成,其他处理器完成各自的配置即可。

<table>
 <tr>
    <td bgcolor=#00F5FF>prime  核心</td>
    <td bgcolor=#00F5FF>其他核心</td>
 </tr>
 <tr>
    <td>保存内核启动参数</td>
    <td>跳过</td>
 </tr>
 <tr>
    <td colspan="2">初始化 EL1 - EL3 的异常配置</td>
 </tr>
  <tr>
    <td colspan="2">初始化缓存</td>
 </tr>
 <tr>
    <td>修复 kernel base 地址 </td>
    <td>跳过</td>
 </tr>
  <tr>
    <td>检查并等待 .bss 段数据清除</td>
    <td>跳过</td>
 </tr>
  <tr>
    <td>创建启动阶段的页表</td>
    <td>自旋等待页表创建完成</td>
 </tr>
 <tr>
    <td colspan="2">打开 MMU 之前的准备工作</td>
 </tr>
 <tr>
    <td colspan="2">打开 MMU (以上代码运行在物理地址,以下代码运行在虚拟地址)</td>
 </tr>
 <tr>
    <td>重新配置内核栈指针</td>
    <td>配置其他 CPU 的栈指针</td>
 </tr>
  <tr>
    <td>跳转到 C 世界继续初始化</td>
    <td>休眠等待唤醒</td>
 </tr>
</table>



保存内核启动参数


start.S - _start

mrs     cpuid, mpidr_el1 //aff0 记录 cpu ID,aff1 记录是否支持超线程

cbnz    cpuid, .Lno_save_bootinfo //如果不是 prim 核心(0 号核心),则不需要启动内核,也就不需要准备内核启动参数,直接执行核心初始化工作

/* save x0 in zbi_paddr */

//prim 核心走这里,准备并保存内核启动参数

//计算 zbi_paddr 段中数据的地址,保存在 x0 中,下同

adrp    tmp, zbi_paddr

str     x0, [tmp, #:lo12:zbi_paddr]

/* save entry point physical address in kernel_entry_paddr */

adrp    tmp, kernel_entry_paddr

adr     tmp2, _start

str     tmp2, [tmp, #:lo12:kernel_entry_paddr]

adrp    tmp2, arch_boot_el

mrs     x2, CurrentEL

str     x2, [tmp2, #:lo12:arch_boot_el]

//总之,x0 - x4 现在保存了核心初始化需要的参数,为跳转到 C 世界作准备。



初始化 EL1 - EL3


asm.S - arm64_elX_to_el1


对各个 EL 的配置,需要 CPU 在对应的 EL 状态下才能配置


EL1


EL1 不需要配置直接返回

//读取现在的异常级别

mrs x9, CurrentEL

cmp x9, #(0b01 << 2)

//不等于 0 时,说明不是在异常级别 1,跳转到 notEL1 代码

bne .notEL1

/* Already in EL1 */

//EL1 直接返回

ret


EL2


EL2 状态下主要配置了:

  • 配置 EL2 的异常向量表

  • 配置时钟

  • 清除 EL2 的转换表寄存器

  • 配置 SPSR 和 ELR 寄存器,这两个看上面的寄存器介绍


实际上 EL2 在 Zircon 中还没有具体用处,所以此处初始化基本上就是设一些空值

/* Setup the init vector table for EL2. */

   //计算EL2的异常向量表的基地址

   adr_global x9, arm64_el2_init_table

   //设定EL2的异常向量表的基地址

   msr vbar_el2, x9



   /* Ensure EL1 timers are properly configured, disable EL2 trapping of

       EL1 access to timer control registers.  Also clear virtual offset.

   */


   //检查并配置时钟

   mrs x9, cnthctl_el2

   orr x9, x9, #3

   msr cnthctl_el2, x9

   msr cntvoff_el2, xzr



   /* clear out stage 2 translations */

   //清除 vttbr_el2 寄存器,vttbr_el2 保存了转换表的基地址,负责在 EL2 下进行 EL0 -> EL1 的非安全存储器访问的转换

   msr vttbr_el2, xzr //http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.100403_0200_00_en/lau1457340777806.html



   //当系统发生了异常并进入EL2,SPSR_EL2,Saved Program Status Register (EL2)会保存处理器状态,ELR_EL2,Exception Link Register (EL2)会保存返回发生exception的现场的返回地址。

   //这里是设定SPSR_EL2和ELR_EL2的初始值。

   adr x9, .Ltarget

   msr elr_el2, x9

   //ELR 定义看上面

   mov x9, #((0b1111 << 6) | (0b0101)) /* EL1h runlevel */

   msr spsr_el2, x9


EL3


EL3 状态的主要任务就是配置 EL0/EL1 的 Secure State/HVC/运行指令集,其他的也是上面 EL2 一样付空值。


  • 设置 EL0/EL1 为 non-Secure State

  • 开启 HVC 指令

  • 使用 AARCH64 指令

cmp x9, #(0b10 << 2)



//当前为异常级别 2,跳转到 inEL2

beq .inEL2



//当不在 EL2 状态时,则为 EL3

/* set EL2 to 64bit and enable HVC instruction */

//scr_el3 控制EL0/EL1/EL2的异常路由  逻辑1允许

//http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.100403_0200_00_en/lau1457340777806.html

//若SCR_EL3.RW == 1,则决定 EL2/EL1 是使用AArch64,否则AArch32

mrs x9, scr_el3

//打开 EL0/EL1 的非安全状态,EL0/EL1 无法访问安全内存

orr x9, x9, #SCR_EL3_NS

//开启 HVC 指令

//关于 HVC,看 http://www.wowotech.net/armv8a_arch/238.html

orr x9, x9, #SCR_EL3_HCE

//设置 SCR_EL3.RW == 1,EL2/EL1 是使用AArch64

orr x9, x9, #SCR_EL3_RW

msr scr_el3, x9



//ELR 寄存器 Exception Link Register,用于保存异常进入ELX的异常地址,在返回异常现场的时候,可以使用 ELR_ELX(x = 1/2/3) 来恢复PC值, 异常迁移到哪一个exception level就使用哪一个ELR

//同样的,由于不会有异常把系统状态迁移到EL0,因此也就不存在ELR_EL0了。

adr x9, .Ltarget

//这里异常进入地址为 Ltarget

msr elr_el3, x9





//设定 spsr_el3

mov x9, #((0b1111 << 6) | (0b0101)) /* EL1h runlevel */

msr spsr_el3, x9



//配置 EL1 并准备进入 EL1 *

b   .confEL1


从 EL3 返回 EL1

/* disable EL2 coprocessor traps */

mov x9, #0x33ff

msr cptr_el2, x9



/* set EL1 to 64bit */

//设置 EL1 的异常处理为 AARCH64 指令,同上

mov x9, #HCR_EL2_RW

msr hcr_el2, x9



/* disable EL1 FPU traps */

mov x9, #(0b11<<20)

msr cpacr_el1, x9



/* set up the EL1 bounce interrupt */



//配置 EL1 栈指针

mov x9, sp  

msr sp_el1, x9



isb

//模拟异常返回,执行该指令会使得CPU返回EL1状态

eret



初始化缓存


//使缓存失效 *

bl      arch_invalidate_cache_all



/* enable caches so atomics and spinlocks work */

//启用缓存,使原子操作和自旋锁生效

mrs     tmp, sctlr_el1

//打开指令缓存

orr     tmp, tmp, #(1<<12) /* Enable icache */

//打开数据缓存

orr     tmp, tmp, #(1<<2)  /* Enable dcache/ucache */

msr     sctlr_el1, tmp


修复重定向的 Kernel Base 地址


此工作由 prime cpu 完成,其他 cpu 开始进入自旋等待:

//加载 kernel_relocated_base 段地址

//内核重定向的基地址,即内核开始的虚拟地址

adr_global  tmp, kernel_relocated_base

//负值给 kernel_vaddr

ldr     kernel_vaddr, [tmp]



// Load the base of the translation tables.

//貌似 Zircon 中 1GB 物理内存由一个 translation_table 维护,所以这里 tt_trampoline 相当于一级页表?

adr_global page_table0, tt_trampoline

//虚拟地址内存页地址转换表

adr_global page_table1, arm64_kernel_translation_table



// Send secondary cpus over to a waiting spot for the primary to finish.

//如果不是 prim CPU 内核,则跳转到 Lmmu_enable_secondary 后等待 prim 内核运行完下面代码

cbnz    cpuid, .Lmmu_enable_secondary

//下面的代码只有 prim CPU 内核执行



// The fixup code appears right after the kernel image (at __data_end in

// our view).  Note this code overlaps with the kernel's bss!  It

// expects x0 to contain the actual runtime address of __code_start.

//将内核代码开始的虚拟地址保存到 x0 中

mov     x0, kernel_vaddr

//跳转到 __data_end *

//__data_end 指向 image.S - apply_fixups 方法

bl      __data_end


FUNCTION(apply_fixups)

   // This is the constant address the kernel was linked for.

   movlit x9, KERNEL_BASE

   sub x0, x0, x9



// The generated kernel-fixups.inc invokes this macro for each run of fixups.

.macro fixup addr, n, stride

   adr x9, FIXUP_LOCATION(\addr)

.if \n >= 4 && \stride == 8

   // Do a loop handling adjacent pairs.

   mov x16, #(\n / 2)

0:  fixup_pair

   subs x16, x16, #1

   b.ne 0b

.if \n % 2

   // Handle the odd remainder after those pairs.

   fixup_single 8

.endif

.elseif \n >= 2 && \stride == 8

   // Do a single adjacent pair.

   fixup_pair

.if \n == 3

   // Do the third adjacent one.

   fixup_single 8

.endif

.elseif \n > 1

   // Do a strided loop.

   mov x16, #\n

0:  fixup_single \stride

   subs x16, x16, #1

   b.ne 0b

.else

   // Do a singleton.

   fixup_single 8

.endif

.endm



.macro fixup_pair

   ldp x10, x11, [x9]

   add x10, x10, x0

   add x11, x11, x0

   stp x10, x11, [x9], #16

.endm



.macro fixup_single stride

   ldr x10, [x9]

   add x10, x10, x0

   str x10, [x9], #\stride

.endm



#include "kernel-fixups.inc"



   ret



DATA(apply_fixups_end)

END_FUNCTION(apply_fixups)



检查等待清除 .bss 段


//检查内核 bss 段是否被清除,猜测是因为前面 bss 段所在内存已经被操作过  

.Ldo_bss:

   //见 kernel.ld

   //计算保存内核 .bss 段开始地址

   adr_global tmp, __bss_start

   //计算保存内核 .bss 段结束地址

   adr_global tmp2, _end

   //计算 .bss 段大小

   sub     tmp2, tmp2, tmp

   //.bss 段大小为 0 则跳转 Lbss_loop_done

   cbz     tmp2, .Lbss_loop_done



//不为 0 则循环等待

.Lbss_loop:

   sub     tmp2, tmp2, #16

   stp     xzr, xzr, [tmp], #16

   cbnz    tmp2, .Lbss_loop

.Lbss_loop_done:



创建启动阶段的页表


首先要把也表中的内存清除:

//清除内核虚地址转换表

.Lclear_top_page_table_loop:



   //遍历转换表中的所有条目并设置 0

   /**

       等价于

       for(int tmp = 0;tmp < MMU_KERNEL_PAGE_TABLE_ENTRIES_TOP;tmp++) {

           page_table1[tmp] = 0;

       }



   关于 xzr 寄存器 https://community.arm.com/processors/f/discussions/3185/wzr-xzr-register-s-purpose

   **/


   str     xzr, [page_table1, tmp, lsl #3]

   add     tmp, tmp, #1

   cmp     tmp, #MMU_KERNEL_PAGE_TABLE_ENTRIES_TOP

   bne     .Lclear_top_page_table_loop


在初始化阶段,需要映射三段地址:

  • 第一段是identity mapping,其实就是把物理地址mapping到物理地址上去,在打开MMU的时候需要这样的mapping(ARM ARCH强烈推荐这么做的)

  • 第二段是kernel image mapping,内核代码欢快的执行当然需要将kernel running需要的地址(kernel txt、dernel rodata、data、bss等等)进行映射了

  • 第三段是blob memory对应的mapping


因为页表映射调用到了 C 方法,所以需要提前为 CPU 配置 SP 指针:

.Lbss_loop_done:



   /* set up a functional stack pointer */

   //设定内核栈地址,准备调用 C 代码

   adr_global tmp, boot_cpu_kstack_end

   mov     sp, tmp



   /* make sure the boot allocator is given a chance to figure out where

    * we are loaded in physical memory. */


   bl      boot_alloc_init



   /* save the physical address the kernel is loaded at */

   //保存内核开始地址到 kernel_base_phys 全局变量

   adr_global x0, __code_start

   adr_global x1, kernel_base_phys

   str     x0, [x1]



   /* set up the mmu according to mmu_initial_mappings */



   /* clear out the kernel translation table */



   mov     tmp, #0


映射物理内存:

//准备调用 C 函数 arm64_boot_map

   //1.该函数任务是帮内核映射物理内存

   //先准备 5 个参数 x0-x4 寄存器保存函数参数

   /* void arm64_boot_map(pte_t* kernel_table0, vaddr_t vaddr, paddr_t paddr, size_t len, pte_t flags); */

   /* map a large run of physical memory at the base of the kernel's address space */

   mov     x0, page_table1

   mov     x1, KERNEL_ASPACE_BASE

   mov     x2, 0

   mov     x3, ARCH_PHYSMAP_SIZE

   movlit  x4, MMU_PTE_KERNEL_DATA_FLAGS

   //调用 arm64_boot_map *

   bl      arm64_boot_map


映射内核运行内存:

//2.映射内核的地址

/* map the kernel to a fixed address */

/* note: mapping the kernel here with full rwx, this will get locked down later in vm initialization; */

mov     x0, page_table1

mov     x1, kernel_vaddr

adr_global x2, __code_start

adr_global x3, _end

sub     x3, x3, x2

mov     x4, MMU_PTE_KERNEL_RWX_FLAGS

bl      arm64_boot_map


通知页表配置完毕:

//标记页表已经设置完毕,通知其他 CPU 内核可以继续往下跑了

adr_global tmp, page_tables_not_ready

str     wzr, [tmp]

//prime CPU 内核跳入 Lpage_tables_ready

b       .Lpage_tables_ready



准备打开 MMU


打开 MMU 之前需要做一些配置。


清理垃圾数据


需要重置一下 MMU 和 Cache 的状态以清除里面的残余数据,在进入 Kernel 代码之前,Bootloader 可能使用过 MMU 和 Cache,所以 ICache 和 TLB 中可能还有前面留下来的残余垃圾数据。

//使 TLB 失效以清除数据

   /* Invalidate TLB */

   tlbi    vmalle1is


初始化 Memory attributes 配置


Memory attributes 简单来说就是将 Memory 加上了几种属性,每种属性都会影响 Memory 的读写策略。

 

因为 Memory 读写策略是非常复杂的,比如一段内存区域指向的是一个 FIFO 设备,对内存的读写有严格的时序要求,则需要配置 Memory attributes 来禁止 CPU 读写重排,Cache 等等优化,因为这些对于这段 Memory 没有意义,还会影响数据的读写的正确性。

movlit  tmp, MMU_MAIR_VAL

msr     mair_el1, tmp



/* Initialize TCR_EL1 */

/* set cacheable attributes on translation walk */

/* (SMP extensions) non-shareable, inner write-back write-allocate */

movlit  tmp, MMU_TCR_FLAGS_IDENT

msr     tcr_el1, tmp


看一下 Zircon 的默认 Memory Attribute 配置:

/* Default configuration for main kernel page table:

*    - do cached translation walks

*/




/* Device-nGnRnE memory */

#define MMU_MAIR_ATTR0                  MMU_MAIR_ATTR(0, 0x00)

#define MMU_PTE_ATTR_STRONGLY_ORDERED   MMU_PTE_ATTR_ATTR_INDEX(0)



/* Device-nGnRE memory */

#define MMU_MAIR_ATTR1                  MMU_MAIR_ATTR(1, 0x04)

#define MMU_PTE_ATTR_DEVICE             MMU_PTE_ATTR_ATTR_INDEX(1)



/* Normal Memory, Outer Write-back non-transient Read/Write allocate,

* Inner Write-back non-transient Read/Write allocate

*/


#define MMU_MAIR_ATTR2                  MMU_MAIR_ATTR(2, 0xff)

#define MMU_PTE_ATTR_NORMAL_MEMORY      MMU_PTE_ATTR_ATTR_INDEX(2)



/* Normal Memory, Inner/Outer uncached, Write Combined */

#define MMU_MAIR_ATTR3                  MMU_MAIR_ATTR(3, 0x44)

#define MMU_PTE_ATTR_NORMAL_UNCACHED    MMU_PTE_ATTR_ATTR_INDEX(3)



#define MMU_MAIR_ATTR4                  (0)

#define MMU_MAIR_ATTR5                  (0)

#define MMU_MAIR_ATTR6                  (0)

#define MMU_MAIR_ATTR7                  (0)



#define MMU_MAIR_VAL                    (MMU_MAIR_ATTR0 | MMU_MAIR_ATTR1 | \

                                        MMU_MAIR_ATTR2 | MMU_MAIR_ATTR3 | \

                                        MMU_MAIR_ATTR4 | MMU_MAIR_ATTR5 | \

                                        MMU_MAIR_ATTR6 | MMU_MAIR_ATTR7 )



#define MMU_TCR_IPS_DEFAULT MMU_TCR_IPS(2) /* TODO: read at runtime, or configure per platform */



/* Enable cached page table walks:

* inner/outer (IRGN/ORGN): write-back + write-allocate

*/


#define MMU_TCR_FLAGS1 (MMU_TCR_TG1(MMU_TG1(MMU_KERNEL_PAGE_SIZE_SHIFT)) | \

                       MMU_TCR_SH1(MMU_SH_INNER_SHAREABLE) | \

                       MMU_TCR_ORGN1(MMU_RGN_WRITE_BACK_ALLOCATE) | \

                       MMU_TCR_IRGN1(MMU_RGN_WRITE_BACK_ALLOCATE) | \

                       MMU_TCR_T1SZ(64 - MMU_KERNEL_SIZE_SHIFT))

#define MMU_TCR_FLAGS0 (MMU_TCR_TG0(MMU_TG0(MMU_USER_PAGE_SIZE_SHIFT)) | \

                       MMU_TCR_SH0(MMU_SH_INNER_SHAREABLE) | \

                       MMU_TCR_ORGN0(MMU_RGN_WRITE_BACK_ALLOCATE) | \

                       MMU_TCR_IRGN0(MMU_RGN_WRITE_BACK_ALLOCATE) | \

                       MMU_TCR_T0SZ(64 - MMU_USER_SIZE_SHIFT))

#define MMU_TCR_FLAGS0_IDENT \

                      (MMU_TCR_TG0(MMU_TG0(MMU_IDENT_PAGE_SIZE_SHIFT)) | \

                       MMU_TCR_SH0(MMU_SH_INNER_SHAREABLE) | \

                       MMU_TCR_ORGN0(MMU_RGN_WRITE_BACK_ALLOCATE) | \

                       MMU_TCR_IRGN0(MMU_RGN_WRITE_BACK_ALLOCATE) | \

                       MMU_TCR_T0SZ(64 - MMU_IDENT_SIZE_SHIFT))

#define MMU_TCR_FLAGS_IDENT (MMU_TCR_IPS_DEFAULT | MMU_TCR_FLAGS1 | MMU_TCR_FLAGS0_IDENT)



打开 MMU


这里打开 MMU 非常简单,打开之前,以上代码都在物理地址下运行,打开之后则在虚拟地址下运行了。

//内存栅栏

isb



//保存 EL1 状态的异常向量表

/* Read SCTLR */

mrs     tmp, sctlr_el1



//打开 MMU

/* Turn on the MMU */

orr     tmp, tmp, #0x1



//恢复 EL1 状态的异常向量表

/* Write back SCTLR */

msr     sctlr_el1, tmp


这里注意备份还原异常向量表。


内存栅栏防止打开 MMU 前后代码乱序执行,造成逻辑错误。



准备跳入 C 世界


重新设置栈指针


在此之前,重新设置栈指针,因为此时已经变成虚拟地址:

//重新设置 prime CPU 的内核栈指针,因为现在 MMU 已经打开,需要使用虚拟地址

// set up the boot stack for real

adr_global tmp, boot_cpu_kstack_end

mov     sp, tmp



设置栈溢出异常


配置 Stack Guard,其实就是在栈末尾设置一个页中断,如果程序读写到这里,代表栈溢出,触发异常。


防止编译期间没有启用栈保护:

//配置 Stack Guard,其实就是在栈末尾设置一个页中断,如果程序读写到这里,代表栈溢出,触发异常

adr_global tmp, boot_cpu_fake_thread_pointer_location

msr     tpidr_el1, tmp



// set the per cpu pointer for cpu 0

adr_global x18, arm64_percpu_array



// Choose a good (ideally random) stack-guard value as early as possible.

bl      choose_stack_guard

mrs     tmp, tpidr_el1

str     x0, [tmp, #ZX_TLS_STACK_GUARD_OFFSET]

// Don't leak the value to other code.

mov     x0, xzr


其他 CPU 设置栈,初始化, 并且进入休眠:

.Lsecondary_boot:



   //配置其他 CPU 内核的栈指针

   bl      arm64_get_secondary_sp

   cbz     x0, .Lunsupported_cpu_trap

   mov     sp, x0

   msr     tpidr_el1, x1



   bl      arm64_secondary_entry



.Lunsupported_cpu_trap:

   //其他 CPU 内核初始化完毕

   wfe

   b       .Lunsupported_cpu_trap


prime 内核进入 C 世界:

//跳转到内核 C 代码入口

bl  lk_main

b



关于内核初始化后期


这部分大部分在 C/C++ 中完成,下一篇分析。



- End -


看雪ID:坑大              

https://bbs.pediy.com/user-675677.htm


本文由看雪论坛 坑大 原创

转载请注明来自看雪社区



热门图书推荐:

立即购买!



征题正在火热进行中!

(晋级赛Q1即将于3月10日开启,敬请期待!)



热门文章阅读


1、微信PC端技术研究:保存聊天语音

2、萌新对C++编写的动态库逆向分析

3、一个PESpin保护程序脱壳调试报告

4、【ScyllaHide】 03 PEB相关反调试


热门课程推荐











公众号ID:ikanxue

官方微博:看雪安全

商务合作:wsc@kanxue.com


点击下方“阅读原文”

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存