简介:这份汇编语言万年历显示程序文档,面向正在学习《微机接口与汇编语言》并需要完成课程设计的学生,解决从需求分析到源码实现一整套写作与编码参考的问题。压缩包内仅含1个doc文件,体积约581KB,属于以设计报告为主体、附带源程序清单的文档型资料,可直接用于对照排版与内容组织。内容按需求与思路分析、概要设计、详细设计、程序实现四部分展开:输入模块借助INT 21H的0AH功能接收年月日并校验合法性,闰年判断子程序结合月份天数表确定当月最大天数;显示模块用蔡勒公式推算当月第一天是星期几,再按行列定位输出日历,并调用INT 10H的06H功能将星期六、日两列置为红色,遇到错误输入则提示重新输入。目前已有837人学习,适合需要理解蔡勒公式、光标定位与彩色显示细节的汇编初学者参考。
1. 用汇编写万年历,真正卡住人的不是打印 1 到 31
提起用汇编实现万年历,多数人的第一反应是「不就是循环打印 1 到 31」。真坐到屏幕前敲代码,最先崩掉的往往是另一件事:本月 1 号到底该落在第几列。这个位置由输入的年月决定,中间要穿过闰年判断、每月天数表和一条把年月日换算成星期几的公式,任何一个环节错一位,整月的格子就全部错位。更棘手的是,汇编里没有现成的字符串拼接、光标对象和颜色属性,每一次光标跳格、每一段红色区间,都要用 INT 10H 和 INT 21H 自己拼出来。这份程序把输入缓冲区、闰年判断、蔡勒公式和颜色窗口逐段拆开,正好能看清一个日历从键盘敲入到字符上屏是怎么一层层被组织起来的。
2. 输入缓冲区:INT 21H 0AH 的「账头」结构为什么不能省
键盘输入看似只是「读一行字符」,但在 DOS 中断体系里,它是一个带账头、带结束符的固定结构。程序没有用 INT 21H 的 01H 逐字符读取,而是选了 0AH 一次性接收整行——这背后有一个很现实的原因:逐字符读取需要自己处理退格、回显、换行,稍有遗漏就会让输入界面出现残影;0AH 把这些细节全部包进了中断里,程序只需要读缓冲区。
2.1 9 个字节怎么划分
缓冲区在数据段里是这样定义的:
STR1 DB 9 ; 允许输入的最大字符数(含回车) DB ? ; 实际输入的长度(不含回车),由 0AH 回填 DB 9 DUP(?) ; 9 字节接收区单独看这三行没有感觉,把它和 0AH 的约定对照起来才清楚。0AH 要求缓冲区的第一个字节预置「最大字符数,包含回车键」,调用时把首地址偏移交给 DX,按下回车后中断会把「实际字符数,不含回车」写进第二个字节,字符本身从第三个字节开始依次存放。也就是说 9 个字节的容量对应最多 8 个有效字符加 1 个回车,刚好装下输入的YYYYMMDD这一串。这就是为什么容量规划必须加 2:一个字节给最大长度,一个字节给实际长度,剩下的才是数据。少了这两个字节的账头,0AH 就会把长度写进数据区,后面的字符解析全部错位。
2.2 字符到数值:循环乘 10 加当前位
缓冲区里躺着的仍然是 ASCII 码,要变成年份、月份、日这三个整数,需要单独做一次转换。程序用的是一个很典型的「乘 10 累加」循环:
LEA DI, STR1+2 ; DI 指向字符区起点 MOV CX, 4 ; 年份固定取 4 位 MOV AX, 0 ; AX 作为累加器 P1: MOV BL, [DI] ; 取一位字符 SUB BL, 30H ; '0'~'9' 的 ASCII 减 0x30 得到真值 MOV BH, 0 MOV P, 10 MUL P ; AX = AX * 10,DX 高位忽略 ADD AX, BX ; AX += 本位数字 INC DI DEC CX JNZ P1 MOV YEAR, AX ; 例如 2009 → 0x07D9逻辑本身不复杂,关键在于三个隐含约束。第一,SUB BL, 30H只在输入字符确实是数字时才成立,如果键盘上敲进了字母,这一步得到的就是一个巨大的偏移量,累加结果会飞到年份字段之外,所以后面的合法性校验不能省。第二,MUL P用的是 16 位无符号乘法,结果放在 DX:AX 中,程序只用 AX,说明它默认四年份不会超过 65535,这也就限定了年份的有效范围。第三,字符串偏移STR1+2和STR1+6、STR1+8是写死的,意味着输入格式必须是严格的 8 位YYYYMMDD,多一位少一位都会读错位置。这三条约束不是缺陷,而是这类小工具在汇编里为了省寄存器、省指令而做出的硬性约定。
2.3 校验的顺序比校验本身更容易出错
输入转换成数字后,程序没有立刻拿去算星期几,而是先做了一轮边界检查:
CMP MONTH, 1 JC ERROR ; 月份 < 1 CMP MONTH, 13 JNC ERROR ; 月份 >= 13 CMP DAY, 1 JC ERROR ; 日 < 1 CALL GETN ; 先算出当月 1 号是星期几 → N MOV DI, OFFSET BUF1 DEC MONTH DEC MONTH ADD DI, MONTH MOV CL, [DI] ; CL = 当月最大天数 CMP BYTE PTR DAY, CL JA ERROR ; 日 > 当月最大天数这里有一个顺序上的讲究:日期上限必须放在闰年判断和BUF1改写之后才能比较。原因是BUF1里二月那一格初始是一个问号,程序先靠CALL PR判断闰年,再把 28 或 29 写进BUF1[1],只有这时「当月最大天数」才是可靠的。
注意:上面
DEC MONTH出现了两次,严格意义上会让 1 月索引退到BUF1之前一个字节,属于原稿遗留的边界问题。正确写法应当只DEC一次,或直接用DI = OFFSET BUF1 + MONTH - 1。自己复现这段逻辑时,这里必须确认。
3. 蔡勒公式:从年月日到星期几,为什么 1 月和 2 月要借上一年的年份
日历显示的核心是一个映射:给定年月日,算出它是星期几。程序选择了蔡勒公式,原因很直接——它只用整数四则运算,不依赖查表,也不需要事先知道前一个月的排布,非常适合放进一段子程序里反复调用。
3.1 每一项在算什么
程序里实现的版本是:
W = [C/4] - 2C + y + [y/4] + [13(M+1)/5] + d - 1其中 C 取「年份 / 100」的整数部分,y 取年份后两位,M 是月份,d 是日。这个式子里最有意思的是[13(M+1)/5]这一项——它不是某种玄学常数,而是一个把 12 个月的天数差异压缩成一个线性项的近似:3 月到 12 月的月长在 30 和 31 之间摆动,用13(M+1)/5取整就能刚好贴合这种摆动,从而避免为每个月单独建一张偏移表。[C/4] - 2C则是把百年跨度和世纪闰年规则一起折进去,让公式在跨世纪时依然成立。
程序实现这段时没有做任何浮点运算,全部走移位和整数除法:
MOV BL, 100 MOV AX, YEAR DIV BL ; AL = 年份/100(世纪),AH = 年份后两位 MOV C2, AL MOV Y, AH MOV CL, 2 MOV BL, C2 SHR BL, CL ; BL = [C/4],除以 4 用右移两位完成 SHL C2, 1 ; C2 = 2C SUB BL, C2 ; BL = [C/4] - 2C ADD BL, Y ; BL += y SHR Y, CL ; Y = [y/4] ADD BL, Y ; BL = [C/4] - 2C + y + [y/4]这里用SHR代替除法,是因为公式要求的就是「向下取整」;而对正整数来说右移两位和整除 4 是等价的,于是省掉了一次DIV调用。注意DIV BL这一步的副作用:AX 的低字节被替换成商、高字节被替换成余数。这是 8086 除法的默认行为,利用它可以一次拿到 C 和 y,也是这段代码能写得这么紧凑的原因。
3.2 1 月和 2 月回退一年的真正原因
公式里的13(M+1)/5项是按 3 月到次年 2 月这个顺序拟合的,因为公历的闰日恰好落在 2 月末,把 1 月和 2 月挪到上一年的 13 月和 14 月,就能让整年的月长序列变得
本文还有配套的精品资源,点击获取