我们之前学习过panic,这个部分对于Rust是非常重要的,例如索引等内置操作在运行时进行内存安全检查,当尝试超出边界索引时,这会导致恐慌。在标准库(Std)中,panic有一个定义的行为:它展开 panic 线程的堆栈,除非用户选择在 panic 时中止程序,否则会进行错误位置的一个堆栈定位。
但是,在没有标准库的程序中,恐慌行为是未定义的。可以通过声明函数来选择行为。此函数必须在程序的依赖关系图中只出现一次,并且必须具有以下签名:,其中 PanicInfo 是包含有关 panic 位置的信息的结构。#[panic_handler] fn(&PanicInfo) -> !
对于panic,有如下的一些常用种类:
1.panic_halt:死机会导致程序或当前线程通过进入无限循环而停止。
2.panic_abort:紧急导致中止指令被执行。
3.panic_itm:使用 ITM(一种特定于 ARM cortex-M 的外设)记录死机消息。
4.panic_semihosting:使用半主机技术将死机消息记录到主机,这个常用在qemu中,可以打印出错误信息。
这些panic的使用方法如下:
use panic_halt as _
use panic_semihosting as _
如果不重命名,编译器会警告说你有一个未使用的导入,当然不是错误,可以编译通过。
下面通过一个例子来理解一下,panic的使用方法,是一个数组访问越界的一个错误。
#![no_main]
#![no_std]
use panic_semihosting as _;
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
let xs = [0, 1, 2];
let i = xs.len() + 1;
let _y = xs[i]; //访问越界,加下划线变量就是说未使用不警告。
loop {}
}
出现以下运行结果,用的是semihosting,会打印输出
这里可以看到,对于panic,是程序运行时自动判断的,整个代码也就加了个semihosting的处理,使用起来很方便。你也可以试试其他类型的panic,观察结果的不同,其实也就是不同的panic处理方式。
异常和中断是处理器处理异步事件和致命错误(例如执行无效指令)的硬件机制。异常意味着抢占,并涉及异常处理程序,即响应触发事件的信号而执行的子例程,这就是和。是硬件上的,而panic理解成软件上的,比如访问文件不存在啊、访问越界啊、除0操作等等,而且panic一般没有那么多处理方式,没有抢占和触发panic处理程序等等。cortex-m-rt crate 库提供了一个异常属性标志#[exception]用来声明一个异常处理。
SysTick是系统定时器,能产生周期性的中断。每当中断产生时,就执行处理函数SysTick()。系统定时器要能够产生定时中断,就要先初始化,设置时钟源,设置计数值,启动计数器,开启中断。
详细代码如下:
#![deny(unsafe_code)]
#![no_main]
#![no_std]
use panic_halt as _;
use core::fmt::Write;
use cortex_m::peripheral::syst::SystClkSource;
use cortex_m_rt::{entry, exception};
use cortex_m_semihosting::{
debug,
hio::{self, HStdout},
};
#[entry]
fn main() -> ! {
let p = cortex_m::Peripherals::take().unwrap();
let mut syst = p.SYST;
//将系统计时器配置为每秒触发一次SysTick异常
syst.set_clock_source(SystClkSource::Core);
syst.set_reload(12_000_000);//周期 = 1s
syst.clear_current();
syst.enable_counter();//使能systick计数器
syst.enable_interrupt();//使能中断
loop {}
}
#[exception]
fn SysTick() {
static mut COUNT: u32 = 0;
static mut STDOUT: Option<HStdout> = None;
*COUNT += 1;
// Lazy initialization
if STDOUT.is_none() {
*STDOUT = hio::hstdout().ok();
}
if let Some(hstdout) = STDOUT.as_mut() {
write!(hstdout, "{}", *COUNT).ok();
}
if *COUNT == 9 {
//qemu中运行到第九秒暂停退出
debug::exit(debug::EXIT_SUCCESS);
}
}
终端打印123456789后退出。
该属性的实际作用是覆盖特定异常的默认异常处理程序。如果不重写特定异常的处理程序,则该函数将由函数处理,该函数默认为:exceptionDefaultHandler
fn DefaultHandler() {
loop {}
}
你可以在“默认处理程序”上放置断点并捕获未处理的异常,当然也可以重写和覆盖。
当程序进入无效状态时,将触发此异常,因此其处理程序无法返回,因为这可能导致未定义的行为。此外,运行时crate在调用用户定义的处理程序之前会执行一些工作,以提高可调试性。处理程序的参数是指向由异常推送到堆栈中的寄存器的指针。这些寄存器是触发异常时处理器状态的快照,可用于诊断硬故障。HardFaultfn(&ExceptionFrame) -> !
下面是一个执行非法操作的示例:读取不存在的内存位置。该程序在QEMU上不起作用,即它不会崩溃,因为不会检查内存负载,并且会在读取无效内存时返回。
#![no_main]
#![no_std]
use panic_halt as _;
use core::fmt::Write;
use core::ptr;
use cortex_m_rt::{entry, exception, ExceptionFrame};
use cortex_m_semihosting::hio;
#[entry]
fn main() -> ! {
// 读取不存在的内存地址,注意写法
unsafe {
ptr::read_volatile(0x3FFF_FFFE as *const u32);
}
loop {}
}
#[exception]
fn HardFault(ef: &ExceptionFrame) -> ! {
if let Ok(mut hstdout) = hio::hstdout() {
writeln!(hstdout, "{:#?}", ef).ok();
}
loop {}
}
处理程序打印值。如果运行此命令,您将在 OpenOCD 控制台上看到类似的内容。
该值是发生异常时程序计数器的值,它指向触发异常的指令,所以可以看一下pc的值指向的指令就是错误的地方。
这里可以看到,是因为ldr指令出现错误,也就是加载读取指令。