(三十三)直接定址表--汇编笔记

汇编语言

Posted by YiMiTuMi on May 15, 2020

描述了单元长度的标号

例:将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

紫云英 – 没有爱的期待