zoukankan      html  css  js  c++  java
  • 第十六章 直接定址表(需要回头学)

    16.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 ,长度为2 字节。

    下面的指令会引起编译错误: mov al,b
    为什么?
    没错啦,聪明的你肯定意识到了:因为b代表的内存单元是字单元,而al 是8 位寄存器。

    因此,如果我们将程序中的指令:add b,ax ,写为add b,al,将出现同样的编译错误。

    对于程序中的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]

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


    16.2 在其他段中使用数据标号
    一般来说,我们不会在代码段中定义数据,而是将数据定义到其他段中。

    在其他段中,我们也可以使用数据标号来描述存储数据的单元的地址和长度。

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

    下边程序将data 段中 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 段相联。

    在程序中,我们用 ds寄存器和 data 段相联,则编译器对相关指令的编译如下:
    指令: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操作符,功能为取得某一标号的段地址。


    16.3 直接定址表
    这一节课,我们将讨论用“查表”的方法编写相关程序的技巧。

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

    一个字节需要用两个十六进制数码来表示,所以,子程序需要在屏幕上显示两个ASCII 字符。

    我们当然要用“0”、“1”、“2”、“3”、“4”、“5” 、“6” 、“7” 、“8” 、“9” 、“A”、“B”、“C”、“D”、“E”、“F”这16个字符来显示十六进制数码。

    我们可以将一个byte的高4位和低4 位分开,分别用它们的值得到对应的数码字符。比如 2Bh ,我们可以得到高4 位的值为2,低4 位的值为11。

    那么我们如何用这两个数值得到对应的数码字符“2”和“B”呢?我知道,我们一看就知道是 2和 B ,但CPU 是 SB, 它不懂,它只懂 1 和 0。
    最简单的办法就是一个一个地比较,如下:
    如果数值为 0,则显示“0”;
    如果数值为 1,则显示“1”;
    :
    如果数值为15,则显示“F”;
    我们可以看出,这样做,程序中要使用多条比较、转移指令。程序将比较长,混乱。

    显然,我们希望能够在数值0~15和字符“0 ~ F”之间找到一种映射关系。这样我们用015间的任何数值,都可以通过这种映射关系直接得到“0”“F”中对应的字符。

    数值09和字符“0”“9”之间的映射:
    数值 + 30h = 对应字符的ASCII值:
    0+30h = “0”的ASCII值
    1+30h = “1”的ASCII值
    但是,1015和“A”“F”之间的映射关系是:
    数值+37h=对应字符的ASCII值:
    10+37h=“A”的ASCII值
    11+37h=“B”的ASCII值
    12+37h=“C”的ASCII值
    :
    可见,我们是利用数值和字符之间的这种原本存在的映射关系,通过高 4 位和低4 位值得到对应的字符码。但我们发现一个问题……

    但是由于映射关系的不同,我们在程序中必须进行一些比较,对于大于 9 的数值,我们要用不同的计算方法。
    这样做,虽然使程序得到了简化。但是,如果我们希望用更简捷的算法,就要考虑用同一种映射关系从数值得到字符码。
    所以,我们就不能利用09和“0”“9” 之间与 10~15 和 “A” ~“F ” 之间原有的映射关系。因为他们有两个映射关系,不满足我们的条件。

    具体的做法是,我们建立一张表,表中依次存储字符“0”“F”,我们可以通过数值015直接查找到对应的字符。

    子程序实现代码:p282.asm
    assume cs:code

    code segment
    start:
    mov al,0eh

        call showbyte
    
        mov ax,4c00h
        int 21h
    

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

    code ends
    end start

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

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

    这样做的目的一般来说有三个:
    (1)为了算法的清晰和简洁。
    (2)为了加快运算速度。
    (3)为了使程序易于扩充。
    在刚刚的子程序中,我们更多的是为了算法的清晰和简洁,而采用了查表的方法。
    下面我们来看一下,为了加快运算速度而采用查表的方法的情况。

    任务二:编写一个子程序,计算sin(x),x∈{0°,30°,60°,90°,120°,150°,180°},并在屏幕中间显示计算结果。

    例如sin(30) 的结果显示为“0.5”。
    我们可以利用麦克劳林公式来计算sin(x)。
    x 为角度,麦克劳林公式中需要代入弧度,则:

    y=x/180*3.1415926

    可以看出,计算sin(x)需要进行多次乘法和除法。乘除是非常费时的运算,它们的执行时间大约是加法、比较等指令的5倍。
    那么我们如何才能够不做乘除而计算sin(x)呢?
    我们看一下需要计算的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(x)的值。

    我们用 ax 向子程序传递角度。
    assume cs:code

    code segment
    start:
    mov al,60

        call showsin
    
        mov ax,4c00h
        int 21h
    

    showsin:
    jmp short show

        table dw ag0,ag30,ag60,ag90,ag120,ag150,ag180	;字符串偏移地址表
        ag0      db '0',0			;sin(0)对应的字符串“0”
        ag30     db '0.5',0			;sin(0)对应的字符串“0.5”
        ag60     db '0.866',0			;sin(0)对应的字符串“0.866”
        ag90     db '1',0			;sin(0)对应的字符串“1”
        ag120    db '0.866',0			;sin(0)对应的字符串“0.866”
        ag150    db '0.5',0			;sin(0)对应的字符串“0.5”
        ag180    db '0',0			;sin(0)对应的字符串“0”
    

    show: push bx
    push es
    push si

        mov bx,0b800h
        mov es,bx
    

    ;以下用角度值/30 作为相对于table的偏移量,取得对应的字符串的偏移地址,放在bx中
    mov ah,0
    mov bl,30
    div bl
    mov bl,al
    mov bh,0
    add bx,bx
    mov bx,table[bx]

    ;以下显示sin(x)对应的字符串
    mov si,16012+402
    shows:
    mov ah,cs:[bx]
    cmp ah,0
    je showret
    mov es:[si],ah
    inc bx
    add si,2
    jmp shows

    showret:
    pop si
    pop es
    pop bx
    ret

    code ends

    end start
    在上面的子程序中,我们在角度值x和表示 sin(x) 的字符串集合table 之间建立的映射关系为:

    以角度值 /30 为table 表中的偏移,可以找到对应的字符串的首地址。
    编程的时候要注意程序的容错性,即对于错误的输入要有处理能力。

    在上面的子程序中,我们还应该在加上对提供的角度值是否超范围的检测。
    如果提供的角度值不在合法的集合中,程序将定位不到正确的字符串,出现错误。

    对于角度值的检测,大家请自行完成。

    上面的两个子程序中,我们将通过给出的数据进行计算或比较而得到结果的问题,转化为用给出的数据作为查表的依据,通过查表得到结果的问题。
    具体的查表方法 ,是用查表的依据数据 ,直接计算出所要查找的元素在表中的位置。

    像这种可以通过依据数据,直接计算出所要找的元素的位置的表,我们称其为:直接定址表。

    我们可以在直接定址表中存储子程序的地址,从而方便地实现不同子程序的调用。


    16.4 程序入口地址的直接定址表
    我们看下面的问题: 实现一个子程序setscreen ,为显示输出提供如下功能:
    (1)清屏。
    (2)设置前景色。
    (3)设置背景色。
    (4)向上滚动一行

    那么入口参数如何设置呢?
    入口参数说明:
    (1)用ah 寄存器传递功能号:
    0 表示清屏,
    1表示设置前景色,
    2 表示设置背景色,
    3 表示向上滚动一行;

    (2)对于2、3号功能,用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 个子程序,请读者根据编程思想,自行读懂下面的程序。

    程序功能分别实现讲解:gogogo ---->>>

    功能子程序1:清屏
    sub1: push bx
    push cx
    push es
    mov 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

    功能子程序2:设置前景
    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
    

    功能子程序3:设置背景色
    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 sub2s

    pop es
    pop cx
    pop bx
    ret 
    

    ;功能子程序4:向上滚动一行
    sub4:
    push cx
    push si
    push di
    push es
    push ds

    mov si,0b800h
    mov es,si
    mov ds,si
    mov si,160			;ds: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 es:[160*24+si],' ' ;最后一行清空
    add si,2
    loop sub4s1

    pop ds
    pop es
    pop di
    pop si
    pop cx
    ret ;sub4 ends
    

    我们可以将这些功能子程序的入口地址存储在一个表中,它们在表中的位置和功能号相对应。

    对应的映射关系为:
    功能号*2=对应的功能子程序在地址表中的偏移
    ;编程:实现一个子程序setscreen,为显示输出提供如下功能:
    ;(1) 清屏。
    ;(2) 设置前景色。
    ;(3) 设置背景色。
    ;(4) 向上滚动一行。
    ;
    ;入口参数说明:
    ;(1) 用 ah 寄存器传递功能号:0 表示清屏,1表示设置前景色,2 表示设置背景色,3 表示向上滚动一行;
    ;(2) 对于2、3号功能,用 al 传送颜色值,(al) ∈{0,1,2,3,4,5,6,7}

    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
    iret

    ;功能子程序1:清屏
    sub1:
    push bx
    push cx
    push es
    mov 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 ;sub1 ends

    ;功能子程序2:设置前景色
    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 ;sub2 ends
    

    ;功能子程序3:设置背景色
    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 sub2s

    pop es
    pop cx
    pop bx
    ret ; sub3 ends
    

    ;功能子程序4:向上滚动一行
    sub4:
    push cx
    push si
    push di
    push es
    push ds

    mov si,0b800h
    mov es,si
    mov ds,si
    mov si,160			;ds: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 es:[160*24+si],' ' ;最后一行清空
    add si,2
    loop sub4s1

    pop ds
    pop es
    pop di
    pop si
    pop cx
    ret ;sub4 ends
    

    当然,我们也可以将子程序setscreen如下实现:
    ;编程:实现一个子程序setscreen,为显示输出提供如下功能:
    ;(1) 清屏。
    ;(2) 设置前景色。
    ;(3) 设置背景色。
    ;(4) 向上滚动一行。
    ;
    ;入口参数说明:
    ;(1) 用 ah 寄存器传递功能号:0 表示清屏,1表示设置前景色,2 表示设置背景色,3 表示向上滚动一行;
    ;(2) 对于2、3号功能,用 al 传送颜色值,(al) ∈{0,1,2,3,4,5,6,7}

    setscreen:
    cmp ah,0
    je do1
    cmp ah,1
    je do2
    cmp ah,2
    je do3
    cmp ah,3
    je do4
    jmp short sret

    do1:
    call sub1
    jmp short sret

    do2:
    call sub2
    jmp short sret

    do3:
    call sub3
    jmp short sret

    do4:
    call sub4

    sret:
    iret

    ;功能子程序1:清屏
    sub1:
    push bx
    push cx
    push es
    mov 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 ;sub1 ends

    ;功能子程序2:设置前景色
    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 ;sub2 ends
    

    ;功能子程序3:设置背景色
    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 sub2s

    pop es
    pop cx
    pop bx
    ret ; sub3 ends
    

    ;功能子程序4:向上滚动一行
    sub4:
    push cx
    push si
    push di
    push es
    push ds

    mov si,0b800h
    mov es,si
    mov ds,si
    mov si,160			;ds: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 es:[160*24+si],' ' ;最后一行清空
    add si,2
    loop sub4s1

    pop ds
    pop es
    pop di
    pop si
    pop cx
    ret ;sub4 ends
    

    用根据功能号查找地址表的方法,程序的结构清晰,便于扩充。

    如果加入一个新的功能子程序,那么只需要在地址表中加入它的入口地址就可以了。


  • 相关阅读:
    Dot Net WinForm 控件开发 (七) 为属性提下拉式属性编辑器
    WinForm 程序的界面多语言切换
    c#遍历HashTable
    Dot Net WinForm 控件开发 (三) 自定义类型的属性需要自定义类型转换器
    Dot Net WinForm 控件开发 (六) 为属性提供弹出式编辑对话框
    Dot Net WinForm 控件开发 (一) 写一个最简单的控件
    Dot Net WinForm 控件开发 (四) 设置属性的默认值
    Dot Net WinForm 控件开发 (二) 给控件来点描述信息
    Dot Net WinForm 控件开发 (八) 调试控件的设计时行为
    Dot Net WinForm 控件开发 (五) 复杂属性的子属性
  • 原文地址:https://www.cnblogs.com/poli/p/4731549.html
Copyright © 2011-2022 走看看