O desenvolvimento de sistemas operacionais embarcados em arquiteturas ARMv8-A exige um controle rigoroso sobre os níveis de exceção (Expection Levels - EL). Durante o processo de boot em plataformas como o Raspberry Pi, o processador frequentemente inicia sua execução em um nível de privilégio superior, especificamente o EL2 (modo Hypervisor). Para que o núcleo do sistema operacional (Kernel) seja executado com segurança e isolamento adequados, é imperativo realizar uma transição controlada para o EL1.
Contexto dos Níveis de Exceção
A arquitetura ARMv8-A define quatro níveis de exceção (EL0 a EL3). A transição de EL2 para EL1 não é apenas uma mudança de estado, mas a fundação para o isolamento de memória e a proteção do sistema. O código responsável por essa orquestração geralmente reside nos módulos iniciais de boot, configurando o ambiente antes de saltar para a lógica principal do kernel.
Configuração de Registradores de Sistema
Antes de executar a mudança de contexto, é necessário configurar uma série de registradores de sistema para garantir que o ambiente do EL1 esteja corretamente inicializado. O processo envolve a preparação de timers, estados de execução, máscaras de interrupção e ponteiros de pilha.
// Configuração do ambiente EL1 e preparação para a transição
#[inline(always)]
unsafe fn setup_el1_environment(kernel_entry: usize, kernel_stack: usize) {
// Habilita acesso do EL1 aos contadores físicos
core::arch::asm!("msr cnthctl_el2, {val}", val = in(reg) 0x3);
// Define o estado de execução do EL1 para AArch64 e configura virtualização
core::arch::asm!("msr hcr_el2, {val}", val = in(reg) 0x80000000);
// Configura o status de programa salvo para EL1 (mascarando interrupções)
let spsr_val: u64 = 0b1101 << 6 | 0b0101; // D, A, I, F mascarados, EL1h
core::arch::asm!("msr spsr_el2, {val}", val = in(reg) spsr_val);
// Define o ponteiro de pilha (Stack Pointer) para o EL1
core::arch::asm!("msr sp_el1, {val}", val = in(reg) kernel_stack);
// Define o endereço de retorno para a rotina de inicialização do kernel
core::arch::asm!("msr elr_el2, {val}", val = in(reg) kernel_entry);
}
Execução da Transição via ERET
Uma vez que os registradores de estado e de propósito geral estejam configurados, a transição efetiva é disparada. A instrução eret (Expection Return) é o mecanismo central para essa operação. Ela instrui o processador a restaurar o contador de programa a partir do registrador de link de exceção (ELR_EL2) e o estado do processador a partir do registrador de status salvo (SPSR_EL2).
// Dispara a instrução de retorno de exceção para mudar para EL1
#[inline(always)]
unsafe fn drop_to_el1() {
core::arch::asm!(
"eret",
options(noreturn)
);
}
Espaço de Endereçamento e Detecção em Tempo de Execução
A mudança de nível de privilégio está intrinsecamente ligada à reconfiguração do espaço de endereçamento. Ao operar em EL1, o kernel ganha acesso a regiões de memória privilegiadas, isolando-as do espaço de usuário (EL0). A tabela de páginas e os registradores de tradução de endereços são fundamentais para mapear essas regiões de forma segura.
Para fins de depuração e garantia de integridade, o sistemma deve ser capaz de consultar seu nível de execução atual. Na arquitetura AArch64, isso é feito lendo o registrador CurrentEL, extraindo os bits que indicam o nível ativo.
#[derive(Debug, PartialEq, Eq)]
pub enum CurrentExceptionLevel {
EL0,
EL1,
EL2,
EL3,
}
#[inline(always)]
pub fn query_current_el() -> CurrentExceptionLevel {
let el_value: u64;
unsafe {
core::arch::asm!(
"mrs {reg}, CurrentEL",
reg = out(reg) el_value,
);
}
// Os bits [3:2] do registrador CurrentEL armazenam o nível de exceção
match (el_value >> 2) & 0x3 {
0 => CurrentExceptionLevel::EL0,
1 => CurrentExceptionLevel::EL1,
2 => CurrentExceptionLevel::EL2,
3 => CurrentExceptionLevel::EL3,
_ => unreachable!(),
}
}
Validação em Ambiente Emulado
Para validar essa implementação em um ambiente emulado, o fluxo de compilação e execução segue os padrões do ecossistema Rust para bare-metal:
- Obtenha o código-fonte do tutorial de OS em Rust para Raspberry Pi.
- Navegue até o diretório do módulo correspondente aos níveis de privilégio.
- Compile o binário utilizando o
Makefileprovido pelo projeto. - Inicie a emulação via QEMU com o comando de execução padrão (
make qemu).
Ao concluir o boot, a saída serial exibirá a confirmação de que o código do kernel está sendo executado sob o contexto do EL1, validando a transição de privilégio.