汇编语言与接口设计实验 赶作业,顺便留点开源复习资料吧:)
实验 1 查看 CPU 和内存,用机器指令和汇编指令编程 1、相关知识 主要是关于debug使用,要了解的包括8086 CPU 的寄存器、内存地址表示方法,以及常见Debug调试指令。
寄存器 8086 常用寄存器可以分成几类:
通用寄存器:AX、BX、CX、DX。它们都可以作为 16 位寄存器使用,也可以拆成高 8 位和低 8 位,如 AX=AH+AL。
AX:累加器,很多算术运算、乘除法、I/O 指令默认使用它。例如 add ax,ax 就是把 AX 翻倍。
BX:基址寄存器,常用于内存寻址,例如 [bx] 表示访问 DS:BX。
CX:计数寄存器,loop 指令默认使用 CX 作为循环次数。
DX:数据寄存器,常配合 AX 表示 32 位数据 DX:AX,也常用于端口号。
段寄存器:CS、DS、SS、ES。CS 是代码段,DS 是数据段,SS 是栈段,ES 是附加段,常用于数据搬运和显存操作。
指针和变址寄存器:SP、BP、SI、DI。SP 指向栈顶,BP 常用于访问栈中参数,SI 常作源地址,DI 常作目标地址。
指令指针:IP 保存下一条将要执行的指令偏移地址,它不能像普通寄存器一样直接 mov ip,...,通常通过跳转、调用、中断等方式改变。
【CS:IP 指向下一条要执行的指令;DS 通常指向数据段;SS:SP 指向栈顶。理解这三个组合非常重要:CS:IP 管“执行哪里”,DS:偏移 管“取哪里的数据”,SS:SP 管“栈顶在哪里”】
标志寄存器:FLAGS 保存运算状态和控制状态。常见标志有 CF 进位、ZF 零、SF 符号、OF 溢出、PF 奇偶、DF 方向、IF 中断允许、TF 单步。他们的位置如下:
位号
15
14
13
12
11
10
9
8
7
6
5
4
3
2
1
0
标志位
OF
DF
IF
TF
SF
ZF
AF
PF
CF
内存地址表示方法 通过段地址:偏移地址 访问内存
物理地址计算公式为: $$ 物理地址 = 段地址 * 16 + 偏移地址 $$
debug常用指令 包括:
R:查看或修改寄存器。
D:查看内存。
E:修改内存。
A:输入汇编指令。
U:反汇编。
T:单步执行。
2、实验指令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 debug -r -a 100 mov ax,1 add ax,ax add ax,ax add ax,ax add ax,ax add ax,ax add ax,ax add ax,ax add ax,ax -r cs 1000 -r ip 0100 -t -d f000:fff0 -e b800:0000 41 1e 42 2e 43 4e 44 5e
3、实验结果 add ax,ax 连续执行后:
1 0001 -> 0002 -> 0004 -> 0008 -> 0010 -> 0020 -> 0040 -> 0080 -> 0100
最终 AX=0100H。BIOS 日期处读到 01/01/92。向 B800:0000 写入后,显存内容为:
实验 2 用机器指令和汇编指令编程 1、相关知识 本实验涉及存储方式、存取方法
8086数据存储方式 小端存储
1234H:
对应读取也要这么理解
SS:SP工作原理(栈) SS:SP 指向当前栈顶,8086 的栈向低地址增长:
push ax:先执行 SP=SP-2,再把 AX 写入 SS:SP。
pop ax:先从 SS:SP 读出一个字给 AX,再执行 SP=SP+2。
(每次push、pop操作的是两个字节,一个字)
注意,执行 mov ss,ax 后,CPU 会临时不响应中断,使紧随其后的 mov sp,... 能连续执行。原因是 SS:SP 必须共同指向正确栈顶,如果只改了 SS 还没改 SP 就发生中断,中断过程要压栈,可能把数据压到错误位置
2、实验内容 (1)设置栈 把 SS 设置为 2200H,把 SP 设置为 0100H,让 SS:SP=2200:0100 成为新的栈顶。(任意选一块普通可写内存)
(2)访问 BIOS 高地址区域 把 DS 设置为 FFFFH,用 [0]、[2]、[4]、[6] 读取连续的字数据,观察小端存储如何影响寄存器结果。(目标寄存器16位,所以一个单位要包括两个字节)
(3)用栈交换寄存器 利用栈的后进先出性质,可以实现寄存器互换数据,belike:
1 2 3 4 push ax push bx pop ax pop bx
3、实验代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 mov ax,2200h mov ss,ax mov sp,0100h mov ax,0ffffh mov ds,ax mov ax,[0] add ax,[2] mov bx,[4] add bx,[6] push ax push bx pop ax pop bx
4、实验结果 关键结果:
步骤
指令
变化说明
结果
1
mov ss,ax
设置栈段
SS=2200H
2
mov sp,0100h
设置栈顶偏移
SS:SP=2200:0100
3
mov ds,0ffffh
设置数据段
后续 [偏移] 默认访问 FFFF:偏移
4
mov ax,[0]
读取 EA 5B,小端组成 5BEA
AX=5BEAH
5
add ax,[2]
[2] 为 00E0H
AX=5CCAH
6
mov bx,[4]
读取 F0 30,小端组成 30F0
BX=30F0H
7
add bx,[6]
[6] 为 2F31H
BX=6021H
栈交换过程:
步骤
指令
栈顶变化
数据变化
初始
-
SP=0100H
AX=5CCAH,BX=6021H
1
push ax
SP=00FEH
2200:00FE 保存 5CCAH
2
push bx
SP=00FCH
2200:00FC 保存 6021H
3
pop ax
SP=00FEH
栈顶 6021H 弹入 AX,所以 AX=6021H
4
pop bx
SP=0100H
下一层 5CCAH 弹入 BX,所以 BX=5CCAH
最终:
1 2 3 AX = 6021H BX = 5CCAH SP = 0100H
实验 3 编程、编译、连接、跟踪 1、相关知识 这一节主要是走一遍标准汇编程序开发流程:写 .ASM 源文件,用 MASM 汇编成 .OBJ,用 LINK 连接成 .EXE,再用 Debug 跟踪。
一个 MASM 程序需要包含:
segment / ends:定义一个段,例如代码段、数据段、栈段。
assume cs:codesg:告诉汇编器哪个段寄存器和哪个段名对应。注意它是伪指令,不会真的修改 CPU 寄存器。
标号 start::表示程序入口处的一段地址。
end start:告诉连接器程序入口是 start。
mov ax,4c00h 和 int 21h:调用 DOS 功能返回操作系统。
DOS 加载 .EXE 时,会在程序前面建立 PSP,也就是程序段前缀。PSP 中保存了命令行、文件控制块、返回入口等信息。PSP 首部通常是 CD 20,表示一条 int 20h 指令。Debug 加载程序后看到的 DS、ES 往往先指向 PSP,而不是直接指向你的数据段。
调试 .EXE 时要关注 CS:IP 是否指向入口,SS:SP 是否指向正确栈顶,以及 DS 是否已经被程序设置到数据段。
2、实验内容 (1)编写最小 .ASM程序 程序只定义一个代码段,入口为 start,最后用 DOS 中断返回。
(2)在程序中设置栈并执行 push/pop 代码设置 SS、SP 后执行几次入栈和出栈。
(3)用 MASM、LINK、Debug 验证 先汇编生成 .OBJ,再连接生成 .EXE,最后用 Debug 加载跟踪。
3、实验代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 assume cs:codesg codesg segment start: mov ax,2000h mov ss,ax mov sp,0 add sp,10 pop ax pop bx push ax push bx pop ax pop bx mov ax,4c00h int 21h codesg ends end start
4、实验结果 程序成功汇编、连接、运行。Debug 加载后可看到 PSP 首部:
CD 20 是机器码,表示 int 20h。它出现在 PSP 开头,可以看出 DS、ES 初始指向 PSP,而不是源程序里定义的代码段或数据段。Debug 加载 .EXE 后常见现象是:
1 2 3 4 DS = PSP 段地址 ES = PSP 段地址 CS = 代码段地址 IP = 程序入口偏移
所以如果程序中要访问自己的数据段,不能假设 DS 自动正确,必须显式执行:
这个实验程序能跟踪到 int 21h 并正常返回 DOS,可以看出入口设置正确,栈也没有被破坏。
实验 4 [bx] 和 loop 的使用 本实验涉及间接寻址和循环控制
间接寻址-[bx] 寻址方式就是“CPU 到哪里找操作数”。如果指令里直接写出地址,比如 mov al,[0],这种更接近直接寻址;如果指令里不直接写死地址,而是把地址放在寄存器里,比如 mov al,[bx],这就是寄存器间接寻址。
方括号 [] 表示访问内存。BX 和 [BX] 不是一回事:
1 2 BX 表示寄存器 BX 里的数值 [BX] 表示以 BX 的值作为偏移地址,到内存中取数据
8086 访问内存时必须有“段地址 + 偏移地址”。[bx] 只给出了偏移地址,默认段寄存器是 DS,所以:
1 2 [bx] 实际访问 DS:BX 物理地址 = DS * 16 + BX
例如这个实验先设置:
1 2 3 mov ax,0020h mov ds,ax mov bx,0
那么 mov [bx],bl 第一次访问的是 0020:0000。执行 inc bx 后,BX=0001H,下一次访问的就是 0020:0001。这样 BX 就像数组下标一样,控制当前访问第几个内存单元。
mov [bx],bl 中,源操作数 BL 是 8 位寄存器,所以这条指令每次只写 1 个字节。如果写成 mov [bx],bx,源操作数是 16 位寄存器,就会写入一个字,也就是连续 2 个字节。内存操作的大小一定要看清楚,否则写入范围会和预期不一样。
在 8086 中,能直接放进 [] 里做偏移寻址的寄存器主要是 BX、BP、SI、DI。其中 BX、SI、DI 默认配合 DS,而 BP 默认配合 SS。这个区别在访问普通数据和访问栈中数据时很重要。
这个实验选择 BX,是因为目标是一段连续内存:BX 负责指出当前地址,BL 又刚好是 BX 的低 8 位。于是随着 BX 从 0000H 增加到 003FH,BL 也从 00H 增加到 3FH,写入内存的内容自然就是 00H~3FH。
循环控制-loop指令 loop 标号 是 8086 中非常常用的循环指令,它隐含使用 CX:
1 2 3 4 5 执行 loop 时,先 CX =CX -1 如果 CX 不等于 0 ,就跳转到标号 如果 CX 等于 0 ,就顺序执行下一条指令
所以使用 loop 前必须先正确设置 CX。如果循环内部还要使用 CX,就必须先保存它,否则外层循环会被破坏。
2、实验内容 (1)建立目标数据段 把 DS 设置为 0020H,让后续 [bx] 都访问 0020:BX。(要求向 0020:0000 一带写入数据)
(2)用BX作为移动的偏移地址 从 BX=0 开始,每次写完后 inc bx。(目标内存是连续区域,BX 正好适合做下标)
(3)用CX和loop控制循环 设置 CX=64,每次循环写一个字节。(要写 0000H~003FH 共 64 个字节)
3、实验代码 T41.ASM:把 DS 指向 0020H,用 BX 做偏移地址,用 AL 保存要写入的字节,循环 64 次写入连续内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 assume cs:code code segment start: mov ax,0020h mov ds,ax mov bx,0 mov al,0 mov cx,64 s: mov [bx],al inc bx inc al loop s mov ax,4c00h int 21h code ends end start
T42.ASM:用 BX 同时作为地址来源,BL 作为写入数据,突出 [bx] 间接寻址和 loop 的配合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 assume cs:code code segment start: mov ax,0020h mov ds,ax mov bx,0 mov cx,64 s: mov [bx],bl inc bx loop s mov ax,4c00h int 21h code ends end start
T43.ASM:让 DS 指向代码段,ES 指向 0020H,把程序开头的机器码逐字节复制到目标内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 assume cs:code code segment start: mov ax,cs mov ds,ax mov ax,0020h mov es,ax mov bx,0 mov cx,17h s: mov al,[bx] mov es:[bx],al inc bx loop s mov ax,4c00h int 21h code ends end start
4、实验结果 0020:0000~003F 写入结果为:
1 2 3 4 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F
结果正好是 0 到 63,可以看出循环执行了 64 次。最后一次写入时 BX=003FH、BL=3FH,写入后 BX 变成 0040H,CX 变成 0,loop 不再跳转。
结果中出现类似:
1 8C C8 8E D8 B8 20 00 ...
这些不是随机数据,而是程序指令的机器码。可以看出程序确实把自己的代码字节复制到了 0020:0000。这能帮助理解“代码也是内存中的数据”。
实验 5 编写、调试具有多个段的程序 1、相关知识 代码放代码段,变量放数据段,临时保存和调用返回地址放栈段。
assume cs:code, ds:data, ss:stack 只是告诉汇编器段寄存器和段名的对应关系,并不会真正修改 CPU 的段寄存器。真正生效必须使用机器指令:
也就是说,assume 解决的是“汇编器如何理解地址”,mov ds,ax 解决的是“CPU 实际访问哪个段”。二者不能混淆。
程序入口由 end start 指定。start 是代码段中的标号,连接器会把这个入口写入 .EXE 文件头。程序加载后,DOS 根据入口设置 CS:IP,CPU 才会从正确位置执行。如果只写 end,连接器可能默认把程序开头当入口,若开头是数据段,CPU 就会把数据当指令执行。
段在 .EXE 中装入时通常按 16 字节对齐。若某段实际占 N 字节,装入后占用空间通常是 16 * ceil(N/16) 字节。这就是为什么观察段地址关系时,段之间常常相差整数个 16 字节段落。
多段数据搬运常用 DS 和 ES 配合。DS 可以指向源数据段,ES 可以指向目标数据段,通过段前缀可以把数据写入另一个段。
2、实验内容 (1)观察多段程序的装入关系 编译、连接多个段顺序不同的程序,用 Debug 查看 CS、DS、SS 和各段内容。(多段程序最容易混淆的是“源程序里的段名”和“运行时段寄存器的值”)
(2)完成 a+b -> c 先把 a 段复制到 cdata 段,再把 b 段逐字节加到 cdata 对应位置。(源数据在两个不同段中,目标数据在第三个段中,正好练习多段访问)
(3)用栈实现逆序存储 把 a 段中的字数据依次 push 入栈,再依次 pop 到 b 段。(栈后进先出,天然会把顺序反过来)
3、实验代码 Q5.ASM:定义 a、b、cdata 三个数据段,先复制 a 到 cdata,再把 b 逐字节加到 cdata。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 assume cs:code a segment db 1,2,3,4,5,6,7,8 a ends b segment db 1,2,3,4,5,6,7,8 b ends cdata segment db 0,0,0,0,0,0,0,0 cdata ends code segment start: mov ax,a mov ds,ax mov ax,cdata mov es,ax mov bx,0 mov cx,8 copy_a: mov al,[bx] mov es:[bx],al inc bx loop copy_a mov ax,b mov ds,ax mov bx,0 mov cx,8 add_b: mov al,[bx] add es:[bx],al inc bx loop add_b mov ax,4c00h int 21h code ends end start
Q6.ASM:把 a 段中的字数据依次压入以 b 段为栈空间的栈中,用来观察栈和多段装入关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 assume cs:code a segment dw 1,2,3,4,5,6,7,8,9,0ah,0bh,0ch,0dh,0eh,0fh,0ffh a ends b segment dw 0,0,0,0,0,0,0,0 b ends code segment start: mov ax,a mov ds,ax mov ax,b mov ss,ax mov sp,10h mov bx,0 mov cx,8 s: push word ptr [bx] add bx,2 loop s mov ax,4c00h int 21h code ends end start
4、实验结果 c 段结果:
原因是:
1 2 3 a = 01 02 03 04 05 06 07 08 b = 01 02 03 04 05 06 07 08 c = 02 04 06 08 0A 0C 0E 10
通过 push 逆序存储后,b 段结果:
1 08 00 07 00 06 00 05 00 04 00 03 00 02 00 01 00
这个结果每两个字节一组看更清楚:
1 0008 0007 0006 0005 0004 0003 0002 0001
因为 push 压入的是字数据,a 段中的 1,2,3...8 被依次压栈后,最后压入的 8 在栈顶,所以弹出并写入 b 段时顺序变成 8,7,6...1。内存中显示为 08 00 而不是 00 08,仍然是小端存储的结果。
实验 6 实践课程中的程序 1、相关知识 字符串和固定长度记录 这个实验集中练习第 7 章的字符串处理、灵活寻址方式和嵌套循环。汇编程序没有高级语言里的数组、字符串、结构体语法,所谓
ASCII 大小写转换 ASCII 编码是这个实验的基础。英文字母的大小写编码有规律:小写字母比对应大写字母大 20H。例如:
所以小写转大写可以减 20H,也可以用 and al,11011111b 清除第 5 位。但这种方法只适合确定目标字符是字母的情况,严格程序还应先判断范围。
寻址方式和嵌套循环 包括:
ASCII 大小写转换:小写转大写可清除第 5 位,即 and al,11011111b。
[bx+idata] 适合访问结构固定的数据。
SI、DI 常用于字符串源地址和目标地址。
嵌套循环中必须保护外层 CX,常见方法是 push cx / pop cx。
手工访问结构字段 处理固定长度记录时,可以让一个寄存器保存
2、实验内容 (1)明确字符串记录格式 每行菜单字符串占固定长度,单词部分从固定偏移开始。(记录长度固定后,可以用 BX 表示当前行首地址)
(2)外层循环处理行,内层循环处理字符 外层循环 4 次,每次处理一行;内层循环 4 次,每次改一个字母。(共有 4 行,所以外层 CX=4)
(3)用位运算改大写 读取字符后执行 and al,11011111b,再写回原位置。(ASCII 中小写字母比大写字母多 20H)
3、实验代码 这段代码用于:处理固定长度字符串记录,把每行单词前 4 个小写字母用位运算转换成大写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 assume cs:codesg,ss:stacksg,ds:datasg stacksg segment dw 0,0,0,0,0,0,0,0 stacksg ends datasg segment db '1. display ' db '2. brows ' db '3. replace ' db '4. modify ' datasg ends codesg segment start: mov ax,stacksg mov ss,ax mov sp,16 mov ax,datasg mov ds,ax mov bx,0 mov cx,4 row_loop: push cx mov si,0 mov cx,4 col_loop: mov al,[bx+3+si] and al,11011111b mov [bx+3+si],al inc si loop col_loop add bx,16 pop cx loop row_loop mov ax,4c00h int 21h codesg ends end start
4、实验结果 4 行字符串变为:
1 2 3 4 1. DISPlay 2. BROWs 3. REPLace 4. MODIfy
每个单词只有前 4 个字母被改成大写,是因为内层 CX=4,只循环 4 次。后面的字母保持原样,可以看出 [bx+3+si] 的定位正确,没有改到编号、点号、空格或其他字段。
实验 7 寻址方式在结构化数据访问中的应用 1、相关知识 结构化数据 结构化数据访问是前面寻址知识的综合应用。汇编没有真正的
原始数据和目标表 原始数据分别存放:年份为字符串,收入为 dd 双字,雇员数为 dw 字。目标表每行固定 16 字节,通过固定偏移写入各字段。
双字数据和除法 关键点:
dd 数据需要用 DX:AX 处理。
div word ptr [...] 可进行双字除以字,商在 AX,余数在 DX。
固定记录大小适合用 BX 做行偏移。
db、dw、dd db、dw、dd 分别定义字节、字、双字数据:
DX:AX 和下标推进 因为收入可能大于 65535,所以不能只用一个 16 位寄存器保存,需要用双字。8086 处理双字除以字时,被除数放在 DX:AX 中。
这个实验里 SI 每次加 4,是因为年份和收入都按 4 字节推进;DI 每次加 2,是因为雇员数是字数据;BX 每次加 16,是因为目标表每条记录占 16 字节。
2、实验内容 (1)设计目标表记录格式 每条记录固定 16 字节,分别存放年份、收入、雇员数和人均收入。(固定长度记录便于用 BX 按行推进)
(2)设置源段和目标段 让 DS 指向原始数据段,让 ES 指向目标 table 段。(原始数据和目标表不在同一段)
(3)按 21 条记录循环生成表 循环 21 次,每次复制年份、收入、雇员数,再计算人均收入。(原始数据包含 1975 到 1995 年)
(4)用除法计算人均收入 把收入放入 DX:AX,用雇员数作为除数,执行 div。(收入是 dd 双字,可能超过 16 位,需要用 DX:AX 表示)
3、实验代码 这段代码用于:把年份、收入、雇员数整理成 21 条固定长度记录,并计算每年人均收入。 完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 assume cs:codesg year_base equ 0 sum_base equ 84 ne_base equ 168 record_size equ 16 sum_off equ 5 ne_off equ 10 avg_off equ 13 data segment db '1975','1976','1977','1978','1979' db '1980','1981','1982','1983','1984' db '1985','1986','1987','1988','1989' db '1990','1991','1992','1993','1994','1995' dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417 dd 197514,345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000 dw 3,7,9,13,28,38,130,220,476,778,1001 dw 1442,2258,2793,4037,5635,8226,11542,14430,15257,17800 data ends table segment db 21 dup ('year summ ne ?? ') table ends codesg segment start: mov ax,data mov ds,ax mov ax,table mov es,ax mov bx,0 mov si,0 mov di,0 mov cx,21 row_loop: mov ax,[year_base+si] mov es:[bx],ax mov ax,[year_base+si+2] mov es:[bx+2],ax mov ax,[sum_base+si] mov dx,[sum_base+si+2] mov es:[bx+sum_off],ax mov es:[bx+sum_off+2],dx mov ax,[ne_base+di] mov es:[bx+ne_off],ax mov ax,[sum_base+si] mov dx,[sum_base+si+2] div word ptr [ne_base+di] mov es:[bx+avg_off],ax add si,4 add di,2 add bx,record_size loop row_loop mov ax,4c00h int 21h codesg ends end start
4、实验结果 table 段生成 21 条记录。前几条为:
1 2 3 4 5 1975 16 3 5 1976 22 7 3 1977 382 9 42 ... 1995 5937000 17800 333
以 1975 年为例,原始数据是收入 16、雇员数 3,所以人均收入为:
表中保存的是整数商 5。8086 的 div 指令会把商放入 AX,余数放入 DX,这个实验只把商写入人均收入字段。
实验 8 分析一个奇怪的程序 1、相关知识 自修改代码 自修改代码的基础是:代码段在内存中也是字节序列,程序可以通过代码段地址读写自己的机器码。平时我们把代码和数据分开理解,是为了方便编程;但从内存角度看,它们都只是二进制数据。
nop nop 是空操作指令,占 1 字节,执行后除了 IP 前进外不做实际工作。连续两个 nop 正好占 2 字节。
jmp short jmp short 是短转移指令,通常占 2 字节:第 1 字节是操作码,第 2 字节是相对位移。程序把 s2 处的 jmp short s1 两个字节复制到 s 处,就能覆盖原来的两个 nop。
关键标号 理解这个实验时,要关注三个地址:
s:第一次执行时是两个 nop,第二次执行时已经被改成跳转。
s2:保存一条现成的 jmp short s1 机器码。
s1:程序最终跳过去并返回的位置。
2、实验内容 (1)预留可被覆盖的位置 在标号 s 处放两条 nop。(两条 nop 正好占 2 字节)
(2)从代码段复制跳转指令 让 SI 指向 s2,DI 指向 s,把 cs:[si] 的一个字复制到 cs:[di]。(s2 处已经放着一条现成的 jmp short s1)
(3)跳回 s 验证自修改结果 第一次到 s 时执行的是 nop;复制完成后通过 jmp short s 回到 s,第二次到 s 时执行的已经是 jmp short s1。(必须第二次回到 s,才能验证前面覆盖 nop 是否生效)
3、实验代码 这段代码用于:把代码段中的跳转指令机器码复制到前面的 nop 位置,观察自修改代码的执行效果。 完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 assume cs:codesg codesg segment mov ax,4c00h int 21h start: mov ax,0 s: nop nop mov di,offset s mov si,offset s2 mov ax,cs:[si] mov cs:[di],ax s0: jmp short s s1: mov ax,0 int 21h mov ax,0 s2: jmp short s1 nop codesg ends end start
4、实验结果 程序可以正确返回 DOS。原因是第二次到达 s 时已经执行被复制过来的 jmp short s1。
这个实验的结果可以看出两件事:
CPU 不会记住某个地址“原来是什么指令”,它每次都是从 CS:IP 指向的内存重新取机器码执行。
只要代码段可写,程序就能修改后续将要执行的指令。这种技巧叫自修改代码,实际工程中应谨慎使用,但它非常适合理解“指令也是内存数据”。
实验 9 根据材料编程 1、相关知识 文本显存 直接操作文本显存是 DOS 汇编中常见的屏幕输出方法。DOS 文本模式下,彩色显示缓冲区通常从 B800:0000 开始。屏幕每行 80 个字符,每个字符占 2 字节,所以一行占 160 字节。
字符和属性 每个显示字符的两个字节含义如下:
例如:
表示显示字符 'A',属性为 1EH。
显存偏移 某行某列的显存偏移为:
1 offset = 行号 * 160 + 列号 * 2
颜色属性 颜色属性字节中,低 4 位是前景色,高 4 位是背景色。例如 02H 表示黑底绿色字,24H 表示绿底红字,71H 表示白底蓝字。写显存时必须字符和属性成对写入,否则屏幕内容或颜色会错位。
2、实验内容 (1)准备字符串和属性表 定义 welcome to masm!,再定义三个属性字节 02H、24H、71H。(字符串内容三行相同,单独定义一次即可)
(2)计算显存位置并逐字符写入 把 ES 设置为 B800H,用 DI 指向屏幕中间位置,循环写入字符和属性。(文本显存每个字符占 2 字节)
(3)换行显示三次 每显示完一行,DI 增加 0A0H,移动到下一行相同列。(0A0H 等于十进制 160)
3、实验代码 这段代码用于:把 welcome to masm! 按三种颜色写到文本显存的屏幕中间位置。 完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 assume cs:code, ds:code code segment msg db 'welcome to masm!' msglen equ $-msg attrs db 02h,24h,71h start: push cs pop ds mov ax,0b800h mov es,ax mov bx,offset attrs mov di,0720h mov dx,3 row_loop: push dx push di mov si,offset msg mov ah,[bx] mov cx,msglen char_loop: lodsb mov es:[di],al mov es:[di+1],ah add di,2 loop char_loop pop di add di,0a0h inc bx pop dx dec dx jnz row_loop push ds mov ax,0b800h mov ds,ax xor ax,ax mov es,ax mov si,0720h mov di,0220h mov cx,16 rep movsw mov si,07c0h mov di,0240h mov cx,16 rep movsw mov si,0860h mov di,0260h mov cx,16 rep movsw pop ds mov ax,4c00h int 21h code ends end start
4、实验结果 三行属性分别为:
1 2 3 绿色字符:02H 绿底红字:24H 白底蓝字:71H
显存中可见字符和属性交替出现,例如第一行:
1 77 02 65 02 6C 02 63 02 ... 21 02
这里 77H 是字符 'w' 的 ASCII 码,后面的 02H 是绿色属性;65H 是 'e',后面的 02H 仍然是属性。显存结果按“字符、属性、字符、属性”交替出现,可以看出写入方式正确。
实验 10 编写子程序 1、相关知识 call 和 ret 子程序相当于汇编中的“函数”,用 call 调用,用 ret 返回。执行 call 时,CPU 会把返回地址压入栈,再跳到子程序;执行 ret 时,CPU 会从栈中弹出返回地址,回到调用点之后继续执行。所以,子程序是否能正确返回,依赖 SS:SP 指向正确的栈。
子程序调用约定 编写通用子程序时必须约定清楚三件事:
入口参数:调用者通过哪些寄存器或内存传入数据。
出口结果:子程序把结果放在哪里。
现场保护:子程序会破坏哪些寄存器,哪些需要 push 保存并在返回前 pop 恢复。
参数传递方式 子程序的参数可以通过寄存器传递,也可以通过内存或栈传递。寄存器传参速度快、代码短,适合参数数量较少的情况。内存或栈传参更灵活,适合较复杂的调用约定。
0 结尾字符串 0 结尾字符串是一种常见字符串表示方法。字符串内容连续存放,最后用字节 00H 表示结束。处理这类字符串时,循环每次取一个字符,遇到 0 就停止。
divdw 8086 的 div 指令有固定限制:如果除数是 16 位,默认被除数是 DX:AX,商必须能放入 AX。如果商超过 FFFFH,就会触发除法溢出。所以,处理较大的 32 位数除以 16 位数时,需要设计更安全的分步除法。
dtoc 二进制整数转换为十进制字符串的常用方法是反复除以 10。每次除法的余数就是当前最低位数字。因为先得到个位,再得到十位、百位,所以通常要把余数暂存起来,最后逆序输出。
2、实验内容 (1)show_str:显示字符串 入口参数为 DH=行号、DL=列号、CL=颜色、DS:SI=字符串首地址。(行号、列号、颜色都只需 1 字节)
(2)divdw:解决双字除以字 入口参数为 DX:AX=被除数、CX=除数。(8086 约定 DX:AX 表示 32 位被除数)
(3)dtoc:数字转十进制字符串 入口参数为 AX=待转换数、DS:SI=输出缓冲区。(十进制显示必须把二进制数转换成 ASCII 字符)
3、实验代码 文件 SHOWSTR.ASM:实现 show_str 子程序,通过行号、列号、颜色和 DS:SI 字符串地址,把 0 结尾字符串写入显存。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 assume cs:code, ds:code code segment msg db 'Welcome to masm!',0 start: push cs pop ds mov dh,8 mov dl,3 mov cl,2 mov si,offset msg call show_str push ds mov ax,0b800h mov ds,ax xor ax,ax mov es,ax mov si,0506h mov di,0280h mov cx,16 rep movsw pop ds mov ax,4c00h int 21h show_str: push ax push bx push cx push dx push si push di push es mov ax,0b800h mov es,ax xor ax,ax mov al,dh mov bl,160 mul bl mov di,ax xor ax,ax mov al,dl shl ax,1 add di,ax show_loop: mov al,[si] or al,al jz show_done mov es:[di],al mov es:[di+1],cl inc si add di,2 jmp short show_loop show_done: pop es pop di pop si pop dx pop cx pop bx pop ax ret code ends end start
文件 DIVDW.ASM:实现并测试双字除以字的 divdw 子程序,返回商和余数,并把验证结果写入低地址内存。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 assume cs:code, ds:code code segment quo_lo dw 0 quo_hi dw 0 remv dw 0 start: push cs pop ds mov ax,4240h mov dx,000fh mov cx,000ah call divdw mov quo_lo,ax mov quo_hi,dx mov remv,cx mov bx,ax mov si,dx mov di,cx xor ax,ax mov es,ax mov es:[0200h],bx mov es:[0202h],si mov es:[0204h],di mov ax,4c00h int 21h divdw: push bx mov bx,ax mov ax,dx xor dx,dx div cx push ax mov ax,bx div cx mov cx,dx pop dx pop bx ret code ends end start
文件 DTOC.ASM:实现并测试 dtoc,把二进制整数反复除以 10,转换成十进制 ASCII 字符串。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 assume cs:code, ds:code code segment buf db 10 dup (0) start: push cs pop ds mov ax,12666 mov si,offset buf call dtoc mov si,offset buf xor ax,ax mov es,ax mov di,0210h mov cx,10 copy_buf: mov al,[si] mov es:[di],al inc si inc di loop copy_buf mov ax,4c00h int 21h dtoc: push ax push bx push cx push dx push si mov bx,0 mov cx,10 dtoc_div: xor dx,dx div cx push dx inc bx or ax,ax jnz dtoc_div dtoc_out: pop dx add dl,30h mov [si],dl inc si dec bx jnz dtoc_out mov byte ptr [si],0 pop si pop dx pop cx pop bx pop ax ret code ends end start
4、实验结果 show_str 成功显示 Welcome to masm!。divdw 对 1000000 / 10 得到商 000186A0H,余数 0。dtoc 将 12666 转换为:
对应字符串 12666。
实验 11 编写子程序 1、相关知识 cmp 和条件判断 这个实验重点是标志寄存器、比较指令和条件转移。cmp a,b 的本质是执行一次不保存结果的减法 a-b,它不改变操作数,但会影响标志寄存器。后面的条件转移指令根据这些标志决定是否跳转。
常见标志 常见标志含义:
ZF:零标志。比较结果为 0 时置 1,常用于判断相等。
CF:进位/借位标志。无符号数比较时,若前者小于后者,通常会置 1。
SF:符号标志。结果最高位为 1 时置 1。
OF:溢出标志。有符号运算超出范围时置 1。
字符范围判断 判断字符是否为小写字母,可以用两次比较:
1 2 3 4 cmp al,'a' jb not_lower cmp al,'z' ja not_lower
jb 和 ja 是无符号条件转移,适合 ASCII 编码这种按数值大小排列的字符判断。如果字符在 'a' 到 'z' 范围内,减去 20H 就能转为大写。
0 结尾字符串 这个实验的字符串以 0 结尾,这种字符串也叫 0 结尾字符串。循环每次读一个字节,遇到 0 表示字符串结束。
2、实验内容 (1)用 DS:SI 指向字符串 让 SI 保存当前字符偏移。(字符串是连续字节序列,SI 适合逐字符向后移动)
(2)判断是否到字符串结尾 每次读取 [si],如果字符为 0 就结束。(字符串以 0 结尾,没有单独保存长度)
(3)判断并转换小写字母 用 cmp 判断字符是否在 'a' 到 'z' 之间,若在范围内就减 20H。(ASCII 中小写字母和大写字母相差 20H)
3、实验代码 这段代码用于:实现 letterc 子程序,扫描 0 结尾字符串,把小写字母转换成大写字母。 完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 assume cs:code, ds:data data segment msg db "Beginner's All-purpose Symbolic Instruction Code.",0 crlf db 13,10 data ends code segment start: mov ax,data mov ds,ax mov si,offset msg call letterc mov dx,offset msg call strlen mov bx,1 mov ah,40h int 21h mov dx,offset crlf mov cx,2 mov bx,1 mov ah,40h int 21h mov ax,4c00h int 21h letterc: push ax lc1: mov al,[si] or al,al jz lc_done cmp al,'a' jb lc_next cmp al,'z' ja lc_next sub byte ptr [si],20h lc_next: inc si jmp short lc1 lc_done: pop ax ret strlen: push ax push si mov si,dx xor cx,cx sl1: mov al,[si] or al,al jz sl_done inc si inc cx jmp short sl1 sl_done: pop si pop ax ret code ends end start
4、实验结果 输入:
1 Beginner's All-purpose Symbolic Instruction Code.
输出:
1 BEGINNER'S ALL-PURPOSE SYMBOLIC INSTRUCTION CODE.
实验 12 编写 0 号中断的处理程序 1、相关知识 中断和 0 号中断 这个实验进入中断机制。中断可以理解为 CPU 在正常执行流程之外,转去处理某个特殊事件。0 号中断是除法溢出中断,当执行除法指令时商放不进目标寄存器,就会自动触发 0 号中断。
中断向量表 中断向量表位于物理地址 0000:0000,每个中断向量占 4 字节:
1 2 低 2 字节:中断例程 IP 高 2 字节:中断例程 CS
0 号中断向量位置为 0000:0000,7Ch 号中断向量位置为 0000:01F0,计算公式是:
CPU 响应中断的过程 CPU 响应中断时大致做这些事:
标志寄存器入栈。
清除 TF、IF 等控制位。
当前 CS、IP 入栈。
从中断向量表取新的 CS:IP。
转去执行中断处理程序。
iret 返回 中断处理程序通常用 iret 返回,因为 iret 会同时恢复 IP、CS 和标志寄存器。若中断例程不打算回到原程序,也可以在例程中转去执行其他退出逻辑,但这属于具体程序设计。
cli 和 sti 安装中断例程时要关闭中断:
2、实验内容 (1)复制中断例程到低地址内存 把自定义 0 号中断处理程序复制到 0000:0200。(中断向量只能保存一个 CS:IP 地址)
(2)修改 0 号中断向量 把中断向量表中 0 号中断入口改成 0000:0200。(0 号中断向量位于 0000:0000)
(3)制造除法溢出验证 执行会产生溢出的除法,观察是否显示 divide error!。(除法溢出会自动触发 0 号中断)
3、实验代码 这段代码用于:把自定义 0 号中断例程安装到低地址内存,触发除法溢出后显示 divide error!。 完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 assume cs:code, ds:code code segment start: push cs pop ds xor ax,ax mov es,ax mov si,offset int0 mov di,200h mov cx,offset int0end-offset int0 cld rep movsb cli mov word ptr es:[0],200h mov word ptr es:[2],0 sti mov ax,1000h mov bl,1 div bl mov ax,4c00h int 21h int0: jmp short int0start msg db 'divide error!',13,10 msglen equ $-msg int0start: push ax push bx push cx push dx push ds push cs pop ds mov dx,202h mov cx,msglen mov bx,1 mov ah,40h int 21h mov ax,4c00h int 21h int0end: nop code ends end start
4、实验结果 除法溢出触发后输出:
实验 13 编写、应用中断例程 1、相关知识 软件中断 int n 会主动触发 n 号中断,这叫软件中断。它和除法溢出这类自动触发的内中断使用同一套中断向量机制。CPU 执行 int n 时,会把标志寄存器、CS、IP 入栈,然后从中断向量表读取新的 CS:IP。
ret 和 iret 中断例程返回使用 iret,它比 ret 多恢复一个标志寄存器:
1 2 ret :只弹出 IP iret :弹出 IP、CS、FLAGS
所以,普通子程序用 ret,中断例程用 iret。
自定义中断服务 自定义中断例程可以看作一种“系统服务”。调用者先按约定把参数放入寄存器或内存,再执行 int n。中断例程读取参数、完成工作,然后通过 iret 返回。
修改返回地址 中断发生时,返回地址已经被压入栈中。理论上,中断例程可以修改栈中的返回 IP 或 CS,让 iret 返回到不同位置。这种做法可以改变程序控制流,但必须非常谨慎。
2、实验内容 (1)安装显示字符串的 int 7Ch 约定调用者传入 DH=行号、DL=列号、CL=颜色、DS:SI=字符串首地址。(这些参数和实验 10 的 show_str 子程序类似)
(2)用 int 7Ch 模拟 loop 中断例程让 CX 减 1,并在需要继续循环时修改栈中的返回地址。(中断发生时返回地址已经压入栈中)
(3)应用中断例程显示英文诗 用前面安装的显示服务多次输出不同字符串到指定行。(可以验证中断例程不只适用于一个字符串)
3、实验代码 文件 SHOW7C.ASM:把显示字符串例程复制到 0000:0200,修改 7Ch 中断向量,然后用 int 7Ch 显示字符串并导出显存验证结果。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 assume cs:code, ds:data data segment msg db 'welcome to masm!',0 outbuf db 16 dup (0),13,10 data ends code segment start: mov ax,data mov ds,ax push cs pop ds mov si,offset show7c xor ax,ax mov es,ax mov di,200h mov cx,offset show7cend-offset show7c cld rep movsb cli mov word ptr es:[7ch*4],200h mov word ptr es:[7ch*4+2],0 sti mov dh,10 mov dl,10 mov cl,2 mov ax,data mov ds,ax mov si,offset msg int 7ch mov ax,0b800h mov ds,ax mov si,10*160+10*2 mov ax,data mov es,ax mov di,offset outbuf mov cx,16 copy_show: lodsb mov es:[di],al inc di lodsb loop copy_show mov ax,data mov ds,ax mov dx,offset outbuf mov cx,18 mov bx,1 mov ah,40h int 21h mov ax,4c00h int 21h show7c: jmp short show7cstart show7cstart: push ax push bx push cx push dx push si push di push es mov al,dh xor ah,ah mov bl,160 mul bl mov di,ax mov al,dl xor ah,ah shl ax,1 add di,ax mov ax,0b800h mov es,ax showlp: lodsb or al,al jz showdone mov es:[di],al mov es:[di+1],cl add di,2 jmp short showlp showdone: pop es pop di pop si pop dx pop cx pop bx pop ax iret show7cend: nop code ends end start
文件 LOOP7C.ASM:安装修改返回地址的 7Ch 中断例程,通过改变栈中返回 IP 来模拟 loop 的跳转效果。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 assume cs:code code segment start: push cs pop ds mov si,offset loop7c xor ax,ax mov es,ax mov di,200h mov cx,offset loop7cend-offset loop7c cld rep movsb cli mov word ptr es:[7ch*4],200h mov word ptr es:[7ch*4+2],0 sti mov ax,0b800h mov es,ax mov di,160*12 mov bx,offset s-offset se mov cx,80 s: mov byte ptr es:[di],'!' add di,2 int 7ch se: nop mov ax,0b800h mov ds,ax mov si,160*12 push cs pop es mov di,offset outbuf mov cx,80 copy_loop: lodsb stosb lodsb loop copy_loop push cs pop ds mov dx,offset outbuf mov cx,82 mov bx,1 mov ah,40h int 21h mov ax,4c00h int 21h loop7c: push bp mov bp,sp dec cx jcxz loopret add [bp+2],bx loopret: pop bp iret loop7cend: nop outbuf db 80 dup (0),13,10 code ends end start
文件 POEM.ASM:用 BIOS 设置光标、DOS 显示字符串,在不同行输出英文诗,作为中断调用和屏幕输出的应用程序。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 assume cs:code code segment s1 db 'Good,better,best,','$' s2 db 'Never let it rest,','$' s3 db 'Till good is better,','$' s4 db 'And better,best.','$' s dw offset s1,offset s2,offset s3,offset s4 row db 2,4,6,8 start: mov ax,cs mov ds,ax mov bx,offset s mov si,offset row mov cx,4 ok: mov bh,0 mov dh,[si] mov dl,0 mov ah,2 int 10h mov dx,[bx] mov ah,9 int 21h inc si add bx,2 loop ok mov ax,4c00h int 21h code ends end start
4、实验结果 显示字符串结果:
模拟 loop 后在屏幕中间显示 80 个 !:
1 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
实验 14 访问 CMOS RAM 1、相关知识 端口 I/O 端口 I/O 是 CPU 访问外设的重要方式。8086 不只访问内存,也可以通过端口访问外设。端口访问使用 in 和 out 指令。out 70h,al 表示把 AL 写到端口 70H,in al,71h 表示从端口 71H 读入一个字节到 AL。
CMOS 访问步骤 CMOS RAM 中保存系统日期和时间。访问 CMOS 通常分两步:
向端口 70H 写入要访问的 CMOS 单元号。
从端口 71H 读出该单元的数据,或向 71H 写入数据。
时间单元号 常用时间单元号:
BCD 码 时间数据通常是 BCD 码,例如 26H 表示十进制 26,而不是十六进制的 38。BCD 的高 4 位表示十位,低 4 位表示个位。
BCD 转 ASCII BCD 转 ASCII 方法:
1 2 高位 = (BCD >> 4) + '0' 低位 = (BCD & 0FH) + '0'
读取一致性 读 CMOS 时要注意,硬件时间可能正在更新。严谨程序会检查更新状态,避免读到一半更新前、一半更新后的不一致时间。
2、实验内容 (1)准备显示缓冲区 先定义形如 00/00/00 00:00:00 的字符串。(日期时间格式中的 /、空格、: 是固定字符)
(2)依次读取 CMOS 时间单元 读取年、月、日、时、分、秒,对应 CMOS 单元号为 9、8、7、4、2、0。(CMOS 中各时间字段分散在固定单元号)
(3)BCD 转 ASCII 并显示 把 BCD 的高 4 位和低 4 位分别转成 ASCII 数字,写入缓冲区。(一个 BCD 字节正好包含两位十进制数字)
3、实验代码 这段代码用于:读取 CMOS 中的年、月、日、时、分、秒,转换成 ASCII 日期时间字符串并输出验证。 完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 assume cs:code, ds:data data segment buf db '00/00/00 00:00:00',13,10 data ends code segment start: mov ax,data mov ds,ax mov bx,offset buf mov al,9 call read2 mov bx,offset buf+3 mov al,8 call read2 mov bx,offset buf+6 mov al,7 call read2 mov bx,offset buf+9 mov al,4 call read2 mov bx,offset buf+12 mov al,2 call read2 mov bx,offset buf+15 mov al,0 call read2 mov dx,offset buf mov cx,19 mov bx,1 mov ah,40h int 21h mov ax,4c00h int 21h read2: push ax push cx out 70h,al in al,71h mov ah,al mov cl,4 shr ah,cl and al,0fh add ah,'0' add al,'0' mov [bx],ah mov [bx+1],al pop cx pop ax ret code ends end start
4、实验结果 一次运行结果:
实验 15 安装新的 int 9 中断例程 1、相关知识 外中断和 int 9 外中断是由 CPU 外部设备触发的中断。键盘输入由硬件触发,会引发 9 号中断。BIOS 的 int 9 中断例程会从端口 60H 读取键盘扫描码,然后根据 Shift、Ctrl 等状态把扫描码转换为 ASCII 码或状态信息,写入 BIOS 键盘缓冲区。
键盘扫描码 键盘扫描码分为通码和断码:
按下按键产生通码。
松开按键产生断码。
多数断码 = 通码 + 80H。
例如 A 键通码为 1EH,断码为 9EH。
保存原中断入口 改写硬件中断时一般要保存原中断入口,在新例程中先调用原例程,再执行自己的附加逻辑。
代码位置 驻留类中断例程还要注意代码位置。程序结束后,若中断向量仍指向已经释放或可能被覆盖的代码区域,系统会出错。所以常见做法是把中断例程复制到相对稳定的低地址内存,或编写真正的驻留程序。
2、实验内容 (1)保存原 int 9 中断向量 把原来的 int 9 入口地址保存到低内存备用。(新例程不应该完全替代 BIOS 键盘处理)
(2)安装新的 int 9 例程 把新例程复制到低地址内存,并修改 9 号中断向量。(程序结束后原代码段可能被覆盖)
(3)读取扫描码并调用原例程 新例程从端口 60H 读扫描码,然后调用保存的原 int 9。(读端口 60H 才能知道是哪一个按键事件)
(4)检测 A 键断码并改显存 如果扫描码是 9EH,就把屏幕字符区写满 A。(A 键通码是 1EH)
3、实验代码 文件 AINT9.ASM:保存原 int 9 向量,安装新键盘中断例程;检测 A 键断码后,把屏幕字符区写满 A。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 assume cs:code stack segment db 128 dup (0) stack ends code segment start: mov ax,stack mov ss,ax mov sp,128 push cs pop ds xor ax,ax mov es,ax mov si,offset int9 mov di,204h mov cx,offset int9end-offset int9 cld rep movsb push es:[9*4] pop es:[200h] push es:[9*4+2] pop es:[202h] cli mov word ptr es:[9*4],204h mov word ptr es:[9*4+2],0 sti mov ax,4c00h int 21h int9: push ax push bx push cx push es in al,60h pushf call dword ptr cs:[200h] cmp al,9eh jne int9ret mov ax,0b800h mov es,ax xor bx,bx mov cx,2000 fillscr: mov byte ptr es:[bx],'A' add bx,2 loop fillscr int9ret: pop es pop cx pop bx pop ax iret int9end: nop code ends end start
文件 WAITA.ASM:等待一段时间后扫描文本显存,统计屏幕上字符 A 的数量,用来验证新键盘中断是否生效。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 assume cs:code, ds:data data segment msg1 db 'COUNT=',0 buf db 6 dup (0) crlf db 13,10 data ends code segment start: mov ax,data mov ds,ax mov ax,0b800h mov es,ax xor di,di mov ax,0720h mov cx,2000 clearlp: stosw loop clearlp mov ah,0 int 1ah add dx,91 adc cx,0 mov si,cx mov di,dx waitlp: mov ah,0 int 1ah cmp cx,si jb waitlp ja waitok cmp dx,di jb waitlp waitok: mov ax,0b800h mov es,ax xor di,di mov cx,2000 xor bx,bx cntlp: cmp byte ptr es:[di],'A' jne cntnext inc bx cntnext: add di,2 loop cntlp mov ax,bx mov di,offset buf call utoa mov dx,offset msg1 call strlen mov bx,1 mov ah,40h int 21h mov dx,offset buf call strlen mov bx,1 mov ah,40h int 21h mov dx,offset crlf mov cx,2 mov bx,1 mov ah,40h int 21h mov ax,4c00h int 21h utoa: push ax push bx push cx push dx xor cx,cx mov bx,10 ut1: xor dx,dx div bx push dx inc cx or ax,ax jnz ut1 ut2: pop dx add dl,'0' mov [di],dl inc di loop ut2 mov byte ptr [di],0 pop dx pop cx pop bx pop ax ret strlen: push ax push si mov si,dx xor cx,cx sl1: mov al,[si] or al,al jz sl2 inc si inc cx jmp short sl1 sl2: pop si pop ax ret code ends end start
4、实验结果 自动测试环境没有模拟真实按键,结果为:
在真实 DOSBox 前台环境中按下并松开 A 键,可触发满屏 A。
实验 16 编写包含多个功能子程序的中断例程 1、相关知识 多功能中断服务 多功能中断例程类似 DOS 和 BIOS 中断服务:同一个中断号下面可以有多个功能,通过 AH 或其他寄存器区分具体功能。
功能分发 常见功能分发方式有两种:
连续比较功能号,然后跳转到对应子程序。
建立地址表,根据功能号查表调用对应子程序。
地址表 多个功能的分发可以用地址表实现,比连续 cmp/jmp 更清晰。地址表本质上是一组子程序入口偏移,功能号乘以 2 后就能找到对应入口。
文本显存功能 屏幕操作仍然基于文本显存 B800:0000:
清屏:把屏幕字符单元写成空格和默认属性。
设置前景色:修改属性字节中的前景色位。
设置背景色:修改属性字节中的背景色位。
上卷一行:把后面的行整体复制到前一行,最后一行清空。
保护现场 实现这些功能时要注意保护寄存器和段寄存器,尤其是 DS、ES,因为调用者可能依赖它们继续执行。
2、实验内容 (1)约定功能号 使用 AH 传递功能号(DOS/BIOS 中断服务常用 AH 传功能号):
1 2 3 4 AH=0 清屏 AH=1 设置前景色 AH=2 设置背景色 AH=3 向上滚动一行
(2)建立地址表 把 4 个功能子程序入口放入表中,根据 AH 查表调用。(地址表比连续比较更容易扩展)
(3)实现显存操作 清屏和改色直接操作 B800 显存,上卷一行用 rep movsw 把第 2 行到第 25 行搬到上一行。(文本屏幕共有 80*25=2000 个字符单元)
(4)编写测试输出 分别调用 4 个功能,再读取显存关键位置输出验证值。(直接看屏幕效果不方便自动记录)
3、实验代码 这段代码用于:安装多功能 int 7Ch,通过 AH 选择清屏、设置前景色、设置背景色、上卷一行,并输出验证结果。 完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 assume cs:code code segment start: push cs pop ds mov si,offset int7c xor ax,ax mov es,ax mov di,200h mov cx,offset int7cend-offset int7c cld rep movsb cli mov word ptr es:[7ch*4],200h mov word ptr es:[7ch*4+2],0 sti call testclr call testfg call testbg call testup mov ax,4c00h int 21h putc: push ax mov ah,2 int 21h pop ax ret puts: push ax push si ps1: lodsb or al,al jz ps2 mov dl,al call putc jmp short ps1 ps2: pop si pop ax ret hexnib: and al,0fh cmp al,9 jbe hexdec add al,7 hexdec: add al,'0' ret puthex: push ax mov ah,al mov al,ah shr al,1 shr al,1 shr al,1 shr al,1 call hexnib mov dl,al call putc mov al,ah call hexnib mov dl,al call putc pop ax ret crlf: mov dl,13 call putc mov dl,10 call putc ret fillscr: push ax push bx push cx push es mov ax,0b800h mov es,ax xor bx,bx mov cx,2000 fs1: mov es:[bx],dl mov es:[bx+1],dh add bx,2 loop fs1 pop es pop cx pop bx pop ax ret testclr: mov dl,'X' mov dh,1eh call fillscr mov ah,0 int 7ch mov si,offset txtclr call puts mov ax,0b800h mov es,ax mov al,es:[0] call puthex mov dl,' ' call putc mov al,es:[1] call puthex call crlf ret testfg: mov ax,0b800h mov es,ax mov byte ptr es:[0],'A' mov byte ptr es:[1],07h mov ah,1 mov al,2 int 7ch mov si,offset txtfg call puts mov al,es:[1] call puthex call crlf ret testbg: mov ax,0b800h mov es,ax mov byte ptr es:[0],'A' mov byte ptr es:[1],07h mov ah,2 mov al,4 int 7ch mov si,offset txtbg call puts mov al,es:[1] call puthex call crlf ret testup: mov dl,'1' mov dh,07h call fillscr mov ax,0b800h mov es,ax mov byte ptr es:[0],'A' mov byte ptr es:[160],'B' mov ah,3 int 7ch mov si,offset txtup call puts mov al,es:[0] call puthex mov dl,' ' call putc mov al,es:[24*160] call puthex call crlf ret txtclr db 'CLR=',0 txtfg db 'FG=',0 txtbg db 'BG=',0 txtup db 'UP=',0 int7c: jmp short setstart table dw do0-int7c+200h,do1-int7c+200h,do2-int7c+200h,do3-int7c+200h setstart: push bx cmp ah,3 ja setret mov bl,ah xor bh,bh shl bx,1 call word ptr cs:[bx+202h] setret: pop bx iret do0: push ax push bx push cx push es mov ax,0b800h mov es,ax xor bx,bx mov ax,0720h mov cx,2000 do0lp: mov es:[bx],ax add bx,2 loop do0lp pop es pop cx pop bx pop ax ret do1: push ax push bx push cx push dx push es mov dl,al mov ax,0b800h mov es,ax mov bx,1 mov cx,2000 do1lp: and byte ptr es:[bx],0f8h or byte ptr es:[bx],dl add bx,2 loop do1lp pop es pop dx pop cx pop bx pop ax ret do2: push ax push bx push cx push dx push es mov dl,al shl dl,1 shl dl,1 shl dl,1 shl dl,1 mov ax,0b800h mov es,ax mov bx,1 mov cx,2000 do2lp: and byte ptr es:[bx],8fh or byte ptr es:[bx],dl add bx,2 loop do2lp pop es pop dx pop cx pop bx pop ax ret do3: push ax push bx push cx push si push di push ds push es mov ax,0b800h mov ds,ax mov es,ax mov si,160 xor di,di mov cx,24*80 cld rep movsw mov ax,0720h mov cx,80 do3lp: stosw loop do3lp pop es pop ds pop di pop si pop cx pop bx pop ax ret int7cend: nop code ends end start
4、实验结果 验证输出:
1 2 3 4 CLR=20 07 FG=02 BG=47 UP=42 20
可以看出清屏、前景色、背景色、上卷功能均成功。
实验 17 编写包含多个功能子程序的中断例程 1、相关知识 BIOS int 13h BIOS int 13h 用于磁盘读写。磁盘传统访问方式使用 CHS 编号,即柱面/磁道、磁头、扇区。对 1.44MB 软盘来说,通常有 2 面、80 磁道、每磁道 18 扇区,共 2880 个扇区。
CHS 参数 BIOS 读写扇区时常用参数:
1 2 3 4 5 6 7 8 AH = 02H 读扇区 AH = 03H 写扇区 AL = 扇区数量 CH = 磁道号 CL = 扇区号,从 1 开始 DH = 磁头号/面号 DL = 驱动器号,软盘 A 通常是 0 ES:BX = 数据缓冲区
逻辑扇区号 直接使用 CHS 不方便,所以可以把所有扇区统一编号为逻辑扇区号。逻辑扇区号从 0 到 2879。逻辑扇区号和物理编号转换:
1 2 3 面号 = 逻辑扇区号 / 1440 磁道号 = (逻辑扇区号 % 1440) / 18 扇区号 = (逻辑扇区号 % 18) + 1
写磁盘风险 注意扇区号从 1 开始,而逻辑扇区号从 0 开始,所以最后要 +1。磁盘写入有破坏性,实验应使用测试软盘镜像,不要随意写真实硬盘。
2、实验内容 (1)设计 int 7Ch 磁盘服务接口 用 AH=0 表示读扇区,AH=1 表示写扇区,DX 传逻辑扇区号,ES:BX 指向数据缓冲区。(AH 传功能号符合中断服务习惯)
(2)逻辑扇区号转换为 CHS 先用 DX / 1440 得到面号,再用余数除以 18 得到磁道号和扇区偏移。(1.44MB 软盘每面有 80*18=1440 个扇区)
(3)调用 BIOS int 13h 把自定义功能号转换成 BIOS 功能号:读为 AH=02H,写为 AH=03H。(实际磁盘读写仍由 BIOS 完成)
(4)写入后读回验证 先向逻辑扇区 2879 写入字符串,再读回比较。(2879 是 1.44MB 软盘最后一个逻辑扇区)
3、实验代码 这段代码用于:安装磁盘扇区读写服务,把逻辑扇区号转换为 BIOS CHS 参数,再调用 int 13h 读写软盘扇区。 完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 assume cs:code, ds:data data segment wbuf db 'LOGICAL SECTOR TEST',0,493 dup (0) rbuf db 512 dup (0) okm db 'OK ',0 erm db 'ERR',13,10,0 crlf db 13,10,0 data ends code segment start: mov ax,data mov ds,ax push cs pop ds mov si,offset int7c xor ax,ax mov es,ax mov di,200h mov cx,offset int7cend-offset int7c cld rep movsb cli mov word ptr es:[7ch*4],200h mov word ptr es:[7ch*4+2],0 sti mov ax,data mov ds,ax mov es,ax mov bx,offset wbuf mov dx,2879 mov ah,1 int 7ch mov di,offset rbuf mov cx,64 xor ax,ax clrr: stosb loop clrr mov ax,data mov es,ax mov bx,offset rbuf mov dx,2879 mov ah,0 int 7ch mov si,offset wbuf mov di,offset rbuf cmplp: mov al,[si] cmp al,[di] jne bad or al,al jz good inc si inc di jmp short cmplp good: mov si,offset okm call puts mov si,offset rbuf call puts mov si,offset crlf call puts jmp short finish bad: mov si,offset erm call puts finish: mov ax,4c00h int 21h puts: push ax ps1: lodsb or al,al jz ps2 mov dl,al mov ah,2 int 21h jmp short ps1 ps2: pop ax ret int7c: push ax push bx push cx push dx push si push di push bp push ax mov ax,dx xor dx,dx mov cx,1440 div cx mov di,ax mov ax,dx xor dx,dx mov cx,18 div cx mov ch,al mov cl,dl inc cl mov dx,di mov dh,dl xor dl,dl mov al,1 mov ah,2 pop si and si,0ff00h cmp si,0 je doio inc ah doio: int 13h pop bp pop di pop si pop dx pop cx pop bx pop ax iret int7cend: nop code ends end start
4、实验结果 测试逻辑扇区 2879,先写入再读回比较: