资讯详情

资讯详情

rust-raspberrypi-OS-tutorials 教程 18:在 AArch64 裸机内核中实现基于帧指针的 Backtrace

rust-raspberrypi-OS-tutorials 教程 18在 AArch64 裸机内核中实现基于帧指针的 Backtrace【免费下载链接】rust-raspberrypi-OS-tutorials:books: Learn to write an embedded OS in Rust :crab:项目地址: https://gitcode.com/gh_mirrors/ru/rust-raspberrypi-OS-tutorials本教程讲解如何在自研 Rust 裸机内核中实现栈回溯backtrace / stack trace能力借助 AArch64 调用约定中由x29帧指针与x30链接寄存器构建的栈帧链实现一个能在 panic 时逐帧打印地址 函数符号调用链的模块并通过编译器选项、build-std、异常入口汇编与启动代码的配合让回溯链既完整又可校验。读完本文你将掌握栈帧记录Stack Frame Record的数据结构设计、迭代器式的回溯遍历、地址合法性校验以及如何把它集成进 panic handler 并编写对应的集成测试。本文基于仓库中18_backtrace教程目录正文中的源码路径均以仓库根目录为基准例如 kernel/src/backtrace.rs。前置教程17_kernel_symbols已在内核中实现符号名查询能力这是本教程回溯打印函数名的前提。tl;dr一次 panic 中的回溯输出在上一教程内核符号表的基础上18_backtrace在内核中加入了 backtrace 支持。触发一次 panic例如向地址空间底部地址写入 1 GiB 处后控制台会输出如下内容[ 0.002782] Writing to bottom of address space to address 1 GiB... [ 0.004623] Kernel panic! Panic location: File kernel/src/_arch/aarch64/exception.rs, line 59, column 5 [...] Backtrace: ---------------------------------------------------------------------------------------------- Address Function containing address ---------------------------------------------------------------------------------------------- 1. ffffffffc0005560 | libkernel::panic_wait::_panic_print 2. ffffffffc00054a0 | rust_begin_unwind 3. ffffffffc0002950 | core::panicking::panic_fmt 4. ffffffffc0004898 | current_elx_synchronous 5. ffffffffc0000a74 | __vector_current_elx_synchronous 6. ffffffffc000111c | kernel_init ----------------------------------------------------------------------------------------------可以看到从最内层的_panic_print一路回溯到异常向量入口__vector_current_elx_synchronous最终停在内核入口kernel_init。这正是本教程要实现的核心效果。Introduction为什么要加回溯上一教程之后内核已经能够通过symbols::lookup_symbol()把地址翻译成符号名见 kernel/src/symbols.rs 中的lookup_symbol。在此基础上实现有意义的回溯stack trace便水到渠成。回溯最主要的应用场景是在panic 期间打印调用链这能大幅简化调试。选择在这个时间点加入该特性是因为后续教程将涉及复杂主题与大量代码变更调试利器自然越早准备越好。Implementation回溯代码的组织结构回溯机制通常由处理器架构的调用约定calling convention决定因此与体系结构强耦合回溯代码的核心必然放在_arch目录中而不同架构可以共享的是格式化与打印部分。代码因此被组织成两部分kernel/src/backtrace.rs通用定义BacktraceItem并提供使用IteratorItem BacktraceItem完成格式化和打印的代码。kernel/src/_arch/aarch64/backtrace.rs生成实际迭代器的架构相关代码。BacktraceItem的定义如下摘自 kernel/src/backtrace.rspub enum BacktraceItem { InvalidFramePointer(AddressVirtual), InvalidLink(AddressVirtual), Link(AddressVirtual), }它包含两个错误情形InvalidFramePointer、InvalidLink和一个有效情形Link。为什么需要错误情形看完栈帧与帧指针的本质后就会明白回溯遍历的是内存中的指针链而裸机内核的内存可能被破坏任何一步都可能遇到非法地址因此遍历必须对每一步做校验并把错误显式暴露给打印端。Chasing Frames追逐栈帧链AAPCS64 与 ARMv8-A 指南怎么说对于 AArch64需要参考 Procedure Call Standard for the Arm® 64-bit ArchitectureAAPCS64。其核心要求如下符合规范的代码应构造一个栈帧的链表。每个帧通过栈上的一个由两个 64 位值组成的帧记录frame record与数据模型无关链接到其调用者的帧。最内层帧属于最近一次例程调用的帧记录应由帧指针寄存器FP指向。地址最低的双字double-word应指向上一个帧记录地址最高的双字应包含函数入口时传入 LR 的值 [...]. 帧记录在栈帧内的位置不作规定。再看 ARM Cortex-A Series Programmer’s Guide for ARMv8-A 的对应章节链表结构更清晰一个 AAPCS64 栈帧如图 9-2 所示。帧指针X29应指向保存在栈上的上一个帧指针保存的 LRX30紧随其后。链中最后一个帧指针应置为 0。栈指针必须始终按 16 字节边界对齐。这正是栈帧链的图景用 Rust 表达栈帧记录依据上述规范可以在 kernel/src/_arch/aarch64/backtrace.rs 中定义如下栈帧记录结构体#[repr(C)] struct StackFrameRecorda { previous_record: Optiona StackFrameRecorda, link: AddressVirtual, }有趣的是previous_record成员。从上面两份文档可以得知地址最低的双字要么是零要么指向上一个栈帧记录。得益于 Rust 的空指针优化null pointer optimization这个成员可以非常自然地写成OptionStackFrameRecord值为None表示已经到达回溯的终点链尾值为Some(frame)则继续沿链回溯。迭代器从帧指针出发回溯的起点可以通过x29帧指针寄存器FP直接获得。在 stack_frame_record_iterator 中先对帧指针地址做一次有效性检查再据此构造迭代器struct StackFrameRecordIteratora { cur: a StackFrameRecorda, } /// [...] fn stack_frame_record_iteratora() - OptionStackFrameRecordIteratora { let fp Address::Virtual::new(FP.get() as usize); if !fp.is_valid_stack_addr() { return None; } Some(StackFrameRecordIterator { cur: unsafe { *(fp.as_usize() as *const _) }, }) }虽然理论上编译器以及任何手写汇编应保证x29指向合法的栈地址但在生成引用之前做一次检查依然有意义——内存损坏随时可能发生。迭代器自身的next()实现同样会在每次推进时做健全性检查并且在把link地址交给调用方之前还会确认它位于内核合法的code段内kernel/src/_arch/aarch64/backtrace.rsimpla Iterator for StackFrameRecordIteratora { type Item BacktraceItem; fn next(mut self) - OptionSelf::Item { static ABORT_FRAME: StackFrameRecord StackFrameRecord { previous_record: None, link: Address::new(0), }; // If previous is None, this is the root frame, so iteration will stop here. let previous self.cur.previous_record?; // Need to abort if the pointer to the previous frame record is invalid. let prev_addr Address::Virtual::new(previous as *const _ as usize); if !prev_addr.is_valid_stack_addr() { // This allows to return the error and then stop on the next iteration. self.cur ABORT_FRAME; return Some(BacktraceItem::InvalidFramePointer(prev_addr)); } let ret if !self.cur.link.is_valid_code_addr() { Some(BacktraceItem::InvalidLink(self.cur.link)) } else { // The link points to the instruction to be executed _after_ returning from a branch. // However, we want to show the instruction that caused the branch, so subtract by one // instruction. // // This might be called from panic!, so it must not panic itself on the subtraction. let link if self.cur.link Address::new(4) { self.cur.link - 4 } else { self.cur.link }; Some(BacktraceItem::Link(link)) }; // Advance the iterator. self.cur previous; ret } }这段代码有三个值得细读的点ABORT_FRAME哨兵帧当检测到非法的上一帧指针时把self.cur指向一个previous_record None的静态哨兵帧从而让先返回一个InvalidFramePointer错误下一次迭代自然停止。link 减 4LR 保存的是分支返回后要执行的那条指令的地址而回溯希望展示的是发起分支的那条指令所以需要减掉一条指令的长度AArch64 固定 4 字节。同时该代码可能从panic!中被调用因此减法本身必须用checked_sub语义保护这里表现为 Address::new(4)判断底层实现见 kernel/src/memory.rs 的Subusize其内部使用checked_sub溢出时才会 panic。两种校验方法is_valid_stack_addr()检查地址是否落在引导核栈区域内is_valid_code_addr()检查地址是否落在内核代码页区域内。两者的实现位于 kernel/src/memory.rs分别委托给 kernel/src/bsp/raspberrypi/memory/mmu.rs 中的virt_boot_core_stack_region()与virt_code_region()这两个区域原本是mmu.rs的私有函数本教程将它们提升为pub供内存模块复用。通用打印端符号查找与格式化架构部分到此就是核心了。在通用部分打印回溯时BacktraceItem::Link返回的地址还会被用来查询对应符号便于直接打印函数名kernel/src/backtrace.rsmatch backtrace_res { // omitted BacktraceItem::Link(addr) { fmt_res writeln!( f, {:2}. {:016x} | {:50}, i 1, addr.as_usize(), match symbols::lookup_symbol(addr) { Some(sym) sym.name(), _ Symbol not found, } ) } };打印格式为序号. 16 位十六进制地址 | 左对齐 50 字符的符号名。两个错误分支则分别输出ERROR! Encountered invalid frame pointer (...) during backtraceERROR! Link address (...) is not contained in kernel .text section另外两个实现细节打印时iter.skip(1)由于回溯打印本身就是从core::fmt::write开始的第一帧必然是它跳过以避免输出膨胀见 kernel/src/backtrace.rs。若迭代器根本构造不出来起始帧指针就不是合法栈地址打印端输出ERROR! No valid stack frame found。打印通过impl fmt::Display for Backtrace完成Backtrace是一个零大小的伪结构体这样在println!中直接传入backtrace::Backtrace即可触发格式化见 kernel/src/backtrace.rs。集成进 panic handler最后把回溯打印加入panic!kernel/src/panic_wait.rsprintln!( [ {:3}.{:06}] Kernel panic!\n\n\ Panic location:\n File {}, line {}, column {}\n\n\ {}\n\n\ {}, timestamp.as_secs(), timestamp.subsec_micros(), location, line, column, info.message().unwrap_or(format_args!()), backtrace::Backtrace );注意_panic_exit被声明为#[linkage weak]集成测试可以覆盖它测试构建时走cpu::qemu_exit_failure()退出普通构建则cpu::wait_forever()详见 kernel/src/panic_wait.rs。Compiler Changes强制生成帧记录默认情况下aarch64-unknown-none*目标并不保证每次函数调用都会生成栈帧记录——没有帧记录回溯代码就无法工作。好在可以通过修改 rustc 的 codegen 选项强制生成。在Makefile中添加如下内容见 Makefileifeq ($(BSP),rpi3) # omitted RUSTC_MISC_ARGS -C target-cpucortex-a53 -C force-frame-pointersrpi4 分支同理RUSTC_MISC_ARGS -C target-cpucortex-a72 -C force-frame-pointers。但仅有这一项还不够此前编译内核时cargo 使用的是 rustup 添加目标时随附的预编译版 Rust core 库。这在编译速度上通常非常有利但预编译版本并没有用-C force-frame-pointers编译。解决办法是使用 cargo 的 [build-std特性]在 Makefile 中设置它让 cargo 用我们的编译器设置一并编译 core 库从而让 core 库函数也获得帧记录# build-std can be skipped for helper commands that do not rely on correct stack frames and other # custom compiler options. This results in a huge speedup. RUSTC_CMD cargo rustc $(COMPILER_ARGS) -Z build-stdcore --manifest-path $(KERNEL_MANIFEST) DOC_CMD cargo doc $(COMPILER_ARGS) CLIPPY_CMD cargo clippy $(COMPILER_ARGS) TEST_CMD cargo test $(COMPILER_ARGS) -Z build-stdcore --manifest-path $(KERNEL_MANIFEST)注意build-std目前仍是 nightly 特性需要通过-Z标志开启DOC_CMD与CLIPPY_CMD不依赖正确的栈帧刻意保持原样以换取构建速度这也是 Makefile 注释里helper commands 可以跳过 build-std的原因。完整的命令构建区见 Makefile。Supporting Changes配套改动速览README 没有在正文中展开、但值得浏览的配套改动包括kernel/src/_arch/aarch64/exception.s在异常入口处构建栈帧记录文件内含大量详细注释。核心逻辑CALL_WITH_CONTEXT宏异常上下文保存区从16 * 17扩大到16 * 18个双字新增的第 18 个双字用于存放栈帧记录x29 返回地址见 exception.s。若异常来自更低特权级is_lower_el 1直接存储xzr, xzr把它做成根帧防止内核回溯进入用户空间。若异常发生在当前特权级则用ELR_EL1而非 LR构建栈帧从而可以穿越异常继续回溯。但ELR_EL1的语义随异常类型而变除非执行的是产生异常的指令exception generating instructionELR_EL1已经指向正确指令此时回溯代码里的减 4会得到错误结果。为此汇编里先检查ESR_EL1.EC是否为SVC640x15不是 SVC 的情况下先对ELR_EL1加 4再存入帧记录BRK/HLT也属于产生异常的指令但当前不预期出现暂未处理。相关常量通过global_asm!从 kernel/src/_arch/aarch64/exception.rs 传入。kernel/src/_arch/aarch64/cpu/boot.rs新增prepare_backtrace_reset()在进入 EL1 前把FP与LR都清零并配compiler_fence(Ordering::SeqCst)防止重排从而保证kernel_init()成为回溯链的根——即其帧记录的previous_record必然为None。根目录 Cargo.toml 中设置了debug true确保内核 ELF 携带最大量的调试信息。注意这不会改变内核运行时的任何行为但它允许你在拿到回溯报告的地址后用addr2line挖得更深。对比一下debug关闭与打开时的差异$ # debug false $ addr2line -p -f -s -i -e target/aarch64-unknown-none-softfloat/release/kernelttablessymbols 0xffffffffc0001da8 | rustfilt kernel::kernel_main at kernel.c562062a-cgu.1:?$ # debug true $ addr2line -p -f -s -i -e target/aarch64-unknown-none-softfloat/release/kernelttablessymbols 0xffffffffc0001da8 | rustfilt libkernel::memory::mmu::mapping_record::MappingRecord::print at mapping_record.rs:136 (inlined by) libkernel::memory::mmu::mapping_record::kernel_print::{{closure}} at mapping_record.rs:232 (inlined by) libkernel::synchronization::InitStateLockT as libkernel::synchronization::interface::ReadWriteEx::read at synchronization.rs:139 (inlined by) libkernel::memory::mmu::mapping_record::kernel_print at mapping_record.rs:232 (inlined by) libkernel::memory::mmu::kernel_print_mappings at mmu.rs:269 (inlined by) kernel::kernel_main at main.rs:84debug true之后addr2line甚至能还原内联展开inlined by的完整调用链。命令中-e指定的kernelttablessymbols正是 Makefile 中KERNEL_ELF $(KERNEL_ELF_TTABLES_SYMS)的产物Makefilerustfilt用于把 Rust 符号解混淆。另外配套的tools/translation_table_tooltools/translation_table_tool/generic.rb也做了小幅增强支持带下划线十六进制字面量如0x0123_abcd的解析并将分区大小输出改为可读的 MiB/KiB 格式。Test it验证回溯的正确性本教程新增了三个针对回溯代码健全性的集成测试同时所有此前会打印 panic 的测试现在也会顺带打印回溯。例如02_exception_sync_page_fault.rs$ TEST02_exception_sync_page_fault make test_integration [...] ------------------------------------------------------------------- Testing synchronous exception handling by causing a page fault ------------------------------------------------------------------- [ 0.002782] Writing to bottom of address space to address 1 GiB... [ 0.004623] Kernel panic! Panic location: File kernel/src/_arch/aarch64/exception.rs, line 59, column 5 CPU Exception! ESR_EL1: 0x96000004 Exception Class (EC) : 0x25 - Data Abort, current EL Instr Specific Syndrome (ISS): 0x4 FAR_EL1: 0x0000000040000000 [...] Backtrace: ---------------------------------------------------------------------------------------------- Address Function containing address ---------------------------------------------------------------------------------------------- 1. ffffffffc0005560 | libkernel::panic_wait::_panic_print 2. ffffffffc00054a0 | rust_begin_unwind 3. ffffffffc0002950 | core::panicking::panic_fmt 4. ffffffffc0004898 | current_elx_synchronous 5. ffffffffc0000a74 | __vector_current_elx_synchronous 6. ffffffffc000111c | kernel_init ---------------------------------------------------------------------------------------------- ------------------------------------------------------------------- ✅ Success: 02_exception_sync_page_fault.rs -------------------------------------------------------------------三个新增测试每个均有一对 Rust 测试内核 Ruby 断言脚本注册在 kernel/Cargo.toml 的[[test]]段中05_backtrace_sanitykernel/tests/05_backtrace_sanity.rs kernel/tests/05_backtrace_sanity.rbnested()中直接panic!()验证 panic 产生回溯且回溯中依次出现| core::panicking::panic、| _05_backtrace_sanity::nested与| kernel_init证明整条链正确。06_backtrace_invalid_framekernel/tests/06_backtrace_invalid_frame.rs kernel/tests/06_backtrace_invalid_frame.rb调用backtrace::corrupt_previous_frame_addr()仅test_buildfeature 下导出见 kernel/src/backtrace.rs 与 kernel/src/_arch/aarch64/backtrace.rs把当前帧的previous_record覆写为0x123验证输出Encountered invalid frame pointer (.*) during backtrace。07_backtrace_invalid_linkkernel/tests/07_backtrace_invalid_link.rs kernel/tests/07_backtrace_invalid_link.rb调用backtrace::corrupt_link()kernel/src/_arch/aarch64/backtrace.rs把当前帧的 link 覆写为0x456验证输出Link address (.*) is not contained in kernel .text section。运行方式与既有测试一致例如$ TEST05_backtrace_sanity make test_integration $ TEST06_backtrace_invalid_frame make test_integration $ TEST07_backtrace_invalid_link make test_integrationDiff to previous与上一教程的改动全貌以下是17_kernel_symbols与18_backtrace之间的核心差异完整内容见 README 的 Diff to previous 一节此处摘录要点diff -uNr 17_kernel_symbols/Cargo.toml 18_backtrace/Cargo.toml [profile.release] lto true debug true diff -uNr 17_kernel_symbols/kernel/Cargo.toml 18_backtrace/kernel/Cargo.toml [package] name mingo -version 0.17.0 version 0.18.0 [[test]] name 03_exception_restore_sanity harness false [[test]] name 05_backtrace_sanity harness false [[test]] name 06_backtrace_invalid_frame harness false [[test]] name 07_backtrace_invalid_link harness false其他主要改动文件包括新增 kernel/src/backtrace.rs通用回溯模块与 kernel/src/_arch/aarch64/backtrace.rs架构回溯实现并在 kernel/src/lib.rs 中公开pub mod backtrace。kernel/src/memory.rs为AddressATYPE新增Subusize内部checked_sub为AddressVirtual新增is_valid_stack_addr()/is_valid_code_addr()。kernel/src/bsp/raspberrypi/memory/mmu.rsvirt_code_region()与virt_boot_core_stack_region()由私有改为pub。kernel/src/panic_wait.rspanic 输出中加入backtrace::Backtrace。kernel/src/_arch/aarch64/cpu/boot.rsprepare_backtrace_reset()清零 FP/LR。kernel/src/_arch/aarch64/exception.rs 与 kernel/src/_arch/aarch64/exception.sglobal_asm!传入ESR_EL1相关常量CALL_WITH_CONTEXT宏扩展为带is_lower_el、is_sync参数并在异常上下文之外构建栈帧记录。MakefileRUSTC_MISC_ARGS增加-C force-frame-pointersRUSTC_CMD/TEST_CMD增加-Z build-stdcoregdb目标不再单独设置-C debuginfo2因为根 Cargo.toml 已全局开启debug true。tools/translation_table_tool/generic.rb 与 tools/translation_table_tool/bsp.rb支持带下划线十六进制字面量解析、人类可读的分区大小输出。小结18_backtrace为内核补上了调试三件套中的最后一块拼图符号表负责把地址翻译成名字回溯负责把散落的地址串成调用链debug true则让addr2line能在链上继续深挖内联细节。实现的关键在于三处协同——-C force-frame-pointers保证每个函数都有帧记录、build-std让 core 库也满足同样约定、异常入口与启动代码则确保回溯链的两端异常向量与kernel_init都正确封口。配合三个针对正常链、坏帧指针、坏 link的集成测试这套回溯机制既实用又可验证为后续教程中更复杂的内核特性开发提供了可靠的调试基础。【免费下载链接】rust-raspberrypi-OS-tutorials:books: Learn to write an embedded OS in Rust :crab:项目地址: https://gitcode.com/gh_mirrors/ru/rust-raspberrypi-OS-tutorials创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →