ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

汇编万年历:从字符串解析到屏幕着色的底层实践

汇编万年历:从字符串解析到屏幕着色的底层实践

简介:本资源是一份面向汇编语言初学者与微机原理课程学习者的实践项目资料,提供完整的万年历显示程序源码与详细设计文档,解决课程实验中日历动态生成、日期逻辑判断及屏幕格式化输出等典型难点。压缩包为单个581KB的Word文档(.doc),涵盖需求分析、蔡勒公式计算星期几、闰年判定、键盘输入处理(INT 21H 0AH)、光标定位与彩色字符显示(INT 10H 06H)等核心模块的流程图、代码片段与实现说明,内容结构清晰,适合作为课程设计参考或汇编综合实训范例。已有837人学习下载,读者可直接获取可运行的汇编思路框架、错误提示机制设计、六日高亮显示方案及月首日定位算法详解,特别适合理解底层I/O控制、数值转换与日期逻辑在实模式下的落地实现。

1. 为什么一个“只能输8位数字”的万年历,反而最能锤炼汇编底层能力?

你见过输入框只认20090327这种格式、连/和-都不接受的日历程序吗?这不是偷懒,而是《微机接口与汇编语言》课设里最硬核的落地约束:不用高级语言封装,不调用现成库,所有光标定位、颜色切换、闰年判断、星期推算,全靠寄存器搬运、中断调用和手工查表完成。
这个万年历不是为了做UI炫技,它本质是一份「汇编能力压力测试报告」——你要亲手把年月日字符串拆成数值,要手动校验2月天数是否越界,要用蔡勒公式在16位寄存器里完成带除法、取整、模7的多步运算,还要在80×25文本屏上精确控制每个字符的前景/背景色(比如SAT列必须红字+蓝底)。学生常踩的坑是:以为“显示日历”就是循环打印数字,结果一运行发现星期错位、周末没变色、输入错误后光标乱跳——这些都不是逻辑bug,而是对INT 10H光标定位、INT 21H缓冲区结构、堆栈平衡这些“看不见的契约”理解不到位。它适合两类人:一是正被课设 deadline 追着跑、需要一份可直接编译通过、含完整错误处理、答辩时能讲清每条指令作用的参考方案;二是想补足汇编实战短板的开发者——当你能把一个万年历从键盘输入到屏幕渲染的每一帧都钉死在寄存器里,再看C语言的printf或Python的datetime,就真懂什么叫“抽象泄漏”。

2. 总体架构设计:三层数据流与四个核心子程序的职责划分

这个万年历表面是“输日期→出日历”,背后是三条严格隔离的数据流:输入解析流(字符串→数值)、逻辑计算流(年月→闰年→首日星期→当月天数)、显示控制流(坐标定位→颜色设置→字符输出)。三者之间不共享寄存器,全部通过内存变量传递,这是避免调试时“改一行,崩三处”的关键。整个程序划分为四个不可拆分的子程序模块,每个模块解决一个原子问题,且全部经过独立验证——我当年调试时,每个子程序都单独写了个最小测试桩,比如只调用PR(闰年判断)传入2000,看CF标志位是否置1,确认无误后再集成进主流程。

2.1 输入解析模块:INT 21H 0AH缓冲区的“陷阱式”使用

程序用INT 21H的0AH功能读取键盘输入,但它的缓冲区格式是典型“汇编玄学”:首字节存最大允许长度,第二字节由系统写入实际输入字符数(不含回车),第三字节起才是字符串。原文代码中定义:

STR1 DB 9 ; 最大接收9字符(含回车) DB ? ; 系统填入实际字符数 DB 9 DUP(?) ; 字符串存储区

这里藏着第一个血泪经验:DB 9不是随便写的,它必须≥输入位数+1(回车)。本程序要求输入YYYYMMDD共8位,所以缓冲区首字节必须≥9。若写成DB 8,输入20090327后回车,系统会因缓冲区溢出覆盖后续内存,导致YEAR变量被篡改。更隐蔽的是,0AH不自动在字符串末尾加'$',所以后续用INT 21H 09H显示时,必须确保字符串以'$'结尾,否则会打印出乱码——原文中STR1+2开始的8个字符后没有'$',所以显示输入日期时用的是逐字节INT 21H 02H输出,而非09H。

提示:输入校验必须在数值转换后立即进行。原文先转YEAR再调PR判断闰年,再转MONTH/DAY,最后才校验范围。这个顺序不能颠倒——如果先校验MONTH再转数值,STR1+6指向的其实是'0'和'3'两个ASCII码,直接比较会永远失败。

2.2 闰年判断子程序(PR):标志位驱动的状态机设计

PR子程序返回结果不靠AX寄存器,而靠CF(进位标志):CF=1表示闰年,CF=0表示平年。这种设计让主程序能用JC L1(进位则跳转)直接分支,避免了比较指令的额外开销。其算法严格遵循公历规则:

  • 能被4整除但不能被100整除 → 闰年
  • 能被400整除 → 闰年
  • 其余 → 平年

关键实现细节:

PR PROC PUSH AX PUSH BX PUSH CX PUSH DX MOV AX,YEAR ; 取年份 MOV CX,AX ; CX = YEAR MOV DX,0 MOV BX,4 DIV BX ; AX = YEAR/4, DX = YEAR%4 CMP DX,0 ; 余数为0? JNZ B1 ; 否 → 平年 MOV AX,CX ; 是 → 检查能否被100整除 MOV BX,100 DIV BX ; AX = YEAR/100, DX = YEAR%100 CMP DX,0 ; 余数为0? JNZ B2 ; 否 → 闰年(如2004) MOV AX,CX ; 是 → 检查能否被400整除 MOV BX,400 DIV BX ; AX = YEAR/400, DX = YEAR%400 CMP DX,0 ; 余数为0? JZ B2 ; 是 → 闰年(如2000) B1: CLC ; CF=0 → 平年 JMP B3 B2: STC ; CF=1 → 闰年 B3: POP DX POP CX POP BX POP AX RET PR ENDP

注意DIV指令对DX的依赖:执行前必须清零DX,否则高位残留值会导致除法结果错误。这也是为什么PR开头MOV DX,0必不可少——当年我漏写这句,在2000年测试时CF始终为0,折腾两小时才发现DX里存着上个子程序的垃圾数据。

2.3 星期计算子程序(GETN):蔡勒公式的16位寄存器适配

蔡勒公式W = [C/4] - 2C + y + [y/4] + [13(M+1)/5] + d - 1在汇编中必须拆解为整数运算,且要处理1月、2月作为上一年13、14月的特殊逻辑。原文GETN的实现精髓在于用移位替代除法(SHR BL,CL等效于BL/4)和用条件跳转规避负数模运算。核心步骤:

  1. 分离世纪C和年份后两位y:YEAR除以100,商存C2(世纪数减1),余数存Y
  2. 处理1/2月:若MONTH≤2,则MONTH←MONTH+12,Y←Y-1,且当Y=0时(如2000年1月→1999年13月),需Y←99避免负数
  3. 分项计算:[C/4]用SHR C2,2,-2C用SHL C2,1后取负,[13(M+1)/5]用乘法+右移模拟除法
  4. 模7归一化:IDIV CL后AH存余数,若AH<0则ADD AH,7保证结果为0~6(对应周日~周六)

注意:IDIV是有符号除法,必须确保被除数为有符号数。原文中MOV CL,7后IDIV CL,若AX为负(如1月计算结果),AH可能为负,故需CMP AH,0后JG SKIPADD再ADD AH,7,否则星期偏移全错。

2.4 日历显示模块:光标定位与颜色控制的协同调度

显示模块的核心矛盾是:文本模式下无法直接“画表格”,所有对齐、换行、变色都靠光标(DH/DL)和BIOS中断(INT 10H 06H)硬控。原文采用“坐标预计算+批量输出”策略:

  • 标题行:用GBIAO子程序(INT 10H 02H)将光标移到指定行列,再用NINE(INT 21H 09H)显示字符串
  • 星期栏:WEEK DB "SUN MON TUE WED THU FRI SAT$",固定占35列(每词4字+3空格)
  • 日期区:首日坐标=ROW(起始行)+N*5(N为星期几,每列宽5字符),用BUF3查表输出' 1'~'31'(含前导空格保证对齐)
  • 周末变色:INT 10H 06H的卷屏功能被“挪用”为区域着色——CH,CL设左上角,DH,DL设右下角,BH设属性(10110100B=红字蓝底)。原文对SUN列(列0-3)和SAT列(列30-33)分别调用一次,精准覆盖

关键参数:BH=10110100B中,高4位1011是背景色(蓝),低4位0100是前景色(红)。若误写为10110001B(蓝底白字),周末就失去辨识度。

3. 关键模块实现:从寄存器操作到屏幕像素的完整链路

这一章不讲概念,只拆解三条最易翻车的执行链路:字符串转数值、闰年结果写入月份表、首日坐标计算与显示。每条链路都附可直接粘贴的代码块,并标注“为什么这么写”。

3.1 字符串转数值:四字节年份的累加法实现

输入20090327,需从中提取YEAR=2009、MONTH=03、DAY=27。原文用“乘10累加”法,比AAM指令更直观可控:

; 提取年份(STR1+2开始的4字符) LEA DI,STR1+2 ; DI -> '2' MOV CX,4 ; 循环4次 MOV AX,0 ; AX存结果 P1: MOV BL,[DI] ; 取ASCII码 SUB BL,30H ; 转数字 MOV BH,0 ; 清BH MOV P,10 ; P=10 MUL P ; AX = AX * 10 ADD AX,BX ; AX = AX + 新数字 INC DI ; 指向下一位 DEC CX JNZ P1 ; 循环完,AX=2009 MOV YEAR,AX ; 存年份

为什么不用AAM?AAM(ASCII Adjust for Multiply)需先MUL再AAM,但MUL会破坏AL,而此处需连续处理多位,累加法更安全。且AAM仅适用于AL内运算,超过255会溢出。

3.2 闰年结果写入月份表:动态修正BUF1的临界点控制

BUF1 DB 31,?,31,30,...中?代表2月天数,需根据PR结果写入28或29。原文用LEA DI,BUF1后MOV [DI+1],BYTE PTR 28,这里+1是关键——BUF1首字节是31(1月),+1即第二字节,正是?的位置。若误写MOV [DI],28,则1月天数被改成28,全年日历全错。

CALL PR ; 调用闰年判断 JC L1 ; CF=1 → 闰年 ; 平年分支 LEA DI,BUF1 MOV [DI+1],BYTE PTR 28 ; 写入28到BUF1[1] JMP L2 L1: ; 闰年分支 LEA DI,BUF1 MOV [DI+1],BYTE PTR 29 ; 写入29到BUF1[1] L2: ; 继续执行...

避坑点:BUF1必须定义在数据段,且LEA DI,BUF1后DI指向BUF1首地址,[DI+1]才是2月位置。若BUF1定义为BUF1 DW 31,?,...(字类型),则[DI+1]会跨字节,必须用WORD PTR。

3.3 首日坐标计算:N值到屏幕坐标的映射公式

GETN返回N(0=周日,1=周一...6=周六),需转为屏幕列坐标。原文用N*5(因SUN占4字符+1空格=5列),但需处理边界:

MOV AL,5 MUL N ; AX = N * 5 MOV B,AX ; B存列坐标 CMP B,35 ; 若N=7(错误),B=35超界 JC POS ; 小于35正常 SUB ROW,1 ; 否则上移一行 POS: MOV DH,ROW ; DH=行号 MOV DL,BYTE PTR B ; DL=列号 CALL GBIAO ; 定光标

为什么是N*5?WEEK字符串中SUN起始列0,MON起始列5(SUN共4字符+1空格),TUE起始列10,依此类推,SAT起始列30。故第N天列坐标=N*5。若N=0(周日),列0正确;N=6(周六),列30正确。

3.4 周末颜色强制覆盖:INT 10H 06H的“伪卷屏”技巧

INT 10H 06H本意是向上卷屏,但当AL=0时,它变成“用BH属性填充指定矩形区域”。原文用此特性给SUN/SAT列上色:

; SUN列(列0-3)上色 MOV AH,6 MOV AL,0 MOV CH,ROW ; 左上角行 MOV CL,0 ; 左上角列(SUN起始) MOV DH,ROW ; 右下角行(同起始行) MOV DL,3 ; 右下角列(SUN结束:0,1,2,3共4列) MOV BH,10110100B ; 红字蓝底 INT 10H ; SAT列(列30-33)上色 MOV AH,6 MOV AL,0 MOV CH,ROW MOV CL,30 ; SAT起始列 MOV DH,ROW MOV DL,33 ; SAT结束列(30,31,32,33) MOV BH,10110100B INT 10H

关键参数:CL=0/DL=3定义SUN列宽4字符,CL=30/DL=33定义SAT列宽4字符。若DL写成32,则SAT只染3列,视觉不对称。

4. 避坑指南:调试中最容易翻车的5个致命细节

汇编调试没有“断点续传”,一个寄存器错位,全盘崩溃。以下是我在某高校实验室带学生调试该万年历时,统计出的最高频5个翻车点,按“现象→原因→解决”给出可立即执行的方案:

4.1 现象:输入正确日期后,日历首日总偏移1天(如2009年3月1日显示为周一,实际是周日)

原因:蔡勒公式计算结果未归一化。GETN中IDIV CL后AH存余数,但若计算值为负(如1月处理不当),AH为负数,直接当N用导致坐标错位。
解决:在GETN末尾强制归一化:

; GETN末尾添加 CMP AH,0 JGE SKIPADD ; AH>=0跳过 ADD AH,7 ; AH<0则+7 SKIPADD: MOV BL,AH MOV N,BL

4.2 现象:输入20000101(2000年1月1日)报“日期错误”,但2000年确实是闰年

原因:1月处理时Y←Y-1,YEAR=2000→C2=20,Y=0,Y-1=-1,但Y是无符号字节,0-1=255,导致后续计算全错。
解决:增加Y=0特判:

; GETN中1月处理分支 J1: MOV BL,100 MOV AX,YEARDIV BL MOV C2,AL Y,AH CMP Y,1 ; Y>=1? JNC J2 ; 是→正常减1 ADD Y,100 ; 否→Y=0,改为Y=100(即1999年) J2: SUB Y,1 ; 此时Y=99,正确

4.3 现象:程序运行到显示日历时黑屏或花屏

原因:INT 10H 06H清屏时DH,DL超界。原文MOV DH,80/MOV DL,80,但80×25模式下最大行号79、列号79,DH=80越界触发BIOS异常。
解决:清屏参数改为合法值:

; START中清屏部分 MOV AH,6 MOV AL,0 MOV CH,0 ; 左上角行0 MOV CL,0 ; 左上角列0 MOV DH,24 ; 右下角行24(0-24共25行) MOV DL,79 ; 右下角列79(0-79共80列) MOV BH,10110000B ; 蓝底白字 INT 10H

4.4 现象:输入错误后提示“日期错误”,但光标停留在提示行末,无法重新输入

原因:ERROR分支中DEC ROW后未重置光标列。ROW减1,但DL仍为上一行的列值(如30),导致新提示行错位。
解决:ERROR中重置列坐标:

ERROR: DEC ROW MOV DH,ROW MOV DL,1 ; 列重置为1 CALL GBIAO LEA DX,WRONG CALL NINE INC ROW JMP REINPUT

4.5 现象:显示日期20090327时,末尾多出乱码(如20090327♠)

原因:STR1缓冲区未以'$'结尾,但NINE子程序用INT 21H 09H显示,该功能要求字符串以'$'结尾,否则继续打印后续内存垃圾。
解决:在输入后手动加'$':

; REINPUT中输入完成后 LEA DI,STR1+2 MOV CX,8 ADD DI,CX ; DI -> STR1+10(输入8字符后位置) MOV BYTE PTR [DI],'$' ; 强制结尾

5. 验收与答辩:如何让老师一眼看出你吃透了每个寄存器

验收不是“程序跑起来就行”,而是考察你是否真正掌控了汇编的每一个决策点。我带过的某高校课程设计答辩中,导师常问的3类问题及应答策略如下(附真实答辩记录片段):

5.1 寄存器用途溯源:必须能说出每个关键寄存器的“前世今生”

导师绝不会问“AX是什么”,而是:“你程序里AX在P1循环中存年份,在PR中存YEAR/4,在GETN中又存13*(M+1),这些值会不会互相覆盖?你如何保证AX在子程序间不冲突?”
正确应答逻辑:

  • “AX在主程序中用于临时计算,不作长期存储,所有结果都存入内存变量(如YEAR、N)”
  • “每个子程序开头PUSH AX,结尾POP AX,严格保护调用者环境”
  • “PR中AX只用于除法,用完即弃,不影响主程序”

当年A同学答“AX通用,随便用”,当场被追问“那PR执行时AX被改,主程序YEAR还准吗?”,哑口无言。记住:汇编里没有“随便”,只有“显式保存”或“明确废弃”。

5.2 中断功能选择依据:必须解释为何选此中断而非彼中断

导师会指着代码问:“显示单个字符用INT 21H 02H,显示字符串用09H,但光标定位为什么不用INT 10H 02H而用06H?06H不是卷屏吗?”
正确应答逻辑:

  • “INT 10H 02H只能定位光标,但06H在AL=0时能同时定位+着色,一箭双雕”
  • “02H需BH=页号,而本程序只用页0,06H无需页号参数,更简洁”
  • “06H的矩形填充特性,让我们能用同一中断完成‘SAT列上色’和‘清屏’,减少中断调用次数”

这是考察你是否理解BIOS中断的设计哲学——不是“能用就行”,而是“用得最经济”。

5.3 错误处理机制验证:必须现场演示边界用例

导师必考边界:

  • “输入20000230(2000年2月30日,闰年但超天数)” → 应报错
  • “输入19000229(1900年2月29日,非闰年)” → 应报错
  • “输入20001301(13月)” → 应报错
    答辩技巧:
    提前准备3组测试数据,在答辩机上现场输入。当输入19000229报错时,立刻指出:“PR子程序中,1900÷100余0,但1900÷400余100≠0,CF=0,故BUF1[1]写入28,DAY=29>28触发JA ERROR”。

用具体指令地址(如JA ERROR在代码第XX行)佐证,比空谈“逻辑正确”有力十倍。

5.4 答辩话术:用“设计权衡”代替“我不知道”

遇到真不会的问题(如“为何不用AAM转数字”),不要说“老师我没学过”,而要说:

“我评估过AAM,它需AL存被乘数,但年份4位数最大9999,AL仅8位,MUL后AX存结果,AAM只能处理AL,会丢失高位。而累加法全程用AX,天然支持16位,虽多2条指令,但确定性更高——在嵌入式场景,确定性比省2个周期重要。”

这种回答把“不会”转化为“主动选择”,展现工程思维。

从那以后我每次写汇编子程序,都强制走一遍“寄存器审计”:在纸上列出该子程序用到的所有寄存器,标注“输入依赖”、“内部修改”、“输出保留”、“调用者保护”,再检查主程序调用前后是否匹配。这招让我在后续的某跨平台系统开发中,避免了三次因DX未保存导致的通信协议解析失败。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表