汇编语言与接口设计实验

汇编语言与接口设计实验

赶作业,顺便留点开源复习资料吧:)

实验 1 查看 CPU 和内存,用机器指令和汇编指令编程

1、相关知识

主要是关于debug使用,要了解的包括8086 CPU 的寄存器、内存地址表示方法,以及常见Debug调试指令。

寄存器

8086 常用寄存器可以分成几类:

  • 通用寄存器:AXBXCXDX。它们都可以作为 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,也常用于端口号。

  • 段寄存器:CSDSSSESCS 是代码段,DS 是数据段,SS 是栈段,ES 是附加段,常用于数据搬运和显存操作。

  • 指针和变址寄存器:SPBPSIDISP 指向栈顶,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 写入后,显存内容为:

1
41 1E 42 2E 43 4E 44 5E

实验 2 用机器指令和汇编指令编程

1、相关知识

本实验涉及存储方式、存取方法

8086数据存储方式

小端存储

1234H:

1
2
低地址 = 34H
高地址 = 12H

对应读取也要这么理解

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=5CCAHBX=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,4c00hint 21h:调用 DOS 功能返回操作系统。

DOS 加载 .EXE 时,会在程序前面建立 PSP,也就是程序段前缀。PSP 中保存了命令行、文件控制块、返回入口等信息。PSP 首部通常是 CD 20,表示一条 int 20h 指令。Debug 加载程序后看到的 DSES 往往先指向 PSP,而不是直接指向你的数据段。

调试 .EXE 时要关注 CS:IP 是否指向入口,SS:SP 是否指向正确栈顶,以及 DS 是否已经被程序设置到数据段。

2、实验内容

(1)编写最小 .ASM程序

程序只定义一个代码段,入口为 start,最后用 DOS 中断返回。

(2)在程序中设置栈并执行 push/pop

代码设置 SSSP 后执行几次入栈和出栈。

(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 首部:

1
0DFA:0000  CD 20 ...

CD 20 是机器码,表示 int 20h。它出现在 PSP 开头,可以看出 DSES 初始指向 PSP,而不是源程序里定义的代码段或数据段。Debug 加载 .EXE 后常见现象是:

1
2
3
4
DS = PSP 段地址
ES = PSP 段地址
CS = 代码段地址
IP = 程序入口偏移

所以如果程序中要访问自己的数据段,不能假设 DS 自动正确,必须显式执行:

1
2
mov ax,data
mov ds,ax

这个实验程序能跟踪到 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 中,能直接放进 [] 里做偏移寻址的寄存器主要是 BXBPSIDI。其中 BXSIDI 默认配合 DS,而 BP 默认配合 SS。这个区别在访问普通数据和访问栈中数据时很重要。

这个实验选择 BX,是因为目标是一段连续内存:BX 负责指出当前地址,BL 又刚好是 BX 的低 8 位。于是随着 BX0000H 增加到 003FHBL 也从 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=003FHBL=3FH,写入后 BX 变成 0040HCX 变成 0,loop 不再跳转。

结果中出现类似:

1
8C C8 8E D8 B8 20 00 ...

这些不是随机数据,而是程序指令的机器码。可以看出程序确实把自己的代码字节复制到了 0020:0000。这能帮助理解“代码也是内存中的数据”。

实验 5 编写、调试具有多个段的程序

1、相关知识

代码放代码段,变量放数据段,临时保存和调用返回地址放栈段。

assume cs:code, ds:data, ss:stack 只是告诉汇编器段寄存器和段名的对应关系,并不会真正修改 CPU 的段寄存器。真正生效必须使用机器指令:

1
2
mov ax,data
mov ds,ax

也就是说,assume 解决的是“汇编器如何理解地址”,mov ds,ax 解决的是“CPU 实际访问哪个段”。二者不能混淆。

程序入口由 end start 指定。start 是代码段中的标号,连接器会把这个入口写入 .EXE 文件头。程序加载后,DOS 根据入口设置 CS:IP,CPU 才会从正确位置执行。如果只写 end,连接器可能默认把程序开头当入口,若开头是数据段,CPU 就会把数据当指令执行。

段在 .EXE 中装入时通常按 16 字节对齐。若某段实际占 N 字节,装入后占用空间通常是 16 * ceil(N/16) 字节。这就是为什么观察段地址关系时,段之间常常相差整数个 16 字节段落。

多段数据搬运常用 DSES 配合。DS 可以指向源数据段,ES 可以指向目标数据段,通过段前缀可以把数据写入另一个段。

2、实验内容

(1)观察多段程序的装入关系

编译、连接多个段顺序不同的程序,用 Debug 查看 CSDSSS 和各段内容。(多段程序最容易混淆的是“源程序里的段名”和“运行时段寄存器的值”)

(2)完成 a+b -> c

先把 a 段复制到 cdata 段,再把 b 段逐字节加到 cdata 对应位置。(源数据在两个不同段中,目标数据在第三个段中,正好练习多段访问)

(3)用栈实现逆序存储

a 段中的字数据依次 push 入栈,再依次 popb 段。(栈后进先出,天然会把顺序反过来)

3、实验代码

Q5.ASM:定义 abcdata 三个数据段,先复制 acdata,再把 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
02 04 06 08 0A 0C 0E 10

原因是:

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。例如:

1
2
'A' = 41H
'a' = 61H

所以小写转大写可以减 20H,也可以用 and al,11011111b 清除第 5 位。但这种方法只适合确定目标字符是字母的情况,严格程序还应先判断范围。

寻址方式和嵌套循环

包括:

  • ASCII 大小写转换:小写转大写可清除第 5 位,即 and al,11011111b
  • [bx+idata] 适合访问结构固定的数据。
  • SIDI 常用于字符串源地址和目标地址。
  • 嵌套循环中必须保护外层 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

dbdwdd 分别定义字节、字、双字数据:

1
2
3
db:1 字节
dw:2 字节
dd:4 字节

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,所以人均收入为:

1
16 / 3 = 5 余 1

表中保存的是整数商 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 指向 s2DI 指向 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

这个实验的结果可以看出两件事:

  1. CPU 不会记住某个地址“原来是什么指令”,它每次都是从 CS:IP 指向的内存重新取机器码执行。
  2. 只要代码段可写,程序就能修改后续将要执行的指令。这种技巧叫自修改代码,实际工程中应谨慎使用,但它非常适合理解“指令也是内存数据”。

实验 9 根据材料编程

1、相关知识

文本显存

直接操作文本显存是 DOS 汇编中常见的屏幕输出方法。DOS 文本模式下,彩色显示缓冲区通常从 B800:0000 开始。屏幕每行 80 个字符,每个字符占 2 字节,所以一行占 160 字节。

字符和属性

每个显示字符的两个字节含义如下:

1
2
低字节:ASCII 字符
高字节:颜色属性

例如:

1
41 1E

表示显示字符 'A',属性为 1EH

显存偏移

某行某列的显存偏移为:

1
offset = 行号 * 160 + 列号 * 2

颜色属性

颜色属性字节中,低 4 位是前景色,高 4 位是背景色。例如 02H 表示黑底绿色字,24H 表示绿底红字,71H 表示白底蓝字。写显存时必须字符和属性成对写入,否则屏幕内容或颜色会错位。

2、实验内容

(1)准备字符串和属性表

定义 welcome to masm!,再定义三个属性字节 02H24H71H。(字符串内容三行相同,单独定义一次即可)

(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!divdw1000000 / 10 得到商 000186A0H,余数 0dtoc12666 转换为:

1
31 32 36 36 36 00

对应字符串 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

jbja 是无符号条件转移,适合 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,计算公式是:

1
中断向量地址 = 中断类型码 * 4

CPU 响应中断的过程

CPU 响应中断时大致做这些事:

  1. 标志寄存器入栈。
  2. 清除 TFIF 等控制位。
  3. 当前 CSIP 入栈。
  4. 从中断向量表取新的 CS:IP
  5. 转去执行中断处理程序。

iret 返回

中断处理程序通常用 iret 返回,因为 iret 会同时恢复 IPCS 和标志寄存器。若中断例程不打算回到原程序,也可以在例程中转去执行其他退出逻辑,但这属于具体程序设计。

cli 和 sti

安装中断例程时要关闭中断:

1
2
3
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、实验结果

除法溢出触发后输出:

1
divide error!

实验 13 编写、应用中断例程

1、相关知识

软件中断

int n 会主动触发 n 号中断,这叫软件中断。它和除法溢出这类自动触发的内中断使用同一套中断向量机制。CPU 执行 int n 时,会把标志寄存器、CSIP 入栈,然后从中断向量表读取新的 CS:IP

ret 和 iret

中断例程返回使用 iret,它比 ret 多恢复一个标志寄存器:

1
2
ret  :只弹出 IP
iret :弹出 IP、CS、FLAGS

所以,普通子程序用 ret,中断例程用 iret

自定义中断服务

自定义中断例程可以看作一种“系统服务”。调用者先按约定把参数放入寄存器或内存,再执行 int n。中断例程读取参数、完成工作,然后通过 iret 返回。

修改返回地址

中断发生时,返回地址已经被压入栈中。理论上,中断例程可以修改栈中的返回 IPCS,让 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、实验结果

显示字符串结果:

1
welcome to masm!

模拟 loop 后在屏幕中间显示 80 个 !

1
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

实验 14 访问 CMOS RAM

1、相关知识

端口 I/O

端口 I/O 是 CPU 访问外设的重要方式。8086 不只访问内存,也可以通过端口访问外设。端口访问使用 inout 指令。out 70h,al 表示把 AL 写到端口 70Hin al,71h 表示从端口 71H 读入一个字节到 AL

CMOS 访问步骤

CMOS RAM 中保存系统日期和时间。访问 CMOS 通常分两步:

  1. 向端口 70H 写入要访问的 CMOS 单元号。
  2. 从端口 71H 读出该单元的数据,或向 71H 写入数据。

时间单元号

常用时间单元号:

1
2
3
4
5
6
0:秒
2:分
4:时
7:日
8:月
9:年

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 单元号为 987420。(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、实验结果

一次运行结果:

1
26/04/23 15:55:15

实验 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、实验结果

自动测试环境没有模拟真实按键,结果为:

1
COUNT=0

在真实 DOSBox 前台环境中按下并松开 A 键,可触发满屏 A

实验 16 编写包含多个功能子程序的中断例程

1、相关知识

多功能中断服务

多功能中断例程类似 DOS 和 BIOS 中断服务:同一个中断号下面可以有多个功能,通过 AH 或其他寄存器区分具体功能。

功能分发

常见功能分发方式有两种:

  • 连续比较功能号,然后跳转到对应子程序。
  • 建立地址表,根据功能号查表调用对应子程序。

地址表

多个功能的分发可以用地址表实现,比连续 cmp/jmp 更清晰。地址表本质上是一组子程序入口偏移,功能号乘以 2 后就能找到对应入口。

文本显存功能

屏幕操作仍然基于文本显存 B800:0000

  • 清屏:把屏幕字符单元写成空格和默认属性。
  • 设置前景色:修改属性字节中的前景色位。
  • 设置背景色:修改属性字节中的背景色位。
  • 上卷一行:把后面的行整体复制到前一行,最后一行清空。

保护现场

实现这些功能时要注意保护寄存器和段寄存器,尤其是 DSES,因为调用者可能依赖它们继续执行。

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,先写入再读回比较:

1
OK LOGICAL SECTOR TEST

汇编语言与接口设计实验
http://example.com/2026/05/01/汇编语言与接口设计实验/
作者
oxygen
发布于
2026年5月1日
许可协议