1. 项目概述
这个时钟程序是我在1993年用汇编语言编写的一个小型驻留程序(TSR),它能在DOS环境下常驻内存,并在屏幕右上角显示当前时间,到整点时还会发出报时音效。程序编译后仅有1KB大小,但包含了多个实用的功能模块,展示了汇编语言编程的几个核心技巧。
程序的主要特点包括:
- 通过修改内存控制块实现隐蔽驻留,不会被常规内存查看命令(MEM/MI)发现
- 截取INT 9键盘中断实现热键控制
- 直接操作硬件端口控制扬声器发声
- 实时计算并显示系统时间
- 支持多种显示模式适配(文本/图形模式)
提示:这个程序虽然年代久远,但其中涉及的底层编程思想至今仍有参考价值,特别是在嵌入式开发和系统编程领域。
2. 核心技术解析
2.1 内存驻留机制
程序采用了经典的TSR(Terminate and Stay Resident)技术,通过修改内存控制块(MCB)实现隐蔽驻留。具体实现步骤如下:
- 计算程序所需内存空间
- 调用INT 21h的31h功能终止并驻留
- 修改MCB中的PSP字段,使程序不被常规内存工具检测到
关键代码片段:
asm复制mov ah, 31h ; TSR功能号
mov dx, (offset end_program + 15) / 16 ; 计算驻留段落数
int 21h ; 调用DOS中断
这种技术在现代操作系统中已不适用,但在当时是常见的程序驻留方式,对理解内存管理机制很有帮助。
2.2 时钟中断处理
程序通过接管INT 1Ch(时钟中断)来实现时间计算和显示更新:
- 保存原始INT 1Ch向量
- 设置新的中断处理程序
- 在中断处理程序中:
- 读取BIOS计时器值(0040:006C)
- 转换为时:分:秒格式
- 更新屏幕显示
时间计算算法解析:
asm复制mov cx, es:[046eh] ; 读取BIOS计时器高字
mov dx, es:[046ch] ; 读取BIOS计时器低字
; 将计时器值转换为秒数
; (cx:dx)*5/(65536*65536/86400)
2.3 键盘中断截取
程序通过接管INT 9(键盘硬件中断)实现热键功能:
- 保存原始INT 9向量
- 设置新的键盘中断处理程序
- 检测特定组合键(Ctrl+Alt+U/C/O)
- 执行相应功能(卸载/改颜色/静音)
关键检测逻辑:
asm复制in al, 60h ; 读取键盘扫描码
cmp al, 22h ; 'U'键扫描码
jz unload_program ; 如果是卸载热键
3. 程序功能详解
3.1 时间显示功能
程序在屏幕右上角(默认位置)显示当前时间,格式为HH:MM:SS。显示逻辑会根据当前视频模式自动调整:
- 检测显示模式(INT 10h)
- 确定显存地址(0B800h或0B000h)
- 计算显示位置(默认第72列)
- 将时间数字转换为ASCII字符
- 写入显存
显示位置调整代码:
asm复制cmp al, 3 ; 检查显示模式
jbe text_mode ; 如果是文本模式
mov word ptr v_buffer-2, es ; 图形模式使用当前段
text_mode:
mov di, 72*2 ; 默认显示位置(第72列)
3.2 报时功能
程序会在以下时间点触发报时音效:
- 整点(12:00:00等)
- 每10分钟(10:00:00, 20:00:00等)
- 59分59秒(准备整点报时)
音效控制通过直接操作8253定时器和8255并行接口实现:
asm复制mov al, 0B6h ; 设置定时器模式
out 43h, al ; 写入控制端口
mov ax, bx ; 设置频率值
out 42h, al ; 写入低字节
mov al, ah
out 42h, al ; 写入高字节
in al, 61h ; 打开扬声器
or al, 3
out 61h, al
3.3 热键控制功能
程序支持以下组合键操作:
- Ctrl+Alt+U: 卸载程序
- Ctrl+Alt+C: 改变时钟颜色
- Ctrl+Alt+O: 开关报时音效
- Ctrl+Alt+B: 改变背景颜色
热键检测流程:
- 检查键盘状态字节(0040:0017)
- 确认Ctrl和Alt键按下
- 读取扫描码确认具体按键
- 执行相应功能
4. 程序使用指南
4.1 编译与运行
-
使用MASM或TASM汇编器编译:
code复制masm clock.asm; link clock.obj; exe2bin clock.exe clock.com -
直接运行编译后的COM文件:
code复制clock.com -
程序将驻留内存,在屏幕右上角显示时间
4.2 热键操作
- Ctrl+Alt+U: 立即卸载程序,释放内存
- Ctrl+Alt+C: 循环切换时钟显示颜色
- Ctrl+Alt+O: 切换报时音效开关状态
- Ctrl+Alt+B: 循环切换背景颜色
4.3 自定义修改
-
修改初始显示颜色:
更改源代码中的DISPLAY_COLOR EQU 0EH(默认黄色) -
调整显示位置:
修改clock_place标签处的值(默认72*2) -
改变报时模式:
修改is_min0和is_sec0处的逻辑
5. 技术难点与解决方案
5.1 精确时间计算
挑战:DOS系统没有直接提供获取当前时间的API,需要从BIOS计时器计算
解决方案:
- 读取BIOS计时器值(0040:006C)
- 转换为秒数(每天计时器溢出约18.2次)
- 计算时、分、秒
关键算法:
asm复制; cx:dx = 计时器值
; 转换为秒数 = (cx:dx)*5/65536
mov ax, cx
mov bx, dx
shl dx, 1
rcl cx, 1
shl dx, 1
rcl cx, 1
add dx, bx
adc ax, cx
5.2 多显示模式适配
挑战:不同显示模式使用不同的显存地址和布局
解决方案:
- 检测当前显示模式(INT 10h)
- 根据模式选择显存段地址:
- 文本模式:0B800h
- 单色模式:0B000h
- 图形模式:当前段
- 调整显示位置避免冲突
5.3 中断处理冲突
挑战:接管系统中断可能与其他程序冲突
解决方案:
- 保存原始中断向量
- 在自定义处理程序中调用原始处理程序
- 卸载时恢复原始向量
关键代码:
asm复制; 保存原始INT 1Ch
mov ax, 351Ch
int 21h
mov word ptr [OFF1C], bx
mov word ptr [SEG1C], es
; 恢复原始INT 1Ch
mov dx, word ptr [OFF1C]
mov ds, word ptr [SEG1C]
mov ax, 251Ch
int 21h
6. 编程技巧与最佳实践
6.1 高效的内存驻留
- 精确计算驻留大小,避免浪费内存
- 清理不需要的初始化代码
- 使用COM格式减少开销
6.2 健壮的中断处理
- 保存和恢复所有使用的寄存器
- 处理重入问题(关键部分加锁)
- 尽快完成中断处理
6.3 优化显示更新
- 只在时间变化时更新显示
- 使用直接显存写入提高速度
- 避免屏幕闪烁(垂直回扫期更新)
示例代码:
asm复制; 等待垂直回扫开始
mov dx, 3DAh
wait_vsync:
in al, dx
test al, 8
jnz wait_vsync
; 更新显示
mov cx, 8
rep movsw
7. 常见问题排查
7.1 程序无法驻留
可能原因:
- 内存不足
- 已驻留相同程序
- 文件格式错误
解决方案:
- 检查可用内存(MEM命令)
- 确认没有重复驻留
- 确保使用COM格式
7.2 时间显示不正确
可能原因:
- 中断冲突
- 计时器计算错误
- 显示位置错误
解决方案:
- 检查中断向量(INT 1Ch)
- 验证时间计算算法
- 调整显示位置参数
7.3 热键无响应
可能原因:
- 其他程序截获键盘中断
- 热键冲突
- 键盘状态检测错误
解决方案:
- 检查INT 9链
- 更改热键组合
- 调试键盘状态检测
8. 扩展与改进建议
- 增加日期显示:扩展程序显示当前日期
- 添加闹钟功能:允许设置自定义提醒时间
- 支持更多显示位置:通过参数指定显示坐标
- 国际化支持:添加多语言时间格式
- 节能模式:在闲置时减少CPU占用
实现闹钟功能的伪代码:
asm复制; 新增变量
alarm_hour db 0
alarm_min db 0
; 在INT 1Ch中检查
mov al, [clock]
cmp al, [alarm_hour]
jne no_alarm
mov al, [minu]
cmp al, [alarm_min]
jne no_alarm
; 触发闹钟
这个时钟程序虽然小巧,但涵盖了汇编语言编程的多个关键方面。通过研究它的源代码,可以深入理解DOS环境下的中断处理、内存管理、硬件控制和TSR编程等技术。这些底层知识对于理解现代操作系统的运作机制仍有重要参考价值。