异常向量表与异常入口
好,咱们接着聊异常处理。上一章我讲了异常的基本概念,这一章咱们深入底层,看看异常向量表到底长什么样,怎么用。
说实话,我刚接触VxWorks时,觉得异常向量表就是个跳转列表。后来踩过坑才明白——这东西要是没配好,系统连启动都启动不了。你想想看,CPU一上电,第一条指令就是去查异常向量表。如果表是空的或者地址错了,那系统直接就“飞”了。
异常向量表的结构
异常向量表,说白了就是一张地址映射表。每个异常类型对应一个固定的表项,表项里存放的是异常处理函数的入口地址。
在ARM架构下,异常向量表通常放在内存的起始位置,也就是0x00000000处。每个表项占4个字节,一共8个异常类型,总共32个字节。我列个表给你看:
| 偏移地址 | 异常类型 | 说明 |
|---|---|---|
| 0x00 | 复位异常 | 系统上电或复位时触发 |
| 0x04 | 未定义指令异常 | CPU遇到不认识的操作码 |
| 0x08 | 软件中断异常 | SWI指令触发,常用于系统调用 |
| 0x0C | 预取指令中止异常 | 取指令时发生错误 |
| 0x10 | 数据中止异常 | 读写数据时发生错误 |
| 0x14 | 保留 | ARM架构保留未用 |
| 0x18 | IRQ中断 | 普通中断请求 |
| 0x1C | FIQ中断 | 快速中断请求,优先级最高 |
每个表项里放的是什么?是一条跳转指令。比如:
B _excHandlerReset ; 复位异常跳转
B _excHandlerUndef ; 未定义指令跳转
B _excHandlerSwi ; 软件中断跳转
B _excHandlerPrefetch ; 预取中止跳转
B _excHandlerDataAbort ; 数据中止跳转
B _excHandlerReserved ; 保留
B _excHandlerIrq ; IRQ跳转
B _excHandlerFiq ; FIQ跳转
嗯,这里要注意:有些架构允许放绝对地址,但ARM推荐用跳转指令。为什么?因为跳转指令是位置无关的,你挪动代码位置也不用改表项。我在项目中遇到过有人直接放地址,结果代码重定位后系统直接崩溃——那场面,真是惨不忍睹。
异常入口函数的编写
异常入口函数,就是真正处理异常的地方。但你不能直接写C函数就完事了。为啥?因为异常发生时,CPU的上下文(寄存器、状态位)是混乱的。你得先保存现场,再处理异常,最后恢复现场。
我一般这样写入口函数:
_excHandlerDataAbort:
// 第一步:保存上下文
STMFD sp!, {r0-r12, lr} // 压栈保存通用寄存器和返回地址
// 第二步:获取异常原因
MRS r0, cpsr // 读取当前程序状态寄存器
AND r0, r0, #0x1F // 提取模式位
// 第三步:调用C语言处理函数
BL excDataAbortHandler // 跳转到C函数处理
// 第四步:恢复上下文
LDMFD sp!, {r0-r12, lr} // 出栈恢复寄存器
// 第五步:返回异常点
SUBS pc, lr, #8 // 修正返回地址并跳回
这里有个关键点:返回地址的修正。数据中止异常发生时,LR寄存器保存的是异常指令的下一条指令地址。但你想继续执行,得回到异常指令本身重新尝试。所以需要减8(ARM模式下每条指令4字节,两条指令就是8字节)。
我个人习惯在入口函数里加一个调试钩子:
_excHandlerDataAbort:
STMFD sp!, {r0-r12, lr}
// 调试钩子:记录异常发生时的PC值
LDR r0, =excDebugFlag
LDR r1, [r0]
CMP r1, #1
BNE skipDebug
BL excSaveDebugInfo
skipDebug:
MRS r0, cpsr
AND r0, r0, #0x1F
BL excDataAbortHandler
LDMFD sp!, {r0-r12, lr}
SUBS pc, lr, #8
这个钩子平时是关的,出问题时打开,能帮你定位到异常发生的精确位置。我曾经靠这个钩子,花了一下午就找到了一个内存越界bug——要是没有它,估计得查三天。
异常向量表的初始化
初始化异常向量表,说白了就是把表放到正确的位置,然后告诉CPU“表在这里”。
在VxWorks中,初始化通常在sysHwInit()函数里完成。我一般这样做:
STATUS sysExcVecInit(void)
{
UINT32 *vecTable;
int i;
// 第一步:分配向量表空间
// 注意:必须放在非缓存区域,否则异常发生时缓存还没初始化
vecTable = (UINT32 *)EXC_VEC_BASE;
// 第二步:填充向量表
// 每个表项放一条跳转指令
for (i = 0; i < 8; i++)
{
vecTable[i] = EXC_BRANCH_INSTR(i);
}
// 第三步:刷新指令缓存
// 确保CPU看到的是最新的表内容
cacheInvalidate(DATA_CACHE);
cacheInvalidate(INSTRUCTION_CACHE);
// 第四步:设置向量表基址寄存器
// 不同架构寄存器不同,这里以ARM为例
__asm__ volatile (
"MCR p15, 0, %0, c12, c0, 0\n\t"
:
: "r" (vecTable)
);
return OK;
}
初始化完成后,最好做个自检:
STATUS sysExcVecVerify(void)
{
UINT32 *vecTable = (UINT32 *)EXC_VEC_BASE;
int i;
// 验证每个表项是否有效
for (i = 0; i < 8; i++)
{
// 检查是否是跳转指令
if ((vecTable[i] & 0xFF000000) != 0xEA000000)
{
return ERROR; // 表项损坏
}
}
return OK;
}
最后说一句:异常向量表是系统的“命门”。你把它配好了,系统就稳如泰山;配不好,那就是定时炸弹。我个人建议,每次修改BSP后都重新检查一遍向量表配置,别偷懒。
下一章咱们聊聊具体异常类型的处理策略,比如数据中止怎么恢复、未定义指令怎么处理。到时候我会分享一些实战中的坑,保证让你少走弯路。