6.1 异常与中断处理:向量表排错实录


6.1 异常与中断处理:向量表排错实录

本节摘要:异常到来时硬件自动完成"存返回点、存状态、查向量、跳转"四连,软件负责保存现场、处理、恢复、eret。本节拆解 AArch64 向量表十六槽位的布局规则,走通一次定时器中断的完整流程,并以"向量表地址未对齐"这一经典故障为样本演示排错套路。

第 2 章讲异常级别时,我们把 svc 那趟电梯坐了一遍。本节把电梯系统完整拆开:异常的入口怎么组织、进来之后软件要还哪些债、常见故障怎么查。这一节的内容在内核调试与裸机开发里每天都会用到,是"认识中断"到"驾驭中断"的分水岭。

硬件侧:不容商量的四连

异常发生的瞬间,硬件自动做四件事,一条指令都不经过你:把返回地址存进 elr_elx(同步异常存的是当前指令地址,中断存的是下一条);把 pstate 快照存进 spsr_elx;切换到目标异常级别(沿用现级别或上浮一级,由异常类型与配置决定);从向量表取出对应槽位的入口地址跳过去。注意硬件不保存任何通用寄存器——这既是设计(保存全部寄存器太贵,多数异常用不了几个),也是责任(软件必须保存自己要动的每一个,第 5 章 AAPCS 的保存责任在这里原样适用)。

向量表:十六个槽位的地图

AArch64 向量表是 2048 字节、十六个槽位,每个槽位 128 字节(放一段短桩或直接放处理代码)。布局规则是三个维度各分一半:来源异常级别(当前级别使用、低级别 A64、低级别 A32)、异常类型(同步、IRQ、FIQ、系统错误)、奇偶(同类成对,偶数槽给当前 SP 方式,奇数槽给另一方式)。初学不必背全表,先记住最常用的一条路径:内核态来的 IRQ 落在第 9 槽附近(从基址偏移 0x80 的第 9 个 128 字节)。设置基址的指令一行:

ldr x0, =vectors // 向量表标号 msr vbar_el1, x0 // 基址必须是 2048 字节对齐——记住这个数,排错要用 isb // 改系统寄存器后冲流水线,第 4 章的老朋友

槽位里的短桩通常长这样:保存现场必要部分、跳到统一的分发函数、分发完恢复、eret。因为 128 字节放不下完整处理,真实系统都是"小桩加大分发器"结构。

实录:一次定时器中断的完整旅程

光说不练假把式,走一遍最小可用的定时器中断。第一步,向量表骨架(节选第 9 槽与两个兜底槽):

.balign 2048 // 强制对齐,vbar 的硬要求 vectors: .space 0x400 // 前 8 槽:本节实验用不到,留空 // 第 9 槽:当前级别 SP 方式的 IRQ stp x29, x30, [sp, #-16]! bl irq_handler ldp x29, x30, [sp], #16 eret .space 0x380 // 其余槽位同样留空,真实系统每个都要填

第二步,处理函数:读定时器状态寄存器清中断、干正事、发结束信号:

void irq_handler(void) { unsigned int status = *TIMER_STATUS; if (status & TIMER_BIT_PENDING) { *TIMER_ACK = TIMER_BIT_PENDING; // 清中断,否则退出后又立刻进来 ticks++; // 正事:计数 } }

第三步,在 C 侧配置定时器周期、使能中断、解除 IRQ 屏蔽(msr daifclr, #2)。烧进模拟器跑起来,ticks 开始跳动——中断系统活了。解读这套流程的三个要点:清中断必须早于处理完成,否则 eret 一返回马上再次进入("中断风暴"是新手第一大坑);处理函数遵守 AAPCS,被它用到的被调方保存寄存器编译器自动安排保存;daif 屏蔽位决定 IRQ 能不能进来,排错时先查它。

图 6-2:十六槽向量表地图与本次实验的落点

图 6-2:十六槽向量表地图与本次实验的落点

排错实录:卡死在启动的中断

把本节压轴给一个真实故障复盘。某实验代码烧上去后中断一次不来,程序还在跑(主循环在转),就是 ticks 不动。按清单四步走:daif 屏蔽——读 pstate 发现 I 位是 1,IRQ 全被屏蔽,msr daifclr, #2 放行后……依然不动。第二查 vbar——读出来 0x40020104,末尾不是 0x800 的倍数,没对齐,硬件直接不认。修正链接脚本把向量表节对齐到 2048 后,中断如期而至。解读这个案例的教训:两处错误叠加时,修掉第一个不代表问题解决,清单要查到底;而"程序还在跑"恰恰说明异常级别与主流程都没事,问题必然在"入口没接上"这一层。变式:把清中断那行注释掉再跑,你会看到 ticks 飞速狂飙后系统卡死——中断风暴现场。见过一次,以后遇到"中断越处理越多"就能秒判。

深挖:中断控制器与嵌套的两个为什么

向量表解决"来了往哪跳",中断控制器解决"谁来的、谁先来"。通用中断控制器(GIC)是 ARM 生态的标准答案:外设中断先进 GIC 分发器,按优先级与目标核配置路由,再由接口模块通知核心。对汇编工程师需要掌握的最小集合是三件事:查"哪个中断号在响"读 GIC 的确认寄存器(读动作同时完成仲裁锁定)、处理完向同一寄存器写回结束信号(不清账下一个同优先级中断不再放行)、中断号到处理函数的映射表由软件自己维护(向量表第 9 槽的桩把所有 IRQ 汇到一处,分发靠这张表)。

嵌套中断要不要开,是一个诚实的取舍题。开嵌套(处理函数内再放行中断)能压低高优先级事件的延迟,代价是栈深度不可控与重入复杂度骤增——处理函数自身的中断安全、共享数据的多层保护全都来了。裸机小系统的常见决策是不开嵌套、缩短关键区:处理函数尽量短,把非紧急工作排进主循环。判断依据是"最长处理时长"与"最高事件频率"的比值——比值接近 1,才值得上嵌套。给中断处理时长做测量也很简单:入口出口各读一次计时器(7.2 节的读计数器样本),差值即时长,超标就重构。

常见问答

问:为什么我的中断偶尔"丢一次"? 答:多数是电平触发与脉冲触发配置错位——电平中断在清除源之前保持有效,脉冲中断则一闪而过;软件在屏蔽窗口期错过了脉冲,事件就没了。对策按外设手册核对触发类型,脉冲类外设改用挂锁存寄存器兜底。

本节要点回顾

  • 硬件四连:存返回点、存状态、换级别、查向量跳转,通用寄存器一概不管。
  • 十六槽地图:级别、类型、方式三维各分半,内核 IRQ 常走第 9 槽。
  • vbarisb:基址 2048 对齐是硬门槛,改完必须冲流水线。
  • 清中断先行:处理完不 ack 就有中断风暴,eret 变成无限循环。
  • 排错四步:屏蔽、对齐、外设使能、槽位内容,从粗到细一次查穿。

中断这条路走通了,就该试试"完全没有操作系统"的世界了。下一节把启动代码逐行拆给你看。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U