You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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
汇编语言 第四版 王爽著(中)
[TOC]
第 7 章 更灵活的定位内存地址的方法
7.1 and 和 or 指令
相应位都为 1,得 1
相应位都为 0,得 0
其实这句话“通过该指令可将操作对象的相应位设为 0”不太理解
7.2 关于 ASCII 码
ASCII编码是世界上众多编码的其中一种
在文本编辑过程中,我们按下键盘中的 a 键,就会在屏幕中看到“a”的过程:
显卡在处理文本信息的时候,是按照 ASCII 码的规则进行的。也就是说要想在显示器看到“a”,就要给显卡的显存中写入“a”的 ASCII 码,61H
7.3 以字符形式给出的数据
在汇编程序中,用
'...'
的方式指明数据是以字符的形式给出的,编译器将它们转化为相对应的 ASCII 码。如下面程序在上面程序中:
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 中的第一个字符串转化为大写,第二个字符串转化为小写
由ASCII 码对应
'A'=41h=01000001b
,'a'=61h=01100001b
可得大小写之间十六进制相差 20h,二进制的第 5 位是否为 0,为 0 则大写,为 1 则小写如果我们使用十六进制则还需要判断字母是否为大写或小写,所以使用二进制第 5 位与 and、or 的特性:
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)
也可以写成:
7.6 用[bx+idata]的方式进行数据的处理
在 codesg 中填写代码,将 datasg 中定义第一个字符串转化为大写,第二个字符串转化为小写
如果用高级语言,比如 C 语言来描述上面的程序,大致是这样:
通过比较可以看到,[bx+idata] 的方式为高级语言实现数组提供了便利机制
7.7 SI 和 DI
si(Source Index)和 di(Destination Index)是 8086CPU 中和 bx 功能相近的寄存器,si 和 di 不能够分成两个 8 位寄存器来使用。
问题 7.2
用 si 和 di 实现将字符串'welcome to masm!'复制到它后面的数据区中
分析:
编写程序大都是进行数据的处理,而数据在内存中存放,所以在处理数据前先搞清楚数据存储在什么地方,也就是内存地址。
要对 datasg 段中的数据进行复制,要进行复制数据的地址是 datasg:0。它要被复制到它后面的数据区,“welcome to masm!”从地址 0 开始存放,长度为 16 个字节,所以它后面的数据区的偏移地址为 16,就是字符串“................”存放的空间。
清楚了地址之后,就可以进行处理了,用 ds:si 指向要复制的源始字符串,用 ds:di 指向复制的目的空间,然后用一个循环来完成复制。代码如下:
在上面程序中,用 16 位寄存器进行内存之间的数据传送,一次复制 2 个字节,一共循环 8 次
问题 7.3
用更少的代码,实现 7.2 中的程序
分析:可以利用
[bx(si或di)+idata]
的方式,来使程序简洁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 不同的寻址方式的灵活应用
比较一下前面用到的几种定位内存地址的方法(可称为寻址方式),就可以发现:
问题 7.9
编程,将 datasg 段中的每个单词的前 4 个字母改为大写字母
分析:
dagasg 中数据存储结构,如图:
总结
这一章,主要讲解了更灵活的寻址方式的应用和一些编程方法,主要内存有:
第 8 章 数据处理的两个基本问题
(1)处理的数据在什么地方?
(2)要处理的数据有多长?
这两个问题,在机器指令中必须给出明确或隐含的说明,否则计算机就无法工作。本章中针对 8086CPU 对着两个基本问题进行讨论
后面的课程中使用 reg 来表示一个寄存器,sreg 表示一个段寄存器
8.1 bx、si、di 和 bp
前 3 个寄存器我们已经用过了,现在进行一下总结
(1)在 8086CPU 中,中有这 4 个寄存器可以用在“[...]”中来进行内存单元的寻址。比如下面指令都是正确的:
而下面的指令都是错误的:
(2)在[...]中,这 4 个寄存器可以单个出现,或只能以 4 种组合出现:
(3)只要在[...]中使用寄存器 bp,而指令中没有显性地给出段地址,段地址就默认在 ss 中。比如下面指令:
8.2 机器指令处理的数据在什么地方
绝大部分机器指令都是进行数据处理的指令,处理大致可分为 3 类:读取、写入、运算。
在机器指令着一层来讲,并不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。
指令在执行前,索要处理的数据可以在 3 个地方:CPU 内部、内存、端口(端口在后面的课程中进行讨论)
8.3 汇编语言中数据位置的表达
汇编语言中用 3 个概念来表达数据的位置
(1)立即数(idata)
对于直接包含在机器指令中的数据(执行前在 CPU 的指令缓冲器中),在汇编语言中称为:立即数,在汇编指令中直接给出。例如:
(2)寄存器
指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。例如:
(3)段地址(SA)和偏移地址(EA)
指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出 EA,SA 在某个段寄存器中。
存放段地址的寄存器可以使默认的,例如:
8.4 寻址方式
当数据存放在内存中的时候,我们可以用多种方式来给定这个内存单元的偏移地址,这种定位内存单元的方法一般被称为寻址方式
8.5 指令要处理的数据有多长
8086CPU 的指令,可以处理两种尺寸的数据,byte 和 word。所以在机器指令中要指明,指令进行的是字操作还是字节操作。
X ptr
指明内存单元的长度,X
在汇编指令中可以为 word 或 byte8.6 寻址方式的综合应用
下面通过一个问题来进一步讨论各种寻址方式的作用
关于 DEC 公司的一条记录(1982年)如下。
这些数据在内存中以图 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 公司的信息有了如下变化:
编程修改内存中的过时数据。分析一下要修改的内容为:
从要修改的内容,可以逐步确定修改的方法:
要修改的产品字段的是一个字符串(或一个数组),需要访问字符串中的每一个字符。所以要进一步确定每一个字符在字符串中的位置
依据上面的分析,程序如下:
用 C 语言来描述这个程序大致应该是这样的:
我们在按照 C 语言的风格,用汇编语言写一下这个程序,注意和 C 语言相关语句的对比:
可以看到,8086CPU 提供的如[bx+si+idata]的寻址方式为结构化数据的处理提供了方便。使得我们可以在编程的时候,从结构化的角度去看待所要处理的数据。
从上面课程看到,一个结构化的数据包含了多个数据项,而数据项的类型又不相同,字型、字节型、数组(字符串)。一般来说,可以用[bx+idata+si]的方式来访问结构体中的数据。用 bx 定位整个结构体,用 idata 定位结构体中的某一个数据项,用 si 定位数组中的每个元素。
为此,汇编语言提供了更为贴切的书写方式,如:[bx].idata、[bx].idata[si]
8.7 div 指令
div 是除法指令,是用 div 做除法的时候应该注意以下问题:
格式如下:
现在,我们可以用多种方法来表示一个内存单元了,比如下面例子:
数学中
[]
中括号表示为:一个式子中有了小括号,再要用括号的话,外面就要用中括号编程,利用除法指令计算 10001/100
分析:被除数 100001 大于 65536,不能用 ax 寄存器存放,所以只能用 dx 和 ax 两个寄存器联合存放,也就是要进行 16 位的除法。除数 100 小于 255,可以在一个 8 位寄存器中存放,但是,因为被除数是 32 位,除数应为 16 位,所以用一个 16 位寄存器来存放除数 100.
因为要分别为 dx 和 ax 赋 100001 的高 16 位值和低 16 位值,所以先将 100001 表示为 16 进制形式:
186A1H
。程序执行后,(ax)=03e8h=1000,(dx)=1 余数为 1。
编程,利用除法指令计算 1001/100
分析:被除数 1001 可用 ax 寄存器存放,除数 100 可用 8 位寄存器存放,也就是说,要进行 8 位的除法。程序如下:
程序执行后,(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 年的基本情况如下:
下面程序中,已经定义好了这些数据:
编程,将 data 段中的数据按如下格式写入到 table 段中,并计算 21 年中的人均收入(取整),结果也按照下面的格式保存在 table 段中。
提示,可将 data 段中的数据看成多个数组,而将 table 中的数据看成一个结构型的数组,每个结构型数据中包含多个数据项。可用 bx 定位每个结构型数据,用 idata 定位数据项,用 si 定位数组中的每个元素,对于 table 中数据的访问可采用 [bx].idata 和 [bx].idata[si] 的寻址方式
最终执行结果:
第 9 章 转移指令原理
**可以修改 IP,或同时修改 CS 和 IP 的指令统称为转移指令。**也就是说转移指令就是可以控制 CPU 执行内存中某处代码的指令。
8086CPU 的转移行为有以下几类:
由于转移指令对 IP 的修改范围不同,段内转移又分为:
8086CPU 的转移指令分为一下几类:
这些转移指令的前提条件可能不同,但转移的基本原理是相同的。我们主要通过深入学习无条件转移指令 jmp 来理解 CPU 执行转移指令的基本原理
9.1 操作符 offset
操作符 offse 在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址。比如下面程序:
9.2 jmp 指令
jmp 为无条件转移指令,可以只修改 IP,也可以同时修改 CS 和 IP
9.3 ⭐️️依据位移进行转移的 jmp 指令
jmp short 标号(转到标号处执行指令)
这种格式是段内短转移,它对 IP 的修改范围为 -128~127short
- 符号说明指令进行的是短转移标号
- 是代码段中的标号,指明了指令要转移的目的地,转移指令结束后,CS:IP 应该指向标号处的指令程序 9.1:
程序 9.2:
比较程序 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 执行指令的过程
按照 CPU 执行指令的过程,参照程序 9.2 中的 jmp short s 指令的读取和执行过程:
EB 03
(jmp short s
的机器码)EB 03
进入指令缓冲器add ax,1
EB 03
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位位移还有一种和
jmp short 标号
功能相近的指令格式,jmp near ptr 标号
,它实现的是段内近转移。(IP)=(IP)+16位位移9.4 转移的目的地址在指令中的 jmp 指令
jmp far ptr 标号
实现的是段间转移,又称为远转移。功能:(CS)=标号所在段的段地址;(IP)=标号在段中的偏移地址程序 9.3
在 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
是偏移地址:010BH9.5 转移地址在寄存器中的 jmp 指令
指令格式:jmp 16位reg
功能:(IP)=(16位reg)
9.6 转移地址在内存中的 jmp 指令
转移地址在内存中的 jmp 指令的两种格式:
(1)jmp word ptr 内存单元地址(段内转移)
功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。内存单元地址可用寻址方式的任一格式给出
(2)jmp dword ptr 内存单元地址(段间转移)
功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。内存单元地址可用寻址方式的任一格式给出
(CS)=(内存单元地址+2);(IP)=(内存单元地址)
检测点 9.1
(1)程序如下
若要使程序中的 jmp 指令执行后,CS:IP 指向程序的第一条指令,在 data 段中应该定义哪些数据
(2)程序如下
补全程序,是 jmp 指令执行后,CS:IP 指向程序的第一条指令
(3)用 Debug 查看内存,结果如下:
则此时,CPU 执行指令:
后,(CS)=___, (IP)=___。
答案为:
0006H:BE
9.7 jcxz 指令
jcxz 指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对 IP 的修改范围都为:-128~127
指令格式:
jcxz 标号
;如果(cx)=0),转移到标号
处执行操作:当 (cx)=0 时,(IP)=(IP)+8位位移
当 (cx)≠0 时,什么也不做(程序向下执行)。
从 jcxz 的功能中可以看出,
jcxz 标号
的功能相当于:if((cx)==0) jmp short 标号
。(这种用 C 语言和汇编进行的综合描述,或许对理解有条件转移指令更加清楚)检测点 9.2
补全程序,利用 jcxz 指令,实现在内存 2000H 段中查找第一个值为 0 的字节,找到后,将它的偏移地址存储在 dx 中
9.8 loop 指令
loop 指令为循环指令,所有的循环指令都是短转移
指令格式:
loop 标号
操作分两步:
标号
处执行检测点 9.3
补全程序,利用 loop 指令,实现在内存 2000H 段中查找第一个值为 0 的字节,找到后,将它的偏移地址存储的 dx 中
9.9 根据位移进行转移的意义
方便程序段在内存中的浮动装配。TODO: 对于这里的深入理解
9.10 编译器对转移位移超界的检测
实验8 分析一个奇怪的程序
分析下面程序,在运行前思考:这个程序可以正确返回吗?运行后再思考:为什么是这种结果?
**编译器先进行编译得到机器码,再执行时按照编译后的机器码执行,并不会动态更改。**执行到 s2:
jmp short s1
时的机器码为 BEF6,也是就向前移动 10 个字节即mov ax 4c00h
可以正常执行实验9 根据材料编程
编程:在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串“welcome to masm!”。
由图可以得到
00000010B=02h
:绿字00100100B=24h
:红字绿底01110001B=71h
:蓝字白底第 10 章 CALL 和 RET 指令
call 和 ret 指令都是转移指令用来修改 IP,或同时修改 CS 和 IP。经常被共同用来实现子程序的设计
10.1 ret 和 retf
CPU 执行时进行下面两步操作
用汇编语法来解释
CPU 执行时进行下面四步操作(先入栈 cs,后入栈 ip)
用汇编语法来解释
例:下面程序中,ret 指令执行后,(IP)=0,CS:IP 指向代码段的第一条指令
下面程序中,retf 指令执行后,CS:IP 指向代码的第一条指令
检测点 10.1
补全程序,实现从内存 1000:0000 处开始执行指令
10.2 call 指令
CPU 执行 call 指令时,进行两步操作:
call 指令不能实现短转移,除此之外 call 指令实现转移的方法和 jmp 指令的原理相同
10.3 依据位移进行转移的 call 指令
call 标号
:将当前的 IP 入栈后,转到标号
处执行指令CPU 执行此种格式的 call 指令是,进行如下操作:
((ss)*16+(sp))=(IP)
16 位位移=标号处的地址-call指令后的第一个字节的地址
16 位位移的范围为 -32768~32767,用补码表示
16 位位移由编译程序在编译时算出
用汇编语法拉解释此种格式的 call 指令为:
检测点 10.2
下面程序执行后,ax 中的数值为多少?
10.4 转移的目的地址在指令中的 call 指令
call far ptr 标号
实现的是段间转移((ss)*16+(sp))=(CS)
(sp)=(sp)-2
((ss)*16+(sp))=(CS)
(IP)=标号所在段中的偏移地址
用汇编语法来解释此种格式的 call 指令
检测点 10.3
下面程序执行后,ax 中的数值为多少?
10.5 转移地址在寄存器中的 call 指令
格式:
call 16位reg
功能:
用汇编语法来解释:
10.6 转移地址在内存中的 call 指令
有两种格式
call word ptr 内存单元地址
用汇编来解释:
call dword ptr 内存单元地址
用汇编来解释:
10.7 call 和 ret 的配合使用
具有一定功能的程序段,称为子程序。
在需要的时候,用 call 指令转去执行。可是执行完子程序后,如何让CPU接着 call 指令向下执行?
call 指令转去执行子程序之前,call 指令后面的指令的地址将存储在栈中,所以可在子程序后面使用 ret 指令,用栈中的数据设置 IP 的值,从而转到 call 指令后面的代码处继续执行。子程序框架如下:
具有子程序的源程序框架如下:
10.8 mul 指令
mul
是乘法指令,注意一下两点格式如下:
举例:
(1)计算 100*10
100 和 10 小于 255,可以做 8 位乘法,程序如下
结果:(ax)=1000=03E8H
(2)计算 100*10000
100 小于 255,可 10000 大于 255,所以必须做 16 位乘法,程序如下
结果:(ax)=4240H (dx)=000FH;结果是 16 位,高位存在 DX 中,1000000=000F4240H
10.9 模块化程序设计
call 与 ret 指令共同支持了汇编语言中的模块化设计。
10.10 参数和结果传递的问题
子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者。
比如,设计一个子程序,可以根据提供的 N,来计算 N 的 3 次方。这里面就有两个问题:
很显然,可以用寄存器来存储,将参数放在 bx 中;因为子程序中要计算 N*N*N,可以使用多个 mul 指令,为了方便,可以将结果放在 dx 和 ax 中。子程序如下:
用寄存器来存储参数和结果是最常使用的方法。对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:调用者将参数送入寄存器,从结果寄存器中取到返回值;子程序从参数寄存器中取到参数,将返回值送入结果寄存器
10.11 批量数据的传递
将批量数据放到内存中,然后将它们所在的内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可用同样的方法。
将字符串的首地址传递给子程序,然后在子程序中使用 loop ,次数(cx)就是字符串的长度,这样就可以批量传递数据。
编程,将 data 段中的字符串转化为大写
注意,除了用寄存器传递参数外,还有一种通用的方法是用栈来传递参数。参看 附录4
10.12 寄存器冲突的问题
设计一个子程序,功能:将一个全是字母,以 0 结尾的字符串,转化为大写。字符串可以定义为:
db 'conversation',0
用 jcxz 来检测 0,代码如下:
captial
子程序的应用问题 10.2
子程序
captial
思想上没有问题,但是细节上有错误:在 cx 的使用,主程序要使用 cx 记录循环次数,可子程序中也使用了 cx,在执行子程序时,cx 中保存的循环计数值也被改变,使得主程序的循环出错。从上面得问题中,引出了一个一般化的问题:子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的冲突
子程序要实现以下目的:
解决者个问题的简洁方法是:在子程序的开始将子程序中所有用到的寄存器中的内存都保存起来,在子程序返回前再恢复。可以用栈来保存寄存器中的内存
以后,编写子程序的标准框架如下:
按照上面框架改进一下子程序
captial
:实验10 编写子程序
显示字符串
子程序描述:
提示:
解决除法溢出的问题
TODO:
数值显示
TODO:
课程设计1
TODO:
第 11 章 标志寄存器
8086CPU 的标志寄存器(以下简称为 flag)有 16 位,其中存储的信息通常被称为程序状态字(PSW),有以下三种作用:
11.1 ZF 标志
零标志位,第 6 位。它记录相关指令执行后,结果是否为 0
注意,在 8086CPU 的指令集中:
在使用一条指令的时候,要注意这条指令的全部功能,其中包括,执行结果对标志寄存器的哪些标志位造成影响
11.2 PF 标志
奇偶标志位,第 2 位。它记录相关指令执行后,结果的所有 bit 位中 1 的个数是否为偶数
bit 为中 1 的个数为:
11.3 SF 标志
符号标志位,第 7 位。它记录相关指令执行后,结果是否为负
检测点 11.1
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例:
先看一下 CF 的含义。在执行 adc 指令的时候加上 CF 的值的含义,是由 adc 指令前面的指令决定的,也就是说,关键在于所加上的 CF 值是被什么指令设置的。如果 CF 的值是被 sub 指令设置的,那么它的含义就是借位值;如果是被 add 指令设置的,那么它的含义就是进位值。
看一下两个数据 0198H 和 0183H 如何相加的:
从图可以看出,加法可以分两步来进行:
下面指令和
add ax,bx
具有相同的结果:看来 CPU 提供 adc 指令的目的,就是用来进行加法的第二部运算的。adc 指令和 add 指令相配合就可以对更大的数据进行加法运算。来看一个例子:
编程,计算 1EF000H+201000H,结果放在 ax(高16位)和 bx(低16位)中。
因为两个数据的位数都大于 16,用 add 指令无法进行计算。我们将计算分为两步进行,先将低 16 位相加,然后将高 16 位和进位值相加。程序如下:
adc 指令执行后,也可能产生进位值,所以也会对 CF 位进行设置。由于有这样的功能,我们就可以对任意大的数据进行加法运算。
11.7 sbb 指令
sbb 是带借位减法指令,它利用 CF 位上记录的借位值。
指令格式:
sub 操作对象1, 操作对象2
功能:操作对象1 = 操作对象1 - 操作对象2 - CF
比如指令:
sub ax,bx
实现的功能是:(ax)=(ax)-(bx)-CFsbb 指令执行后,将对 CF 进行设置。用力 sbb 指令可以对任意大的数据进行减法运算。例如,计算 003E1000H-00202000H,结果放在 ax,bx 中,程序如下:
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下面指令:
通过 cmp 指令执行后,相关标志位的值就可以看出比较的结果,
cmp ax,bx
现在可以看出比较指令的设计思路,即:通过做减法运算,影响标志寄存器,标志寄存器的相关位记录了比较的结果
同 add、sub 指令一样,CPU 在执行 cmp 指令的时候,也包含两种含义:
下面我们在来看一下如果用 cmp 进行有符号数比较时,CPU 用哪些标志位 对比较结果进行记录。
比如:
结果 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 的值是如何来说明比较的结果的。of=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负
因 sf=1,实际结果为负,所以逻辑上真正的结果为负,(ah)<(bh)
of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负
因 sf=1,实际结果为负
实际结果为负,而又有溢出,这说明是由于溢出导致了实际结果为负,简单分析一下,就可以看出,如果因为溢出导致了实际结果为负,那么逻辑上真正的结果必然为正
这样,sf=1、of=1,说明了 (ah)>(bh)
of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负
因 sf=0,实际结果非负。而 of=1 说明有溢出,则结果非 0,所以实际结果为正
实际结果为正,而又有溢出,说明是由于溢出导致了实际结果非负。如果因为溢出导致了实际结果为正,那么逻辑上真正的结果必然为负
这样,sf=0、of=1,说明了 (ah)<(bh)
of=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负
因 sf=0,实际结果非负,所以逻辑上真正的结果非负,所以 (ah)≥(bh)
11.9 检测比较结果的条件转移指令
“转”移指的是它能够修改 IP,“条件”指的是它可以根据某种条件决定是否修改 IP
下面是常用的根据无符号数的比较结果进行转移的条件转移指令,第一个字母都是 j,表示 jump
je
jne
jb
jnb
ja
jna
观察得到,它们所检测的标志位,都是 cmp 指令进行无符号数比较的时候,记录比较结果的标志位。
编程实现功能:如果 (ah)=(bh) 则 (ah)=(ah)+(ah),否则 (ah)=(ah)+(bh)
je 检测的是 zf 位置,不管前面是什么指令,只要执行 je 时 zf=1,就发生转移,比如:
执行后,(ax)=1。
add ax,0
使得 zf=1,所以 je 指令将进行转移。可在这个时候发生的转移的确不带有“相等则转移”的含义。虽然此处 zf=1 不是由 cmp 比较指令设置的,不具有“俩数相等”的含义,但是 je 指令执行时只要 zf=1 就可以发生转移。后面我们可以只考虑 cmp 和 je 等指令配合使用所表现的逻辑含义。
来看下面一组程序,data 段中的 8 个字节如下:
(1)编程,统计 data 段中数值为 8 的字节的个数,用 ax 保存统计结果。
编程思路:初始设置 (ax)=0,然后循环依次比较每个字节的值,找到一个和 8 相等的数就将 ax 的值加 1。程序如下:
(2)编程,统计 data 段中数值大于 8 的字节的个数,用 ax 保存统计结果
编程思路:初始设置 (ax)=0,然后用循环依次比较每个字节的值,找到一个大于 8 的就将 ax 的值加 1。程序如下:
检测点 11.3
(1)补全下面程序,统计 F000:0 处 32 个字节中,大小在 [32,128] 的数据的个数
(2)补全下面程序,统计 F000:0 处 32 个字节中,大小在 (32,128) 的数据的个数
11.10 DF 标志和串传送指令
方向标志位,在 flag 第 10 位。在串处理指令中,控制每次操作后 si、di 的增减
格式:
movsb
功能:执行 movsb 指令相当于进行下面几步操作
用汇编语法描述的话:
可以看出,movsb 的功能是将 ds:si 指向的内存单元中的字节送入 es:di 中,然后根据标志寄存器 df 位的值,将 si 和 di 递增或递减
也可以传送一个字,指令如下
movsw
:功能是将 ds:si 指向的内存单元中的字送入 es:di 中,然后根据标志寄存器 df 位的值,将 si 和 di 递增 2 或递减 2一般来说 movsb 和 movsw 都和 rep 配合使用,格式如下:
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 结尾的字符串中的小写字母转变成大写字母,描述如下。
The text was updated successfully, but these errors were encountered: