8086汇编万年历:从DOS中断到蔡勒公式的实模式实践

发布时间:2026/10/29 21:25:39
8086汇编万年历:从DOS中断到蔡勒公式的实模式实践 简介面向汇编语言课程设计与微机接口实验的万年历显示程序设计文档适合正在学习8086汇编、需要完成课程设计或想弄清字符界面日历实现思路的学生与自学者。文档围绕键盘输入年月日、校验输入合法性、计算当月第一天是星期几、输出整月日历这一完整流程展开重点讲解蔡勒公式的推导与取模对应关系、闰年判定与各月天数表处理以及借助INT 21H的0AH功能读入字符串、用INT 10H的06H功能把周六、周日两列设为红色的实现细节同时给出需求分析、概要设计、详细设计与各子程序流程图方便对照代码理解模块划分以及光标定位、一位数与两位数排版等易错点。压缩包内为1个doc文档约581KB结构完整、内容紧凑。目前已有837人学习下载适合作为课程设计参考和汇编入门案例精读。1. 一份能跑的 8086 万年历先看清它到底解决了什么先说个略带反直觉的结论这份万年历汇编语言编写的源码里蔡勒公式那十来行并不是最容易翻车的地方真正耗时间的是 8086 实模式下几个中断的副作用——INT 21H的 0AH 缓冲区里那个由系统回填的长度字节、INT 10H的 06H 上卷窗口在左列大于右列时直接返回、还有DIV之前忘了清DX直接触发除法溢出。程序本身的目标很朴素从键盘敲进八位年月日校验合法性算出这个月一号是星期几然后按七列网格把整月打印出来周六周日用不同颜色标出来。它适合两类人正在做《微机接口与汇编语言》课程设计、需要一份能编译能演示还能讲清原理的完整参考的以及写过 C 但对段定义、中断调用、子程序设计已经手生的老手拿回去当实模式模板改。单文件、MASM 语法、纯 DOS 中断不依赖任何库。2. 骨架与数据段把课程要求拆成能单独调试的模块2.1 从题目要求反推模块边界课程要求拆开看只有四件事输入年月日、判断日期是否成立、算出当月一号是星期几、按网格画出来并给周末上色。这四件事的耦合点很少只有三个共享变量YEAR、MONTH、DAY以及一个“当月一号是星期几”的结果N。所以最省事的分法是四个可独立跑通的子程序加一个主流程而不是从头到尾一条直线写下来。我一般会先把每个子程序单独做成一个能INT 20H退出的小 EXE验证输入输出对了再往主程序里搬。这个习惯在汇编里尤其重要因为段寄存器、SP、标志位全是全局状态一旦搬进去出错你根本分不清是逻辑错还是环境被上一个子程序改坏了。原文里那句“每次编写一个子程序都先把它当成独立程序段跑一次”说的就是这件事只是没展开成流程。工程组织上这类作业通常是单文件目录长这样文件作用备注CAL.ASM全部源码三个段主文件可独立汇编CAL.OBJMASM 汇编产物由MASM CAL.ASM;生成CAL.EXE链接产物由LINK CAL.OBJ;生成FLOW.VSD整体流程图与三个子程序流程图答辩时讲设计思想用编译命令就两行注意分号是让 MASM 用默认参数、不再交互提问MASM CAL.ASM; LINK CAL.OBJ; CAL.EXE在老版本 DOS 环境下直接敲现在多半是在 DOSBox 里挂载目录跑或者用带 MASM 环境的集成编辑器一键汇编链接。跑不起来的第一件事是确认CAL.EXE和你的工作目录在同一个盘符路径下第二件事才是看源码。2.2 三个段与显式栈空间8086 实模式程序的骨架是固定的三段结构但细节上有个坑必须提前说栈段不能只写个空的SEGMENT了事。DATAS SEGMENT ; 全局数据输入缓冲区、月份天数表、字符串表、游标 DATAS ENDS STACKS SEGMENT DB 256 DUP(0) ; 必须显式留空间否则 SP 指向未知区域 STACKS ENDS CODES SEGMENT ASSUME CS:CODES, DS:DATAS, SS:STACKS START: MOV AX, DATAS MOV DS, AX ; 只有 DS 需要手动装CS/SS 由 DOS 装载 ; ... 主流程 MOV AH, 4CH INT 21H CODES ENDS END STARTASSUME只是告诉汇编器“假定”某个段寄存器指向某个段它不产生任何指令真正把段地址装进DS的还是那两条MOV。CS和SS不需要手动装是因为 DOS 加载 EXE 时按重定位表已经填好了。栈段只写STACKS SEGMENT/STACKS ENDS中间空着链接时可能出warning: no stack segment运行起来SP指向0000第一次CALL就把返回地址写到了中断向量表上程序会以各种莫名其妙的方式跑飞——这是汇编课设里最常见的“玄学”故障其实一点都不玄。2.3 数据段布局缓冲区、天数表、字符表数据段里真正需要动脑的只有三块。第一块是 0AH 输入缓冲区格式被 DOS 写死了首字节是最大允许长度第二字节由系统回填实际长度第三字节开始才是字符。STR1 DB 9 ; 最多接收 8 个字符 1 个回车 DB ? ; 0AH 返回后这里存实际字符数不含回车 DB 9 DUP(?) ; 字符从这里开始即 STR12第二块是月份天数表二月的值先留空等闰年判定完再回填BUF1 DB 31,?,31,30,31,30,31,31,30,31,30,31 ; 下标 月份 - 1第三块是日期字符表BUF3。原作者没用“数值转 ASCII 再输出”的常规路子而是把 1到31三十一条以$结尾的字符串预先写死在数据段每项严格 5 字节两位数字 0AH 0DH $一位数字补前导空格凑齐。这样显示日期时只要SI BUF3 (日-1)*5直接INT 21H的 09H 输出不用做除法取十位个位。好处是简单坏处是数据段占了几百字节而且换行符会跟着一起输出——后面排版那一节会看到这个换行其实被光标重定位覆盖掉了属于冗余动作。2.4 三个公共子程序的调用约定主流程里反复出现的三件事被抽成了子程序调用约定必须写清楚否则改起来会互相踩。子程序入参出参副作用GBIAODH行,DL列无光标移动寄存器不变NINEDX字符串偏移无从当前光标位置输出到$PRYEARCF1 闰年0 平年只用AX/BX/DXGETNYEAR,MONTHN当月一号星期值会修改MONTH; 定光标入口 DH行号 DL列号 GBIAO PROC PUSH AX PUSH BX MOV AH, 2 ; 02H 设置光标位置 MOV BH, 0 ; 页号 0 INT 10H POP BX POP AX RET GBIAO ENDP ; 显示字符串入口 DX以 $ 结尾的串偏移 NINE PROC PUSH AX MOV AH, 9 ; 09H 显示字符串 INT 21H POP AX RET NINE ENDPGBIAO里的PUSH AX/BX其实没保护到点子上因为它真正读的是DX全程没改过任何寄存器压栈弹栈纯属保险。真正要注意的是NINE——09H 会一直输出到遇见$为止数据段里任何一个字符串漏掉$它就会顺着往下把后面的变量当字符打出来屏幕上出现一堆乱码然后继续往后读直到撞上一个恰好是24H的字节。这类问题查起来非常费时间血泪经验是每写一个字符串常量写完立刻补$不要等编译报错。3. 输入与校验INT 21H 0AH 缓冲区、字符转数值与闰年判定3.1 0AH 缓冲区到底是怎么被填的调用前把缓冲区首地址给DX功能号放AH然后INT 21H。键盘敲进去的字符不回显到程序控制的缓冲区之外按回车结束光标回到当前行行首回车符本身留在缓冲区里但不计入长度字节。LEA DX, STR1 ; DX 指向缓冲区首字节 MOV AH, 0AH INT 21H ; 返回后STR11 实际字符数STR12 起为字符内容有三个参数层面的细节必须记住。第一首字节写的是“含回车在内”的最大字符数所以想输入 8 位日期这里至少写 9缓冲区总长度还得再 2长度字节 内容首址偏移写成DB 9 / DB ? / DB 9 DUP(?)正好够。第二如果你输入超过上限0AH 会“嘀”一声拒绝继续接收剩下的字符留在键盘缓冲区里下一次输入会读到脏数据——这就是为什么很多人的程序第二次输入直接跳过。第三0AH 结束后光标回到行首如果你紧接着用 09H 输出会覆盖刚输入的那一行所以主流程里每输入一次都要把ROW往下推两行再继续。3.2 字符转数值一个能复用的 ATOI 循环输入进来的是 ASCII 码2 0 2 4要变成数值 2024。做法是逐位乘十累加年份取 4 位月、日各取 2 位三段循环完全同构可以抽成一个子程序; 入口: DI 数字串首地址, CX 位数 ; 出口: AX 转换后的数值 ATOI PROC PUSH BX PUSH DX XOR AX, AX ; 累加器清零 NEXT: MOV BL, [DI] SUB BL, 30H ; ASCII 0 30H减出真值 MOV BH, 0 ; 高位清零保证 BX 就是那一位的值 MOV DX, 10 MUL DX ; DX:AX AX * 10注意 DX 被覆盖 ADD AX, BX INC DI LOOP NEXT ; CX 自减不为零继续 POP DX PUSH BX POP BX POP BX POP DX RET ATOI ENDP上面这段为了展示LOOP的用法省略了正确配对的PUSH/POP实际写成PUSH BX / PUSH DX ... POP DX / POP BX / RET即可。这里最需要盯住的是MUL8 位乘法的结果在AX16 位乘法是DX:AX无论哪种DX都会被写坏。所以循环里绝不能把需要跨轮保存的东西放在DX。原文这里用了一个内存变量P存 10MUL P直接乘内存操作数避开了寄存器分配问题是个偷懒但有效的写法。另外SUB BL, 30H之后必须MOV BH, 0因为MUL之后BX的高字节不可信不清零会把垃圾算进去。三次调用分别得到YEARSTR12起 4 位、MONTHSTR16起 2 位、DAYSTR18起 2 位存进三个DW变量。3.3 闰年判定CF 当返回值的经典写法闰年规则能被 4 整除但不能被 100 整除或者能被 400 整除。三层除法用CF把布尔结果带出来主程序用JC分流。; 入口: YEAR 出口: CF1 闰年, CF0 平年 PR PROC PUSH AX PUSH BX PUSH DX MOV AX, YEAR XOR DX, DX ; DIV 前必须清 DX否则除法溢出 MOV BX, 4 DIV BX CMP DX, 0 JNZ NOT_LEAP ; 不能被 4 整除 - 平年 MOV AX, YEAR XOR DX, DX MOV BX, 100 DIV BX CMP DX, 0 JNZ IS_LEAP ; 能被 4 整除、不能被 100 整除 - 闰年 MOV AX, YEAR XOR DX, DX MOV BX, 400 DIV BX CMP DX, 0 JNZ NOT_LEAP ; 能被 100 整除、不能被 400 整除 - 平年 IS_LEAP: STC ; CF1 JMP PR_DONE NOT_LEAP: CLC ; CF0 PR_DONE: POP DX POP BX POP AX RET PR ENDPDIV BX是 16 位除法被除数在DX:AX商在AX、余数在DX。如果DX里残留着上一次的垃圾商就会超过FFFFHCPU 立刻抛 0 号中断除法溢出在 DOS 下表现为程序直接卡死或者打印一串乱码后退出。每一次DIV前面加一条XOR DX, DX是这类程序里最便宜的一份后悔药。判定完立刻回填二月天数这一步要放在任何校验之前LEA DI, BUF1 JC WRITE29 MOV BYTE PTR [DI1], 28 JMP SET_OK WRITE29: MOV BYTE PTR [DI1], 29 SET_OK:3.4 校验顺序月、日、当月最大天数输入合法性检查的顺序会直接影响报错信息的准确度。正确的顺序是先查月份1 MONTH 12再查日DAY 1最后用BUF1[MONTH-1]取出当月最大天数比对。CMP MONTH, 1 JC ERROR ; 月份小于 1 CMP MONTH, 13 JNC ERROR ; 月份大于 12 CMP DAY, 1 JC ERROR ; 日期小于 1 LEA DI, BUF1 MOV AX, MONTH DEC AX ; 下标 月份 - 1 ADD DI, AX MOV CL, [DI] ; CL 当月最大天数 CMP BYTE PTR DAY, CL JA ERROR ; 日期超过当月天数注意CMP BYTE PTR DAY, CL这里DAY是DW必须显式加BYTE PTR才能和CL比较否则汇编器报操作数尺寸不匹配。另外这一步必须放在闰年回填之后否则二月永远是未知值?输入 2 月 29 号会被误判。报错分支里有两件事要做把光标挪到干净位置显示The date you input is wrong! Please try again然后回退行号再跳回输入入口。原代码在ERROR分支里写了DEC ROW再INC ROW目的是让下一次输入覆盖掉报错那一行而不是一直往下滚。这个思路对但只回退了一行报错信息和输入提示其实是两行回车几次之后还是会滚屏后面避坑那一节细说。4. 蔡勒公式与网格排版从 N 值到周末标红的完整链路4.1 把公式拆成 16 位能算的四则运算蔡勒公式的形式是W [C/4] - 2C y [y/4] [13(M1)/5] d - 1其中C是世纪数年份前两位y是年份后两位M是月份d是日。计算完对 7 取模得到 0 到 6 的星期值。在 16 位寄存器里逐项拆; 入参 YEAR / MONTH出参 N GETN PROC PUSH AX PUSH BX PUSH CX PUSH DX MOV BL, 100 MOV AX, YEAR DIV BL ; AL 世纪数 C, AH 年份后两位 y MOV C2, AL MOV Y, AH MOV CL, 2 MOV BL, C2 SHR BL, CL ; BL [C/4]右移两位等价于整除 4 SHL C2, 1 ; C2 2C左移一位等价于乘 2 SUB BL, C2 ; BL [C/4] - 2C ADD BL, Y ; 加上 y SHR Y, CL ; Y [y/4] ADD BL, Y ; BL [C/4] - 2C y [y/4] ; ... 下面接月份项用移位代替除法是因为这里除的都是 2、4 这类 2 的幂SHR一条指令、一个机器周期比DIV快得多也不占用DX。这段是整个程序里最“汇编味”的地方没有函数调用、没有临时变量全靠寄存器轮转。月份项是[13*(M1)/5]必须用乘法除法XOR AH, AH MOV AX, MONTH MOV DX, 13 MUL DX ; DX:AX 13 * MONTH MOV CX, 5 DIV CX ; AX [13*MONTH/5] XCHG AX, BX CBW ; 把 BL 里的累计值符号扩展成 AX 再交换回去 XCHG AX, BX ADD BX, AXXCHG加CBW这两句看着绕实际是绕开“一个寄存器同时当累加器和被乘数”的冲突先把累加值挪到AX做符号扩展再换回来保证负数参与后续取模时不出错。写的时候如果不加CBWBL里若是负数C/4 - 2C对 20 世纪以后的年份基本是负的高位是垃圾加出来的结果就偏了。4.2 1 月和 2 月先 INC 再 12 的巧劲公式有个硬性约定1 月和 2 月要当作上一年的 13 月和 14 月来算同时C和y都按上一年取值。这份源码用了个很省事的顺序先把MONTH自增 1再做判断。INC MONTH CMP MONTH, 3 JC JAN_PATH ; INC 后为 2说明原月份是 1 月 CMP MONTH, 4 JC JAN_PATH ; INC 后为 3说明原月份是 2 月 JMP NORMAL_PATH自增之后正常月份的MONTH正好等于M1后面乘 13 除 5 就直接对应公式里的13*(M1)/5。而 1 月进来时MONTH2跳进JAN_PATH后执行ADD MONTH, 12变成 14 —— 等于把原月份 1 当成了 1313*(131)/5正好对上2 月同理变成 15。一条INC顺手解决了“加一”和“映射到 13/14 月”两件事这是整份代码里最值得学的一个小技巧。跨年处理上还有一处必须补y要减 1。如果原年份是 2000 年y00直接减一变成FFH也就是 255参与运算就全错了。所以要先判断CMP Y, 1 JNC J2 ADD Y, 100 ; y0 时先加 100借位成 99 J2: SUB Y, 1 ; 再减 1得到上一年的后两位4.3 负余数、IDIV 与 N 的落点取模这一步是另一个坑区。AX里可能是负数用无符号DIV会溢出必须用IDIVMOV AX, BX MOV CL, 7 IDIV CL ; 8 位有符号除法商在 AL余数在 AH CMP AH, 0 JG SKIPADD ADD AH, 7 ; 余数为负时补 7映射到 0..6 SKIPADD: MOV N, AHIDIV用 8 位操作数时被除数是AX、除数是CL商写AL、余数写AH。有符号除法得到的余数符号跟被除数一致可能是-6到0所以统一加 7 拉回正区间。这一步如果漏了遇到 1 月、2 月这类需要跨年的情况就会算出负数N变成FFH之类后面乘 5 算列偏移直接溢出日历会画到屏幕外面去。还有一处必须自己验算这段GETN累加到最后没有加公式里的d项。也就是说它算出的是“当月一号的前一天”是星期几用的时候实际等于把基准挪了一格。原作者在调用处没有显式 1而是靠后面的列偏移B 5 * N直接落地。这种情况不能想当然一定要拿已知日期对表。我一般会准备三个锚点2000-01-01 是星期六、2024-02-29 是星期四、1900-03-01 是星期四。三个都对得上公式和基准才算真正通了只要有一个错先怀疑d项和 1/2 月分支不用去翻显示代码那不是它的锅。4.4 从 N 到行列5 字节一格的网格算术屏幕上一行七格每格宽 5 个字符所以 1 号的列偏移就是5 * N。行号由变量ROW维护每画完一行加 1。MOV SI, OFFSET BUF3 MOV AL, 5 MUL N ; AX 5 * N起始列偏移 MOV B, AX CMP B, 35 ; N7 理论不存在防御性判断 JC POS SUB ROW, 1 POS: MOV DH, ROW MOV DL, BYTE PTR B ; 列号 5 * N CALL GBIAOBUF3每项 5 字节所以每打印一天SI 5、列号B 5。当列号超过 30也就是第 6 格之后说明这一行满了ROW 1、B 0重新从第一列开始MOV A7, 31 CMP DL, A7 JBE POS1 ; 列号 31还在本行内 ADD ROW, 1 ; 超过则换行 MOV AX, 0 MOV B, AX JMP POS POS1: MOV DX, SI CALL NINE ; 在当前光标处输出日期串 ADD SI, 5 ADD B, 5 DEC G ; G 当月天数循环计数 JNZ POS这里有个容易忽略的边界一行七格最后一格起始列是5*630加上 5 字节宽度正好顶到 35。判断条件写成 31是够用的但如果某天日期串宽度没对齐比如两位数没补前导空格就会串列。所以BUF3里一位数必须写成 1这种带前导空格的形式占满 5 字节不能图省事写成1。4.5 上色06H 上卷窗口与显存直写INT 10H的 06H 是“向上滚动窗口”AL0时不清屏而是把窗口内全部填成BH指定的属性。属性字节高 4 位是背景色、低 4 位是前景色10110100B就是“背景亮青、前景红”也常写成00000100B黑底红字。窗口由CH/CL左上行列和DH/DL右下行列圈定。; 给第 ROW 行的 SAT 那一格涂成红底 MOV AH, 6 MOV AL, 0 ; AL0填充窗口不滚动 MOV BH, 10110100B ; 属性字节 MOV CH, ROW MOV CL, 30 ; 左列 MOV DH, ROW MOV DL, 33 ; 右列 INT 10H要让日期字符本身变红是“先涂底色、再用 09H 写字符”。因为 09H 只写字符码、不改属性字节字符会保留底下的颜色看起来就是红块上写着字。但这里有个必须自己上机验证的点标准 BIOS 在CL DL或CH DH时把窗口判定为空、直接返回、什么都不画。同一段代码里给 SUN 列上色时写的是CL3 / DL0左列大于右列等于空窗口这一格根本不会被涂上颜色。如果你跑出来发现只有周六是红的、周日不是八成就是这里把CL改成 0 即可。更稳妥的做法是绕开 BIOS直接写文本模式显存B800H段的属性字节; 把第 ROW 行第 COL 列的字符改成红色前景80x25 文本模式 MOV AX, 0B800H MOV ES, AX MOV AX, 80 MUL BYTE PTR ROW ; 每行 80 列 ADD AL, COL ADC AH, 0 SHL AX, 1 ; 每个字符占 2 字节字符码 属性 MOV DI, AX MOV AL, ES:[DI] ; 取原字符码 MOV AH, 04H ; 黑底红字 MOV ES:[DI], AX MOV ES:[DI1], AH ; 只改属性不动字符用显存直写的好处是精确到单个字符、不受窗口判定的限制坏处是要求ES指向B800H而且只在INT 10H设成 03H 模式80×25 彩色文本时成立。所以在START开头那句MOV AX, 0003H / INT 10H不能省否则显存基址和后端颜色格式都不是你以为的那个。5. 避坑与排查六个真实翻车点5.1 输入与计算阶段的三个坑现象一第一次输入正常第二次按回车就整个乱套。原因有两层。一是 0AH 的缓冲区长度字节STR11由系统回填你没有主动重置但更常见的是屏幕已经滚动了——每次输入和报错都让ROW往下走几次之后ROW超过 24INT 10H定位到屏幕外字符触底滚屏标题和提示全被推走肉眼看上去像“输不进去”。解决是把ROW当成有限资源管理报错分支里把ROW回退到本次输入之前的值用DEC ROW是不够的一次回退两行或者干脆每次重画之前用 06H 整屏清一次再从头排版。现象二1 月、2 月输入正确日期星期却差一天甚至差好几天。原因是GETN的跨年分支没走对。要么是ADD MONTH, 12放在自增之前那样 1 月会变成 13 而不是 14要么是y减 1 时没处理y0的情况00H - 1变成FFH之后整条式子全歪。解决严格按“先INC MONTH判断是否为 1/2 月再ADD 12最后处理y-1”的顺序写并且用 2000 年 1 月、2000 年 2 月这两个最边界的数据单测。现象三程序在某个输入上直接卡死无响应也不报错。八成是DIV/IDIV溢出。DIV BX要求商不超过FFFFH而你忘了清DX或者IDIV CL时AX里的值超出了-128~127的范围8 位除法的商必须能放进AL。解决所有DIV前XOR DX, DX所有IDIV r8前用CBW把被除数压进 8 位范围或者直接改用 16 位除数IDIV CX并确保DX是符号扩展值。5.2 显示与调试阶段的三个坑现象四把MOV CX, 4写成MOV CL, 4循环次数完全不对。这个坑原文里踩过。CX是 16 位寄存器你只改低 8 位CL高 8 位CH保留着上一次操作留下的垃圾值LOOP指令看的是整个CX于是循环次数变成几万次屏幕疯狂输出直到滚屏。解决原则用LOOP/JCXZ之前必须用MOV CX, n整条赋值绝不用MOV CL, n偷懒如果确实只需要 8 位计数用DEC CL / JNZ不要用LOOP。现象五字符串输出成乱码或者程序跳到别的地方去了。两个原因。一是字符串常量漏写$09H 一路往后读读到数据段末尾就读进了代码段二是DS没装对LEA DX, A1取到的是CODES段的偏移输出的是指令机器码。解决所有以 09H 输出的串强制以$结尾START后第一件事就是MOV AX, DATAS / MOV DS, AX中间任何子程序只要用了MOV AX, 段名就必须自己负责恢复DS不能依赖调用者。现象六日期数字出现了但周末那一列没有颜色。就是上一节说的窗口判定问题。INT 10H06H 在CL DL时按空窗口处理直接返回。给 SUN 列上色的那段CL3 / DL0正好命中。解决把左列改成 0、右列改成 4或者直接显存直写属性字节一次改一个字符精确且不依赖窗口语义。调试手段上补一句这类程序不要靠“编译通过 看起来对”来判断。用带单步和寄存器窗口的调试器MASM 自带的CV或者 DOSBox 里的DEBUG在GETN的IDIV之后下断点看AH里的余数是不是在 0..6在每轮打印前看DH/DL是不是你算出来的行列——比盯着屏幕猜快十倍。6. 二次开发把它改成顺手的小工具这份代码能跑、能演示但离“好用”还差几步。最值得动手的三处改造按投入产出排序。改造点涉及中断/机制难度收益读系统日期默认显示当月INT 1AHAH04H读日期低打开就能看不用每次敲高亮“今天”显存直写B800H属性字节中界面立刻专业一档上下月翻页INT 16HAH00H 读键 重新计算中从“一次性作业”变成工具支持 1582 年前日期改用儒略日算法高学术价值大于实用价值先做第一个。INT 1AH的 AH04H 返回CH世纪19/20、CL年、DH月、DL日直接写进YEAR/MONTH/DAY就能省掉输入环节; 读系统日期CH世纪 CL年 DH月 DL日 MOV AH, 04H INT 1AH MOV AL, CH MOV AH, 100 MUL AH ; AX 世纪 * 100 MOV BL, CL MOV BH, 0 ADD AX, BX ; AX 完整年份 MOV YEAR, AX MOV BH, 0 MOV BL, DH MOV MONTH, BX MOV BL, DL MOV DAY, BX注意MUL AH之后AH已经被破坏不能再拿它当 100 用这里必须在MUL之前把 100 放进一个安全的位置否则算出来的年份会莫名其妙偏掉这类“算完才发现源操作数被自己的结果覆盖”的错误在寄存器紧张的汇编里发生频率远超想象。第二个改造是给“今天”加反显。做法是在打印每一天之前判断DAY 今天成立就把这一格的属性字节通过B800H直写成01110000B白底黑字反显不成立就走原来的路径。这里的关键是把“写字符”和“改属性”拆成两步先 09H 输出字符再按行列算出显存偏移改属性。顺序反过来的话09H 会按当前属性重写字符把刚设好的反显冲掉。第三个改造需要把主流程从“一次执行到底”改成“事件循环”显示完一个月后读一个键左箭头重算上个月、右箭头重算下个月、ESC退出。注意月份减到 0 时要回退到上一年 12 月加到 13 时进到下一年 1 月而一旦年份变了BUF1里的二月天数必须重新跑一遍PR回填——这是重画之前最容易漏的一步漏了就会出现“闰年翻到 3 月再翻回 2 月29 号消失”的怪现象。验证方法不用复杂写一个小脚本把GETN的逻辑用 C 或者 Python 复刻一遍跑 1901 到 2099 每一年的 1 月 1 日和标准库的datetime对拍全过了再信汇编那份。这个习惯我保持了挺久凡是涉及日期、进制、边界条件的汇编子程序先在对拍脚本里跑通逻辑再往寄存器里搬比在调试器里一条一条STEP快太多。从那以后我每次写涉及DIV和INT 10H窗口的汇编都强制在动手前把输入范围、行列边界、除数是否为零这三件事列在纸上写完先单步走一遍边界值再谈功能。这份万年历的源码不算短但真正值得抠的就是那几个位置0AH 的缓冲区头部、闰年的三层除法、蔡勒公式的跨年分支、06H 的窗口判定。把这四处吃透剩下的排版和上色都是体力活。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询