简介:本资源是面向高校电子信息类专业本科生的《微机原理及应用技术》课程设计实践材料,聚焦密码校验这一典型软硬协同任务,帮助学生掌握汇编级密码验证逻辑、MASM开发流程与系统安全基础。压缩包为1个249KB的Word文档(.doc),完整包含河北科技大学电信091班课程设计报告全文:涵盖题目说明、详细设计要求(如8–16位混合字符校验、明文禁用与哈希思路)、进度与小组分工安排、MASM环境实现方案、含LEA/MOV/CMP/JMP等指令的算法解析、完整流程图、分模块程序代码(含注释)及界面截图与总结反思。报告目录结构规范,从需求分析到附录代码层层递进,便于理解汇编语言中字符串处理、条件跳转与子程序调用的实际应用。目前已有652人学习下载,是微机原理课设中兼具教学完整性与工程参考价值的典型范例。
1. 密码校验不是逻辑判断,而是寄存器状态流的精确编排
你写完CMP AL, '2'然后JNZ CP3,以为只是“比较字符是否为2”——错了。这行指令真正干的事,是把键盘扫描码、ASCII码、DOS缓冲区结构、INT 21H输入协议、内存对齐约束、段寄存器加载顺序这五层硬件-固件-软件契约,全部压缩进一个字节的AL里执行原子比对。它不依赖C语言的字符串库,不经过操作系统抽象层,甚至不查ASCII表——因为DOS软中断在INT 16H读键时,已把扫描码转成ASCII塞进AL;而INT 21H的0AH功能又强制要求输入缓冲区首字节存最大长度、次字节存实际长度、后续才是字符串本体。所以当你用LEA SI, BUF1去比对密码时,真正比的不是“字符串相等”,而是两个内存块从偏移量+1开始、按CX计数器控制的字节级逐位异或归零过程(REPZ CMPSB的本质)。这个程序能在1995年的486机器上跑通,不是因为它“功能简单”,而是因为它的每一行汇编都踩在x86实模式下段地址计算、中断向量跳转、BIOS视频缓冲区映射这三根钢丝上。搞懂它,你就拿到了打开微机原理黑匣子的第一把物理钥匙:所有高级语言的安全机制,最终都要坍缩成寄存器里几个比特的翻转节奏。
2. DOS软中断不是API调用,而是硬件事件驱动的寄存器状态机
2.1 INT 21H 0AH输入协议:缓冲区结构决定校验逻辑起点
DOS的字符串输入功能(INT 21H, AH=0AH)根本不是“读一行”,而是一次内存块预分配+状态反馈+长度回填的三阶段操作。看这段关键代码:
MSG1 DB 255, 0 ; 缓冲区定义:首字节=最大长度,次字节=实际长度(返回时填入) BUF1 DB 255 DUP(0) ; 实际字符串存储区(从MSG1+2开始)当执行LEA DX, MSG1+INT 21H时,CPU做的真实动作是:
- 将DS:DX指向的内存首地址(MSG1)作为输入缓冲区基址;
- 读取MSG1[0]的值(255)作为用户允许输入的最大字符数;
- 在键盘输入过程中,实时将ASCII码写入MSG1+2起始位置;
- 遇到回车(0DH)时,停止输入,自动将实际输入字符数(不含回车)写入MSG1[1];
- 在MSG1+2+实际长度处写入0DH(回车符),再写入00H(字符串结束符)。
提示:这就是为什么密码校验必须用
CMP CL, MSG2+1比较长度——因为MSG2+1存放的是用户真实输入的字符数,而BUF1和BUF2的字符串本体从MSG1+2/MSG2+2开始。若直接用LEN BUF1计算,会得到255(DUP定义的长度),完全失真。
这个设计暴露了DOS时代的底层哲学:不信任软件层的内存管理,把长度控制权交给硬件输入过程本身。现代程序员用fgets()时传入buffer size,本质是重演这套协议——只是把MSG1[0]换成了函数参数。
2.2 INT 10H视频服务:光标位置与屏幕刷新的硬同步
菜单界面中反复出现的MOV AX, 3+INT 10H并非单纯“清屏”。AX=3表示调用功能号00H(设置显示模式),但AL=3的隐含语义是:启用80×25彩色文本模式,同时强制重置光标到(0,0)位置,并清空整个视频缓冲区(B800:0000起始的64KB显存)。
更关键的是光标移动逻辑:
MOV AX, 0E0DH ; AH=0EH(TTE mode), AL=0DH(回车) INT 10H ; 此调用使光标回到行首并下移一行 MOV AL, 0AH ; 换行符 INT 10H ; 再次下移一行(因0AH在TTE模式下仅换行不回车)这里藏着x86实模式下最经典的I/O端口竞争陷阱:BIOS视频服务通过向0x3D4/0x3D5端口写入光标位置寄存器值来控制光标,而DOS的INT 21H显示字符串功能(AH=09H)内部也会调用INT 10H更新光标。若不手动用MOV AX, 0E0DH强制同步,菜单文字和提示字符串会出现错位——因为INT 21H显示完"MAIN MENU"后,光标停在字符串末尾,而下一条LEA DX, TIPS的显示会从该位置继续输出,导致界面叠印。
2.3 INT 16H键盘服务:扫描码与ASCII的双重身份切换
MOV AH, 0+INT 16H读键指令返回的AL值,表面是ASCII码,实则是硬件扫描码经BIOS键盘驱动翻译后的中间态。当用户按下数字键'1'时:
- 键盘控制器(8042芯片)发送扫描码0x16到CPU;
- BIOS中断处理程序查表将0x16转为ASCII 0x31('1'),存入AL;
- 但若用户按的是功能键(如F1),AL返回0,AH返回扫描码0x3B。
这解释了为何主循环用CMP AL, '1'而不用CMP AL, 1:'1'是ASCII 0x31,而数字1是0x01,二者在寄存器中是完全不同的比特模式。更隐蔽的是,当程序用INT 16H读功能键时,必须检查AH寄存器,否则AL=0会被误判为“空输入”。
这种设计迫使程序员直面硬件真相:键盘不是字符设备,而是状态机设备;每次按键都是对CPU的一次中断请求,其响应结果取决于当前键盘布局、CapsLock状态、NumLock状态三重上下文。
3. 密码比对不是字符串相等,而是内存块CRC式逐字节消融
3.1 REPZ CMPSB的硬件级实现:DF标志位控制的自动迭代
密码校验核心逻辑:
LEA SI, BUF1 ; SI指向已存密码(从MSG1+2开始) LEA DI, BUF2 ; DI指向新输密码(从MSG2+2开始) MOV CH, 0 MOV CL, MSG1+1 ; CX = 已存密码长度 CMP CL, MSG2+1 ; 先比长度,不等直接跳WRONG JNZ WRONG REPZ CMPSB ; CX次字节比较,ZF=1且CX=0才成功 JZ RIGHT ; ZF=1表示所有字节相等REPZ CMPSB不是高级语言的for循环,而是CPU内部的硬件迭代单元:
- 每次执行CMPSB时,CPU自动做三件事:①
DS:[SI] - ES:[DI](字节减法,影响ZF);②SI ← SI ± 1;③DI ← DI ± 1; REPZ前缀使该操作重复CX次,但一旦ZF=0(某字节不等)立即终止;MOV CH,0是必须的——因为CX是16位寄存器,而MSG1+1只存8位长度,若CH残留旧值会导致CX远大于实际长度,触发越界访问。
注意:此处ES段寄存器必须与DS一致!原程序未显式设置
MOV AX, DS+MOV ES, AX,依赖于启动时DS=ES的默认状态。若在其他环境中运行,需补此指令,否则CMPSB会跨段比较,结果不可预测。
3.2 缓冲区布局的魔鬼细节:DOS输入协议强制的内存偏移
观察BUF1和BUF2的定义方式:
MSG1 DB 255, 0 ; 输入缓冲区头(2字节) BUF1 DB 255 DUP(0) ; 实际字符串存储区(255字节) MSG2 DB 255, 0 ; 校验缓冲区头(2字节) BUF2 DB 255 DUP(0) ; 实际字符串存储区(255字节)这意味着:
LEA SI, BUF1得到的地址 =MSG1 + 2LEA DI, BUF2得到的地址 =MSG2 + 2- 但
MSG1+1存放的是长度值,MSG1+2开始才是字符串首字符
因此,当用户输入"123"(3字符)时:
- MSG1[0] = 255(最大长度)
- MSG1[1] = 3(实际长度)
- MSG1[2] = '1', MSG1[3] = '2', MSG1[4] = '3', MSG1[5] = 0DH, MSG1[6] = 00H
所有密码操作必须严格遵循这个2字节头部偏移规则。若错误地用LEA SI, MSG1作为字符串起点,比较时会把MSG1[0](255)和MSG1[1](3)当作密码前两位,必然失败。
3.3 三次错误退出机制:BP寄存器的计数器本质
错误计数用MOV BP, 3+DEC BP+JNZ SSS实现,但BP在此处承担双重角色:
- 作为计数器:初始值3,每次错误减1;
- 作为栈帧指针?不,此处未建立栈帧,BP纯粹被借作通用计数寄存器。
关键陷阱在于DEC BP后的JNZ判断:当BP从1减为0时,ZF=1(零标志置位),JNZ(Jump if Not Zero)不跳转,执行STC(Set Carry Flag)后JMP OK。但STC的存在暴露了设计者的防御性思维——为防止BP减到负数导致无限循环,用进位标志标记错误终态。现代等价逻辑是if (--attempts == 0) break;,而这里用STC+JC EXIT实现相同语义。
4. 界面绘制不是打印字符,而是显存字节的精准覆写
4.1 ASCII图形字符的显存映射:B800:0000的物理地址真相
菜单边框使用186、187、188、200、201、205等ASCII码,这些并非普通字符,而是IBM PC BIOS定义的图形字符集(Code Page 437)。当执行MOV AL, 205+INT 10H时:
- BIOS将AL值作为字符码,在字符发生器ROM中查找对应8×14点阵;
- 将点阵数据写入显存B800:0000起始位置;
- 每个字符占2字节:低字节=字符码,高字节=属性(前景色/背景色)。
因此DB 21 DUP(205)生成的是一条横线,其本质是向显存连续写入21个“字符码205+属性字节”的字对。若属性字节设为0x1F(蓝底白字),则整条横线呈现蓝色背景白色横线。
4.2 定位输出的底层实现:光标寄存器的端口编程
MOV AX, 0E0DH中的0E0DH是功能号0EH(TTE模式)+ 0DH(回车),但真正的光标定位需直接操作I/O端口:
- 向0x3D4端口写入0x0F(光标位置低字节索引)
- 向0x3D5端口写入光标位置低字节值
- 向0x3D4端口写入0x0E(光标位置高字节索引)
- 向0x3D5端口写入光标位置高字节值
原程序未显式设置光标位置,依赖INT 10H功能00H(设置模式)自动复位光标到(0,0),再通过多次回车/换行到达目标位置。这是典型的用功能调用掩盖硬件细节的设计,虽简化开发,却丧失对光标坐标的精确控制能力。
4.3 字符串显示的零拷贝真相:DS:DX的段地址绑定
LEA DX, MENU+MOV AH, 9+INT 21H的执行流程:
- CPU将DS段寄存器值左移4位,加上DX偏移量,得到物理地址;
- DOS内核从此地址读取字符串,直到遇到'$'终止符;
- 整个过程不复制字符串到DOS缓冲区,而是直接读取用户内存。
这意味着:若MENU定义在代码段(CS),但DS未正确初始化为CODE段,则INT 21H会从错误地址读取,显示乱码。原程序开头MOV AX, CODE+MOV DS, AX正是为此——段寄存器不是可选配置,而是内存寻址的强制前提。
5. 避坑:微机原理课设中汇编调试的五大血泪现场
5.1 现象:菜单显示乱码,出现方块或问号
原因:DS段寄存器未正确加载代码段地址。ASSUME CS:CODE, DS:CODE只是告诉汇编器段关联关系,不产生机器码;若启动后未执行MOV AX, CODE+MOV DS, AX,DS仍为DOS加载程序时的随机值,导致INT 21H从错误内存读取MENU字符串。
解决:在START标号后立即添加段寄存器初始化代码,且确保在任何INT 21H调用前完成。
5.2 现象:输入密码后直接崩溃,或校验永远失败
原因:缓冲区定义错误。常见错误是写成MSG1 DB 255 DUP(0)(255字节全0),而非MSG1 DB 255, 0+BUF1 DB 255 DUP(0)。前者导致MSG1+1位置无长度值,CMP CL, MSG2+1比较的是随机内存值。
解决:严格按DOS输入协议定义缓冲区:首字节最大长度,次字节实际长度,后续为字符串区。
5.3 现象:光标卡死在菜单某处,无法输入
原因:INT 10H视频服务调用后未重置光标位置。例如显示菜单后,光标停在最后一行末尾,而INT 16H读键不改变光标,导致后续INT 21H输入时从错误位置开始覆盖。
解决:在每次显示操作后,用MOV AH, 2+MOV DX, 0+INT 10H显式设置光标到(0,0),或用MOV AX, 0E0DH强制回车换行。
5.4 现象:密码校验时长度相同但总提示错误
原因:REPZ CMPSB比较的是字节值,而DOS输入在字符串末尾自动添加0DH(回车)和00H(结束符)。若已存密码BUF1未手动添加00H,而新输密码BUF2包含0DH+00H,则CMP CL, MSG2+1比较长度时相等,但CMPSB会把BUF1的垃圾值与0DH比较,必然失败。
解决:在输入密码后,手动在BUF1末尾写入00H:MOV BYTE PTR [BUF1+BX], 0(BX=输入长度)。
5.5 现象:按'3'退出时显示乱码,然后卡死
原因:EXIT标号中LEA DX, MESG后调用INT 21H,但MESG字符串末尾缺少'$'终止符。DOS的AH=09H功能要求字符串以'$'结尾,否则会持续读取内存直到遇到'$',造成越界显示。
解决:检查所有DB定义的字符串,确保末尾为'$',如MESG DB 'PRESS ANY KEY...$', 0DH, 0AH。
6. 设计取舍:为什么不用高级语言?——实模式下的确定性生存法则
6.1 确定性:毫秒级响应与零抽象开销的刚性需求
这个密码校验程序部署在DOS实模式下,没有内存管理单元(MMU)、没有虚拟内存、没有系统调用抽象层。当用户按下'1'键时,硬件中断信号经8259A PIC芯片传递给CPU,BIOS键盘中断服务程序(INT 09H)在20μs内完成扫描码转换,DOS的INT 21H服务在50μs内将ASCII写入AL寄存器——整个链路延迟稳定在100μs以内。若改用C语言编写,需经历:C运行时库初始化 → stdio缓冲区管理 → DOS文件句柄分配 → 多层函数调用栈压栈 → 最终调用INT 21H。即使最精简的Tiny C,启动时间也达200ms量级,且每次输入响应波动在±5ms。对于需要精确控制光标位置、实时反馈的交互界面,这种不确定性是致命的。
6.2 可验证性:寄存器状态即程序状态的终极透明
在调试器中单步执行时,你可以随时查看:
- AL = 当前按键ASCII码(物理键盘输入的直接映射)
- CX = 密码长度(内存中某个字节的原始值)
- SI/DI = 字符串地址(物理内存地址,无指针抽象)
- ZF/CF = 比较结果(CPU标志寄存器的物理比特)
这种状态与硬件一一对应的特性,让bug定位变成确定性过程。比如发现密码校验失败,只需在REPZ CMPSB前暂停,检查SI指向的BUF1内容、DI指向的BUF2内容、CX值,三者对比即可定位是长度读取错误、字符串存储偏移错误,还是输入时未清空缓冲区。而高级语言中,strcmp()返回-1/0/1,你永远不知道是哪个字节不等、缓冲区是否越界、内存是否被其他函数污染。
6.3 资源确定性:256字节内存的硬边界生存
整个程序编译后不超过1KB,常驻内存仅占用640KB常规内存中的极小部分。DOS时代内存是硬资源:TSR程序(Terminate and Stay Resident)需精确计算自身占用字节数,避免覆盖其他程序。本程序用MSG1 DB 255,0明确声明最大内存需求,编译器据此分配段空间。若用C语言,malloc()的堆管理器需额外内存维护空闲链表,且碎片化不可预测——在640KB内存中,一个未释放的100字节内存块可能导致后续1KB分配失败。
我至今保留着当年调试这个程序的习惯:每次修改后,先用DEBUG.EXE加载.COM文件,用R命令查看所有寄存器初始值,用D DS:0确认数据段布局,用T单步跟踪INT 21H调用前后AL/CX的变化。不是因为怀旧,而是因为当你的程序必须在没有操作系统庇护的裸金属上运行时,寄存器就是你唯一的操作系统。希望帮到你。
本文还有配套的精品资源,点击获取