汇编中的直接定址表

po1.写在前面

前面已经完全的介绍了CPU的中断,内中断,外中断,然后修改了一些内置的中断的程序。

2.本篇博客的概述

在这里插入图片描述

3.直接定址表

3.1描述了单元长度的标号

我们一直在代码段中使用标号来标记指令、数据、段的起始地址。比如,下面的程序将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,长度为两个字节。

下面的指令会引起编译错误:

mov al,b

因为b代表的内存单元是子单元,而al是8位寄存器。

对于程序中的"a db 1,2,3,4,5,6,7,8"

指令:   mov al,a[si]
相当于: mov al,cs:[0][si]

指令:   mov al,a[3]
相当于: mov al,cs:0[3]

指令:   mov al,a[bx+si+3]
想当于: mov al,cs:0[bx+si+3]

可见,使用这种包含单元长度的标号,可以使我们以简洁的形式访问内存中的数据。以后,我们将这种标号称为数据标号,它标记了存储数据的单元的地址和长度。它不同于仅仅表示地址的地址标号。

3.2在其他段中使用数据标号

一般来说,我们不在代码段中定义数据,而是将数据定义到其他段中。在其他段中,我们也可以使用数据标号来描述存储数据的单元的地址和长度。

注意,在后面加有":"的地址标号,只能在代码段中抵用,不能在其他段中使用。

先看下面的代码:

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指令将段寄存器和某个段相联系,段寄存器中就会真的存放该段的地址。

指令:   mov al,a[si]
编译为: mov al,[si+0]

指令:   add b,ax
编译为: add [8],ax

因为这些实际编译出的指令,都默认所访问单元的段地址在ds中,而实际要访问的段为data,所以若要访问正确,在这些指令执行前,ds中必须为data段地址。则我们在程序中使用指令

mov ax,data
mov ds,ax

设置ds指向data段

可以将标号当做数据来定义,此时,编译器将标号所表示的地址当做数据的值。

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操作符,功能为取得某一标号的段地址。

3.3直接定址表

编写子程序,以十六进制的形式在屏幕中间显示给定的字节型数据。

分析:一个字节需要用两个十六进制数码来表示,所以子程序需要在屏幕上显示两个ASCII字符。我们当然要用’0’,‘1’,‘2’,‘3’,‘4’,‘5’,‘6’,‘7’,‘8’,‘9’,‘A’,‘B’,‘C’,‘D’,‘E’,'F’这16个字符来显示16进制数码。

我们可以将一个字节的高4位和低4位分开,分别用它们的值得到对应的数码字符。为了解决多次比较的问题,我们应该建立一张表,表中依次存储字符’0’‘F’,我们可以通过数值015直接查找对应的字符。

子程序如下:

;用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,ob800h
          mov es,bx
          mov es:[160*12+40*2],ah
          
          mov bl,al
          mov bh,0
          mov al,table[bx];用低4位的值作为相对于table的偏移,取得对应的字符
          
          mov es:[160*12+40*2+2],al
          
          pop es
          pop bx
          
          ret

可以看出,在子程序中,我们在数值015和字符’0’'F’之间建立的映射关系为:以数值N为table表中的偏移,可以找到对应的字符。

利用表,在两个数据集合之间建立一种映射关系,使我们可以用查表的方法根据的方法根据给出的数据得到其在另一个集合中的对应数据。这样做的目的一般来说有以下3个。

  1. 为了算法的清晰和简洁
  2. 为了加快运算速度
  3. 为了使程序易于扩充

具体的查表方法,是用查表的依据数据,直接计算出所要查找的元素在表中的位置。像这种可以通过依据数据,直接计算出所要查的元素的位置的表,我们称其为直接定址表。

3.4程序入口地址的直接定址表

我们可以在直接定址表中存储子程序的地址,从而方便地实现不同子程序的调用。先来看下面的问题:

实现一个子程序setscreen,为显示输出提供如下的功能。

  1. 清屏
  2. 设置前景色
  3. 设置背景色
  4. 向上滚动一行。

入口参数说明如下:

  1. 用ah寄存器传递功能号:0表示清屏,1表示设置前景色,2表示设置背景色,3表示向上滚动一行;
  2. 对于1,2号功能,用al传递颜色值,(al)属于{0,1,2,3,4,5,6,7}

下面我们讨论一下各种功能如何实现。

  1. 清屏:将显存中当前屏幕中的字符设置为空字符
  2. 设置前景色:设置显存中当前屏幕中处于奇地址的属性字节的第0、1、2位
  3. 设置背景色:设置显存中当前屏幕中处于奇地址的属性字节的第4、5、6位
  4. 向上滚动一行:依次将第n+1行的内容复制到第n行处;最后一行为空。

我们将这4个功能分别写成4个子程序。具体的如下:

sub1: push bx
      push cx
      push es
      push bx,0b800h
      mov es,bx
      mov bx,0
      mov cx,2000
sub1s:mov byte ptr es:[bx],' '
      add bx,2
      loop sub1s
      pop es
      pop cx
      pop bx
      ret
      
 sub2:push bx
      push cx
      push es
      
      mov bx,0b800h
      mov es,bx
      mov bx,1
      mov cx,2000
sub2s:and byte ptr es:[bx],11111000b
      or es:[bx],al
      add bx,2
      loop sub2s
      
      pop es
      pop cx
      pop bx
      ret
      
 sub3:push bx
      push cx
      push es
      mov cl,4
      shl al,cl
      mov bx,0b800h
      mov es,bx
      mov bx,1
      mov cx,2000
sub3s:and byte ptr es:[bx],10001111b
      or es:[bx],al
      add bx,2
      loop sub3s
      pop es
      pop cx
      pop bx
      ret
 
sub4: push cx
      push si 
      push di
      push es
      push ds
      
      mov si,0b800h
      mov es,si
      mov ds,si
      mov si,160 ;di:si指向第n+1行
      mov di,0 ;es:di指向第n行
      cld
      mov cx,24;共复制24行
      
sub4s:push cx
      mov cx,160
      rep movsb ;复制
      pop cx
      loop sub4s
      
      mov cx,80
      mov si,0
sub4s1:mov byte ptr [160*24+si],' ';最后一行清空
       add si,2
       loop sub4s1
       
       pop ds
       pop es
       pop di
       pop si
       pop cx
       ret

我们可以将这些功能子程序的入口地址存储在一个表中,它们在表中的位置和功能号相对应。对应的关系为:功能号*2=对应的功能子程序在地址表中的偏移。程序如下:

setscreen: jmp short set
  
  table dw sub1,sub2,sub3,sub4
  
      set: push bx
           cmp ah,3 ;判断功能号是否大于3
           ja sret
           mov bl,ah
           mov bh,0
           add bx,bx ;根据ah中的功能号计算对应子程序在table表中的偏移
           
           call word ptr table[bx] ;调用对应的功能子程序
      sret:pop bx
           ret

4.使用BIOS进行键盘输入和磁盘读写

4.1int9中断例程对键盘输入的处理

前面我们已经讲过,键盘输入将引发9号中断。CPU在9号中断发生后,执行int9中断例程,从60h端口读出扫描码,并将其转换为相应的ASCII码和状态信息,存储在内存的指定空间中。

一般的键盘输入,在CPU执行完int 9中断例程后,都放到了键盘缓冲区中。键盘缓冲区中16个字单元,可以存储15个按键的扫描码和对应的ASCII码。

下面,我们通过下面的几个键:

A、B、C、D、E、Shift_A、A

的输入过程,简要地看一下int 9 中断例程对键盘输入的处理方法。

  1. 初始状态下,没有键盘输入,键盘缓冲区空,此时没有任何元素。

    在这里插入图片描述

  2. 按下A键,引发键盘中断,CPU执行int 9中断例程,从60h端口读出A键的通码;然后检测状态字节,看看是否有Shift、Ctrl等切换键按下;发现没有切换键按下,则将A的扫描码1eh和对应的ASCII码,即字母’a’的ASCII码61h,写入键盘缓冲区。缓冲区的字节单元中,高位字节存储扫描码,低位字节存储ASCII码。此时缓冲区的内容如下:

    在这里插入图片描述

  3. 按下B键,引发键盘中断,CPU执行int 9中断例程,从60h端口读出B键的通码;然后检测状态字节,看看是否有切换键按下;发现没有切换键按下,将B键的扫描ma30h和对应的ASCII码,即字母’b’的ASCII码62h,写入键盘缓冲区。此时缓冲区中的内容如下:

在这里插入图片描述

  1. 按下C、D、E键后,缓冲区中的内容如下:

    在这里插入图片描述

  2. 按下左Shift键,引发键盘中断;int9 中断例程接收左Shift键的通码,设置0040:17处的状态字节的第1位位1,表示左Shift键按下。

  3. 按下A键,引发键盘中断;CPU执行int 9中断例程,从60h端口读出A键的通码;检测状态字节,看看是否有切换键按下;发现左Shift键被按下,则将A键的扫描码1Eh和Shift_A对应的ASCII码,即字母’A’的ASCII码41h,写入键盘缓冲区。此时缓冲区中的内容如下:

    在这里插入图片描述

  4. 松开左Shift键,引发键盘中断;int 9中断例程接收左Shift键的断码,设置0040:17处的状态字节的第1位为0,表示左Shift键松开。

  5. 按下A键,引发键盘中断,CPU执行int 9中断例程,从60h端口读出A键的通码;然后检测状态字节,看看是否有切换键按下;发现没有切换键按下,则将A键的扫描码1Eh和A对应ASCII码,即字母"a"的ASCII码61h,写入键盘缓冲区。此时缓冲区中的内容如下:

    在这里插入图片描述

4.2使用int 16h中断例程读取键盘缓冲区

BIOS提供了int 16h中断例程供程序员调用。int 16h中断例程中包含的一个最重要功能是从键盘缓冲区中读取一个键盘输入,该功能的编号为0。下面的指令从键盘缓冲区中去除一个键盘输入,并且将其从缓冲区中删除:

mov ah,0
int 16h

结果(ah)=扫描码,(al)=ASCII码

下面我们接着上一节中的键盘输入的过程,看一下int 16h如何读取键盘缓冲区

  1. 执行

    mov ah,0
    int 16h
    

    后,缓冲区中的内容如下:

    在这里插入图片描述

    ah中的内容为1Eh,al中的内容为61h

  2. 执行

    mov ah,0
    int 16h
    

    后,缓冲区中的内容如下:

    在这里插入图片描述

    ah中的内容为30h,al中的内容为62h

  3. 执行

    mov ah,0
    int 16h
    

    后,缓冲区中的内容如下:

    在这里插入图片描述

    ah中的内容为2Eh,al中的内容为63h

  4. 执行了4次

    mov ah,0
    int 16h
    

    后,缓冲区为空

    在这里插入图片描述

    ah中的内容为1Eh,al中的内容给为61h

  5. 执行

    mov ah,0
    int 16h
    

    int 16h中断例程检测键盘缓冲区,发现缓冲区空,则循环等待,知道缓冲区有数据

  6. 按下A键后,缓冲区中的内容如下。

    在这里插入图片描述

  7. 循环等待的int 16h中断例程检测到键盘缓冲区中有数据,将其读出,缓冲区又为空

    在这里插入图片描述

    Ah中的内容为1Eh,al中的内容为61h。

总结下大致的功能如下:

  1. 检测键盘缓冲区中是否有数据
  2. 没有则继续做第1步
  3. 读取缓冲区第一个字单元中的键盘输入
  4. 将读取的扫描码送入ah,ASCII码送入al
  5. 将已读的键盘输入从缓冲区中删除

我们来编写一般的处理键盘输入的程序的时候,可以调用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

4.3字符串的输入

用户通过键盘的输入的通常不仅仅是当字符而是字符串。

最基本的字符串输入程序,需要具备下面的功能。

  1. 在输入的同时显示这个字符串;
  2. 一般在输入回车符后,字符串输入结束
  3. 能够删除已经输入的字符。

编写一个接受程收字符串输入的子程序,实现上面的3个基本功能,参数如下:

(dh)、(dl)=字符串在屏幕上显示的行、列位置;

ds:si指向字符串的存储空间,字符串以0位结尾符。

具体的代码如下:

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,1ch ;Enter键的扫描码
         je enter
         jmp getstrs
 
backspace:mov ah,1
          call charstack ;字符出栈
          mov ah,2
          call charstack ;显示栈中的字符
          jmp getstrs

    enter:mov al,0
          mov ah,0
          call charstack ;0入栈
          mov ah,2
          call charstack ;显示栈中的字符
          pop ax
          ret

4.4应用int 13h中断例程对磁盘进行读写

磁盘的实际访问由磁盘控制器进行,我们可以通过控制磁盘控制器来访问磁盘。只能以扇区为单位对磁盘进行读写。在读写扇区的时候,要给出面号、磁道号和扇区号。面号和磁道号从0开始,而扇区号从1开始。

如果我们通过直接控制磁盘控制器来访问磁盘,则需要涉及许多硬件细节。BIOS提供了对扇区进行读写的中断过程,这些中断例程完成了许多复杂的和硬件相关的工作。我们可以通过调用BIOS中断例程来访问磁盘。

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)=驱动器号

es:bx 指向接收从扇区读入数据的内存区

返回参数:

操作成功:(ah)=0,(al)=读入的扇区数

操作失败:(ah)=出错代码

5.写在最后

本篇博客主要介绍汇编中的直接定址表,以及一些BIOS的应用。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值