原文链接:
http://cxc200026.blog.163.com/blog/static/34268672009127101618670/
应朋友之约写一个DOS下的时钟程序,要求在屏幕右上角显示实时时间,想了下有两种方案:一种是在系统中安装一个时钟中断hook,每次发生时钟中断时计数,累计18次之后刷新显示,程序驻留内存但不活动;第二种就简单了,程序在前台运行,循环更新时间,更新间隔(1秒)必须用CPU空转耗过去,这样方法只用于演示获取时间和打印字符串的一些函数调用,没有实用价值。
下面是第一种方案的代码:
; clock.asm
; 此程序的功能是在DOS系统中安装一个时钟,一直在屏幕右上角显示当前时间;
; 程序修改了中断向量表,并使相关代码常驻内存;但目前并不具备卸载功能;
; 通过int 21 - 2ch功能调用取得当前时间,然后每隔18次时钟中断更新一次显示;
; 在Microsoft MASM 5中编译通过,编译步骤为:
; masm clock;
; link clock;
; 龙第九子 2009/02/26
; 龙第九子 2009/02/28修改
stack segment stack 'stack'
dw 32 dup(?)
stack ends
code segment
clock proc far
assume ss:stack, cs:code
jmp start
old_i8 dd ? ; 保存旧的时钟中断向量
cursor dw ? ; 保存旧的光标位置
hour dw 0 ; 保存当前小时数
minute dw 0 ; 保存当前分钟数
second dw 0 ; 保存当前分钟数
tickcnt dw 1 ; 时钟中断计数器
obuf db "00:00:00" ; 输出缓冲区
new_i8 proc far ; 新的时钟中断处理器
pushf ;
call dword ptr cs:old_i8 ; 先调用原时钟中断处理程序
dec cs:tickcnt ;
jz show ; tickcnt变为0时刷新显示
iret ; 否则,中断返回
show: mov cs:tickcnt, 18
sti ; 及时开中断,使CPU能响应外设的中断请求
; 保护现场 pusha
push ax
push bx
push cx
push dx
push si
push di
push bp
push sp
push ds
push es
mov ax,cs
mov ds,ax
mov es,ax
;保存原光标位置
mov bh,0
mov ah,3
int 10h
mov cursor,dx
call showtm ; 获取并显示当前时间
;恢复光标位置
mov bh,0
mov dx,cursor
mov ah,2
int 10h
pop es
pop ds
; 恢复现场 popa
pop sp
pop bp
pop di
pop si
pop dx
pop cx
pop bx
pop ax
iret
new_i8 endp
showtm proc near ; 在屏幕右上角显示当前时间
; DOS功能调用2Ch:real-time -> ch:cl:dh
mov ah, 2ch
int 21h
mov byte ptr hour, ch
mov byte ptr minute, cl
mov byte ptr second, dh
; hour -> obuf[0,1]
mov cx, 10
mov ax, hour
div cl
add al, 30h
mov obuf[0], al
add ah, 30h
mov obuf[1], ah
; minute -> obuf[3,4]
mov ax, minute
div cl
add al, 30h
mov obuf[3], al
add ah, 30h
mov obuf[4], ah
; second -> obuf[6,7]
mov ax, second
div cl
add al, 30h
mov obuf[6], al
add ah, 30h
mov obuf[7], ah
; display
mov ax, cs
mov es, ax
mov bp, offset obuf ; es:[bp] is the address of the string
mov al, 0 ; flag
mov bh, 0 ; page
mov bl, 0Eh ; character property
mov cx, 8 ; character count
mov dx, 46h ; cursor potition
mov ah, 13h
int 10h
ret
showtm endp
;;;;;;;;;;;;;;;;;;;;;;;; Entry Point ;;;;;;;;;;;;;;;;;;;;;;;;
start: ; 把返回地址设置在PSP开头
push ds
xor ax, ax
push ax
mov ax, code
mov ds, ax
; 保存旧的时钟中断向量
mov ax, 3508h
int 21h
mov word ptr old_i8, bx
mov word ptr old_i8[2], es
; 设置新的时钟中断向量
mov dx, offset new_i8
mov ax, 2508h
int 21h
; 退出后仍驻留内存
mov dx, offset start+15 ; 计算中断处理程序所占用的字节数,+15为了>>4后向上去整
mov cl, 4
shr dx, cl
add dx, 10h ; 驻留的长度还需包括PSP的内容(100h个字节=10h节)
mov ax, 3100h
int 21h
clock endp
code ends
end start
int 21H(DOS功能调用)
AH=2CH 取系统时间并存放到CH:CL:DH 没有参数
AH=31H 终止用户程序并驻留在主存 AL=退出码 DX=驻留节数(从PSP开始的程序总字节数/16)
int10H(显示器驱动程序)
AH=13H 写字符串到指定的光标位置
AL(0)等于0时表示写后光标不动,等于1时表示写后修改光标位置;AL(1)等于0表示在BL中包含字符属性,等于1时表示字符属性包含在字符串中;BH=页号;CX=字符串长度;(DH DL)=起始光标位置(行 列);ES:[BP]=待显示字符串的起始地址。
8位字符属性的定义:[0-2]表示字符的前景颜色;[3]表示字符亮度“增亮/普通”;[4-6]为字符的背景颜色;[7]表示字符是否闪烁。[0-2]&[4-6]的值为:000黑 001蓝 010绿 011青 100红 101洋红 110褐 111白
以上代码在MASM5中编译通过,在Win32 ntvdm中测试可用,但是程序运行时ntvdm会发生一些诡异的问题,不知道是不是和兼容性有关。在FreeDOS下面测试根本就通过不了。看改系统中断向量表的做法是危险的,或者说我水平比较菜,干不了那么高深的活儿。下面是第二种解决方案的主调过程代码:
;;;;;;;;;;;;;;;;;;;;;;;; Entry Point ;;;;;;;;;;;;;;;;;;;;;;;;
start: push ds
xor ax, ax
push ax
loop1: ; refresh display before each loop
call get_tm
call showtm
; loop 40000^2 times, about 1 sec
mov cx, 40000
loop2: mov dx, 40000
loop3: sub dx, 1
jnz loop3
sub cx, 1
jnz loop2
sub bx, 1
mov ah, 0Bh
int 21h
test al, al
je loop1
ret
编程过程中主要参考了 王元珍等《80X86汇编语言程序设计》,以及网上一些代码,在此表示感谢。
[龙第九子 2009-02-27]