8086汇编上机实验1. 上机实验的本质验证“地址与总线”的手感1.1 从课本例题到可运行程序之间藏着一道环境鸿沟很多朋友第一次接触8086汇编是在课本上看一堆 MOV、INT 21H、LOOP 指令觉得逻辑也不难。可一旦坐到电脑前把书上的例题敲进去按下回车要么屏幕上什么都没有要么直接死机甚至系统报错。我当年上8086汇编上机实验时就是这个状态代码抄得一个字母不差结果程序一运行就乱码最后发现是字符串定义末尾少了一个$结束符。从那一刻起我意识到汇编上机实验和看课本完全是两回事。所以这篇文章我想认真聊聊8086汇编上机实验到底在练什么以及怎么把纸面上的片段变成屏幕上能跑的机器码程序。适合三类读者正在上微机原理相关课程、需要做汇编实验的学生想自己补一补底层基础、准备搭建环境练手的人以及那些已经学过但始终没搞懂“为什么代码能跑”的人。1.2 8086汇编实验真正训练的三个能力一次合格的上机实验不是把题目的程序抄一遍然后截个图交差。它真正在训练三件事。第一理解分段内存模型。8086靠段地址和偏移地址组成20位物理地址这段地址怎么算、DS凭什么知道你的数据在哪、CS和IP怎么配合这些只有在调试器里看到地址变化才真正有感觉。第二理解数据表示与IO约定。屏幕上显示一个字符和内存里的字节和寄存器里的二进制这三者之间的转换关系是汇编实验最核心的关卡。键盘输入一个“5”你拿到的其实是 0x35不是数值5。这种ASCII与数值之间的别扭恰恰是上机实验真正要练的东西。第三理解“程序不是写出来就能跑”的残酷现实。高级语言出错有编译器帮你兜底汇编出错就是地址错位、栈溢出、中断被误调大概率直接崩溃。这种调试能力是上机实验最有迁移价值的收获。2. 搭建最小实验环境DOSBox、MASM和DEBUG的搭配2.1 为什么还在用DOSBox跑16位汇编直接在现代操作系统里跑8086汇编并不现实。它要访问实模式中断、直接操作内存和端口现代系统的保护模式和权限隔离不会放行。所以大多数上机实验都选在DOSBox这类模拟环境里运行把完整的DOS环境封装成一个小窗口。那为什么模拟的是DOS而不是别的因为8086时代的程序通常依托DOS系统调用。中断向量表里一堆功能号比如 INT 21H 是DOS功能调用INT 10H 是显示服务这些中断只有在DOS环境下才完整可用。DOSBox模拟的就是这套环境并且体积小、启动快、开箱即用不需要安装虚拟机。2.2 环境搭建步骤与配置参数我以最常见的组合为例DOSBox MASM 6.11 DEBUG 命令。整个过程大概花不到十分钟细节如下。第一步安装DOSBox。安装完成后DOSBox启动是一个Z盘提示符但Z盘是虚拟的重启后会丢。所以需要把装有MASM等工具的真实目录挂载进去。我在配置里通常这么写mount c d:\8086lab set pathc:\masm c:这段配置的意思是把d:\8086lab这个本地文件夹挂载成DOSBox里的C盘然后把MASM工具目录加入PATH。实际操作时可以把这两行写进DOSBox根目录下的配置文件里以后启动直接进入工作区。MASM6.11里面主要用的是ML.EXE做编译用LINK.EXE做链接还有DEBUG.EXE做调试。第二步把需要的文件拷贝进工作目录。通常包括自己写的.ASM源文件、MASM工具集。如果想省事把整个MASM目录都放进去编译时用完整路径调用。第三步跑一次最简单的编译。比如把存好的一段源码命名为HELLO.ASM然后依次执行masm HELLO.ASM link HELLO.OBJMASM会生成.OBJ目标文件LINK再把它变成.EXE可执行文件最后直接在提示符下输入HELLO就能运行。注意MASM5.0之后可以直接用ML /c跳过交互式询问但很多课程默认用的MASM 6.11还是需要按步骤来。2.3 一张工具分工表工具作用常见操作文本编辑器编写 .ASM 源文件任意编辑器保存为纯文本注意扩展名MASM把源文件编译为OBJmasm 文件名.ASMLINK把OBJ链接为EXElink 文件名.OBJDEBUG加载程序并单步调试debug 文件名.EXE这套组合里很多人容易忽略的一点是文本编辑器保存的文件可能在文件头加BOM标记导致MASM第一行报错。我建议用无BOM的UTF-8或者ANSI编码保存。另外文件名最好不要用中文字符——MASM对中文文件名的兼容性极差我见过太多同学卡在这一步改回英文名瞬间就好了。3. 一套可复用的程序骨架数据段、代码段与DOS中断的协作3.1 写汇编先问自己数据放哪里、代码从哪里开始上机实验写程序最稳的做法不是从第一行开始憋而是先搭骨架。8086汇编程序一般包含数据段、代码段有时还有栈段。我习惯先写一个模板每次上机前默写一遍既能热身也能避免最常见的低级错误。下面是我自己在实验里反复使用的最小框架。它什么都不干只是初始化数据段然后调用DOS功能显示一行字符串最后正常退出。写熟了之后所有实验都可以在这个框架上填充。; 最小可运行框架显示一行字符串后退出 DATA SEGMENT MSG DB Hello, 8086!$ DATA ENDS CODE SEGMENT ASSUME CS:CODE, DS:DATA START: MOV AX, DATA MOV DS, AX LEA DX, MSG MOV AH, 09H INT 21H MOV AX, 4C00H INT 21H CODE ENDS END START这里有几处关键细节值得反复琢磨。MOV AX, DATA / MOV DS, AX这两句绝对不能不写。很多同学以为自己是写在DATA段里的DS就自动指向DATA但事实不是这样。DS在程序加载时并没有自动变成你定义的这一段必须用代码主动赋值。这一步如果不做后续所有对变量的访问都会指向错误的内存区域。ASSUME只是告诉汇编器你的段寄存器逻辑上指向哪个段它不生成任何机器指令。它影响的是汇编器对变量和标号的地址计算方式。3.2 DOS中断调用的关键约定INT 21H是DOS提供的中断例程功能多到可以写一本书。在实验中天天打交道的就几个AH01H读单个字符并回显AH02H显示单个字符AH09H显示字符串AH0AH读入带缓冲的字符串。上机实验要记住一个核心原则功能号放在AH里参数按该功能号的要求放在对应寄存器里。拿上面的 09H 举例。AH09H要求DS:DX指向一个以$结尾的字符串。我们用LEA DX, MSG取MSG的偏移地址放入DX因为DS已经指向DATA段所以DS:DX就构成了完整地址。如果忘记LEA直接用MOV DX, MSG汇编器会把MSG的第一个字节内容当成偏移地址传入程序大概率会在屏幕上显示一堆无意义字符。3.3 程序正常退出的两种做法很多同学写的汇编程序最后是直接跑没的没有任何退出机制窗口卡死。8086程序退出给DOS最标准的方式是MOV AX, 4C00H INT 21HAH4CH表示“结束当前进程并返回父进程”AL里放退出码0表示正常结束。这样程序结束时会回到DOS提示符而不是继续往下执行死循环。如果不这样做程序执行完最后一条指令后会顺着内存继续跑任意数据那个结果谁也没法预料。在实验中发现另一种合理做法是调用 INT 20H但 INT 20H 要求CS指向程序段前缀PSP否则行为不确定所以我还是更推荐 4CH 这条路径。要用菜单选择功能时老师若指定用某种方式再按老师的要求来就行。4. 上机必练的输入输出转换字符、数字与ASCII之间拐弯4.1 数字在机器里不分十进制到了上机阶段最大的认知转变就是明白“内存里根本没有十进制”。你看到屏幕上的数字“123”是系统一点点把二进制值转换成了ASCII字符再显示的。这听起来很基础但实操时踩坑的人不计其数因为实验题经常是要求“从键盘输入一个两位数再输出这个数的平方”这里的输入输出都涉及ASCII和数值之间的转换。首先明确8086从键盘读字符时你得到的是字符的ASCII码。比如键盘输入“5”AL里收到的是 0x35。如果直接用这个值参与算术运算算出来的结果一定不对。要把它变成真正的数值5必须减去0x30也就是SUB AL, 30H。反过来一个数值5要显示在屏幕上必须先加上0x30变成字符5再调用显示单字符的功能。这就是最基础的ASCII转换数字字符与数值之间相差0x30。4.2 从键盘读数字逐位累乘与ASCII减30H以读取一个两位十进制数为例。假设用户会输入两个数字字符第一个是十位第二个是个位。算法的核心是每一位字符转成数值后用乘以10再加上下一位的方式累加。伪代码是这样的; 假设AH0从键盘读两个数字字符组合成CX中的两位数 MOV AH, 01H INT 21H ; AL 第一个字符 SUB AL, 30H ; 转成数值 MOV CL, 10 MUL CL ; AX 十位 * 10 MOV CX, AX ; 暂存 MOV AH, 01H INT 21H ; AL 第二个字符 SUB AL, 30H ; 转成数值 MOV AH, 0 ADD CX, AX ; CX 十位*10 个位这里有一个细节MUL CL默认的被乘数是AL乘数是CL结果放在AX里。做完第一步后AX里可能是两位数乘以10结果最大99完全放得下所以AX够用。但如果你读取的是三位数或者用了MUL CL后结果超过255就需要考虑使用MOV DX处理高位的问题不能默认AX一定能装下。另一个容易忽略的细节在读取用户输入时DOS会把回显也打在屏幕上用户按回车键时 AL 得到的是 0x0D。如果程序要连续读取多个输入需要自己判断缓存中的回车换行否则第二次调用就可能在用户没输入时直接读到残留的换行字符。4.3 输出数字从低位反向除10取余输出一个数值比输入更麻烦。比如要显示数字123你不能直接把0x7B加到0x30上因为123的ASCII不是单个字符。正确做法是不断除以10取出余数再从高位到低位依次显示。我经常用“倒着求正着打”来概括。先把123除以10余数3是个位商1212再除以10余数2是十位商11再除以10余数1是百位商0。这样从低位到高位依次得到3、2、1但显示顺序必须是1、2、3所以要把余数先存到一个数组里最后一次倒序输出。典型的代码片段是这样; 假设AX中是要显示的16位无符号数结果逐位压栈然后弹栈显示 MOV CX, 0 ; 计数器记录压入了几位 MOV BX, 10 LOOP1: MOV DX, 0 ; 防止除法溢出DIV要求DX:AX除以BX DIV BX ; AX 商DX 余数 ADD DX, 30H ; 余数转ASCII字符 PUSH DX ; 保存到栈 INC CX CMP AX, 0 JNE LOOP1 LOOP2: POP DX MOV AH, 02H INT 21H LOOP LOOP2这段代码尽管短内部的坑非常多。第一个坑是DIV BX之前必须对DX清零。DIV指令做的是“DX:AX32位除以BX”如果不先清DXDX里一旦残留上次运算的高位垃圾结果完全不可控。第二个坑是每显示一个字符要调用一次INT 21H功能号AH02H要求DL里放字符调用的开销很小但你要注意别把DX当成了DL以外的用途还在别处保留了数据。第三个坑当数值很大时比如可能要显示65432这里的流程没有问题但如果实验题要求输出的是带符号数就要先判断正负输出负号再对绝对值做除10取余。4.4 字符串显示与“$”结束符的坑字符串显示用的功能号是9号规定字符串必须以$结尾。这个约束在上机实验里坑了无数人。你定义一个MSG DB Hello, 0DH, 0AH没有加 $程序会一直往后巧合显示内存里乱七八糟的内容直到碰巧碰见某个字节等于$才停止。解决办法是定义时手动加MSG DB Result is: $如果想要换行效果记得把回车换行放在字符串里面比如MSG DB Hello, 0DH, 0AH, $这里要注意顺序0DH是回车0AH是换行$一定要在最后。我在带实验时不止一次遇到同学把$放在字符串最前面然后屏幕上只显示“$”后面到字符串结尾的部分效果看起来像是程序少打印了前一半内容。还有一个很隐蔽的问题DOS功能09H显示字符串时因为是以$为结束标志所以如果变量内容本身需要包含美元符号你没法直接输出。处理办法是按字符输出不要用09H。这个场景不常见但一旦撞上就要知道原因。5. DEBUG单步调试让程序在眼皮底下跑才能看见“内存视角”5.1 怎么用DEBUG加载刚编好的EXE上机实验绝不是写完、编译、运行、结束。真正让水平拉开差距的是调试能力。DEBUG是DOS自带的调试器功能足够应付大多数实验课。使用时先编译链接出EXE然后执行DEBUG 你的程序名.EXE就能进入-提示符。这里能做的操作包括查看寄存器、查看内存、单步执行、设置断点、反汇编等等。我第一次学会单步调试的时候最大的震撼是发现程序执行的每一步都会真实改变寄存器和内存。以前在课本上写ADD AX, BX觉得平淡无奇看到它实际改变AX的值时才真正理解了“指令就是操作数据”。5.2 常用DEBUG命令速查表命令含义典型用法备注R显示或修改寄存器R / R AX直接看各寄存器和标志位D显示内存数据D DS:0100查看数据段内容验证变量E修改内存E DS:0100 41调试时手工改数据U反汇编U CS:0100看当前内存的指令T单步执行一条指令T / T 5一次执行5条指令P执行当前调用/循环到结束P遇到INT 21H时用P跳过G运行到指定地址G 0108配合断点定位问题Q退出DEBUGQ退出后回到DOS提示符5.3 单步跟INT 21H时要注意什么这里有个重要的实操经验用T单步跟到INT 21H这一行时按一次 T 会跳进DOS的中断处理例程那是一个巨大的代码库你会在里面迷失方向。正确做法是看到 INT 21H 时改用P命令它会把整个中断调用当作一步执行完。这是DEBUG使用中实操最频繁的经验不掌握的话调试会非常痛苦。另外一个经验是程序启动后先用R查看初始寄存器状态。你会看到DS等于PSP段而CS和IP已经指向代码段第一条指令。这两条信息对定位问题非常有价值因为可以立即算出你的变量在内存中的逻辑地址。5.4 一个可以用DEBUG排查的经典崩溃例子假设有一段程序运行时直接卡死。我们用DEBUG加载后U反汇编看到代码执行到了数据区域这通常是因为程序没有正确退出或者函数调用返回错误。我一般按这样的流程排查首先用R看当前的CS:IP确认它是否还在代码段范围内。然后D查看当前指令附近的数据看是否都是一些无意义的数字如果是几乎可以断定程序流程跑飞了。这时回溯上一层找到应该RET或者INT 21H的地方用G运行到该处再用P单步观察。曾经遇到学生的程序是这样的子程序RET前少弹了一个栈上的数据导致返回地址变成了某个变量的内容。从代码上根本看不出来但DEBUG里看到RET之后IP跳到了一个奇怪的位置马上就能定位。这种排查能力靠看书学不来只能靠上机时一遍遍操作积累。6. 纸上推演没问题、一上机就崩的六个典型场景6.1 忘记初始化DS导致数据段寻址错位没有MOV DS, AX所有变量的地址都会错位。比如LEA DX, MSG如果DS没有指向数据段那么DX只是正确的偏移DS:DX却组合出了错误地址。表现可能是屏幕显示乱码也可能是程序崩溃。我自己的习惯是一上来就把段赋值写完当成模板肌肉记忆而不是每次重新想。6.2 INT 21H功能号忘记放在AH里把功能号写成MOV AX, 09H与MOV AH, 09H是两码事前者会覆盖AL中可能存在的其他数据。如果前面的步骤刚好依赖AL错误很隐蔽。曾经有同学在输入字符后马上用09H功能显示字符串但他没有把AH重新设置成09H因为之前AH01H的残留还在程序就调用了错误的中断功能显示结果完全不是预期。6.3 除法溢出与标志位状态8086的DIV指令在商溢出时会产生除法错中断程序直接终止。比如将字节除法结果期望不超过255却把除法当成字除法就可能意外溢出。所以每次用DIV/DIV之前必须确认除数、被除数范围。稳妥的做法是字除法前清DX字节除法前确保AH为0。6.4 栈空间不足8086的分段机制带来的隐蔽问题8086没有自动在程序里划分栈空间但程序里往往需要使用栈比如刚才的除10取余输出。如果不指定SS和SP程序加载时会沿用PSP所提供的栈空间可用空间很小。一旦程序里的PUSH比较多栈就会侵入别的内存区域导致后续数据被破坏。解决办法是程序开头显式定义栈段STACK SEGMENT DW 100H DUP(0) STACK ENDS然后在代码段的 ASSUME 里加上SS:STACK并用代码把栈段地址加载进SS。如果你用END START这种方式进入程序会自动初始化部分内容但栈这个事我建议还是写明白免得某些实验模板里缺这段直接翻车。6.5 字符串定义少了“$”结束符这个问题在上面已经讲过是课本和上机实验差距最大的体现之一。纸面上写一个数组看不出毛病放到真机上跑就输出乱码。更麻烦的是这种现象时好时坏取决于后面内存碰巧是什么内容。所以我的建议必须是每个准备用09H显示的字符串定义完先扫一眼末尾有没有$。6.6 段间跳转与近跳转的界限8086的跳转指令有短跳转-128到127、近跳转同一个段的64KB内、远跳转跨段。实验中最容易出问题的不是JMP本身而是子程序的CALL和RET。如果子程序在同一代码段内调用用近调用没问题如果一不小心在另一个代码段定义了一个跳转目标汇编器又会给出各种问题。这个场景不常碰到但一旦用到了把段归属理清楚是必须的。7. 带完实验后沉淀下来的高效率上机习惯7.1 先画“内存布局图”再写代码上机实验频繁踩坑之后我养成了一个习惯。每次写代码前先在草稿纸上画出这个程序启动后内存大概怎么分布。数据段里有哪些变量每个变量占几个字节代码段从哪个地址开始栈顶在什么地方。这个过程不花几分钟但能避免大量“边写边猜”导致的问题。尤其是涉及到多个变量、数组和缓冲区时用DEBUG验证内存布局能精准定位问题。比如输入一个两位数后需要存到一个缓冲里我会在纸上标记缓冲区起始偏移是多少存十位和个位的位置在哪。写完之后用DEBUG的D命令查看数据段就能直接对照纸上画的布局检查程序是否正确。7.2 让代码“跑一步看一步”而不是写完再调我发现很多同学写程序习惯一口气写完再想一次性跑通。但汇编不是这样因为一条指令的执行效果常常会影响后面十几条指令一次性写完调试时错误来源可能很多。正确做法是每完成一个小的模块就编译一次用DEBUG或直接运行验证结果。比如先写一个字符串显示跑通再加入输入验证读到的是否正确再加算术运算最后加输出。每次增量都很小出问题了马上知道嫌疑在哪个模块。7.3 保存若干可复用的宏片段几次实验后手里会有不少稳定可靠的小代码块。比如“显示一个字符”“显示一个字符串”“读取一个字符”“十进制数转ASCII输出”“清空并退出”。我平时会把它们放在一个独立文件里需要时复制进当前实验。这不算抄袭而是实验效率的体现。关键一点是复制过来之前想一遍每个寄存器怎么用避免冲突。另外实验里写过的代码一定要留好。不只为了交作业更为了后续复习时能快速回忆起当时踩过的坑。我自己的习惯是每个实验源文件开头都留几行注释记录这个程序当时的重点和最容易出错的地方。等几个实验做完那些注释就成了自己错题本。说了这么多其实上机实验最核心的还是多练。很多配置和技巧看十遍不如亲手上机敲一遍。我第一次用DEBUG单步看到内存变化时的震撼现在还记得清清楚楚。希望你也能在反复调试中体会到那种“原来如此”的感觉而不是把8086汇编只当成一门背完就忘的课程。
阅读完成 · 觉得有帮助?