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 dorustuppara 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 estruturaPanicInfo, 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 aPanicInfo(ignorada com_infoneste 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 umpanic_handlerem 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, comoobjdump,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 orustcusa 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
filecomando: 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 de0x0aqui é comum quando o ponto de entrada é gerido externamente ou através de um símbolo_startdefinido 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.