【文档说明】(完整)32位汇编语言程序设计17稿b.ppt,共(768)页,5.668 MB,由小橙橙上传
转载请保留链接:https://www.ichengzhen.cn/view-2110.html
以下为本文档部分文字说明:
2022/11/12132位汇编语言程序设计计算机科学与信息工程学院赵建州2022/11/122前言《32位汇编语言程序设计》是计算机软件课程。也是计算机专业学生的必修课程,与其他程序设计语言相比,汇编语言对机器的硬件层封装最
少,在操作系统的控制下允许程序员最大限度地直接访问计算机硬件。汇编语言从语言的角度逼真地描述了微处理器的体系结构,从软件角度描述了计算机系统硬件层的运行规则。不同的CPU体系有不同的汇编语言。本课介绍的是Intel80X86CPU系列的32位汇编语言。2022/11/123在
学习计算机专业其他课程时,会遇到一些该课程不能解答的问题:常数为什么不能修改,数据为什么要有数据类型,指针为什么也要有数据类型,为什么C/C++语言中数组的下标从0开始,为什么不能用值传递参数或自动局
部变量从函数带回信息,函数的形参变量和自动局部变量放在内存的哪一部分,它们为什么会随着函数的结束而消失,C/C++语言中为什么要区分整数运算和浮点数运算等等,通过学习32位汇编语言程序设计,会帮助你理解许多其他课程留下的问题。2022/11
/124第一章汇编语言单词第二章CPU映像和机器数第三章内存数据映像第四章操作数寻址方式第五章输入输出函数第六章整型运算第七章整型控制结构结束放映2022/11/125第八章浮点型运算第九章指针和字符串第十章数组和串第十二章过程结束放映2
022/11/126第一章汇编语言单词1.1汇编语言字符集和单词1.2注释符1.3专用符号1.4保留字1.5标识符1.6汇编语言程序格式2022/11/1271.1汇编语言字符集和单词汇编语言采用ASCII码字符作为自己语言的字符集。汇编语言的单词由一个或
多个ASCII码字符组成,它们对于汇编程序有预定的语法意义。汇编语言的单词包括注释符、专用符号、保留字、标识符等。2022/11/1281.2注释符汇编语言的注释符分单行注释符和跨行注释符。单行释符//可以放在程序某行中任何位置
,编译程序忽略从//开始到行尾的所有内容。跨行注释符/**/编译程序忽略从/*开始到*/之间的所有内容。2022/11/129例1:注释的用法(1)跨行注释符/*下面的程序段对数组实施快速排序算法*/……m
ov(365,ecx);/*给计数器赋初值*/(2)单行注释符mov(365,ecx);//给计数器赋初值2022/11/12101.3专用符号专用符号主要包括汇编语言的运算符、分隔符、数制标识符。专用符号有一个字符的也有两个字符的。下面是一个字符的专用符号。*/+-()[]:
;,.=&|^!@$%'"{}下面是两个字符的专用符号。&&||<=>=<>!===:=..2022/11/1211例2:专用符号举例mov(&data,eax);stdout.put("32位汇编语言",nl);stdout.put("pi=",pi:10:3,nl);stdout.p
ut($6f,nl);stdout.put(%10101010,nl);2022/11/12121.4保留字保留字也称关键字,保留字包括CPU中的寄存器名,汇编语言的指令助记符,语句,数据类型名称等。汇编语言的保留字不区分大小
写。2022/11/1213CPU中的寄存器名ALAHAXEAXBLBHBXEBXCLCHCXECXDLDHDXEDXSIESIDIEDIBPEBPSPESPST0ST1ST2ST3ST4ST5ST6ST7202
2/11/1214指令助记符和语句ADDINCADCSUBDECCMPNEGSBBMULIMULINTMULDIVIDIVMODIMODIFELSEENDIFWHILEENDWHILEFORENDFORFOREVERENDFORREPEATUNTIL20
22/11/1215数据类型名称int8int16int32uns8uns16uns32real32real64real80byteworddwordqwordlwordcharstringcsetarrayboolean
2022/11/1216其他符号programbeginendproceduretypevalstaticvarreadonlyconst……如果想了解汇编语言专用符号和保留字的更多的相关信息,请参考HLA手册。2022/11/12171.5标识符
标识符可用作程序名、变量名、常量名、函数名、过程名、标号等。汇编语言的标识符必须以字母或下划线开始,后面可跟字母、数字、下划线。由于受MASM的限制,标识符的长度不能超过236个ASCII字符。汇编语言的标识符区分大小写字母。标
识符不能与保留字同名。2022/11/1218例3:合法的标识符和非法的标识符(1)合法的标识符addtionbigfirstlasta1s1smallSmall(2)非法的标识符%a15sALs[1]2022/11/12191.6汇编语言程序格式program程序名
;<<各种数据段声明>>begin程序名;<<程序指令序列>>//代码段end程序名;2022/11/1220程序中的program,begin,end是汇编语言的保留字。程序名要按标识符取名。声明段用来声明各种变量、常量为它们赋予初
值;还用来声明过程。begin和end之间是代码段。汇编语言提供了输入输出标准库,为了使用输入输出标准库,必须在程序开始用包含语句把标准库的头文件"stdlib.hhf"包含到程序中来。2022/11/1221例3:显示“汇编语言编程开始了!
”的汇编语言程序programHelloWorld;#include("stdlib.hhf")//包含标准库头文件beginHelloWorld;stdout.put("汇编语言编程开始了!",nl);stdout.p
ut(“击回车键退出");stdin.readLn();endHelloWorld;2022/11/12222022/11/12232022/11/12242022/11/1225如果用C++编写同样的程序,编写的代码如下:#incl
ude"stdafx.h"#include"stdio.h"intmain(intargc,char*argv[]){printf("汇编语言编程开始了!\n");return0;}2022/11/1226#include"stdaf
x.h"#include"iostream"usingnamespacestd;intmain(intargc,char*argv[]){cout<<"汇编语言编程开始了!"<<endl;return0
;}2022/11/1227汇编语言程序具有占用空间小,运行效率高的优点,与这个汇编语言程序相似的C/C++程序经编译后产生的执行代码约有152KB,而汇编语言编译的结果不到8K,汇编语言程序常用于嵌入系统,控制系统编程,也用于编写设备驱动程序,而设计图形界面不是汇编语言的特长,每
种程序语言都各有擅长,在应用中注意要扬长避短。2022/11/1228第二章机器数和CPU映像2.1整数机器数编码2.2机器整数的加减法运算及溢出问题2.3字符机器数编码2.4Intel80x86CPU映像2022/11/12292.1整数机
器数编码汇编语言语法把二进制整数分为两种,无符号和有符号整数。无符号整数是指数据的每一位都代表数值。在汇编语言程序设计里,内存地址(指针),循环次数,ASCII码等都是无符号整数。数学运算中整数值有正数和负
数之分,在计算机元件级怎样表示正号和负号?答案是只能用数字表示正号和负号。因此必须进行符号和数值混合编码。2022/11/1230(一)原码表示法如果正数的符号用0表示,负数的符号用1表示,绝对值用二进制数表示,这就是原码表示法。假设计算机字长是n位,能表示n-1位有符号整数,设X=Xn
-2Xn-3…X0,原码表示是:0Xn-2Xn-3…X0X≥0[X]源码=1Xn-2Xn-3…X0X≤02022/11/1231例1:设n=8,求二进制数10010,-10010的原码。二进制数10010的原码是000100
10,在此最高位的0代表+号。二进制数-10010的原码是10010010,在此最高位的1代表–号。根据定义,0在原码中有两种表示,所以计算机内整数不采用原码表示法。2022/11/1232(二)补码表示法n位二进制补码的定义是:X0≤X
≤2n-1-1[X]补码=2n+X-2n-1≤X<0X是n-1位二进制整数2022/11/1233二进制数转换为补码二进制正数转换成补码与原码相同。二进制负数转换成补码有两种方法:方法1:根据定义求补码例
2:n=8,求(-1010111)2和-119的补码。[-1010111]补码=28-1010111=100000000-1010111=10101001[-119]补码=28-1110111=100000000-1110111=10001001100000000-10101111
0101001100000000-1110111100010012022/11/1234方法2:写出负数的绝对值,求绝对值的补码,然后对每位取反(包括符号位),末位加1。例3:n=8,求(-1010111)2和-119的补码。计算(-1010111)2的补码1.求负数绝对值10101112
.求补码01010111//n位补码3.取反码101010004.加1101010012022/11/1235计算-119的补码1.十进制换成二进制-11101112.求负数绝对值11101113.求补码011101114.取反码100010005.末位加1100
010012022/11/12360在补码中只有一种表示法。整数数值在计算机元件级用二进制补码表示。注意补码表示是非对称性的。8位补码表示有符号数的范围是-128~127,16位补码表示有符号数的范围是-32768~32767。
我们将补码逐位取反,再加1的运算称为求补运算。正数的补码经过求补运算结果是它相反数的补码;负数的补码经过求补运算结果也是它相反数的补码。2022/11/1237从机器数计算它的真值对于正数,直接用权的多项式展开求和。例:已知n=8位
,求01001010的真值用权的多项式展开求和:原式=26+23+21=74对于负数,有两种方法,每种方法有多个计算步骤。2022/11/1238方法一:根据补码的定义[X]补码=2n+X-X=2n-[X]补码注意X本身是负数。方法二:对负数
的补码求补,得到它的绝对值,计算绝对值的权的多项式的和,然后对和实施取负运算。2022/11/1239我们看同一个整数补码用不同位数表示时有何差别。例4:求117,-117的8位和16位的补码。117的补码n=801110101n=16000000000111
0101-117的补码n=810001011n=161111111110001011从例子中得出:一个8位补码数要变成16位补码数时,只需用它的符号位的值填满高8位。这个操作称为符号扩展。2022/11/1240因为在计算机元件级无法区分一个二进
制数是有符号的还是无符号的。所以在机器内一个二进制数可代表有符号数,也可代表无符号数。例如:假定n=8位,机器内有个整数%10001010,它可以代表无符号数138;也可以代表有符号数-118。汇编语言程序员对程序中的
整数哪些是无符号的,哪些是有符号的必须一清二楚。2022/11/12412.2机器整数加减法运算及溢出问题(一)零扩展和符号扩展计算机内整数的加法和减法运算要求参加运算数据的长度要相同。如果参加运算的数据长度不同时,在
运算之前要把位数短的扩展为长的,否则运算结果可能是错误的。对于无符号的数,高位部分全部用0填满(零扩展);对于有符号的数,高位部分全部用符号值填满(符号扩展)。加减运算时符号值与数值一样参加运算。2022/11/1242例5:计算两个无符号数加法1010+01100000分析:无
符号数位数不同时,位数短的必须进行零扩展,即高4位必须填满0。1.左加数零扩展000010102.加右加数011000003.运算结果011010102022/11/1243例6:计算两个有符号数的加法00001000+
1100即(8+(-4))分析:有符号数位数不同时,必须进行符号扩展。因为右加数最高位是1,代表负号,把它变成8位时,高4位必须填满符号值1。1.左加数不变000010002.右加数符号扩展111111003.运算结果100000100因为数据
位指定为8位,进位丢失,和等于00000100即4,结果正确。2022/11/1244如果不进行符号扩展,计算两个有符号数加法00001000+11001.左加数不变000010002.右加数不符号扩展11003.运算的结果00010100结果等于20,
显然是错误的。2022/11/1245(二)整数加减运算可能出现的溢出问题在计算机内运算与人类运算最大的不同之一是计算机只能用有限位数表示数值。如果运算结果超出了有限位数,称为溢出。溢出是一种错误,运算到此只能中断,要程序员修改程序后,重新编译,
再执行。2022/11/1246无符号数加减运算和有符号数加减运算都有可能发生溢出。对于无符号数的加法,运算中最高位如产生进位;对于无符号数减法,运算中最高位如产生了借位,这都是溢出。2022/11/1247例7:n=8,计算130+145
1.130的机内表示100000102.145的机内表示100010013.运算的结果100001011运算中最高位产生进位,由于机器内数据规定是8位,进位丢失,这就是溢出,结果是错误的。2022/11/1248例8:n=8,计算130-1451.130
的机内表示100000102.145的机内表示100010013.运算的结果11111001运算中最高位产生借位,由于机器内数据规定是8位,借位丢失,这也是溢出,结果是错误的。2022/11/1249根据补码定义和数学公理,有符号数加减法溢出可能发生在两个同号
数相加或两个异号数相减的时候。如果两个同号数相加,结果的符号与加数的符号相反;两个异号数相减,结果的符号与减数的符号相同时,这就是溢出。因为,两个正数相加结果绝对不可能是负数;同样一个负数减正数,结果绝对不可能是正数等等2022/
11/1250例9:n=8,计算72+981.72的机内表示010010002.98的机内表示011000103.运算的结果10101010结果不是正数,而是负数-86。显而易见结果是错误的。2022/11/1251例10:n=8,计算-83+
(-80)1.-83的机内表示101011012.-80的机内表示101100003.运算结果101011101由于机器是8位,结果的最高位丢失,结果变成正数93,是错误的。2022/11/1252例11:n=8,计算72-(-98)1.72的机内表示0
10010002.-98的机内表示100111103.运算的结果10101010结果与减数符号相同,正数减负数结果是负数。它的值是-86。结果是错误的。2022/11/1253例12:n=8,计算-83–801.-83的机内表示101011012.80的机内表示010100003.
运算的结果01011101结果与减数符号相同,负数减正数结果是正数。它的值是93。结果是错误的。2022/11/12542.3字符型机器数编码在计算机内,字符也必须用二进制数编码表示。最通用的字符是ASCII码字符,
它是美国标准信息交换码。它用二进制代码规定了英文字母、阿拉伯数字、算术运算符、英文标点符号和其他常用符号以及控制符号的表示,ASCII码包括了128个字符,具体内容见本书附录1。2022/11/1255中国在1980年发表GB2312-80信息交换码标准
,这一标准后来被国际标准化组织接纳为国际标准,它是个双字节编码标准,一个汉字用两个字节表示,每个字节只用了低七位,每个字节的最高位值是0。因为ASCII码字符用一个字节的低七位表示,最高位的值也是0,而且它已经是机器数编码了,所以为了在机器里区分汉字和ASCII码,在机器里把汉字的GB2312码的
每个字节的最高位的值规定为1后作为汉字的机器码。2022/11/1256例13:输出汉字的机器码例如汉字“水”,它的机器码值是$AE_CB,要把机器码变成GB2312-80码,用机器码减$80_80即可,“水”字的GB2312码值是$2E_4B。2022/11/12572.4
Intel80x86映像Intel80x86CPU系列属于VonNeumann结构的机器,VonNeumann计算机体系结构包括三个主要部分:中央处理单元(CPU),存储器,输入/输出设备(I/O)。这三部分由系统总线连在一起,见图2.1。系统总线包括控制总线、数据总线、地址总线。2
022/11/1258中央处理机CPU总线控制逻辑接口接口内存大容量外存储器I/O设备I/O子系统系统总线......图2.1计算机硬件结构2022/11/1259从80486开始,Intel把浮点运算协处理器集成到CPU内,在CPU内称它为浮点运算单元(FPU),现暂不介绍浮点运算
单元。CPU内部最显著的部件是寄存器。IntelCPU寄存器可分为四类:段寄存器,内核模式专用寄存器,通用寄存器,应用程序可访问的专用寄存器。2022/11/1260段寄存器组包括CS、DS、SS、ES、FS、GS等6个16位寄存器。CS是代码段寄存器,SS是堆栈段寄存器,其余的
是数据段寄存器。内核模式专用寄存器供系统编程使用,这里不做介绍。2022/11/1261图2.280x86通用寄存器2022/11/1262通用寄存器组包括EAX,EBX,ECX,EDX,ESI,EDI,EBP和ESP8个32位寄存器(图3.2)。每个名字开头字母“
E”代表扩展(extended)。这个前缀用来区分8个32位寄存器和以下8个16位寄存器:AX,BX,CX,DX,SI,DI,BP和SPAX,BX,CX,DX又可分成8个8位寄存器:AL,AH,BL,BH,CL,CH,DL和DHIntelCPUs的这些寄存器不都是独立的寄存器
,当修改一个寄存器时则至少同时修改了另一个寄存器。2022/11/1263应用程序可访问的专用寄存器(1)标志寄存器Eflag它是32位寄存器。Bit0CF最高位有进位或借位时置1Bit2PF低8位有偶数个1时置1Bit4AFD3位有进位或借位时置1Bi
t6ZF运算结果是零时置131..22131211109876543210CFPFAFZFSFTFIFDFOFIOPL保留2022/11/1264Bit7SF运算结果最高位是1时置1Bit8TF如置1每执行一条指令发生中断Bit9IF如置1允许响应可屏蔽中断Bit10DF请参考第十一章Bit1
1OF指令执行后发生溢出时置12022/11/1265很多指令执行后会影响条件标志,尤其在进行加减运算时,CPU并不知操作数是有符号还是无符号数,它只是按照运算结果设置条件标志,需要程序员自己来判断运算结果是否正确。2022/11/1266例14:计算010010
11+00111000左加数01001011右加数00111000相加结果10000011运算结果使AF=1,OF=1,SF=1,ZF=0,CF=0,PF=0。如果这两个数是无符号数,运算结果是正确的,因为没发生溢出(CF=0);如果这两个数是有符号数,
运算结果是错误的,因为发生了溢出(OF=1)。这要由程序员自己判断。计算机硬件不保证运算结果是否正确,只负责按结果设标志。2022/11/1267(2)指令计数器EIP指令计数器EIP是32位寄存器,它存放着下一条要执行的机器指
令的偏移地址。程序装入内存时,由操作系统负责把第一条指令的偏移地址装入EIP,CPU每取一条指令就自动修改EIP,将它的值变成下一条指令的偏移地址。由于EIP起着直接保证程序正确执行的关键作用,在80x86指令集中看不见EIP2022/11/1268偏移
地址是为了方便计算机内存系统管理而引入的一个逻辑地址概念,因为程序员在编程时,无法知道将来程序运行时放在内存的哪个位置,所以让编译程序在编译源程序时,以字节为单位按顺序给每条指令、每个数据赋予一个逻辑
地址,并且每个段都从零开始编址,这个逻辑地址就称作偏移地址。在程序执行中,由内存管理机构将逻辑地址映射到真正的物理地址。这部分内容将在寻址方式一章中介绍。指令B指令A代码段042022/11/1269(3)堆栈指针寄存器ESP32位寄存器ESP它只用来作为堆栈栈顶指
针。它是栈顶的偏移地址。堆栈操作指令都以它的值访问堆栈。(4)堆栈指针、数据寄存器EBP32位EBP寄存器除了可用作数据寄存器外,还可用作访问堆栈的指针寄存器,也是具有特殊用途的寄存器。2022/11/1270……堆栈段栈顶指针ESP基址指针EBP016k20
22/11/1271第三章内存数据映像3.1基本数据类型3.2常量3.3变量3.4程序分段2022/11/12723.1基本数据类型汇编语言基本数据类型uns8无符号的8位整数uns16无符号的16位
整数uns32无符号的32位整数int8有符号的8位整数int16有符号的16位整数int32有符号的32位整数boolean布尔型,占一个字节char字符类型,占一个字节2022/11/1273real3232位浮点数rea
l6464位浮点数real8080位浮点数byte通用的8位类型tbyte通用的80位类型word通用的16位类型dword通用的32位类型qword通用的64位类型lword通用的128位类型2022/11/1274汇编
语言复合数据类型string字符串(字符串变量实际是4个字节长的指针)text与字符串相似,文本常数常用于指令中,减小输入击键次数2022/11/12753.2常量3.2.1数值常数和布尔型常数3.2.2字符串和字符常量3.2.3符号常量3.2.4常量表达式2022/11/1276常量是编
译程序时,已经有确定的值,在程序运行中这个值不能改变。每个常量具有数据类型,常量的类型可以用HLA的任何一种类型。注意语法认为常量的数据类型是不明确的。2022/11/12773.2.1数值常数和布尔型
常数(1)十进制整数进位计数制是常用的计数方法。人们习惯使用十进制,每位的数字可以是0到9中的任何一个,它的基数是十,计数时逢十进一。这也是十进制的名称由来。2022/11/1278在程序里可以用十进制整数。例1:123,32_123,0,-23高级汇编语言允许数值中用下划线作为分隔符,便于人们
阅读。从语法上讲,数值前可有正号或负号,数值中允许有下划线,从语法上讲,下划线的位置可以在任意两个数字之间,在实际应用中将下划线作为千分号,使人们阅读方便。2022/11/1279(2)二进制整数在数字电子计算机内,每位数字要用一个电子元件(如晶体管)的一个物理状态
来表示,通常一个电子元件存在两种稳定的状态(如晶体管的饱和与截止),而一位二进制也存在两种状态0和1。一个电子元件恰好可以表示一位二进制,所以在数字电子计算机内使用二进制。一个数值无论用二进制还是十进制表示,其本质是代表一个值。2022/11
/1280在程序里可以用二进制整数。高级汇编语言要求以%前缀开始,不能带符号。例2:%1101_0001高级汇编语言允许在数值中用下划线作为分隔符,便于人们阅读。2022/11/1281n位m进制可以表示mn种状态。如三位二进制可以表示23种状态:%000%001%010%011%100%101
%110%1112022/11/1282十六进制整数用二进制表示数字对程序员来讲阅读和书写都不方便,为了方便阅读和书写程序,在汇编语言源程序里还可以用十六进制表示数值。汇编语言要求以$前缀开始,不能带符号。例3:$123,$12_1ae6高级汇编语言允许在数值中用下划线作为
分隔符,便于人们阅读。2022/11/1283在汇编语言源程序里程序员可以用上面三种数制中的任何一种表示自己的数据,其实机器只识别二进制,源程序中用非二进制表示的数由汇编程序或编译程序转换成二进制数。2022/11/1284(4)实型常数H
LA的浮点类型常数表示如下:(1)一个“+”或“-”表示尾数的符号。如没有符号,HLA默认它是正数。(2)跟随一位或多位十进制数。(必选项)(3)跟随小数点和一位或多位十进制数(可选项)。(4)跟随一个“e”或“E”,其后是符号(“+”或2022/11/1285必须有小数点或”e”/“E”来区分浮
点数和整数注意浮点常量不能以小数点开始,在程序里不能用“.1”表示“0.1”。例4:合法的浮点常量1.234,3.75e2,-1.0,1.1e-1,0.1,-123.456e+789,+25e0,1e+4,1_234_837.25,1_000.00,789_934.999_9
99.992022/11/1286(5)布尔常数HLA预定义两个布尔型常数,true和false在内部,HLA定义true和false值分别是1和0。HLA默认为每个布尔型常量分配一个字节。实际上布尔操
作只看布尔值的#0位而把其他位清零。你不能像在C/C++语言中在用布尔表达式的地方用整型表达式。2022/11/12873.2.2字符串和字符常量(1)字符串常量字符串常量是用双引号括住的零个或多个字符。例5:"32位汇编语言""""a""123"性质1:HLA
自动将程序中相邻的字符串联接起来。性质2:在字符串常量中插入两个双引号来代表一个双引号。2022/11/1288(2)字符常量格式1:用两个单引号括住的一个ASCII码字符。格式2:#整数常数。整数常数可以用十进制、十六进制或二进制,代表字符的AS
CII码值。数值在0到255的范围内,但只显示32到127范围内的字符。如果指定的数值超过0到255的范围,系统在编译时会报错。系统默认为每个字符常量分配一个字节。2022/11/1289性质1:在两个单引号中放两个单引号代表一个单引号。性质2:HLA可以联接任何相邻的字符常量和字符串常量组成一
个字符串常量在程序中,语法认为字符常量的数据类型是不明确的。2022/11/1290例7:'2','a','''',#13,#$d,#%1101上面第三个字符表示一个单引号,后面的三个字符都代表回车。记住在程序中“a”和‘a’是不相同的。2022/11/12913.2.3符号
常量在HLA的CONST段声明符号常量。该段中声明的常量的值从编译到运行都不会改变;该段位于程序中的声明段位置。它以CONST保留字开始。CONST标识符:数据类型:=常数表达式;标识符:=常数表达式;第二种格式是省略数据类型的语法格式,此时系统自动赋予默认数据
类型。HLA按数据类型为符号常量分配内存空间。2022/11/1292在CONST段声明常量时,必须对其初始化。:=在语法里是赋值号且只能用于数据定义的场合。在程序运行中,不允许对CONST段中的对象进行修改。在CONST段中声明字
符串常量可以省略数据类型,但文本常量不能省略数据类型。虽然,声明符号常量时,设置了数据类型,这里数据类型仅仅是为给常量分配内存单元而设置的,在指令中,语法仍然认为符号常量的数据类型是不明确的2022/11/1293例9:带数据类型的常量声明。constpi:real32:=3.14
159;Max:uns32:=15;des:char:='/';Mask:byte:=$F0;Active:Boolean:=true;例10:不带数据类型的常量声明。constpi:=3.14159;//默认类型是real80Max:=15;
//默认类型是uns32Des:='/';//默认类型是charActive:=true;//默认类型是boolean2022/11/12943.2.4常量表达式常数表达式的形式与高级语言的相似,他们包括字符、数字常量和前面已声明过的符号常量,各种运算符。常量表达式在语法上仍是常量。2
022/11/1295-(一元运算符)对“-”后的表达式取负*乘法div整数除法整数除以整数,取商的整数mod求余整数除以整数,取余数/除法结果是浮点数+加法-减法2022/11/1296例13:对于常数表达式,汇编语言在编译程序时计算出值。co
nstx:=10;y:=6;Sum:=x+y;//常量表达式HLA在编译时直接翻译常量表达式的值。它不为计算常量表达式“x+y”编译出机器指令,它直接计算这两个常数值的和。从此,在本程序里汇编语言把16和常数Sum联系起来。2022/11/12973.3变量3.3.1有符号整型变量3.3.2无符号
整型变量3.3.3实型变量3.3.4布尔型变量3.3.5字符型变量2022/11/1298变量要先定义,后使用。程序员要按照变量的性质,在相应的段中定义变量。定义变量的语法:变量名:数据类型;变量名:数据类型:=常量表达
式;变量名按标识符取名。数据类型可以是HLA的任何一种数据类型。:=在语法里是赋值号且只能用于数据定义的场合。常量的类型必须与指定的数据类型一致。2022/11/12993.3.1有符号整型变量汇编语言常用的三种长度的有符号的十进制整数类型分别是:
int8,int16,int32。分别对应长度为一个字节、两个字节和四个字节的有符号整数。HLA按照数据类型为每个有符号整数变量分配内存空间。例15:i8:int8;i16:int16;i32:int32;
2022/11/12100在声明变量时,可以赋给变量一个初始值,在程序装入内存时由操作系统赋予变量。例16:i8:int8:=8;i16:int16:=1600;i32:int32:=-320000;在赋值号(“
:=”)后边必须是常数表达式。不能用另一个变量给变量赋值。2022/11/121013.3.2无符号整型变量HLA常用的三种长度的无符号十进制整数类型分别是:uns8,uns16,uns32。分别对应一个字节、两个
字节和四个字节的无符号整数。HLA按照数据类型为每个无符号整数变量分配内存空间。例17:声明无符号整数变量u8:uns8;u16:uns16;u32:uns32;2022/11/12102例18:声明变量时赋初值u8:uns8:=255;u16:uns16:=6500;u32:
uns32:=5900;2022/11/121033.3.3实型变量HLA只有三种长度的浮点类型:real32(4个字节)代表单精度浮点数,real64(8个字节)代表双精度浮点数,real80(10个字节)代表扩展精度浮
点数。汇编语言根据数据类型为每个浮点型变量分配内存空间。系统把浮点类型数值一律当做有符号的数值。2022/11/12104例19:定义浮点型变量f32a1:real32;f32a2:real32:=2.7;pi:real32:=3.14159;f64b
1:real64;f64b2:real64:=1.23456789e+10;f80c1:real80;f80c2:real80:=-1.0e-104;2022/11/121053.3.4布尔型变量HLA汇编语言程序中可以使用布尔型常量和变量,用布尔型变量
或常量组成布尔型表达式,一个布尔型变量是最简单的布尔型表达式。例20:a:boolean;b:boolean:=false;c:boolean:=true;每个布尔型变量占一个字节。你能声明未初始化的变量
也能声明初始化的变量。2022/11/121063.3.5字符型变量你能用char数据类型声明占一个字节的ASCII字符变量。例21:字符变量的定义c:char;d:char:='A';2022/11/121073.4程序分段3.4.1代码段3.4.2静态数据段3.4.
4堆栈段3.4.5自动变量段3.4.6类型段(略)3.4.7符号常量段(略)3.4.8在程序中声明段的组织形式(略)3.4.9数据在内存中存放格式2022/11/121083.4.1代码段汇编语言程序从begin开始到end之间的所有指令和数据组成代码段,代码段是一个程序的执行规则,它用指令描
述了程序的功能,在代码段里放的是指令及指令中携带的数据根据计算机工作原理,CPU从内存取来第一条指令,执行,然后取下一条指令,再执行直到执行完最后一条指令,程序结束。为了实现程序运行,CPU必须知道指令在哪里,为了帮助CPU找到指令,人们做出了这样设2022/11/12109
计,在CPU中设置一个指令计数器EIP,用它放下条指令的偏移地址,再用一个寄存器保存段起始地址的索引,这是硬件必须提供的支持,在80X86中代码段起始地址的索引由操作系统在装入程序时放入CS中。段寄存器CS中的索引值指向代码段描述符,代码段描述符中包括了
段的32位起始地址(段基址)、段的长度、段的属性等内容。2022/11/12110用指令计数器EIP存放下一条指令在代码段中的偏移地址,指令的内存地址(线性地址)是:线性地址=段基址(32位)+偏移地址(32位)在Windows控制下,线性地址通过页表换算出物理地址
。在32位汇编语言程序中很难看到段寄存器,因为它们被操作系统封装起来了。任何一个段的起始地址是该段的最小地址,所以偏移地址必须是正数。每取一条指令,EIP自动修改,指向下条指令的地址。2022/11/12111012315CS寄存器索引字0TIRPL2022/11/12112偏
移地址是为了方便计算机内存系统管理而引入的一个逻辑地址概念,因为程序员在编程时,无法知道将来程序运行时放在内存的哪个位置,所以让编译程序在编译源程序时,以字节为单位按顺序给每条指令、每个数据赋予一个逻辑地址,并且每个段都从零开始编址,
这个逻辑地址就称作偏移地址。在程序执行中,由内存管理机构将逻辑地址映射到真正的物理地址。指令B指令A代码段04EIPEIP2022/11/12113偏移地址内存代码段基址BEIPB+EIP低地址端高地址端2022/11/121143.4.2静态数据段在
静态(STATIC)内存段声明的变量称静态变量,如果没有初始化,系统默认用0作变量的初值。在装入程序时给静态数据段配内存,到程序结束时,和代码段同时撤内存。2022/11/12115汇编语言定义了取变量偏移地址的运算,语法是:&静态变量运算的结果是该静态变量的偏移地
址,它的长度是32位。2022/11/12116例23:计算静态变量的偏移地址mov(&b,eax);指令将变量b的偏移地址装入寄存器EAX2022/11/12117除了定义静态变量外,你还可使用伪操作码b
yte,word,dword,uns32等,把数据插入静态内存段。2022/11/12118例24:插入数据staticb:byte:=0;//为每个数据分配1个字节byte1,2,3;u:uns32:=1;//为每个数据分配4个字节uns325,2,
10;HLA在变量定义后,使用伪操作码将数据写入静态内存段。如字节值1、2、3嵌入变量b字节之后。这些值没有变量名,在学习寻址方式时,将讨论怎样访问没有变量名的值。插入的数据在内存怎样存放?2022/11/12119……0123&b+0&b
+1&b+2&b+3偏移地址内存&u+12偏移地址内存&u+8&u+0&u+410251………………段基址段基址2022/11/12120为变量分配地址但不分配空间STATIC段里变量的@NOSTORAGE属性表示在数据段给变量赋予当前的地址,但不给变量分配内存空
间,所以变量与段里的下一个对象共用同一个地址,语法表示如下:变量名:类型;@nostorage;2022/11/12121例25:staticx:dword;@nostorage;byte'a','b','c','d';假
定变量x的地址是1120,则数据‘a’的地址也是1120,数据‘b’的地址是1121,数据‘c’的地址是1122,数据‘d’的地址是1123。插入的数据是这样存放的:2022/11/121221123'a''b''c''d'内存变量x与后面的4个数据共享内存空间
。数据a的偏移地址=1120+0数据b的偏移地址=1120+1数据c的偏移地址=1120+2数据d的偏移地址=1120+3112211211120x2022/11/121233.4.4堆栈段堆栈段是用先进后出的动态数据结构维护的一个内存区。段寄存器SS中的索引
值指向堆栈段描述符,描述符保存了段的起始地址(32位段基址),而指针寄存器ESP值是当前栈顶的偏移地址。由CPU的内存管理组件把栈顶的逻辑地址映射为线性地址。系统默认为你的程序建立堆栈段,为它分配16MB空间。2022/11/12124偏移地
址内存堆栈段基址B栈底栈顶espB+esp低地址端高地址端B+16M初始栈顶栈顶是浮动的而栈底是固定的2022/11/121253.4.5自动变量段VAR段在堆栈段里分配内存空间。主程序里声明的var段与static段具有同样的生命周期。汇编语言不允许对VAR
段的变量初始化。系统也不会对VAR段的变量初始化。2022/11/12126例27:vara:int32;c:char;2022/11/121273.4.7符号常量段HLA的CONST段用来声明符号常量。
CONST段中声明的常量其值从编译到运行都不会改变;CONST段位于程序中的声明段位置。它以CONST保留字开始。例:constx:=10;y:=6;Sum:=x+y;//常量表达式2022/11/121303.4.9数据
在内存中存放格式80X86系列的内存每个字节有一个地址,由于指令和数据长度少则一个字节,多则多个字节。如果数据就占一个字节,这个字节的地址就是数据的地址;如果一个数据或一条指令占多个字节(操作系统会为它分配连续的
地址空间),操作系统就按低地址作为数据或指令的地址,并遵循低位数据(指令)装入低地址字节,高位数据(指令)装入高地址字节的规则将数据或指令装入内存。2022/11/12131如字节型数据值是’A’,字类型数据值是$125
6双字类型数据值是$106f3a6e。三个数据的地址分别是186、188、192。数据如图存放:低放低,高放高。2022/11/12132例:数据在寄存器中存放格式如有数据$1234abcd要放在寄存器EAX中,根据规则存放的结果是:1234abcdEAXALAH2022/11
/12133例:如有数据$1234abcd要放在地址是1200的内存单元中,根据规则存放的结果是:12341200abcd1201120212032022/11/12134第四章寻址方式汇编语言的指令按其指令格式包含的操作数的个数可以分为
:无操作数、单操作数、双操作数三种形式:指令助记符();指令助记符(源操作数/目的操作数);指令助记符(源操作数,目的操作数);2022/11/12135操作数在指令中最多要表达三种含义:1.表示操作数的值(数值或字符)2.表示操作数所在的位置(内存、指令、中央处理器)
3.表示操作数的数据类型至少要表达二种含义即1.和2.,其中2.是汇编语言独有的,它要明确地表示操作数在哪里,使CPU可以准确地取到操作数。2022/11/12136指令助记符表示指令能产生的操作,常用英文的
缩写表示。如“add”代表“addition”(加法)。“sub”代表“subtracting”(减法)。源操作数参加运算后其值不变,通常目的操作数参加运算后又用来存放运算结果。指令中的操作数必须用寻址方式表示。下面我们以操作数所处的
位置分类学习操作数的寻址方式。2022/11/121374.1寄存器寻址方式4.2立即寻址方式4.3保护模式的内存寻址方式4.4数据类型限制2022/11/121384.1寄存器寻址方式操作数格式:寄存器名寄存器名是任何一个通用寄存器的名字。寄存器的内容就是指令需要的操作数。2022/11/
12139例1:mov(EBX,EAX);//将源操作数ebx值复制给目的操作数eax2F672F67547A547AEBXEAX2022/11/12140inc(ecx);13AF26446679ABCD13AF2645执行前的ECX执行后
的ECX13AF2644+12022/11/121414.2立即寻址方式操作数格式:常数或常数表达式常数值就是指令的操作数的值,它包含在指令中,当从内存取来这样的指令,指令中就包括了操作数。在所有寻址方式里,这种方式的访问时间是最短的。
但它天生的缺陷是在程序运行中不能修改操作数。2022/11/12142例2:mov(-50,AX);/*源操作数是立即寻址方式,目的操作数是寄存器寻址方式*/mov(false,DH);2022/11/12
143例:mov(‘a’,DL);‘a’0110000111100000执行前的DL执行后的DL01100001机器数表示2022/11/121444.3保护模式的内存寻址方式4.3.1直接寻址方式4.3.2寄存器间接寻址4.3.3
变址寻址方式4.3.4比例变址寻址方式2022/11/121454.3.1直接寻址方式操作数格式:变量名变量名[偏移量]第一种形式:用变量的内存地址访问内存单元。第二种形式:用变量的偏移地址加偏移量作为操作数的偏移地址。偏移量是常数,可以是正数也可以是负数。偏移量是某个内存单元与某个偏移地址之间的
字节距离。2022/11/12146这种寻址方式是专门为变量设置的,用它可以方便地访问有名变量,但是这种方式天生的缺陷是在程序运行中无法修改操作数的偏移地址,因为偏移量必须是常量。2022/11/12147例3:mov(J,al);mov
(dl,K);2022/11/12148例5:MOV(i[3],AL);/*i[3]表示将i的偏移地址加3作为操作数偏移地址。*/2022/11/12149注意:第一,偏移量必须是常数。“变量[变量]”是非法表示。第二是在“变量[偏移量]”中偏移量是字节偏移值,表示操作数地址距变量偏移
地址的字节距离。HLA允许在使用常数的地方使用常数表达式。2022/11/121504.3.2寄存器间接寻址语法:[寄存器名]寄存器可以是32位通用寄存器中的任何一个。寄存器的内容是操作数的偏移地址。2022/11/12151例8:mov([eax],AX);12AB3356指令执行前的
EAX12AB33563A指令执行后的EAX783A内存低端地址高端地址786612ABAXAX12AB335712AB33552022/11/12152寄存器EAX中是一个32位的偏移地址,将这个地址所指的内存字节单元的
内容复制给寄存器AX。寄存器间接寻址方式可以理解为,寄存器中存放的是个指针,它所指的内存单元是一个无名变量。80X86指令系统中没有内存间接寻址方式,只有寄存器间接寻址方式,如果用指针访问内存,这个指针必须装入一个3
2位通用寄存器,然后再用寄存器间接寻址方式访问内存。2022/11/121534.3.3变址寻址方式操作数格式格式(1):变量[寄存器]变量代表程序中的变量。寄存器可以是任何一个32位寄存器。变量的偏移地址加上寄存器的内容作为操作数的偏移地
址。2022/11/12154例9:mov(VarName[ebx],AL)假设变量的地址是$1100,寄存器EBX的内容是8,源操作数的地址等于$1108,指令把这个单元的内容复制给寄存器AL。2022/11/12155操作数格式
格式(2):[寄存器+offset][寄存器-offset]寻址方式中的offset是常数,它的意义是偏移量或理解为字节距离。寄存器是任何一个32位通用寄存器。寄存器的内容加上(或减去)offset值,作为操作数的偏移地址
。2022/11/12156例:mov([ebx+$30],ax);345F73A2345F73A2+30345F78D2345F78D1345F78D3EBXAX6889AF68AF2022/11/12157操作数格式格式(3):变量[寄存器+off
set]变量[寄存器-offset]寄存器是任何一个32位通用寄存器。寄存器的内容加上(或减去)offset值,结果再与变量的偏移地址相加作为操作数的偏移地址。2022/11/12158例:mov(data[ebx+$30],AX)
;345F73A210000000+345F73A2+30445F78D2445F78D1445F78D3EBXAX6889AF68AF10000000Data的地址……2022/11/121594.3.4比例变址寻址方式语法格式:变量[变址寄存器*scale]变量[变址寄存器*scale
+offset]变量[变址寄存器*scale-offset][基址寄存器+变址寄存器*scale][基址寄存器+变址寄存器*scale+offset][基址寄存器+变址寄存器*scale-offset]变量[基址寄存器+变址寄存器*scale]
变量[基址寄存器+变址寄存器*scale+offset]变量[基址寄存器+变址寄存器*scale-offset]2022/11/12160基址寄存器是任何一个32位通用寄存器,变址寄存器代表除ESP外的任何一个32位寄存器,比例因子scale是个常数,其值是1,2,4或8中一
个。offset是一个常数。寻址方式中的变量用它的偏移地址参加计算。2022/11/12161寻址方式的含义:变量[基址寄存器+变址寄存器*scale-offset]操作数的偏移地址是变量的偏移地址加基址寄存器的内容加变址寄存器*scale的值,减常数offset的值。20
22/11/12162例10:mov(VarName[ebx+esi*4+4],al);假定EBX内容是$100,ESI内容是20,VarName的偏移地址是$2000。源操作数偏移地址为($2000+$100+$20*4+4)=
$2184,指令执行结果是将$2184所指的一个字节数据复制给寄存器AL。2022/11/121634.4数据类型限制汇编语言是一种强调数据类型的程序设计语言,它要求程序中使用的数据具有明确的数据类型,在应用中有时需要按照非原来的数据类型操作数据,有时需要把数据类型
不明确的数据明确为某个数据类型,这都需要类型限制。类型限制分内存对象类型限制和寄存器类型限制。2022/11/121644.4.1内存对象类型限制4.4.2寄存器类型限制2022/11/121654.4.1强制限制内存对象
类型强制类型限制是显式地说明按指定的类型来处理一个对象,而不论它原来是什么类型。对一个内存对象强制类型限制的语法是:(type新类型名内存寻址方式)新类型名用来指定新类型。内存寻址方式是任何一个合法的内存寻址方式。括号不能省略。2022/11/12166例12:staticv
a:byte;@nostorage;byte0,1;...mov(va,ax);程序员想用一条指令把0装入AL、把1装入AH,在汇编时汇编程序会报错。va01va+12022/11/12167因为va是字节类型,AX是字类型,两个操作数的类型不匹配。如果不用类
型限制则必须用两条指令:mov(va,AL);mov(va[1],AH);使用类型限制用一条指令能解决这个问题:va01va+12022/11/12168mov((typewordva),ax);这条指令告诉HLA以va的地址为地址,以word为操作数的类型,从内
存读一个字,写入寄存器AX。强制类型限制只在它所在的指令中生效,指令执行后,被强制类型限制的对象仍然是原来的数据类型。2022/11/12169在汇编语言源程序里,如果指令是双操作数,要求两个操作数的长度必须符合指令的要求。操作数的长度由操作数的数据类型决定。指令中所有操作数都是用寻址形式表示
的,也就要求程序员从寻址形式上能判断操作数的数据类型。可惜寻址方式中有的明确了操作数的数据类型,而有的没有明确操作数的数据类型。2022/11/12170数据类型明确的寻址形式有:寄存器//寄存器寻址变量//直接寻址变量[offset]变量[基址寄存器]//带变量变址寻址变量[基址寄
存器+offset]变量[变址寄存器*scale]//带变量比例变址寻址变量[变址寄存器*scale+offset]变量[基址寄存器+变址寄存器*scale]变量[基址寄存器+变址寄存器*scale+of
fset]2022/11/12171寄存器寻址方式的数据类型由寄存器的位数决定。其他的寻址形式的数据类型由变量的类型决定。简而言之,寻址方式中数据类型明确的有寄存器寻址方式,直接寻址方式,带变量的变址寻址方式和带变量的比例变址寻址方式。2022/1
1/12172数据类型不明确的寻址形式有:立即寻址寄存器间接寻址[基址寄存器+offset]//变址寻址[变址寄存器*scale]//比例变址寻址[变址寄存器*scale+offset][基址寄存器+变址寄存器*
scale][基址寄存器+变址寄存器*scale+offset]2022/11/12173简而言之,寻址方式中数据类型不明确的有立即寻址方式,寄存器间接寻址方式,不带变量的变址寻址方式和不带变量的比例变址寻址方式。2022/11/121
74汇编语言语法规定:在双操作数的指令中,如果两个操作数的类型都是明确的,它们长度必须相同;如果一个明确,另一个不明确,按明确的类型检查、运算;如果两个操作数的类型都不明确,必须明确一个。2022/11/1217
5在单操作数的指令中,操作数的类型必须明确,操作数的类型不明确时,除非指令自行明确数据类型,否则必须加类型强制限制。2022/11/12176例13:inc([ebx]);分析:这是个单操作数指令,采用寄存器间接寻址方
式,在指令里只知道操作数的地址,而不知道操作数的类型,因为这个数据可能是一个字节长、两个字节长、四个字节长、八个字节长、十个字节长等。所以指令必须明确操作数的数据类型。2022/11/12177inc([ebx]);457AF669EBX4
57AF6693A682189457AF66A457AF66B457AF66C8921683A+1683A+13A+18位的类型16位的类型32位的类型2022/11/12178可以用下面的某个强制说明。inc((typebyte[ebx]));inc
((typeword[ebx]));inc((typedword[ebx]));inc((typeqword[ebx]));2022/11/12179例14:pushd(30);pushd([ebx]);虽然操作数30、[ebx]都属于数据类型不明确之类,但是指令已自
行把它们明确为双字类型。所以不需要类型强制转换。2022/11/121804.4.2对寄存器强制类型限制虽然寄存器类型是明确的,但是有时也需要类型限制,而且寄存器类型限制必须与寄存器的长度相同。即新类型的长度必须与寄存器的长度一样。这是和变量类型限制的重要区别。寄存器类型限制的语法
格式为:(type数据类型寄存器名)2022/11/12183HLA标准库stdout.put函数只用十六进制数输出byte、word、dword类型和寄存器的数据值。所以,如果想用其他数制输出一个寄
存器值,必须用类型限制:stdout.put("AL=`",(typecharal),"`",nl);stdout.put("AL=`",(typebooleanal),"`",nl);stdout.put("AL=`",(typeint8al),"`",nl);s
tdout.put("AL=`",(typeuns8al),"`",nl);2022/11/12184第五章输入输出函数5.1标准输出函数stdout.put()5.2stdio模块中预定义的常数5.3字符输入函数stdin.getc()5.4
浮点输入函数stdin.getf()5.5通用输入函数stdin.get()5.7清除缓冲区函数2022/11/121855.1通用输出函数stdout.put()语法格式:stdout.put(实参表);stdout.put函数的实参表可包含若干实
参,实参之间用逗号分开,实参可以是常数(常数表达式)、变量、寄存器、其他内存寻址方式(参考第四章)。表中数值实参可以用以下三种格式中任何一种:实参实参:总宽度实参:总宽度:小数位数2022/11/12186总宽度指定输出的最小宽度。总宽度包括符号位、小数点位、数
字位。小数位数是指定小数点后的位数。stdout.put函数默认显示规则:byte,word和dword及寄存器实参用十六进制数显示整数类型实参用十进制数显示二进制和十六进制常数用十六进制整数显示不指定小数位数的浮点类型变量用科学计数法显示指定小数位数的浮点类型变
量用小数形式显示浮点型常量必须指定总长度和小数位数字符和字符串类型实参用字符和字符串显示2022/11/12187例22:在程序中可以用HLA标准函数输出十进制数值常数stdout.put(123,nl);结果输出:123实参表中的实参nl是HLA的
常数,代表光标换行。请参考2.4.2节2022/11/121885.2stdio模块中预定义的常数HLA标准库中有stdio模块,其中定义了可以用于输出函数实参的常数。stdio.bellASCII鸣笛nl光标换行(它是字符串常数)st
dio.bsASCII退格。stdio.tabASCIItab符stdio.eoln换行符stdio.lfASCII换行stdio.crASCII回车2022/11/12189例34:stdout.put(stdio.bell);让喇叭发出一声鸣笛例35:stdout.put("
西风残照,汉家陵阙。",stdio.lf);输出:西风残照,汉家陵阙。然后光标换行。例36:stdout.put("云破月来花弄影。",stdio.eoln);输出:云破月来花弄影。然后光标换行。例37:stdout.put(
"无数杨花过无影。",nl);输出:无数杨花过无影。然后光标换行。2022/11/121905.3字符输入函数stdin.getc()语法格式:stdin.getc();stdin.getc()函数从标准输入设备的缓冲区读一个字
符,并将字符的ASCII码写入CPU的寄存器AL中。例39:输入一个字符然后输出这个字符。stdin.getc();mov(AL,cc);//cc是字符型变量stdout.put(cc,nl);2022/11/121915.4浮点输入函
数stdin.getf()语法格式:stdin.getf();stdin.getf()函数从标准输入设备缓冲区读一个浮点值。并将它压入浮点寄存器ST0。例40:stdin.getf();执行函数后,输入的值被压入浮点栈
的栈顶寄存器st0。关于浮点栈在第七章的浮点运算部分详细介绍2022/11/121925.5通用输入函数stdin.get()语法格式:stdin.get(变量表或寄存器表);变量表中各项是同种变量或寄存器,两项之间用逗号分隔。数值变量默认输入的数据是十进制数;
寄存器默认输入的数据是十六进制数;字符变量要求输入的数据是字符。输入时,两个数值之间用空格分隔;两个字符之间不用分隔。2022/11/12193例41:输入十进制整数stdout.put("输入两个十进制整数值:");stdin.get(a,b);//a,b是整型变量2022/11/121
94例42:输入十六进制整数stdout.put("输入两个十六进制整数值:");stdin.get(EAX,EBX);//把两个十六进制数装入两个寄存器例43:输入整数和浮点数混合stdout.put("输入一个整数和一个浮点数",nl);stdi
n.get(c,f3);//c,f3分别是整型和浮点型变量2022/11/12195例44:字符变量的定义、输入、输出c:char;d:char:='A';...stdin.get(c);stdout.put(c,nl,d,nl);注意:数值和字符不能直接混合输入。数值和字符混合输入的
方法看下一小节。2022/11/12196例45:数值和字符直接混合输入的错误例子stdout.put("输入两个十六进制整数值",nl);stdin.get(eax,ebx);stdout.put("输入两个字符",nl,nl);stdin.get(c1,c2);//c1
,c2是字符变量,//程序不能正常工作2022/11/12197例46:有符号整数的输入和输出statica:int32:=5;b:int32;…stdout.put("输入一个整数值:");stdin.get(b);//从键盘接收
一个十进制的值stdout.put("你输入的是:",b,nl);stdout.put("a的值是",a,nl);…2022/11/121985.7stdin.readLn()和stdin.flushInput()函数HLA标准库缓冲区接收用户输入的一行数据。调用输入函数
从这个输入缓冲区读入数据直到数据读完。stdout.put(“读一个8位的有符号整数:");stdin.get(i8);stdout.put(“读一个16位的有符号整数:");stdin.get(i16);2022/11/12199如执行这段代码时,输入"1
23456"后,程序虽然显示第二个提示,但并不等待输入,而是直接执行stdin.get(i16)后的指令。其实在执行stdin.get(i8)调用时已经将456作为第二个整数从输入缓冲区读入,通常stdin函数只在输入缓冲区空时,才从用户那里读数据。只要输入缓冲
区中还剩有数据,输入函数就会继续从缓冲区读。2022/11/12200HLA标准库提供两个函数其语法格式:stdin.readLn();stdin.flushInput();stdin.readLn函数放弃输入缓冲区中的所有数据,等待输入回
车。stdin.flushInput函数只是简单地放弃输入缓冲区中的所有数据。2022/11/12201在输入函数之前调用stdin.readLn函数在输入函数之后立即调用stdin.flushInput函数。2022/11/12202例48:stdout.put
("inputa"nl);stdin.get(a);stdin.flushInput();stdout.put("inputb"nl);stdin.get(b);用了stdin.flushInput()函数,这次输入每次只接收一个值。要求分两次输入。2022/11/12203例49:
数值和字符混合输入stdout.put("输入两个十六进制整数值",nl);stdin.get(eax,ebx);stdout.put("输入两个字符",nl,nl);stdin.flushInput();st
din.get(c1,c2);//c1,c2是字符变量2022/11/12204第六章整型运算6.1基本机器指令6.2整型算术表达式2022/11/122056.1部分基本机器指令6.1.1复制数据指令6.1.2整数加法指令6.1.3整数减法指令6.1.4整数符号扩展、零扩展指令6
.1.5无符号整数乘法指令6.1.6有符号整数乘法指令6.1.7无符号整数除法指令6.1.8有符号整数除法指令2022/11/122066.1.9整数位逻辑运算指令6.1.10整数移位指令6.1.11整数交换指令6.1.12访问堆栈
指令6.1.13传送地址指令6.1.14输入输出指令6.1.16溢出中断指令2022/11/122076.1.1复制数据指令语法:mov(源操作数,目的操作数);源操作数:可以是常数、变量、寄存器。目的操作数:可以是变量
、寄存器。功能:将源操作数复制到目的操作数。指令操作不影响条件标志。80x86指令集不允许双操作数指令中两个操作数都是内存操作数。并要求两个操作数的长度相同。2022/11/12208汇编语言指令操作数搭配源操作数目
的操作数通用寄存器通用寄存器内存单元常数通用寄存器内存单元内存单元通用寄存器2022/11/12209大多数的80x86通用指令都使用以上的语法。HLAMOV指令允许同时用两个16位或32位的内存变量作为源操作数和目的操作
数。而且仅仅在MOV指令中允许这样。由于CPU内通用寄存器是32位,所以汇编语言非浮点运算指令中操作数的位数最长是32位。2022/11/12210例1:mov(35,ax);mov(’3’,DL);m
ov(true,DH);2022/11/122116.1.2整数加法指令(1)双操作数加法指令语法:add(源操作数,目的操作数);加法指令功能用高级语言的赋值语句表示如下:目的操作数=目的操作数+源操作数;2022/11/12212加法指令按运算结果设置条件标志。前
面讲到加法运算中符号值与数值一样参加运算,因为机器字长的限制,运算过程中最高位产生的进位就"丢失"了。实际上运算过程中最高位产生的进位被CF标志记录。2022/11/12213由于CPU结构的限制,算术运算指令中包含两个操作数,这两个操作数参与运算后,其
中一个还要保存运算的结果,在语法上把保存结果的操作数称为目的操作数,另一个操作数称为源操作数,目的操作数在算术运算后内容变成运算的结果,只有源操作数未被修改,在写程序时要注意这个问题。2022/11/12214例2:add(eax,ebx);指令把EAX和EBX的内容相加,结果
装入EBX,按运算结果设置条件标志。2022/11/12215(2)加1指令语法:inc(目的操作数);指令的功能用高级语言的赋值语句可描述为:目的操作数=目的操作数+1inc比add指令要短。运算结果不影响CF标志,按结果设
置其他条件标志。这条指令在程序设计中常用来修改循环变量、内存地址。2022/11/12216例3:inc(ax);指令对寄存器AX内容加1。2022/11/122176.1.3整数减法指令(1)双操作数减法指令语法:sub(源操作数,目的操作数);减法指令功能用高级语言的赋
值语句表示如下:目的操作数=目的操作数-源操作数;减法指令按运算结果设置条件标志。前面讲到减法运算中符号值与数值一样参加运算,因为机器字长的限制,运算过程中最高位产生的借位就"丢失"了。实际运算过程中最高位产生的借位被CF标志记录。2022/11/12
218(2)减1指令语法:dec(目的操作数);目的操作数只能是8位、16位、32位寄存器或内存单元,数据类型必须明确。指令的功能用高级语言的赋值语句可描述为:目的操作数=目的操作数-1dec比sub指令要短。运算结果不影响C
F标志,按结果设置其他条件标志。这条指令在程序设计中常用来修改循环变量、内存地址。2022/11/12219例6:用MOV和SUB指令求一个整数补码的相反数programex;#include("stdlib.hhf");statici8:int8:=-8;i16:
int16:=-16;i32:int32:=-32;beginex;stdout.put("i8=",i8,",i16=",i16,",i32=",i32,nl);mov(0,al);//计算i8=-i8;用0减这个数
。2022/11/12220sub(i8,al);mov(al,i8);mov(0,ax);//计算i16=-i16;sub(i16,ax);mov(ax,i16);mov(0,eax);//计算i32=-i32;sub(i32,eax);mov(e
ax,i32);stdout.put("i8=",i8,",i16=",i16,",i32=",i32,nl);endex;2022/11/12221(3)求补指令语法:neg(目的操作数);目的操作数只能是8位、16位、32位寄存器或内存单元,数据类
型必须明确。指令功能:用零减目的操作数,结果装入目的操作数。运算的意义是在补码域求目的操作数的相反数。在程序设计中,常用它对一个负数补码求其绝对值。由于补码的非对称性,在对负数求绝对值时,可能会发生溢出。2022/11/12222实际
机器执行求补指令时,是用0减去目的操作数,再将结果装入目的操作数。如果操作数值是0,则符号不会改变,但清除CF标志位。对其他数求补,都会设置CF标志位。如对-128(8位)、-32768(16位)、-2147483648(32位)值求补时,操作数不会变,但要设置溢出标志OF
。指令运算结果还影响AF、SF、PF、ZF标志。2022/11/12223例7:求A、B的相反数programtwosComplement;#include("stdlib.hhf");staticA:int8:=-45;B:int8:=-90;C:int8;begintwosCompl
ement;2022/11/12224mov(A,al);stdout.put(nl,"A值为:",A,nl);neg(al);mov(al,C);stdout.put("A的绝对值为:",C,nl);mov(B,al);stdout.put("B的值为:",B,nl);n
eg(al);mov(al,C);stdout.put("B的绝对值为:",C,nl);endtwosComplement;2022/11/122256.1.4整数符号扩展、零扩展指令我们知道对于整数的加法、减
法运算,指令要求两个操作数的长度必须一致,如长度不一致时,要把短操作数扩展为长的操作数;在做除法时,除法指令要求被除数的长度是除数长度的2倍,这也需要将被除数的长度扩展为除数长度的2倍。对有符号整数进行符号扩展,对无符号整数进行零扩展。2022/11/12226(1)8位
到16位符号扩展指令cbw();把AL中有符号数扩展成16位放在寄存器AX。2022/11/12227(2)16位到32位符号扩展指令cwd();把AX中有符号数扩展成32位放在寄存器DX:AX。20
22/11/12228(3)32位到64位符号扩展指令cdq();把EAX中有符号数扩展成64位放在寄存器EDX:EAX。2022/11/12229(5)复制实现的符号扩展指令指令movsx(源操作数,目的操作数);目的操作数长度必须大于源操
作数长度,如源操作数是8位,目的操作数必须是16或32位,并且目的操作数只能是寄存器。指令将源操作数复制到目的操作数,同时进行符号扩展。2022/11/12230(6)复制实现的零扩展指令movzx(源操作数,目的操作数);指令的限制与movsx相同,指令将
源操作数复制到目的操作数,同时进行零扩展。符号扩展、零扩展指令的操作结果不影响条件标志。2022/11/12231例10:对负数进行符号扩展。stdout.put("输入一个不小于-128的负数:");stdin.get(
i8);mov(i8,al);stdout.put("输入的是",i8,"($",al,")",nl);cbw();mov(ax,i16);stdout.put("16位符号扩展:",i16,nl);2022/11/12232cwde();mov(eax,i32);stdo
ut.put("32位符号扩展:",i32,nl);movsx(i8,ax);mov(ax,i16);stdout.put("16位符号扩展:",i16,nl);movsx(i8,eax);mov(eax,i32);stdout.put("32位符号扩展:",i32,nl);2022/11/1
22336.1.5无符号整数乘法指令由于在机器内不能确定一个整数是有符号的还是无符号的,所以一个机器数可能代表有符号整数也可能代表无符号整数。假定n=8位,机器内有个整数%10001010,如认为它是无符号数,这个值是138;如认为它是有符号数,这个
值是-118。对于补码,在做加减法时不区分有符号整数和无符号整数,对于无符号整数判断CF标志,对于有符号整数判断OF标志。2022/11/12234但在做乘除法时就必须区分有符号整数和无符号整数。假定
n=8位,机器数%11111111如果是有符号整数,它的值是-1;如果是无符号整数它的值是255,做乘法%00000011*%11111111时怎么让机器知道%11111111代表-1或255?为此汇编语言在乘除法指令上区分有符号整数和无符号整数。2022/11/12235(1)无符号单操作
数乘法指令指令格式:mul(源操作数);源操作数可以是8,16,32位内存或寄存器操作数。源操作数是8位,执行AX=AL*源操作数源操作数是16位,执行DX:AX=AX*源操作数源操作数是32位,执行EDX:EAX=EAX*源操作数20
22/11/12236两个n位操作数相乘,结果需要2*n位的空间。如8x8,16x16,或32x32的结果超过8位、16位或32位时,MUL指令将进位和溢出标志置1。注意其实没有溢出。MUL指令对负数标志和零标志无定义。202
2/11/12237例11:计算3*18将一个常数装入寄存器AL;3AL将另一个常数装入寄存器AH;18AH做乘法;AH*ALAX2022/11/12238程序为:MOV(3,AL);MOV(18,AH);MUL(AH
);结果值是54,放在寄存器AX中。2022/11/12239例12:输入两个8位无符号的整数,计算它们的乘积。输入两个无符号整数ua,ub将一个乘数装入ALuaAL做乘法AL*ubAX准备输出AXuc输出
uc2022/11/12240staticua:uns8;ub:uns8;uc:uns16;…stdout.put("输入两个无符号整数:",nl,nl);stdin.get(ua,ub);mov(ua,al);mul(ub);mov(ax,uc);stdout.put(ua,"*",ub,"="
,uc:5,nl);…2022/11/122416.1.6有符号整数乘法指令(1)有符号单操作数乘法指令指令格式:imul(源操作数);源操作数可以是8位、16位、32位内存或寄存器操作数。源操作数是8位,执行AX=AL*源操作数;源操作数是16位,执行DX:AX=AX*源操作
数;源操作数是32位,执行EDX:EAX=EAX*源操作数。影响条件标志与mul指令相同。2022/11/12242例14:计算-3*6Mov(-3,al);Mov(6,ah);imul(ah);计算结果-18放在寄存器AX中。将
一个乘数-3装入AL将另一个乘数6装入AH做有符号整数乘法AL*AHAX2022/11/12243例15:输入两个8位有符号的整数,计算它们的乘积。输入两个有符号整数a,b将一个乘数装入ALaAL做乘法AL*bAX准备
输出AXc输出c2022/11/12244a:int8;b:int8;c:int16;…stdout.put(“输入两个-128到127之间的整数:",nl);stdin.get(a,b);mov(a,al)
;imul(b);mov(ax,c);stdout.put(a,"*",b,"=",c:5,nl);…2022/11/12245(2)用常数的有符号的整数乘指令格式1intmul(源操作数,目的操作数);目的操作数必须是寄存器寻址方式。源操作数可以是立即寻址方式、寄存器寻
址方式、内存操作数寻址方式。长度可以是16位,32位。源操作数和目的操作数的长度必须相同,不能用8位的操作数。指令功能:目的操作数=源操作数*目的操作数,乘积的长度和乘数的长度相同。指令要影响所有条件标志。2022/11/12246例1
6:输入两个16位有符号的整数,计算它们的乘积。输入两个有符号整数a,b将一个乘数装入AXaAX做乘法AX*bAX准备输出AXc输出c2022/11/12247a:int16;b:int16;c:int16;…stdout.put("输入两个整数:",nl);
stdin.get(a,b);mov(a,ax);intmul(b,ax);mov(ax,c);stdout.put(a,"*",b,"=",c:5,nl);2022/11/12248计算:假定变量
长度是32位,结果不超过32位将结果放在寄存器EAX中。1)A+B–(B+C)2)X-Y+(Y-S+W)2022/11/122491)A+B–(B+C)Mov(A,eax);Add(B,eax);
Mov(B,ebx);Add(C,ebx);Sub(ebx,eax);2022/11/122502)X-Y+(Y-S+W)Mov(X,eax);Sub(Y,eax);Mov(Y,ebx);Sub
(S,ebx);Add(W,ebx);Add(ebx,eax);2022/11/12251例:从键盘输入一个矩形边长,计算矩形的面积,假定边长都是整数。分析:矩形的边长应大于0,为了避免输入错误的数据,可以在数据类型方面做限定,设置边长的数据类型为无符号
整数,这样当输入负数时,程序会自动保护。设三个变量都是无符号整数类型,变量名按照标识符取名规则取名,假定变量名分别是A、B、C,长度用16位。2022/11/12252无符号整数乘法指令会将两个十六位乘数相乘变成32位,分别放在两个
十六位的寄存器里,给输出带来不便,现在假定输入的整数相乘后不超过十六位,输入的整数小于等于255。2022/11/12253输入两个整数,由A、B接收将A装入寄存器AX做乘法B*AX给DX:AX把AX复制给变量C输出矩形面积C2022/11/12254staticA:uns16;
B:uns16;C:uns16;……stdin.get(A,B);mov(A,ax);mul(B);mov(ax,C);stdout.put("矩形的面积是",A,"*",B,"=",C,nl);2022/11/12255例:计算X=532–78*3+893–25*9分
析:根据四则运算法则,必须先做乘法,再做加、减法。先分别计算78*3和25*9,结果分别装入两个寄存器,再参加加、减法运算。乘法可用无符号整数乘法指令也可以用有符号整数乘法指令。从表达式中看,每个数和每个乘积都不超过16位。最后的结果也小于32767,可以确
定X用16位整数类型。2022/11/12256将78装入寄存器AL将3装入寄存器AH做乘法AL*AH给AX把AX复制给变量A(78*3)把532复制给BX做减法BX-A结果给BX将25装入寄存器AL将9装
入寄存器AH做乘法AL*AH给AX把AX复制给变量B(25*9)2022/11/12257把893复制给寄存器AX做加法AX+BX结果装入BX做减法BX-B结果装入BX把BX复制给变量X输出XstaticA:int16;B:int16;X:int16;……2022/11/12258mov(7
8,ah);mov(3,al);imul(ah);mov(ax,A);//A保存78*3mov(25,al);mov(9,ah);imul(ah);mov(ax,B);//B保存25*9mov(532,Bx);sub(A,BX);//BX保存532-78*3mov(893,A
X);add(AX,BX);//BX保存532-78*3+893sub(B,BX);//BX保存532-78*3+893-25*9mov(BX,X);stdout.put("532–78*3+893–25*9=",X,nl);202
2/11/122596.1.7无符号整数除法指令(1)无符号单操作数除法指令div(源操作数);源操作数代表除数,在汇编语言中可以是寄存器寻址方式,内存操作数寻址方式。长度可以是8位、16位、32位。2022/11/12260指令功能源操作数是8位,被除数必须在寄存器AX中,指令执行后AL放整数
商,AH放余数。源操作数是16位,被除数必须在寄存器DX:AX中,指令执行后AX放整数商,DX放余数。源操作数是32位,被除数必须在寄存器EDX:EAX中,指令执行后EAX放整数商,EDX放余数。2022/11/12261在80X86中如除数是8位
值,被除数必须是16位值。如用无符号的8位值做被除数,必须先把它用零扩展成16位,可以将8位被除数放在寄存器AL中,然后将寄存器AH清零,再做除法;如要用两个16位无符号值做除法运算,可以将16位被除数放在寄存器AX中,然后将寄存器DX清零,再做除法;如要
用两个32位无符号值做除法运算,可以将32位被除数放在寄存器EAX中,然后将寄存器EDX清零,再做除法。2022/11/12262例16:无符号整数除法计算128/42staticx:uns16:=128;y:uns16:=42;a:uns16;b:uns16;…mov(x,
AX);mov(0,DX);//被除数0扩展div(y);mov(AX,a);//商放入变量amov(DX,b);//余数放入变量b2022/11/122636.1.8有符号整数除法指令(1)有符号单操作数除法指令idiv(源操作数);除了操作数是有符号数外,其他与相应的
无符号数除法指令相同。2022/11/12264当处理有符号整数时,需要在执行IDIV之前,对被除数进行符号扩展,而且只能在累加寄存器(AL/AX/EAX)中进行符号扩展。除法指令对进位、溢出、符号和零标志没有定义。2022/11/122
65例18:有符号整数除法计算-128/42staticx:int16:=-128;y:int16:=42;a:int16;b:int16;…mov(x,AX);cwd();//被除数符号扩展idiv
(y);mov(AX,a);//商放入变量amov(DX,b);//余数放入变量b2022/11/12266使用除法指令时,可能会发生错误。首先可能会用零做除数。第二,商可能会溢出。例如,用16位/8位除法指令计算"$8000/2"得到的商是$4000,AL是放不下的。当发
生这些错误时,会产生ex.DivisionError异常或ex.IntoInstr整数溢出错误。这通常会显示相应的错误提示对话框,然后程序中止。所以做除法时,要谨慎选择被除数和除数的值。2022/11/12267例:一个年级有120人,现在将他们平均分成三个班,计算每个班级
的人数。分析:这是一个除法问题,用汇编语言做除法时,要考虑被除数和除数各用多少位;再就要考虑用哪种除法指令,必要时再增加零扩展或符号扩展。2022/11/12268把120复制给寄存器AX把3复制给寄存器BL做除法AX/BL结果装
入AL把AL复制给变量A输出AstaticA:int8;……2022/11/12269mov(120,ax);mov(3,bl);idiv(bl);mov(al,A);stdout.put("120人平
均分成三个班,每个班有",A,"人。",nl);2022/11/12270例:输入一个0到20之间的整数,输出它的平方值。本题有多种解法,可以直接用乘法计算;也可以用查表的方法,这里学习查表法求平方值。在内存
建立一个平方表,因为20的平方值是400,超过了8位,所以每个平方值用16位存储(2个字节)。用变量tablex表示平方表的起始地址。2022/11/12271Tablex+0Tablex+2Tablex+4Tablex+6Tablex+8Tablex+40.........
014916400从键盘输入0到20中某个值xXX2所在地址0Tablex+01Tablex+22Tablex+43Tablex+64Tablex+8......20Tablex+40内存中的平方表2022/11/12272从列表分析
得出:只要输入值X在允许范围内,它的平方值所在的偏移地址是tablex+2*X,关键问题是用什么寻址方式来表示这个偏移地址?从理论上讲,可以采用寄存器间接寻址、变址寻址、比例变址寻址方式。为什么不能采用直接寻址方式?请大家想一想。
实际中选择原则是方便简单为好。2022/11/12273(1)寄存器间接寻址方式:Xeaxeax左移1位(实现X*2)Tablex偏移地址ebxeax+ebxebx[ebx](用来访问内存取X的平方值)2022/11/12274(2)变址寻址方式:Xeaxeax左移一位(实现X*2)
tablex[eax](用来访问内存取X的平方值)2022/11/12275(3)比例变址寻址方式:XEAXtablex[EAX*2](用来取X的平方值)从以上三种寻址方式中看出比例变址寻址方式在书写上是最简单的,它只要
一条指令就可以完成寻址准备,而寄存器间接寻址和变址寻址方式分别要4条和2条指令完成寻址准备。2022/11/12276取X的平方值tablex[eax*2]复制给BX开始结束输入x把X值复制给寄存器EAXBX
的值复制给变量y输出X的平方值y2022/11/12277statictablex:uns16;@nostorage;uns160,1,4,9,16,25,36,49,64,81,100,121,144,169,196,225,256,289,324,361,400;X:int32
;y:int16;...2022/11/12278stdout.put("输入一个0..20的整数:",nl);stdin.get(X);mov(X,eax);mov(tablex[eax*2],bx);mov(bx,y);stdout.put(X,"的平方值=",y,nl)
;2022/11/122796.1.9整数位逻辑运算指令双操作数位逻辑运算指令and(源操作数,目的操作数);or(源操作数,目的操作数);xor(源操作数,目的操作数);目的操作数可以是各种内存寻址方式、寄存器寻址方式。源操作数可以是立即寻址方式
、内存寻址方式、寄存器寻址方式。操作数长度可以是8位,16位或32位。对CF、OF标志清零,不影响AF标志,按运算结果设置其他标志。2022/11/12280逻辑乘运算and(源操作数,目的操作数);功能:对源操作数和目的操作数逐位实施逻辑乘运算,运算的结果保存到目的操作数。ABAandB00
00101001112022/11/12281逻辑乘运算从真值表中看到,只有当参加运算的两个二进制位的值都是1时,运算结果才是1;只要参加运算的两个二进制位中有一个是0,运算结果就是0。利用逻辑乘的特性,可以实现对一个二进制数的任何位清
零。2022/11/12282逻辑乘运算例:把01110001中两个红色的1变成0。01110001and1100111101000001分析:根据逻辑乘运算的真值表,如要把某位变成零只需把该位和零相乘,不变的位要
乘以一。2022/11/12283逻辑加运算or(源操作数,目的操作数);功能:对源操作数和目的操作数逐位实施逻辑加运算,运算的结果保存到目的操作数。ABAorB0000111011112022/11/12284逻辑加运
算从真值表中看到,只有当参加运算的两个二进制位的值都是0时,运算结果才是0;只要参加运算的两个二进制位中有一个是1,运算结果就是1。利用逻辑加的特性,可以实现对一个二进制数的任何位置1。2022/11/12285逻辑加运算例:把01100001中三个红色的零变成一。01100001or
0000111001101111分析:根据逻辑加运算的真值表,如要把某位变成一只需把该位和一相加,不变的位要加零。2022/11/12286逻辑异或运算xor(源操作数,目的操作数);功能:对源操作数和目的操作数逐位实施逻辑异或运算,运算的结果保存到目的操作数。ABAxorB0000
111011102022/11/12287逻辑异或运算从真值表中看到,当参加运算的两个二进制位的值都相同时,运算结果才是0;当参加运算的两个二进制位的值不同时,运算结果就是1。利用逻辑异或的特性,可以实现对一
个二进制数的任何位取反。从真值表看到异或运算中不管某位原来的值是什么,只要给该位异或1,结果这位的值被取反;如果给该位异或0,结果这位的值不变。2022/11/12288逻辑异或运算例:把11110111中三个红色的位取反。11110111xor1000101001111101分析:根据逻辑异
或运算的真值表,如要把某位取反需把该位和一异或,不取反的位要异或零。2022/11/12289逻辑取反运算汇编语言有一条单操作数逻辑运算指令,它就是逻辑取反运算指令。注意,对于单操作数指令要求操作数的类型必须明确。202
2/11/12290逻辑取反运算not(目的操作数);功能:对目的操作数逐位实施取反运算,运算的结果保存到目的操作数。目的操作数只能是寄存器寻址或内存寻址方式,指令不影响条件标志。AnotA01102022/11/122
91逻辑取反运算例:对一个二进制整数实施逻辑取反运算1101011100101000not()00000001not()1111111000000000not()111111112022/11/12292注意,位逻辑运算与逻辑运算有
区别,不要把它们等同起来。例如,逻辑值真是1,假是0,在机器中一个逻辑值用一个字节表示,根据逻辑取反运算定义,对真取反结果是假,对假取反结果是真。但是用位逻辑运算是得不到这个结果的,我们从前面的例子里已经得到了这个信息。
2022/11/12294逻辑运算指令小结如果要对某位清零,只须对该位逻辑乘0;如果要对某位置位,只须对该位逻辑加1;如果要对某位取反,只须对该位异或1。在此要强调的是,不要将汇编语言中整数的位逻辑运算与高级语言中的逻辑运算等同起来,它们是两个完全不同的概念。汇编语言
只提供整数的位逻辑运算指令,程序员可以将整数的位逻辑运算指令组合起来实现逻辑运算功能。2022/11/122956.1.10整数移位指令(1)SHL逻辑左移、SAL算术左移shl(count,目的操作数);sal(count,目的操作数);co
unt可以是寄存器CL或是常数,取值范围0..n-1,n代表目的操作数的位数,目的操作数可以是内存单元或寄存器。2022/11/12296功能:将目的操作数左移count位,右边空出的位补0。指令逐位将目的操作数的当前最高位移到CF中。所以CF的值
是最后移进去的目的操作数的最高位。指令影响CF、ZF、PF、SF、OF。对AF无定义。1CF1001011002022/11/12297例19:SHL(1,AL);移位次数是1,左移一位,最高位移入进位标志,其他位依次向左移一位,最低位补0。001001101CF02022/11/12298%0
0000100左移三位后等于%00100000,相当于原来的值%00000100乘以%1000,而%1000就是23。如果左移n位没有溢出的话,对一个值左移n位和用2n乘它是一样的。2022/11/12299(2)逻辑右移指令格式:SHR(源操作数,目的操作数);目的操作
数右移由源操作数指定的位数。左边空出位补0。指令影响CF、ZF、PF、SF、OF。对AF无定义。101110101CF02022/11/12300例20:SHR(1,AL)移位次数是1,右移一位,最低位进入进位标志,其他位依次向右移一位,最高位补0。001111110CF0AL63312022/
11/12301例21:输入一个32位有符号整数,判断其奇偶性。分析:1)在机器内,有符号整数用二进制补码表示。2022/11/12302表6.2部分偶数补码(n=8位)正数负数2%00000010-2%111111104%00000
100-4%111111006%00000110-6%111110108%00001000-8%1111100010%00001010-10%1111011012%00001100-12%1111010014%00001110-14%1111001016%0001
0000-16%11110000…………2022/11/123032)因为偶数的最低位值是0,所以偶数除以2时,余数才是0。3)判断一个整数是不是偶数变成判断一个整数的最低位是不是0。2022/11/12304判断一个整数最低位的值有多种方法,其中一个
方法就是用逻辑右移指令。假定数据的拷贝在寄存器EAX。SHR(1,EAX);如果EAX中是偶数,执行指令后,CF=0;否则CF=1。2022/11/12306(3)算术右移指令格式:SAR(源操作数,目的操作数);目的操作数右移由源操作数指定的位数。左边空出位补原来的符号位。
指令影响CF、ZF、PF、SF。对AF、OF无定义。2022/11/12307算术右移指令SAR执行演示:目的操作数右移由源操作数指定的位数。左边空出位补原最高位的值。101110101CF12022
/11/12308算术右移指令在没有溢出的条件下,实质是做有符号整数的除法,商取整。右移n位,等于除以2n,取整数商。例22:ebx中是32位有符号整数(绝对值>16),求它除以16后的整数商。SAR(4,EB
X);指令执行后在EBX中就是所要的整数商。如果此例中没有绝对值>16的条件,结果会怎样?2022/11/12309(4)循环左移和右移指令格式:rol(源操作数,目的操作数);ror(源操作数,目的操作数);想像将目的操作数首尾相连,形成环,操作数值按指
令要求循环移位。每移一位,最高位回到最低位(左移)或最低位回到最高位(右移),同时将它复制到CF标志。指令影响CF、ZF、PF、SF、OF。对AF无定义。2022/11/12310循环右移指令执行演示:101110101CF02022/11
/12311循环左移指令执行演示:101110101CF2022/11/12312(5)带进位循环移位指令指令格式:RCL(源操作数,目的操作数);RCR(源操作数,目的操作数);目的操作数首尾之间连CF标志,形成环。目的操作数按指令
要求循环移动源操作数指定的位数。指令影响CF、ZF、PF、SF、OF。对AF无定义。2022/11/12313带进位循环右移指令执行演示:101110101CF12022/11/12314带进位循环左移指令执行演示:101110101CF12022/11/123
156.1.11整数交换指令指令格式:xchg(源操作数,目的操作数);指令功能:源操作数内容和目的操作数的内容交换。指令对标志寄存器没有影响。例23:mov(35,AX);mov(-50,BX);xchg(AX,BX);2022/11/12316AXBX35-5035
2022/11/123176.1.12的压栈指令push(16位通用寄存器);push(32位通用寄存器);push(16位内存单元);push(32位内存单元);pushw(常数/类型不明确的内存寻址方式);
pushd(常数/类型不明确的内存寻址方式);2022/11/12318PUSH指令是单操作数,所以操作数的类型必须明确,指令功能用高级语言的赋值语句表示:ESP=ESP–操作数的长度(2或4)[ESP]=操作数值指令不影响条件标志2022
/11/12319ESP低地址1992执行push(eax);eax中是326,首先ESP的值减4,变成1988,然后以ESP的值作为偏移地址,将数据放入堆栈。ESPESP326执行push(ebx);ebx中是969,首先ESP的值减4,变成1984,然后以ESP的
值作为偏移地址,将数据放入堆栈。969执行push(ecx);ecx中是873,首先堆栈栈顶指针ESP的值减4,栈顶偏移地址变成1980,然后按此偏移地址将数据压入堆栈。873198819841980例:假定当前栈顶偏移
地址是1992,要连续执行push(eax)、push(ebx)及push(ecx)三条指令,分析堆栈状态的变化。2022/11/123226.1.12的弹栈指令pop(16位通用寄存器);pop(32位通用寄存器);pop(16位内存单元);pop
(32位内存单元);POP指令功能用高级语言的赋值语句表示如下:目的操作数=[ESP]ESP=ESP+操作数的长度(2或4)2022/11/12323POP指令的操作数也只有16位和32位两种。POP操作是PUSH操作的逆操作。POP在修改之前把栈顶数据[ESP]复制给目的操作数,然后ES
P值自加,指向下一个数据。指令不影响条件标志。2022/11/12324ESP低地址1992执行pop(eax);先将栈顶单元的值873弹给eax,然后ESP的值+4,变成1984。ESPESP326执行pop(ebx);先将栈顶单元的值969弹给ebx,然后栈
顶指针ESP的值加4,变成1988。969执行pop(ecx);先将栈顶单元的值326弹给ecx,然后堆栈栈顶指针ESP的值+4,变成1992。873198819841980例:假定当前栈顶偏移地址是1980,要连续执行pop(eax)、pop(ebx)及pop(ec
x)三条指令,分析堆栈状态的变化。2022/11/12327访问堆栈时,程序执行的PUSH指令的次数必须与POP指令的次数相同。执行POP指令的顺序和执行PUSH指令的顺序要相反。执行PUSH指令的压入的数据长度与对应的POP指令弹出的数
据长度要相同。push(eax);push(ebx);<<使用EAX和EBX的代码>>pop(ebx);pop(eax);2022/11/123286.1.13传送地址指令指令语法:lea(源操作数,目的操
作数);目的操作数必须是32位寄存器,源操作数是任何合法的内存寻址方式。指令功能是将寻址方式指定的内存单元的偏移地址装入指定寄存器。2022/11/12329例24:staticb:uns8;@nostorage;byte7,0,6,1,5,2,4,3;…输出数据值334251607bb+2
b+7直接寻址方式stdout.put(b[7]);寄存器间接寻址方式lea(b,ebx);add(7,ebx);stdout.put((typebyte[ebx]);变址寻址方式lea(b,ebx);stdout.put((typebyte[ebx+7]);使用寄存器间接寻址方
式和变址寻址方式时,必须先将变量的偏移地址计算出来,除了用今天学习的传送地址指令外,还可以用计算静态变量偏移地址运算符:mov(&b,ebx);add(7,ebx);stdout.put((typebyte[ebx]);变址寻址方式mov(&b,ebx);stdout.put
((typebyte[ebx+7]);2022/11/123306.1.14输入/输出指令输入指令输出指令in(port,al);out(al,port);in(port,ax);out(ax,port);in(port,eax);out(eax,port);in(dx,al);
out(al,dx);in(dx,ax);out(ax,dx);in(dx,eax);out(eax,dx);2022/11/12331port是0..255中的一个值。80X86可寻址65536个I/O端口,端口号在0..255之间时,可
用port格式分别输入和输出,端口号不在0..255之间时,只能采用dx格式分别输入和输出。无论哪种格式只能用寄存器al/ax/eax中某一个,这由端口的位数决定,如是8位端口就用AL,如是16位端口就用AX,如是32位端口就
用EAX。2022/11/12332例29:in($60,al);//读端口$60mov($378,dx);//端口号$378给dxin(dx,al);//读$378端口的数据inc(al)
;//修改数据,数据值加1out(al,dx);/*把修改后的数据输出给端口$378*/2022/11/12333从windowsNT、windows2000开始,输入和输出指令已经归为特权指令,不允许用户级程序使用输入/输出指令。2022
/11/123346.1.16溢出中断指令指令格式:into();指令功能:执行这条指令时,检查标志寄存器中的溢出标志OF,如果该标志值是1,就产生中断;否则继续执行into()后的指令。2022/11/12335通常在一条有符号数算术操作指令(会影响溢出条件标志的)后,立即使用i
nto指令检查是否发生溢出。如果溢出标志不是1,系统不理会into指令;如果溢出标志是1,into指令产生HLA的ex.IntoInstr异常。2022/11/12351第七章整型控制结构7.1比较和测试指令7
.2标号7.3无条件转移指令7.4条件转移指令7.5选择结构7.6循环结构2022/11/123527.1比较和测试指令7.1.1比较指令7.1.2测试指令7.1.3测试并保存条件标志指令7.1.4布尔表达式2022/11/123537.1.1比较指令指令格式:cmp(源操作数,目
的操作数);源操作数可以是寄存器、内存单元。长度与目的操作数一致。目的操作数可以是寄存器、内存单元、常数。操作数长度可以是8位,16位,32位。操作数必须是非浮点类型的基本类型。功能:指令计算“源操作数-目的操作数”
(注意与SUB相反)。按结果修改80X86的标志寄存器。与SUB指令不同的是:它以源操作数作为被减数;它不会修改目的操作数。2022/11/12354比较指令应用举例例:cmp(EAX,0);指令执行EAX-0,按结果设置条件标志。例:cmp(eax,ebx);指令执
行eax-ebx,按结果设置条件标志。2022/11/123557.1.2测试指令指令格式:test(源操作数,目的操作数);源操作数可以是常数、寄存器、内存单元。的操作数可以是寄存器、内存单元。两个操作数不能同时为内存单元,两个操作数的长度要相同。TEST指令对操作数进行AN
D(位逻辑乘)运算,根据运算结果设置条件标志,但是不保存运算的结果。2022/11/12356TEST指令用来测试某位是0或是1。例:test(1,AL);如AL的#0位是0,运算结果为0,设置ZF标志如
AL的#0位是1,运算结果为1,清除ZF标志TEST指令用来测试寄存器是否为0。例:test(eax,eax);如eax的值是0,运算结果为0,设置ZF标志如eax的值不是0,清除ZF标志2022/11/123577.1.3测试并保存条件标志
指令指令格式setxx(目的操作数);在指令中xx表示测试条件,目的操作数只能是8位的寄存器或变量。指令功能:根据测试条件xx测试条件标志寄存器,如果测试条件xx是真,对目的操作数置1;否则对目的操作数清零。2022/11/12
358(一)简单条件SETCCF=1时就置1,否则清零SETNCCF=0时就置1,否则清零SETZZF=1时就置1,否则清零SETNZZF=0时就置1,否则清零SETSSF=1时就置1,否则清零SETNSSF=0
时就置1,否则清零SETOOF=1时就置1,否则清零SETNOOF=0时就置1,否则清零SETPPF=1时就置1,否则清零SETNPPF=0时就置1,否则清零2022/11/12359假定在测试指令之前是比较指令:cmp(X,B);//假设X是变量,B是常量(二)无
符号数比较SETAX>B时,对目的操作数置1,否则清零SETAEX>=B时,对目的操作数置1,否则清零SETBX<B时,对目的操作数置1,否则清零SETBEX<=B时,对目的操作数置1,否则清零SETEX=B时,对目的操作数置1,否则清零SETNEX<>B时,
对目的操作数置1,否则清零2022/11/12360(三)有符号数比较SETGX>B时,对目的操作数置1,否则清零SETGEX>=B时,对目的操作数置1,否则清零SETLX<B时,对目的操作数置1,否则清零SETLEX<=B时,对目的操作数置1,否则清零SETEX=B时,对
目的操作数置1,否则清零SETNEX<>B时,对目的操作数置1,否则清零2022/11/12361例4:假定变量A、B的长度都是32位,把A<=B运算的结果存放在布尔变量bool中mov(A,eax);cmp(eax,B);//比较后设置条件标志setle(bool);//根据条
件标志设置bool如果A<=B,bool的值是1,否则是0。因为SETxx指令只产生0或1,所以可以用AND指令、OR指令、SETxx指令计算复杂的布尔表达式。2022/11/12362例5:假定有符号整型变量长度
都是32位,计算A<=B&&D==E表达式的值,结果装入布尔变量bool。分析:先分别计算表达式A<=B和D==E,两个计算结果放在寄存器BL和BH中,最后用指令AND(BL,BH)完成计算。2022/11/12363将A装入寄存器EAXEAX和B比较比较结果装入
寄存器BL将D装入寄存器EAXEAX和E比较比较结果装入寄存器BH做逻辑乘BLandBH→BH把BH复制给变量boolmov(A,eax);cmp(eax,B);setle(BL);mov(D,eax);c
mp(eax,E);sete(BH);and(BL,BH);mov(BH,bool);bool=A<=B&&D==E2022/11/123647.1.4布尔表达式与高级程序设计语言最大不同之一,在汇编言中布尔表达式也必
须用指令序列描述,汇编语言中没有逻辑指令,只有位逻辑运算指令。假设有三个布尔型变量a、b和c,下面的例子说明怎样在汇编语言中计算布尔表达式。2022/11/12365例7:用汇编语言表示高级语言中的表达式c=a&&bmov(a,AL);and(b
,AL);mov(AL,c);例8:用汇编语言表示高级语言中的表达式c=a||bmov(a,AL);or(b,AL);mov(AL,c);2022/11/12366例9:用汇编语言表示高级语言中的表达式
c=aXORbmov(a,AL);xor(b,AL);mov(AL,c);例10:用汇编语言表示高级语言中的表达式b=!amov(a,AL);//NOT指令不是对操作数not(AL);//逻辑非,而是逐位取反and(1,AL);
//所以,(not0)不等于1。mov(AL,b);//要用AND指令纠正。2022/11/12367例10的另一种实现方法:mov(a,al);xor(1,al);//对0位取反mov(al,b);注意,就像上面指出的,NOT指令是
对每一位取反。对$00取反是$FF;对$01取反是$FE。两个结果即不是1也不是0。把这些结果与1逻辑乘,就会得到正确的结果。你如果用“xor(1,ax);”指令实现逻辑非操作,可以节省一条指令。2022/11/1
2368例:B=(X==Y)&&(A<=D)||((Z-A)!=5);mov(X,eax);cmp(eax,Y);sete(AL);//AL为(x==y)mov(A,ebx);cmp(ebx,D);setle(BL);//BL为(a<=d)and(AL,BL);//B
L为(x==y)&&(a<=d);mov(Z,eax);sub(A,eax);cmp(eax,5);setne(AL);//AL为(Z-A)!=5or(BL,AL);//AL为((X==Y)&&(A<=D))||mov(AL,B);//((Z-A)!=5);2022/11/123697.2标号语法
格式:标号标号按标识符规则取名。放在程序中某条指令之前时,需要在它和指令之间加分隔符冒号:,在一个程序模块中不允许出现同名标号。标号按长度分16位和32位,这是两种不同性质的标号。2022/11/1237032位标号是一个符号地址常量,在设计程序时确定其值后,在执行程
序中不能改变其值。对32位的标号可以用LEA指令或用“&”取地址运算符计算指令的偏移地址;16位标号是一个偏移量常数,代表转移指令到目的地的字节距离。标号可以与指令在同一行,或自己单独占一行,在另一行写一条可执行指令。2022/11/12371例8:L1:mov(0,eax);或:L1:
mov(0,eax);L1代表"mov(0,eax);"指令的起始地址。2022/11/123727.3无条件转移指令jmp指令无条件把控制转移到程序中的另一点。这条指令有三种格式,常用的是直接转移格式:jmp标号;标号指定转移目的地的地址。2022/11/12373无条件
转移指令用的标号是32位标号,所以无条件转移指令可在内存地址范围内转移。例:jmpoveraddress;无论之前的指令执行的结果怎样,执行该指令之后,将overaddress代表的偏移地址值装入
寄存器EIP,overaddress就是处理器执行的下一条指令的偏移地址。2022/11/123747.4条件转移指令机器的电子元件不能区别一个整数是不是有符号数,所以比较指令只是做源操作数减目的操作数运算,按运算结果设置条件标志。至于谁大谁小由程序员根据条
件标志确定。前面学习测试并保存比较结果指令时,它分测试单个标志、测试无符号数比较、有符号数比较结果共三类指令。条件转移指令是判断条件标志值是否与指定条件相同再决定是转移还是继续执行。2022/11/12375条件转
移指令分简单条件转移、无符号数条件转移、有符号数条件转移。条件转移指令的语法是:Jxx标号;功能:根据xx测试标志寄存器中一个或多个标志,看是否满足xx所代表的条件(就像SETxx指令)。如果满足条件,把EIP寄存器内容加标号的值形成下一条指令的偏移地址,如不满足,CPU则执行Jxx指令后的
第一条指令。2022/11/12376有条件和无条件转移指令差异与无条件转移指令相比有两点不同:(1)条件转移指令没有间接转移的格式。只有直接转移的格式。(2)条件转移指令里的标号是16位的,它的值是有符号整数,所以转移的范围是以条件转移指令后第一条指令地址为起点,最远到它前面的32768或到
它后面的32767个字节。条件转移指令只测试标志,不会修改条件标志。2022/11/12377jxxL1;…………XX+32767X-32768条件转移指令的转移范围2022/11/12378条件转移指令通常放在比较指令cmp或测试
指令test之后,汇编语言用比较指令和条件转移指令或测试指令和条件转移指令形成选择机制。用这个机制组成汇编语言程序的选择结构或循环结构。2022/11/12379(1)简单条件转移指令简单条件转移指令只检查零、符号、溢出、进位/借位、奇偶一个条件标志,决定是否转移。假定在比较指令后
,紧跟一条条件转移指令,组成一个基本的控制结构:cmp(源操作数,目的操作数);JxxL1;2022/11/12380比较指令、转移指令组合的流程图yn源操作数与目的操作数比较xx条件成立L12022/11/12381JC(CF=1)转移L1处执行;否则继续执行JNC(CF
=0)转移L1处执行;否则继续执行JZ(ZF=1)转移L1处执行;否则继续执行JNZ(ZF=0)转移L1处执行;否则继续执行JS(SF=1)转移L1处执行;否则继续执行基本的简单条件转移指令2022/11/12382JNS(SF
=0)转移L1处执行;否则继续执行JO(OF=1)转移L1处执行;否则继续执行JNO(OF=0)转移L1处执行;否则继续执行JP(PF=1)转移L1处执行;否则继续执行JNP(PF=0)转移L1处执行;否则继续执行基本的简单条件转移指令2022/11/12383例:判断一个有符号32位整数X是
不是负数。分析:有符号整数的最高位代表符号位,如果是1,表示是负数;否则表示非负数。怎样确定最高位的值?方法有多种,可以把它左移一位,然后判断cf标志;也可以用位逻辑运算等等。我以移位运算为例,为了不破坏数据,先将数据装入一个寄存器中,然后再对寄存器做移位操作。先画流程图:2
022/11/12384把X装入寄存器EAXEAX左移一位CF=0YN输出X是负数L1开始结束…mov(x,eax);shl(1,eax);jncL1;stdout.put(…);L1:stdin.readLn();…2022/11/12385(2)无符号整数的条件转移指令为了说明方便,假
定用比较指令和无符号整数条件转移指令“JxxL1;”组成控制结构cmp(源操作数,目的操作数);JxxL1;2022/11/12386比较指令和转移指令组合的流程图yn源操作数与目的操作数比较xx条件成立L12022/11/12387JA如果“
源操作数”>“目的操作数”就转移到L1处执行,否则执行本指令后的第一条指令。JAE如果"源操作数">="目的操作数"就转移到L1…JB如果“源操作数”<“目的操作数”就转移到L1…JBE如果"源操作数"<="目的操作数"就转移到L
1…基本的无符号数的条件转移指令2022/11/12388基本的无符号数的条件转移指令JE/JZ如果“源操作数”=“目的操作数”就转移到L1…JNE/JNZ如果"源操作数"<>"目的操作数"就转移到L1…2022/11/12389(3)有符号数条件转移指令为了说
明方便,假定在比较指令后使用有符号数条件转移指令“JxxL1;”。cmp(源操作数,目的操作数);JxxL1;2022/11/12390比较指令和转移指令组合的流程图yn源操作数与目的操作数比较符合
xx条件L12022/11/12391JG如果“源操作数”>“目的操作数”就转移到L1处执行,否则执行本指令后的第一条指令。JGE如果"源操作数">="目的操作数"就转移到L1…JL如果“源操作数”<“目的
操作数”就转移到L1…JLE如果"源操作数"<="目的操作数"就转移到L1…JE如果“源操作数”=“目的操作数”就转移到L1…JNE如果"源操作数"<>"目的操作数"就转移到L1…2022/11/123927.5选择结构7.5.1单分支结构7
.5.2多分支结构2022/11/123937.5.1单分支序列单分支格式:条件表达式指令序列JxxL1;//符合条件xx就转移去L1程序段1//否则执行程序段1后去L1L1:条件xx与条件表达式密切
相关。2022/11/12394条件表达式指令序列符合条件xxynL1程序段1在高级语言的单分支IF语句中,如果条件成立就执行IF语句中的语句块,否则执行IF语句后的语句。而汇编语言的转移指令是符合条件就转移,否则执
行转移指令后的第一条指令。2022/11/12395在汇编语言程序中,Jxx是条件转移指令,xx代表测试条件,如果xx成立,就转移到标号L1处去执行;否则就执行程序段1,然后去L1处执行。要根据题目的要求决定xx。例如:A<=B&&D==E如果表达式值是真就输出1,否则不输出。假设布尔表达式
的值放入BH,根据前面所学知识,可以写成如下程序段:2022/11/12396布尔表达式值→BHBH<>1输出1结束ynL1开始mov(A,eax);cmp(eax,B);setle(BL);mov(D,eax);cmp(eax,E);sete(BH)
;and(BL,BH);先计算布尔表达式值2022/11/12397mov(A,eax);cmp(eax,B);setle(BL);mov(D,eax);cmp(eax,E);sete(BH);and(BL,BH);//计算A<=B&&D=Ecmp(B
H,1);jneL1;stdout.put(1,nl);L1:stdin.readLn();2022/11/12398例24:如果AL值不是0就执行mov(BL,CL)test(AL,AL);jzl1;mov(BL,CL);L1://合拢点AL=0BLCLYNL1cmp(AL,
0);jzl1;mov(BL,CL);L1://合拢点2022/11/12399例27:如果CH值>=‘a’就执行and($5f,CH)cmp(CH,'a');jbl1;and($5f,CH);L1://合拢点ynL1CH<‘a’CH
逻辑乘$5f2022/11/12400例30:如果a>b,就把d的值赋予c。假设题目中的变量都是32位有符号整数。程序的流程图如下:aEAXEAX<=bynL1dc2022/11/12401根据流程图写出下列代码:mov(a,eax);cmp(eax,b);jlel
1;mov(d,c);l1:合拢点2022/11/124027.5.2多分支序列汇编语言一种表示双分支的方法是:布尔表达式指令序列JxxL1;//表达式为真执行程序段1程序段2//否则执行程序段2jm
pL2;L1:程序段1L2:……2022/11/12403双分支选择结构的一种流程图ynL1L2表达式值为真程序段1程序段22022/11/12404例33:如eax值在100..1250之间就执行sub(100,eax);cmp(eax,100);jbl1;cmp(ea
x,1250);jal1;sub(100,eax);L1:eax<100eax>1250eax–100eaxyynnL12022/11/12405例34:如果i32的值在-5..5之间就执行add(5,i32);cmp(i32,-5);jll1;cmp(i32,5);jgl1;
add(5,i32);L1:i32>5i32<-5i32+5i32yynnL12022/11/12406例35:如果eax的值不在100..1250之间就执行add(100,eax)ynnyL1eax>=100eax<=1250eax+100eax(这是一张错误的流程图)2022/11/124
07如果eax的值不在100..1250之间就执行add(100,eax)(1)EAX<100EAX>1250yynneax+100eaxL1L22022/11/12408cmp(eax,100);jb
L1;cmp(eax,1250);jaL1;jmpL2;L1:add(100,eax);L2://选择结构的合拢点2022/11/12409输入一个整数,判断它的值是零还是非零。分析:可以用输入的值与零比较,如相等,它就是零,如不等,它的值就是非零。这是一个选择结构。2
022/11/12410输入一个整数XX=0X不等于0x等于0结束ynL1L2开始stdin.get(x);cmp(x,0);jzL1;stdout.put(“x<>0”);jmpL2;stdout.put(“x=0”);L1:L2:stdin.readLn();2022/11/12411
例:输入两个无符号整数,比较它们的大小,输出比较结果。分析:这道题目是选择结构,要在三种结果中选择其中一种,所以有三条通路,先画出流程图。2022/11/12412输入两个无符号整数uaub把ua装
入寄存器EAXEAX>ubEAX=ubua=ubua<ubua>ub结束ynnyL1L2L32022/11/12413stdin.get(ua,ub);mov(ua,eax);cmp(eax,ub);jaL1;jeL2;stdout.p
ut(ua,”<”,ub,nl);jmpL3;L1:stdout.put(ua,”>”,ub,nl);jmpL3;L2:stdout.put(ua,”=”,ub,nl);L3:stdin.readLn();2022/11/12414例:输入一个0到20之间的整数,输出它的平方值。
在内存建立一个平方表,因为20的平方值是400,超过了8位,所以每个平方值用16位存储(2个字节)。用变量tablex表示平方表的起始地址。2022/11/12415Tablex+0Tablex+2Tablex+4Tablex+6Tablex+8Tablex+40.........014916
400从键盘输入0到20中某个值xXX2所在地址0Tablex+01Tablex+22Tablex+43Tablex+64Tablex+8......20Tablex+40内存中的平方表2022/11/12416从列表分析得出:只要输入值X在允许范围内,它的平方值所在的偏移地址是tablex+2
*X,关键问题是用什么寻址方式来表示这个偏移地址?从理论上讲,可以采用寄存器间接寻址、变址寻址、比例变址寻址方式。为什么不能采用直接寻址方式?请大家想一想。实际中选择原则是方便简单为好。2022/11/12417(1)寄存器间接寻址方式:Xeaxeax左
移1位(实现X*2)Tablex偏移地址ebxeax+ebxebx[ebx](用来访问内存取X的平方值)2022/11/12418(2)变址寻址方式:Xeaxeax左移一位(实现X*2)tablex[eax](用来访问内存取X的平方值
)2022/11/12419(3)比例变址寻址方式:XEAXtablex[EAX*2](用来取X的平方值)从以上三种寻址方式中看出比例变址寻址方式在书写上是最简单的,它只要一条指令就可以完成寻址准备,而寄存器间接寻址和变址寻址方式分别要4条和2条指令完成寻址准备。2022/11/1
2420输入xX<0X>20把X的值复制给寄存器EAX取X的平方值tablex[eax*2]BXEBX的值给变量ybxy输出X的平方值y开始结束L1YYN输入越界L22022/11/12421statictablex:uns16;@nostorage;
uns160,1,4,9,16,25,36,49,64,81,100,121,144,169,196,225,256,289,324,361,400;X:int32;y:int16;...2022/11/12422stdout.put("输入
一个0..20的整数:",nl);stdin.get(X);cmp(X,0);jlL1;cmp(X,20);jgL1;mov(X,eax);mov(tablex[eax*2],y);stdout.put(X,"的平方值=",y,nl);j
mpL2;L1:stdout.put("输入越界",nl);L2:2022/11/124237.6循环结构7.6.1三种循环结构7.6.2循环和寄存器2022/11/124247.6.1三种循环结构汇编语言程序循环结构是利用比较指令、条件转移指令、无条件转移指令或
循环转移控制指令实现的,从逻辑上可以把循环结构分成循环初始化、循环(结束)条件判断、循环体三大部分。循环(结束)条件判断部分可以放在循环体前、中、后的位置.2022/11/12425(1)在循环体前判断循环初始
化指令序列符合循环结束条件ynL1L2循环体循环初始化指令序列L1:cmp(源,目的);jxxL2;循环体jmpL1;L2:……2022/11/12426(2)在循环体中判断循环初始化循环体前部循环体后部符合循环结束条件ynL1L2循环初始化L1
:循环体前部cmp(源,目的);JxxL2;循环体后部JmpL1;L2:...2022/11/12427(3)在循环体后判断循环初始化循环体指令序列符合循环条件ynL1循环初始化指令序列L1:循环体指令序列cmp(源,目的);jxxL1;...2022/11/124
28(4)用ecx的值作为循环结束条件循环初始化…necx循环体指令序列(ecx--)!=0L1yn循环初始化mov(n,ecx);L1:循环体指令序列loopL1;...2022/11/12429例:求1+2+3
..+100数列之和分析:汇编语言的加法指令一次只能加两个数,并且要修改目的操作数,设一个和变量sum,初始值为0,算法用文字描述为次数操作1sum+1sum2sum+2sum3sum+3sum…100sum+100s
um2022/11/12430次数是个变化的量,它的初始值是1,每做一次加法它的值要自加1,设一个次数变量,用寄存器cx表示,算法用文字描述次数操作1sum+cxsum,cx++2sum+cxsum,cx++3sum+cxsum,cx++…100sum+cxsum,cx++2022/
11/12431这个问题是个循环问题,首先要找到循环条件或循环结束条件:循环条件:cx<=100循环结束条件:cx>100下面分别用不同形式的循环结构解决这个问题2022/11/124320sum;1cxcx>100sum+cxsum;cx++;
L1L2结束输出sumYN开始2022/11/12433stdout.put("求1+2+3..+100",nl);mov(0,sum);mov(1,cx);L1:cmp(cx,100);jaL2;a
dd(cx,sum);inc(cx);jmpL1;L2:stdout.put("sum=",sum,nl);2022/11/124341cx;0sum;sum+cxsum;cx++;cx<=100输出sum
结束L1yn开始2022/11/12435stdout.put("计算1+2+3+...+100",nl);mov(0,sum);mov(1,cx);L1:add(cx,sum);inc(cx);cmp(cx,100);jbeL1;stdout.put("sum=",sum,nl);202
2/11/12436例:编程序输出2**0到2**30的乘幂表分析:这个问题可以用乘法指令求解,因为它的特点是计算2的幂,所以用移位指令更方便,根据二进制数移位的性质,只要没有溢出,一个数左移一位相当于该数乘以2。2022/11/12437十进制表示二进制表示20121102
2100231000241000025100000261000000……2301000…000002022/11/12438从表格中看出,20就是1,把它左移一位,它的值变成21,把它再左移一位,它的值
变成22,…设变量p,初值是1,它就是20,第一次左移结果是1*2,它就是21,第二次左移结果是1*2*2=4,它就是22,第三次左移结果是1*2*2*2=8,它就是23,……,如此重复31次就计算出
20到230乘方值。2022/11/12439这个变量要多大?按题目要求要计算230,变量至少要有32位长。设置一个循环控制变量cnt,它的初值是0,循环条件是cnt<31。2022/11/12440循环初始化1pmo
v(0,cnt);输出pp左移1位修改循环计数器cnt++cnt<31YL1结束开始N2022/11/12441staticp:int32;cnt:int32……mov(1,p);mov(0,cnt);L1:stdout.put("2**(",cnt:
2,")=",p:10,nl);shl(1,p);inc(cnt);cmp(cnt,31);jbL1;2022/11/12442例:输出裴波那奇数据的前40项分析:从第三项开始,数列每项的值是它前两项值之和。可以采用递推的算法,从第一和第二项计算出第三项,然后从第二和
第三项计算出第四项,...为了叙述方便,把参加运算的两项和运算的结果分别称为前、中、后项。下面分析前中后三项的变化规律:2022/11/12443前+中后1121232353585813用变量first和寄存器EBX,EAX,分别代表前、中、后三项,递推算法用文字表达
为:从表中可以看到,前、中、后项的值在程序运行中是变化的,所以必须用变量表示。2022/11/124441.输出前项2.前项加中项得后项3.中项复制给前项4.后项复制给中项用流程图表示算法如下:重复40次2022/11/12445开始循环初始化1first;1EBX;1c
x;输出前项first准备计算firstEAX计算后项EAX+EBXEAX中项复制给前项EBXfirst后项复制给中项EAXEBX修改计数器CX++CX<41YL1结束N2022/11/12446mov(1,first);mov(1,ebx);mov(
1,cx);L1:stdout.put(first:10);mov(first,eax);//准备计算add(ebx,eax);//计算后项mov(ebx,first);//中项变前项mov(eax,ebx);//后项变中项inc(c
x);cmp(cx,41);jbL1;……2022/11/12452例:输入一个8位16进制数,颠倒输出。分析:一种方法是用求余数的办法;另一种方法是用移位的办法,注意在机器中数据是用二进制数表示的,我们也知道每
位十六进制数由四位二进制数组成。从数据的左边开始,逐位移动,每移四位就得到一位十六进制数,第一次得到的是十六进制最高位,第二次得到的是十六进制次高位……,现在问题是怎样实现颠倒输出?第一次得到的数最后输出,最后得到的数要第一个输出,这是什么
规律?2022/11/12453BX=0000000000000000EAX左移1位BX带进位左移一位BX入栈这是个循环嵌套的问题。算法用流程图表示如下:BXCFEAX重复四次重复八次1102022/11/12454输入十六进制数EAX;输出E
AX;0i0bx;0j;EAX左移一位;BX带CF循环左移一位修改循环变量j++;j<4BX入栈;修改外循环变量i++开始i<8①YYNN输入数据,将8位十六进制数从高到低逐位压入堆栈L1L22022/11/12455当8位十六进制数全部压入堆栈后,就要从堆栈里向外弹数据,第一个弹出的就是原来最低
位的十六进制数,一次弹出一个字,有效的只是低四位,为了美观,在一字节里装两位十六进制数,把8位十六进制数合并到寄存器EAX中,然后输出EAX。算法如图所示:2022/11/12456ESP+2110111011
1010000ESPBHBLAHALEAX00000……00000……0000000000……0000000000000000000000000000①②③00001101000000000000111100000000EAX左移4位弹栈给BXBL与AL逻辑加给AL
①②③0000000011012022/11/124571.0EAX,0BX2.弹一个字BX3.BL||ALAL4.EAX左移四位从以上分析可以看出输出预处理是一个循环结构,注意第四步只需做七次。重复8次重复7次2022/11/12458①对BX
初始化,0EAX;0j从堆栈栈顶弹出一个字BX把BL的内容合并到ALj>=7EAX内容左移4位j++j<8L3L4YY输出翻转后的结果EAX结束2022/11/12459statici:byte;j:byte;stdin.
get(eax);stdout.put("你输入的值是:",eax,nl);mov(0,i);L1:mov(0,bx);mov(0,j);L2:shl(1,eax);rcl(1,bx);inc(j);cmp
(j,4);jbL2;push(bx);2022/11/12460inc(i);cmp(i,8);jbL1;mov(0,eax);mov(0,bx);mov(0,j);L3:pop(bx);or(bl,al);cmp(j,7);jaeL4;shl(4,eax);L4:inc(j);cm
p(j,8);jbL3;stdout.put(eax,nl);2022/11/124617.6.2循环嵌套和寄存器寄存器是存放循环控制变量值的理想场所。在循环嵌套中使用寄存器时,可能会发生错误修改寄存器的问题。mov(8,cx);//外循环用cx计数l1:mo
v(4,cx);//内循环也用cx计数l2:<<语句块>>dec(cx);jnzl2;dec(cx);jnzl1;2022/11/12462这是循环嵌套的例子,内外循环都用同一个寄存器作为循环控制变量。本例会产生无限循环,因为内循环结束时,CX的值是0,经过外
循环中的dec(cx);指令,使CX的值永远达不到0。解决的方法是保存和恢复CX寄存器或用不同的寄存器作为循环控制变量。2022/11/12463mov(8,cx);l1:push(cx);//保护外循环计数器CXmov(4,cx);l2:<<语句块>>dec(
cx);jnzl2;pop(cx);//恢复外循环计数器CXdec(cx);jnzl1;2022/11/12464第八章浮点型运算8.1浮点指令8.2浮点算术表达式2022/11/124658.1浮点运算指令8.1.1FPU寄存器8.1.2浮点机器数编码8.1.3数据复制指令8.
1.4类型转换指令8.1.5算术指令8.1.6比较指令8.1.7常数指令8.1.9浮点数和整数运算指令2022/11/124668.1.1FPU寄存器8.1.1.1FPU数据寄存器8.1.1.2FPU控制寄存器8.1.1.3FPU状态寄存器202
2/11/12467Intel在80486和以后的微处理器里增加了浮点处理单元(FPU),80X86FPU中包含了13个寄存器。它们是8个浮点数据寄存器,一个控制寄存器,一个状态寄存器,一个标签寄存器,一个数据指针,一个指令指针。这里只讨论数
据寄存器、控制寄存器、状态寄存器。2022/11/124688.1.1.1FPU数据寄存器FPU提供8个80位数据寄存器,它们组成一个寄存器栈。它们的物理编号从0到7。汇编语言根据它们在栈里的位置,逻辑命名为ST0,ST1,…ST
7。ST0代表当前的栈顶,每当有入栈和出栈操作时,都会修改栈顶指针,而ST0这个名字就随着栈顶指针的修改而代表不同的物理寄存器。随着修改栈顶指针,ST0,ST1,..ST7代表的寄存器同步发生改变。物理编号是不变的,逻辑名是可变的
。2022/11/12469st0st1st7st6st5st4st3st2796408.1.1.1FPU数据寄存器000001010011100101110111物理号逻辑号st0st1st2st3st4st5st6st7浮点数进栈2022/11/12470st0st1st7st6st5st4
st3st2796408.1.1.1FPU数据寄存器000001010011100101110111物理号逻辑号st0st1st2st3st4st5st6st7浮点数出栈2022/11/12471ST0总是堆栈栈顶寄存器的名字,ST1是S
T0的下一个寄存器的名字,ST2是ST1的下一个寄存器的名字,等等。每当指令要压入一个数据时,首先修改栈顶指针(用寄存器的物理编号表示),修改后的栈顶指针所指的寄存器就用ST0这个名字,指令执行前用ST0这个名字的寄存器在指令执行后就改用ST1这个名字,入栈前用S
T1这个名字的2022/11/12472寄存器在入栈后就用ST2这个名字,以此类推;出栈时,出栈前的ST0在出栈后用ST7这个名字,出栈前的ST1在出栈后用ST0这个名字,出栈前的ST2在出栈后用ST1这个
名字,…以此类推。如果需要输出ST0的值时,不能用ST0作为标准输出函数的实参,可以将它先装入一个浮点型变量,用这个浮点型变量作为标准输出函数的实参。15111098543210非法操作下溢出上溢出零除非规格化精度舍入控制
精度控制异常屏蔽保留8.1.1.2FPU控制寄存器2022/11/12474精度控制(位8、9)它提供了与需要IEEE754标准的旧软件兼容的能力。通常FPU默认设置为%11,即选择64位尾数精度。"00
"精度为24位"01"保留"10"精度为53位"11"精度为64位异常标志堆栈失败标志精度异常标志下溢出标志上溢出标志零除标志非规格化标志非法操作标志异常标志栈顶指针C2C1C0C31514131211109876543210忙8.1.1.3FPU状态寄存器2022/11/1
2476位8、9、10、14是FPU的条件码位。位11到13提供栈顶寄存器的物理编号。初始值是0,它代表当前栈顶的指针。当压入一个数据时,栈顶寄存器物理号减1再对8取模结果是新栈顶的物理寄存器号;如果从栈顶弹出数据,栈顶寄存器号加1再对8取模,结
果是新栈顶的寄存器号。在程序里使用的是寄存器的逻辑号stn,为了访问浮点寄存器,机器必须将这个逻辑号转换成相应的寄存器物理编号。2022/11/12479表8.1FPU条件码值浮点指令条件码代表条件C3C2C1C0FCOMI(S
T0,STi)FCOMIP(ST0,STi)00110001XXXX0101ST0>STiST0<STiST0=STiST0或STi无定义ftst()00110001XXXX0101ST0是正数ST0是负数S
T0是零ST0不能比较2022/11/12480标志寄存器的低8位布局浮点状态寄存器的高8位布局76543210CFPFZF15141312111098C2C0C32022/11/124818.1.2FPU数据类型FPU支持7种数据类型:三种整型、一种
压缩十进制类型,三种浮点类型。整型提供16、32、64位整数。压缩十进制类型提供17位有符号十进制整数(BCD)。剩下的三种数据类型是32、64、80位浮点数据类型。2022/11/124828.1.3数据复制指令8.1.3.1FLD指令8.1.3.2FST和FSTP指令8.
1.3.3交换指令2022/11/124838.1.3.1FLD指令指令格式:Fld(浮点寄存器);Fld(浮点变量);Fld(浮点常数);浮点寄存器可以是st0..st7中任何一个寄存器。浮点变量可以是32
位、64位、80位。FLD指令会自动将32位、64位操作数转换成80位后压入堆栈。2022/11/12484指令功能:FLD指令首先减栈顶指针值(TOS)然后将80位值存入由TOS指针指定的物理寄存器。如
堆栈溢出,FLD指令设置堆栈失败位,如装入的是80位非规格化的值,它设置非规格化的异常。如想给栈顶装入空的浮点寄存器(或执行其他非法操作),它设置非法操作位。2022/11/12485例:fld(st1);//浮点寄存器fl
d(real32_variable);//浮点型变量fld(real64_variable);fld(real80_variable);fld(3.0);//常数是HLA的扩充注意:即使32位通用寄存器里装的是REAL32类型的值也不能直接把32位通用寄存器的值装入浮点
堆栈。2022/11/124868.1.3.2FST和FSTP指令指令格式:FST(目的操作数);FSTP(目的操作数);在此目的操作数可以是32位、64位、80位内存变量,也可以是浮点寄存器(st0..st7)。FST指令只将浮点寄存器栈顶的值复制给另一个
浮点寄存器或32、64、80位内存变量。指令不修改浮点寄存器堆栈的指针。FSTP指令把栈顶值传送给目的操作数,然后弹出栈顶的值。它修改状态寄存器中栈顶指针的值,使ST1变成ST0。2022/11/12487fst(rea
l32_variable);fst(real64_variable);fst(realdata[ebx*8]);fst(st2);fstp(st0);//等效栈顶值作废fstp(st1);//等效保留栈顶值2022/11/12488fstp(st0);从指令功能分析
可知,先将浮点栈栈顶st0复制给目的操作数st0,然后再弹出栈顶,st1变成st0,这就相当于原来栈顶值作废。fstp(st1);从指令功能分析可知,先将浮点栈栈顶st0复制给目的操作数st1,st1的值被修改,然后再弹出栈顶,st1变成st0,这就相
当于保留了原栈项。2022/11/124898.1.3.3交换指令指令格式(1):FXCH(STi);功能:将栈顶ST0值与指定寄存器STi的值交换。指令格式(2):FXCH();功能:将栈顶ST0值与浮点寄存器ST1的值交换。2022/11/12490例:求ST2平方根fxch(st2);//
ST0与ST2值交换fsqrt();//求ST0的平方根fxch(st2);//ST0与ST2值交换2022/11/124918.1.4类型转换指令8.1.4.1FILD指令8.1.4.2FIST和FISTP指令2022/11/124928.1.4.1FILD指令指令格式:fild(
16位整型变量);fild(32位整型变量);fild(64位整型变量);FILD指令把16位、32位、64位二进制补码整数转换成80位扩展精度格式,将转换结果压入堆栈。注意操作数只能用内存变量,不能用通用寄存器。2022/11/124938.1
.4.2FIST和FISTP指令fist(目的操作数);fistp(目的操作数);目的操作数是16、32或64位整数变量,指令将栈顶80位扩展精度值转换成16,32,64位的整数,然后存入由目的操作数指定的内存变量。指令不能用通用寄存器作为目的操
作数。FIST指令转换栈顶的值,保存转换的结果。但它不会影响浮点寄存器栈。FIDTP指令在存储转换的值后,弹出栈顶值。2022/11/124948.1.5算术指令8.1.5.1加法指令8.1.5.2减法指令8.1.5.
3乘法指令8.1.5.4除法指令8.1.5.5平方根指令8.1.5.8求绝对值指令8.1.5.9改变符号指令2022/11/124958.1.5.1加法指令(1)无操作数的指令格式:fadd();faddp();2种格式是等价的。从堆栈分两次弹出st0和st1,相加后将它们
的和放入st0。结果堆栈中只留了两个数的和。2022/11/12496例12:fa,fb,fc都是浮点变量,计算fa+fb+fc,把和存放在栈顶寄存器。解法一:用无操作数的加法指令做。Fld(fa);Fld(fb);Fadd();Fld(fc);Fadd();2022/11/1
2497(4)带操作数的指令格式:fadd(32位/64位浮点变量);fadd(浮点常数);FADD指令把32位、64位浮点型变量值与ST0值相加。在相加之前,指令自动把操作数转换成80位扩展精度值,弹出ST0做加法,相加的结果放入栈顶(ST0),原来的栈顶值被修改。注意不允许使用80位内存操作数
。这两条指令节省了压栈操作时间。2022/11/12498例12:fa,fb,fc都是64位的浮点变量,计算fa+fb+fc,只把和存放在栈顶寄存器。解法二:用带操作数的加法指令做。Fld(fa);Fadd(fb);Fadd(fc);2022/
11/124998.1.5.2减法指令(1)无操作数指令格式:fsub();//ST0=ST1-ST0fsubp();FSUB和FSUBP指令功能:弹出ST0和ST1,计算ST1-ST0,把差压入浮点寄存器栈。这条指令暗
示,要先压入被减数,再压入减数。2022/11/12500例:从键盘输入两个浮点数fa和fb,计算fa-fb,输出fa和fb及计算结果。分析:利用fsub指令,先将被减数压入堆栈然后再将减数压入堆栈,执行减法指令后,栈顶内容就是所求结果。N-S流程图如下:2022/11/12501输
入两个浮点数fa,fb被减数fa入栈fa减数fb入栈fb浮点减法fsub在栈顶保留结果弹出栈顶fc输出fafbfc解:(浮点减法举例1)stdin.get(fa,fb);fld(fa);fld(fb);fsub();fstp(fc
);stdout.put(fa,fb,fc);2022/11/12502(4)带一个操作数的指令格式:fsub(32位/64位浮点变量);fsub(浮点常数);指令功能:ST0是被减数,先弹出ST0,用弹出的ST0减浮点变量或
浮点常数,再把差压入浮点寄存器栈。注意:指令先弹出ST0再做减法,将差压入堆栈2022/11/12503fa入栈fast0+fbst0st0-fcst0例14:计算fa+fb-fc,和存放在栈顶寄存器
浮点减法举例14fld(fa);fadd(fb);fsub(fc);2022/11/125048.1.5.3乘法指令浮点数与整数不同处之一是,汇编语言中把浮点数一律当作有符号数,所以浮点的乘法和除法只有一种指令;不要求乘积
的长度是乘数长度的2倍;不要求被除数的长度必须是除数长度的2倍;另外,乘积和商总是放在浮点寄存器栈顶。2022/11/12505(1)无操作数的指令格式:fmul();//ST0=ST0*ST1fmulp();指令功能:弹出ST0和ST1,把它们相乘,将乘积压入堆栈(ST0)。2022/11/12
506浮点数fa入栈,fafb入栈,fbfc入栈,fc做乘法st0*st1结果入栈做减法st1–st0st0例16:计算fa-fb*fc,差放在栈顶寄存器。fld(fa);fld(fb);fld(fc);fmul();
fsub();解:(浮点数乘法举例)2022/11/12507(4)带一个操作数的指令格式:fmul(32位/64位浮点变量);fmul(浮点常量);浮点变量长度是32位或64位。指令将浮点变量或浮点常量转换成80位扩
展精度值,弹出ST0做乘法。乘积压入栈顶。2022/11/12508浮点数fa入栈,fafb入栈,fbSt0*fc积入栈做减法st1–st0差st0例16:计算fa-fb*fc,差放在栈顶寄存器。fld(fa);fld(fb);fmul(fc);fsub();解:(
浮点乘法举例16b)2022/11/125098.1.5.4除法指令(1)无操作数的指令格式:fdiv();//ST0=ST1/ST0fdivp();指令功能:指令弹出ST0和ST1,计算ST1/ST0,然后把结果压入堆栈。指令
暗示,要先将被除数压入浮点栈,再将除数压入浮点栈。2022/11/12510例:输入两个浮点数ra,rb,计算ra/rb,显示计算结果。解:(浮点数除法举例1)输入两个浮点数rarbra入栈rarb入栈rb做除法st1/st0st0栈顶出栈st0rc输出结果rcstdin.ge
t(ra,rb);fld(ra);fld(rb);fdiv();fstp(rc);stdout.put(rc,nl);2022/11/12511(6)带一个操作数的指令格式:fdiv(除数);指令中的除数可以是浮点常数、32位或64位浮点变量。指令功能:用弹出的st0做被除数,除法运算后将商压入
st0。2022/11/12512例:输入两个浮点数ra,rb,计算ra/rb,显示计算结果。解:(浮点除法举例1b)输入两个浮点数rarbra入栈ra做除法st0/rbst0栈顶出栈st0rc输出结果rcstdi
n.get(ra,rb);fld(ra);fdiv(rb);fstp(rc);stdout.put(rc);2022/11/125138.1.5.5平方根指令指令格式:fsqrt();指令功能:FSQRT指令不带操作数。它计算
栈顶值的平方根,然后将平方根放入ST0。要求栈顶值必须大于或等于0,否则指令会产生非法操作异常。2022/11/12514浮点数x入栈,x复制栈顶st0*st1st0浮点数y入栈,y复制栈顶例21:计算Z=sqrt(x*x+y*y)。st0*st1st0做加法s
t0+st1st0计算平方根,st0Zfld(x);fld(st0);fmul();fld(y);fld(st0);fmul();fadd();fsqrt();fstp(z);2022/11/12515浮点
数x入栈,xx*st0st0浮点数y入栈,yy*st0st0解法2:计算Z=sqrt(x*x+y*y)。做加法st0+st1st0计算平方根,st0Zfld(x);fmul(x);fld(y);fmul(y);fadd();fsqrt();fstp(z);202
2/11/125168.1.5.8求绝对值指令指令格式:fabs();指令功能:FABS清除ST0中尾数的符号,计算ST0的绝对值。如堆栈是空的,指令设置堆栈异常位和非法操作位。2022/11/12517
浮点数x入栈,x例29:计算x=sqrt(abs(x))计算栈顶st0的平方根st0栈顶st0x求栈顶st0的绝对值st0fld(x);fabs();fsqrt();fstp(x);2022/11/125188.1.5
.9改变符号指令指令格式:fchs();指令功能:FCHS指令改变ST0中值的符号,它对尾数的符号位取反(这是浮点数的求补指令)。如堆栈是空的,指令设置堆栈异常位和非法操作位。2022/11/12519例:计算(-b+a)/(
2*c),假定a,b,c是浮点型变量分析:先求b的相反数,再与a相加作为分子(被除数),计算2*c,作为分母,最后做除法。只是这些运算必须在浮点栈中进行。算法表现为顺序结构,N-S流程图如下:2022/11/1
2520b入栈b求b的相反数a入栈ast0+st1st0st0*2.0st0c入栈cst1/st0st0fld(b);fchs();fld(a);fadd();fld(c);fmul(2.0);fdi
v();2022/11/12521b入栈b求b的相反数st0+ast0c入栈cst0*2.0st0st1/st0st0解法2:(-b+a)/(2*c)fld(b);fchs();fadd(a);fld(c);fmul(2.0);fdi
v();2022/11/125228.1.6比较指令8.1.6.1*与零比较指令8.1.6.2通用比较指令2022/11/125238.1.6.2通用比较指令指令格式FCOMI(ST0,STi);FCOMIP(ST0,STi);/
/比较后弹出st0指令功能:FCOMI和FCOMIP指令用ST0减目的操作数STi,按减法结果设置80X86的标志寄存器。不保存结果。注:这两条指令只能在PentiumPro以及其后的CPU上运行。2022/11/1252476543210CFPF
ZF15141312111098C2C0C32022/11/12525表8.1FPU条件码值浮点指令条件码代表条件C3C2C1C0FCOMI(ST0,STi)FCOMIP(ST0,STi)00110001XXXX0101ST0>STiST0<STiST0=STiST0或STi无
定义C3对应条件标志ZF,C0对应条件标志CF当C3=0即ZF=0,C0=0即CF=0时,表示ST0>STi当C3=0即ZF=0,C0=1即CF=1时,表示ST0<STi当C3=1即ZF=1,C0=0即CF=0时,表示ST0=ST
i2022/11/12526intel为了简化设计,用无符号数转移指令替代浮点数转移指令,所以在浮点比较指令FCOMI和FCOMIP之后,用无符号数条件转移指令构造控制结构:JA当ST0>STi转移,否则继续JA
E当ST0>=STi转移,否则继续JB当ST0<STi转移,否则继续JBE当ST0<=STi转移,否则继续JE当ST0==STi转移,否则继续JNE当ST0<>STi转移,否则继续2022/11/12
527st0>st1st0=st1A=BA<BA>BYNYN结束L1L2L3例29:比较两个浮点数的大小B入栈;A入栈2022/11/12528(浮点数比较例29低)fld(b);fld(a);fcomi(st0,st1);//a在st
0,b在st1jaL1;jeL2;stdout.put("a<b",nl);jmpL3;L1:stdout.put("a>b",nl);jmpL3;L2:stdout.put("a=b",nl);L3:……2022/11/125298.1.7常
数指令fldz();//0.0入栈fld1();//1.0入栈fldpi();//π入栈2022/11/12530例:计算圆面积……fld(r);fmul(r);fldpi();fmul();半径入栈r计算半径平方st0*r圆周率入栈π圆周率乘半径平方2022/11
/125318.1.9浮点数和整数运算指令fiadd(int_16_32);fisub(int_16_32);fidiv(int_16_32);fimul(int_16_32);这些指令把16或32位整型内存操作数转换成80位扩展精度浮点值,做减法或除法时,ST0作为被
减数或被除数。算术运算指令先弹出ST0内容,运算后再将结果压入ST0。2022/11/12532减法指令、除法指令的被减数和被除数fsub()、fsubp()fdiv()、fdivp()都是以st1为被减数或被除数。fsub(浮点变量
)、fisub(整型变量)fdiv(浮点变量)、fidiv(整型变量)都是以st0为被减数或被除数。2022/11/12533浮点数运算长度限制纯浮点四则运算时,浮点型变量长度限制为32位或64位;把整数装入浮点
栈或把浮点栈顶复制给整数变量时,整数变量限制为16位、32位或64位。当以整数为操作数和浮点数进行混合四则运算时,整数变量限制为16位或32位。2022/11/125348.2浮点表达式转换成指令序列8.2.1算术表达式转换成后缀表示
法8.2.2后缀表示转换成汇编语言指令2022/11/125358.2.1后缀表示法简单表达式包含两个操作数和一个运算符的表达式是简单表达式。把简单中缀表达式转换成后缀表达式只要将运算符从中间位置移到后缀位置(把运算符从两个操作数的中间移到第二个操作数的后边)。例如"5+6"变成"56+
"。2022/11/12536中缀表示法逆波兰表示法(RPN)5+656+7–272-x*yxy*a/bab/2022/11/12537复杂表达式包含两个及两个以上运算符的表达式是复杂表达式。转换时,从左到右先把复杂浮点表达式分解为若干简单的子表达式,将它们用后缀法表示,在余下的表达式里把每
个后缀法表示的子表达式作为一个操作数,继续转换,直到所有运算符全部用后缀法表示,转换结束。转换浮点表达式从最里层括号中的子表达式开始,按优先级、结合性逐层向外分解。2022/11/12538中缀表示法后缀表示法(x+y)*
2xy+2*x*2-(a+b)x2*ab+-(a+b)*(c+d)ab+cd+*例:2022/11/12539将(x+y)*2转换成后缀表达式(1)[xy+]*2(2)[xy+]2*(3)xy+2*中括号表示其中的内容是已经转换成的后缀表达式,它
又可以作为下一步转换中的一个操作数。转换结束时要擦去中括号。2022/11/12540将x*2-(a+b)转换成后缀表达式(1)[x2*]–(a+b)(2)[x2*]–[ab+](3)[x2*][ab+]–(4)x2*
ab+-2022/11/12541将(a+b)*(c+d)转换为后缀表达式(1)[ab+]*(c+d)(2)[ab+]*[cd+](3)[ab+][cd+]*(4)ab+cd+*2022/11/12542例39:把中缀表达式x=((y-z)*a)-
(a+b*c)/3.14159转换成后缀表达式。(一)把子表达式“(y-z)”转换成后缀表示法x=([yz-]*a)-(a+b*c)/3.14159为了和中缀编码分开,用中括号括住转换后的后缀编码。中括号中的内容“[yz-]”作为剩余表达式中的一个操作数。2022/1
1/12543(二)是把"([yz-]*a)"转换成后缀表示形式:x=[yz-a*]-(a+b*c)/3.14159(三)处理括号表达式“(a+b*c)”。因为乘法比加法的优先级高,先转换"b*c":x=[yz-a*]-(a+[bc*])/3.14159完成"
b*c"转换后,再完成括号表达式转换:x=[yz-a*]-[abc*+]/3.141592022/11/12544(四)剩下只有两个中缀运算符:减法和除法。按优先级先转换除法:x=[yz-a*]-[abc*+3.14159/](五
)转换减法:x=[yz-a*abc*+3.14159/–](六)去掉中括号,得到RPN表达式:x=yz-a*abc*+3.14159/-2022/11/12545例40:把中缀表达式a=(x*y-z+t)/2.0转换成后缀表
达式。(一):从括号内开始,先转换乘法:a=([xy*]-z+t)/2.0(二):仍在括号内,加法和减法有同等优先级,按结合顺序。先转换减法:a=([xy*z-]+t)/2.02022/11/12546(三):转换括号内的加法。转换后可以除去小括号:a=[xy*z-t+]/2.0(四):转换除法。
a=[xy*z-t+2.0/](五):除去中括号:a=xy*z-t+2.0/2022/11/125478.2.2指令表示法如你已经完成算术表达式转换成后缀表示法,再转换成汇编语言就特别容易。你所要做的是当你遇到操作数时发布FLD指令,当你遇到运算符时
发布相应的算术指令。2022/11/12548例41:将x=yz-a*abc*+3.14159/-表示为指令序列(1):y转换为FLD(y);(2):z转换为FLD(z);(3):-转换为FSUB();(4)
:a转换为FLD(a);(5):"*"转换为FMUL();(6)…从左到右逐步创建以下代码:2022/11/12549fld(y);fld(z);fsub();fld(a);fmul();fld(a);fld(b);fld(c);fmu
l();fadd();fldpi();//装入pi(3.14159)fdiv();fsub();fstp(x);//把结果存入x2022/11/12550例:计算一元二次方程的实数根分析:程序分为两部分。(一)判断输入的系数是否合理:a=
0b2-4ac<0(二)计算两个实数根要重新输入系数2022/11/12551ZF=1b入栈,st0*bst0a入栈,4*st0st0,c*st0st0st1–st0st0;st0<st1“a=0”“delt<0”yyL1L2L3n弹出st0即(delt)st0delt,
弹st0L4st0与st1比较①开始0入栈输入系数a,b,c;a入栈,st0与st1比较,弹出st0弹出st0即(0)2022/11/12552fldz();L1:stdout.put("输入三个系数:",nl,nl)stdin.get(a
,b,c);fld(a);fcomip(st0,st1);jeL2;fld(b);fmul(b);fld(a);fmul(4.0);fmul(c);fsub();//b*b–4*a*c2022/11/12553fcomi(st0,st1);//delt与0比较
jbL3;fstp(delt);fstp(st0);//弹出0jmpL4;L2:stdout.put("a=0",nl,nl);jmpL1;L3:stdout.put("判别式<0",n
l,nl);jmpL1;L4:2022/11/12554fld(b);fchs();fld(delt);fsqrt();fadd();fld(2.0);fld(a);fmul();fdiv();fstp(root1);L
4①②装入b求补装入delt求平方根做加法装入2.0装入a做乘法做除法保存第一个根2022/11/12555fld(b);fchs();fld(delt);fsqrt();fsub();fld(2.0);fld(a);fmul()
;fdiv();fstp(root2);②结束装入b求补装入delt求平方根做减法装入2.0装入a做乘法做除法保存第二个根2022/11/12556第九章指针类型和字符串9.1指针概念9.2指针常数9.3指针变量9.6字符串概念9.7字符串变量
9.8访问字符串中字符2022/11/125579.1指针概念在32位汇编语言里,指针是一个实际的具体的概念。汇编语言程序中操作数的偏移地址就是指针在前面的章节里,内存操作数寻址方式、对静态对象提取地址运算,提取内存对象的地址指令都隐含了指针
的概念。可以用32位的内存变量或寄存器存放偏移地址。2022/11/12558如果要用偏移地址访问内存,必须将指针装入一个32位的通用寄存器,然后用寄存器间接寻址方式访问内存。例:用指针访问32位静态变量a的值mov(&a,ebx);//指针装入EBXmo
v([ebx],eax);//将a的值复制给EAX2022/11/125599.2指针常数指针常量(这里常量的含义是在编译已有明确的值,在程序执行中其值不能改变。)格式如下:(1)&静态变量(2)&静态变量+常整数(3)&静态变量-常整数(4)0常整数的值
代表字节的个数。从物理上可以理解–号代表在变量之前;+号代表在变量之后。常数0代表NULL或NIL指针,对用户程序来说是一个非法的不存在的地址。2022/11/12560&a+0&a+8&a+16&a+24&a+4&a+12&a+20&a
+2807389562……例1:定义指针常数statica:int32:=0;int327,3,8,9,5,6,2;constpb:=&a+4*4;...如果要用指针访问内存单元,必须先将指针装入一个32位寄存器,再用寄存器间接寻址方式访问。2022/1
1/12561mov(pb,ebx);//必须用寄存器间接寻址mov([ebx],eax);//数据类型是一个双字stdout.put("pb所指单元的值=$",eax,nl);程序输出:pb所指单元的值=$092022/11/125629.3
指针变量汇编语言程序里除用32位通用寄存器外还可以用dword类型内存单元存放指针。称为指针变量。例1:staticb:byte;d:dword;p1:dword:=&b;p2:dword:=&d;2022/11/125639.6字符串概念字符串是一个ASC
II字符序列,不同的语言用不同的数据结构表示字符串。汇编语言的一个字串由四部分组成。第一部分用四个字节存放字串最大长度,第二部分用四个字节存放字串当前长度,第三部分是字符串序列,第四部分是一个内容为零的字节,代表字符串结束。当前长度和最大长度不包括结尾符。字符串最大长度字符串当前长度字符串字
符串结尾符2022/11/125649.7字符串变量字符串变量要先定义再使用。定义语法:static变量名:string;变量名按标识符取名。string是HLA的类型名。系统将为变量分配4个字节的空间,将来可以装入字符串的起始地址。注意是
第三部分的起始地址。字符串变量是指针变量,使用的第一步是对它赋予正确的地址。2022/11/12565有三种方法用合法地址对字串变量初始化:(1)静态初始化(2)用strallo过程(3)调用其他标准库函数2022/11/125
66(1)静态初始化在一个静态声明段里,可以用字符串常量对字串变量静态初始化。这样的字符串在程序执行中是不能修改的。这其实是用一个指针变量指向一个字符串常量。汇编语言在一个只读内存段里为这个字符串分配空间,字符串变量的值是这个字符串第一个字符的偏移地址。字符串变量的值是可以改变的。2022/1
1/12567例1:对字符串变量初始化staticstr1:string:="窗含西岭千秋雪,门泊东吴万里船。";...stdout.put(str1,nl);//字串变量为实参窗西岭千秋雪门,泊…万里船。str1100
1011…110把字符串的第一个字符的偏移地址赋予指针变量str12022/11/12568例2:访问字符串当前长度和最大长度programStr;#include("stdlib.hhf");staticlen:uns32;maxle
n:uns32;S1:string:="西塞山前白鹭飞,桃花流水鳜鱼肥。";2022/11/12569beginStr;mov(S1,ebx);//取字串的地址mov([ebx-4],len);//取当前长度mov([ebx-8],maxlen);//
取最大长度stdout.put(“S1=‘”,S1,“’”,nl,//用指针变量S1"S1的当前长度=",len,nl,"S1的最大长度=",maxlen,nl);endStr;2022/11/12570(2)用strallo过程操纵字符串的第
二种方法是在堆段里分配空间保存字符串数据。字符串变量是特殊的指针(字符串需要访问指针所指地址之前的八个字节),汇编语言标准库内存模块提供了为字符串预分配内存空间、释放字符串内存空间、输入字符串的函数。下面介绍这三个函数。2022/11/12571(1)为字符串预分配内存函数函数格式:stra
lloc(常数/变量/寄存器);函数的实参是双字类型,用它指定字符串的最大字符个数。这个值不包括字符串最大长度、字符串当前长度和字符串结尾符占用的空间。函数把参数指定的数加上9到13中某个数作为为字符串分配内存的字节数。函
数功能:函数在堆段中分配一个内存区,准备存放字符串,stralloc函数用参数值初始化最大长度,当前长度初始化为0,将一个0(结尾符)存放在字符串第一个字符的位置。然后将字符串起始地址作为函数返回值放入寄存器EAX中。2022/11/12572(2
)输入字符串函数函数格式:stdin.gets(字串变量);字串变量:用它装入在此之前调用stralloc函数分配内存后获得的指针。函数功能:将输入的字符串保存在字串变量所指的内存区,并填写当前字符串长度。如果用户输入的字符数超过最大字符
数限制,stdin.gets产生ex.StringOverflow异常。(3)释放字符串内存区函数函数格式:strfree(指针);指针:它是用stralloc函数分配内存时返回的内存区地址。函数功能:将指针所指的内存区
归还系统。2022/11/12573例:将一个汉字字符串颠倒后输出。分析:设计两个字符串,一个是源串,另一个是目的串。源串的指针先指向最后一个汉字,目的串的指针先指向第一个汉字的位置。1.从源串读一个汉字,将它写入目的串;2.分别
修改源串和目的串的指针;3.如果还未读完,返回1.4.输出颠倒后的字符串。2022/11/12574sp1:string:="美利坚合众国";sp2:string;……stralloc(12);mov(eax,sp2);mov(eax,edi);//放置目的字符串的指
针mov(12,(typedword[edi-4]));//设目的字串当前长度mov(12,(typedword[edi-8]));//设目的字串最大长度mov(sp1,eax);mov(12,ebx);//取原字符串的当前长度sub(2
,ebx);//计算最后一个字符的偏移2022/11/12575L1:mov([eax+ebx],cx);mov(cx,[edi]);add(2,edi);//修改目的串的指针sub(2,ebx);//修改源串的偏移cmp(e
bx,0);jgeL1;//当偏移值是负数时结束循环inc(edi);//修改目的串的指针mov(0,(typebyte[edi]));//填写字符串的结束符stdout.put("颠倒的汉字符串是:",sp2
,nl);strfree(sp2);2022/11/12576(3)调用其他标准库函数第三种方法是调用其他函数,函数初始化一个字串,并返回指向字串的指针。隐含分配内存功能的输入字符串函数stdin.a_gets();函数功能
:从输入缓冲区读入一行文本,在堆里动态分配内存,保存输入数据,函数返回值用寄存器EAX带回,是输入字符串的地址。2022/11/12577对于动态分配的内存,使用结束后,必须归还。归还使用释放字符串内存区函数。释放字符串内存区函数:strf
ree(字符串指针);函数的实参就是分配函数返回的指针。函数功能:释放由stdin.a_gets分配的内存。2022/11/12578例5:使用strfree和stdin.a_gets函数programstr2;#include("std
lib.hhf");staticSt1:string;beginstr2;stdout.put("输入一行字符:");stdin.flushInput();stdin.a_gets();mov(eax,St1);stdout.put("你输入的字符串是:",St1,nl);strfree(St1
);//释放内存endstr2;2022/11/125799.8访问字符串中字符访问字符串中的字符仅仅使用机器指令就可以了。根据指向字符串的指针和该字符在字符串中的偏移量,利用变址寻址方式就能完成剩下的工作。例如,s是英文字符串变量,现在要访问
字符串中的第四个字符。根据字符串组成结构,字符串变量的值是第一个字符的地址,第四个字符距它的偏移量是3,所以第四个字符的偏移地址是s的值加3。问题是用什2022/11/12580么寻址方式表示这个偏移地址?1)用寄存器间接寻址方式:mov(s,ebx);add(3,ebx);mov
([ebx],al);2)用变址寻址方式:mov(s,ebx);mov([ebx+3],al);abcde…偏移地址s01234偏移量s+32022/11/12581如果要遍历一个字符串,也是根据字符串的指针和字符的偏移量组
成字符的偏移地址,用某种寻址方式表示这个偏移地址,再用偏移地址访问字符串,每次访问一个字符,访问次数就是字符串的长度,所以采用循环结构,循环次数是字符串的长度。指针+偏移量指针0n-1n是字符串的当前长度2022/11/12
582例10:输入一个字符串,将其翻转输出。分析:现有的条件是,已知字符串的起始地址,字符串的长度。翻转输出就是从字符串的最后一个字符开始逐个输出字符,最后输出第一个字符。问题的关键是计算出最后一
个字符的地址,根据这个地址输出最后一个字符,然后修改地址输出它前面的字符……最后输出第一个字符2022/11/12583算法流程图输入一个字符串EAX取字符串的长度EBX计算最后一个字符的偏移EBX取一
个字符[EAX+EBX]c输出该字符c修改偏移EBX--EBX>=0结束YNL1开始2022/11/12584programHelloWorld;#include("stdlib.hhf")stati
cc:char;beginHelloWorld;stdout.put("输入一个字符串",nl);stdin.a_gets();2022/11/12585mov([eax-4],ebx);//取当前长度dec(
ebx);//计算最后一个字符的偏移L1:mov([eax+ebx],cl);mov(cl,c);stdout.put(c);dec(ebx);cmp(ebx,0);jgeL1;strfree(EAX);endHelloWorld;2
022/11/12586颠倒一个汉字字符串programHelloWorld;#include("stdlib.hhf")staticsp1:string:="美利坚合众国";sp2:string;beginHelloWorld;stdout.put("原来的汉字符串是:",sp
1,nl);2022/11/12587stralloc(12);//为目的字符串分配内存mov(eax,sp2);mov(eax,edi);//放置目的字符串的指针mov(12,(typedword[edi-4]));//设置目的字符串的当前长度mov(12
,(typedword[edi-8]));//设置目的字符串的最大长度mov(sp1,eax);mov([eax-4],ebx);//取原字符串的当前长度sub(2,ebx);//计算最后一个字符的偏移2022/11/12588L1:mov([eax+ebx],
cx);mov(cx,[edi]);add(2,edi);sub(2,ebx);cmp(ebx,0);jgeL1;inc(edi);mov(0,(typebyte[edi]));//填写字符串的结束符2022/11/1
2589stdout.put("颠倒的汉字符串是:");mov(sp2,ebx);l2:cmp((typechar[ebx]),0);jel3;stdout.put((typechar[ebx]));inc(ebx);//修改字符串指针jmpl
2;l3:stdout.put(nl,"击回车键继续:");strfree(sp2);stdin.readLn();2022/11/12590例:输入一个字符串,计算串中1的个数。分析:每个字符是由二进制代码组成,其中包含若干个1,统计一个字符串中1的个
数,就是对每个字符的1的个数求和。2022/11/12591开始准备循环,字符个数ESI计数器清零,0EDIESI=0YNL1L4统计一个字符中1的个数指针初值EAX修改ESI,移动指针L3结束2022/11/12592
统计一个字符中1的个数不是一条指令所能解决的,这句话还要细化。一个字节中包括的1的个数是随机的,可以将一个字节逐位移动,对移出的值进行判断,如是1就计数,否则不计数。这又是一个循环,循环次数是多少?最多循环8次,为了节省时间,可以这样考虑,只要字节中没有1,循环就结束,也就
是判断如果字节的内容是0,循环到此结束。2022/11/12593取一个字符BLBL=0YNBL左移一位CF=0计数器计数,EDI++L2L3YN2022/11/12594第十章数组和串10.1数组概念10.2数组常量10.3定义一维数组10.4访问
一维数组中的元素访问数组举例10.8串的概念10.9串指令操作环境10.10重复前缀2022/11/1259510.12复制串指令10.13比较串指令10.14搜索串指令10.15装入串指令10.16给串复制数据指令2022/11/1259610.1数组概念数组是同类数据在内
存里的顺序连续排列。一个数组的所有元素(数据)具有相同的数据类型,并且元素之间有先后顺序关系。2022/11/12597100001000210004100061000810010…………假定一个数组有六个元素,每个元素占两个字节,如果第一个元素的地址是10000,那么第二个元素的地址是
10002,所有元素按其顺序在内存中依次排列。怎样命名各个元素?假如按地址为元素命名,其实在编程时数组元素还没有地址;另一个方法是按顺序为元素编号。数组内存映像2022/11/12598每个元素按其先后顺序进行编号,这
个编号又称为数组元素下标,简称下标。每个元素具有元素值和元素的下标值两个属性。在汇编语言中关于数组的关键问题是下标和偏移地址之间的关系。2022/11/1259910000100021000410006100
0810010…………数组内存映像N+0N+1N+2N+3N+4N+5元素地址元素下标012345在高级程序设计语言中,下标封装了访问内存的细节,允许直接用下标访问数组元素。在汇编语言中,下标只是元素地址的索引,不能直接用下标访问数组元素。下标相邻的元素在内存中仍然相邻。356432598127
7766652022/11/1260010.2数组常量高级汇编语言数组常量是以方括号为边界的一组同类值的常量列表。语法格式(1):[以逗号分隔的常量列表]语法格式(2):ndup[以逗号分隔的常量列表]n是一个正整数常数。格式2表
示,数组常量包括n个方括号里的常数列表。2022/11/12601例1:高级汇编语言数组常量[1,2,3,4]//整数[2.0,3.14,1.0,0.5]//浮点数[‘a’,‘b’,‘c’,‘d’]//
字符6dup[3]//与[3,3,3,3,3,3,]等价2dup[1,5]//与[1,5,1,5]等价数组常量用于对数组变量赋值,它的类型要与数组变量的类型一致。2022/11/1260210.3定义一维数组变
量定义数组的格式:数组变量名:基类型[N]数组变量名:基类型[N]:=数组常量数组变量名:按照标识符取名,可以对数组名实施取地址运算,运算结果就是数组的起始地址。基类型:是数组元素的数据类型。可以是汇编语言中任何合法的数据类型。N:一个常数,说明该数组有N个元素。2022
/11/12603数组常量:数组常量的类型和元素个数要分别与基类型和N的值一致。在汇编时,汇编程序根据数组定义,为数组预留一片连续的数据区,这个数据区以“&数组名”作为起始地址,这个数据区的长度等于数组元素个数N乘以基类
型的字节数。注意汇编语言用“&数组名”表示数组起始地址。2022/11/12604staticBdata:byte[10];//字节数组Pdata:dword[4];//双字数组……Lea(Pdata,
eax);//数组首址装入EAXMov(&Bdata,ebx);//……装入EBX从例子中看到数组名与变量名的性质是一样的。在汇编语言中,数组名本身代表数组第一个元素值,而不是数组的起始地址。下面看两个数组在内存中的布局:2022/11/12605&Bdata+
9&Pdata+1298765432103210&Bdata+8&Bdata+7&Bdata+6&Bdata+5&Bdata+4&Bdata+3&Bdata+2&Bdata+1&Bdata+0&Pdata+8&Pdata+4&Pdata+0偏移地址下标数组在
内存占一片连续区域,汇编语言用下标作为偏移地址的索引。下标偏移地址数组内存映像2022/11/12606例3:在STATIC或READONLY段里,用数组常数对数组初始化staticR1:real32[6]:=[1
.0,2.0,3.0,4.0,5.0,6.0];d2:int32[8]:=[1,2,3,4,5,6,7,8];2022/11/12607例4:data:uns32[100]:=100dup[1];用一个包含100个1的数组常数,分别赋予数组的100个元素。例5:S:int32[16]:=4dup
[1,2,3,4];用一个数组常数,它的元素是将1,2,3,4重复4次获得的16个值,然后分别赋予数组S的16个元素。2022/11/12608&S+01234……4153210&S+60&S+12&S+8&S+4偏移地址元素下标……内存数组映像
2022/11/1260910.4访问一维数组中的元素一个数组占据一片连续的内存区。汇编语言只能按照地址访问数组元素。元素下标代表了元素在数组中的位置,它是元素地址的索引。这个索引在计算数组元素地址中具有关键作用。在汇编语言里,为了访问数组中的一个元素,需要一个将数组元素的下
标转换成访问内存的地址,这个转换关系又称地址映射函数。2022/11/12610一维数组的地址映射函数是:元素地址=基址+(下标–下标下界)*元素长度基址:是数组在内存中的起始地址。下标下界:是数组中第一个元素的下标值。如果是0,计算中可以省略。元素长度:元素长度代表一个元素占用的字节数。如果
元素类型是int32,元素长度就是4。下标:是要转换成地址的元素下标。2022/11/12611“(下标–下标下界)*元素长度”又称为偏移量。物理含义是“下标”指定的元素与第一个元素相隔的字节个数。对于下标下界是0的数组,地址映射函数也可以写成:&数组
名+下标*元素长度&数组名+偏移量2022/11/12612&S+051263344……48153210&S+?偏移地址……内存数组映像(3–0)*4&S+12假定要访问下标值是3的数组元素,程序员必须用指令和寻址方式表示地址映射函数。对于以0为
第一个下标的数组,下标3表示该元素之前有3个元素。3个元素占据的字节数就是3*4,3*4就是下标为3的元素距数组首址的偏移量2022/11/12613S:int32[16]:=4dup[1,2,3,4];为访问数组S中下标值是index的元素,计算偏移地址的公式是:元素偏移地址=&S+ind
ex*4汇编语言只能用指令和寻址方式表示地址映射函数。所以问题的关键是为数组元素找到某种合适的寻址方式,而寻址方式的多样性体现了汇编语言程序访问内存的灵活性。2022/11/12614(1)直接寻址方式数组名[偏移量]//寻址方式自动计算地址这里偏
移量必须是常数。如果已知一个数组元素的偏移量,就能把这个值写入直接寻址方式。假定数组的下标值从0开始,要读S数组的下标是4的元素值给寄存器EAX,根据偏移量的计算公式:下标*元素长度=4*4=16偏移量值:16直接寻址方式:S[
16]指令为:mov(S[16],eax);2022/11/12615访问数组时,常常要遍历数组,每访问一个元素后,又要修改地址,准备访问下一个元素。所以必须用能动态修改地址的寻址方式访问数组。为此可以采用寄存器间接寻址
、变址寻址、比例化变址寻址。2022/11/12616在“&S+index*4”公式中,“&S”是数组的基址,是常量,可变的是index*4这部分。其中只有index才变(index就是数组元素的下标)。为了能动态修改下标,必须将下
标放入寄存器。2022/11/12617(2)寄存器间接寻址方式[寄存器]为实现“&S+index*4”映射,就要把&S+index*4的值装入寄存器:lea(S,eax);//取数组的基址mov(ind
ex,ebx);//下标装入ebxshl(2,ebx);//计算index*4add(eax,ebx);//计算&s+index*4mov([ebx],eax);//eax放数组元素值2022/11/12618(3)变址寻址方式数组名[寄存器]为实现“&S+index*4”映射,寄存器的值应该
是index*4。可以先将index放入寄存器,然后把寄存器的值乘以4。mov(index,ebx);//index是下标变量shl(2,ebx);//左移2位,实现index*4mov(S[ebx],eax);2022/11/
12619(4)比例变址寻址方式数组名[变址寄存器*d]其中d是比例因子,可以取1、2、4、8中的某个值,寻址方式能自动地将下标值乘以比例因子作为偏移量,用比例变址寻址方式写出下面的代码:mov(index
,ebx);mov(S[ebx*4],eax);2022/11/12620注意如果不是乘以1、2、4、8的话,就不能用比例寻址方式。同样地,如果元素的长度值不是2的某次方,也不能用SHL移位指令,你必须用乘法指令做乘法。其实从数据类型的长度看,汇编语
言的整数数据类型通常用就是一个字节、二个字节、四个字节。所以可以理解变址寻址方式是专门为访问一维整数数组而设置的。2022/11/12621例:输出20到231的值首先分析怎样表示数据。这是一组整数,而整数之间又有先后
顺序,第一个整数是20,第二个整数是21,…,最后一个整数是231,用什么表示具有两个属性的数据?数组元素具有两个属性,元素值和下标值,自然用元素值表示2的整数次幂,用下标值表示整数之间的顺序。2021222323101233
1……2022/11/12622每个数组元素需要多少字节?即数组元素用什么数据类型?根据题目,先找到最大的数,由最大的数确定数组元素的数据类型。最大的数是231,所以数组元素的数据类型是无符号32位整数。数组具有32个元素。定义数组:data:uns32[32]确定了数据结构后,再分析解决
这个问题的算法。2022/11/12623计算每个元素的值输出每个元素的值开始结束2022/11/12624怎样计算数组元素值?第一个元素值是20,第二个元素值是21,第三个元素值是22,…,不难看出第二个元素值是第一个元素值乘以2,第三个元素值是第二个元素值乘以2,…,这是一
个递推算法。递推算法要用循环结构,循环条件分析,循环次数就是数组元素的个数。元素是内存单元,访问元素必须用地址,用什么寻址方式表示元素的地址?因为元素的长度是4个字节,可以用比例变址寻址方式:data[reg32*4]reg32代表某个32位通用寄存器。2022/11/12
625开始设第一个元素值:1data设下标初值:1ebxdata[ebx*4-4]eaxeaxdata[ebx*4]eax左移一位修改ebx,ebx++ebx<32ynL12022/11/12626mov(1,
data);mov(1,ebx);L1:mov(data[ebx*4-4],eax);shl(1,eax);mov(eax,data[ebx*4]);inc(ebx);cmp(ebx,32);jbL1;2022/11/12627循环初始化:0
ebx输出data[ebx*4]修改ebx,ebx++ebx<32ynL2计算出每个数组元素的值后,就可以按顺序输出。输出算法仍是循环结构。结束2022/11/12628根据流程图写出的程序段如下:mov(0,ebx);L2:stdout.put(data[e
bx*4]:10,nl);inc(ebx);cmp(ebx,32);jbL2;2022/11/12629数组排序举例选择法排升序冒泡法排升序2022/11/12630012341415前元素后元素前后前后...前后[0][1][1][2][2][3]...[14][15][2][3][4]………
[15][15][15]经过若干轮比较,完成数组排序。通过分析每轮第一个前元素的下标,可知要经过15轮。下标可以用变量表示,这个变量又是循环变量,因寻址方式的限制(没有变量间接寻址),必须用寄存器,假定用esi表示前元素的下标,用edi表示后元素的下标。下标必须与循环变量结合起来;下标
必须与寻址方式结合起来。2022/11/12631分析寻址方式:数组名是data,数组元素长度是4数组元素下标数组元素的地址0&data+0*41&data+1*4……k&data+k*4地址映射函数是&data+k*4,k是数组元素的下
标,将k装入寄存器,用比例变址寻址方式data[寄存器*4]访问数组元素。2022/11/12632通过分析后,采用比例变址寻址方式,用esi装前元素下标,用edi装后元素的下标。前元素表示为:data[esi*
4]后元素表示为:data[edi*4]从比较算法分析可知esi的初值是0。算法是循环结构,循环次数由esi的值决定,循环条件是esi<15。流程图如下:2022/11/12633循环初始化:0esi进行一轮比较修改前元素下
标esi++esi<15结束L1yn选择法排序初步流程图2022/11/12634一轮比较又包括多次前后元素的比较,在每轮比较里,前元素的下标(地址)是不变的,只是后元素的下标变化。后元素下标变化规律分析:每轮比较中,参加比较的第一个后元素下标值是不一样的,而最后比较
的后元素下标值是15,每轮第一个后元素下标值总比这轮的前元素的下标值大1,在每轮比较前,根据前元素下标值计算后元素下标的初值。由于不能用两个变量比较,在每轮比较前将前元素装入一个寄存器。进行一轮比较的流程图如下:2022/11/12635取前元素下标esiedi取前元素的
值eax计算后元素下标初值edi++eax与data[edi*4]比较eax<=后元素修改当前最小值:eax与data[edi*4]交换①L2L3yynn修改后元素的下标edi++edi<16每轮比较前进行循环初始化进行一次比较2
022/11/12636eax=data[esi*4]①本轮最小元素装入前元素位置eaxdata[esi*4]ynL4每轮比较结束后,将这一轮的最小元素值放入前元素。2022/11/12637mov(0,esi);L1:mov(esi,edi);mov(data[esi*4],eax
);//前元素给eaxinc(edi);//计算出后元素的下标L2:cmp(eax,data[edi*4]);jleL3;xchg(eax,data[edi*4]);//eax和后元素交换L3:inc(edi);//修改后元素下标2022/11/12638cmp(edi,16);jbL
2;//后元素下标如小于16,继续比较cmp(eax,data[esi*4]);//eax与前元素比较jeL4;mov(eax,data[esi*4]);//与前元素交换L4:inc(esi);//修改前元素下标cmp(esi,15);jbL1;...2022/11/12639为了区别排序前后的
数组,也是为了检验排序算法是否正确,在程序设计时,在排序前输出数组(每行4个元素),在排序后,同样输出数组(每行4个元素)。下面是输出数组元素的算法流程图和程序段。2022/11/12640循环初始化0eax;0ecx输出数组元素data[eax*4]:5计数器计数ecx+
+ecx<>4光标换行,计数器清零ecx=0修改下标eax++eax<16yynnL5L6每行输出4个数组元素的流程图2022/11/12641mov(0,ecx);//每行输出4个元素mov(0,eax);//输出排序前的
数组L5:stdout.put(data[eax*4]:5);inc(ecx);cmp(ecx,4);jneL6;stdout.put(nl);//输出4个元素后光标换行mov(0,ecx);//计数器清零L6:inc(eax);//修改元素的下标cmp(eax,16);jbL5
;2022/11/12642冒泡法排序题目要求与前面的选择法排序相同。算法分析:前元素后元素[0][1][1][2][2][3].........[14][15]重复若干轮,如果某一轮没有发生交换,说明16个数据已排好序2022/11/12643循环条件分析:如果一轮比较后
发生了交换,就要再进行一轮比较,设一个标志变量,每轮开始比较前,将它设置为1,如果发生交换,把它修改为0。每轮比较后判断标志变量决定是否继续循环。因为前元素和后元素都是内存操作数,不能直接进行比较,所以要将一个操作数
装入寄存器。2022/11/12644循环初始化:1bool0esi进行一轮比较bool=0L1yn2022/11/12645每轮比较包括若干次前、后元素的比较。这又是循环,每轮都从第一个元素开始[0]与[1]比较,[1]
和[2]比较…[14]和[15]比较,根据比较结果决定是否交换。这里循环控制必须与下标结合起来,下标必须与寻址方式结合起来。前元素寻址方式:data[esi*4]后元素寻址方式:data[esi*4+4]2022/11/12646循环初始化:1bool;
0esi取前元素data[esi*4]eaxeax与后元素data[esi*4+4]比较eax<=后元素eax与后元素交换eax装入前元素修改标志0bool修改前元素下标esi++esi<15L2L3ynyn2022/11/12647L1:mov
(1,bool);mov(0,esi);L2:mov(data[esi*4],eax);cmp(eax,data[esi*4+4]);jleL3;xchg(eax,data[esi*4+4]);mov(eax,dat
a[esi*4]);mov(0,bool);2022/11/12648L3:inc(esi);cmp(esi,15);jbL2;cmp(bool,0);jeL1;...2022/11/1264910.8串的概念
串是字节、字或双字类型数组,80X86处理器家族提供了专门处理串的指令。80X86CPU能处理三种串,字节串,字串,双字串。可以遍历串,比较串,在串中搜索一个指定的值,用一个值初始化一个串,或在串上做其他的原始操作。202
2/11/1265010.9串指令操作环境串指令在内存块(连续线性数组)上操作。串指令需要先设置初始条件(源数据串的起始地址,目的串的起始地址,数据的个数,地址修改方向),然后选择恰当的重复前缀,就可以用一条指令实现循环操作。2022/
11/12651ESI存放源数据串的地址EDI存放目的串的地址ECX存放数据个数DF条件标志。DF=0,每处理一个数据,ESI,EDI自动做增量修改。DF=1,每处理一个数据,ESI,EDI自动做减量修改。2022/11/12652标志寄存
器中的方向标志DF控制串指令修改源和目的串地址的方向。80X86提供了修改方向标志的指令。CLD将DF清零。STD将DF置1。在使用重复前缀时,要先设置方向标志,才能正确修改源、目的串的地址。2022/11/1265
310.10重复前缀◼指令格式:重复前缀.串指令◼学习汇编语言重复前缀的关键是掌握带重复前缀串指令的执行步骤,它包含了循环结构,特别是有条件重复前缀的循环条件是两个子条件的逻辑乘,只要一个子条件不符合就停止重复。在带有条件重复前缀的
串指令后必须判断是哪个子条件导致停止重复。汇编语言的重复前缀有三种,下面分别介绍它们的功能:2022/11/12654(1)无条件重复前缀REP◼REP.串指令⚫(1)先判断ECX,如不等于0去(2);否则去(4);⚫(2)执行一次串指令,自动修改指针;⚫(3)ECX自动
减一,去(1);⚫(4)执行串指令后面的第一条指令。◼能否执行串指令,ECX的值是关键。2022/11/12655(2)相等就重复前缀REPE◼REPE.串指令◼(1)先判断ECX的值,如不等于0去(2);否则去(4);◼(2)执行一次串指令,自动
修改指针;◼(3)ECX的值自动减一,然后判断ZF的值,如等于1去(1);否则去(4);◼(4)执行串指令后面的第一条指令。2022/11/12656(3)不相等就重复前缀REPNE◼REPNE.串指令◼(1)先判断ECX的值,
如不等于0去(2);否则去(4);◼(2)执行一次串指令,自动修改指针;◼(3)ECX的值自动减一,然后判断ZF的值,如等于0去(1);否则去(4);◼(4)执行串指令后面的第一条指令。2022/11/1265710.12MOVS指令复制指令指令格式:movsb()movsw()movsd()
指令中的B,W,D分别代表字节、字、双字。指令功能:不带重复前缀的指令,将源数据串的一个数据复制给目的串。当DF=0,ESI和EDI分别做增量修改;当DF=1,ESI和EDI分别做减量修改。带重复前缀的指令,指令将重复执行N次,这个N放在寄存器ECX中20
22/11/12658例1:复制一个有200个字节的数组Array1:byte[200];Array2:byte[200];…cld();lea(esi,Array1);lea(edi,Array2);mov(200,ecx);rep.movsb();2022/
11/1265910.13CMPS指令比较指令指令格式:cmpsb();cmpsw();cmpsd();指令功能:(1)不带重复前缀:执行[ESI]-[EDI],按运算结果设置条件标志,不保存结果,按D
F修改ESI和EDI指针2022/11/12660(2)带REPE重复前缀:如果ECX<>0,且运算结果使ZF=1,继续比较;否则执行比较指令后的下一条指令。(3)带REPNE重复前缀:如果ECX<>0,且运算结果使
ZF=0,继续比较;否则执行比较指令后的下一条指令。2022/11/12661例3:从键盘输入两个字符串,分别用str1和str2表示,比较两个字符串,判断是否相等。分析:两个字符串相等的话,首先它们的长度相等,长度如
不等,字符串一定不相等。如果长度相等,再判断对应位置的每个字符是否相等。2022/11/12662输入一个字符串,取字符串长度[eax–4]ebx保存字符串指针,eaxstr1输入一个字符串,取字符串长度[eax–4]ecx保存字符串指针,eaxstr2e
bx<>ecx设置修改方向:0DF设置源地址:str1→esi设置目的地址:str2→ediynrepe.cmpsbL1①②开始2022/11/12663ZF<>1yn源数据串=目的数据串源数据串<>目的数据串①L2
释放源指针:str1释放目的指针:str2结束②2022/11/12664stdout.put(“输入一个字符串”,nl);stdin.a_gets();mov(eax,str1);mov([eax–4],ebx);//st
r1的长度stdout.put(“输入一个符字串”,nl);stdin.a_gets();mov(eax,str2);mov([eax–4],ecx);//str2的长度2022/11/12665cmp(ecx,ebx);jn
eL1;//不等就去L1cld();mov(str1,esi);mov(str2,edi);repe.cmpsb();jneL1;//不等就去L1stdout.put("两个字串相等",nl);jmpL2;2022/11/12666L1:stdout.put("两个字串不相等",nl);L2:s
trfree(str1);strfree(str2);2022/11/12667注意,在带REPE前缀的串比较指令执行结束后,有两种可能,一是比较成功,二是比较失败,怎样判断成功与失败?串比较指令每次比较后,按比较结果设置条件标志,如ZF=1,比较成功,否则,比较失败。所
以用条件转移指令je或jne控制是否转移。2022/11/1266810.14SCAS指令搜索指令格式如下:scasb()scasw()scasd()指令功能:(1)不带重复前缀的:搜索指令的一个操作数必须放
在寄存器AL、AX、EAX中某一个,数据串的起始地址必须放在EDI中。指令执行AL-[EDI]或AX-[EDI]或EAX-[EDI],按比较结果设置条件标志,不保存结果,按DF修改字串的地址EDI。2022/11/12669(2)带REP
E重复前缀的:如ECX<>0,且ZF=1时,重复搜索;否则执行串指令后的第一条指令。(3)带REPNE重复前缀的:如ECX<>0,且ZF=0时,重复搜索;否则执行串指令后的第一条指令。2022/11/12670例4:在字串str1中搜索子串
“AB”出现的次数。已知字串长度为N。设一个计数器EBX,初值为0,字符‘A’放在AL中。mov(str1,edi);mov(N,ecx);mov(0,ebx);//计数器初化cld();mov(‘A’,al);2022/11/12671l1:repne
.scasb();jnel3;cmp((typebyte[edi]),’B’);jnel2;inc(ebx);//计数器加1l2:cmp(ecx,0);jnel1;l3:…2022/11/1267210.15LODS指令指令语法格式如下:lodsb();lodsw(
);lodsd();指令功能:将串数据[ESI]复制给寄存器AL、AX、EAX中某一个。按DF的值修改ESI。指令不影响条件标志。2022/11/1267310.16STOS指令给目的串复制一个数据的指令格式:stosb(
);stosw();stosd();数据必须放在寄存器AL、AX、EAX中某一个。指令功能:2022/11/12674(1)不带重复前缀的:将数据复制给[EDI],按DF值修改EDI。指令不影响条件标志。
(2)带REP重复前缀的:把复制操作重复N次,N放在寄存器ECX中。2022/11/12675例5:以array1为起始地址的双字存储区中有N个有符号数,请将它们中的正数和负数分别放在PLUS和MIN为起始地址的存储区。分析:本题有
一个源数据区,两个目的数据区。需要三个指针寄存器,ESI,EDI,EBX。为了利用串指令,用ESI指向源数据区,EDI指向PLUS数据区,EBX指向MIN数据区。处理步骤为:2022/11/12676(1)从源数据区取一个数据给
EAX(2)判断EAX的正负(3)如果是正数,EAX的值复制给[EDI];如果是负数,EAX的值复制给[EBX],修改EBX。(4)判断如果数据处理完,去(5);否则返回(1)。(5)分别输出三个数组。202
2/11/12677源地址ESI;非负数地址EDI;负数地址EBX;数据个数ECX;从源串取一个数据[ESI]EAX;修改源地址ESI+4ESI;设置修改地址方向0→DFEAX[EDI];修改地址EDI+4计数EAX[EBX];计数修改地址EBX+4ECX-1<>0L1L2L3YY
EAX<02022/11/12678串首址ESI;串长度ECX取数据EAX;输出数据EAX;修改数据地址ESI+4;修改计数器ECX--;ECX<>0YLx输出数据流程图2022/11/12679mov(N,ecx);lea(esi,array1);lea(edi,plus);lea(
ebx,min);cld();l1:lodsd();//从源串复制一个数据给EAXcmp(eax,0);jll2;//如是负数转移到L22022/11/12680stosd();//保存正数inc(X1);//正数计数jmpl3;l2:mov(ea
x,[ebx]);//保存负数add(4,ebx);//修改负数区地址inc(X2);//负数计数l3:loopl1;mov(N,ecx);//输出源数据2022/11/12681lea(esi,array1);l4:lodsd();stdout.put((typeint32eax):5);l
oopl4;stdout.put(nl);mov(X1,ecx);//输出>=0的数据lea(esi,plus);l5:lodsd();2022/11/12682stdout.put((typeint32eax):5);loopl5;stdout.put(nl);mov(X2,ecx);
//输出<0的数据lea(esi,min);l6:lodsd();stdout.put((typeint32eax):5);loopl6;stdout.put(nl);2022/11/12683第十二章过程在程序设计中,有类程序不能独立运行,它们需要被另一个程序调用后才能运行
,汇编语言称这类程序为过程,称调用过程的程序为调用程序。在此,过程、函数、子程序的语义不予区分。本章主要介绍汇编语言过程的概念、过程的定义及应用。2022/11/12684过程的概念主要包含了过程中各个对象间的关
系。过程调用与堆栈的关系;过程返回与堆栈的关系;参数和参数传递与堆栈的关系;局部变量与堆栈的关系;寄存器保护与堆栈的关系;堆栈是所有这些关系的核心,简而言之,本章的内容就是阐述汇编语言中以堆栈结构为核心的过程调用、参数传递、过程返回的机制。20
22/11/1268512.1过程机制12.2过程定义12.3在过程内访问参数12.4全局变量和局部变量12.5保护机器的状态12.6过程结束清除堆栈12.7过程中的进栈出栈操作12.8过程中的标号2022/11/1268612.1过程机制call和ret指令提供了过程机制。调用程序用cal
l指令调用过程,过程用ret指令返回调用程序。2022/11/1268712.1.1调用过程指令•调用过程指令格式:•call过程名;•指令功能:•①首先修改栈顶指针esp=esp–4;•②再将CALL指令后第一条指令的地址压入堆栈,这个地址又称为返回地址(断点
)。•③通过操作系统把过程的地址装入指令计数器EIP,CPU执行call指令后,立即执行过程的第一条指令。返回地址2022/11/12688call指令工作原理Call指令后第一条指令地址ESPESP1000996Call指令后第一条指令地址过程第一条指令地址EIP图12.1执行call指令
开始过程调用前的堆栈内容从内存取走call指令call指令执行结束堆栈call指令执行结束,CPU立即执行过程的第一条指令。Call指令的偏移地址取call指令之前2022/11/12689(1)调用无参数的过程如果调用过程时,不给过程传递参数称为调
用无参数过程。调用无参数的过程是最简单的,只要在调用程序中用一条调用指令:call过程名;根据call指令功能,这条指令执行后,就立即执行过程的第一条指令。2022/11/12690◼根据call指令功能,call指令被执行后立即转入过程执行,调用程序要给过程传递参数,显然必须在c
all指令之前完成。◼汇编语言主要用堆栈传递参数。本课程只介绍使用堆栈作为传递参数的载体。◼在调用有参数过程时,调用程序在call指令之前必须将实参值或实参的地址压入堆栈。(2)调用有参数的过程2022/11/12691◼向堆栈压入参数时有两点要注意:
◼(1)由于高级汇编语言要求每个参数的长度是四个字节,所以要求堆栈内实参拷贝的长度之和必须是四的倍数。◼(2)如果在过程里不用形参名访问形参,压入实参的顺序由你自己决定;否则要按照参数在形参表中的位置从左到右把实
参压入堆栈。2022/11/126921.传递实参值◼假定要传递n个32位的实参值,在call指令前先将实参值逐个压入堆栈,最后写出call指令。◼push(实参1);◼……◼push(实参n);◼CAL
L过程名;◼常数、寄存器、变量实参可以直接利用压栈指令将实参值压入堆栈;对于表达式要先计算出表达式的值,然后将该值压入堆栈。2022/11/12693ESP1000实参1的值①push(实参1);ESP996②p
ush(实参2);③push(实参3);实参2的值实参3的值992ESPESP988④push(实参4);实参4的值ESP984图12.2⑤push(实参n);实参n的值1000-4*nESP⑥call过程名返回地址……1000-4*n-4ESP2022/11/12694◼如果要在过程里修改实参,
就必须向过程传递实参的地址,过程里用实参的地址访问实参。传递指针的方式与实参有关:➢(1)实参是静态变量x,可以用一条指令传递它的地址:➢pushd(&x);➢(2)实参是指针变量p所指对象,也用一条指令传递实参的地址:➢push(p);//把地址(p的值)压入堆栈2.传递实参的地址(传
递指针)2022/11/12695➢(3)实参是除静态变量外的其他内存寻址方式,要用两条指令传递它的地址:➢lea(内存寻址方式,EAX);➢push(EAX);➢向堆栈复制地址和复制数据的操作是一模一样的。2022/11/12696◼ret([
N]);◼N:是一个常数,它代表参数字节长度之和,在本课程里它必须是四的倍数。如调用程序未给过程传递参数,则省略N。◼指令功能:◼①先将[esp]所指单元的内容复制给指令计数器EIP;◼②然后修改esp,es
p=esp+4;◼③如有N,再修改esp,esp=esp+N。◼如果程序正确,①中弹给指令计数器EIP的值,就是call指令压入堆栈的返回地址。ret指令结束后,CPU立即执行返回地址所指的指令。12.1.2过程返回指令2022
/11/12697ret指令工作原理返回地址ESPESP1000996返回地址EIPXX指令地址图12.3执行ret指令开始ret指令结束ret指令结束后,立即执行返回地址所指的指令。2022/11/12698ret(N)指令工作原理返回地址E
SPESP988984返回地址EIPXX指令地址图12.4执行ret(N)指令开始过程调用前的堆栈内容给过程传递的三个实参值1000ESPN的值是3*4ret指令结束后,堆栈恢复到调用过程前的状态;CPU立即执行返回地址所指的指令。2022/11/12699◼
CALL和RET两条指令都要根据栈顶指针ESP进行压栈和弹栈操作,都隐含了修改栈顶指针的操作。◼调用程序用CALL指令将返回地址压入堆栈,过程用RET指令把返回地址弹回EIP寄存器。返回地址是连接调用程序和过程的关键纽带。2022/
11/1270012.2过程定义使用过程必须先定义,然后再调用。过程定义的位置在主程序的声明变量的区域。2022/11/12701(一)过程的框架结构procedure过程名;@noframe;局部静态变量定义be
gin过程名;push(ebp);mov(esp,ebp);为局部自动变量分配内存保护寄存器指令序列…恢复寄存器mov(ebp,esp);pop(ebp);ret([N]);end过程名;2022/11/12702procedure:是保留字,
用来区分program。过程名:按标识符取名。标识符要放在过程的保留字procedure、begin和end后面。关于局部静态变量定义在后面的小节中专门介绍。@noframe:短语的含义是让程序员在过程为普通指令访问堆栈建立指针EBP,为局部动变量分配堆栈空间、过程结束时撤销指针BP、回收局部
自动变量占用的空间,用RET指令返回调用程序回收参数占用的空间。2022/11/12703例1:定义一个无参过程,输出20个星号。procedureasterisk;@noframe;beginasterisk;push(ebp);//保护寄存器EBPmov(esp,ebp
);//建立基址指针EBPpush(ecx);//保护寄存器2022/11/12704mov(0,ecx);//计数器初始化L1:stdout.put("*");inc(ecx);cmp(ecx,20);jbL1;stdou
t.put(nl);pop(ecx);//恢复寄存器mov(ebp,esp);pop(ebp);ret();//过程返回endasterisk;2022/11/12705例2:调用输出20个星号的过程(
无参数过程)programasterisks;#include("stdlib.hhf");procedureasterisk;@noframe;beginasterisk;//无局部变量push(ebp);//②mov(esp,ebp);//③push(ec
x);//④保护寄存器mov(0,ecx);L1:stdout.put("*");inc(ecx);cmp(ecx,20);jbL1;2022/11/12706stdout.put(nl);pop(ecx);//㈠恢复寄存器mov(ebp,esp);//㈡pop(ebp);//㈢r
et();//㈣endasterisk;beginasterisks;mov(0,eax);L2:callasterisk;//①调用过程inc(eax);cmp(eax,10);jbL2;endasterisks;2022/11/12707mov(0,eax);L
2:callasterisk;//①inc(eax);cmp(eax,10);jbL2;push(ebp);//②mov(esp,ebp);//③push(ecx);//④mov(0,ecx);L1:stdout.put("*");inc(ecx);cmp(ecx,20);jbL1;st
dout.put(nl);pop(ecx);//㈣mov(ebp,esp);//㈢pop(ebp);//㈡ret();//㈠调用程序过程图12.52022/11/12708ESPESPESPESP……调用程序、过程都要修改堆
栈返回地址EBP旧值ECX旧值1000996992988①callasterisk②push(ebp)④push(ecx)ebp③mov(esp,ebp)调用程序调用过程时堆栈的变化图12.62022/11/12709ESPESPESPESP……
过程自己清除堆栈返回地址EBP旧值ECX旧值1000996992988④ret③pop(ebp)①pop(ecx)ebp②mov(ebp,esp)过程返回调用程序时堆栈的变化图12.72022/11/12710(二)从过程带回结果汇编语言过程定义
语法没有带回函数结果的定义,所以程序员要自己处理过程如何带回结果的问题。通用寄存器是常用的带回函数结果的载体之一。如用寄存器带回指针,必须用32位的通用寄存器。对于浮点型的结果,可以用浮点寄存器带回结果。2022/11/1271112.3在过程内访问
参数在12.1节介绍了调用程序用压栈指令逐个将实参值(或偏移地址)复制到堆栈。然后执行call指令,随即就执行过程的第一条指令。那么在过程中怎样访问参数?显然是不能用push和pop指令,因为它们总是对栈顶操
作而参数又不在栈顶。2022/11/1271212.3.1访问堆栈中的值形参80X86系列为此专门提供了一个基址寄存器EBP,通过EBP和一个偏移量相加产生访问堆栈单元的偏移地址。所以过程首先要建立访问堆栈的基址指针。push(ebp)
;mov(esp,ebp);过程开始的二条指令,正是用来建立访问堆栈的基址指针。例:过程exam有三个32位参数,调用时必须先将三个参数值逐个压入堆栈,然后执行call指令进入过程,过程里首先建立基址指针,建立基址指针后堆栈布局如图所示:2022/11/12713ESP10
00实参a的值①push(a);ESP996②push(b);③push(c);实参b的值实参c的值992ESPESP988④callexam返回地址ESP984ESP980EBP旧值⑤push(ebp);EBP⑥mov(esp,ebp);图12.8访问实参c值的偏移地址[ebp+8
]访问实参b值的偏移地址[ebp+12]访问实参a值的偏移地址[ebp+16][ebp+偏移量]正是过程访问形参的寻址方式2022/11/12714例3:过程利用基址访问参数programParameters;//(过程用传递结构基址访问参数)#incl
ude("stdlib.hhf")procedureadd2;@noframe;beginadd2;push(ebp);//③mov(esp,ebp);//④push(eax);//⑤mov([EBP+8],eax);//访问形参add(2,eax);2
022/11/12715stdout.put((typeuns32[ebp+8]),"+2=",(typeuns32eax),nl);pop(eax);//㈠mov(ebp,esp);//㈡pop(ebp);//㈢ret(4);
//㈣endadd2;2022/11/12716beginParameters;pushd(10);//①calladd2;//②pushd(135);//①calladd2;//②endParameters;程序执行结果:10+2=12135+2
=1372022/11/12717ESP100010①pushd(10);ESP996②calladd2③push(ebp)返回地址EBP旧值992ESPESP988④mov(esp,ebp);EAX旧值ESP984⑤push(eax);EBPEBP+8图12.92022/11/12718ES
P100010ESP996④ret(4)返回地址ESP992ESP988EBP旧值EAX旧值ESP984③pop(ebp);①pop(eax);②mov(ebp,esp);EBP图12.102022/11/1271912.
3.2访问堆栈中的指针形参◼如果调用程序传递的是实参的地址,过程访问堆栈得到的是一个地址。◼与访问值形参相似,以EBP作为基址,加一个正偏移量d,用变址寻址方式[EBP+d]访问堆栈,而堆栈中的内容是实参的地址,为访问实参,必须将这个地址装入一个
32位通用寄存器,然后用寄存器间接寻址方式访问实参。2022/11/12720例4:访问引用参数举例programAccess;#include("stdlib.hhf")procedureadd2;@noframe;beginadd2;push(ebp);//③mov(
esp,ebp);//④push(eax);//⑤mov([ebp+8],eax);//实参的地址给EAXadd(2,(typeuns32[eax]));//实参值自加2mov([eax],eax);//实参值装入EAX2022/11/12721stdout.put((typeuns32eax)
,nl);pop(eax);//㈠mov(ebp,esp);//㈡pop(ebp);//㈢ret(4);//㈣endadd2;2022/11/12722staticn1:uns32:=10;n2:uns32:=15;beginAccess;stdout.put("n1=",n1
,nl,"n2=",n2,nl);pushd(&n1);//①calladd2;//②pushd(&n2);//①calladd2;//②stdout.put("n1=",n1,nl,"n2=",n2,nl);endAccess;2022/11/12723ad
d2过程有一个引用形参“a”,引用形参是指针变量,它指向实参对象(“n1”、“n2”),为了访问“a”指向的实参,在代码里必须把指针装入32位的寄存器,然后用寄存器间接寻址方式访问数据。"mov([ebp+8],eax)"指令就是把实参地址装入寄存器EAX,然后用"[
EAX]"寻址方式访问实参。把实参值自加2,过程修改了实参。可以理解为过程通过引用形参给调用程序返回了结果,这正是使用引用形参的目的。2022/11/12724ESP1000n1的地址①push(&n1)
;ESP996②calladd2③push(ebp)返回地址EBP旧值992ESPESP988④mov(esp,ebp);EAX旧值ESP984⑤push(eax);EBPEBP+8图12.112022/11/12725ESP1000n1的地址ESP996④ret(4)返回地址
ESP992ESP988EBP旧值EAX旧值ESP984③pop(ebp);①pop(eax);②mov(ebp,esp);EBP图12.122022/11/12726ESP1000n2的地址①push(&n2);ESP996②calladd2③push(eb
p)返回地址EBP旧值992ESPESP988④mov(esp,ebp);EAX旧值ESP984⑤push(eax);EBPEBP+8图12.132022/11/12727ESP1000n2的地址ES
P996④ret(4)返回地址ESP992ESP988EBP旧值EAX旧值ESP984③pop(ebp);①pop(eax);②mov(ebp,esp);EBP图12.142022/11/1272812.4全局变量和局部变量◼在program
中定义的变量为全局变量。◼在一个过程内定义的变量为局部变量。◼局部变量又分静态变量static和自动变量两种。局部自动变量是在堆栈段中分配内存空间,局部静态变量在过程的静态段中分配内存空间,不同的段使变量具备不同的特性。2022/11/1272912.4.1定义局部变量◼在定义局部静态变量时,在
过程的声明区必须用static指定段名,用标识符为局部静态变量取名,与在主程序里定义静态变量方法没有两样,然后在过程里用变量名访问局部静态变量。◼过程里如使用局部自动变量,要求在建立堆栈基址指针EBP后,紧接着为局部自动变量在堆栈段中分配内存空间。然后是保护寄
存器。2022/11/12730例5:过程procedureexam中有三个形参,三个32位局部自动变量。假设调用过程时三个形参对应的实参分别是a、b、c。调用程序要做:push(a);//①复制a的值push(b);//②复制b的值push(c);//③复制c的值
callexam;//④调用过程2022/11/12731ESP1000实参a的值①push(a);ESP996②push(b);③push(c);实参b的值实参c的值992ESPESP988④callexam返回地址ESP984图12.152022/11/12732procedureex
am;@noframe;beginexam;push(ebp);//⑤保护寄存器EBPmov(esp,ebp);//⑥建立基址指针EBPsub(12,esp);//⑦为局部变量分配空间and($FFFF_FFFC,esp);//⑦强制按双字对齐p
ush(eax);//⑧保护寄存器…ret(12);endexam;2022/11/12733在过程开始,首先把EBP的值压入堆栈,然后把ESP的值复制给EBP。接着为局部变量在堆栈里分配内存空间:push(ebp);mov(es
p,ebp);sub(12,esp);//为局部变量分配空间and($FFFF_FFFC,esp);因为ESP寄存器用双字对齐,and指令目的是将ESP的值调整为4的倍数。C的二进制的值是%1100,这时堆栈状态如图12.16。2022/11/127
34实参a的值①push(a);②push(b);③push(c);实参b的值实参c的值④callexam返回地址ESP984ESP980EBP旧值局部自动变量区ESP968⑤push(ebp);⑦sub(12,esp);and($FFFF_FFFC,esp);EBP⑥mov(
esp,ebp);⑧push(eax);ESP964EAX旧值图12.162022/11/12735ESP1000实参a的值①push(a);ESP996②push(b);③push(c);实参b的值实参c的值992E
SPESP988④callexam返回地址ESP984ESP980EBP旧值局部自动变量区ESP968⑤push(ebp);⑦sub(12,esp);and($FFFF_FFFC,esp);EBP⑥mov(esp,ebp
);⑧push(eax);ESP964EAX旧值图12.172022/11/1273612.4.2访问局部自动变量◼为了访问局部自动变量和形参变量,根据寻址方式,用基址EBP的值加一个偏移量作为访问地址,从
图12.18中看出,要用正的偏移量加EBP作为地址访问形参变量;用负的偏移量加EBP作为地址访问局部变量。◼在过程里只能以EBP为基址,用变址寻址方式访问局部自动变量,局部自动变量是无名变量。2022/11/12737ESP100
0实参a的值ESP996实参b的值实参c的值992ESPESP988返回地址ESP984ESP980EBP旧值局部自动变量区ESP968EBPESP964EAX旧值+4+8+12+16-4-8-12图12.180相对于E
BP的偏移量物理地址形参变量局部自动变量2022/11/12738形参变量区局部自动变量区返回地址旧EBP值调用过程前堆栈的内容ESP保护寄存器状态区EBPEBP+8调用程序用push指令建立过程用sub(N,esp);and($
FFFF_FFFC,esp);指令建立调用程序用call指令建立由过程建立的部分由调用程序建立的部分图12.192022/11/12739例6:以EBP为基址访问过程局部自动变量procedureexam;@n
oframe;beginexam;push(ebp);//保护寄存器EBPmov(esp,ebp);//建立基址指针EBPsub(8,esp);//为局部变量分配内存空间push(eax);//保护寄存器
mov(6,(typedword[ebp-4]));//访问局部变量amov([ebp-4],eax);mov(eax,[ebp-8]);//访问局部自动变量b…endexam;2022/11/12740在过程里为局部变量分配堆栈空间完毕,堆栈的状态如图
12.20过程中使用“[ebp-4]”和“[ebp-8]”变址寻址方式正是汇编语言访问局部自动变量的方法。当过程结束时,恢复寄存器后,为了取得正确的返回地址,必须清除局部自动变量:mov(ebp,esp);//清除局部变量pop(ebp);//恢复EBP,此
后栈顶//的双字正是返回地址学习了汇编语言怎样为局部自动变量分配、回收内存单元后,也就理解了为什么局部自动变量在两次调用同一过程之间不能保存它们的值。2022/11/12741ESPESPESPESP……返回
地址EBP旧值局部变量a1000996992984①callexam②push(ebp)④sub(8,esp)ebp③mov(esp,ebp)局部变量bEBP-4EBP-8图12.202022/11/12742ESPESPESPESP……过程自己清除堆栈返回地址EBP旧
值局部变量a1000996992984③ret()②pop(ebp)ebp①mov(ebp,esp)局部变量b图12.212022/11/12745局部自动变量性质(1)过程里定义的局部变量只能在那个过程里访问。其他过程或程序不
能访问该过程的局部变量。(2)不同过程的局部变量可以同名。(3)局部变量和全局变量可以同名,汇编语言为全局变量和局部变量在不同的内存区分配内存单元。在过程内,只能访问局部变量,在过程外,只能访问全局变量。2022/11/12746过程只能访问在过程
定义之前定义的与局部变量不同名的全局变量。全局变量只有一次生命周期,从主程序开始执行的那一刻起到主程序结束。所有的静态对象(在STATIC,READONLY段声明的对象)也只有一次生命周期,它是程序的执行时间。对于过程也是这样
。所以,局部静态对象的生命周期与全局静态对象的性质上没有区别。例:《过程局部变量与全局变量同名》2022/11/12747过程使用局部自动变量的优点:(1)多个过程分时共享一个固定的内存区每个过程开始执行时在堆栈中为局部自动变量分配内存空间,返回时归还局部自动变量
占用的内存空间,这种内存分时复用,有效利用了内存资源,这也是使用自动(VAR)变量的最大优点。(2)方便模块化设计局部自动变量可以与全局变量同名,各个过程的局部变量也可以同名,任何一个过程内的局部变量与外界没有直接联系,把同名
干扰降到最低。2022/11/1274812.5保护机器的状态调用程序和过程都要使用CPU寄存器,而CPU不能在同一时刻执行调用程序和过程。过程和调用程序只能分时使用CPU,例如,这一时刻是调用程序占用CPU,它使用了寄存器EAX,下一时刻可能是过程占用CPU,它也可能使用寄存器EA
X,过程结束后返回调用程序,调用程序可能要继续使用寄存器EAX,此时,对于调用程序来说,EAX正确的状态必须是调用过程前的状态。2022/11/12749过程是插在调用程序中运行的,执行CALL指令时,只是把程序计数器EIP的值(返回地址)压入堆栈,而此时通用寄存器(EAX,EBX,ECX,ED
X,ESI,EDI,EBP)值是调用程序留下的,在过程结束后,调用程序可能还要继续使用留在寄存器里原来的值。2022/11/12750我们把程序运行中任意时刻CPU的寄存器状态称为机器状态,为了使调用程序在过程结束后仍能正常运行,就要保证调用过程前的机器状态和调用过程后的机器状态一致,这就
是汇编语言过程调用中一个重要的问题。2022/11/12751例7:过程没有保护寄存器造成调用程序无限循环programnonWork;#include("stdlib.hhf");procedurePChars;@nofram
e;beginPChars;push(ebp);mov(esp,ebp)mov(25,ecx);//ECX作为过程的循环计数器L1:stdout.put('A');//输出一个字符dec(ecx);//计数器统计cmp(ecx,0);jneL1;mov(ebp,esp);pop(ebp
);ret();endPChars;2022/11/12752beginnonWork;mov(10,ecx);//用ECX作为计数器L2:callPChars();stdout.put(nl);dec(ecx);cmp(ecx,0);jnzL2;endnonWork;2022/11/12753
因为调用程序和过程都用ecx做循环计数器,过程结束时,ecx的值是0,在调用程序里dec(ecx)指令使ecx的值变成$FFFF_FFFF,次次如此所以外循环无法结束。原因是过程没有保护ecx的值。保护寄存器的意思是在执行过程前把过
程要使用寄存器的值保存,在过程结束时再恢复它们原来的值。只要PChar过程保护了ecx内容,程序就能正常地运行了。2022/11/12754procedurePrintChar;@noframebeginPrintCh
ar;push(ebp);mov(esp,ebp);push(ecx);mov(25,ecx);L1:stdout.put('A');//输出一个字符dec(ecx);//计数器统计cmp(ecx,0);j
nzL1;pop(ecx);mov(ebp,esp);pop(ebp);ret();endPrintChar;//保护ECX//恢复ECX2022/11/12755由谁保护寄存器状态?通常用堆栈保护在子程序中使用的寄存器。无论是
调用程序还是过程都可以承担保护寄存器的责任。由过程保护寄存器有两个优点:节省空间和容易维护。由子程序保护寄存器,只需要子程序自己包含PUSH和POP指令的拷贝。2022/11/12756如让调用程序保护寄存器,每个调用程序都需要一份PUSH和POP指令的拷贝,而且不好维护,如修
改子程序,可能所有的调用程序都要修改。保护寄存器不是保护现场环境的全部。如果需要,也可用PUSH和POP保护过程可能改变的变量值。2022/11/1275712.6过程结束清除堆栈每当过程返回调用程序时,必须清除使用过的堆栈内容。堆栈内容包括多种对象,
其中有的对象要物归原主,有的对象要作废,总之,系统必须收回它们占用的堆栈空间,让栈顶指针回到调用过程前的位置。先回顾调用过程时的堆栈内容:2022/11/12758形参变量区局部自动变量区返回地址旧EBP值调用过程前堆栈的内容ESP保护寄存
器状态区EBPEBP+8图12.22怎样让栈顶指针回到调用过程前的位置?让栈顶指针逐步向高地址方向移动。1.移过保护寄存器状态区2.移过局部自动变量区3.恢复EBP4.将返回地址装入EIP5.移过形参变量区2022/11/12759要将指针移过保护寄存器区,这个区中的内容都是过程在执行前保存的过
程中使用的寄存器内容。这些寄存器内容调用程序可能还要使用,所以必须完璧归赵,使用的方法是按照先进后出的次序将这个区的内容逐个弹回各个寄存器。2022/11/12762形参变量区局部自动变量区返回地址旧EBP值调用过程前堆栈的内容ESP保护机器状态区用POP指令恢复机器状态用MOV(EBP,E
SP)指令回收局部自动变量区用POP(EBP)指令恢复寄存器EBP的原来值用RET(N)指令返回断点回收形参变量区ESPESPESPESPEBPESP图12.232022/11/12763总而言之,程序员必须保证程序在执行ret()指令时,栈顶的内容正是返回地址,同时必须恰如其分地收回形参变量占
用的堆栈空间。2022/11/1276412.7过程中的进栈出栈操作因为过程用堆栈保存返回地址,所以在过程里要谨慎地进行压栈和弹栈操作。2022/11/12765例8:观察下面错误的过程procedureMessed;@noframe;beginMessed;push(e
bp);//②mov(esp,ebp);//③push(eax);//④保护寄存器...//程序中使用了EAXmov(ebp,esp);//㈠pop(ebp);//㈡ret();//㈢未恢复寄存器就返回endMessed;2022/11/12766ESP1000返回地址①
callmessed;ESP996②push(ebp)③mov(esp,ebp)EBP旧值EAX旧值992ESPESP988④push(eax);EBP图12.242022/11/12767ESP1000③ret();返回地址ESP996ESP992EBP旧值EAX旧值ESP988②pop(
ebp);①mov(ebp,esp);EBP虽然从指针观点看最后返回调用程序的地址是正确的,但是寄存器EAX值是过程修改后的值,调用程序访问EAX会出错。图12.252022/11/12768从前面分析看到,过程在返回时未恢复寄存器的值,如果调用程序再访问
这个寄存器时,它的值已经被过程修改,会造成调用程序错误。在过程里如果保护了寄存器,那么在退出过程之前,必须恢复寄存器。2022/11/12769例9:过程过多弹出堆栈的数据procedureMessed;@noframe;beg
inMessed;push(ebp);②mov(esp,ebp);③……pop(eax);㈠mov(ebp,esp);㈡pop(ebp);㈢ret();㈣endMessed;2022/11/12770ESP1000返回地址①callmessed;ESP996②pus
h(ebp)③mov(esp,ebp)EBP旧值992ESPEBP图12.262022/11/12771ESP1000④ret();返回地址ESP996ESP992EBP旧值③pop(ebp);②mov(ebp,esp);EBP①pop(eax
);图12.272022/11/12772程序将EBP的旧值复制给了EAX。通过对照,发现mov(ebp,esp)指令另一个重要作用,它保证ret指令复制给IP寄存器的是正确的返回地址。在过程里要特别注意压栈和弹栈的操作,你要确认在过程里push和po
p指令必须符合一对一和先进后出的关系。2022/11/1277312.8过程中的标号语法允许过程和调用程序使用同名标号。其实在HLA编译源程序生成.ASM文件时,会给程序中的所有标号重新取名,在.ASM文件中每个标号都是唯一的。然
后再汇编.ASM文件,生成.OBJ文件,然后链接成.EXE文件。2022/11/12774例10:用过程输出字符programnonWork;#include("stdlib.hhf");procedurePChars;@noframe;beginPChars;push(ebp);mo
v(esp,ebp);push(ecx);mov(25,ecx);L1:stdout.put('A');//输出一个字符dec(ecx);//计数器统计cmp(ecx,0);jnzL1;2022/11/12775pop(ecx);mov(ebp,esp);pop(e
bp);ret();endPChars;beginnonWork;mov(10,ecx);//用ECX作为计数器L1:callPChars;stdout.put(nl);dec(ecx);cmp(ecx,0);jnzL1;endnonWork;2022/11/12
776虽然语法上允许过程和调用程序使用同名标号,但这样的程序不容易阅读和理解,所以建议调用程序和过程最好不要用同名标号。2022/11/12777例11:求2到100之间的所有素数分析:设计一个过程,判断一个整数是不是素数,过程需要从调用程
序得到那个整数,而调用程序需要知道判断的结果,调用程序和过程需要传递信息,用一个值形参传递这个整数,用寄存器EAX带回判断结果。在过程中设置一个局部自动变量i,作为循环变量。过程的头部可以是:procedureprimfun;@nofra
me;过程的参数和局部自动变量分别用寻址方式[ebp+8]、[ebp-4]表示2022/11/12778局部自动变量[EBP-4]放除数,它也是循环变量,初值是2。形参[EBP+8]放要判断的整数即被除数在计算前先设置循环变量的上限值,这里假
设它是[EBP+8]值的一半,即将[EBP+8]的值右移一位。算法思想是除数从2开始,逐个去除[EBP+8],如果除数从2开始到[EBP+8]/2都除不尽[EBP+8]的话,[EBP+8]就是素数,否则是非素
数。下面是算法的流程图。判断素数算法2022/11/12779[EBP+8]ECX;ECX右移一位;2[EBP–4];[EBP+8]EAX;0EDX;[EDX,EAX]/[EBP–4];[EBP-4]>ECX
EDX=0[EBP-4]++L1L2YY①过程初始化2022/11/12780[EBP-4]<=ECX1EAX0EAXL3L4Y①EAX=1表示是素数清除堆栈返回调用程序图12.282022/11/127
81programHelloWorld;#include("stdlib.hhf")staticn:uns32;procedureprimfun;@noframe;beginprimfun;push(ebp);mov(esp,ebp);sub(4,esp);//为局部变量分配分配内存pu
sh(ecx);//保护寄存器2022/11/12782push(edx);//保护寄存器mov([ebp+8],ecx);//取形参的值shr(1,ecx);//除2,计算循环上界mov(2,(type
uns32[ebp-4]));//访问局部变量L1:cmp([ebp-4],ecx);jaL2;mov([ebp+8],eax);mov(0,edx);2022/11/12783div((typeuns32[ebp-4]));cmp(edx,0);jeL2;//余数等于0
就转移到L2inc((typeuns32[ebp-4]));jmpL1;L2:cmp((typeuns32[ebp-4]),ecx);jbeL3;mov(1,eax);//eax=1代表此数是素数jmpL4;2022/
11/12784L3:mov(0,eax);//eax=0代表此数是非素数stdout.put((typeuns32[ebp-4]),"是第一个除尽它的数",nl);L4:pop(edx);//恢复寄存器pop(ecx);//恢复寄存器
mov(ebp,esp);pop(ebp);ret(4);endprimfun;2022/11/12785beginHelloWorld;mov(2,n);//给循环变量赋初值La:cmp(n,100);jaLd;push(n);//压入实参值callprimf
un;//调用过程cmp(eax,0);jeLb;2022/11/12786stdout.put(n,"是素数",nl);jmpLc;Lb:stdout.put(n,"不是素数",nl);Lc:inc(n);jmpLa;Ld:std
in.readLn();endHelloWorld;结束放映2022/11/12787汇编语言中,数组的下标从()开始。A.0B.1C.2D.任何值在程序里用整数时,HLA要求以%前缀开始,不能带符号。做加法运算时,如CF=1,运算结果是错误的。()D
二进制×2022/11/12788用过程实现冒泡法排序设计一个函数sort2,实现对整型数组的冒泡排序。算法分析:前元素后元素[0][1][1][2][2][3]......[n-2][n-1]重复若干轮,如果某一轮没有发生交换,说明n个数据已排
好序2022/11/12789用过程实现对数组访问,必须考虑怎样才能让过程对数组操作,根据参数传递规则可知,需要引用传递数组的起始地址,过程才能访问实参数组,过程除了知道数组的起始地址外,还必须知道数组的元素个数,过程的参数有两个,一个是引用参数用来传递数组的地址,另一个是值参数用来传
递数组元素的个数。过程定义为:proceduresort2(vararray1:uns32;n:uns32);@noframe;varbool:boolean;2022/11/12790true[ebp-4];0ebx取前元素[ecx+ebx*4]eaxeax与元素[ec
x+ebx*4+4]比较YL1前元素<=后元素①(1)内循环进行一次比较(2)外循环进行一轮比较[ebp+8]--;[ebp+12]ecx[ecx+ebx*4+4][ecx+ebx*4]eax[ecx+ebx*4+4]false[ebp-4]L2L0②③续建传递结构;2022/11/1279
1L0ebx<[ebp+8][ebp-4]=falseYY①(3)如ebx小于n,说明一轮比较未结束,再进行下一次比较。修改元素下标ebx++L1②③(4)如[ebp-4]等于false,说明一轮比较中发生了交换需要再进行一轮比较。恢复现场返回调用程序2022/11/12
792push(ebp);mov(esp,ebp);sub(4,esp);push(eax);push(ebx);push(ecx);dec((typeuns32[ebp+8]));//前元素下标上界mov([ebp+12],ecx);//取数组首址2022/11
/12793L0:mov(true,(typebyte[ebp-4]));//设标志mov(0,ebx);L1:mov([ecx+ebx*4],eax);//取前元素cmp(eax,[ecx+ebx*4+4]);jbeL2;mov((typeuns32[ecx+ebx*4+
4]),[ecx+ebx*4]);mov(eax,[ecx+ebx*4+4]);//前后元素交换mov(false,(typebyte[ebp-4]));//标志清零L2:inc(ebx);2022/11/12794cmp(ebx,[ebp+8]
);jbL1;cmp((typebyte[ebp-4]),false);jeL0;pop(ecx);pop(ebx);pop(eax);mov(ebp,esp);pop(ebp);ret(8);2022/11/12795设计一个过程,将一维数组中后m个元素移到数组
前部。1234567899876123452022/11/12796算法分析:把最后一个元素复制给变量;把其余的元素依次后移一个位置;把变量值赋予第一个元素重复m次2022/11/12797数组地址给ESI;计算最后元素的
下标放入n;计数器ECX初值为0;最后元素的下标给EBX;把最后一个元素值保存到EDX;把数组中其余的元素依次向后移一个位置;EDX给首元素,计数器ECX值加1;ECX<mYN返回调用程序L12022/11
/12798准备移动,计算第一个要移动元素的下标EBX--;把EBX所指元素值复制给相邻的后元素[esi+ebx*4][esi+ebx*4+4]修改下标EBX准备下次移动EBX--;EBX>=0YNEDX值复制给第一个元素EDX[esi];修改循环计数
器ECX++L22022/11/12799programHelloWorld;#include("stdlib.hhf")constN:=10;staticdata:int32[N];inpu
t:int32;input1:uns32;2022/11/12800proceduremovef(vardata1:int32;n:int32;m:int32);@noframe;push(ebp);mov(esp,ebp);push(e
ax);push(ebx);push(ecx);push(edx);push(esi);mov(data1,esi);dec(n);mov(0,ecx);2022/11/12801L1:mov(n,ebx);
mov([esi+ebx*4],edx);dec(ebx);L2:mov((typeint32[esi+ebx*4]),[esi+ebx*4+4]);dec(ebx);cmp(ebx,0);jgeL2;mov(edx,[esi]);inc(ecx);cmp(ec
x,m);jbL12022/11/12802stdout.put(nl);pop(esi);pop(edx);pop(ecx);pop(ebx);pop(eax);mov(ebp,esp);pop(ebp);ret(12);2022
/11/12803beginHelloWorld;mov(0,eax);L3:stdin.get(input);mov(input,data[eax*4]);inc(eax);cmp(eax,N);jbL3;L4:mov(0,eax);stdou
t.put(data[eax*4]:4);inc(eax);cmp(eax,N);jbL4;2022/11/12804L5:stdin.geti32();cmp(eax,1);jlL5;cmp(eax,N-1);j
gL5;pushd(&data);pushd(N);push(eax);callmovef;mov(0,eax);L6:stdout.put(data[eax*4]:4);inc(eax);cmp(eax,N);jbL6;endHelloWorld;