ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

在裸机 Rust 中实现 `log` crate 日志记录:以 comprehensive-rust 应用处理器篇的 `Log` trait 实战为例

在裸机 Rust 中实现 `log` crate 日志记录:以 comprehensive-rust 应用处理器篇的 `Log` trait 实战为例 在裸机 Rust 中实现logcrate 日志记录以 comprehensive-rust 应用处理器篇的Logtrait 实战为例【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust在嵌入式与裸机bare-metal开发中串口UART往往是唯一的调试输出通道而 Rust 生态中事实标准的日志门面logcrate 提供的info!、error!等宏却默认没有后端可用。本文基于 Google Android 团队的 Rust 课程项目 comprehensive-rust 中应用处理器Application Processors一节的src/bare-metal/aps/logging.md及其配套示例讲解如何在no_std裸机程序中通过实现Logtrait 为logcrate 接入一个基于 PL011 UART 的日志后端并让 panic 处理器也能输出错误日志。读完本文你将掌握Logtrait 的完整实现要点、日志初始化时序以及如何在 QEMU 的 aarch64 virt 板上运行并验证这套日志方案。背景应用处理器裸机场景下的日志需求在课程的应用处理器章节见 aps.md之前课程讨论的是 Cortex-M 之类的微控制器而本节的实验环境是基于 QEMU 的 aarch64virt虚拟板按裸机方式编程仿佛在直接编写操作系统。所有示例默认运行在异常级别 EL1入口代码位于 entry.S负责初始化处理器状态、清零 BSS、设置栈指针并启用 MMU 与缓存。在这样的环境里Rust 标准库不可用#![no_std]没有println!也没有现成的系统日志服务。好消息是logcrate 本身只是一个日志门面facade它把log、info、warn、error等宏编译成对全局Log后端的调用。我们只需要在目标平台上实现Logtrait 并注册进去就能在整个程序包括库与驱动代码中使用统一的日志宏。课程给出的做法非常直白It would be nice to be able to use the logging macros from thelogcrate. We can do this by implementing theLogtrait.——这正是本节的核心任务。核心实现实现Logtrait让日志落到 UART课程示例中日志后端的完整实现位于 logger.rs其核心代码src/bare-metal/aps/examples/src/logger.rs中的main锚点如下use crate::pl011::Uart; use core::fmt::Write; use log::{LevelFilter, Log, Metadata, Record, SetLoggerError}; use spin::mutex::SpinMutex; static LOGGER: Logger Logger { uart: SpinMutex::new(None) }; struct Logger { uart: SpinMutexOptionUartstatic, } impl Log for Logger { fn enabled(self, _metadata: Metadata) - bool { true } fn log(self, record: Record) { writeln!( self.uart.lock().as_mut().unwrap(), [{}] {}, record.level(), record.args() ) .unwrap(); } fn flush(self) {} } /// Initialises UART logger. pub fn init( uart: Uartstatic, max_level: LevelFilter, ) - Result(), SetLoggerError { LOGGER.uart.lock().replace(uart); log::set_logger(LOGGER)?; log::set_max_level(max_level); Ok(()) }这段代码可以拆解为四个要点1. 静态单例与内部可变性logcrate 的set_logger要求传入一个static的日志后端因此这里声明了一个全局静态变量LOGGER。由于Log::log接收的是self不可变引用而写串口需要可变状态课程使用spincrate 的SpinMutexOptionUartstatic实现内部可变性SpinMutex是无std环境下的自旋锁互斥量见 Cargo.toml 中的spin 0.12.0依赖避免了对操作系统锁的依赖内部用OptionUartstatic表示UART 尚未注入的状态这样静态变量可以零初始化之后再通过init注入真正的驱动实例之所以能安全地放进static单例是因为 UART 外设寄存器地址在裸机上是固定的——课程示例中 PL011 的基地址被硬编码为0x900_0000见 main_logger.rs该设备地址范围不会因程序运行而消失。2.Logtrait 的三个方法Logtrait 要求实现三个方法方法本实现的作用enabled(self, metadata: Metadata) - bool决定某条日志是否要被记录。这里直接返回true即全量放行把过滤职责完全交给set_max_level设置的级别阈值log(self, record: Record)真正输出一条日志。这里取出record.level()如INFO、ERROR与record.args()格式化参数拼成[级别] 消息的格式通过writeln!写入 UARTflush(self)冲刷缓冲。UART 是即时写入、没有软件缓冲所以实现为空操作{}这里enabled与flush的简单实现很好地体现了门面模式的用法宏调用经过enabled的快速过滤后才进入可能开销较大的log。3. 借fmt::Write复用格式化能力writeln!是core::fmt的格式化宏要求目标类型实现fmt::Writetrait。在前面的课程内容uart/traits.md中驱动为 PL011 UART 实现了Writeimpl Write for Uart_ { fn write_str(mut self, s: str) - fmt::Result { for c in s.as_bytes() { self.write_byte(*c); } Ok(()) } }完整实现见 pl011.rs。write_byte会先轮询标志寄存器等待发送 FIFO 有空位TXFF位写入数据后再等待BUSY位清零从而保证字节真正发送完毕。正是这个实现让write_str总是返回Ok——这是后文第二个 unwrap 一定成功的依据。也就是说课程把格式化 逐字节串口输出两层能力解耦Logger只负责格式与级别前缀字节级传输交给 UART 驱动。4.init函数的初始化时序init做了三件事顺序很重要LOGGER.uart.lock().replace(uart); // 1. 先注入 UART log::set_logger(LOGGER)?; // 2. 注册全局日志后端 log::set_max_level(max_level); // 3. 设置全局最大日志级别第 1 步把OptionUart从None替换为Some(uart)第 2 步调用log::set_logger注册后端返回Result(), SetLoggerError?会把重复注册之类的错误向上传递第 3 步通过log::set_max_level设置过滤阈值如LevelFilter::Trace表示全部放行。先注入 UART、再注册后端保证了注册完成的那一刻任何宏调用都能成功拿到 UART——这正是原文档details中强调的第一个 unwrap 会成功的原因。使用它初始化日志并在main与 panic 处理器中输出配套文档 logging/using.md 说明了使用方法We need to initialise the logger before we use it.必须先初始化再使用。课程示例的完整入口位于 main_logger.rs#![no_main] #![no_std] mod asm; mod exceptions; mod logger; mod pl011; use crate::pl011::Uart; use core::panic::PanicInfo; use core::ptr::NonNull; use log::{LevelFilter, error, info}; use safe_mmio::UniqueMmioPointer; use smccc::Hvc; use smccc::psci::system_off; /// Base address of the primary PL011 UART. const PL011_BASE_ADDRESS: NonNullpl011::Registers NonNull::new(0x900_0000 as _).unwrap(); // SAFETY: There is no other global function of this name. #[unsafe(no_mangle)] extern C fn main(x0: u64, x1: u64, x2: u64, x3: u64) { // SAFETY: PL011_BASE_ADDRESS is the base address of a PL011 device, and // nothing else accesses that address range. let uart unsafe { Uart::new(UniqueMmioPointer::new(PL011_BASE_ADDRESS)) }; logger::init(uart, LevelFilter::Trace).unwrap(); info!(main({x0:#x}, {x1:#x}, {x2:#x}, {x3:#x})); assert_eq!(x1, 42); system_off::Hvc().unwrap(); } #[panic_handler] fn panic(info: PanicInfo) - ! { error!({info}); system_off::Hvc().unwrap(); loop {} }这段代码展示了一个完整的裸机日志使用流程构造 UART通过safe_mmio的UniqueMmioPointer把0x900_0000这个设备地址包装为对 PL011Registers的独占访问指针再由它构造Uart。关于这套 MMIO 安全包装的细节可参考 safemmio 章节。初始化日志调用logger::init(uart, LevelFilter::Trace)传入 UART 与最大日志级别.unwrap()处理注册失败如重复调用init。正常路径打日志info!(main({x0:#x}, ...))把入口参数以十六进制打印出来随后assert_eq!(x1, 42)校验参数成功则通过 PSCIsystem_off关闭虚拟机。panic 路径打日志#[panic_handler]中调用error!({info})把PanicInfo含 panic 位置与消息完整输出到串口。这正是原文档特别指出的收益panic 处理器现在也能记录 panic 的细节了。值得注意的时序保证logger::init发生在任何info!/error!调用之前并且init内部先replace(uart)再set_logger所以log宏内部的self.uart.lock().as_mut().unwrap()一定取到Some。原文档details中对两个 unwrap 的说明可以对应到具体代码第一个 unwrapas_mut().unwrap()LOGGER在调用set_logger之前就已注入 UART因此锁内永远是Some第二个 unwrapwriteln!(...).unwrap()Uart实现的Write::write_str总是返回Ok(())见 pl011.rs 第 152-159 行因此格式化写入不会失败。运行验证在 QEMU 中跑起来课程为这一节提供了现成的 Makefile 目标。在src/bare-metal/aps/examples目录下执行make qemu_loggerMakefile中对应规则Makefile是logger.bin: build cargo objcopy --bin logger -- -O binary $ qemu_logger: logger.bin qemu-system-aarch64 -machine virt -cpu max -serial mon:stdio -display none -kernel $ -s它的完整流程是cargo build编译工作区二进制目标logger已声明在 Cargo.toml 中[[bin]] name logger, path src/main_logger.rs用cargo objcopy把 ELF 转成裸二进制logger.bin用qemu-system-aarch64启动 aarch64virt虚拟板-kernel加载logger.bin-serial mon:stdio把串口接到当前终端。运行后串口上应依次看到info!打印的入口参数日志例如[INFO] main(0x0, 0x2a, ...)其中x1的十六进制0x2a即 42以及error!在断言失败时的 panic 详情。若要修改入参验证 panic 路径可改动入口调用方的传参后重新编译观察输出。同类目标还包括qemu_minimal、qemu_rt等便于对照不同阶段的驱动能力。源码纵深Logtrait 与依赖链从实现层面回顾这套方案依赖三个关键库均声明在 Cargo.toml依赖版本在本例中的作用log0.4.30提供Logtrait、LevelFilter、Record/Metadata类型及info!/error!等宏spin0.12.0提供SpinMutex在无 OS 环境下实现单例内部可变性safe-mmio0.3.0提供UniqueMmioPointer与字段访问宏安全地访问 PL011 寄存器课程把日志后端放在独立模块logger中与入口main_logger.rs、UART 驱动pl011.rs分离是一个可以直接复用的分层模式入口负责初始化驱动负责字节传输logger 模块负责格式化与注册。当项目需要从 UART 换成其他输出如网络调试口或内存环形缓冲只需替换Logger内部的输出对象并保持Logtrait 接口不变。如果想让日志更丰富还可以扩展enabled实现例如按Metadata::level()做细粒度过滤或给flush加上真正的缓冲冲刷逻辑这些都是在保持Logtrait 契约不变的前提下对本文方案的直接延伸。【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进