描述了单元长度的标号
例:将code段中的a标号处的8个数据累加,结果保存到b标号处。
assume cs:code
code segment
a: db 1, 2, 3, 4, 5, 6, 7, 8
b: dw 0
start:
mov si, offset a
mov bx, offset b
mov cx, 8
s: mov al, cs:[si]
mov ah, 0
add cs:[bx], ax
inc si
loop s
mov ax, 4c00h
int 21h
code ends
end start
程序中,code、a、b、start、s都是标号。这些标号仅仅表示了内存单元的地址。
例改:
assume cs:code
code segment
a: db 1, 2, 3, 4, 5, 6, 7, 8
b: dw 0
start:
mov si, 0
mov cx, 8
s: mov al, a[si]
mov ah, 0
add b, ax
inc si
loop s
mov ax, 4c00h
int 21h
code ends
end start
在code段中使用的标号a、b后面都没有“:”,它们是同时描述内存地址和单元长度的标号。
标号a:描述了地址code:0,和从这个地址开始,以后的内存单元都是字节单元。
标号b:描述了地址code:8,和从这个地址开始,以后的内存单元都是字节单元。
例:b dw 0
指令: mov ax, b
相当于: mov ax, cs:[8]
指令: mov b, 2
相当于: mov word ptr cs:[8], 2
指令: inc b
相当于: inc word ptr cs:[8]
这些指令中,标号b代表了一个内存单元,地址位code:8,长度为两个字节。
例:a db 1,2,3,4,5,6,7,8
指令: mov al, a[si]
相当于: mov al, cs:0[si]
指令: mov al, [3]
相当于: mov al, cs:0[3]
指令: mov al, a[bx + si + 3]
相当于: mov al, cs:0[bx + si + 3]
使用这种包含单元长度的标号,可以是我们以简洁的形式访问内存中的数据。我们将这种标号称为数据标号,他标记了存储数据的单位的地址和长度。它不同于仅仅表示地址的地址符号。
在其他段中使用数据标号
一般不在代码段中定义数据,而是将数据定义到其他段中。在其他段中,我们也可以使用数据标号来描述存储数据的单元和长度。
注意:在后面加有“:”的地址符号,只能在代码段中使用,不能再其他段中使用。
例:将code段中的a标号处的8个数据累加,结果保存到b标号处。
assume cs:code, ds:data
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
data ends
code segment
start: mov ax, data
mov ds, ax
mov si, 0
mov cx, 8
s: mov al, a[si]
mov ah, 0
add b, ax
inc si
loop s
mov ax, 4c00H
int 21h
code ends
end start
注意:如果想在代码段中直接使用数据标号访问数据,则需要用伪指令assume将标号所在的段和一个寄存器联系起来。因为编译器的工作需要,用assume指令将段寄存器和某个段相关联,段寄存器中就会真的存放该段的嗲之。在程序中还要使用指令对寄存器进行设置。
上面程序中,要在代码段code中用data段中的数据标号a、b访问数据,则必须用assume将一个寄存器和data段相关联。
例:
指令: mov al, a[si]
相当于: mov al, [si + 0] -> mov al, ds:[si + 0]
指令: add b, ax
相当于: add [8], ax
这些实际编译出的指令,都默认所访问单元的段地址在ds中,而实际要访问的段为data,所以,这些指令执行前,ds中必须为data段的段地址,设置ds指向data段:
mov ax, data
mov ds, ax
可以将标号当作数据来定义,此时,编译器将标号所表示的地址当作数据的值。
比如:
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw a, b
data ends
数据标号c处存储的两个字型数据为标号a、b的偏移地址。
相当于:
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw offset a, offset b
data ends
再比如:
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dd a, b
data ends
数据标号c处存储的两个双字型数据为标号a的偏移地址和段地址、标号b的偏移地址和段地址。
相当于:
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw offset a, seg a, offset b, seg b
data ends
seg操作符,功能为获取某一标号的段地址。
直接定址表
例:以十六进制的形式在屏幕中间显示给定的字节型数据。
数值 + 30h = 对应字符的ASCII值
0 + 30h = “0”的ASCII值
1 + 30h = “1”的ASCII值
数值 + 37h = 对应字符的ASCII值
10 + 37h = “A”的ASCII值
11 + 37h = "B"的ASCII值
还可以,建立一张表,在表中依次存储字符“0” ~ “F”,我们可以通过数值 0 ~ 15直接查找到对应的字符。
子程序:
//用 al 传递要显示的数据
showbyte: jmp short show
table db '0123456789ABCDEF'
show: push bx
push es
mov ah, al
shr ah, 1
shr ah, 1
shr ah, 1
shr ah, 1 //右移4位,ah中得到高4位的值
and al, 00001111b //al中为低4位的值
mov bl, ah
mov bh, 0
mov ah, table[bx] //用高4位的值作为相对于table的偏移取得对应的字符
mov bx, 0b800h
mov es, bx
mov es:[160 * 12 + 40 * 2], ah
mov bl, al
mov bh, 0
mov al, table[bx]
mov es:[160 * 12 + 40 * 2 + 2], al
pop es
pop bx
ret
程序中:以数值N为table表中的偏移,可以找到对应的字符。
例:计算sin(x), x∈{0°,30°,60°,90°,120°,150°,180°}并在屏幕中间显示结果。
可以提前保存一下sin(x)的结果。如:
sin(0) = 0
sin(30) = 0.5
sin(60) = 0.866
sin(90) = 1
sin(120) = 0.866
sin(150) = 0.5
sin(180) = 0
公式:
sin(x) = sin(y) ≈ y - 1/3 * y * 3 + 1/5! * y * 5
y = x/180 * 3.1415926
用ax向子程序传递角度:
showsin: jmp short show
table dw ag0, ag30, ag60, ag90, ag120, ag150, ag180
ag0 db '0', 0
ag30 db '0.5', 0
ag60 db '0.866', 0
ag90 db '1', 0
ag120 db '0.866', 0
ag150 db '0.5', 0
ag180 db '0', 0
show: push bx
push es
push si
mov bx, 0b800h
mov es, bx
//30度设定偏移量
mov ah, 0
mov bl, 30
div bl
mov bl, al
mov bh, 0
add bx, bx
mov bx, table[bx]
//显示对应的字符串
mov si, 160 * 12 + 40 * 2
shows: mov ah, cs:[bx]
cmp ah, 0
je showret
mov es:[si], ah
inc bx
add si, 2
jmp short shows
showret: pop si
pop es
pop bx
ret
注意:以角度值/30 为table表中的偏移,可以找到对应的字符串的首地址。
像这种可以通过依据数据,直接计算出所要找的元素的位置的表,称为:直接定址表。
还可以将子程序地入口地址存到table中进行使用。
s: mov ax, 0
d: mov ax, 1
c: mov ax, 2
table dw a, b, c
mov bx, 2
call word ptr table[bx] //可以这样跳转
使用BIOS进行键盘输入和磁盘读写
大多数有用地程序都需要处理用户的输入,键盘输入是最基本的输入。程序和数据通常需要长期存储,磁盘是最常用的存储设备。BIOS为这两种外设的I/O提供了最基本的中断例程。
int 9 中断例程对键盘输入的处理
键盘输入将引发9号中断,BIOS提供了int 9中断例程。CPU在9号中断发生后,执行int 9中断例程,从 60h 端口读出扫描码,并将其转换为相应的ASCII码或状态信息,存储在内存的指定空间(键盘缓冲区或状态字节)中。键盘缓冲区中有16个字单元,可以存储15个按键的扫描码对应的ASCII码。
使用int 16h 中断例程读取键盘缓冲区
BIOS提供了 int 16h 中断例程供程序员调用。int 16h中断例程中包含的一个最重要的功能是从键盘缓冲区中读取一个键盘输入,该功能的编号为0。
指令从键盘缓冲区中读取一个键盘输入,并将其从缓冲区中删除:
mov ah, 0
int 16h
结果:(ah) = 扫描码, (al) = ASCII码
int 16h 中断例程的0号中断功能:
1)检测键盘缓冲区中是否有数据;
2)没有则继续做第一步;
3)读取缓冲区第一个字单元中的键盘输入;
4)将读取的扫描码送入ah,ASCII码送入al;
5)将已读的键盘输入从缓冲区中删除。
BIOS的int 9 中断例程和int 16h中断是一对相互配合的程序:
int 9中断例程向键盘缓冲区中写入。int 9中断例程是在有按键按下的时候向键盘缓冲区中写入数据。
int 16h中断例程从键盘缓冲区中读出。int 16h中断例程是在应用程序对其进行调用的时候,将数据从键盘缓冲区中读出。
例:接受用户输入的数据:输入“r”,将屏幕上的字符设置为红色,“g”将屏幕上的字符设置为绿色,“b”将屏幕上的字符设置为蓝色
assume cs:code
code segment
start: mov ah, 0
int 16h
mov ah, 1
cmp al, 'r'
je red
cmp al, 'g'
je green
cmp al, 'b'
je blue
jmp short sret
red: shl ah, 1
green: shl ah, 1
blue: mov bx, 0b800h
mov es, bx
mov bx, 1
mov cx, 2000
s: and byte ptr es:[bx], 11111000b
or es:[bx], ah
add bx, 2
loop s
sret: mov ax, 4c00h
int 21h
code ends
end start
字符串的输入
字符长输入程序:
1)在输入的同时需要显示这个字符串
2)一般在输入回车符后,字符串输入结束
3)能够删除已经输入的字符
子程序:
(dh)、(dl)= 字符串在屏幕上显示的行、列位置
ds:si指向字符串的存储空间,字符串以0为结尾。
1)字符的输入和删除
每个新输入的字符都存储在前一个输入的字符之后,而删除时从最后面的字符进行的。
字符的输入和输出是按照栈的访问规则进行的,即后进先出。即,可以用栈的方式来管理字符串的存储空间,也就是说,字符串的存储空间实际上是一个字符栈。字符栈中的所有字符,从栈底到栈顶,组成一个字符串。
2)在输入回车符后,字符串输入结束
输入回车符后,可以在字符串中加入0,表示字符串结束。
3)在输入的同时需要显示这个字符串。
每次有新的字符输入和删除一个字符的时候,都应该重新显示字符串,即从字符栈的栈底到栈顶,显示所有的字符。
4)程序的处理过程
(1)调用 int 16h 读取键盘输入
(2)如果是字符,进入字符栈,显示字符栈中的所有字符:继续执行(1)
(3)如果是退格键,从字符栈中弹出一个字符,显示字符栈中的所有字符:继续执行(1)
(4)如果是Enter键,向字符栈中压入0,返回。
子程序:字符栈的入栈、出栈和显示
参数说明:
(ah)= 功能号,0表示入栈,1出栈,2显示
ds:si指向字符栈空间
0号:(al)= 入栈字符
1号:(al)= 返回的字符
2号:(dh)、(dl)= 字符串在屏幕上显示的行、列位置。
charstack: jmp short charstart
table dw charpush,charpop,charshow
top dw 0
charstart: push bx
push dx
push di
push es
cmp ah, 2
ja sret
mov bl, ah
mov bh, 0
add bx, bx
jmp word ptr table[bx]
charpush: mov bx, top
mov ds:[si][bx], al
inc top
jmp sret
charpop: cmp top, 0
je sret
dec top
mov bx, top
mov al, ds:[si][bx]
jmp sret
charshow: mov bx, 0b800h
mov es, bx
mov al, 160
mov ah, 0
mul dh
mov di, ax
add di, ax
mov dh, 0
add di, dx
mov bx, 0
charshows: cmp bx, top
jne noempty
mov byte ptr es:[di], ' '
jmp sret
noempty: mov al, [si][bx]
mov es:[dl], al
mov byte ptr es:[di + 2], ' '
inc bx
add di, 2
jmp charshows
sret: pop es
pop di
pop dx
pop bx
ret
完整程序:
getstr: push ax
getstrs: mov ah, 0
int 16h
cmp al, 20h
jb nochar //ASCII码小于20h,说明不是字符
mov ah, 0
call charstack
mov ah, 2
call charstack
jmp getstrs
nochar: cmp ah, 0eh //退格键扫描码
je backspace
cmp ah, lch //Enterj键
je enter
jmp getstrs
backspacr:
mov al, 1
call charstack
mov ah, 2
call charstack
jmp getstrs
enter: mov al, 0
mov ah, 0
call charstack
mov ah, 2
call charstack
pop ax
ret
int 13h 中断例程对磁盘进行读写
磁盘的实际访问由磁盘控制器进行,我们可以通过控制磁盘控制器来访问磁盘。只能以扇区为为单位进行读写。在读写扇区的时候,要给出面号、磁道号和扇区号。面号和磁道号从0开始,而扇区号从1开始。
3.5英寸软盘分为上下两面,每面有80个磁道,每个磁道又分为18个扇区,每个扇区的大小为512字节。
则:2面 * 80磁道 * 18扇区 * 512字节 = 1.44MB
BIOS提供的访问磁盘的中断例程为 int 13h。读取0面0道1扇区的内容到0:200的程序:
mov ax, 0
mov es, ax
mov bx, 200h
mov al, 1
mov ch, 0
mov cl, 1
mov dl, 0
mov dh, 0
mov ah, 2
int 13h
入口参数:
(ah) = int 13h 的功能号(2表示读扇区)
(al) = 读取的扇区数
(ch) = 磁道号
(cl) = 扇区号
(dh) = 磁头号(对于软盘即面号,因为一个面用一个磁头来读写)
(dl) = 驱动器号
1)软盘从0开始,0:软盘A,1:软驱B;
2)硬盘从80h开始,80h:硬盘C,81h:硬盘D
es:bx 指向接受从扇区读入数据的内存区
返回参数:
操作成功:(ah) = 0,(al) = 读入的扇区数
操作失败:(ah) = 出错代码
将 0:200 中的内容写入0面0道1扇区。
mov ax, 0
mov es, ax
mov bx, 200h
mov al, 1
mov ch, 0
moc cl, 1
mov dl, 0
mov dh, 0
mov ah, 3
int 13h
入口参数:
(ah) = int 13h 的功能号(3表示读扇区)
(al) = 读取的扇区数
(ch) = 磁道号
(cl) = 扇区号
(dh) = 磁头号(对于软盘即面号,因为一个面用一个磁头来读写)
(dl) = 驱动器号
1)软盘从0开始,0:软盘A,1:软驱B;
2)硬盘从80h开始,80h:硬盘C,81h:硬盘D
es:bx 指向接受从扇区读入数据的内存区
返回参数:
操作成功:(ah) = 0,(al) = 读入的扇区数
操作失败:(ah) = 出错代码
注意:直接向磁盘区写入数据是很危险的,很有可能覆盖掉重要的数据。如果向软盘的0面0道1扇区中写入了数据,要使软盘在现有的操作系统下可以使用,必须要重新格式化。在使用int 13h中断例程时一定要注意驱动器号是否正确,千万不要随便对硬盘中的扇区进行写入。
编程:将当前屏幕的内容保存在磁盘上。
分析:1屏的内容占4000个字节,需要8个扇区,用0面0道的1-8扇区存储显存中的内容。
assume cs:code
code segment
start: mov ax, 0b800h
mov es, ax
mov bx, 0
mov al, 8
mov ch, 0
mov cl, 1
mov dl, 0
mov dh, 0
mov ah, 3
int 13h
mov ax, 4c00h
int 21h
code ends
end start