- 在早期引导代码中,我们从
Hypervisor
特权级别(AArch64中的EL2
)过渡到Kernel
(EL1
)特权级别。
应用级别的CPU具有所谓的privilege levels
,它们具有不同的目的:
Typically used for | AArch64 | RISC-V | x86 |
---|---|---|---|
Userspace applications | EL0 | U/VU | Ring 3 |
OS Kernel | EL1 | S/VS | Ring 0 |
Hypervisor | EL2 | HS | Ring -1 |
Low-Level Firmware | EL3 | M |
在AArch64中,EL
代表Exception Level
(异常级别)。如果您想获取有关其他体系结构的更多信息,请查看以下链接:
在继续之前,我强烈建议您先浏览一下Programmer’s Guide for ARMv8-A的第3章
。它提供了关于该主题的简明概述。
默认情况下,树莓派将始终在EL2
中开始执行。由于我们正在编写一个传统的Kernel
,我们需要过渡到更合适的EL1
。
首先,我们需要确保我们实际上是在EL2
中执行,然后才能调用相应的代码过渡到EL1
。
因此,我们在boot.s
的顶部添加了一个新的检查,如果CPU核心不在EL2
中,则将其停止。
// Only proceed if the core executes in EL2. Park it otherwise.
mrs x0, CurrentEL
cmp x0, {CONST_CURRENTEL_EL2}
b.ne .L_parking_loop
接下来,在boot.rs
中继续准备从EL2
到EL1
的过渡,通过调用prepare_el2_to_el1_transition()
函数。
#[no_mangle]
pub unsafe extern "C" fn _start_rust(phys_boot_core_stack_end_exclusive_addr: u64) -> ! {
prepare_el2_to_el1_transition(phys_boot_core_stack_end_exclusive_addr);
// Use `eret` to "return" to EL1. This results in execution of kernel_init() in EL1.
asm::eret()
}
由于EL2
比EL1
更具特权,它可以控制各种处理器功能,并允许或禁止EL1
代码使用它们。
其中一个例子是访问计时器和计数器寄存器。我们已经在tutorial 07中使用了它们,所以当然我们希望保留它们。
因此,我们在Counter-timer Hypervisor Control register中设置相应的标志,并将虚拟偏移量设置为零,以获取真实的物理值。
// Enable timer counter registers for EL1.
CNTHCTL_EL2.write(CNTHCTL_EL2::EL1PCEN::SET + CNTHCTL_EL2::EL1PCTEN::SET);
// No offset for reading the counters.
CNTVOFF_EL2.set(0);
接下来,我们配置Hypervisor Configuration Register,使EL1
在AArch64
模式下运行,而不是在AArch32
模式下运行,这也是可能的。
// Set EL1 execution state to AArch64.
HCR_EL2.write(HCR_EL2::RW::EL1IsAarch64);
实际上,从较高的EL过渡到较低的EL只有一种方式,即通过执行ERET指令。
在这个指令中,它将会将Saved Program Status Register - EL2的内容复制到Current Program Status Register - EL1
,并跳转到存储在Exception Link Register - EL2。
这基本上是在发生异常时所发生的相反过程。您将在即将发布的教程中了解更多相关内容。
// Set up a simulated exception return.
//
// First, fake a saved program status where all interrupts were masked and SP_EL1 was used as a
// stack pointer.
SPSR_EL2.write(
SPSR_EL2::D::Masked
+ SPSR_EL2::A::Masked
+ SPSR_EL2::I::Masked
+ SPSR_EL2::F::Masked
+ SPSR_EL2::M::EL1h,
);
// Second, let the link register point to kernel_init().
ELR_EL2.set(crate::kernel_init as *const () as u64);
// Set up SP_EL1 (stack pointer), which will be used by EL1 once we "return" to it. Since there
// are no plans to ever return to EL2, just re-use the same stack.
SP_EL1.set(phys_boot_core_stack_end_exclusive_addr);
正如您所看到的,我们将ELR_EL2
的值设置为之前直接从入口点调用的kernel_init()
函数的地址。最后,我们设置了SP_EL1
的堆栈指针。
您可能已经注意到,堆栈的地址作为函数参数进行了传递。正如您可能记得的,在boot.s
的_start()
函数中,
我们已经为EL2
设置了堆栈。由于没有计划返回到EL2
,我们可以直接重用相同的堆栈作为EL1
的堆栈,
因此使用函数参数将其地址传递。
最后,在_start_rust()
函数中调用了ERET
指令。
#[no_mangle]
pub unsafe extern "C" fn _start_rust(phys_boot_core_stack_end_exclusive_addr: u64) -> ! {
prepare_el2_to_el1_transition(phys_boot_core_stack_end_exclusive_addr);
// Use `eret` to "return" to EL1. This results in execution of kernel_init() in EL1.
asm::eret()
}
在main.rs
中,我们打印current privilege level
,并额外检查SPSR_EL2
中的掩码位是否传递到了EL1
:
$ make chainboot
[...]
Minipush 1.0
[MP] ⏳ Waiting for /dev/ttyUSB0
[MP] ✅ Serial connected
[MP] 🔌 Please power the target now
__ __ _ _ _ _
| \/ (_)_ _ (_) | ___ __ _ __| |
| |\/| | | ' \| | |__/ _ \/ _` / _` |
|_| |_|_|_||_|_|____\___/\__,_\__,_|
Raspberry Pi 3
[ML] Requesting binary
[MP] ⏩ Pushing 14 KiB =========================================🦀 100% 0 KiB/s Time: 00:00:00
[ML] Loaded! Executing the payload now
[ 0.162546] mingo version 0.9.0
[ 0.162745] Booting on: Raspberry Pi 3
[ 0.163201] Current privilege level: EL1
[ 0.163677] Exception handling state:
[ 0.164122] Debug: Masked
[ 0.164511] SError: Masked
[ 0.164901] IRQ: Masked
[ 0.165291] FIQ: Masked
[ 0.165681] Architectural timer resolution: 52 ns
[ 0.166255] Drivers loaded:
[ 0.166592] 1. BCM PL011 UART
[ 0.167014] 2. BCM GPIO
[ 0.167371] Timer test, spinning for 1 second
[ 1.167904] Echoing input now
请检查英文版本,这是最新的。