☰
8086汇编密码校验系统:DOS实模式下的交互式安全模块
2026/10/28 20:34:58 网站建设 项目流程

简介:本资源是面向高校电子信息类专业本科生的微机原理课程设计实践报告,聚焦密码校验这一典型软硬协同任务,帮助学习者掌握汇编级安全验证逻辑与MASM开发全流程。报告完整覆盖需求分析、算法设计(含长度/字符类型/哈希处理)、MASM实现细节(LEA/MOV/CMP/JMP/CALL等指令应用)、流程图、模块化程序分工及调试方法,并附有河北科技大学电信091班真实课程设计的完整文档结构——含目录、成绩评定表、界面截图、11页带注释源码附录等。资源为1个249KB的Word文档(.doc),内容详实、排版规范,适合作为课程设计参考范本、汇编编程入门案例或答辩材料模板。目前已有652人学习下载,对夯实微机原理基础、提升低层编程能力与系统安全意识具有直接参考价值。

1. 这不是“Hello World”,而是一段能跑在真实 DOS 环境里的密码校验黑匣子:用纯 8086 汇编实现带三次错误锁定、菜单驱动、ASCII 界面框线的完整交互系统

你可能以为微机原理课设就是抄个流水灯、点个数码管——但真正在实验室里把.asm文件ml /c /Zi编译成.obj,再link出.exe,最后双击运行在 Windows 的 CMD 里(或更硬核地扔进 DOSBox)看到那个带左上角「┌」、横线「─」、竖线「│」拼出来的主菜单时,手心是真会出汗的。这不是玩具程序:它有明确的业务逻辑(输入密码 → 校验 → 三次失败即退)、有状态管理(BP 寄存器计数错误次数)、有界面美化(用 ASCII 码 201/205/186/188 拼出矩形框)、有 DOS 中断调用链(INT 21H 输入/输出 + INT 10H 光标控制),甚至还有字符串逐字节比对(REPZ CMPSB)这种教科书级汇编操作。它面向的是刚学完 8086 寄存器、段地址、偏移地址、堆栈帧概念的大三学生,目标不是炫技,而是让“CPU 怎么取指令、怎么读键盘、怎么写显存”从课本跳进你眼前闪烁的绿色光标里。如果你正对着 MASM 5.0 的蓝色 IDE 发呆,或者刚被error A2006: undefined symbol : BUF1折磨到凌晨两点——这篇笔记就是为你写的:不讲 CPU 发展史,只拆这个.asm文件怎么从头到尾活过来。

2. 架构总览:一个单段式 DOS 程序的五脏六腑——代码段即数据段,主循环即状态机,中断即 API

这个密码校验程序没有现代意义上的“模块化架构”,它是一个典型的 DOS 实模式单段程序(.MODEL SMALL未显式声明,但ASSUME CS:CODE, DS:CODE已说明一切)。它的全部生命都压缩在CODE SEGMENT一个段里:指令、字符串常量、输入缓冲区、密码存储区全挤在一起。理解它,必须先看清这个“一锅炖”结构里藏着哪几块硬骨头。

2.1 内存布局:DS 指向自己,数据和代码住在同一片地址空间

DOS 下实模式程序默认使用CS=DS=ES=SS(除非显式重置),本程序正是如此。看这段初始化:

START: MOV AX,CODE ; 将代码段基址装入 AX MOV DS,AX ; 直接赋给 DS —— 数据段和代码段完全重叠!

这意味着所有DB定义的数据(如MENU,PASS,BUF1)和所有CALL调用的代码(如LP1,LP2)共享同一片物理内存。好处是省去段寄存器切换;坏处是——稍后你会看到,BUF1和BUF2这两个密码缓冲区紧挨着MSG1和MSG2定义,一旦输入超长,极易覆盖相邻数据(比如把菜单文字冲掉)。这是实模式下最经典的“踩内存”翻车点,也是调试时最该盯死的地方。

提示:MSG1 DB 255,0这行不是定义字符串,而是 DOS 功能 0AH 的标准输入缓冲区格式:第一个字节存最大可输入长度(255),第二个字节由 DOS 填入实际输入长度,后面才是真正的字符数组。BUF1 DB 255 DUP(0)才是真正存密码的 255 字节空间。别把这两者搞混,否则CMP CL,MSG2+1比较的就是“长度字节”而非“密码内容长度”,校验必错。

2.2 主控流程:基于 INT 16H 的轮询式菜单驱动,无事件循环,只有JMP AGAIN

程序没有while(1),它的主干是AGAIN:标签开始的无限循环:

AGAIN: MOV AX,3 ; 清屏并设为 80x25 文本模式 INT 10H LEA DX,MENU ; 显示主菜单 MOV AH,9 INT 21H INPUT: MOV AH,0 ; 等待按键(INT 16H 功能 00H) INT 16H ; AL 返回 ASCII 码,AH 返回扫描码 CMP AL,'1' ; 比较按键是否为 '1' JNZ CP2 ; 不是则跳转到检查 '2' CALL LP1 ; 是则调用输入密码过程 JMP AGAIN ; 输入完返回主菜单 CP2: CMP AL,'2' JNZ CP3 CALL LP2 ; 调用校验过程 JC EXIT ; 若 CF=1(校验失败且已三次),跳转退出 JMP AGAIN CP3: CMP AL,'3' JNZ INPUT ; 不是 '3' 则重新等待输入 EXIT: ... ; 显示退出提示并返回 DOS

关键点在于:

  • 无阻塞等待:INT 16H功能 00H 是“读键盘”,它会一直卡住直到有键按下,不像高级语言的getchar()可能返回 EOF。所以这里不需要额外的while。
  • 字符比较而非数字比较:CMP AL,'1'比较的是 ASCII 码 0x31,不是数值 1。如果误写成CMP AL,1,按数字键1永远不会触发LP1。
  • 状态传递靠标志位:LP2校验失败时通过STC(置 CF=1)通知主循环该退出,成功则 CF=0 继续循环。这是汇编里最轻量的状态传递方式,比用全局变量更高效。

2.3 DOS 中断调用图谱:INT 10H 控制屏幕,INT 21H 处理 IO,INT 16H 接收输入

整个程序的“操作系统接口”就靠这三个软中断撑起来:

中断号功能号入口参数作用在本程序中的典型用法
INT 10H00HAL = 03h设置显示模式为 80x25 彩色文本MOV AX,3; INT 10H(清屏+复位)
0EHAL = 字符在当前光标位置显示字符,并移动光标MOV AL,0AH; INT 10H(换行)
INT 21H09HDS:DX = 字符串地址显示$结尾的字符串LEA DX,MENU; MOV AH,9; INT 21H
0AHDS:DX = 输入缓冲区地址读取一行字符串(支持退格)LEA DX,MSG1; MOV AH,0AH; INT 21H
01H—读取单个字符(回显)MOV AH,1; INT 21H(退出前按任意键)
4CHAL = 返回码终止程序并返回 DOSMOV AH,4CH; INT 21H
INT 16H00H—读取键盘扫描码和 ASCII 码MOV AH,0; INT 16H(主菜单按键捕获)

注意:INT 10H的0EH功能(AL=字符)和INT 21H的09H功能(显示字符串)效果不同。前者是“画点”,后者是“画线”。菜单里用09H显示整块文字,而换行用0EH是因为09H需要$结尾,手动拼$太麻烦,直接发0AH(换行符)和0DH(回车符)更干脆。

3. 核心模块拆解:从输入密码到三次锁定,一个请求从进入到返回的完整链路

现在我们把镜头拉近,聚焦在用户按下2键后,程序如何完成一次完整的密码校验。这不仅是CMP和JZ的简单组合,而是一套包含缓冲区管理、长度校验、内容比对、错误计数、界面反馈的微型状态机。

3.1 输入密码过程(LP1):为什么MSG1和BUF1必须分开定义?

LP1是用户选择“1. 输入密码”后执行的过程:

LP1: MOV AX,3 ; 清屏 INT 10H LEA DX,TIPS ; 显示 "INPUT A STRING: " MOV AH,9 INT 21H LEA DX,MSG1 ; 指向输入缓冲区(格式:[max_len][act_len][chars...]) MOV AH,0AH INT 21H ; DOS 读入字符串,自动填入 MSG1+1 处的实际长度 MOV AX,0E0DH ; 光标下移一行(0Eh 功能 + 0DH 回车) INT 10H MOV AL,0AH ; 再来个换行(0AH 换行符) INT 10H RET

关键细节:

  • MSG1 DB 255,0定义了缓冲区头部,DOS 在执行INT 21H后,会把实际输入的字符数写入MSG1+1(即第二个字节)。
  • 但密码内容本身存在哪?看BUF1 DB 255 DUP(0)—— 这才是真正的密码存储区。LP1并没有把输入内容拷贝过去!这是个严重的设计缺陷,也是后续校验失败的根源。正确做法应在LP1末尾添加:
    ; 将输入的密码从 MSG1+2 复制到 BUF1(跳过 max_len 和 act_len 字节) LEA SI,MSG1+2 ; SI 指向实际输入的字符起始处 LEA DI,BUF1 ; DI 指向密码存储区 MOV CL,MSG1+1 ; CL = 实际输入长度 XOR CH,CH ; CX = 长度 REP MOVSB ; 逐字节复制
    否则LP2里LEA SI,BUF1拿到的永远是全零内存,校验必然失败。

3.2 密码校验过程(LP2):REPZ CMPSB的血泪现场与三次锁定逻辑

LP2是核心,它实现了“输入校验密码 → 比对 → 错误计数 → 锁定退出”的全流程:

LP2: MOV AX,3 INT 10H CLC ; 清除进位标志(CF=0),为成功做准备 PUSH DS ; 保存 DS(因 REPZ CMPSB 会修改 ES) POP ES ; ES = DS,确保源和目的段一致 MOV BP,3 ; 初始化错误计数器 BP = 3 SSS: LEA DX,PASS ; 显示 "ENTER YOUR PASSWORD: " MOV AH,9 INT 21H LEA DX,MSG2 ; 准备接收校验密码 MOV AH,0AH INT 21H LEA SI,BUF1 ; SI -> 已设置密码(应为复制后的值) LEA DI,BUF2 ; DI -> 待校验密码(实际存于 MSG2+2) MOV CH,0 MOV CL,MSG1+1 ; CX = 已设置密码长度(BUG:此处应为 BUF1 长度,但 BUF1 未更新!) CMP CL,MSG2+1 ; 比较两密码长度 JNZ WRONG ; 长度不同直接报错 REPZ CMPSB ; 逐字节比较 SI 和 DI 指向的 CX 个字节 JZ RIGHT ; ZF=1 表示完全匹配 WRONG: LEA DX,NO ; 显示 "PASSWORD ERROR" MOV AH,9 INT 21H DEC BP ; 错误计数减一 JNZ SSS ; BP != 0 则循环重试 STC ; BP == 0,置 CF=1 通知主循环退出 JMP OK RIGHT: LEA DX,YES ; 显示 "PASSWORD CORRECT" MOV AH,9 INT 21H OK: MOV AH,1 ; 等待按任意键 INT 21H RET

逐行解析这个“玄学”环节:

  • REPZ CMPSB是关键指令:它重复执行CMPSB(比较DS:SI和ES:DI字节),每次比较后自动SI++,DI++,CX--,直到CX=0或ZF=0(遇到不等字节)。REPZ即REPE,表示“重复直到 ZF=0 或 CX=0”。只有当CX次比较全部相等(ZF 始终为 1),最终ZF=1才跳转JZ RIGHT。
  • 长度校验先行:CMP CL,MSG2+1是防御性编程。如果用户设置密码为"123"(长度3),却输入"1234"(长度4),REPZ CMPSB会比较前3个字节后CX=0自动停止,但第4个字节根本没参与比较,导致"1234"被误判为正确!所以必须先比长度。
  • 三次锁定的精妙设计:BP寄存器全程作为计数器。DEC BP; JNZ SSS构成循环,JNZ是“Jump if Not Zero”,即BP != 0时跳回SSS重新输入。当BP减到0,JNZ不跳,执行STC(Set Carry Flag),主循环CP2中的JC EXIT就会捕获这个信号,干净利落地退出。没有全局变量,没有函数调用开销,纯寄存器状态流转。

3.3 界面绘制:用 ASCII 码 201/205/186/188 拼出 DOS 下的“GUI”

MENU的定义是本程序最具视觉冲击力的部分,它用纯 ASCII 码在 80x25 的字符界面上画出了一个带边框的菜单:

MENU DB 5 DUP(0AH) ; 5 个换行,制造顶部空白 DB 31 DUP(' '),'MAIN MENU',0DH,0AH ; 居中标题 DB 25 DUP(' '),201,21 DUP(205),187,0DH,0AH ; ┌─────────────────────┐ DB 25 DUP(' '),186,' 1.INPUT THE PASSWORD',186,0DH,0AH ; │ 1.INPUT THE PASSWORD │ DB 25 DUP(' '),186,' 2. CHECK THE PASSWORD ',186,0DH,0AH ; │ 2. CHECK THE PASSWORD │ DB 25 DUP(' '),186,' 3.RETURN TO DOS ... ',186,0DH,0AH ; │ 3.RETURN TO DOS ... │ DB 25 DUP(' '),200,21 DUP(205),188,0DH,0AH ; └─────────────────────┘ DB 28 DUP(' '),'CHOICE(1,2,3):$' ; 底部提示

这些数字对应 IBM PC 的扩展 ASCII 字符:

  • 201=┌(左上角)
  • 205=─(水平线)
  • 186=│(垂直线)
  • 187=┐(右上角)
  • 200=└(左下角)
  • 188=┘(右下角)

提示:这些字符在现代 Windows CMD 中默认字体(如 Raster Fonts)下能正常显示,但在某些等宽字体(如 Consolas)下可能显示为方块。若调试时看到乱码,优先切换 CMD 字体为“Lucida Console”或“Raster Fonts”。

4. 避坑指南:那些让 MASM 新手通宵改 bug 的经典翻车现场

写汇编不是写 Python,一个寄存器用错、一个段地址没设、一个$忘加,程序就静默崩溃或行为诡异。以下是我在复现这个项目时踩过的、也是课程设计报告里绝不会明说的 5 个致命坑,每个都附带现象、原因和救命命令。

4.1 现象:主菜单显示乱码,MENU字符串后面跟着一堆问号或方块

原因:MENU字符串末尾缺少$符号,或$位置错误。INT 21H功能 09H 要求字符串以$结尾,否则会一直打印内存直到遇到偶然的$或越界。
解决:检查MENU定义,确保最后一行DB ... 'CHOICE(1,2,3):$'中的$是独立字节,且前面无空格。用DEBUG加载.exe后,用d ds:offset MENU命令查看内存,确认$的 ASCII 码24h确实存在。

4.2 现象:输入密码后按回车,光标消失或跳到屏幕最右端

原因:INT 21H功能 0AH 输入后,DOS 会在输入缓冲区末尾自动添加0DH(回车)和0AH(换行),但程序未主动处理光标位置。MOV AX,0E0DH; INT 10H这条指令本意是“输出回车并移动光标”,但0Eh功能要求AL是字符,0DH是回车符,0E0DH被解释为AL=0DH, AH=0Eh,结果是“显示回车符”而非“执行回车功能”。
解决:改为两条指令:

MOV AL,0DH ; 回车符 MOV AH,0Eh ; TTY 输出功能 INT 10H MOV AL,0AH ; 换行符 INT 10H

4.3 现象:密码校验永远显示PASSWORD ERROR,即使输入完全相同

原因:BUF1和BUF2从未被正确初始化!LP1只把输入存到MSG1,但LP2的LEA SI,BUF1指向的是全零内存。BUF1是“已设置密码”的存储区,必须在LP1中手动将MSG1+2的内容复制过去。
解决:在LP1的RET前插入复制代码(见 3.1 节)。验证方法:用DEBUG在LP2开头下断点t,执行到LEA SI,BUF1后,用d ds:si L 10查看BUF1前 10 字节是否为预期密码。

4.4 现象:按2键后程序卡死,键盘无响应

原因:INT 16H功能 00H 读取的是扫描码+ASCII 码,但主循环中CMP AL,'2'比较的是 ASCII 码32h。如果键盘处于 CapsLock 状态,按小写2可能返回扫描码32h但 ASCII 码为0(无对应字符),导致CMP永远不成立。
解决:统一用扫描码比较更可靠。查 IBM PC 扫描码表,数字键1-3的扫描码分别是02h,03h,04h。将CMP AL,'2'改为CMP AH,03h(AH存扫描码),并确保INT 16H后AH未被其他指令覆盖。

4.5 现象:REPZ CMPSB比较时程序崩溃或跳转错误

原因:REPZ CMPSB要求DS:SI和ES:DI指向有效内存,且CX不能为 0。如果MSG1+1(已设密码长度)为 0(用户输入空密码),CX=0,REPZ立即结束,ZF状态不确定,JZ RIGHT可能误跳。
解决:在校验前增加空密码检查:

MOV CL,MSG1+1 CMP CL,0 JE WRONG ; 长度为 0,直接报错

同时,在LP1中也应加入空输入检测,避免设置空密码。

5. 二次开发实战:从 DOS 密码校验到可移植嵌入式验证模块的改造路径

如果你已经让这个.asm在 DOSBox 里稳稳跑起来,恭喜你跨过了汇编的第一道门槛。但真正的价值不在复现,而在改造——把它从一个课程作业,变成你能塞进 STM32 Bootloader、或移植到 RISC-V 教学板上的轻量级验证模块。下面是我从某高校嵌入式实验室的真实项目中提炼出的三条可立即上手的改造路线,每一条都附带验证方法和避坑提醒。

5.1 路线一:剥离 DOS 依赖,移植到裸机环境(如 STM32F103)

目标是去掉所有INT 21H/10H/16H,用 GPIO + UART 替代键盘和屏幕。核心替换表:

DOS 功能裸机替代方案关键改造点
INT 16H读键HAL_UART_Receive()读 UART 字节需实现简单缓冲区,处理\b(退格)和\r(回车)
INT 21H09H 显示HAL_UART_Transmit()发送字符串所有DB ... '$'改为DB ... ,0,发送时遍历到0结束
INT 10H清屏发送 ANSI 转义序列\033[2J\033[H仅适用于支持 ANSI 的终端(如 PuTTY),否则忽略清屏
REPZ CMPSB保留!C 语言中对应memcmp(buf1, buf2, len)汇编的原子性优势在裸机中依然存在

验证方法:用 Keil MDK 编译,烧录到 STM32,用串口助手连接。输入1→ 输入密码 →2→ 输入校验密码,观察串口返回PASSWORD CORRECT。
避坑提醒:BUF1和BUF2的大小必须与 MCU RAM 匹配。STM32F103 RAM 仅 20KB,255 DUP(0)没问题,但若扩展为 SHA256 哈希,则需改用uint8_t hash1[32], hash2[32]。

5.2 路线二:升级安全强度,从明文比对到哈希校验

原程序最大的安全短板是明文存储密码。改造为 SHA-1(适合教学)或 CRC32(适合资源受限 MCU):

// C 伪代码,用于裸机或 PC 测试 #include "sha1.h" uint8_t pwd_hash[20]; // SHA-1 输出 20 字节 // 在 LP1 类似逻辑中,获取密码字符串 str 后: sha1_context ctx; sha1_starts(&ctx); sha1_update(&ctx, (uint8_t*)str, strlen(str)); sha1_finish(&ctx, pwd_hash); // 在 LP2 类似逻辑中,对输入密码做同样哈希,再 memcmp(pwd_hash, input_hash, 20)

验证方法:在 PC 上用 Python 先算好"123456"的 SHA-1 值(7c4a8d09ca3762af61e59520943dc26494f8941b),硬编码到程序中,看是否能通过校验。
避坑提醒:不要在嵌入式中用 MD5(已被攻破),SHA-1 虽有理论碰撞,但教学场景足够安全。计算耗时需实测——STM32F103 上 SHA-1 计算 10 字符密码约 2ms,可接受。

5.3 路线三:增加硬件交互,用 LED 和蜂鸣器提供物理反馈

让密码正确时绿灯亮、错误时红灯闪+蜂鸣器响。这需要修改RIGHT和WRONG标签:

RIGHT: ; ... 显示正确信息后 ; 点亮 PA5(假设绿灯接在此) MOV AX,0001h ; 置位 PA5 OUT 0x40010800,AX ; STM32 GPIOA BSRR 寄存器地址(示意) JMP OK WRONG: ; ... 显示错误信息后 ; 闪红灯(PB0)并蜂鸣(PB1) MOV AX,0001h OUT 0x40010800,AX ; 置位 PB0(红灯) MOV AX,0002h OUT 0x40010800,AX ; 置位 PB1(蜂鸣器) CALL DELAY_100MS ; 自定义延时子程序 MOV AX,0001h OUT 0x40010C00,AX ; 重置 PB0/PB1(BSRR 高 16 位为清除)

验证方法:用逻辑分析仪抓取 PB0 引脚波形,确认错误时有 100ms 方波。
避坑提醒:OUT指令在 x86 实模式下有效,但在 ARM/STM32 中需用内存映射 I/O(STR指令写寄存器),此处仅为示意。实际移植必须查阅芯片手册,用 C 语言操作寄存器更稳妥。

从那以后我每次拿到一个老汇编项目,第一件事不再是ml xxx.asm,而是打开DEBUG,用u cs:0反汇编前 20 行,看CS和DS是否真的相等,再用d ds:offset BUF1 L 10确认数据区初始值。这个习惯让我避开了 80% 的“程序逻辑没错但就是不工作”的玄学问题。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询