Removendo Dependências da Biblioteca Padrão em Rust para Bare-Metal

Preparando um Projeto Rust para Ambientes Embarcados

Este guia aborda o processo de adaptação de um projeto Rust básico para operar em um ambiente bare-metal, removendo as dependências da biblioteca padrão (std) e introduzindo as configurações necessárias para compilação cruzada.

Eliminando a Macro println!

Para começar, precisamos configurar o ambiente de desenvolvimento Rust para suportar a compilação para um alvo específico que não possui um sistema operacional. Isso é fundamental para o desenvolvimento bare-metal.

Adicionando Suporte a Alvo Bare-Metal com rustup

O primeiro passo é instalar o alvo de compilação necessário usando rustup:

rustup target add riscv64gc-unknown-none-elf

  • rustup: Gerenciador de toolchains do Rust, que facilita a instalação e gerenciamento de versões do Rust e seus componentes, incluindo alvos de compilação cruzada.
  • target add: Um subcomando do rustup para incluir suporte a um novo alvo de compilação.
  • riscv64gc-unknown-none-elf: Este é o "triple" que descreve o alvo de compilação:
    • riscv64gc: Indica uma arquitetura RISC-V de 64 bits, com o conjunto de instruções "G" (general-purpose) e "C" (compressed).
    • unknown: Designa que não há um fornecedor específico ou variante de sistema operacional.
    • none: Especifica que não há uma biblioteca padrão do sistema operacional, o que é crucial para ambientes bare-metal.
    • elf: Define o formato de arquivo de saída como ELF (Executable and Linkible Format), comum em sistemas Unix-like e para executáveis em muitos ambientes embarcados.

Configurando o Cargo para Compilação Cruzada

Para que o Cargo compile automaticamente para o alvo RISC-V, criamos um arquivo de configuração:

mkdir .cargo
cd .cargo
touch config

Adicione o seguinte conteúdo ao arquivo config (ou config.toml, que é a versão moderna):

# os/.cargo/config
[build]
target = "riscv64gc-unknown-none-elf"

Esta configuração instrui o Cargo a usar riscv64gc-unknown-none-elf como o alvo de compilação padrão, em vez do alvo nativo da máquina de desenvolvimento (por exemplo, x86_64-unknown-linux-gnu). Este cenário, onde a plataforma de desenvolvimento difere da plataforma de execução, é conhecido como compilação cruzada.

Desabilitando a Biblioteca Padrão no Código-Fonte

No arquivo src/main.rs, adicione a diretiva #![no_std] na primeira linha. Isso informa ao compilador Rust para não vincular a biblioteca padrão (std) e, em vez disso, utilizar a biblioteca core, que é um subconjunto mais fundamental sem dependências do sistema operacional.

#![no_std]
fn main() {
    println!("Hello, world!");
}

Ao tentar compilar com cargo build, observaremos um erro:

error: cannot find macro `println` in this scope
 --> src/main.rs:3:5
  |
3 |     println!("Hello, world!");
  |     ^^^^^^^

error: `#[panic_handler]` function required, but not found

error: could not compile `os` (bin "os") due to 2 previous errors

Este log indica que a macro println! não está disponível na biblioteca core. Além disso, uma função #[panic_handler] é necessária.

Para resolver o erro do println!, precisamos removê-lo ou comentá-lo. O arquivo main.rs ficará assim:

#![no_std]
fn main() {
    // println!("Hello, world!");
}

Implementando um Tratador de Pânico Personalizado

Após comentar a macro println!, uma nova tentativa de compilação (cargo build) revelará a ausência do tratador de pânico:

error: `#[panic_handler]` function required, but not found

error: could not compile `os` (bin "os") due to 1 previous error

Contexto do panic_handler

Em aplicações Rust que utilizam a biblioteca padrão, o macro panic! é implementado para imprimir informações de erro e encerrar a aplicação. No entanto, em ambientes no_std, a biblioteca core fornece apenas uma interface para panic!, sem uma implementação concreta. Por questões de segurança e robustez, o compilador Rust exige uma implementação de panic_handler para lidar com erros irrecuperáveis. Em um sistema operacional ou firmware bare-metal, precisamos definir como o sistema deve se comportar em caso de pânico.

Criando a Implementação do panic!

Crie um novo arquivo para o tratador de pânico:

cd os/src
touch lang_items.rs

Adicione o seguinte código a os/src/lang_items.rs:

// os/src/lang_items.rs

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

  • use core::panic::PanicInfo;: Importa a estrutura PanicInfo, que contém detalhes sobre a ocorrência do pânico (localização, mensagem, etc.).
  • #[panic_handler]: Este atributo especial marca a função seguinte como o ponto de entrada global para o tratamento de pânico no programa.
  • fn panic(_info: &PanicInfo) -> !: Define a função de pânico. Ela recebe uma referência a PanicInfo (ignorada com _info neste exemplo simples) e retorna o tipo "never" (!), indicando que a função nunca retorna ao seu chamador, pois o programa é interrompido ou entra em um estado irrecuperável.
  • loop {}: A implementação mais básica para um panic_handler em bare-metal é um loop infinito. Isso evita que o programa continue executando código potencialmente inválido após um erro fatal. Em sistemas reais, esta função poderia reiniciar o sistema, registrar logs de erro, etc.

Declarando o Módulo

Para que o compilador encontre a função panic, declare o novo módulo em main.rs:

#![no_std]
mod lang_items; // Adicione esta linha

fn main() {
    // println!("Hello, world!");
}

Gerenciando o Ponto de Entrada do Programa

Ao compilar novamente (cargo build), o Rust sinaliza outra questão:

error: using `fn main` requires the standard library
  |
  = help: use `#![no_main]` to bypass the Rust generated entrypoint and declare a platform specific entrypoint yourself, usually with `#[no_mangle]`

error: could not compile `os` (bin "os") due to 1 previous error

Este erro informa que a função fn main padrão do Rust depende da biblioteca padrão. Em um ambiente no_std, precisamos definir nosso próprio ponto de entrada.

Para contornar isso, podemos simplesmente remover a função main, deixando o main.rs assim:

#![no_std]
mod lang_items;

Uma nova tentativa de compilação (cargo build) resultará em:

error[E0601]: `main` function not found in crate `os`
 --> src/main.rs:2:16
  |
2 | mod lang_items;
  |                ^ consider adding a `main` function to `src/main.rs`

O compilador ainda espera uma função main ou um ponto de entrada alternativo.

Para resolver isso, adicionamos a diretiva #![no_main] na primeira linha do main.rs. Isso instrui o compilador a não injetar o ponto de entrada padrão do Rust, permitindo que definamos o nosso próprio.

#![no_main]
#![no_std]
mod lang_items;

// Opcional: Um ponto de entrada bare-metal seria definido aqui, por exemplo:
// #[no_mangle]
// extern "C" fn _start() -> ! {
//     loop {}
// }

Com esta modificação, a compilação final com cargo build deve ser bem-sucedida:

   Compiling os v0.1.0 (/home/user/workspace/os)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.67s

O projeto Rust está agora configurado para compilação bare-metal, sem dependências da biblioteca padrão, com um tratador de pânico personalizado e sem o ponto de entrada main padrão.

Análise do Binário Gerado para Ambiente Bare-Metal

Com o programa compilado, podemos agora inspecionar o binário resultante para entender melhor sua estrutura e características em um ambiente bare-metal.

Instalando Ferramentas de Análise

Para analisar o binário ELF, utilizaremos ferramentas adicionais do ecossistema Rust e LLVM:

cargo install cargo-binutils
rustup component add llvm-tools-preview

  • cargo install cargo-binutils: Instala um conjunto de utilitários de binários integrados ao Cargo, como objdump, nm, size, que são essenciais para examinar arquivos executáveis.
  • rustup component add llvm-tools-preview: Adiciona as ferramentas LLVM ao seu toolchain Rust. Como o rustc usa o LLVM para geração de código, essas ferramentas (e.g., llvm-objdump) são úteis para análise profunda.

Determinando o Formato do Arquivo

Para verificar o formato do arquivo binário, usamos o comando file:

file target/riscv64gc-unknown-none-elf/debug/os

A saída esperada é:

./target/riscv64gc-unknown-none-elf/debug/os: ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), statically linked, with debug_info, not stripped

  • file comando: Um utilitário Unix/Linux que identifica o tipo de um arquivo.
  • ELF 64-bit LSB executable: Confirma que é um executável ELF de 64 bits, com ordenação de bytes little-endian (LSB).
  • UCB RISC-V: Indica a arquitetura RISC-V.
  • RVC: Suporte ao conjunto de instruções RISC-V Compressed (reduz o tamanho do código).
  • double-float ABI: A Interface Binária da Aplicação (ABI) usa ponto flutuante de dupla precisão.
  • statically linked: Todas as bibliotecas necessárias estão embutidas no executável, ideal para ambientes sem sistema operacional.
  • with debug_info, not stripped: Contém informações de depuração e não foi otimizado para tamanho (não "stripado"), útil para depuração.

Analisando o Cabeçalho ELF

Para uma inspeção mais detalhada do cabeçalho do arquivo ELF, usamos rust-readobj:

rust-readobj -h target/riscv64gc-unknown-none-elf/debug/os

A saída detalha a estrutura do cabeçalho ELF:

File: target/riscv64gc-unknown-none-elf/debug/os
Format: elf64-littleriscv
Arch: riscv64
AddressSize: 64bit
LoadName: <not found="">
ElfHeader {
  Ident {
    Magic: (7F 45 4C 46)
    Class: 64-bit (0x2)
    DataEncoding: LittleEndian (0x1)
    FileVersion: 1
    OS/ABI: SystemV (0x0)
    ABIVersion: 0
    Unused: (00 00 00 00 00 00 00)
  }
  Type: Executable (0x2)
  Machine: EM_RISCV (0xF3)
  Version: 1
  Entry: 0x0
  ProgramHeaderOffset: 0x40
  SectionHeaderOffset: 0x18F8
  Flags [ (0x5)
    EF_RISCV_FLOAT_ABI_DOUBLE (0x4)
    EF_RISCV_RVC (0x1)
  ]
  HeaderSize: 64
  ProgramHeaderEntrySize: 56
  ProgramHeaderCount: 4
  SectionHeaderEntrySize: 64
  SectionHeaderCount: 12
  StringTableSectionIndex: 10
}
</not>

Pontos chave da análise do cabeçalho:

  • Format: elf64-littleriscv: Confirma o formato ELF de 64 bits para RISC-V little-endian.
  • Arch: riscv64: Arquitetura de processador é RISC-V de 64 bits.
  • Entry: 0x0: Indica que o ponto de entrada (endereço inicial de execução) é zero. Em sistemas bare-metal, o carregador (ou firmware) define onde o código começa a ser executado, e um valor de 0x0 aqui é comum quando o ponto de entrada é gerido externamente ou através de um símbolo _start definido pelo usuário.
  • Flags: Detalha as capacidades do RISC-V, como suporte a ponto flutuante de dupla precisão e instruções comprimidas (RVC).

Desmontando o Binário

Para ver o código de máquina gerado, podemos usar rust-objdump para desmontar o executável:

rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os

Este comando gera o código Assembly correspondente ao binário, com a opção -S que tenta intercalar as linhas de código-fonte Rust quando informações de depuração estão disponíveis. A saída inicial confirma o formato do arquivo:

target/riscv64gc-unknown-none-elf/debug/os:     file format elf64-littleriscv

A análise completa do Assembly seria extensa, mas o importante é que esta ferramenta nos permite examinar como o código Rust é traduzido para instruções de máquina para o alvo RISC-V bare-metal.

Tags: Rust BareMetal RISC-V no_std Cross-Compilation

Publicado em 6-5 06:23 por Thomas