Gerenciamento de Níveis de Exceção no ARMv8-A: Transição de EL2 para EL1 em Rust

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 Makefile provido 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.

Tags: ARMv8-A Rust Bare-Metal Exception-Levels raspberry-pi

Publicado em 7-9 07:58