Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

汇编语言 第四版 王爽著(中) #46

Open
Ray-56 opened this issue Sep 3, 2021 · 0 comments
Open

汇编语言 第四版 王爽著(中) #46

Ray-56 opened this issue Sep 3, 2021 · 0 comments
Labels
StudyNotes 读书笔记

Comments

@Ray-56
Copy link
Owner

Ray-56 commented Sep 3, 2021

汇编语言 第四版 王爽著(中)

[TOC]

第 7 章 更灵活的定位内存地址的方法

7.1 and 和 or 指令

  • and 指令:逻辑与指令,按位进行与运算
    mov al,01100011B
    and al,00111011B
    ; al = 00100011B 得到的值
    
    通过该指令可将操作对象的相应位设为 0,其它位不变
    相应位都为 1,得 1
  • or 指令:逻辑或指令,按位进行或运算
    mov al,01100011B
    or  al,00111011B
    ; al = 01111011B 得到的值
    
    通过该指令可将操作对象的相应位设为 1,其它位不变
    相应位都为 0,得 0

其实这句话“通过该指令可将操作对象的相应位设为 0”不太理解

7.2 关于 ASCII 码

ASCII编码是世界上众多编码的其中一种

在文本编辑过程中,我们按下键盘中的 a 键,就会在屏幕中看到“a”的过程:

  1. 按下键盘的 a 键,这个按键信息被送入计算机,计算机用 ASCII 码的规则对其进行编码,将其转化为 61H 存储在内存的指定空间中
  2. 文本编辑软件从内存中取出 61H,将其送入显卡的内存中
  3. 工作在文本模式下的显卡,用 ASCII 码的规则解释显存中的内容,61H 被当作字符“a”,显卡驱动显示器,将字符“a”的图像画在屏幕上

显卡在处理文本信息的时候,是按照 ASCII 码的规则进行的。也就是说要想在显示器看到“a”,就要给显卡的显存中写入“a”的 ASCII 码,61H

7.3 以字符形式给出的数据

在汇编程序中,用'...'的方式指明数据是以字符的形式给出的,编译器将它们转化为相对应的 ASCII 码。如下面程序

assume cs:code,ds:data
data segment
    db 'unIX'
    db 'foRK'
data ends
code segment
start:  mov al,'a'
        mov bl,'b'
        mov ax,4c00h
        int 21h
code ends
end start

在上面程序中:

  • db 'unIX' 相当于 db 75h,6eh,49h,58h
  • db 'foRK' 相当于 db 66h,6fh,52h,4bh
  • mov al,'a' 相当于 mov al,61h
  • mov bl,'b' 相当于 mov bl,62h


从图中可以看到 ds=075a,所以程序是从 076aH 段开始的,data 段又是程序中的第一个段,所以 data 段的段地址为 076ah

程序从 DS 开始,前 256 个字节(100H)是 DOS 系统用来和程序通信用的,所以 075ah+100h=076aH 才是程序开始的地址

7.4 大小写转换的问题

在 codesg 中填写代码,将 datasg 中的第一个字符串转化为大写,第二个字符串转化为小写

assume cs:codesg,ds:datasg
datasg segment
    db 'BaSiC'
    db 'InfOrMaTiOn'
datasg ends
codesg segment

; ****** 写入程序开始 ******
start:  mov ax,datasg
        mov ds,ax           ; 设置 ds 指向 datasg 段
        
        mov bx,0            ; 设置 (bx)=0, ds:bx 指向'BsAiC'的第一个字母
        mov cx,5            ; 设置循环次数,因为'BsAiC'有 5 个字母
    s:  mov al,[bx]         ; 将 ASCII 码从 ds:bx 所指向的单元中取出
        and al,11011111B    ; 将 al 中的 ASCII 码的第 5 位,置为 0,变为大写字母
        mov [bx],al         ; 将转变后的 ASCII 码写回原单元
        inc bx              ; (bx) 加 1,ds:bx 指向下一个字母
        loop s

        mov bx,5            ; 设置 (bx)=5,ds:bx 指向 'InfOrMaTiOn' 的第一个字母
        mov cx,11           ; 设置循环次数,因为 'InfOrMaTiOn' 有 11 个字母
    s:  mov al,[bx]
        or al,00100000B     ; 将 al 中的 ASCII 码的第 5 位,置为 1,变为小写字母
        mov [bx],al
        inc bx
        loop s0
        
        mov ax,4c00h
        int 21h
; ****** 写入程序结束 ******

codesg ends
end start

由ASCII 码对应'A'=41h=01000001b'a'=61h=01100001b可得大小写之间十六进制相差 20h,二进制的第 5 位是否为 0,为 0 则大写,为 1 则小写

如果我们使用十六进制则还需要判断字母是否为大写或小写,所以使用二进制第 5 位与 and、or 的特性:

  • 将第一个字符串中的字母都用 and 来把第 5 位,置为 0
  • 将第二个字符串中的字母都用 or 来吧第 5 位,置为 1

and:相应位都为 1,得 1
or :相应位都为 0,的 0

7.5 [bx+idata]

在前面,我们用 [bx] 的方式来指明一个内存单元,还可以用一种更为灵活的方式来指明内存单元:[bx+idata],它的偏移地址为 (bx)+idata(也就是 bx 中的数值加上常量 idata)。

指令mov ax,[bx+200]的含义:

将一个内存单元的内存送入 ax,这个内存单元的长度为 2 个字节(字单元),存放一个字,偏移地址为 bx 中的数值加上 200,段地址在 ds 中。

数学化描述为:(ax)=((ds)*16+(bx)+200)

也可以写成:

mov ax,[200+bx]
mov ax,200[bx]
mov ax,[bx].200

7.6 用[bx+idata]的方式进行数据的处理

在 codesg 中填写代码,将 datasg 中定义第一个字符串转化为大写,第二个字符串转化为小写

assume cs:codesg,ds:datasg
datasg segment
    db 'BaSiC'
    db 'MinIX'
datasg ends

codesg segment
; ****** 填入代码开始 ******
start:  mov ax,datasg
        mov ds,ax
        mov bx,0
        mov cx,5
    s:  mov al,[bx]         ; 定位第一个字符串中的字符
        ; 上面也可以改成 mov al,0[bx]
        and al,11011111b
        mov [bx],al
        mov al,[5+bx]       ; 定位第二个字符串中的字符
        ; 上面也可以改成 mov al,5[bx]
        or al,00100000b
        mov [5+bx],al
        ; 上面也可以改成 mov 5[bx],al
        inc bx
        loop s
; ****** 填入代码结束 ******
codesg ends

end start

如果用高级语言,比如 C 语言来描述上面的程序,大致是这样:

char a[5]="BaSiC";
char b[5]="MinIX";

main () {
    int i;
    i = 0;
    do {
        a[i] = a[i] & 0xDF;
        b[i] = b[i] | 0x20;
    } while (i < 5);
}

通过比较可以看到,[bx+idata] 的方式为高级语言实现数组提供了便利机制

7.7 SI 和 DI

si(Source Index)和 di(Destination Index)是 8086CPU 中和 bx 功能相近的寄存器,si 和 di 不能够分成两个 8 位寄存器来使用。

; 下面三组指令功能相同
mov bx,0
mov ax,[bx]

mov si,0
mov di,0

mov ax,[si]
mov ax,[di]

; 下面三组指令功能也相同
mov bx,0
mov ax,[bx+123]

mov si,0
mov ax,[si+123]

mov di,0
mov ax,[di+123]

问题 7.2

用 si 和 di 实现将字符串'welcome to masm!'复制到它后面的数据区中

assume cs:codesg,ds:datasg

datasg segment
    db 'welcome to masm!'
    do '................'
datasg ends

分析:

编写程序大都是进行数据的处理,而数据在内存中存放,所以在处理数据前先搞清楚数据存储在什么地方,也就是内存地址。

要对 datasg 段中的数据进行复制,要进行复制数据的地址是 datasg:0。它要被复制到它后面的数据区,“welcome to masm!”从地址 0 开始存放,长度为 16 个字节,所以它后面的数据区的偏移地址为 16,就是字符串“................”存放的空间。

清楚了地址之后,就可以进行处理了,用 ds:si 指向要复制的源始字符串,用 ds:di 指向复制的目的空间,然后用一个循环来完成复制。代码如下:

codesg segment
start:  mov ax,datasg
        mov ds,ax
        mov si,0
        mov di,16
        
        mov cx,8
    s:  mov ax,[si]
        mov [di],ax
        add si,2
        add di,2
        loop s
        
        mov 4c00h
        int 21h
codesg ends
end start

在上面程序中,用 16 位寄存器进行内存之间的数据传送,一次复制 2 个字节,一共循环 8 次

问题 7.3

用更少的代码,实现 7.2 中的程序

分析:可以利用[bx(si或di)+idata]的方式,来使程序简洁

codesg segment
start:  mov ax,datasg
        mov ds,ax
        mov si,0
        mov cs,0
    s:  mov ax,0[si]
        mov 16[si],ax
        add si,2
        loop s
        
        mov 4c00h
        int 21h
codesg ends
end start

7.8 [bx+si]和[bx+di]

前面,我们用[bx(si或di)][bx(si或di)+idata]的方式来指明一个内存单元,还可以用更为灵活的方式:[bx+si][bx+di]

[bx+si]表示一个内存单元,它的偏移地址为(bx)+(si),也就是 bx 中的数值加上 si 中的数值。

指令mov ax,[bx+si]的含义如下:

将一个内存单元的内容送入 ax,这个内存单元的长度为 2 字节(字单元),存放一个字,偏移地址为 bx 中的数值加上 si 中的数值,段地址在 ds 中。

数学化的描述为:(ax)=((ds)*16+(bx)+(si))

也可以写成常用格式:mov ax,[bx][si]

7.9 [bx+si+idata]和[bx+di+idata]

[bx+si+idata] 表示一个内存单元,它的偏移地址为(bx)+(si)+idata,也就是 bx 中的数值加上 si 中的数值再加上 idata。

指令mov,[bx+si+idata]的含义:将一个内存单元的内容送入 ax,这个内存单元的长度为 2 字节(字单元),存放一个字,偏移地址为 bx 中的数值加上 si 中的数值再加上 idata,段地址在 ds 中。

数学化描述为:(ax)=((ds)*16+(bx)+(si)+idata)

常用格式:

  • mov ax,[bx+200+si]
  • mov ax,[200+bx+si]
  • mov ax,200[bx][si]
  • mov ax,[bx].200[si]
  • mov ax,[bx][si].200

7.10 不同的寻址方式的灵活应用

比较一下前面用到的几种定位内存地址的方法(可称为寻址方式),就可以发现:

  1. [idata] 用一个常量来表是内存地址,可用于直接定位一个内存单元
  2. [bx] 用一个变量表示内存地址,可用于间接定位一个内存单元
  3. [bx+idata] 用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元
  4. [bx+si] 用两个变量表示地址
  5. [bx+si+idata] 用两个变量和一个常量表示地址

问题 7.9

编程,将 datasg 段中的每个单词的前 4 个字母改为大写字母

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,datasg
        mov ds,ax
        mov bx,0            ; 作为基本偏移地址

        mov ax,stacksg
        mov ss,ax           ; 栈寄存器地址指向
        mov sp,16           ; 将 ss:sp 指向栈顶

        mov cx,4            ; 外层循环 4 次
    s0: push cx             ; 外层循环计数器保护起来,内存循环时也需要用到 cx
        mov si,0            ; si 作为每个字符串的偏移地址

        mov cx,4            ; 内层循环次数,修改前四个字母为大写
    s1: mov al,[bx+si+3]    ; 使用[bx+si+3]寻址方式,并送入 al
        add al,11011111b    ; 转换大写字母
        mov [bx+si+3],al    ; 回写内存
        inc si
        loop s1

        add bx,16           ; 4 个字符串长度一致,都是 16 字节,增量为 16
        pop cx              ; 将外层循环计数器恢复
        loop s0

        mov ax,4c00h
        int 21h
codesg ends
end start

分析:

dagasg 中数据存储结构,如图:

总结

这一章,主要讲解了更灵活的寻址方式的应用和一些编程方法,主要内存有:

  • 寻址方式[bx(或 si、di)+idata]、[bx+si(或 di)]、[bx+si(或 di)+idata]的意义和应用
  • 二重循环问题的处理
  • 栈的应用
  • 大小写转化的方法
  • and、or 指令

第 8 章 数据处理的两个基本问题

(1)处理的数据在什么地方?
(2)要处理的数据有多长?

这两个问题,在机器指令中必须给出明确或隐含的说明,否则计算机就无法工作。本章中针对 8086CPU 对着两个基本问题进行讨论

后面的课程中使用 reg 来表示一个寄存器,sreg 表示一个段寄存器

  • reg 的集合包括:ax、bx、cx、dx、ah、al、bh、bl、ch、cl、dh、dl、sp、bp、si、di
  • sreg 的集合包括:ds、ss、cs、es

8.1 bx、si、di 和 bp

前 3 个寄存器我们已经用过了,现在进行一下总结

(1)在 8086CPU 中,中有这 4 个寄存器可以用在“[...]”中来进行内存单元的寻址。比如下面指令都是正确的:

mov ax,[bx]
mov ax,[bx+si]
mov ax,[bx+di]
mov ax,[bp]
mov ax,[bp+si]
mov ax,[bp+di]

而下面的指令都是错误的:

mov ax,[cx]
mov ax,[ax]
mov ax,[dx]
mov ax,[ds]

(2)在[...]中,这 4 个寄存器可以单个出现,或只能以 4 种组合出现:

  • bx 和 si
  • bx 和 di
  • bp 和 si
  • bp 和 di

(3)只要在[...]中使用寄存器 bp,而指令中没有显性地给出段地址,段地址就默认在 ss 中。比如下面指令:

mov ax,[bp]             ; 含义:(ax)=((ss)*16+(bp))
mov ax,[bp+idata]       ; 含义:(ax)=((ss)*16+(bp)+idata)
mov ax,[bp+si]          ; 含义:(ax)=((ss)*16+(bp)+(si))
mov ax,[bp+si+idata]    ; 含义:(ax)=((ss)*16+(bp)+(si)+idata)

8.2 机器指令处理的数据在什么地方

绝大部分机器指令都是进行数据处理的指令,处理大致可分为 3 类:读取、写入、运算。

在机器指令着一层来讲,并不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。

指令在执行前,索要处理的数据可以在 3 个地方:CPU 内部、内存、端口(端口在后面的课程中进行讨论)

8.3 汇编语言中数据位置的表达

汇编语言中用 3 个概念来表达数据的位置

(1)立即数(idata)

对于直接包含在机器指令中的数据(执行前在 CPU 的指令缓冲器中),在汇编语言中称为:立即数,在汇编指令中直接给出。例如:

mov ax,1
add bx,2000h
or bx,00010000b
mov al,'a'

(2)寄存器

指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。例如:

mov ax,bx
mov ds,ax
push bx
mov ds:[0],bx
push ds
mov ss,ax
mov sp,ax

(3)段地址(SA)和偏移地址(EA)

指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出 EA,SA 在某个段寄存器中。

存放段地址的寄存器可以使默认的,例如:

; 下面指令段地址默认在 ds 中
mov ax,[0]
mov ax,[di]
mov ax,[bx,si+8]

; 下面指令段地址默认在 ss 中
mov ax,[bp]
mov ax,[bp+si+8]

; 下面指令,存放段地址的寄存器显性给出
mov ax,ds:[bp]
mov ax,es:[bx]
mov ax,cs:[bx+si+8]

8.4 寻址方式

当数据存放在内存中的时候,我们可以用多种方式来给定这个内存单元的偏移地址,这种定位内存单元的方法一般被称为寻址方式

8.5 指令要处理的数据有多长

8086CPU 的指令,可以处理两种尺寸的数据,byte 和 word。所以在机器指令中要指明,指令进行的是字操作还是字节操作。

  1. 通过寄存器名指令要处理的数据的尺寸
    ; 以下为字操作
    mov ax,1
    mov bx,ds:[0]
    mov ds,ax
    mov ds:[0],ax
    inc ax
    add ax,1000
    
    ; 以下为字节操作
    mov al,1
    mov al,bl
    mov al,ds:[0]
    mov ds:[0],al
    inc al
    add al,100
    
  2. 在没有寄存器名存在的情况下,用操作符X ptr指明内存单元的长度,X在汇编指令中可以为 word 或 byte
    ; 下面指令中,用 word ptr 指明了指令访问的内存单元是一个字单元
    mov word ptr ds:[0],1
    inc word ptr [bx]
    inc word ptr ds:[0]
    add word ptr [bx],2
    
    ; 下面指令中,用 byte ptr 指明了指令访问的内存单元是一个字节单元
    mov byte ptr ds:[0],1
    inc byte ptr [bx]
    int byte ptr ds:[0]
    add byte ptr [bx],2
    
  3. 其他方法。有些指令默认了访问的是字单元还是字节单元
    • 比如 push [1000h] 就不用指明,因为 push 指令只进行字操作

8.6 寻址方式的综合应用

下面通过一个问题来进一步讨论各种寻址方式的作用

关于 DEC 公司的一条记录(1982年)如下。

公司名称: DEC
总裁姓名: Ken Olsen
排    名: 137
收    入: 40(40 亿美元)
著名产品: PDP(小型机)

这些数据在内存中以图 8.1 所示的方式存放。

可以看到,这些数据被存放在 seg 段中从偏移地址 60h 起始的位置:

  • seg:60起始以 ASCII 字符的形式存储了 3 个字节的公司名称
  • seg:60+3起始以 ASCII 字符的形式存储了 9 个字节的总裁姓名
  • seg:60+0c起始存放了一个字型数据,总裁在富豪榜上的排名
  • seg:60+0e起始存放了一个字型数据,公司的收入
  • seg:60+10起始以 ASCII 字符形式存储了 3 个字节的产品名称

以上是该公司 1982 年的情况,到了 1988 年 DEC 公司的信息有了如下变化:

  1. Ken Olsen 在富豪榜上的排名已升至 38 位
  2. DEC 的收入增加了 70 亿美元
  3. 该公司的著名产品已变为 VAX 系列计算机

编程修改内存中的过时数据。分析一下要修改的内容为:

  1. 排名字段
  2. 收入字段
  3. 产品字段的(第一、二、三个字符)

从要修改的内容,可以逐步确定修改的方法:

  1. 要访问的数据是 DEC 公司的记录,所以,首先确定 DEC 公司记录的位置:
    R=seg:60
    
    确定了公司记录的位置后,下面就进一步确定要访问的内容在记录中的位置
  2. 确定排名字段在记录中的位置:0ch
  3. 修改 R+0ch 处的数据
  4. 确定收入字段在记录中的位置:0eh
  5. 修改 R+0eh 处的数据
  6. 确定产品字段在记录中的位置:10h
    要修改的产品字段的是一个字符串(或一个数组),需要访问字符串中的每一个字符。所以要进一步确定每一个字符在字符串中的位置
    1. 确定第一个字符在产品字段中的位置:P=0
    2. 修改 R+10h+P 处的数据:P=P+1
    3. 修改 R+10h+P 处的数据:P=P+1
    4. 修改 R+10h+P 处的数据

依据上面的分析,程序如下:

mov ax,seg
mov ds,ax
mov bx,60h                  ; 确定记录地址,ds:bx

mov word ptr [bx+0ch],38    ; 排名字段改为 38
add word ptr [bx+0eh],70    ; 收入字段增加 70

mov si,0                    ; 用 si 来定位产品字符串中的字符
mov byte ptr [bx+10h+si],'V'
inc si
mov byte ptr [bx+10h+si],'A'
inc si
mov byte ptr [bx+10h+si],'X'

用 C 语言来描述这个程序大致应该是这样的:

struct company {
    char cn[3];
    char hn[9];
    int pm;
    int sr;
    char cp[3];
};
struct company dec={"DEC", "Ken Olsen", 137, 40, "PDP"};

main() {
    int i;
    dec.pm=38;
    dec.sr=dec.sr+70;
    i=0;
    dec.cp[i]='V';
    i++;
    dec.cp[i]='A';
    i++;
    dec.cp[i]='X';
    return 0;
}

我们在按照 C 语言的风格,用汇编语言写一下这个程序,注意和 C 语言相关语句的对比:

mov ax,seg
mov ds,ax
mov bx,60h

mov word ptr [bx].0ch,38        ; C: dec.pm=38;
add word ptr [bx].0eh,70        ; C: dec.sr=dec.sr+70;

mov si,0                        ; C: i=0;
mov byte ptr [bx].10h[si],'V'   ; C: dec.cp[i]='V';
inc si                          ; C: i++;
mov byte ptr [bx].10h[si],'A'   ; C: dec.cp[i]='A';
inc si                          ; C: i++;
mov byte ptr [bx].10h[si],'X'   ; C: dec.cp[i]='X';

可以看到,8086CPU 提供的如[bx+si+idata]的寻址方式为结构化数据的处理提供了方便。使得我们可以在编程的时候,从结构化的角度去看待所要处理的数据。

从上面课程看到,一个结构化的数据包含了多个数据项,而数据项的类型又不相同,字型、字节型、数组(字符串)。一般来说,可以用[bx+idata+si]的方式来访问结构体中的数据。用 bx 定位整个结构体,用 idata 定位结构体中的某一个数据项,用 si 定位数组中的每个元素。

为此,汇编语言提供了更为贴切的书写方式,如:[bx].idata、[bx].idata[si]

8.7 div 指令

div 是除法指令,是用 div 做除法的时候应该注意以下问题:

  • 除数:有 8 位和 16 位两种,在一个 reg 或内存单元中
  • 被除数:默认放在 AX 或 DX 和 AX 中。
    • 如果除数为 8 位,被除数则为 16 位,默认在 AX 中存放
    • 如果除数为 16 位,被除数则为 32 位,在 DX 和 AX 从存放,DX 存放高 16 位,AX 存放低 16 位
  • 结果:
    • 如果除数为 8 位,则 AL 存储除法操作的商,AH 存储除法操作的余数
    • 如果除数为 16 位,则 AX 存储除法操作的商,DX 存储除法操作的余数

格式如下:

div reg
div 内存单元

现在,我们可以用多种方法来表示一个内存单元了,比如下面例子:

div byte ptr ds:[0]
; 含义:  (al)=(ax)/((ds)*16+0) 的商
;       (ah)=(ax)/((ds)*16+0) 的余数

div word ptr es:[0]
; 含义:  (ax)=[(dx)*10000H+(ax)]/((es)*16+0) 的商
;       (ah)=[(ds)*10000H+(ax)]/((es)*16+0) 的余数

div byte ptr [bx+si+8]
; 含义: (al)=(ax)/((ds)*16+(bx)+(si)+8) 的商
;      (ah)=(ax)/((ds)*16+(bx)+(si)+8) 的余数

数学中[]中括号表示为:一个式子中有了小括号,再要用括号的话,外面就要用中括号


编程,利用除法指令计算 10001/100

分析:被除数 100001 大于 65536,不能用 ax 寄存器存放,所以只能用 dx 和 ax 两个寄存器联合存放,也就是要进行 16 位的除法。除数 100 小于 255,可以在一个 8 位寄存器中存放,但是,因为被除数是 32 位,除数应为 16 位,所以用一个 16 位寄存器来存放除数 100.

因为要分别为 dx 和 ax 赋 100001 的高 16 位值和低 16 位值,所以先将 100001 表示为 16 进制形式:186A1H

mov dx,1
mov ax,86a1h    ; (dx)*10000H+(ax)=100001
mov bx,100
div bx

程序执行后,(ax)=03e8h=1000,(dx)=1 余数为 1。


编程,利用除法指令计算 1001/100

分析:被除数 1001 可用 ax 寄存器存放,除数 100 可用 8 位寄存器存放,也就是说,要进行 8 位的除法。程序如下:

mov ax,1001
mov bl,100
div bl

程序执行后,(al)=0ah=10,(ah)=1 余数为 1

伪指令 dd

前面我们用 db 和 dw 定义字节型数据和字型数据。dd 用来定义 dword(doubleword,双字)型数据的。

8.9 dup

dup 是一个操作符,在汇编语言中同 db、dw、dd 等一样,也是由编译器识别处理的符号。它是和 db、dw、dd 等数据定义伪指令配合使用的,用来进行数据的重复。比如:

  • db 3 dup (0) 定义了 3 个字节,它们的值都是 0,相当于db 0,0,0
  • db 3 dup (0,1,2) 定义了 9 个字节,它们是 0、1、2、0、1、2、0、1、2,相当于db 0,1,2,0,1,2,0,1,2

dup 使用的格式为:db 重复的次数 dup (重复的字节型数据)

实验7 寻址方式在结构化数据访问中的应用

Power idea 公司从 1975 年成立一直到 1995 年的基本情况如下:

下面程序中,已经定义好了这些数据:

assume cs:code

data segment
    db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
    db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
    db '1993','1994','1995'
    ; 以上是表示 21 年的 21 个字符串

    dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
    dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
    ; 以上是表示 21 年公司总收入的 21 个 dword 型数据

    dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
    dw 11542,14430,15257,17800
    ; 以上是表示 21 年公司雇员人数的 21 个 word 型数据
data ends

table segment
    db 21 dup ('year summ ne ?? ')
table ends

; 以下为加入代码
code segment
	start:
		mov ax,data
		mov ds,ax
		mov ax,table
		mov es,ax

		mov bx,0		; table 数据的偏移地址 +16
		mov bp,0		; 年份、收入数据的偏移地址 +4
		mov si,0		; 雇员的偏移地址 +2
		mov cx,21

		s:
			; 复制年份
			mov ax,ds:[bp+0]
			mov es:[bx+0],ax
			mov ax,ds:[bp+2]
			mov es:[bx+2],ax

			; 复制收入
			mov ax,ds:[bp+54h]
			mov es:[bx+5],ax
			mov ax,ds:[bp+56h]
			mov es:[bx+7],ax

			; 复制雇员数量
			mov ax,ds:[si+0a8h]
			mov es:[bx+10],ax

			; 求人均
			mov ax,es:[bx+5]
			mov dx,es:[bx+7]
			div word ptr es:[bx+0ah]
			mov es:[bx+0dh],ax

			add bx,10h
			add bp,4
			add si,2

			loop s

			mov ax,4c00h
			int 21h
code ends
end start

编程,将 data 段中的数据按如下格式写入到 table 段中,并计算 21 年中的人均收入(取整),结果也按照下面的格式保存在 table 段中。

提示,可将 data 段中的数据看成多个数组,而将 table 中的数据看成一个结构型的数组,每个结构型数据中包含多个数据项。可用 bx 定位每个结构型数据,用 idata 定位数据项,用 si 定位数组中的每个元素,对于 table 中数据的访问可采用 [bx].idata 和 [bx].idata[si] 的寻址方式

最终执行结果:

第 9 章 转移指令原理

**可以修改 IP,或同时修改 CS 和 IP 的指令统称为转移指令。**也就是说转移指令就是可以控制 CPU 执行内存中某处代码的指令。

8086CPU 的转移行为有以下几类:

  • 段内转移 - 只修改 IP,比如:jmp ax
    由于转移指令对 IP 的修改范围不同,段内转移又分为:
    • 短转移 - 修改 IP 的范围为 -128~127
    • 近转移 - 修改 IP 的范围为 -32768~32767
  • 段间转移 - 同时修改 CS 和 IP,也被称为远转移,比如:jmp 1000:0

8086CPU 的转移指令分为一下几类:

  • 无条件转移指令,比如:jmp
  • 条件转移指令
  • 循环指令,比如:loop
  • 过程
  • 中断

这些转移指令的前提条件可能不同,但转移的基本原理是相同的。我们主要通过深入学习无条件转移指令 jmp 来理解 CPU 执行转移指令的基本原理

9.1 操作符 offset

操作符 offse 在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址。比如下面程序:

assume cs:codesg
codesg segment
    start: mov ax,offset start  ; 相当于 mov ax,0; 
        s: mov ax,offset s      ; 相当于 mov ax,3; 因为前面一条指令长度为 3 个字节
codesg ends
end start

9.2 jmp 指令

jmp 为无条件转移指令,可以只修改 IP,也可以同时修改 CS 和 IP

  • 转移的目的地址
  • 转移的距离
    • 段间转移
    • 段内短转移
    • 段内近转移

9.3 ⭐️️依据位移进行转移的 jmp 指令

jmp short 标号(转到标号处执行指令)这种格式是段内短转移,它对 IP 的修改范围为 -128~127

  • short - 符号说明指令进行的是短转移
  • 标号 - 是代码段中的标号,指明了指令要转移的目的地,转移指令结束后,CS:IP 应该指向标号处的指令

程序 9.1:

assume cs:codesg
codesg segment              ; 偏移地址  机器码
    start:  mov ax,0        ; 0000     B80000
            jmp short s     ; 0003     EB03
            add ax,1        ; 0005     050100
        s:  inc ax          ; 0008     40
codesg ends
end start

程序 9.2:

assume cs:codesg
codesg segment              ; 偏移地址  机器码
    start:  mov ax,0        ; 0000     B80000
            mov bx,0        ; 0003     BB0000
            jmp short s     ; 0006     EB03
            add ax,1        ; 0008     050100
        s:  inc ax          ; 000B     40
codesg ends
end start

比较程序 9.1 和 9.2 用 Debug 查看结果

注意,两个程序中的 jmp 指令都要使 IP 指向inc ax 指令,但是程序 9.1 的 inc ax 指令的偏移地址为0008H,而程序 9.2 的 inc ax 指令的偏移地址为000BH。再看对应的机器码都为EB 03,这说明CPU 在执行 jmp 指令的时候并不需要转移的目的地址

两个程序中的 jmp 指令的转移目的地址并不一样(cs:0008 和 cs:000B),如果机器中包含了转移的目的地址的话,那么它们对应的机器码应该是不同的。可是它们对应的机器码都是EB 03,这说明在机器指令中并不包含转移的目的地址,也就是说CPU 不需要这个目的地址就可以实现对 IP 的修改

回忆一下 CPU 执行指令的过程

  1. 从 CS:IP 指向内存单元读取指令,读取的指令进入指令缓冲器
  2. (IP)=(IP)+所读取指令的长度,从而指向下一条指令
  3. 执行指令。转到步骤 1,重复这个过程

按照 CPU 执行指令的过程,参照程序 9.2 中的 jmp short s 指令的读取和执行过程:

  1. (CS)=0BBDH, (IP)=0006H, CS:IP 指向EB 03jmp short s 的机器码)
  2. 读取指令码EB 03进入指令缓冲器
  3. (IP)=(IP)+所读取指令的长度=(IP)+2=0008H,CS:IP 指向add ax,1
  4. CPU 执行指令缓冲器中的指令EB 03
  5. 指令EB 03执行后,(IP)=000BH,CS:IP 指向inc ax

CPU 在执行EB 03的时候根据指令码中的03来修改 IP 使其指向目标指令。注意,要转移的目的地址是 CS:000B,而 CPU 执行EB 03时,当前的 (IP)=0008H,如果将当前的 IP 值加 3,使 (IP)=000BH,CS:IP 就可指向目标指令。

从上面的过程和分析中,可以得到jmp short 标号指令所对应的机器码中,并不包含转移的目的地址,而包含的是转移的位移。这个位移编译器根据汇编指令中的标号计算出来的 具体的计算方法如图 9.3:

实际上,jmp short 标号的功能为:(IP)=(IP)+8位位移

  1. 8位位移=标号处的地址-jmp指令后的第一个字节的地址
  2. short 指明此处的位移为 8 位位移
  3. 8 位位移的范围为 -128~127,用补码表示(如果对补码不了解,请阅读附注2)
  4. 8 位位移由编译程序在编译时算出

还有一种和jmp short 标号功能相近的指令格式,jmp near ptr 标号,它实现的是段内近转移。(IP)=(IP)+16位位移

  1. 16位位移=标号处的地址-jmp指令后的第一个字节的地址
  2. near ptr 指令此处的位移为 16 位位移,进行的是段内近转移
  3. 16 位位移的范围是 -32768~32767,用补码表示
  4. 16 位位移由编译程序在编译时算出

9.4 转移的目的地址在指令中的 jmp 指令

jmp far ptr 标号实现的是段间转移,又称为远转移。功能:(CS)=标号所在段的段地址;(IP)=标号在段中的偏移地址

程序 9.3

assume cs:codesg
codesg segment
    start:  mov ax,0
            mov bx,0
            jmp far ptr s
            db 256 dup (0)
        s:  add ax,1
            inc ax
codesg ends
end start

在 Debug 中讲程序 9.3 翻译成机器码,看到结果如下:

如图所示,源程序中的db 256 dup (0),被 Debug 解释为相应的若干条汇编指令,这不是关键。我们要注意一下jmp far ptr s所对应的机器码:EA 0B 01 BD 0B,其中包含转移的目标地址。0B 01 BD 0B的目的地址在指令中的存储顺序,高地址的BD 0B是转移的段地址:0BBDH,低地址的0B 01是偏移地址:010BH

9.5 转移地址在寄存器中的 jmp 指令

指令格式:jmp 16位reg

功能:(IP)=(16位reg)

9.6 转移地址在内存中的 jmp 指令

转移地址在内存中的 jmp 指令的两种格式:

(1)jmp word ptr 内存单元地址(段内转移)

功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。内存单元地址可用寻址方式的任一格式给出

mov ax,0123h
mov ds:[0],ax
jmp word ptr ds:[0] ; 执行后,(IP)=0123h

; 或下面格式
mov ax,0123h
mov [bx],ax
jmp word ptr [bx] ; 执行后,(IP)=0123h

(2)jmp dword ptr 内存单元地址(段间转移)

功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。内存单元地址可用寻址方式的任一格式给出

(CS)=(内存单元地址+2);(IP)=(内存单元地址)

mov ax,0123h
mov ds:[0],ax
mov word ptr ds:[2],0
jmp dword ptr ds:[0]    ; 执行后 (CS)=0,(IP)=0123H, CS:IP 指向 0000:0123

; 或下面格式
mov ax,0123h
mov [bx],ax
mov word ptr [bx+2],0
jmp dword ptr [bx]      ; 执行后 (CS)=0,(IP)=0123H,CS:IP 指向 0000:0123

检测点 9.1

(1)程序如下

assume cs:code

data segment
    db 0,0,0,0 ; 保证 1、2 位为 0
data ends

code segment
    start:
        mov ax,data
        mov ds,ax
        mov bx,0
        jmp word ptr [bx+1]
code ends
end start

若要使程序中的 jmp 指令执行后,CS:IP 指向程序的第一条指令,在 data 段中应该定义哪些数据

(2)程序如下

assume cs:code
data segment
    dd 12345678H
data ends
code segment
    start:
        mov ax,data
        mov ds,ax
        mov bx,0
        mov [bx],bx             ; 这里为填入。也就是 IP 地址
        mov [bx+2],cs           ; 这里为填入。也就是 CS 地址
        jmp dword ptr ds:[0]    ; 段间转移,(CS)=(内存单元地址)+2  (IP)=(内存单元地址)
code ends
end start

补全程序,是 jmp 指令执行后,CS:IP 指向程序的第一条指令

(3)用 Debug 查看内存,结果如下:

2000:1000 BE 00 06 00 00 00 ......

则此时,CPU 执行指令:

mov ax,2000H
mov es,ax
jmp dword ptr es:[1000H]

后,(CS)=___, (IP)=___。

答案为:0006H:BE

9.7 jcxz 指令

jcxz 指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对 IP 的修改范围都为:-128~127

指令格式:jcxz 标号;如果(cx)=0),转移到标号处执行

操作:当 (cx)=0 时,(IP)=(IP)+8位位移

  • 8位位移=标号处的地址-jcxz指令后的第一个字节的地址
  • 8位位移的范围为-128~127
  • 8位位移由编译程序在编译时算出

当 (cx)≠0 时,什么也不做(程序向下执行)。

从 jcxz 的功能中可以看出,jcxz 标号的功能相当于:if((cx)==0) jmp short 标号。(这种用 C 语言和汇编进行的综合描述,或许对理解有条件转移指令更加清楚)

检测点 9.2

补全程序,利用 jcxz 指令,实现在内存 2000H 段中查找第一个值为 0 的字节,找到后,将它的偏移地址存储在 dx 中

assume cs:code
code segment
    start:
        mov ax,2000H
        mov ds,ax
        mov bx,0
    s:
        mov cl,[bx]     ; 填入
        mov ch,0        ; 填入
        jcxz ok         ; 填入
        inc bx          ; 填入
        jmp short s
    ok:
        mov dx,bx
        mov ax,4c00h
        int 21h
code ends
end start

9.8 loop 指令

loop 指令为循环指令,所有的循环指令都是短转移

指令格式:loop 标号

操作分两步:

  1. (cx)=(cx)-1
  2. 判断 cx 中的值
    • 不为 0,转移至标号处执行
    • 为 0,向下执行
loop 标号 # 相当于下面伪代码:

(cx)--;
if((cx) ≠ 0) jmp short 标号;

检测点 9.3

补全程序,利用 loop 指令,实现在内存 2000H 段中查找第一个值为 0 的字节,找到后,将它的偏移地址存储的 dx 中

assume cs:code
code segment
    start:
        mov ax,2000H
        mov ds,ax
        mov bx,0
    s:
        mov cl,[bx]
        mov ch,0
        inc cx      ; 填入
        inc bx
        loop s
    ok:
        dec bx      ; dec 指令的功能和 inc 相反,dec bx 进行的操作为:(bx)=(bx)-1
        mov dx,bx
        mov ax,4c00h
        int 21h
code ends
end start

9.9 根据位移进行转移的意义

方便程序段在内存中的浮动装配。TODO: 对于这里的深入理解

9.10 编译器对转移位移超界的检测

实验8 分析一个奇怪的程序

分析下面程序,在运行前思考:这个程序可以正确返回吗?运行后再思考:为什么是这种结果?

assume cs:codesg
codesg segment
    mov ax,4c00h            ; 机器码占用 3 字节,偏移地址 (IP)=0000h
    int 21h                 ; 2字节,(IP)=0003h
start:  mov ax,0            ; 3字节,(IP)=0005H
    s:  nop                 ; 1字节,(IP)=0008H
        nop                 ; 1字节,(IP)=0009H
        
        mov di,offset s     ; 3字节,(IP)=000AH
        mov si,offset s2    ; 3字节,(IP)=000DH
        mov ax,cs:[si]      ; 1 + 3字节,(IP)=0010H,这里实际上被编译成两条指令,先获取 CS (占用 1 字节)再赋值,(AX)=0020H
        mov cs:[di],ax      ; 1 + 3字节,(IP)=0014H,[0008H]=0020H;也就是将 s 处的两个字节内容改为 s2 的两个字节内容 EBF6 也就是 jmp short s1,F6 为 -9 的补码
        
    s0: jmp short s         ; 2字节
    
    s1: mov ax,0            ; 3字节
        int 21h             ; 2字节
        mov ax,0            ; 3字节
        
    s2: jmp short s1        ; 2字节
        nop
codesg ends
end start

**编译器先进行编译得到机器码,再执行时按照编译后的机器码执行,并不会动态更改。**执行到 s2:jmp short s1时的机器码为 BEF6,也是就向前移动 10 个字节即mov ax 4c00h可以正常执行

实验9 根据材料编程

编程:在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串“welcome to masm!”。

assume cs:codesg,ss:stack
data segment
    db 'welcome to masm!'
    db 02h,24h,71h      ; 字符显示的属性
data ends
stack segment
    db 10 dup (0)
stack ends

codesg segment
start:
    ; 初始化 data 数据段,es:di 指向 data 段
    mov ax,data
    mov es,ax
    mov di,0
    
    ; 初始化显示缓冲区,ds:bx 指向显示缓冲区
    mov ax,0b800h
    mov ds,ax
    
    ; 25 行取中间是 12、13、14 行,80列取中间开始是 61 列
    ; 12 行的偏移量是 12*16=1920
    ; 总偏移量是 1920+60=1980
    mov bx,1980
    
    mov si,16       ; 字符显示属性在数据段中的偏移量
    
    mov cx,3        ; 计数器初始化 3
s:
    push cx         ; 入栈保护 cx
    
    mov cx,16       ; 内循环 16 次,16 个字符
output:             ; 循环将字符写入显存中
    mov al,es:[di]
    mov [bx],al
    mov ah,es:[si]  ; 字符属性写入显存
    mov [bx+1],ah
    
    inc di
    add bx,2
    loop output
    
    add bx,128      ; 每行输出的偏移量为 128 字节
    
    mov di,0
    inc si
    pop cx          ; 恢复 cx 计数器
    loop s
    
    mov ax,4c00h
    int 21h
codesg ends
end start


由图可以得到

  • 00000010B=02h:绿字
  • 00100100B=24h:红字绿底
  • 01110001B=71h:蓝字白底

第 10 章 CALL 和 RET 指令

call 和 ret 指令都是转移指令用来修改 IP,或同时修改 CS 和 IP。经常被共同用来实现子程序的设计

10.1 ret 和 retf

  • ret 指令用栈中的数据,修改 IP 的内容,实现近转移
    CPU 执行时进行下面两步操作
    1. (IP)=((ss)*16+(sp))
    2. (sp)=(sp)+2
      用汇编语法来解释
    pop IP
    
  • retf 指令用栈中的数据,修改 CS 和 IP 的内容,实现远转移
    CPU 执行时进行下面四步操作(先入栈 cs,后入栈 ip)
    1. (IP)=((ss)*16+(sp))
    2. (sp)=(sp)+2
    3. (CS)=((ss)*16+(sp))
    4. (sp)=(sp)+2
      用汇编语法来解释
    pop IP
    pop CS
    

例:下面程序中,ret 指令执行后,(IP)=0,CS:IP 指向代码段的第一条指令

assume cs:code
stack segment
    db 16 dup (0)
stack ends
code segment
    mov ax,4c00h
    int 21h
    
start:
    mov ax,stack
    mov ss,ax
    mov sp,16
    mov ax,0
    push ax
    mov bx,0    ; TODO: 这里的指令感觉没有作用
    ret
code ends
end start

下面程序中,retf 指令执行后,CS:IP 指向代码的第一条指令

; ... start 之前代码于上面程序一致
start:
    mov ax,stack
    mov ss,ax
    mov sp,16
    mov ax,0
    push cs
    push ax
    mov bx,0    ; TODO: 这里的指令感觉没有作用
    retf
code ends
end start

检测点 10.1

补全程序,实现从内存 1000:0000 处开始执行指令

assume cs:code
stack segment
    db 16 dup (0)
stack ends
code segment
start:
    mov ax,stack
    mov ss,ax
    mov sp,16
    mov ax,______   ; 填入 1000H
    push ax
    mov ax,______   ; 填入 0000H
    push ax
    retf
code ends
end start

10.2 call 指令

CPU 执行 call 指令时,进行两步操作:

  1. 将当前的 IP 或 CS 和 IP 压入栈
  2. 转移

call 指令不能实现短转移,除此之外 call 指令实现转移的方法和 jmp 指令的原理相同

10.3 依据位移进行转移的 call 指令

call 标号:将当前的 IP 入栈后,转到标号处执行指令

CPU 执行此种格式的 call 指令是,进行如下操作:

  1. (sp)=(sp)-2
    ((ss)*16+(sp))=(IP)
  2. (IP)=(IP)+16位位移

16 位位移=标号处的地址-call指令后的第一个字节的地址
16 位位移的范围为 -32768~32767,用补码表示
16 位位移由编译程序在编译时算出

用汇编语法拉解释此种格式的 call 指令为:

push IP
jmp near ptr 标号

检测点 10.2

下面程序执行后,ax 中的数值为多少?

; 内存地址      机器码      汇编指令
1000:0      b8 00 00    mov ax,0   ; 执行后 (ax)=0
1000:3      e8 01 00    call s      ; 走到这里时 ip 的值已经完成自增等于 6,执行时将 6 入栈
1000:6      40          inc ax      ; 这里不执行
1000:7      58          s: pop ax   ; 执行前栈顶是 6 ,所以 (ax)=6

10.4 转移的目的地址在指令中的 call 指令

call far ptr 标号实现的是段间转移

  1. (sp)=(sp)-2
    ((ss)*16+(sp))=(CS)
    (sp)=(sp)-2
    ((ss)*16+(sp))=(CS)
  2. (CS)=标号所在段的段地址
    (IP)=标号所在段中的偏移地址

用汇编语法来解释此种格式的 call 指令

push CS
push IP
jmp far ptr 标号

检测点 10.3

下面程序执行后,ax 中的数值为多少?

; 内存地址      机器码          汇编指令
1000:0      b8 00 00            mov ax,0        ; 执行后 (ax)=0
1000:3      9a 09 00 00 10      call far ptr s  ; 走到这里时 ip 的值已经完成自增等于 8,执行时先将 (cs)=1000H 入栈,再将 (IP)=8 入栈,然后在跳转到标号 s 处执行
1000:8      40                  inc ax          ; 这里不执行
1000:9      58                  s: pop ax       ; 执行前栈顶是 (ip)=8 ,所以 (ax)=8
                                add ax,ax       ; (ax)=(ax)+(ax)=16=10h
                                pop bx          ; (bx)=1000h
                                add ax,bx       ; (ax)=(ax)+(bx)=1010h

10.5 转移地址在寄存器中的 call 指令

格式:call 16位reg
功能:

(sp)=(sp)-2
((ss)*16+(sp))=(IP)
(IP)=(16位reg)

用汇编语法来解释:

push IP
jmp 16位reg

10.6 转移地址在内存中的 call 指令

有两种格式

  1. call word ptr 内存单元地址
    用汇编来解释:
    push IP
    jmp word ptr 内存单元地址
    
  2. call dword ptr 内存单元地址
    用汇编来解释:
    push CS
    push IP
    jmp dword ptr 内存单元地址
    

10.7 call 和 ret 的配合使用

具有一定功能的程序段,称为子程序。

在需要的时候,用 call 指令转去执行。可是执行完子程序后,如何让CPU接着 call 指令向下执行?

call 指令转去执行子程序之前,call 指令后面的指令的地址将存储在栈中,所以可在子程序后面使用 ret 指令,用栈中的数据设置 IP 的值,从而转到 call 指令后面的代码处继续执行。子程序框架如下:

标号:
    指令
    ret

具有子程序的源程序框架如下:

assume cs:code
code segment
    main:
        ...
        call sub1       ; 调用子程序 sub1
        ...
        mov ax,4c00h
        int 21h
        
    sub1:               ; 子程序 sub1 开始
        ...
        call sub2       ; 调用子程序 sub2
        ...
        ret             ; 子程序返回
        
    sub2:               ; 子程序 sub2 开始
        ...
        ...
        ret             ; 子程序返回
code ends
end main

10.8 mul 指令

mul是乘法指令,注意一下两点

  • 两个相乘的数:要么都是 8 位,要么都是 16 位
    • 8 位:一个默认放在 AL 中,另一个放在 8 位 reg 或内存字节单元
    • 16 位:一个默认放在 AX 中,另一个放在 16 位 reg 或内存字单元
  • 结果:
    • 8 位:默认放在 AX 中
    • 16 位:高位默认在 DX 中存放,地位在 AX 中存放
      格式如下:
mul reg
mul 内存单元

举例:

(1)计算 100*10

100 和 10 小于 255,可以做 8 位乘法,程序如下

mov al,100
mov bl,10
mul bl

结果:(ax)=1000=03E8H

(2)计算 100*10000

100 小于 255,可 10000 大于 255,所以必须做 16 位乘法,程序如下

mov ax,100
mov bx,10000
mul bx

结果:(ax)=4240H (dx)=000FH;结果是 16 位,高位存在 DX 中,1000000=000F4240H

10.9 模块化程序设计

call 与 ret 指令共同支持了汇编语言中的模块化设计。

10.10 参数和结果传递的问题

子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者。

比如,设计一个子程序,可以根据提供的 N,来计算 N 的 3 次方。这里面就有两个问题:

  1. 将参数 N 存储在什么地方?
  2. 计算得到的数值,存储在什么地方?

很显然,可以用寄存器来存储,将参数放在 bx 中;因为子程序中要计算 N*N*N,可以使用多个 mul 指令,为了方便,可以将结果放在 dx 和 ax 中。子程序如下:

; 说明:计算 N 的 3 次方
; 参数:(bx)=N
; 结果:(dx:ax)=N^3

cube: mov ax,bx
    mul bx
    mul bx
    ret

用寄存器来存储参数和结果是最常使用的方法。对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:调用者将参数送入寄存器,从结果寄存器中取到返回值;子程序从参数寄存器中取到参数,将返回值送入结果寄存器

10.11 批量数据的传递

将批量数据放到内存中,然后将它们所在的内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可用同样的方法。

将字符串的首地址传递给子程序,然后在子程序中使用 loop ,次数(cx)就是字符串的长度,这样就可以批量传递数据。

编程,将 data 段中的字符串转化为大写

assume cs:code

data segment
    db 'conversation'
data ends

code segment
    start:
        mov ax,data
        mov ds,ax
        mov si,0        ; ds:si 指向字符串(批量数据)所在空间的首地址
        mov cx,12       ; cx 存放字符串的长度
        call capital
        mov ax,4c00h
        int 21h
        
    capital:
        and byte ptr [si],1101111b
        inc si
        loop capital
        ret
code ends
end start

注意,除了用寄存器传递参数外,还有一种通用的方法是用栈来传递参数。参看 附录4

10.12 寄存器冲突的问题

设计一个子程序,功能:将一个全是字母,以 0 结尾的字符串,转化为大写。字符串可以定义为:db 'conversation',0

用 jcxz 来检测 0,代码如下:

; 说明:将一个全是字母,以 0 结尾的字符串,转化为大写
; 参数:ds:si 指向字符串的首地址
; 结果:没有返回值
captial:
    mov cl,[si]                 ; (cl)=((ds)*16+si),将 ds:si 的数据放入 cl 中
    mov ch,0                    ; (cx)=(ch)+(cl),ds:si 中的数据就是 cx 的值
    jcxz ok                     ; 判断 cx 是否为 0,为 0 则跳转至标号 ok 处执行
    and byte ptr [si],11011111b ; 将 ds:si 处的值改为大写
    inc si
    jmp short captial
ok: ret

captial子程序的应用

  1. 将 data 段中的字符串转化为大写
    ; 数据段
    data segment
        db 'conversation',0
    data ends
    
    ; 代码段相关代码
    mov ax,data
    mov ds,ax
    mov si,0
    call captial    ; 这里调用
    
  2. 将 data 段中的字符串全部转化为大写
    ; 数据段
    data segment
        db 'word',0
        db 'unix',0
        db 'wind',0
        db 'good',0
    data ends
    
    ; 代码段相关程序,因为字符串的长度都是 5,使用循环,重复调用子程序 captial,完成对 4 个字符串的处理
    start:
        mov ax,data
        mov ds,ax
        mov bx,0
    
        mov cx,4
    s:  mov si,bx
        call captial    ; 这里调用
        add bx,5
        loop s
        
        mov ax,4c00h
        int 21
    

问题 10.2

子程序captial思想上没有问题,但是细节上有错误:在 cx 的使用,主程序要使用 cx 记录循环次数,可子程序中也使用了 cx,在执行子程序时,cx 中保存的循环计数值也被改变,使得主程序的循环出错。

从上面得问题中,引出了一个一般化的问题:子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的冲突

子程序要实现以下目的:

  • 编写调用子程序的程序的时候不必关心子程序到底使用了哪些寄存器
  • 编写子程序的时候不必关心调用者使用了哪些寄存器
  • 不会发生寄存器冲突

解决者个问题的简洁方法是:在子程序的开始将子程序中所有用到的寄存器中的内存都保存起来,在子程序返回前再恢复。可以用栈来保存寄存器中的内存

以后,编写子程序的标准框架如下:

子程序开始: 子程序中使用的寄存器入栈
            子程序内容
            子程序中使用的寄存器出栈
            返回(ret、retf)

按照上面框架改进一下子程序captial

captial:
    push cx
    push si
    
change:
    mov cl,[si]
    mov ch,0
    jcxz ok
    and byte ptr [si],11011111b
    inc si
    jmp short change
    
ok: pop si ; 注意寄存器入栈与出栈的顺序
    pop cx
    ret

实验10 编写子程序

显示字符串

显示字符串是现实工作中经常要用到的功能,应该编写一个通用的子程序来实现这个功能。我们应该提供灵活的调用接口,使调用者可以决定显示的位置(行、列)、内容和颜色

子程序描述:

; 名称:show_str
; 功能:在指定的位置,用指定的颜色,显示一个用 0 结束的字符串
; 参数:(dh)=行号(取值范围 0~24),(dl)=行号(取值范围 0~79),(cl)=颜色,ds:si 指向字符串的首地址
; 返回:无
; 应用举例:在屏幕的 8 行 3 列,用绿色显示 data 段中的字符串
assume cs:code
data segment
    db 'Welcome to masm!',0
data ends

code segment
start:
    mov dh,8        ; 屏幕的行数
    mov dl,3        ; 所在行的列数
    moc cl,2        ; 颜色数据(二进制 0000 0010B)
    mov ax,data
    mov ds,ax
    mov si,0        ; ds:si 指向字符串
    call show_str
    
    mov ax,4c00h
    int 21h
    
show_str:
    ; 加入代码开始
    push dx
    push cx
    push si
    
    mov ax,0b800h
    mov es,ax       ; 设置显示缓冲区内存段
    
    mov ax,0
    mov al,160      ; 160=a0h   160字节/行
    mul dh          ; 相对于 0b800:0000 第 dh 行偏移量
    mov bx,ax       ; 将第 dh 行的偏移地址送入 bx,bx 代表行偏移
    mov ax,0
    mov al,2        ; 列的标准偏移量是 2 字节
    mul dl          ; 用一行列的偏移量,尽量用乘法 (al)=列偏移
    add bx,ax       ; 最终获得偏移地址 (bx)=506H
    mov di,0        ; 将 di 作为每个字符的偏移量
    mov al,cl       ; 将字符属性写入 al
    mov ch,0        ; 将 cx 高 8 位设置为 0
    
show:
    mov cl,ds:[si]      ;
    jcxz ok             ;
    mov es:[bx+di+0],cl ;
    mov es:[bx+di+1],al ;
    add di,2            
    inc si
    jmp short show

ok: pop si
    pop dx
    pop cx
    ret
    ; 加入代码结束

code ends
end start

提示:

  1. 子程序的入口参数是屏幕上的行号和列号,注意在子程序内部要将它们转化为显存中的地址,首先要分析一下屏幕上的行列位置和显存地址的对应关系
  2. 注意保护子程序中用到的相关寄存器
  3. 这个子程序的内部处理和显存的结构密切相关,但是向外提供了与显存结构无关的接口。通过调用这个子程序,进行字符串的显示时可以不用了解显存的结构,为编程提供了方便。在实验中注意体会这种设计思想

解决除法溢出的问题

TODO:

数值显示

TODO:

课程设计1

TODO:

第 11 章 标志寄存器

8086CPU 的标志寄存器(以下简称为 flag)有 16 位,其中存储的信息通常被称为程序状态字(PSW),有以下三种作用:

  1. 用来存储相关指令的某些执行结果
  2. 用来为 CPU 执行相关指令提供行为依据
  3. 用来控制 CPU 的相关工作方式

11.1 ZF 标志

零标志位,第 6 位。它记录相关指令执行后,结果是否为 0

注意,在 8086CPU 的指令集中:

  • 有的指令执行是影响标志寄存器的,比如:add、sub、mul、div、inc、and 等,它们大都是运算指令(进行逻辑或算数运算)
  • 有的指令执行对标志寄存器没有影响,比如:mov、push、pop 等,它们大都是传送指令

在使用一条指令的时候,要注意这条指令的全部功能,其中包括,执行结果对标志寄存器的哪些标志位造成影响

11.2 PF 标志

奇偶标志位,第 2 位。它记录相关指令执行后,结果的所有 bit 位中 1 的个数是否为偶数

bit 为中 1 的个数为:

  • 偶数,pf=1
  • 奇数,pf=0

11.3 SF 标志

符号标志位,第 7 位。它记录相关指令执行后,结果是否为负

检测点 11.1

; 写出下面每条指令执行后,ZF、PF、SF 等标志位的值
sub al,al       ; ZF=1 ;PF=1 ;SF=0 ;
mov al,1        ; ZF=1 ;PF=1 ;SF=0 ;
push ax         ; ZF=1 ;PF=1 ;SF=0 ;
pop bx          ; ZF=1 ;PF=1 ;SF=0 ;
add al,bl       ; ZF=0 ;PF=0 ;SF=0 ;
add al,10       ; ZF=0 ;PF=1 ;SF=0 ;
mul al          ; ZF=0 ;PF=1 ;SF=0 ;

mov、push、pop 传送指令不影响标志位

11.4 CF 标志

进位标志位,第 0 位。一般情况下,在进行「无符号数」运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值

11.5 OF 标志

溢出标志位,第 11 位。一般情况下,记录了「有符号数」运算的结果是否发生了溢出。

11.6 adc 指令

adc 是带进位加法指令,它利用了 CF 位上记录的进位值

指令格式:adc 操作对象1, 操作对象2
功能:操作对象1 = 操作对象1 + 操作对象2 + CF
比如指令adc ax,bx实现的功能是:(ax)=(ax)+(bx)+CF

例:

mov al,98H
add al,al
adc al,3 ; (ax)=34H,相当于计算:(al)+3+CF=30H+3+1=34H,这里 (al)=30H,因为 98H+98H=130H,al 只能存 8 位,进位 1 不保存,存在 CF 中

先看一下 CF 的含义。在执行 adc 指令的时候加上 CF 的值的含义,是由 adc 指令前面的指令决定的,也就是说,关键在于所加上的 CF 值是被什么指令设置的。如果 CF 的值是被 sub 指令设置的,那么它的含义就是借位值;如果是被 add 指令设置的,那么它的含义就是进位值。

看一下两个数据 0198H 和 0183H 如何相加的:

从图可以看出,加法可以分两步来进行:

  1. 低位相加
  2. 高位相加再加上地位相加产生的进位值

下面指令和add ax,bx具有相同的结果:

add al,bl
adc ah,bh

看来 CPU 提供 adc 指令的目的,就是用来进行加法的第二部运算的。adc 指令和 add 指令相配合就可以对更大的数据进行加法运算。来看一个例子:

编程,计算 1EF000H+201000H,结果放在 ax(高16位)和 bx(低16位)中。

因为两个数据的位数都大于 16,用 add 指令无法进行计算。我们将计算分为两步进行,先将低 16 位相加,然后将高 16 位和进位值相加。程序如下:

mov ax,001EH
mov bx,0F000H
add bx,1000H
adc ax,0020H

adc 指令执行后,也可能产生进位值,所以也会对 CF 位进行设置。由于有这样的功能,我们就可以对任意大的数据进行加法运算。

11.7 sbb 指令

sbb 是带借位减法指令,它利用 CF 位上记录的借位值。

指令格式:sub 操作对象1, 操作对象2
功能:操作对象1 = 操作对象1 - 操作对象2 - CF
比如指令:sub ax,bx实现的功能是:(ax)=(ax)-(bx)-CF

sbb 指令执行后,将对 CF 进行设置。用力 sbb 指令可以对任意大的数据进行减法运算。例如,计算 003E1000H-00202000H,结果放在 ax,bx 中,程序如下:

mov bx 1000H
mov ax 003EH
sub bx,2000H
sbb ax,0020H

11.8 cmp 指令

cmp 是比较指令,cmp 的功能相当于减法指令,只是不保存结果。cmp 指令执行后,将对标志寄存器产生影响。其它相关指令通过识别这些被影响的标志寄存器位来得知比较结果。

指令格式:cmp 操作对象1, 操作对象2
功能:计算 操作对象1-操作对象2 但并不保存结果,仅仅根据计算结果对标志寄存器进行设置
比如,指令cmp ax,ax做 (ax)-(ax) 的运算,结果为 0,但并不在 ax 中保存,进影响 flag 的相关各位。指令执行后:zf=1, pf=1, sf=0, cf=0, of=0

下面指令:

mov ax,8
mov bx,3
cmp ax,bx   ; 执行后:(ax)=8, zf=0, pf=1, sf=0, cf=0, of=0

通过 cmp 指令执行后,相关标志位的值就可以看出比较的结果,cmp ax,bx

  • 如果 (ax)=(bx) 则 (ax)-(bx)=0,所以:zf=1
  • 如果 (ax)≠(bx) 则 (ax)-(bx)≠0,所以:zf=0
  • 如果 (ax)<(bx) 则 (ax)-(bx) 将产生借位,所以:cf=1
  • 如果 (ax)≥(bx) 则 (ax)-(bx) 将不必借位,所以:cf=0
  • 如果 (ax)>(bx) 则 (ax)-(bx) 既不必借位,结果又不为 0,所以:cf=0 并且 zf=0
  • 如果 (ax)≤(bx) 则 (ax)-(bx) 既可能借位,结果可能为 0,所以:cf=1 或 zf=1

现在可以看出比较指令的设计思路,即:通过做减法运算,影响标志寄存器,标志寄存器的相关位记录了比较的结果

同 add、sub 指令一样,CPU 在执行 cmp 指令的时候,也包含两种含义:

  • 进行无符号数运算
  • 进行有符号数运算

下面我们在来看一下如果用 cmp 进行有符号数比较时,CPU 用哪些标志位 对比较结果进行记录。

比如:

mov ah,08AH
mov bh,070H
cmp ah,bh

结果 sf=0,运算 (ah)-(bh) 实际得到的结果是 1AH,但是逻辑上,运算所应该得到的结果是:-(118)-112=-230。sf 记录实际结果的正负,所以 sf=0。但 sf=0 不能说明在逻辑上,运算所得到的正确结果。

但是逻辑上的结果的正负,才是 cmp 指令所求的真正结果,因为我们就是要靠它得到两个操作对象的比较信息。所以 cmp 指令所作的比较结果,不仅靠 sf 就能记录的,因为它只能记录实际结果的正负。

考虑一下,两种结果之间的关系,实际结果的正负,和逻辑上真正结果的正负,它们之间有多大的距离呢?从上面的分析中,我们知道,实际结果的正负,之所以不能说明逻辑上真正结果的正负,关键在于发生了溢出。如果没有溢出发生的话,那么实际结果的正负和逻辑上的真正结果的正负就一致了。

所以应该在考察 sf(得知实际结果的正负)的同时考查 of(看有没有溢出),就可以得知逻辑上真正结果的正负,同时就可以知道比较的结果

下面以cmp ah,bh为例,总结一下 CPU 执行 cmp 指令后,sf 和 of 的值是如何来说明比较的结果的。

  1. 如果 sf=1,而 of=0
    of=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负
    因 sf=1,实际结果为负,所以逻辑上真正的结果为负,(ah)<(bh)
  2. 如果 sf=1,而 of=1
    of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负
    因 sf=1,实际结果为负
    实际结果为负,而又有溢出,这说明是由于溢出导致了实际结果为负,简单分析一下,就可以看出,如果因为溢出导致了实际结果为负,那么逻辑上真正的结果必然为正
    这样,sf=1、of=1,说明了 (ah)>(bh)
  3. 如果 sf=0,而 of=1
    of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负
    因 sf=0,实际结果非负。而 of=1 说明有溢出,则结果非 0,所以实际结果为正
    实际结果为正,而又有溢出,说明是由于溢出导致了实际结果非负。如果因为溢出导致了实际结果为正,那么逻辑上真正的结果必然为负
    这样,sf=0、of=1,说明了 (ah)<(bh)
  4. 如果 sf=0,而 of=0
    of=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负
    因 sf=0,实际结果非负,所以逻辑上真正的结果非负,所以 (ah)≥(bh)

11.9 检测比较结果的条件转移指令

“转”移指的是它能够修改 IP,“条件”指的是它可以根据某种条件决定是否修改 IP

下面是常用的根据无符号数的比较结果进行转移的条件转移指令,第一个字母都是 j,表示 jump

指令 含义 记忆 检测的相关标志位
je 等于则转移 e: equal zf=1
jne 不等于则转移 ne: not equal zf=0
jb 低于则转移 b: below cf=1
jnb 不低于则转移 nb: not below cf=0
ja 高于则转移 a: above cf=0 且 zf=0
jna 不高于则转移 na: not above cf=1 或 zf=1

观察得到,它们所检测的标志位,都是 cmp 指令进行无符号数比较的时候,记录比较结果的标志位。

编程实现功能:如果 (ah)=(bh) 则 (ah)=(ah)+(ah),否则 (ah)=(ah)+(bh)

cmp ah,bh       ; 比较 ah bh 是否相等
je s            ; 相等则转移到标号 s 处执行;体现了 je 指令的逻辑含义(相等则转移)
add ah,bh
jmp short ok
s: add ah,ah
ok: ...

je 检测的是 zf 位置,不管前面是什么指令,只要执行 je 时 zf=1,就发生转移,比如:

mov ax,0
add ax,0
je s
inc ax
s: inc ax

执行后,(ax)=1。add ax,0使得 zf=1,所以 je 指令将进行转移。可在这个时候发生的转移的确不带有“相等则转移”的含义。虽然此处 zf=1 不是由 cmp 比较指令设置的,不具有“俩数相等”的含义,但是 je 指令执行时只要 zf=1 就可以发生转移。

后面我们可以只考虑 cmp 和 je 等指令配合使用所表现的逻辑含义。

来看下面一组程序,data 段中的 8 个字节如下:

data segment
    db 8,11,8,1,8,5,63,38
data ends

(1)编程,统计 data 段中数值为 8 的字节的个数,用 ax 保存统计结果。

编程思路:初始设置 (ax)=0,然后循环依次比较每个字节的值,找到一个和 8 相等的数就将 ax 的值加 1。程序如下:

mov ax,data
mov ds,ax
mov bx,0        ; ds:bx 指向第一个字节
mov ax,0        ; 初始化累加器
mov cx,0

; 方法一
s:
    cmp byte prt [bx],8         ; 和 8 比较
    jne next                    ; 如果不相等则转移到标号 next 处,继续循环
    inc ax                      ; 如果相等就将计数值加 1

next:
    inc bx
    loop s                      ; 程序执行后:(ax)=3
    
; 方法二
s:  cmp byte ptr [bx],8
    je ok                       ; 相等就转移到标号 ok
    jmp short next              ; 不相等则转移到 next
ok: inc ax                      ; 计数值加 1
next: inc bx
    loop s

(2)编程,统计 data 段中数值大于 8 的字节的个数,用 ax 保存统计结果

编程思路:初始设置 (ax)=0,然后用循环依次比较每个字节的值,找到一个大于 8 的就将 ax 的值加 1。程序如下:

mov ax,data
mov ds,ax
mov ax,0
mov bx,0

mov cx,8
s:
    cmp byte ptr [bx],8 ; 和 8 进行比较
    jna next            ; 如果不大于 8 转移到标号 next 处,继续循环
    inc ax              ; 如果不大于 8 则计数值加 1
next:
    inc bx
    loop s              ; 程序执行后 (ax)=3

检测点 11.3

(1)补全下面程序,统计 F000:0 处 32 个字节中,大小在 [32,128] 的数据的个数

mov ax,0f000h
mov ds,ax

mov bx,0
mov ds,0
mov cx,32

s: mov al,[bx]
    cmp al,32
    ______      ; 填入代码为 jb s0,小于32则转移到标号 s0 处,不对计数值操作
    cmp al,128
    ______      ; 填入代码为 ja s0,大于128则转移到标号 s0 处
    inc dx
s0: inc bx
    loop s

(2)补全下面程序,统计 F000:0 处 32 个字节中,大小在 (32,128) 的数据的个数

mov ax,0f000h
mov ds,ax
mov bx,0
mov dx,0

mov cx,32
s: mov al,[bx]
    cmp al,32
    ______      ; 填入代码为 jna s0,不高于32则转移到 s0 处,不对计数值操作
    cmp al,128
    ______      ; 填入代码为 jnb s0,不低于128则转移到 s0 处
    inc dx
s0: inc bx
    loop s

11.10 DF 标志和串传送指令

方向标志位,在 flag 第 10 位。在串处理指令中,控制每次操作后 si、di 的增减

  • df=0:每次操作后 si、di 递增
  • df=1:每次操作后 si、di 递减

格式:movsb
功能:执行 movsb 指令相当于进行下面几步操作

  1. ((es)*16+(di))=((ds)*16+(si))
  2. 如果 df=0 则:
    (si)=(si)+1
    (di)=(di)+1
    
  3. 如果 df=1 则:
    (si)=(si)-1
    (di)=(di)-1
    

用汇编语法描述的话:

mov es:[di],byte ptr ds:[si]    ; 8086 并不支持这样的指令,这里是是做为描述。伪代码

如果 df=0:
inc si
inc di

如果 df=1:
dec si
dec di

可以看出,movsb 的功能是将 ds:si 指向的内存单元中的字节送入 es:di 中,然后根据标志寄存器 df 位的值,将 si 和 di 递增或递减

也可以传送一个字,指令如下

movsw:功能是将 ds:si 指向的内存单元中的字送入 es:di 中,然后根据标志寄存器 df 位的值,将 si 和 di 递增 2 或递减 2

一般来说 movsb 和 movsw 都和 rep 配合使用,格式如下:

rep movsb   ; 格式

; 下面为描述,伪代码
s:  movsb
    loop s

rep 的作用是根据 cx 的值,重复执行后面的串传送指令。由于每执行一次 movsb 指令 si 和 di 都会递增或递减指向后一个单元或前一个单元,则 rep movsb 就可以循环实现 (cx) 个字符的传送

由于 flag 的 df 位决定着串传送指令执行后,si 和 di 改变的方向,所以 CPU 应该提供相应的指令来对 df 位进行设置,从而使程序员能够决定传送的方向。

11.11 pushf 和 popf

pushf 的功能是将标志寄存器的值压栈,popf 是从栈中弹出数据,送入标志寄存器中

pushf 和 popf,为直接访问标志寄存器提供了一种方法

11.12 标志寄存器在 Debug 中的表示

在 Debug 中,标志寄存器是按照有意义的各个标志位单独表示的

实验 11 编写子程序

编写一个子程序,将包含任意字符、以 0 结尾的字符串中的小写字母转变成大写字母,描述如下。

; 名称:letterc
; 功能:将以 0 结尾的字符串中的小写字母 转变成大写字母
; ds:si 指向字符串首地址

assume cs:codesg

datasg segment
    db "Beginner's All-purpose Symbolic Instruction Code.",0
datasg ends

codesg segment
begin:
    mov ax,datasg
    mov ds,ax
    mov si,0
    call letterc
    
    mov ax,4c00h
    mov 21h

letterc:
; 写入代码开始
	push ax	; 保护寄存器的值
	push bx
	push cx
	push si
	mov ch,0	; 清空 cx 的值

s0:
	mov cl,[si]	; 如果为 0 则转到标号 ok 处
	jcxz ok

	; 小写字母范围 a~z=97~122=61h~7ah
	cmp cl,61h	; 如果 (cl)<61h 则表示当前字符不是小写字母,则转到标号 s1 处
	jb s1
	cmp cl,7Ah	; 如果 (cl)>7Ah 则表示当前字符不是小写字母,则转到标号 s1 处
	ja s1

	and cl,11011111B	; 执行到这里说明字母为小写字母,改为大写
	mov [si],cl	; 这里 (cl) 已经为大写字母,再赋值到对应的内存单元中

	jmp s0			; 循环继续

s1:
	inc si
	jmp s0

ok:
	pop si
	pop cx
	pop bx
	pop ax
	ret
; 写入代码结束

codesg ends
end begin
@Ray-56 Ray-56 added the StudyNotes 读书笔记 label Sep 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
StudyNotes 读书笔记
Projects
None yet
Development

No branches or pull requests

1 participant