【LC-3汇编实战】从键盘中断到操作系统雏形:一次完整的中断机制构建
1. LC-3中断机制全景解读
第一次接触LC-3的中断机制时,我盯着KBSR寄存器的IE位发了半小时呆。直到在仿真器里看到用户程序被键盘输入突然"打断"的瞬间,才真正理解中断为何被称为操作系统的基石。LC-3虽然只有256个内存地址用于中断向量表,但已经完整包含了现代操作系统的中断处理雏形。
中断的本质是硬件对CPU的"紧急呼叫"。当键盘按下时,硬件会做三件事:设置KBSR的最高位(准备就绪)、检查IE位是否开启(是否允许呼叫)、若允许则通过中断向量x80发起呼叫。这时CPU会立即保存现场(PC和PSR压栈),跳转到中断服务程序(ISR),就像快递员突然放下手中的包裹去处理更紧急的签收。
在LC-3上构建中断机制需要三个核心组件:
- 用户程序:持续输出"ICS"字符串的"普通任务"
- 键盘ISR:处理键盘输入的"紧急任务"
- OS支持代码:搭建舞台的"幕后工作者"
实测中我发现一个关键细节:LC-3的中断向量表起始地址是x0100,但键盘中断向量x80对应的入口地址却要存放在x0180(x0100 + x80)。这个偏移计算让我栽过跟头——有次误写成x0080导致中断永远无法触发。
2. 用户程序:被中断的"苦力"
用户程序就像流水线上的工人,机械地重复着输出工作。但为了让人类眼睛能看清输出,必须加入延时逻辑。在LC-3上实现延时有个经典套路:
DELAY ST R1, SAVE_R1 ; 保存寄存器 LD R1, COUNT ; 加载倒计时值 LOOP ADD R1, R1, #-1 ; 计数器递减 BRp LOOP ; 正数则继续循环 LD R1, SAVE_R1 ; 恢复寄存器 RET SAVE_R1 .BLKW 1 COUNT .FILL #25000 ; 约0.5秒延时这个延时子程序通过空循环消耗CPU周期。我在调试时发现,当COUNT设为50000时,键盘响应会有明显延迟——这说明中断虽然能打断主程序,但若ISR执行时间过长,仍然会影响系统响应速度。
用户程序交替输出两种ICS图案的设计也很巧妙:
ICS ICS ICS ICS ICS ICS ICS ICS ICS ICS ICS这种视觉差能直观验证程序是否在正常运行。有次我忘记在图案间插入换行符(x0A),结果屏幕显示乱成一团。
3. 键盘中断服务程序:低调的救火队员
键盘ISR的工作流程就像消防员出警:
- 保存现场(寄存器压栈)
- 检查KBDR获取输入字符
- 通过DSR/DDR轮询方式输出字符(不能用TRAP指令)
- 恢复现场(寄存器出栈)
- RTI指令返回
最易出错的是第3步。LC-3的显示输出需要先检查DSR状态位:
POLL LDI R1, DSR ; 加载显示状态 BRzp POLL ; 未就绪则继续轮询 STI R0, DDR ; 就绪则输出字符我曾尝试去掉轮询直接输出,结果字符丢失率高达30%。更坑的是,ISR中若误用TRAP指令会导致无限递归——因为TRAP本身也是通过中断实现的。
一个实用的调试技巧:在ISR开始时先输出特定字符(如'#'),这样在终端能看到中断触发的准确时刻。有次我发现字符重复输出10次的逻辑失效,最终排查是因为忘记初始化循环计数器R2。
4. 操作系统支持代码:看不见的脚手架
没有操作系统支持的LC-3就像没有地基的房子。我们需要手动搭建三个关键设施:
4.1 栈空间初始化
LD R6, STACK_TOP ; R6作为栈指针 STACK_TOP .FILL x3000x3000这个地址我修改过三次:第一次x4000超出内存范围,第二次x2000与程序地址冲突,最终x3000才稳定工作。栈生长方向也很反直觉——LC-3的栈是向低地址增长的。
4.2 中断向量表设置
LD R1, VECTOR_ADDR LD R2, ISR_START STR R2, R1, #0 ; 将ISR地址存入向量表 VECTOR_ADDR .FILL x0180 ISR_START .FILL x2000这里有个隐藏知识点:STR指令只能存储到PCoffset9范围内的地址,因此必须先用LD加载基地址。我曾在STR指令里直接写x0180,结果汇编器报错"offset out of range"。
4.3 中断使能设置
LD R3, IE_MASK STI R3, KBSR ; 设置中断使能位 IE_MASK .FILL x4000 ; 0100_0000_0000_0000x4000这个魔数让我困惑很久,直到用二进制表示才明白:它正是KBSR的第14位(IE位)的掩码。在调试时,可以读取KBSR的值验证IE位是否设置成功。
5. 中断机制的深层思考
完成这个实验后,我重新审视了中断对操作系统的意义。当用户程序在输出第五个ICS图案时被键盘中断打断,现场保护流程如下:
- PC(当前指令地址)压栈
- PSR(程序状态寄存器)压栈
- 跳转到ISR
- ISR执行完毕后RTI指令弹出PSR和PC
这个过程实现了真正的"无缝切换"。我在PSR中特意设置了不同的优先级位,验证了低优先级中断可以被高优先级中断嵌套的特性。
中断向量表的设计更是精妙——通过硬件级的间接寻址,使得操作系统可以在不修改用户程序的情况下,动态更换ISR的实现。这让我联想到现代操作系统通过syscall表实现的功能热更新。
最后分享一个血泪教训:在同时调试用户程序和ISR时,务必先用.ORIG明确指定各自的加载地址。有次我忘记写.ORIG x2000,导致ISR代码覆盖了用户程序,整个系统崩溃得莫名其妙。
