拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从冒泡排序到8259A中断:一份可复现的汇编实验全记录

从冒泡排序到8259A中断:一份可复现的汇编实验全记录

简介:这是一份汇编语言与接口技术课程的实验报告PDF,面向高校计算机、电子及相关专业学生,特别适合正在完成汇编程序设计或中断接口实验的读者。报告整理了两个典型实验:实验1使用MASM 6.11开发环境,通过MOV、CMP、XCHG、LOOP等指令实现冒泡法排序,对一批8位无符号数和8位有符号数分别按升序排列,并给出程序流程图、完整源代码和DEBUG调试的运行结果,其中还涉及数据段、代码段的定义以及循环嵌套和子程序调用;实验2基于8259A可编程中断控制器,自定义78H软件中断完成ASCII码到压缩BCD码的转换,同时设计IRQ10~IRQ12外部硬件中断服务程序,在屏幕上输出“Interrupt service routine n”提示,并包含中断向量安装、中断屏蔽字设置、EOI命令发送等细节。对于理解中断向量表设置、8259A初始化、软硬件中断编程技术也有直接帮助。报告包含实验目的、实验要求、程序设计思路、代码清单及结果分析,结构完整,可直接作为实验报告模板或上机调试参考。压缩包内共1个PDF文件,大小约1.03MB,已有131人学习下载。

1. 这份汇编实验报告值得照着敲一遍:从冒泡排序到8259A中断的完整路线

学汇编语言的人大多有个共同困境:教材把MOV、CMP、LOOP讲得明明白白,一到上机就卡在环境装不上、中断向量看不透、程序跑飞了不知道从哪开始查。这份《汇编语言与接口技术实验报告》恰好把这条路走通了——从MASM 6.11环境搭建、冒泡法排序这批8位数据,到8259A可编程中断控制器的软硬件中断,再到键盘扫描码读取和TSR驻留程序的替换与恢复,四个实验由浅入深,代码基本完整,照着敲一遍就能跑。它的价值不在被扫描的PDF页脚上,而在那一行需要反复推敲的CMP/JNA指令选择、一次8259A屏蔽字的设置、一套换掉INT 09H后又优雅恢复的思路。正在上微机原理课、准备实验考试,或者刚接触汇编语言想找个能实实在在跑起来的入门例子的读者,这份报告比单纯停留在概念层面的教程实用得多。

2. 冒泡法排序的两种打开方式:MASM 6.11环境与jna/jle的分野

2.1 先把MASM 6.11跑起来:DOSBox挂载与编译链接

实验报告第一个任务落在“安装MASM 6.11”,这一步卡住的人远比想象中多。MASM 6.11是16位实模式汇编工具,拿到今天64位Windows上直接运行,多半报错或者一闪而过。常见做法是装一个DOSBox模拟DOS环境,再把MASM工具目录挂载进去。我一般习惯把整个MASM文件夹放到固定路径,比如D:\masm611,里面至少要有ML.EXE、LINK.EXE和DEBUG.EXE三个文件。

mount c: /users/yourname/masm611 c: ml /c sort.asm link sort.obj

mount命令把DOSBox的虚拟C盘指向MASM目录;ml /c只编译不链接,生成sort.obj,link负责把目标文件链接成sort.exe。注意新版MASM的ml默认可能生成32位PE格式,实验里这套代码是16位段,如果link报格式错,改用ml /AT sort.asm这种简化汇编模式,或者直接在命令行加/coff参数调整。DOSBox本身不需要配置内存,默认值足够跑这些实验。

2.2 摸清数据:8个十六进制数在内存里的真实身份

实验给的这批数据是61H, 84H, 93H, 0C4H, 17H, 0FFH, 52H, 2AH,看起来只是8个字节,但“无符号”和“有符号”两种视角下它们的身份完全不同。同一个0FFH,无符号看是255,有符号看是-1,排序结果自然不一样。我先把它列出来。

字节十六进制无符号值有符号值
161H9797
284H132-124
393H147-109
40C4H196-60
517H2323
60FFH255-1
752H8282
82AH4242

无符号排序时,0FFH最大,最终排在最后;有符号排序时,0FFH是-1,84H、93H、0C4H这些带符号的负数全部要排到正数前面。这个对比是实验的核心考点,代码里只差一个条件跳转指令,结果就差出整个顺序。

2.3 无符号冒泡:jna的边界条件

报告给的冒泡代码是典型的双重循环:外层控制轮数,内层从首地址开始相邻比较,不符合升序就交换。完整程序如下。

.model small .data n dw 8 block db 61h,84h,93h,0c4h,17h,0ffh,52h,2ah .stack 60h .code .startup mov ax, @data mov ds, ax mov cx, n dec cx loop1: mov dx, cx ; dx保存外层剩余次数 mov bx, 0 ; bx做偏移指针 loop2: mov al, block[bx] cmp al, block[bx+1] jna continue ; 无符号不高于则跳 xchg al, block[bx+1] mov block[bx], al continue: add bx, 1 loop loop2 mov cx, dx loop loop1 .exit end

重点在jna continue这条指令。jna是“jump if not above”,看的是CF和ZF标志位,含义是“无符号不大于”,也就是当前元素<=下一个元素时不用交换;只有当前元素更大时才执行xchg和mov两步完成互换。这里有一个很多人第一次写会犯的错:把jna写成jnae或者反过来用ja,一旦搞反,排序就从升序变降序,或者出现部分有序的怪序列。外层mov dx, cx保存本轮剩余次数,内层loop loop2把cx减到0,再mov cx, dx恢复,实现每轮内循环次数递减,这是标准冒泡优化。

2.4 切到有符号:只有一行代码不同

第二段程序几乎一模一样,唯一的改动是把jna换成jle。jle是“jump if less or equal”,基于ZF、SF和OF组合判断,适用于带符号数比较。对0FFH这种字节,无符号时它比谁都大,有符号时它比谁都小,比较指令如果不区分这两种视角,结果必然翻车。

loop2: mov al, block[bx] cmp al, block[bx+1] jle continue ; 有符号的小于等于则跳过交换 xchg al, block[bx+1] mov block[bx], al continue: add bx, 1 loop loop2

有符号升序最终结果是84H, 93H, 0C4H, 0FFH, 17H, 2AH, 52H, 61H。可以从标志位上解释:CMP执行减法,无符号比较看CF借位,有符号比较看OF溢出和SF符号的异或结果。jna只看CF/ZF,jle则多看了SF和OF,这是两类比较指令最本质的区别,也是这份实验报告最值得背下来的一条结论。

2.5 DEBUG验证一下:排序结果和内存长什么样

报告里提到的DEBUG工具,很多人以为是老古董,实际在实验环境下它比任何IDE都直观。程序编译链接后,用DEBUG加载EXE,反汇编和查内存都靠几个单字母命令。

debug sort1.exe -u 0 20 -d 0 -g -d 0

u反汇编查看指令序列,先定位程序的入口地址;d直接查看数据段内容,排序前后各执行一次,对比数据区的字节顺序;g是运行到断点或结束。报告里记录入口在0017,那是.startup伪指令生成的启动代码在不同版本下的偏移,不一定每次都一样,以自己反汇编结果为准。DEBUG看内存时注意,block在数据段偏移0处,8个字节连续排在一起,排序结果一眼就能确认。

3. 中断向量与8259A:软件中断78H、硬件中断IRQ10到IRQ12的完整接线

3.1 中断向量表:软件中断怎么“注册”

实验2的软件中断部分,核心操作是“自定义一个中断类型码78H的中断服务程序”。中断类型码对应中断向量表,实模式下向量表在内存0地址开始,每个表项4字节,段地址+偏移地址。类型码78H的向量位置在78H*4 = 0x1E0处。往这个位置写入自己的服务程序入口,最正规的写法是用DOS功能调用。

mov ax, seg asc_bcd mov ds, ax mov dx, offset asc_bcd mov ax, 2578h int 21h

AH=25H是DOS的“设置中断向量”功能,AL=78H是中断类型码,DS:DX指向新服务程序入口。中断调用前要先保证DS指向asc_bcd所在段,所以第一行mov ax, seg asc_bcd取出段地址。完成这三行后,再执行int 78h就会跳进asc_bcd过程。这个套路在所有中断实验里通用,后面改键盘中断也是同一套机制。

3.2 ASCII转压缩BCD的算法拆解

服务程序本身做的是把ASCII数字字符串转成压缩BCD。所谓压缩BCD就是一个字节放两个十进制数,高4位一个、低4位一个。实验的ASCADDR里存了'0'到'9'十个字符,NUMBER=10,转换后应该是5个BCD字节。算法每次从字符串取两个字符放进AX,低字节是第一个字符、高字节是第二个字符。

asc_bcd proc mov ax, dseg mov ds, ax mov si, offset ascaddr mov di, offset bcdaddr mov ch, number shr ch, 1 mov cl, 4 cont: mov ax, [si] ; AX = 两个ASCII字符 and al, 0fh ; 保留低字符低4位 shl ah, cl ; 高字符低4位移到高4位 or al, ah ; 合并成压缩BCD mov [di], al add si, 2 inc di dec ch jnz cont iret asc_bcd endp

shr ch, 1把字符个数除以2,10个字符循环5次;shl ah, cl是关键的移位操作,CL=4,把高字节里的数字位推到高4位位置,再和低4位合并。例如AX中是'1''0',and al, 0fh得到00H,shl ah, 4把31H变10H,or完就是10H,正好是数字10的BCD编码。这个算法一次处理两个字符,效率不错,但有个隐含边界条件,留到避坑部分细说。

3.3 8259A的套路:OCW1写屏蔽字,主片从片分开

硬件中断部分涉及8259A,实验要求IRQ10到IRQ12作为外部中断源,对应中断类型号72H到74H。这里有个必须背下来的常识:8259A主片端口是20H/21H,从片端口是A0H/A1H。IRQ0到IRQ7在主片,IRQ8到IRQ15在从片,IRQ10到IRQ12具体是从片的IR2、IR3、IR4。

要让外部中断触发,先要在中断控制器里把对应的屏蔽位打开。OCW1就是中断屏蔽字寄存器,位为1表示屏蔽,位为0表示允许。从片端口A1H写入11100011B,意思是bit2、bit3、bit4清零,其他位保持屏蔽。

in al, 0a1h and al, 11100011b out 0a1h, al

in al, 0a1h读当前屏蔽字,and操作保留原来该开的位、只清这三位,比直接mov al, 11100011b更安全,因为不会覆盖BIOS已经设置好的其他位。主片这边还要确保级联IRQ2没被屏蔽,实验报告代码里有in al, 21h; and al, 11111101b; out 21h, al,就是把主片IRQ2的屏蔽位清零。漏掉这步,从片的中断请求到达不了CPU,按键不会有任何反应。

3.4 中断服务程序的收尾:EOI不能漏

中断服务程序执行完后,必须向8259A发送EOI(中断结束命令),否则8259A对应位一直处于服务状态,同级别中断再也不会触发。代码里明确的把EOI写到从片端口A0H:

mov al, 20h out 0a0h, al

如果还需要重新开放主片,可以在从片EOI之后向20H端口再发一次。很多人第一次调8259A实验,现象是“中断进去了第一次,第二次按键完全没反应”,十有八九就是EOI没发或者发错端口。这里还有一个小坑:实验要求按数字键1到3模拟外部中断源,实际代码在键盘读取后根据'1'、'2'、'3'分别执行int 72h、int 73h、int 74h,所以EOI也只是针对从片的中断服务,主片的EOI在模拟场景下可发可不发,但真实硬件接线环境里主片也必须发。

4. 键盘扫描码的“截胡”:从60H端口读码到INT 09H替换与TSR驻留

4.1 键盘数据的原始来源:60H端口和扫描码

实验3的标题是“键盘接口实验”,实际做的是把系统键盘中断INT 09H换成自己写的服务程序,直接从硬件端口读扫描码。键盘控制器8255的PA端口地址是60H,每按下一个键,60H端口就产生一个扫描码;松开时产生断码,断码通常是通码的0x80倍。比如按下A键扫描码是1EH,松开是9EH。

实验程序在kbint里第一步就是in al, 60h,然后立刻test al, 80h; jnz keyend,用最高位判断是不是断码。断码直接丢弃,只有通码才进入后面的扫描码转ASCII流程。这个判断如果漏掉,一次按键会被当成两次键事件处理,屏幕上可能打出重字。

4.2 XLAT换码表:扫描码到ASCII的映射

扫描码本身不是ASCII,转成ASCII最常用的手段是XLAT指令。XLAT的作用是AL = DS:BX+AL,把一个字节当表偏移,取表里对应的字节。实验代码里定义了一张scantab:

scantab db 0,0,'1234567890-=',08h,0 db 'QWERTYUIOP[]',0dh,0 db 'ASDFGHJKL;',0,0,0,0 db 'ZXCVBNM,./',0,0,0,20h db 13 dup(0) db '789-456+1230#'

表按键盘扫描码顺序排列,扫描码2对应数字键1,在表里第3个位置;扫描码16对应Q键;扫描码57对应空格。XLAT执行后,AL变成对应的ASCII字符。报告里特意安排了一段“输入小写字母显示大写”的效果,根源就在这张表:表里放的全是大写字母ASCII,不管Shift状态如何,按键直接输出大写。

kbint proc push ax push bx push ds push cs pop ds ; DS指向代码段 sti mov bx, offset scantab in al, 60h ; 读扫描码 test al, 80h jnz keyend ; 断码退出 ... xlat ; AL = DS:BX+AL ... keyend: mov al, 20h out 20h, al ; 发EOI pop ds pop bx pop ax iret kbint endp

push cs; pop ds把数据段寄存器临时指向代码段,这样scantab才能被XLAT访问。mov al, 20h; out 20h, al是发给主片8259A的EOI,键盘中断IRQ1在主片,这个必须发,不然整个键盘系统会冻结。

4.3 偷换INT 09H:取旧向量、装新向量、改8259屏蔽

替换INT 09H不是直接往向量表写地址,而是用DOS功能调用保存和设置。先AH=35H取出旧向量放到ES:BX,再AH=25H把新向量装进去。

mov ax, 3509h int 21h mov word ptr oldint, bx mov word ptr oldint+2, es mov dx, offset kbint mov ax, 2509h int 21h

oldint是个4字节变量,保存旧的段地址和偏移地址。这一步的目的很明确:程序退出时要把原中断恢复回来,或者按下ESC键要临时切回系统键盘处理。装完新向量后,还要改8259A的IMR寄存器,确保IRQ1没被屏蔽。正常系统里IRQ1本来就没屏蔽,但稳妥起见还是做一次:

in al, 21h and al, 11111101b out 21h, al

11111101B是只清bit1,其他位不动。这里如果直接用mov al, 11111101B覆盖,会把其他中断的屏蔽状态全部打乱。

4.4 TSR驻留与ESC切换:把这个程序留在内存里

实验程序用int 27h驻留退出,这是DOS时代标准的TSR做法。驻留的意思是程序结束但代码和数据都留在内存里,中断向量继续指向它。ESC键的切换逻辑是:自己的中断服务程序里判断扫描码是01H(ESC键)时翻转flag,flag=0时走jmp cs:oldint把控制权还给系统原来的INT 09H处理程序。

install: cli push cs pop ds mov ax, 3509h int 21h cmp bx, offset kbint jnz load int 20h ; 已安装则直接退出 load: mov word ptr oldint, bx mov word ptr oldint+2, es mov dx, offset kbint mov ax, 2509h int 21h ... mov dx, offset install int 27h ; TSR驻留退出

cmp bx, offset kbint; jnz load判断当前向量是不是已经指向自己的处理程序,这是防止重复安装的经典防护。mov dx, offset install; int 27h意思是驻留到install标签为止,中断服务程序本身在install之前,所以不会被释放。读oldint里的段值和偏移,用jmp跳过去执行,能保证两个中断服务程序之间安全切换。

4.5 替换中断的正确姿势:键盘控制器端口61H处理不能少

实验代码里有一段看起来无关的指令,是键盘处理绕不开的“握手”步骤:

push ax in al, 61h or al, 80h out 61h, al and al, 7fh out 61h, al pop ax

端口61H是8255的PB端口,bit7控制键盘的允许/禁止信号。or al, 80h把bit7拉高、and al, 7fh再拉低,产生一个负脉冲,通知键盘控制器“扫描码已被读取,可以发下一个”。很多从零开始写键盘中断的人,漏掉这段之后发现:按键第一次有效,第二次就无响应,或者键盘灯都乱了。原因就是键盘控制器没收到清除信号,一直认为上一帧数据还没取走。这个细节在教科书里往往只有一句“读取后需发送应答信号”,报告里这段代码是完整的,值得照抄。

5. 避坑:实验报告里那些疑似笔误和调试中常见的五个问题

5.1 有符号排序结果不对:jna和jle选错

现象:同一批数据,两段程序跑出来的结果完全一样,或者20H以下的负数全排到了最后面。

原因:jna和jle没区分。无符号排序用jna看的是CF和ZF,有符号排序必须用jle看的是SF、OF、ZF联合结果。0FFH无符号是255、有符号是-1,用错指令它在数组里的位置天差地别。

解决:确认实验要求是“有符号”还是“无符号”,有符号一律查jle/jge系列,无符号用jna/jb系列。如果不确定,用DEBUG在排序后d 0看内存,无符号结果里FFH一定在末尾,有符号结果里84H会出现在最前面。

5.2 压缩BCD转换漏掉最后一个字符

现象:源字符串长度是奇数,比如NUMBER=9,转换完发现第一个字符被吞掉或者最后一位丢失。

原因:shr ch, 1把9除以2得到4,循环4次只处理了8个字符,最后一位永远没机会进转换流程。报告里NUMBER是10,碰巧偶数,所以代码没暴露这个问题。常见的临时方案是循环结束后单独判断原长度低位,为1则补一个高4位为0的BCD字节。

解决:先把字符串长度复制一份做奇偶判断。奇数时最后一个字节单独处理,只取低4位存入,高4位补0;或者干脆把源字符串前面补一个'0',让长度变偶数,转换结果高位多一个0,再另做修正。我一般用第二种,代码改动最少。

5.3 硬件中断触发不了:8259A屏蔽字和EOI两个方向查

现象:程序运行后按数字键1、2、3,屏幕上没有任何“Interrupt service routine n is running...”输出。

原因:两种可能。一种是从片OCW1没写对,IRQ10对应从片bit2,屏蔽字11100011B写成了11101111B之类,把要用的位屏蔽了;另一种是服务程序里EOI只发主片out 20h, al,没发从片out 0a0h, al,从片状态一直没清,第二次中断进不来。

解决:先查out 0a1h的屏蔽字,再查中断服务程序末尾有没有out 0a0h, al。EOI一般先发从片,后发主片,两个都发最稳。还有一点被忽略的是主片IRQ2级联位,out 21h时and al, 11111101b,这步漏了从片请求根本到不了CPU。

5.4 TSR退出后系统键盘失灵

现象:程序驻留退出后,按ESC切回系统键盘,输入字母变成大写或者完全没反应。

原因:ESC切换逻辑走的是jmp cs:oldint,但如果oldint保存的向量本身已经被破坏,或者在恢复前程序又跑了一遍install导致重复安装,中断向量会自己指向自己形成死循环。

解决:安装前判断cmp bx, offset kbint,已经装过就直接退出;恢复时用mov dx, word ptr oldint; mov ax, 2509h重新写回向量表。ESC切换后不要立刻int 20h退出驻留,先观察键盘响应是否恢复,恢复了再退出。

5.5 键盘显示乱码:scantab表和实际按键错位

现象:按下A键显示的不是A,而是别的字符;按下数字键显示字母。

原因:scantab表项和扫描码没对齐。XLAT把扫描码直接当表偏移,表里第0项对应扫描码0、第1项对应ESC键。如果表开头少写了一个0占位,整个表往后错一位,所有输出都错位。报告里表开头是0,0,'1234567890-=',第一个0对应扫描码0,第二个0对应ESC键,从第三个字节开始才是数字键1。

解决:把表第一行对照键盘扫描码表一行行检查,特别是08h退格、0dh回车这类控制字符位置。调试时按顺序按一排键,比如依次按1、2、3、4,看输出是否依次是'1' '2' '3' '4',从哪个位置开始错,就重点查它前面那个表的项。

6. 让实验结果“看得见”:DEBUG单步、内存查看与移动笑脸的定时器改造

实验4的“移动笑脸”报告只有流程图没有代码,正好适合自己动手补全。最简单的实现是死循环里“显示、延迟、清除、改坐标”四步:用INT 10H的02H功能定位光标,09H功能在当前光标写字符,延迟用纯loop嵌套空转,之后再定位到原位置写空格清掉笑脸。这个流程能跑,但有个明显缺陷:循环期间按键盘没反应,整个程序占死CPU。常见的改造方案是挂到INT 1CH上——这是ROM BIOS每秒触发约18.2次的定时器中断,把笑脸的坐标更新逻辑放到中断服务程序里,主程序就可以空出来做别的。

clock proc far push ax push bx push dx push ds push cs pop ds mov dl, col mov dh, row mov ah, 02h int 10h ; 定位到当前笑脸坐标 mov al, 01h mov bh, 0 mov cx, 1 mov ah, 09h int 10h ; 显示笑脸 inc col cmp col, 79 jb done mov col, 0 done: pop ds pop dx pop bx pop ax iret clock endp

INT 1CH的中断服务程序必须用iret结尾,不能带EOI——它本身挂靠在INT 08H后面,EOI由INT 08H处理,这里写EOI反而会重复发。坐标变量col每次中断加1,到屏幕边缘回0,实现从左往右循环移动。如果想上下移动,再维护一个row变量控制方向。把这套代码编译后驻留,再配合实验3的键盘TSR一起运行,就能看到键盘切中断的同时笑脸还在后台跑——两个实验变成一个小作品。

从那以后我每次做汇编实验都强制自己走一遍这三步:先看数据段的内存布局,再单步走一遍核心循环,最后把实验结果用DEBUG dump出来留档。遇到中断类实验,先确认向量装没装对、EOI发没发对,再谈功能逻辑,这套顺序帮我少踩了很多坑。这份报告里四个实验都值得亲手跑一遍,尤其是冒泡排序里jna换jle那一行和8259A的屏蔽字设置,自己敲过一遍比背一百条考点都牢。希望这份整理能帮你把实验做得更顺,下载下来照着复现一遍,收获会远超预期。

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

返回列表