汇编语言_入门经典教材


汇编入门(1讲) 核心提示:1.1 汇编语言的由来及其特点1.1.1 机器语言机器指令是 CPU 能直接识别并 执行的指令,它的表现形式是二进制编码。机器指令通常由操作码和操作数两部分组成,操 作码指出该指令所要完成的操 作,即指令的功能,操作数指出参与运算的对象,以及运算 结果所存放的位置等。由于机器指令与 CPU 紧密相关,所以,不同种类... 1.1 汇编语言的由来及其特点 1.1.1 机器语言 机器指令是 CPU 能直接识别并执行的指令,它的表现形式是二进制编码。机器指令通 常由操作码和操作数两部分组成,操作码指出该指令所要完成的操作,即指令的功能,操 作 数指出参与运算的对象,以及运算结果所存放的位置等。 由于机器指令与 CPU 紧密相关,所以,不同种类的 CPU 所对应的机器指令也就不同, 而且它们的指令系统往往相差很大。但对同一系列的 CPU 来说,为了满足各型号之间具有 良好的兼容性,要做到:新一代 CPU 的指令系统必须包括先前同系列 CPU 的指令系统。只 有这样,先前开发出来的各类程序在新一代 CPU 上才能正常运行。 机器语言是用来直接描述机器指令、使用机器指令的规则等。它是 CPU 能直接识别的 唯一一种语言,也就是说,CPU 能直接执行用机器语言描述的程序。 用机器语言编写程序是早期经过严格训练的专业技术人员的工作,普通的程序员一般难 以胜任,而且用机器语言编写的程序不易读、出错率高、难以维护,也不能直观地反映用计 算机解决问题的基本思路。 由于用机器语言编写程序有以上诸多的不便,现在几乎没有程序员这样编写程序了。 1.1.2汇编语言 虽然用机器语言编写程序有很高的要求和许多不便,但编写出来的程序执行效率高, CPU 严格按照程序员的要求去做,没有多余的额外操作。所以,在保留“程序执行效率高” 的前提下,人们就开始着手研究一种能大大改善程序可读性的编程方法。 为了改善机器指令的可读性,选用了一些能反映机器指令功能的单词或词组来代表该机 器指令,而不再关心机器指令的具体二进制编码。与此同时,也把 CPU 内部的各种资源符 号化,使用该符号名也等于引用了该具体的物理资源。 如此一来,令人难懂的二进制机器指令就可以用通俗易懂的、具有一定含义的符号指令 来表示了,于是,汇编语言就有了雏型。现在,我们称这些具有一定含义的符号为助忆符, 用指令助忆符、符号地址等组成的符号指令称为汇编格式指令(或汇编指令)。 汇编语言是汇编指令集、伪指令集和使用它们规则的统称。伪指令是在程序设计时所需 要的一些辅助性说明指令,它不对应具体的机器指令,有关内容在以后的各章节中会有详细 叙述,在此不展开介绍。 用汇编语言编写的程序称为汇编语言程序,或汇编语言源程序,在本教材中或特定的环 境下,也可简称为源程序。汇编语言程序要比用机器指令编写的程序容易理解和维护。 1.1.3汇编程序 用汇编语言编写的程序大大提高了程序的可读性,但失去了 CPU 能直接识别的特性。 例如用汇编语言书写的指令:MOV AX, BX,CPU 不会知道这几个字符所表达出来的功能, 但程序员一看就知道:要求 CPU 把寄存器 BX 的值传送给寄存器 AX。 把机器指令符号化增加了程序的可读性,但引 起了如何让 CPU 知道程序员的用意,并按照其要 求完成相应操作的问题。解决该问题就需要一个翻 译程序,它能把汇编语言编写的源程序翻译成 CPU 能识别的机器指令序列。这里,我们称该翻译程序 为汇编程序。 从图中不难看出:汇编程序能把左边汇编语言源程序翻译成右边的机器指令序列。其中, 把汇编语言指令“MOV AX, BX”和“ADD AX, 5”分别转换成机器指令89D8H 和050500H,而 后者都是 CPU 能直接识别的,所以,可执行它们。 目前,常用的汇编程序有:MASM、TASM 和 DEBUG 等。 1.1.4 汇编语言的主要特点 一方面,汇编语言指令是用一些具有相应含义的助忆符来表达的,所以,它要比机器语 言容易掌握和运用,但另一方面,它要直接使用 CPU 的资源,相对高级程序设计语言来说, 它又显得难掌握。 汇编语言程序归纳起来大概有以下几个主要特性。 1、与机器相关性 汇编语言指令是机器指令的一种符号表示,而不同类型的 CPU 有不同的机器指令系统, 也就有不同的汇编语言,所以,汇编语言程序与机器有着密切的关系。 由于汇编语言程序与机器的相关性,所以,除了同系列、不同型号 CPU 之间的汇编语 言程序有一定程度的可移植性之外,其它不同类型(如:小型机和微机等)CPU 之间的汇编语 言程序是无法移植的,也就是说,汇编语言程序的通用性和可移植性要比高级语言程序低。 2、执行的高效率 正因为汇编语言有“与机器相关性”的特性,程序员用汇编语言编写程序时,可充分发挥 自己的聪明才智,对机器内部的各种资源进行合理的安排,让它们始终处于最佳的使用状态, 这样做的最终效果就是:程序的执行代码短,执行速度快。 现在,高级语言的 编译程序在进行寄存器分配和目标代码生成时,也都有一定程度的 优化(在后续课程《编译原理》的有关章节会有详细介绍),但由于所使用的“优化策略”要适 应 各种不同的情况,所以,这些优化策略只能在宏观上,不可能在微观上、细节上进行优 化。而用汇编语言编写程序几乎是程序员直接在写执行代码,程序员可以在程 序的每个具 体细节上进行优化,这也是汇编语言程序执行高效率的原因之一。 3、编写程序的复杂性 汇编语言是一种面向机器的语言,其汇编指令与机器指令基本上一一对应,所以,汇编 指令也同机器指令一样具有功能单一、具体的特点。要想完成某件工作(如计算:A+B+C 等), 就必须安排 CPU 的每步工作(如:先计算 A+B,再把 C 加到前者的结果上)。另外,在编写 汇编语言程序时,还要考虑机器资源的限制、汇编指令的细节和限制等等。 由于汇编语言程序要安排运算的每一个细节,这就使得编写汇编语言程序比较繁琐、复 杂。一个简单的计算公式或计算方法,也要用一系列汇编指令一步一步来实现。 4、调试的复杂性 在通常情况下,调试汇编语言程序要比调试高级语言程序困难,其主要原因有四: 汇编语言指令涉及到机器资源的细节,在调试过程中,要清楚每个资源的变化情况; 程序员在编写汇编语言程序时,为了提高资源的利用率,可以使用各种实现技巧,而这些 技巧完全有可能破坏程序的可读性。这样,在调试过程中,除了要知道每条指令的执行功 能,还要清楚它在整个解题过程中的作用; 高级语言程序几乎不显式地使用“转移语句”,但汇编语言程序要用到大量的、各类转移指 令,这些跳转指令大大地增加了调试程序的难度。如果在汇编语言程序中也强调不使用“转 移指令”,那么,汇编语言程序就会变成功能单调的顺序程序,这显然是不现实的; 调试工具落后,高级语言程序可以在源程序级进行符号跟踪,而汇编语言程序只能跟踪机 器指令。不过,现在这方面也有所改善,CV(CodeView)、TD(Turbo Debug)等软件也可在 源程序级进行符号跟踪了。 1.1.5 汇编语言的使用领域 综上所说,汇编语言的特点明显,其诱人的优点直接导致其严重的缺点,其“与机器相 关”和“执行的高效率”导致其可移植性差和调试难。所以,我们在选用汇编语言时要根据实 际的应用环境,尽可能避免其缺点对整个应用系统的影响。 下面简单列举几个领域以示说明,但不要把它们绝对化。 1、适用的领域 要求执行效率高、反应快的领域,如:操作系统内核,工业控制,实时系统等; 系统性能的瓶颈,或频繁被使用子程序或程序段; 与硬件资源密切相关的软件开发,如:设备驱动程序等; 受存储容量限制的应用领域,如:家用电器的计算机控制功能等; 没有适当的高级语言开发环境。 2、不宜使用的领域 大型软件的整体开发; 没有特殊要求的一般应用系统的开发等。 1.2 数据的表示和类型 在用汇编语言进行程序设计时,程序员可以直接访问内存,对数据在存储器内的表示形 式要有一个清晰的认识。下面,我们只简单介绍本课程所要用到的数据表示知识,为后面的 学习作一点必要的准备。 有关“数据表示”的详细内容请参阅《计算机组成原理》中的相关章节。 1.2.1 数值数据的表示 (1)、二进制 在计算机内,数值是用二进制来表示的,每个二进制数按权相加就可得到其十进制数值。 在书写二进制时,为了区别,在数据后面紧跟一个字母 B。 二进制的一般表现形式为:bn-1…b1b0B,其代表数值:bn-12n-1+…+b121+b020。 数据的二进制表示形式简单、明了,但它书写起来比较长,所以,通常情况下,我们在 程序中不直接用二进制来书写具体的数值,而改用八进制、十进制或十六进制。 (2)、八进制 八进制是一种二进制的变形,三位二进制可变为一位八进制,反之也然。八进制的表示 元素是:0、1、…、7。在书写时,为了区别,在数据后面紧跟一个字母 Q。如:1234Q、 7654Q、54Q 等都是八进制。 八进制数在程序中的使用频率不高。 (3)、十进制 十进制是我们最熟悉的一种数据表示形式,它的基本元素是:0、1、…、9。在书写时, 为了区别,在数据后面紧跟一个字母 D。在程序中经常用十进制来表示数据。 (4)、十六进制 十六进制是另一种二进制的变形,四位二进制可变为一位十六进制,反之也然。十六进 制的基本元素是:0、1、…、9、A、B、…、F(字母小写也可以),其中:字母 A、B、…、 F 依次代表10、11、…、15。 在书写时,为了区别,在数据后面紧跟一个字母 H。当十六进制数的第一个字符是字母 时,在第一个字符之前必须添加一个‘0’。如:100H、56EFH、0FFH、0ABCDH 等都是十六 进制数。 十六进制在程序中的使用频率很高。 (5)、数值进制的总结和相互转换 表1.1 各种进制及其字符表示 进制 字符 例子 备注 二进制 B/Y(*) 1010B、1011B 八进制 Q/O 1234Q、311Q 十进制 D/T 1234D、512D 十六进制 H 1234H、1011H (*):字符 Y、O 和 T 是宏汇编 MASM 系统所 增加的进制表示 符。 下面是各进制数据之间进行转换的控件,浏览者通过它可很好地掌握这些进制之间的转 换方法。当十进制转化为其它进制时,浏览者还可进行实际的练习操作。 (6)、数的补码表示法 在计算机内,为了表示正负数,并便于进行各种算术运算,对有符号数采用二进制的补 码表示形式。 补码的最高位用来表示正负数:0—正数,1—负数。 正数的补码是其自身的二进制形式,负数的补码是把其正数的二进制编码变“反”,再 加 1而得。 (7)、二进制数的符号扩展 在汇编语言中,我们经常要对字/字节的数据进行操作。当把“字节”转换成“字”,或“字” 转换成“双字”时,就需要进行符号扩展。符号扩展的具体操作就是把已知信息的最高位扩展 到所有更高位。 例1.1 把8位补码01011010、10101100分别扩展成16位补码。 解:根据符号扩展的含义,“字节→字”的具体扩展结果如下: 01011010 10101100 0000000001011010 1111111110101100 例1.2 把16位补码0101101111001010、1010111101011011别扩展成32位补码。 解:根据符号扩展的含义,“字→双字”的具体扩展结果如下: 0101101111001010 1010111101011011 00000000000000000101101111001010 11111111111111111010111101011011 (8)、n 位二进制的表示范围 n 位二进制所能表示的无符号整数的范围:0≤x≤2n-1。 n 位二进制所能表示的有符号整数(补码表示)的范围:-2n-1≤x≤2n-1-1。 在汇编语言中,常用到 n 为8和16时的数值范围: n=8时,无符号整数的范围:0~255,有符号整数的范围:-128~127; n=16时,无符号整数的范围:0~65535,有符号整数的范围:-32768~32767。 (9)、BCD 码 通常,我们习惯用十进制表示的数据,但计算机是用二进制来表示数数据的,这就需要 进行数值进制之间的转换。我们把每位十进制数转换二进制的编码,简称为 BCD 码(Binary Coded Decimal)。 BCD 码是用4位二进制编码来表示1位十进制数。这种编码方法有多种,但常用的编码 是8421BCD 编码,如表1.2所示。这种 BCD 编码实际上就是0~9的“等值”二进制数。 表1.2 8421BCD 编码列表 十进制数 字 8421BCD 码 十进制数 字 8421BCD 码 0 0000 5 0101 1 0001 6 0110 2 0010 7 0111 3 0011 8 1000 4 0100 9 1001 用 BCD 码进行进制的转换时,是要求在二种进制的表现形式上快速转换,而不是要求 在“数值相等”的含义快速转换。 例1.3 求十进制数2000的 BCD 编码和其二进制数。 解:2000的 BCD 编码是把每位上的数2、0、0、0分别转换为其对应的 BCD 编码:0010、 0000、0000和0000,把它们合在一起就是2000的 BCD 编码:0010 0000 0000 0000。 十进制数2000的二进制数是:11111010000,它们在数值上是相等的。 1.2.2 非数值数据的表示 计算机除了具有进行数值计算能力之外,还具有进行非数值计算的能力。现在,后者的 应用领域已远远超过了前者的应用领域,如:文字处理、图形图象处理、信息检索、日常的 办公管理等。所以,对非数值信息的编码就显得越加重要。 1、ASCII 码 ASCII 码 (American Standard Code for Information Interchange)是目前应用极其广泛 的一种信息编码,许多计算机系统都是采用它 为字符进行编码。它是一种7位二进制编码。 右表是 ASCII 码的具体编码方案。在该表 中,对学习本课程有用的主要信息有: 字符'0'~'9'是连续编码的,其编码的低4 位就是该字符在十进制中的数值; 小写字母的编码比大写字母的编码大, 对应字母的编码之间相差20H。 当然,从 ASCII 码表中还可看出其它有用 信息,还有扩展的 ASCII 码等知识,但这些内 容对学习本课程的帮助不明显,故不再叙述。 有兴趣的读者可参阅其它书籍。 表1.3 ASCII 码的编码方案 高 位 低 位 000 001 010 011 100 101 110 111 0000 NUL DEL SP 0 @ P ` p 0001 SOH DC1 ! 1 A Q a q 0010 STX DC2 “ 2 B R b r 0011 ETX DC3 # 3 C S c s 0100 EOT DC4 $ 4 D T d t 0101 ENQ NAK % 5 E U e u 0110 ACK SYN & 6 F V f v 0111 BEL ETB ‘ 7 G W g w 1000 BS CAN ( 8 H X h x 1001 HT EM ) 9 I Y i y 1010 LF SUB * : J Z j z 1011 VT ESC + ; K [ k { 1100 FF FS < L \ l | 1101 CR GS - = M ] m } 1110 SO RS . > N ^ n ~ 1111 SI US / ? O _ o Del 2、汉字编码 ASCII 码是针对英文的字母、数字和其它特殊字符进行编码的,它不能用于对汉字的编 码。要想用计算机来处理汉字,就必须先对汉字进行适当的编码。我国在1981年5月对6000 多个常用的汉字制定了交换码的国家标准,即:GB2312-80。该标准规定了汉字交换用的基 本汉字字符和一些图形字符,它们共计7445个,其中汉字有6763个。该标准给定每个字符的 二进制编码,即国标码。 有关汉字编码的详细信息,请参阅其它有关书籍,在此不再介绍。 1.2.3 基本的数据类型 汇编语言所用到的基本数据类型为:字节、字、双字等,这些数据类型在以后的章节中 都有相应的类型说明符。下面对它们进行最基本的描述。 1、字节 一个字节有8位二进制组成,其最高位是第7位,最低位是第0 位,如右图所示。在表示有符号数时,最高位就是符号位。 通常情况下,存储器按字节编址,读写存储器的最小信息单 位就是一个字节。 2、字 由2个字节组成一个字,其最高位是第15位,最低位是第0 位。高8位称为高字节,低8位称为低字节,如右图所示。 字节和字是汇编语言程序中最常用的两种数据类型,也是 最容易出错的数据类型。 3、双字 用2个字(4个字节)来组成一个双字,其高16位称为高字, 低16位称为低字,如右图所示。 双字有较大的数据表示范围,它通常是为了满足数据的表 示范围而选用的数据类型,也可用于存储远指针。 字节、字和双字是汇编语言最常用的三种数据类型,下图 表现出它们三者之间的组成关系。 4、四字 由4个字(8个字节)组成一个四字类型,它总共有64个二进制位,当然,也就有更大的数 据表示范围,但在汇编语言中很少使用该数据类型。 5、十字节 由10个字节组成一个十字节类型,它总共有80个二进制位。在汇编语言中很少使用该数 据类型。 6、字符串 字符串是由若干个字节组成的,字节数不定,通常每个字节存储一个字符。该数据形式 是汇编语言程序中经常使用的另一种数据形式。 汇编入门(2讲) 时间:2009-5-14 16:13:55 核心提示:第 2章 CPU 资源和存储器计算机的硬件资源是用汇编语言编程所必须要了 解的重要内容,因为汇编语言允许、也需要程序员直接使用这些硬件资源,只有这样才能编 写出 高效的目标代码。在汇编语言中,需要访问的硬件资源主要有:CPU 内部资源、存储 器和 I/O 端口。本章将着重讲解 CPU 内部寄存器的命名、功能及其常见的 用途... 第2章 CPU 资源和存储器 计算机的硬件资源是用汇编语言编程所必须要了解的重要内容,因为汇编语言允许、也 需要程序员直接使用这些硬件资源,只有这样才能编写出高效的目标代码。 在汇编语言中,需要访问的硬件资源主要有:CPU 内部资源、存储器和 I/O 端口。本章 将着重讲解 CPU 内部寄存器的命名、功能及其常见的用途,还要介绍存储器的分段管理模 式、存储单元地址的表示法以及其物理地址的形成方式。 2.1 寄存器组 寄存器是 CPU 内部重要的数据存储资源,是汇编程序员能直接使用的硬件资源之一。 由于寄存器的存取速度比内存快,所以,在用汇编语言编写程序时,要尽可能充分利用寄存 器的存储功能。 寄存器一般用来保存程序的中间结果,为随后的指令快速提供操作数,从而避免把中间 结果存入内存,再读取内存的操作。在高级语言(如:C/C++语言)中,也有定义变量为寄存 器类型的,这就是提高寄存器利用率的一种可行的方法。 另外,由于寄存器 的个数和容量都有限,不可能把所有中间结果都存储在寄存器中, 所以,要对寄存器进行适当的调度。根据指令的要求,如何安排适当的寄存器,避免操作数 过多的 传送操作是一项细致而又周密的工作。有关“寄存器的分配策略”在后续课程《编译 原理》中会有详细的介绍。 由于16位/32位 CPU 是微机 CPU 的两个重要代表,所以,在此只介绍它们内部寄存器 的名称及其主要功能。 2.1.1 寄存器组 1、 16位寄存器组 16位 CPU 所含有的寄存器有(见图2.1中16位寄存器部分): 4个数据寄存器(AX、BX、CX 和 DX) 2个变址和指针寄存器 (SI 和 DI) 2个指针寄存器(SP 和 BP) 4个段寄存器(ES、CS、SS 和 DS) 1个指令指针寄存器(IP) 1个标志寄存器(Flags) 2、 32位寄存器组 32位 CPU 除了包含了先前 CPU 的所有寄存器,并把通用寄存器、指令指针和标志寄存 器从16位扩充成32位之外,还增加了2个16位的段寄存器:FS 和 GS。 32位 CPU 所含有的寄存器有(见图2.1中的寄存器): 4个数据寄存器(EAX、EBX、ECX 和 EDX) 2个变址和指针寄存器(ESI 和 EDI) 2个指针寄存器(ESP 和 EBP) 6个段寄存器(ES、CS、SS、DS、 FS 和 GS) 1个指令指针寄存器(EIP) 1个标志寄存器(EFlags) 2.1.2、通用寄存器的作用 通用寄存器可用于传送和暂存数据,也可参与算术逻辑运算,并保存运算结果。除此之外, 它们还各自具有一些特殊功能。汇编语言程序员必须熟悉每个寄存器的一般用途和特殊用途, 只有这样,才能在程序中做到正确、合理地使用它们。 表2.1 通用寄存器的主要用途 寄存器的分类 寄存器 主 要 用 途 AX 乘、除运算,字的输入输出,中间结 果的缓存 AL 字节的乘、除运算,字节的输入输出, 十进制算术运算 AH 字节的乘、除运算,存放中断的功能 号 BX 存储器指针 CX 串操作、循环控制的计数器 CL 移位操作的计数器 数据 寄存器 DX 字的乘、除运算,间接的输入输出 SI 存储器指针、串指令中的源操作数指 针 通 用 寄 存 器 变址 寄存器 DI 存储器指针、串指令中的目的操作数 指针 BP 存储器指针、存取堆栈的指针 变址 寄存器 SP 堆栈的栈顶指针 指令指针 IP/EIP 标志位寄存器 Flag/EFlag ES 附加段寄存器 CS 代码段寄存器 SS 堆栈段寄存器 16位 CPU 的 段寄存器 DS 数据段寄存器 FS 附加段寄存器 32位 CPU 的 段寄存器 新增加的 段寄存器 GS 附加段寄存器 2.1.3、专用寄存器的作用 16位 CPU 内部有一个16位的标志寄存器,它包含9个标志位。这些标志位主要用来反映处 理器的状态和运算结果的某些特征。各标志位在标志寄存器内的分布如图2.2所示。 1 5 1 4 1 3 1 2 1 1 1 0 9876543 2 1 0 O F D F I F T F S F Z F A F P F CF 31 … 17 1615 14 13 12 11 10 9 8 7 6 5 4 3 2 10 … … V M RF NT IOPL OF DF IF TF SF ZF AF PF CF 图2.2 16位/32位标志寄存器的示意图 上面9个标志位可分为二组:运算结果标志位(有背景色的标志位)和状态控制标志位。前 者受算术运算和逻辑运算结果的影响,后者受一些控制指令执行的影响。 有些指令的执行会改变标志位(如:算术运算指令等),不同的指令会影响不同的标志位, 有些指令的执行不改变任何标志位(如:MOV 指令等),有些指令的执行会受标志位的影响(如: 条件转移指令等),也有指令的执行不受其影响。 程序员要想熟练运用这些标志位,就必须掌握每个标志位的含义、每条指令的执行条件和 执行结果对标志位的作用。 注意:虽然知道每个标志位在标志寄存器内的具体位置是有好处的,但通常情况下,没有 这个必要。在使用第5.2.9节中的“条件转移指令”时,系统会自动引用相应标志位的值来决定是 否需要“转移”的,所以,不必过分强调标志位在标志寄存器内的具体位置。 2.2 存储器的管理模式 Intel 公司的80X86系列的 CPU 基本上采用内存分段的管理模式。它把内存和程序分成若 干个段,每个段的起点用一个段寄存器来记忆,所以,学习微机汇编语言,必须要清楚地理解 存储器的分段含义、存储单元的逻辑地址和其物理地址之间的转换关系。 2.2.1 16位微机的内存管理模式 1、存储器的分段 我们知道:计算机的内存单元是以“字节”为最小单位进行线性编址的。为了标识每个存储 单元,就给每个存储单元规定一个编号,此编号就是该存储单元的物理地址。 存储单元的物理地址是一个无符号的二进制数。但为了书写的简化,物理地址通常用十六 进制来表示。 16位 CPU 内部有20根地址线,其编码区间为:00000H~0FFFFFH,所以,它可直接访问 的物理空间为1M(220)字节。而16位 CPU 内部存放存储单元偏移量的寄存器(如:IP、SP、BP、 SI、DI 和 BX 等)都是16位,它们的编码范围仅为:00000H~0FFFFH。这样,如果用16位寄 存器来访问内存的话,则只能访问内存的最低端的64K,其它的内存将无法访问。为了能用16 位寄存器来有效地访问1M 的存储空间,16位 CPU 采用了内存分段的管理模式,并引用段寄存 器的概念。 16位微机把内存空间划分成若干个逻辑段,每个逻辑段的 要求如下: 逻辑段的起始地址(通常简称为:段地址)必须是16的倍 数,即最低4位二进制必须全为0; 逻辑段的最大容量为64K,这 由 16位寄存器的寻址空间所 决定。 按上述规定,1M 内存最多可分成64K 个段,即65536个段 (段之间相互重叠),至少可分成16个相互不重叠的段。 右图2.4是内存各逻辑段之间的分布情况示意图,其中有相 连的段(如:C 和 D 段)、不相连的段(如:A 和 B 段)以及相互 重叠的段(如:B 和 C 段)。 这种存储器分段的内存管理方法不仅实现了用两个16位寄存器来访问1M 的内存空间,而 且对程序的重定位、浮动地址的编码和提高内存的利用率等方面都具有重要的实用价值。 2、物理地址的形成方式 由于规定段地址必须是16的倍数,所以,其值的一般形式为:XXXX0H,即:前16位二 进制位是变化的,后四位是固定为0。鉴于段地址的这种特性,我们可以仅保存其前16位二进 制来达到保存整个段地址,其后四位可通过“左移补0”来获得。 在确定了某个存储单元所属的内存段后,我们也只知道其所处内存位置的范围,还不能确 定其具体位置。要想确定内存单元的具体位置,还必须知道该单元离该段地址有多远。我们通 常把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也可称为有效地址 (EA—Effective Address)或偏移量(Offset)等。有了段地址和偏移量,就能唯一地确定某一内存 单元在存储器内的具体位置。 由此可见,存储单元的逻辑地址分为两部分:段地址和偏移量。由逻辑地址得到其物理地 址(PA—Physical Address)的计算方法如下: 物理地址 PA=段地址×16 + 偏移量 计算存储单元物理地址的公式可用“左移4位”和“加”运算来实现。图2.5是物理地址的计算 示意图。 对物理地址来说,当段地址变化时,只要对其偏移量进行相应的调整就可对应同一个物理 地址,所以,同一个物理地址可有多个逻辑地址。 在汇编语言程序中,存储单元通常不是用其物理地址标识的,而是用其逻辑地址标识的。 逻辑地址的段地址由段寄存器给出,偏移量可由寄存器(SI、DI、BP 和 BX 等)给出,也可用符 号地址或具体的数值给出。至于在指令中如何指出存储单元的逻辑地址将在第3章“寻址方式” 中给出详细说明。 3、段寄存器的引用 段寄存器是因为对内存的分段管理而设置的。16位 CPU 有四个段寄存器,所以,其程序可同时访问四个不同含义的 段。段寄存器及其偏移量的引用关系如图2.7所示。 段寄存器 CS 指向存放程序的内存段,IP 是用来存放下 条待执行的指令在该段的偏移量,把它们合在一起可在该内 存段内取到下次要执行的指令。 段寄存器 SS 指向用于堆栈的内存段,SP 是用来指向该 堆栈的栈顶,把它们合在一起可访问栈顶单元。另外,当偏 移量用到了指针寄存器 BP,则其缺省的段寄存器也是 SS,并 且用 BP 可访问整个堆栈,不仅仅是只访问栈顶。 段寄存器 DS 指向数据段,ES 指向附加段,在存取操作 数时,二者之一和一个偏移量合并就可得到存储单元的物理 地址。该偏移量可以是具体数值、符号地址和指针寄存器的 值等之一,具体情况将由指令的寻址方式来决定。 通常,缺省的数据段寄存器是 DS,只有一个例外,即:在进行串操作时,其目的地址的 段寄存器规定为 ES。当然,在一般指令中,我们还可以用强置前缀的方法来改变操作数的段 寄存器(见:第3.3节中的强置前缀的书写格式)。 一般情况下,段寄存器及其指针寄存器的引用关系如下表所示。表2.2中的“可选用的段寄 存器”即是可以用强置说明这些段寄存器的值来作为其操作数地址的段地址。 表2.2 段寄存器及其指针寄存器的引用关系 访问存储器方式 缺省的段寄存器 可选用的段寄存器 偏移量 取指令 CS IP 堆栈操作 SS SP 一般取操作数 DS CS、ES、SS 有效地址 源操作数 DS CS、ES、SS SI 串操作 目标操作数 ES DI 使用指针寄存器 BP SS CS、DS、ES 有效地址 由上表可以看出16位 CPU 在段寄存器的引用方面有如下规定: 、取指令所用的段寄存器和偏移量一定是用 CS 和 IP; 、堆栈操作所用的段寄存器和偏移量一定是 SS 和 SP; 、串操作的目标操作数所用的段寄存器和偏移量一定是 ES 和 DI; 、其它情况,段寄存器除了其默认引用的寄存器外,还可以强行改变为其它段寄存器。 对于上述规定,随着后续内容的叙述,将会对它们有更进一步理解。 4、存储单元的内容 上面,我们讲述了16位微机的内存管理及其相关知识,知道了内存单元物理地址的计算方 法,这使我们能很容易地指定所要访问的存储单元。但存储单元里的内容是如何存放的呢?下 面就能描述数值在内存的存放形式。 存储单元中所存放的二进制信息通常称为该存储单元的内容或值,并且规定: 、一个字节的内容是该字节单元内存放的二进制信息; 、一个字的内容是该字地址所指向的单元及其后继一个单元的内容拼接而成; 、一个双字的内容是该字地址所指向的单元及其后继三个单元的内容拼接而成。 在拼接“字内容”时,我们按“高高低低”的原则来处理,即:高 存储单元(地址大的存储单元)的值是“字内容”的高8位,低存储单元 (地址小的存储单元)的值是“字内容”的低8位。在拼接“双字内容”时 也是如此。 右图2.8是一段内存单元存放数据的例子。 从图中可看出下列存储结果: 、字节12340H、12341H 的内容分别为:12H 和34H 等; 、字12340H、12341H 的内容分别为:3412H 和5634H 等; 、双字12340H、12341H 的内容分别为:78563412H 和 90785634H 等。 2.2.2 32位微机的内存管理模式 32位微机的内存存管理仍然采用“分段”的管理模式,存储器的逻辑地址同样由段地址和偏 移量两部分组成。32位微机的内存管理与16位微机的有相同之处,也有不同之处,因为它提供 了两种不同工作方式:实方式和保护方式。 1、物理地址的计算方式 实方式:段地址仍然是16的倍数,每个段的最大容量仍为64K。段寄存器的值是段的 起始地址,存储单元的物理地址仍为段寄存器的值乘16,再加上段内偏移量。 在此方式下,32位微机的内存管理与16位微机是相一致的。 保护方式:段地址可以长达32位,其值可以不是16的倍数,每个段的最大容量可达4G。 段寄存器的值是表示段地址的“选择器”(Selector),用该“选择器”可从内存中 得到一个32位的段地址,存储单元的物理地址就是该段地址加上段内偏移 量,这与16位微机的物理地址计算完全不同。 2、段寄存器的引用 32位 CPU 内有6个段寄存器,程序在某一时刻可访问6个不同的段。其段寄存器的值在不 同的方式下具有不同的含义: (1)、在实方式下,段寄存器的值就是段地址; (2)、在保护方式下,段寄存器的值不是段地址,是段地址的“选择器”。它间接指出一个32 位的段地址。 下面分别说明各段寄存器的用法和作用。 代码段寄存器:32位微机在取指令时,系统自动引用 CS 和 EIP 来取出下条指令。在实 方式下,由于段的最大容量不超过64K,所以,EIP 的高16位全为0,其 效果相当于16位 CPU 中的 IP。 堆栈段寄存器:32位微机在访问堆栈段时,总是引用堆栈段寄存器 SS。但在不同的方 式下其堆栈指针有所不同: 1)、在实方式下,32位微机把 ESP 的低16位 SP 作为指向堆栈的指针, 所以,我们可以认为栈顶单元是由 SS 和 SP 来指定的。这就与16 位微机访问栈顶单元的方法相一致; 2)、在保护方式下,堆栈指针可用32位的 ESP 和16位的 SP。 数据段寄存器:DS 是主要的数据段寄存器。通常情况下,它是除访问堆栈以外数据时 的默认段寄存器。在某些串操作中,其目的操作数的段寄存器被指定为 ES 是另一个例外。 另外,段寄存器 CS、SS、ES、FS 和 GS 也都可以作为访问数据时的段 寄存器,但它们必须用段超越前缀的方式在指令中直接写出。用这种方 式会增加指令的长度,指令的执行时间也有所延长。 一般来说,程序频繁访问的数据段用 DS 来指向,不太经常访问的数据 段可用 ES、FS 和 GS 等来指向。 3、存储单元的内容 32位微机存储单元内容的存储格式与16位微机的完全一致,也都采用“高高低低”的原则来 存放数据。 汇编入门(3讲) 时间:2009-5-14 16:18:41 核心提示:第 3章 操作数的寻址方式操作数是指令或程序的主要处理对象。如果某条 指令或某个程序不处理任何操作数,那么,该指令或程序不可能有数据处理功能。在 CPU 的指令 系统中,除 NOP(空操作指令)、HLT(停机指令)等少数指令之外,大量的指令在执行 过程中都会涉及到操作数。所以,在指令中如何表达操作数或操作数所 在位... 第3章 操作数的寻址方式 操作数是指令或程序的主要处理对象。如果某条指令或某个程序不处理任何操作数,那 么,该指令或程序不可能有数据处理功能。在 CPU 的指令系统中,除 NOP(空操作指令)、 HLT(停机指令)等少数指令之外,大量的指令在执行过程中都会涉及到操作数。所以,在指 令中如何表达操作数或操作数所在位置就是正确运用汇编指令的一个重要因素。 在指令中,指定操作数或操作数存放位置的方法称为寻址方式。操作数的各种寻址方式 是用汇编语言进行程序设计的基础,也是本课程学习的重点之一。 微机系统有七种基本的寻址方式:立即寻址方式、寄存器寻址方式、直接寻址方式、寄 存器间接寻址方式、寄存器相对寻址方式、基址加变址寻址方式、相对基址加变址寻址方式 等。其中,后五种寻址方式是确定内存单元有效地址的五种不同的计算方法,用它们可方便 地实现对数组元素的访问。 另外,在32位微机系统中,为了扩大对存储单元的寻址能力,增加了一种新的寻址方式 ——32位地址的寻址方式。 为了表达方便,我们用符号“(X)”表示 X 的值,如:(AX)表示寄存器 AX 的值。 3.1立即寻址方式 操作数作为指令的一部分而直接写在指令中,这种操作数称为立即数,这种寻址方式也 就称为立即数寻址方式。 立即数可以是8位、16位或32位,该数值紧跟在操作码之后。如果立即数为16位或32位, 那么,它将按“高高低低”的原则进行存储。例如: MOV AH, 80H ADD AX, 1234H MOV ECX, 123456H MOV B1, 12H MOV W1, 3456H ADD D1, 32123456H 其中:B1、W1和 D1分别是字节、字和双字单元。 以上指令中的第二操作数都是立即数,在汇编语言中,规定:立即数不能作为指令中的 第二操作数。该规定与高级语言中“赋值语句的左边不能是常量”的规定相一致。 立即数寻址方式通常用于对通用寄存器或内存单元赋初值。图3.1是指令“MOV AX, 4576H”存储形式和执行示意图。 3.2 寄存器寻址方式 指令所要的操作数已存储在某寄存器中,或把目标操作数存入寄存器。把在指令中指出 所使用寄存器(即:寄存器的助忆符)的寻址方式称为寄存器寻址方式。 指令中可以引用的寄存器及其符号名称如下: 、8位寄存器有:AH、AL、BH、BL、CH、CL、DH 和 DL 等; 、16位寄存器有:AX、BX、CX、DX、SI、DI、SP、BP 和段寄存器等; 、32位寄存器有:EAX、EBX、ECX、EDX、ESI、EDI、ESP 和 EBP 等。 寄存器寻址方式是一种简单快捷的寻址方式,源和目的操作数都可以是寄存器。 1、源操作数是寄存器寻址方式 如:ADD VARD, EAX ADD VARW, AX MOV VARB, BH 等。 其中:VA R D、VA RW 和 VA R B 是双字,字和字节类型的内存变量。在第4章将会学到 如何定义它们。 2、目的操作数是寄存器寻址方式 如:ADD BH, 78h ADD AX, 1234h MOV EBX, 12345678H 等。 3、源和目的操作数都是寄存器寻址方式 如:MOV EAX, EBX MOV AX, BX MOV DH, BL 等。 由于指令所需的操作数已存储在寄存器中,或操作的结果存入寄存器,这样,在指令执 行过程中,会减少读/写存储器单元的次数,所以,使用寄存器寻址方式的指令具有较快的 执行速度。通常情况下,我们提倡在编写汇编语言程序时,应尽可能地使用寄存器寻址方式, 但也不要把它绝对化。 3.3 直接寻址方式 指令所要的操作数存放在内存中,在指令中直接给出该操作数的有效地址,这种寻址方 式为直接寻址方式。 在通常情况下,操作数存放在数据段中,所以,其物理地址将由数据段寄存器 DS 和指 令中给出的有效地址直接形成,但如果使用段超越前缀,那么,操作数可存放在其它段。 例3.1 假设有指令:MOV BX, [1234H],在执行时,(DS)=2000H,内存单元21234H 的值为 5213H。问该指令执行后,BX 的值是什么? 解:根据直接寻址方式的寻址规则,把该指令 的具体执行过程用图3.2来表示。 从图3.2中,可看出执行该指令要分三部 分: 、由于1234H 是一个直接地址,它紧跟 在指令的操作码之后,随取指令而被 读出; 、访问数据段的段寄存器是 DS,所以, 用 DS 的值和偏移量1234H 相加,得 存储单元的物理地址:21234H; 、取单元21234H 的值5213H,并按“高 高低低”的原则存入寄存器 BX 中。 所以,在执行该指令后,BX 的值就为 5213H。 由于数据段的段寄存器默认为 DS,如果要指定访问其它段内的数据,可在指令中用段 前缀的方式显式地书写出来。 下面指令的目标操作数就是带有段前缀的直接寻址方式。 MOV ES:[1000H], AX 直接寻址方式常用于处理内存单元的数据,其操作数是内存变量的值,该寻址方式可在 64K 字节的段内进行寻址。 注意:立即寻址方式和直接寻址方式的书写格式的不同,直接寻址的地址要写在括号 “[”,“]”内。在程序中,直接地址通常用内存变量名来表示,如:MOV BX, VARW, 其中,VA RW 是内存字变量。 试比较下列指令中源操作数的寻址方式(VARW 是内存字变量): MOV AX, 1234H MOV AX, [1234H] ;前者是立即寻址,后者是直接寻址 MOV AX, VARW MOV AX, [VARW] ;两者是等效的,均为直接寻址 3.4 寄存器间接寻址方式 操作数在存储器中,操作数的有效地址用 SI、DI、 BX 和 BP 等四个寄存器之一来指定,称这种寻址方式为 寄存器间接寻址方式。该寻址方式物理地址的计算方法如 下: 寄存器间接寻址方式读取存储单元的原理如图3.3所 示。 在不使用段超越前缀的情况下,有下列规定: 若有效地址用 SI、DI 和 BX 等之一来指定,则其缺省的段寄存器为 DS; 若有效地址用 BP 来指定,则其缺省的段寄存器为 SS(即:堆栈段)。 例3.2 假设有指令:MOV BX,[DI],在执行时,(DS)=1000H,(DI)=2345H,存储单元12345H 的内容是4354H。问执行指令后,BX 的值是什么? 解:根据寄存器间接寻址方式的规则,在执行 本例指令时,寄存器 DI 的值不是操作数, 而是操作数的地址。该操作数的物理地址 应由 DS 和 DI 的值形成,即: PA=(DS)*16+DI=1000H*16+2345H=12345H。 所以,该指令的执行效果是:把从物理地 址为12345H 开始的一个字的值传送给 BX。 3.5 寄存器相对寻址方式 操作数在存储器中,其有效地址是一个基址寄存器(BX、 BP)或变址寄存器(SI、DI)的内容和指令中的8位/16位偏移量 之和。其有效地址的计算公式如右式所示。 在不使用段超越前缀的情况下,有下列规定: 、若有效地址用 SI、DI 和 BX 等之一来指定,则其 缺省的段寄存器为 DS; 、若有效地址用 BP 来指定,则其缺省的段寄存器为 SS。 指令中给出的8位/16位偏移量用补码表示。在计算有效地址时,如果偏移量是8位,则 进行符号扩展成16位。当所得的有效地址超过0FFFFH,则取其64K 的模。 例3.3 假设指令:MOV BX, [SI+100H],在执行它时,(DS)=1000H,(SI)=2345H,内存单元 12445H 的内容为2715H,问该指令执行后,BX 的值是什么? 解:根据寄存器相对寻址方式的规则,在执行 本例指令时,源操作数的有效地址 EA 为: EA=(SI)+100H=2345H+100H=2445H 该操作数的物理地址应由DS和EA的值形 成,即: PA=(DS)*16+EA=1000H*16+2445H=12445H。 所以,该指令的执行效果是:把从物理地 址为12445H 开始的一个字的值传送给 BX。 3.6 基址加变址寻址方式 操作数在存储器中,其 有效地址是一个基址寄存 器(BX、BP)和一个变址寄存 器(SI、DI)的内容之和。其 有效地址的计算公式如右 式所示。 在不使用段超越前缀的情况下,规定:如 果有效地址中含有 BP,则缺省的段寄存器为 SS;否则,缺省的段寄存器为 DS。 例3.4 假设指令:MOV BX, [BX+SI],在执行 时,(DS)=1000H , (BX)=2100H , (SI)=0011H,内存单元12111H 的内容为 1234H。问该指令执行后,BX 的值是什 么? 解:根据基址加变址 寻址方式的规 则,在执行本例 指令时,源操作 数的有效地址 EA 为: EA=(BX)+(SI)=2100 H+0011H=2111H 该操作数的物 理地址应由 DS 和 EA 的值形成,即: PA=(DS)*16+EA=10 00H*16+2111H=121 11H 所以,该指令的 执行效果是:把从物 理地址为12111H 开 始的一个字的值传 送给 BX。 其执行过程如 右图3.6所示。 3.7 相对基址加变址寻址方式 操作数在存储器中,其有效地址是一个基 址寄存器(BX、BP)的值、一个变址寄存器(SI、 DI)的值和指令中的8位/16位偏移量之和。 在不使用段超越前缀的情况下,规定:如 果有效地址中含有 BP,则其缺省的段寄存器为 SS;否则,其缺省的段寄存器为 DS。 指令中给出的8位/16位偏移量用补码表 示。在计算有效地址时,如果偏移量是8位,则 进行符号扩展成16位。当所得的有效地址超过 0FFFFH,则取其64K 的模。 例3.5 假设指令:MOV AX, [BX+SI+200H],在 执行时,(DS)=1000H,(BX)=2100H, (SI)=0010H,内存单元12310H 的内容为 1234H。问该指令执行后,AX 的值是什 么? 解:根据相对基址加变 址寻址方式的规 则,在执行本例指 令时,源操作数的 有效地址 EA 为: EA=(BX)+(SI)+200H= 2100H+0010H+200H= 2310H 该操作数的物理 地址应由 DS 和 EA 的 值形成,即: PA=(DS)*16+EA=100 0H*16+2310H=12310 H 所以,该指令的 执行效果是:把从物理 地址为12310H 开始的 一个字的值传送给 AX。其执行过程如图 3.7所示。 从相对基址加变 址这种寻址方式来看, 由于它的可变因素较 多,看起来就显得复杂 些,但正因为其可变因 素多,它的灵活性也就 很高。比如: 用 D1[i]来访问一维数组 D1的第 i 个元素, 它的寻址有一个自由度,用 D2[i][j]来访问二维 数组 D2的第 i 行、第 j 列的元素,其寻址有二 个自由度。多一个可变的量,其寻址方式的灵 活度也就相应提高了。 相对基址加变址寻址方式有多种等价的书 写方式,下面的书写格式都是正确的,并且其 寻址含义也是一致的。 MOV AX, [BX+SI+1000H] MOV AX, 1000H[BX+SI] MOV AX, 1000H[BX][SI] MOV AX, 1000H[SI][BX] 但书写格式 BX [1000+SI] 和 SI[1000H+BX]等是错误的,即所用寄存器不能 在“[“,”]”之外,该限制对寄存器相对寻址方式 的书写也同样起作用。 相对基址加变址寻址方式是以上7种寻址 方式中最复杂的一种寻址方式,它可变形为其 它类型的存储器寻址方式。表3.1列举出该寻址 方式与其它寻址方式之间的变形关系。 表3.1 相对基址加变址寻址方式与其它寻址方 式之间的变形关系 源操作数 指令的变形 源操作数的 寻址方式 只有偏移 量 MOV AX, [100H] 直接寻址方 式 只有一个 寄存器 MOV AX, [BX] 或 MOV AX, [SI] 寄存器间接 寻址方式 有一个寄 存器和偏 移量 MOV AX, [BX+100H] 或 MOV AX, [SI+100H] 寄存器相对 寻址方式 有二个寄 存器 MOV AX, [BX+SI] 基址加变址 寻址方式 有二个寄 存器和偏 移量 MOV AX, [BX+SI+100H] 相对基址加 变址寻址方 式 3.8 32位地址的寻址方式 在32位微机系统中,除了支持前面的七种 寻址方式外,又提供了一种更灵活、方便,但 也更复杂的内存寻址方式,从而使内存地址的 寻址范围得到了进一步扩大。 在用16位寄存器来访问存储单元时,只能 使用基地址寄存器(BX 和 BP)和变址寄存器(SI 和 DI)来作为地址偏移量的一部分,但在用32 位寄存器寻址时,不存在上述限制,所有32位 寄存器(EAX、EBX、ECX、EDX、ESI、EDI、 EBP 和 ESP)都可以是地址偏移量的一个组成部 分。 当用32位地址偏移量进行寻址时,内存地 址的偏移量可分为三部分:一个32位基址寄存 器,一个可乘1、2、4或8的32位变址寄存器, 一个8位/32位的偏移常量,并且这三部分还可 进行任意组合,省去其中之一或之二。 32位基址寄存器是:EAX、EBX、ECX、 EDX、ESI、EDI、EBP 和 ESP; 32位变址寄存器是:EAX、EBX、ECX、 EDX、ESI、EDI 和 EBP(除 ESP 之外)。 下面列举几个32位地 址寻址指令: MOV AX, [123456H] MOV EAX, [EBX] MOV EBX, [ECX*2] MOV EBX, [EAX+100 H] MOV EDX, [EAX*4+200H] MOV EBX, [EAX+EDX *2] MOV EBX, [EAX+EDX*2+ 300H] MOV AX, [ESP] 用32位地址偏移量进 行寻址的有效地址计算公 式归纳如右式所示。 由于32位寻址方式能 使用所有的通用寄存器,所 以,和该有效地址相组合的 段寄存器也就有新的规定。 具体规定如下: 1、地址中寄存器的书 写顺序决定该寄存器是基 址寄存器,还是变址寄存 器; 如:[EBX+EBP]中的 EBX 是基址寄存 器,EBP 是变址寄存器,而[EBP+EBX]中的 EBP 是基址寄存器,EBX 是变址寄存器; 2、默认段寄存器的选用取决于基址寄存 器; 3、基址寄存器是 EBP 或 ESP 时,默认的 段寄存器是 SS,否则,默认的段寄存器是 DS; 4、在指令中,如果使用段前缀的方式,那 么,显式段寄存器优先。 下面列举几个32位地址寻址指令及其内存 操作数的段寄存器。 指令的举例 访问内存单元所用的段寄存器 MOV AX, [123456H] ;默认段寄存器 DS MOV EAX, [EBX+EBP] ;默认段寄存器 DS MOV EBX, [EBP+EBX] ;默认段寄存器 SS MOV EBX, [EAX+100H] ;默认段寄存器 DS MOV EDX, ES:[EAX*4+2 00H] ;显式段寄存器 ES MOV [ESP+EDX*2] , AX ;默认段寄存器 SS MOV EBX, GS:[EAX+ED X*2+300H] ;显式段寄存器 GS MOV AX, [ESP] ;默认段寄存器 SS 汇编入门(4讲) 时间:2009-5-15 10:08:56 核心提示:第 4章 标识符和表达式标识符和表达式是程序设计经常用到的两个基本概 念。在用高级语言进行程序设计时,如果程序要对某个变化的量进行处理时,通常都要对该 变化量 定义一个具有某种数据类型的符号名,用该符号名也就等于使用了该变化量。在汇 编语言中,也是如此,所不同的是它们的说明和引用方式不同。4.1 标识符在汇编... 第4章 标识符和表达式 标识符和表达式是程序设计经常用到的两个基本概念。在用高级语言进行程序设计时, 如果程序要对某个变化的量进行处理时,通常都要对该变化量定义一个具有某种数据类型的 符号名,用该符号名也就等于使用了该变化量。在汇编语言中,也是如此,所不同的是它们 的说明和引用方式不同。 4.1 标识符 在汇编语言中,标号、内存变量名、子程序名和宏名等都是标识符,它一般最多由31 个字母、数字及规定的特殊字符(?、@、_、$)等组成,并且不能用数字开头。通常情况下, 汇编语言不区分标识符中字母的大小写。 和高级语言的变量名一样,一般要求标识符尽可能取得有点含义,这会大大改善程序的 可读性,并有助于对程序的理解。但标识符不能是汇编语言的保留字,汇编语言的保留字主 要是指:指令助忆符、伪指令定义符、寄存器名以及一些具有特殊含义的字符串等。 例如:MSG1、ERRMSG2、ASC1、asc2等是合法的标识符,而1a、ah、mov 等就不是 合法的标识符。 试比较 ABCDH 和0ABCDH 之间的差异。前者是标识符,而后者是十六位进制数值。 4.2 简单内存变量的定义 在编程序时,我们往往要根据程序的需要定义一些内存单元。 在高级语言程序中,要 给存储单元取一个符号名,然后通过引用该符号名来访问其所对应的存储单元,而在汇编语 言程序中要灵活一些,它可以给存储单元取符号 名,也可以不取符号名。当给存储单元取 符号名时,则可通过该符号名来访问其对应的存储单元;当不给存储单元取符号名时,则可 通过存储单元的偏移量(有效地 址)来访问它。 汇编语言中,常见的数据类型有字节、字和双字等。下面介绍如何定义各种整型类型的 内存变量,有关浮点类型变量的定义方式将在第11章中介绍。 4.2.1 内存变量定义的一般形式 定义数据变量语句是在程序中经常使用的伪指令语句,其一般格式如下: [变量名] 数据定义符 表达式1[, 表达式2, …, 表达式 n] ;注释 该定义格式的主要解释如下: 、变量名必须是一个合法的标识符,它可以写,也可以不写; 、数据定义符用于确定内存单元的数据类型,常用的定义符有:DB、DW 和 DD 等; 、表达式是定义内存单元时的初值表达式,一个定义语句可以有多个初值表达式,各表达 式之间必须用逗号‘,’分开;如果某个存储单元没有初值表达式,则必须用一个问号‘?’ 来表示; 、在定义语句的后面可以书写注释内容,也可以不写。 在定义变量时,虽然可以不写变量名,但我们建议还是要写,因为不写变量名,就意味 着只能用内存单元的偏移量来访问它。这时,一旦内存单元的偏移量发生变化,那么,程序 中的所有引用都要修改,这不仅增加了程序维护的工作量,而且也容易因遗漏修改而出错。 4.2.2 字节变量 定义字节变量的定义符为 DB/BYTE(Define Byte),每个字节只占一个字节单元。其中: BYTE 是 MASM 6.0及其以后版本的数据类型说明符,随后的其它类型说明符同此说明。 例如: DB 6 COUNTER DB 'A', 'D', 0Dh, '$' TABLE DB 1, 3, 5, 7, 9, 11 上面的定义语句经汇编后所产生出的内存单元分配情况如图4.1所示。图中的数据是用 十六进制表示的(以后也如此,不再说明),由引号括起来的字符在内存中是存放其 ASCII 码 值。所以,'D'和0Dh 是不同的,前者是字符'D',后者是数值12的十六进制编码。 … 0 6 4 1 44 0 D 24 01 03 05 07 09 0B … COUNTER TABLE 图4.1 内存单元的分配情况示意图 注意:在上例中,说明语句“DB 'A', 'D', 0Dh, '$'”之前并没有给出变量名,但我们可以从 前面的变量名 COUNTER 一直往后数,或从 TABLE 往前数,来访问某存储单元,因为它们 是一片连续的存储单元,这和高级语言的变量定义有点区别的。在高级语言中,我们一定要 用某个标识符来说明变量,也必须用该变量名来访问其所对应的存储单元。 用定义符 DB 还可定义一种特殊的数据形式——字符串。在定义字符串时,必须用成对 的单引号或双引号把所要的字符括起来,括号内字符的 ASCII 码将依次存放在相应的字节 单元内。例如: MSG1 DB 'I am a student.' 该说明语句所对应的存储单元分布如下所示。为了看起来方便,并没有用字符的 ASCII 码来存放在相应的存储单元内,而直接用该字符,请不要引起误解。 … 'I' ' ' 'a' 'm' ' ' 'a' ' ' 's' 't' 'u' 'd' 'e' 'n' 't' … 上面的例子也可改写为另一种等价的语句: MSG1 DB 'I', ' ', 'a', 'm', ' ', 'a', ' ', 's', 't', 'u', 'd', 'e', 'n', 't', '.' 显然,前者的说明要比后者方便得多,所以,在程序中都采用前者的书写方式。 4.2.3 字变量 定义字变量的定义符为 DW/WORD(Define Word),每个字占用两个连续的字节单元。 例如: Word1 DW 89H, 1909H, -1 DW 0abcdH, ?, 0 上述定义的内存分配如下所示。 … 89 00 09 19 FF FF CD AB -- -- 00 00 … 由于字变量的数据是按照“高高低低”的原则存于存储单元之中的,而字节数据是按照排 列顺序存于存储单元中的,所以,它们的存储方式有所不同。 试比较下面两个定义的存储顺序,其中:41H 和42H 分别是'A'和'B'的 ASCII 码。 B1 DB 'AB' W1 DW 'AB' … 41h 42h 42h 41h … 4.2.4双字变量 定义双字变量的定义符为 DD/DWORD(Define Doubleword),每个双字变量占用二个连 续的字单元(四个字节)。 DW1 DD 12345678H, ? DW2 DD 0abcd1243H 上述定义的内存分配如下所示。 … 78 56 34 12 -- -- -- -- 43 12 CD AB … 4.2.5 六字节变量 定义六字节变量的定义符为 DF/FWORD(Define Farword)。顾名思义,每个六字节变量 占用六个连续的字节。 DF1 DF 1234567890abH, -1 DF 1abcd23H 上述定义的内存分配如下所示。 … abH 90H 78H 56H 34H 12H 0FF H 0FF H 0FF H 0FF H 0FF H 0FF H 23h H 0cd H 0ab H 01H 00H 00H … 4.2.6八字节变量 定义八字节变量的定义符为 DQ/QWORD(Define Quadword)。同理,每个八字节变量占 用八个连续的字节。 DQ1 DQ 12345678H, 0H, -1234H DQ ?, 1238H, ? 第一个八字节常量12345678H 在内存中的分配方式如下所示,其存储原则与前面相同。 其它八字节常量的存储方式与此一致。 … 78 56 34 12 00 00 00 00 … 4.2.7十字节变量 定义十字节变量的定义符为 DT/TBYTE(Define Tenbytes)。同理,每个十字节变量占用 十个连续的字节。 DT1 DT 12345678H, 0H, -1234H DT2 DT ?, -1H 第一个十字节常量12345678H 在内存中的分配方式如下所示,它同样按“高高低低”的原 则来存储。其它十字节常量的存储方式与此一致。 … 7 8 5 6 3 4 1 2 0 0 00 00 00 00 00 … 以上六个数据类型是汇编语言中最基本的数据类型,其中,前三个是在程序中经常使用 的,后三个的使用频率不太高。 4.3 调整偏移量伪指令 调整偏移量伪指令是在内存变量定义时用来调整内存变量起始偏移量的,它们是在把源 程序汇编成目标文件时起作用。常用的调整偏移量伪指令有:EVEN、ALIGN 和 ORG,它 们的主要目的是:为了更有效地读取内存单元的内容。 4.3.1 偶对齐伪指令 EVEN 偶对齐伪指令格式: EVEN 伪指令的作用是:告诉汇编程序(Assember),本伪指令下面的内存变量从下一个偶地址 单元开始分配。 如果下一个偏移量是偶地址,那么,该伪指令不起作用,否则,汇编程序将空出一个字 节,从下一偶地址开始为其后变量分配内存单元。 假设有下列变量定义,并且变量 B1的偏移量是偶数,其内存单元分布如图4.2所示。 B1 DB 12H W1 D W 4567H ;为了表示方便,不妨再假设其偏移量为:xxxx0H 在上述定义情况下,在许多微机系统中,当需要读变量 W1及其后面的字内容时,硬件 将按图4.3所示的方式分二次读出该字内容,再拼接成一个字内容,这时,无疑需要二个读 内存周期,从而影响程序执行的速度。 出现上述问题的主要原因就是字变量 W1在数据段内的偏移 量是奇数,为了保证其偏移量是偶数,需要在其定义之前加上伪 指令 EVEN。 所以,可把前面的变量定义改变成下列形式: D B 12H B1 EVEN W1 D W 4567H 这时,变量的内存分配和读取字变量 W1的过程如 4.3.2 对齐伪指令 ALIGN 对齐伪指令格式: ALIGN Num 其中:Num 必须是2的幂,如:2、4、8和16等。 伪指令的作用是:告诉汇编程序,本伪指令下面的内存变量必须从下一个能被 Num 整 除的地址开始分配。 如果下一个地址正好能被 Num 整除,那么,该伪指令不起作用,否则,汇编程序将空 出若干个字节,直到下一个地址能被 Num 整除为止。 试比较下面二组变量定义,它们的对齐效果一致吗? DB 12H DB 12H E V E N B1 ALIGN 2 DW 4567H W1 DW 4567H 从上面的对比,我们不难看出:伪指令 ALIGN 的说明功能要比伪指令 EVEN 强。 4.3.3 调整偏移量伪指令 ORG 调整偏移量伪指令格式: ORG 数值表达式 伪指令的作用是:告诉汇编程序,本伪指令下面的内存变量从该“数值表达式”所指定的 地址开始分配。 假设有下列变量定义,并且变量 word1的偏移量为0。 word1 DW 1234h byte1 DB 56h word2 DW 0abcdh ORG 1 byte2 DB ? word3 DW ? byte3 DB ? 前三个变量定义的内存分布如图4.5的左边所示,但由于伪指令“ORG 1”的作用,说明其 后面所说明的变量要从偏移量为“1”的内存单元开始存放。所以,后三个变量的内存分配如 图4.5的右边所示。 由图4.5可见,这些变量的内存分配是相互重叠的,对某个变量的操作无疑会影响到与 之重叠的变量。 另外,变量 byte2、word3和 byte3没有赋初值,如果赋初值的话,则重叠部分的内存单 元的原来初值将被覆盖掉。 在以上三个伪指令 EVEN、ALIGN 和 ORG 中,伪指令 EVEN 的使用频率较高。 4.3.4偏移量计数器的值 前面,我们介绍了几种改变偏移量计数器之值的方法,但在程序中还无法引用其值。汇 编语言提供了一个特殊的符号“$”来引用偏移量计数器的值。 例如: W1 DW $, $ ORG $+3 ;从当前地址开始空3个 字节 B1 DB 43h 假设:在给变量 W1分配内存单元时,当前偏移量计数器的值 为2。 于是,变量 W1后面第一个“$”代表数值2,第一个字分配后,此 时偏移量计数器$的值就为4,所以,第二个“$”就代表数值4。 在分配完二个字之后,偏移量计数器的值变为6,$+3的值为9, 所以,伪指令“ORG $+3”就表示下一个变量从偏移量为9的单元地址 开始分配。 综上分析,上述变量说明所对应的内存单元分布如图4.6所示。 汇编入门(5讲) 时间:2009-5-15 10:30:52 核心提示:4.4 复合内存变量的定义上节,我们介绍了汇编语言中六个最基本的数据类 型,这些数据类型能满足程序设计中绝大多数情况的需要,但也存在需要更复杂的数据类型 的 情况。下面介绍汇编语言所提供的三种复合数据类型的说明形式。4.4.1 重复说明符 DUP 从前面的内容里,我们知道了定义少量内存变量的定义形式,但如果在... 4.4 复合内存变量的定义 上节,我们介绍了汇编语言中六个最基本的数据类型,这些数据类型能满足程序设计中 绝大多数情况的需要,但也存在需要更复杂的数据类型的情况。 下面介绍汇编语言所提供的三种复合数据类型的说明形式。 4.4.1重复说明符 DUP 从前面的内容里,我们知道了定义少量内存变量的定义形式,但如果在程序中要说明50 个、100个、200个甚至更多的、同类型的内存变量时,若采用前面所学的方法,对它们一一 加以说明显然是不可行的。为此,汇编语言提供了变量的重复说明符 DUP,其说明的一般 形式如下: count DUP (表达式, 表达式, …, 表达式) 解释:count 是重复次数,(表达式, 表达式, …, 表达式)是被重复的部分,“表达式”可以 是存储单元的初值,也可以是含义另一个 DUP 的式子。如果在表达式的括号中有多个表达 式,那么,它们之间要用逗号','分开。 例如: BUFFER DB 100 DUP(?) STRING DB 120 DUP('ABCDE'), 0 DATA1 D W 50 DUP(10H, 20 DUP(1,2,3), 20H) POINTS D D 12, 30 DUP(0) 从上面的例子可看出:用 DUP 说明内存变量相当于在高级语言中定义数组。 4.4.2 结构类型的定义 重复说明符 DUP 只能用于重复同一数据类型的变量说明,它不可以重复不同数据类型 的变量说明。为了把一组不同类型的变量说明组合在一起,汇编语言提供了另一种复合数据 类型说明符——结构类型说明符 STRUC。 1、结构类型的定义 用 STRUC 和 ENDS 可以把一系列数据定义语句括起来作为一种新的、用户定义的结构 类型。其一般说明格式如下: 结构名 STRUC [Alignment][, NONUNIQUE] 数据定义语句序列 结构名 ENDS 解释:结构名是一个合法的标识符,且具有唯一性。结构名代表整个结构类型,前后两 个结构名必须一致。结构内被定义的变量为结构字段,变量名即为字段名。 一个结构中允许含有任意多个字段,各字段的类型和所占字节数也都可任意。如果字段 有字段名,则字段名必须唯一。每个字段可独立存取。 、对齐方式(Alignment):可用1、2或4来指定结构中字段的字节边界(Byte boundary), 其缺省值为1。见4.3.2节中的有关叙述; 、NONUNIQUE:要求结构中的字段必须用全名才能访问,见本小节中的“结构类 型字段的引用”。 例如: STRUC NO DD ? CNA ME DB 'Assember' COURS E SCOR E DW 0 COURS E ENDS 在左上例中,COURSE 是结构名,它含 有三个字段:NO、CNAME 和 SCORE, 它们的类型分别是 DD、DB 和 DW。 上例中,COURSE 是结构名,它含有三个字段:NO、CNAME 和 SCORE,这些字段的 类型分别是 DD、DB 和 DW。结构 COURSE 的字段分布如图4.7所示。 A s s e m b e r NO CNAME SCORE 图4.7 结构类型 COURSE 的字段分布示意图 从图4.7,我们不难看出:结构类型 COURSE 共占14个字节,其字段 NO、CNAME 和 SCORE 的偏移量分别为:0、4和12。 结构中的字段可以有字段名,也可以没有字段名。有字段名的字段可直接用该字段名来 访问它,没有字段名的字段可以用该字段在结构中的偏移量来访问。 例如: STRUC NO DD ? ;偏移量为0 DB 10 dup (?) ;偏移量为4 PEASON NAME DB 1 ;偏移量为14 PEASOM ENDS 在结构 PEASON 中,有二个字段有字段名,一个字段没有字段名,但不管有无字段名, 我们都可用其偏移量来访问它。 2、结构类型变量的定义 在定义某个结构类型后,程序员就可以说明该结构类型的内存变量。它的说明形式与前 面介绍的简单数据类型的变量说明基本上一致。其定义格式如下: [变量名] 结构名 <[字段值表]> 解释:1)、变量名即为该结构类型的变量名,它可省缺。如果省缺,则不能用符号名来访问该 内存单元; 2)、字段值表是给字段赋初值,中间用逗号','分开,其字段值的排列顺序及类型应与该 结构说明时各字段相一致; 3)、如果结构变量中某字段用其说明时的缺省值,那么,可用逗号来表示;如果所有字 段都如此,则可省去字段值表,但必须保留一对尖括号"<"、">"。 例如: COURSE1 COURSE <> ;使用缺省的初值 COURSE2 COURSE <1, 'Pascal', 60> COURSE3 COURSE <2, , 84> ;使用缺省的课程名 PEASON1 PEASO N <1000, '张 三', 34> 3、结构类型字段的引用 定义了结构类型的变量后,若要访问其结构中的某个字段,则可采用如下形式: 结构变量名.字段名 该引用方式与高级语言的字段引用方式完全一致,我们还可用偏移量来访问其中的某个 字段,但此方法不直观,变动性大,所以,一般情况下,不提倡使用此方法。 例如: STRUC F1 DW ? F2 DB ? EVEN ;偶对齐 EXAM 1 F3 DW ? ENDS EXAM 1 E1 EXAM1 <1234H,'A',8765H> ;定义结构 EXAM1的一个变量 E1 下面二种方法都可以把结构变量 E1中字段的内容赋给寄存器 AX,但如果在字段 F3之 前增加或减少了字段,那么,这些引用需要改变吗? (1)、用字段名直接引用 MOV AX, E1.F3 (2)、用字段的偏移量间接引用 LEA SI, E1 MOV AX, [SI+4] ;其中4是字段 F3的偏移量 4.4.3 联合类型的定义 联合数据类型是一种特殊的数据类型。它可以实现:以一种数据类型存储数据,以 另 一 种数据类型来读取数据。程序员可以根据不同的需要,以不同的数据类型来读取联合类型中 的数据。也就是说,在一些情况下,以一种数据类型来读取联合类型中的数据,而在另一些 情况下,又以另一种数据类型来读取其数据。 1、联合类型的说明 联合数据类型其说明格式如下: [联合类型名] UNION [Alignment] [,NONUNIQUE] 数据定义语句序列 [联合类型名] ENDS 联合类型中的各字段相互覆盖,即同样的存储单元被多个不同的字段所对应,并且其每 个字段的偏移量都为0。 联合类型所占的字节数是其所有字段所占字节数的最大值。 、对齐方式(Alignment):可用1、2或4来指定结构字节的边界,其缺省值为1。它还 用可伪指令 ALIGN 或 EVEN 来重新定界,也可用命令行 选项/Zp 来定界; 、NONUNIQUE:要求联合类型中的字段必须用全名才能访问,引用联合类型字段 的方法见下面的“联合类型字段的引用”。 例如: DATATYPE UNION BB DB ? ;定义一个字节类型的字段 WW DW ? ;定义一个字类型的字段 DD DD ? ;定义一个双字类型的字段 DATATYPE ENDS 联合类型 DATATYPE 的字段分布如图4.8所示。 在联合类型的最外层定义中,在伪指令 UNION 和 ENDS 的前面一定要书写该联合类型名,而在其嵌 套定义的内层,伪指令 UNION 和 ENDS 之前一定不 能写联合类型名。 例如: UNION BB DB ? WW DW ? UNION ;联合类型的嵌套定义形式 W1 DW ? B1 DB ? UNION 1 ENDS UNION 1 ENDS 2、联合类型变量的定义 联合数据类型的变量只能用第一个字段的数据类型来进行初始化。 例如: U1 DATATYP E <'J'> ;定义一个联合变量,并初始化其值 U2 DATATYP E <1234H> ;初始化错误,只能用字节数据来初始化 U3 UNION1 <1> 3、联合类型字段的引用 定义了联合类型的变量后,就可根据需要,以不同的数据类型或字段名来存取该联合类 型中的数据。引用其字段的具体形式如下: 联合类型变量名.字段名 例如: MOV U1.WW, 1234H ;给联合类型变量赋字数据 MOV AL, U1.BB ;AL=34H MOV BX, U1.WW ;BX=1234H MOV U1.BB, 'A' ;U1的值1241H,41H 是'A'的 ASCII 码 4.4.4 记录类型的定义 1、记录类型的说明 汇编语言的记录类型与高级语言的记录类型不同,它是为按二进制位存取数据提供方便 的。记录类型的说明要用到另一个保留字 RECORD,其说明格式如下: 记录名 RECORD 字段 [, 字段, ……] 其中“字段”代表:字段名:宽度[=初值表达式] 1、记录名代表该记录类型; 2、记录类型可以由多个字段组成,每个字段之间要用逗号','分开; 3、字段的属性包括字段名、宽度和初值; 4、字段的“宽度”表示该字段所占的二进制位数,它必须是一个常数,并且所有字段 的宽度之和不能大于16;如果记录的总宽度大于8,则系统为该记录类型分配二个 字节,否则,只分配一个字节; 记录的最后一个字段排在所分配空间的最低位,然后对记录中的字段依次“从右向 左”分配二进制位,左边没有分完的二进制位补0; 解 释: 5、初值表达式给出的是该字段的缺省值。如果初值超过了该字段的表示范围,那么, 在汇编时将产生错误提示信息;如果某字段没有初值表达式,则其初值为0。 例如: CO LO R RECORD BLINK:1, BACK:3=0, INTENSE:1=1, FORE:3 FL OA T RECORD DSIGN:1, DATA:8, ESIGN:1, EXP:4 记录类型 COLOR 有四个字段:BLINK、 BACK、INTENSE 和 FORE,它们的宽度分别 为:1、3、1和3,所以,该记录类型共有8位二 进制,系统分配给它一个字节。 记录类型 COLOR 的二进制位分布如右图 4.9所示。 记录类型 FLOAT 用来模仿《计算机原理》 中的浮点数表示法,它也有四个字段: DSIGN (尾数的符号位); DATA (尾数); ESIGN (指数的符号位); EXP (指数)。 它们的总宽度是14,所以,系统要给它分配二个字节。记录类型 FLOAT 的二进制位分 布如右图4.10所示。 2、记录变量的定义 在程序中,必须先说明记录类型,然后才能定义该记录类型的变量。记录变量是把其二 进制位分成一个或多个字段的字节或字变量。其定义格式与其它类型变量的定义方式类似, 具体如下: [变量名] 记录名 <[字段值表]> 1、变量名即为该记录类型的变量名,它可省缺。如果省缺,则不能用符号名来访问 该内存单元; 2、字段值表是给字段赋初值,中间用逗号','分开,其字段值的排列顺序及大小应与该 记录说明时各字段相一致; 解 释: 3、如果记录变量的某字段用其说明时的缺省值,那么,可用逗号来表示;如果所有 字段都如此,则可省去字段值表,但必须保留一对尖括号"<"、">"。 例如: COL OR1 COLOR <>, <1, 7, 0, 5>, <1, , 0, 7> FLO AT1 FLOAT <1, 23H, 0, 3>, <0, 89H, 1, 5> 3、记录的专用操作符 操作符 WIDTH 和 MASK 是作用于记录类型的两个专用保留字,利用它们可得到记录 类型的不同属性。 操作符 WIDTH 操作符 WIDTH 返回记录或其字段的二进制位数,即其宽度。其一般书写格式如 下: WIDTH 记录名 或 WIDTH 记录字段名 假设有前面定义的记录类型 COLOR,那么,WIDTH COLOR 的值为8,WIDTH BACK 的值为3,WIDTH BLINK 的值为1。 操作符 MASK 操作符 MASK 返回一个8位或16位二进制数。在该二进制数中,被指定记录或字 段使用的对应位的值为1,否则,其值为0。其一般书写格式如下: MASK 记录名 或 MASK 记录字段名 假设有前面定义的记录类型 FLOAT,那么,MASK EXP 的值为000FH,MASK DATA 的值为1FE0H,WIDTH DSIGN 的值为2000H。 记录字段 记录字段名是一个特殊的操作符,它本身也是操作数,其返回值是该字段移到所 在记录的最低位所需要的位数,即该字段最低位在记录中的位置。 假设有前面定义的记录类型 FLOAT,那么,有: MOV CL, EXP 相当于 MOV CL, 0 MOV CL, DATA 相当于 MOV CL, 5 4.4.5 数据类型的自定义 在有了一些数据类型后,程序员还可定义这些数据类型的别名或指针类型。表达这种定 义的伪指令是 TYPEDEF,其定义形式如下: 新数据类型名 TYPEDEF [位距] [PTR] 数据类型 其中:“位距”是 NEAR、FAR 或 PROC 等。 例如: CHAR TYPEDEF BYTE ;给 BYTE 定义另一个别名 CHAR PCHAR TYPEDEF PTR CHAR ;定义一个字符指针数据类型 PCHAR 有了上述定义之后,下面的变量说明就是合法的。 CH1 CHAR 'ABCDEF' ;定义一个字符串常量 PCH1 PCHAR CH1 ;定义一个指向字符串常量 CH1的变量 汇编入门(6讲) 时间:2009-5-15 10:49:43 核心提示:4.5 标号标号是一种特殊的标识符,它代表代码段中的某个具体位置,它主 要用于表明转移的目标位置。其说明形式如下:标号: 汇编语言指令 ;注释解释: 标号必须是一个合法的标识符,在其后面紧跟一个冒号':',冒号与汇编语言指令之间要有分 隔符。通常用若干个空格、TAB 来作分隔符,一般用分隔符使有关 内容... 4.5 标号 标号是一种特殊的标识符,它代表代码段中的某个具体位置,它主要用于表明转移的目 标位置。其说明形式如下: 标号: 汇编语言指令 ;注释 解释:标号必须是一个合法的标识符,在其后面紧跟一个冒号":",冒号与汇编语言指 令之间要有分隔符。通常用若干个空格、TAB 来作分隔符,一般用分隔符使有 关内容对齐为宜。 4.6内存变量和标号的属性 变量是一个符号地质,其值会根据其数据类型来对应从该地址以后的若干个存储单元中 所存的数值。标号也是一个符号地址,它所对应的存储单元中存放的是指令代码。虽然它们 在某些性质上有所不同,但它们都是一个符号地址,代表一个存储单元的地址,所以,它们 都具有存储单元的属性。除此之外,它们还有各自特殊的属性。 下面介绍内存变量和标号的属性及其有关操作符。 4.6.1段属性操作符 段属性操作符(SEG)返回该标识符所在段的段地址。我们一般只会取内存变量所在段的 段地址,而很少取标号所在段的段地址。 假设有下面变量定义: … SCORE DW ? NAME DB 10 DUP(10) ;数据段的变量定义 … MOV AX, SEG SCORE ;代码段的指令 MOV BX, SEG NAME 由于 SCORE 和 NAME 在同一段中定义,所以,寄存器 AX 和 BX 的值是相等的。 4.6.2偏移量属性操作符 偏移量属性操作符(OFFSET)返回该标识符离它所在段的段地址有多少字节。一般情况, 程序员只会取内存变量的偏移量,而不太关心标号的偏移量。 假设有下面变量定义: FIRST DD 12345678H, 0 ;数据段的变量定义 SCORE DW ?, 12H NAME DB 10 DUP(10) … MOV AX, OFFSET SCORE ;代码段的指令 MOV BX, OFFSET NAME … 假设 FIRST 是数据段的第一个被定义的变量名,它的偏移量为0,SCORE 的偏移量为8, 因为它要跳过二个双字,其它如此类推。 由于 NAME 在 SCORE 之后,且 SCORE 之后有二个字,占四个字节,所以,BX 的值 要比 AX 的值大4。 4.6.3 类型属性操作符 类型属性操作符(TYPE)是返回该变量所占字节数,或标号的“远”(FAR)、“近”(NEAR) 类型。常用标识符的类型值如表4.1所列。 表4.1 常用标识符的类型值列表 标识符种类 字节变量 字变量 双字变量 近标号 (NEAR) 远标号(FAR) TYPE 的值 1 2 4 -1 -2 例如: PEASO N STRUC NO DD ? NAME DB 10 dup (?) DW 1 PEASO M ENDS … B1 DB 1, 2, 3 W1 DW 200 DUP(1,2,30 DUP(10,20)), 101H, -1 PEOPLE PEASO N <> 按属性 TYPE 的含义,TYPE B1、TYPE W1和 TYPE PEOPLE 的值分别为:1,2和16。 4.6.4 长度属性操作符 长度属性操作符(LENGTH)是针对内存变量的操作符,它返回重复操作符 DUP 中的重 复数。如果有嵌套的 DUP,则只返回最外层的重复数;如果没有操作符 DUP,则返回1。 如上例所示,根据属性 LENGTH 的含义,LENGTH B1、LENGTH W1和 LENGTH PEOPLE 的值分别为:1,200和1。 4.6.5 容量属性操作符 容量属性操作符(SIZE)也是针对内存变量的操作符。它的返回值按下列公式计算: SIZE 变量 = (LENGTH 变量) × (TYPE 变量) 如上例所示,SIZE B1、SIZE W1和 SIZE PEOPLE 的值分别为:1,400和16。 4.6.6 强制属性操作符 在程序中,我们有时需要对同一个存储单元以不同的属性来访问,或对一些不确定的存 储属性需要显式指定等,这时,我们就需要强制属性操作符 PTR。该操作符的作用有点象 C 语言中的类型强制方法。 对于指令:MOV [BX], 1H,其目标操作数[BX]是寄存器间接寻址方式,它指向一个存 储单元。在作传送操作时,是把“1H”扩展成8位作字节传送,还是扩展成16位作字传送 呢? 这就使该指令具有二义性,因为[BX]指向的存储单元可以字节或字的首地址。含有该指令 的程序在汇编时,可能会产生警告或出错信息。 为了使指令中存储单元操作数具有明确的属性,我们可以使用强制属性操作符 PTR。 其一般格式为: 数据类型 PTR 地址表达式 其中:数据类型是前面所学的各种数据类型,常用的数据类型有:BYTE、WORD、DWORD、 NEAR 和 FAR 等。 为了明确指令中存储单元的属性,可把指令“MOV [BX], 1H”可改写成: MOV byte ptr [BX], 1H 或 MOV word ptr [BX], 1H 在指令中用操作符 PTR 强制后,不管其后的地址表达式原数据类型是什么,在本指令 中就以 PTR 前面的类型为准。该强制属性只在本指令有效,是一种临时性的属性,它不会 改变原内存单元的定义属性。 例如: W1 DW 1234H, 5678H B1 DB 2 DB 5 D1 DD 23456789H … MOV AX, word ptr b1 ;把 B1开始的二个字节拼接成一个字,执行后,(AX)=0502H MOV BH, byte ptr w1 ;把字 W1的低字节传送给 BH,执行后,(BH)=34H MOV CH, byte ptr w1+1 ;把字 W1的高字节传送给 CH,执行后,(CH)=12H MOV word ptr d1, 12H ;把双字 D1的低字修改成0012H,执行后,(D1)=23450012H 上面指令中的强制属性是临时属性,它不能改变这些变量在定义时的永久属性。 4.6.7 存储单元别名操作符 由上一节的内容,我们知道:在程序中,如果需要以另一种数据类型来访问某一存储单 元时,可用强制属性操作符 PTR 来实现。但如果在程序中要经常以某种其它的数据类型来 访问该存储单元的话,那么,就必须在每次访问时都要加上强制属性操作符 PTR。这样做 虽然可行,但在编写程序时就显得比较麻烦。 为了克服上述不便,汇编语言提供了另一种操作符 THIS,它为同一存储单元取另一别 名,该别名可具有其自身的数据属性,但其段地址和偏移量是不变的。 操作符 THIS 的一般格式为: THIS 数据类型 其中:数据类型是前面所学的各种数据类型,常用的数据类型有:BYTE、WORD、DWORD、 NEAR 和 FAR 等。 例如: WBUFF ER EQU THIS WORD ;EQU 是一个等价符号定义语句,在后面有介绍 BUFFE R DB 20 DUP(?) 上述定义后,它们的逻辑关系如图4.11所示。 …… …… …… 如图4.11所示,这样就给同一片存储单元,取了二个具有不同数据类型的变量名。于是, 在指令中,引用不同的变量名,就使用其不同的数据属性: 、如果引用变量名 WBUFFER,则是按“字”属性来访问; 、如果引用变量名 BUFFER,则是按“字节”属性来访问。 如此一来,指令“MOV AX, word ptr BUFFER”和“MOV AX, WBUFFER”是等效的, 所不同的是:当以“字”属性访问 BUFFER 存储区时,不必使用强制属性符 PTR,而改用“字” 属性变量 WBUFFER 即可。 4.7 表达式 表达式是程序设计课程里的一个重要的基本概念,它可由运算符、操作符、括号、常量 和一些符号连在一起的式子。在汇编语言中,表达式分为:数值表达式和地址表达式。 4.7.1 进制伪指令 RADIX 伪指令 RADIX 用来设置整数的缺省进制,宏汇编开始时所默认的整数进制为十进制。 该伪指令的使用格式如下: .RADIX exp 其中:伪指令前面要用点‘.’开始,exp 的值必须是区间[2, 16]内的一个整数。 该伪指令说明其下面整数的默认进制为 exp。如果某整数已显式地表明了其进制,则该 默认进制对其不起作用。在源文件中,可以使用多个 RADIX 伪指令来分别说明其后整数的 默认进制,但为了避免引起不必要误会,我们不提倡这样去做。 例如: .radix 8 B1 DB 10, 11, 12 ;这三个数是八进制数 DB 10D ;这数是十进制数,因为它已用'D'明确说明而不使用缺省进制 … .radix 10 MOV AX, 1234 ;1234是十进制数 MOV AX, 1234H ;1234H 是十六进制数 思考题: .radix 16 DW 90D, 101B ;前者是十进制数,后者是二进制数吗? 4.7.2 数值表达式 数值表达式是在汇编过程中能够由汇编程序计算其值的表达式,其组成部分在汇编时就 能完全确定。它通常是一些常量的运算组合。 1、常量 常量是一个立即数,直接写在汇编语言语句中,在程序的执行过程中,它不可能发生变 化。通常,我们用二进制、八进制、十进制或十六进制来书写常量。 例如:10101011B、324Q、1234D、1234H、0abcdH、'AB'等都是常量。 在程序中,我们还可用伪指令.RADIX 来改变数据的基数,在后面再详细讲解。 2、算术运算符 算术运算符包括符号:+(正)、-(负),运算符:+(加)、-(减)、*(乘)、/(除)和 MOD(取模)。 这些运算符和常量、括号可组成数值表达式。 如:120+(321-90) mod 3,322*5/32,0abcdH+5,-590等 3、关系运算符 关系运算符包括符号:EQ(相等)、NE(不等)、LT(小于)、GT(大于)、LE(小于等于)和 GE(大于等于)。这些关系运算符和常量、括号也可组成数值表达式。该表达式的计算结果规 定如下: 若关系不成立,则该数值表达式的计算结果为0;否则,其结果为0FFFFH。 如:120H LT 100H+3,21H LE 21H 等,它们的计算结果分别为:0和0FFFFH。 4、逻辑运算符 逻辑运算符包括按位操作符和移位操作符。具体是:AND(逻辑与)、OR(逻辑或)、NOT(逻 辑非)、XOR(异或)、SHL(左移位)和 SHR(右移位)。这些逻辑运算符和常量、括号可组成数 值表达式。 如:1 SHL 3,47H AND 0FH,NOT 56H 等,它们的计算结果分别为:8,7和0A9H。 5、表达式中的其它操作符 汇编语言中,还有其它可在数值表达式中使用的操作符。它们是: 、HIGH(高8位)、LOW(低8位) 、SEG(段地址)、OFFSET(偏移量) 、TYPE(标识符类型)、LENGTH(变量长度)、SIZE(变量容量) 、WIDTH(记录/记录字段宽度)、MASK(记录/记录字段的屏蔽位)等 在以上操作符中,只有 HIGH 和 LOW 没有介绍过,它们分别是选取表达式计算结果的 高8位和低8位。其使用格式如下: HIGH 表达式 LOW 表达式 如:HIGH (1234H+100H),LOW 1234H 等,它们的选取结果分别为:13H 和34H。 6、运算符和操作符的优先级 在汇编语言中,有许多各种运算符和操作符,它们的优先级按从高到低的排列如下: 优先级:高 LENGTH、SIZE、WIDTH、MASK、()、[]、.(用于结构字段)、<>(用于记录类 型) PTR、SEG、OFFSET、TYPE、THIS、:(用于段超越前缀) *、/、MOD、SHL、SHR HIGH、LOW +、- EQ、NE、LT、LE、GT、GE NOT AND ↓ ↓ OR、XOR 优先级:低 SHORT 这些符号及其优先级并不要强记它们,有些符号同时出现的可能性非常小。在以后的学 习中对常用的几个加以运用也就记住了。 4.7.3 地址表达式 地址表达式是计算存储单元地址的表达式,它可由标号、变量名和由括号括起来的基址 或变址寄存器组成。其计算结果表示一个存储单元的地址,而不是该存储单元的值。 例如: B1 DB 10H, 11H, 12H DB 'ABCD' W1 DW 1234H, 5678H 表达式 B1+1、B1+3和 W1+2等都是地址表达式,其值所代表的地址位置如图4.10所示。 显然这些地址表达式所对应的存储内容分别为:11H、'A'和5678H。 … 10 11 12 'A' 'B' 'C' 'D' 34 12 78 56 … B1 B1+3 W1+2 图4.10 地址表达式所对应的存储单元位置示意图 注意:地址表达式 W1+1并不表示字变量 W1之后一个字的存储单元,而是字变量 W1 之后一个字节的存储单元,它的存储单元值是:7812H。 4.8 符号定义语句 在程序中,我们经常要使用一些常量或数值表达式,并把它们直接写在指令中,但当需 要修改时,就要对它们逐个进行修改,这无疑会增加维护程序的工作量,而且每个常量或表 达式所代表的含义也容易遗忘。 为了改善程序的可读性,尽量减少维护程序的工作量,汇编语言提供了为常量或表达式 定义一个符号名的方法。一旦定义了符号名,在指令中就可直接使用它们。 4.8.1 等价语句 1、一般格式 等价语句的一般使用格式如下: 符号名 EQU 表达式 作用是左边的符号名代表右边的表达式。 注意:等价语句不会给符号名分配存储空间,符号名不能与其它符号同名,也不能被重 新定义。 2、用符号名代表常量或表达式 当把一个常量或表达式定义成一个具有一定含义的符号名后,在程序中就可以用该符号 名来代表该常量或表达式。 例如: NUMBER EQU 100 ;给缓冲区的长度取一个符号名 BUFF_LE N EQU NUMBER+2 CR EQU 13 ;给“回车”符的 ASCII 码定义一个符号名 LF EQU 10 ;给“换行”符的 ASCII 码定义一个符号名 … BUFFER DB NUMBER, ?, NUMBER DUP (?) ;用符号名来定义缓冲区 … 3、用符号名代表字符串 用一个具有一定含义的符号名定义某一个较长的字符串,在随后的程序中就用该符号 名。例如: GREETING EQU "How are you!" 在该定义之后,就可使用符号名 GREETING 来代表字符串"How are you!"。 4、用符号名关键字或指令助忆符 用一个(组)程序员自己习惯的符号名来代替汇编语言中的关键字或指令助忆符。但在此 建议不要这样做,因为程序的编写者习惯,程序的其他阅读者可能会觉得很别扭。 例如: MOVE EQU MOV ;给指令 MOV 取另一个符号名 MOVE COUNTER EQU CX ;给寄存器 CX 取一个叫“计数器”的符号名 上面的定义只是给原来的助忆符 MOV 和 CX 起了另一个别名,而原来助忆符 MOV 和 CX 仍然可以使用,所以,我们可编写如下语句: MOVE AX, CX ;相当于指令:MOV AX, CX MOV COUNTER, BX ;相当于指令:MOV CX, BX 5、用符号名定义存储单元的别名 可对一片存储单元定义另一个数据类型的符号名,有关叙述参见前面的操作符 THIS。 例如: WOR D1 EQU THIS WORD ;给后面的字节存储单元取一个字属性的符号名 BYTE 1 DB 12h, 21h FLAG DW 1234H FLAG 1 EQU byte ptr FLAG ;给 FLAG 的低字节取一个字节属性的符号名 FLAG 2 EQU byte ptr FLAG+1 ;给 FLAG 的高字节取一个字节属性的符号名 有了上述定义后,可编写如下语句: MOV AX, WORD1 ;执行后,(AX)=2112H MOV BL, FLAG1 ;执行后,(BL)=34H 4.8.2 等号语句 汇编语言提供了用等号来定义符号常数的方法,即可用符号名代表一个常数。其一般格 式如下: 符号名=数值表达式 数值表达式在汇编时应该可以计算出数值,它不能含有向前引用的符号名称。用等号语 句定义的符号可以被重新定义。例如: ABC = 10 + 200 * 5 ;ABC 的值为1010 ABC1 = 5 * ABC + 21 ;ABC1的值为5071 COUNT = 1 ;COUNT 的值为1 COUNT = 2*COUNT + 1 ;COUNT 的值为3 4.8.3 符号名定义语句 符号名定义语句 LABEL 与前面的操作符 THIS 功能类似,该语句定义一个指定的符号 名,该符号名的段地址和偏移量与下面紧跟存储单元的相应属性相同,但该符号的类型是新 指定的。LABEL 语句的一般格式如下: 符号名 LABEL 类型 其中:常用的类型有 BYTE、WORD、DWORD、NEAR 和 FAR 等。 例如: … WBUFFER LABEL WORD BUFFER DB 200 DUP(0) ;WBUFFER 与 BUFFER 具有相同的段地址和;偏移量, 但它们的数据类型不同。 … NEXT1 LABEL FAR NEXT: MOV AX, BX ;NEXT1和 NEXT 具有相同的段地址和偏移量,;NEXT1 是“远”标号,NEXT 是“近”标号。 标号NEXT1和NEXT可用于不同的情况。当在同一个模块内转移时,可使用标号NEXT; 当在不同的模块之间进行转移时,需要使用“远”标号 NEXT1。 汇编入门(7讲) 时间:2009-5-16 8:12:26 核心提示:第 5章 微机 CPU 的指令系统指令系统确定了 CPU 所能完成的功能,是用 汇编语言进行程序设计的最基本部分。如果不熟悉汇编指令的功能及其有关规定,那么,肯 定不 能灵活运用汇编语言。所以,本章的内容是学习本课程的重点和难点。5.1 汇编语言 指令格式为了介绍指令系统中指令的功能,先要清楚汇编语言是如何书写指令的... 第5章 微机 CPU 的指令系统 指令系统确定了 CPU 所能完成的功能,是用汇编语言进行程序设计的最基本部分。如 果不熟悉汇编指令的功能及其有关规定,那么,肯定不能灵活运用汇编语言。所以,本章的 内容是学习本课程的重点和难点。 5.1 汇编语言指令格式 为了介绍指令系统中指令的功能,先要清楚汇编语言是如何书写指令的,这就象在学习 高级语言程序设计时,要清楚高级语言语句的语义、语法及其相关规定一样。 5.1.1 指令格式 汇编语言的指令格式如下: 指令助忆符 [操作数1 [, 操作数2 [, 操作数3]]] [;注释] 指令助忆符体现该指令的功能,它对应一条二进制编码的机器指令。指令的操作数个数 由该指令的确定,可以没有操作数,也可以有一个、二个或三个操作数。绝大多数指令的操 作数要显式的写出来,但也有指令的操作数是隐含的,不需要在指令中写出。 当指令含有操作数,并要求在指令中显式地写出来时,则在书写时必须遵守: 指令助忆符和操作数之间要有分隔符,分隔符可以是若干个空格或 TAB 键; 如果指令含有多个操作数,那么,操作数之间要用逗号","分开。 指令后面还可以书写注释内容,不过,要在注释之前书写分号";"。 5.1.2了解指令的几个方面 在学习汇编指令时,指令的功能无疑是我们学习和掌握的重点,但要准确、有效地运用 这些指令,我们还要熟悉系统对每条指令的一些规定或约束。 归纳起来,对指令还要掌握以下几个方面内容: 、要求指令操作数的寻址方式; 、指令对标志位的影响、标志位对指令的影响; 、指令的执行时间,对可完成同样功能的指令,要选用执行时间短的指令(见附 录2)。 5.2、指令系统 指令系统是 CPU 指令的集合,CPU 除了具有计算功能的指令外,还有实现其它功能的 指令,也有为某种特殊的应用而增设的指令。 通常,把指令按其功能分成以下几大类: 数据传送指令 标志位操作指令 算术运算指令 逻辑运算指令 移位操作指令 位操作指令 比较运算指令 下面,我们逐一介绍每类指令中 的指令。 ƒ 循环指令 ƒ 转移指令 ƒ 条件设置字节指令 ƒ 字符串操作指令 ƒ ASCII-BCD 码运算调整指令 ƒ 处理器指令 5.2.1 数据传送指令 数据传送指令又分为:传送指令、交换指令、地址传送指令、堆栈操作指令、转换指令 和 I/O 指令等。 除了标志位操作指令 SAHF 和 POPF 指令外,本类的其它指令都不影响标志位。 1、传送指令 MOV(Move Instruction) 传送指令是使用最频繁的指令,它相对于高级语言里的赋值语句。指令的格式如下: MOV Reg/Mem, Reg/Mem/Imm 其中:Reg—Register(寄存器),Mem—Memory(存储器),Imm—Immediate(立即数),它 们可以是8位、16位或32位(特别指出其位数的除外)。在本网络课件的网页中,都将采用上 述缩写,此后不再说明。 指令的功能是把源操作数(第二操作数)的值传给目的操作数(第一操作数)。指令执行后, 目的操作数的值被改变,而源操作数的值不变。在存储单元是该指令的一个操作数时,该操 作数的寻址方式可以是任意一种存储单元寻址方式。 下面列举几组正确的指令例子: 源操作数是寄存器 MOV CH, AL MOV BP, SP MOV ECX, EBX MOV DS, AX MOV [BX], CH MOV [BX+SI], AX 源操作数是存储单元 MOV AL, [100H] MOV BX, ES:[DI] MOV EDX, [BX] MOV BX, VARW MOV AX, [BX+SI] MOV CH, [BX+DI+100H] 其中:VA RW 是字类型内存变量(下同)。 源操作数是立即数 MOV AL, 89H MOV BX, -100H MOV EDX, 12345678H MOV VARW, 200H MOV [BX], 2345H MOV [BX+DI], 1234H 在汇编语言中,主要的数据传送方式如图5.1所示。虽然一条 MOV 指令能实现其中大 多数的数据传送方式,但也存在 MOV 指令不能实现的传送方式。 对 MOV 指令有以下几条具体规定,其中有些规定对其它指令也同样有效。 1)、两个操作数的数据类型要相同,要同为8位、16位或32位;如:MOV BL, AX 等是 不正确的; 2)、两个操作数不能同时为段寄存器,如:MOV ES, DS 等; 3)、代码段寄存器 CS 不能为目的操作数,但可作为源操作数,如:指令 MOV CS, AX 等不正确,但指令 MOV AX, CS 等是正确的; 4)、立即数不能直接传给段寄存器,如:MOV DS, 100H 等; 5)、立即数不能作为目的操作数,如:MOV 100H, AX 等; 6)、指令指针 IP,不能作为 MOV 指令的操作数; 7)、两个操作数不能同时为存储单元,如:M O V VA R A , VAR B 等,其中 VA R A 和 VA R B 是同数据类型的内存变量。 对于规定2、4和7,我们可以用通用寄存器作为中转来达到最终目的。表5.1列举一个可 行的解决方案,尽供参考。读者可考虑用其它办法来完成同样的功能。 表5.1 MOV 指令的变通方法 功能描述 不正确的指令 可选的解决方法 把 DS 的值传送给 ES MOV ES, DS MOV AX, DS MOV ES, AX 把100H 传给 DS MOV DS, 100H MOV AX, 100H MOV DS, AX 把字变量 VA R B 的值传送 给字变量 VA R A MOV VARA, VARB MOV AX, VARB MOV VARA, AX 对于情况1:不同位数数据之间的传送问题,在80386及其以后的 CPU 中,增加一组新 的指令——传送-填充指令,它可把位数少的源操作数传送给位数多的目的操作数,多出的 部分按指令的规定进行填充。 2、传送—填充指令(Move-and-Fill Instruction) 传送—填充指令是把位数短的源操作数传送给位数长的目的操作数。指令格式如下: MOVSX/MOVZX Reg/Mem, Reg/Mem/Imm ;80386+ 其中:80386+表示80386及其之后的 CPU,其它类似符号含义类同,不再说明。 指令的主要功能和限制与 MOV 指令类似,不同之处是:在传送时,对目的操作数的高 位进行填充。根据其填充方式,又分为:符号填充和零填充。 传送—填充指令的功能如图5.2所示。 、符号填充指令 MOVSX(Move with Sign-Extend) MOVSX 的填充方式是:用源操作数的符号位来填充目的操作数的高位数据位。 、零填充指令 MOVZX(Move with Zero-Extend) MOVZX 的填充方式是:恒用0来填充目的操作数的高位数据位。 例5.1 已知:AL=87H,指令 MOVSX CX, AL,MOVZX DX, AL 执行后,问 CX 和 DX 的值是什么? 解:根据传送-填充指令的填充方式可知: 指令 MOVSX CX, AL 执行后,(CX)=0FF87H,指令 MOVZX DX, AL 执行后, (DX)=0087H。 从上例可看出,两条指令的源操作数完全一样,但因为它们的填充方式不同,所得到的 结果而就不同。 试比较下列指令,分析它们执行结果的相同和不同之处: MOV AX, 87H MOVSX AX, 87H MOVZX AX, 87H 3、交换指令 XCHG(Exchange Instruction) 交换指令 XCHG 是两个寄存器,寄存器和内存变 量之间内容的交换指令,两个操作数的数据类型要相 同。其指令格式如下: XCHG Reg/Mem, Reg/Mem 该指令的功能和 MOV 指令不同,后者是一个操作 数的内容被修改,而前者是两个操作数都会发生改变。 寄存器不能是段寄存器,两个操作数也不能同时为内存 变量。 XCHG 指令的功能如图5.3所示。 例5.2 已知:AX=5678H,BX=1234H,指令 XCHG AX, BX 执行后,AX 和 BX 的值是什 么? 解:这是两个寄存器内容进行交换,指令执行后,有:(AX)=1234H,(BX)=5678H。 4、取有效地址指令 LEA(Load Effective Address) 指令 LEA 是把一个内存变量的有效地址送给指定的寄存 器。其指令格式如下: LEA Reg, Mem 该指令通常用来对指针或变址寄存器 BX、DI 或 SI 等置初 值之用。其功能如右图所示。 例如: … BUFF ER DB 100 DUP(?) … LEA BX, BUFFER ;把字节变量 BUFFER 在数据段内的偏移量送给 BX … 问题:指令“LEA BX BUFFER”和“MOV BX, OFFSET BUFFER”的执行效果是一样的 吗?指令“LEA BX,[BX+200]”和“MOV BX,OFFSET [BX+200]”二者都正确吗? 5、取段寄存器指令(Load Segment Instruction) 该组指令的功能是把内存单元的一个“低字”传送给指令中指定的16位寄存器,把随后的 一个“高字”传给相应的段寄存器(DS、ES、FS、GS 和 SS)。其指令格式如下: LDS/LES/LFS/LGS/LSS Reg, Mem 指令 LDS(Load Data Segment Register)和 LES(Load Extra Segment Register)在8086CPU 中就存在,而 LFS 和 LGS(Load Extra Segment Register)、LSS(Load Stack Segment Register) 是80386及其以后 CPU 中才有的指令。 若 Reg 是16位寄存器,那么,Men 必须是32位指针;若 Reg 是32位寄存器,那么,Men 必须是48位指针,其低32位给指令中指定的寄存器,高16位给指令中的段寄存器。指令的执 行结果如图5.5所示。 例如: … POINTER DD 12345678H … LDS BX, POINTER … 指令的执行结果如图5.5所示。各寄存器的内容分别为:(BX)=5678H,(DS)=1234H。 下面控件是学习和掌握 MOV、MOVSX/MOVZX、XCHG、LEA、LDS/LES/LFS/LGS/LSS 指令的,它可检查用户输入这些指令的合法性,并对合法的指令显示其执行的结果。 注意:如果指令中含有表示内存单元的寻址方式,那么其控件中的"内存单元的类型" 即表示该指令中内存单元的数据类型。 6、堆栈操作指令(Stack Operation Instruction) 堆栈是一个重要的数据结构,它具有“先进后出”的特点,通常用来保存程序的返回地址。 它主要有两大类操作:进栈操作和出栈操作。 1)、进栈操作 、PUSH(Push Word or Doubleword onto Stack) 指令格式:PUSH Reg/Mem PUSH Imm ;80286+ 一个字进栈,系统自动完成两步操作:SP←SP-2,(SP)←操作数; 一个双字进栈,系统自动完成两步操作:ESP←ESP-4,(ESP)←操作数。 、PUSHA(Push All General Registers) 指令格式:PUSHA ;80286+ 其功能是依次把寄存器 AX、CX、DX、BX、SP、BP、SI 和 DI 等压栈。 、PUSHAD(Push All 32-bit General Registers) 指令格式:PUSHAD ;80386+ 其功能是把寄存器 EAX、ECX、EDX、EBX、ESP、EBP、ESI 和 EDI 等压栈。 2)、出栈操作 、POP(Pop Word or Doubleword off Stack) 指令格式:POP Reg/Mem 弹出一个字,系统自动完成两步操作:操作数←(SP),SP←SP-2; 弹出一个双字,系统自动完成两步操作:操作数←(ESP),ESP←ESP-4。 、POPA(Pop All General Registers) 指令格式:POPA ;80286+ 其功能是依次把寄存器 DI、SI、BP、SP、BX、DX、CX 和 AX 等弹出栈。其实, 程序员不用记住它们的具体顺序,只要与指令 PUSHA 对称使用就可以了。 、POPAD(Pop All 32-bit General Registers) 指令格式:POPAD ;80386+ 其功能是依次把寄存器 EDI、ESI、EBP、ESP、EBX、EDX、ECX 和 EAX 等弹出 栈,它与 PUSHAD 对称使用即可。 7、转换指令 XLAT(Translate Instruction) 转换指令有两个隐含操作数 BX 和 AL。指令格式如 下: XLAT/XLATB 其功能是把 BX 的值作为内存字节数组首地址、下 标为 AL 的数组元素的值传送给 AL。其功能描述的表达 式是:AL←BX[AL],其功能示意图如图5.6所示。 8、I/O 指令 有关 I/O 指令将在第8.1.2节——I/O 指令——中介绍,在此从略。 5.2.2 标志位操作指令 标志位操作指令是一组对标志位置位、复位、保存和恢复等操作的指令。 1、进位 CF 操作指令 、清进位指令 CLC(Clear Carry Flag):CF←0 、置进位指令 STC(Set Carry Flag):CF←1 、进位取反指令 CMC(Complement Carry Flag):CF←not CF 2、方向位 DF 操作指令 、清方向位指令 CLD(Clear Direction Flag):DF←0 、置方向位指令 STD(Set Direction Flag):DF←1 3、中断允许位 IF 操作指令 、清中断允许位指令 CLI(Clear Interrupt Flag):IF←0 其功能是不允许可屏蔽的外部中断来中断其后程序段的执行。 、置中断允许位指令 STI(Set Interrupt Flag):IF←1 其功能是恢复可屏蔽的外部中断的中断响应功能,通常是与 CLI 成对使用的。 4、取标志位操作指令 、LAHF(Load AH from Flags):AH←Flags 的低8位 、SAHF(Store AH in Flags):Flags 的低8位←AH 5、标志位堆栈操作指令 、PUSHF/PUSHFD(Push Flags onto Stack):把16位/32位标志寄存器进栈; 、POPF/POPFD(Pop Flags off Stack):把16位/32位标志寄存器出栈; 6、逻辑操作指令的小结 下面是学习标志位指令的控件,浏览者可以运用此类指令,观看标志寄存器的相应变化。 5.2.3 算术运算指令 算术运算指令是反映 CPU 计算能力的一组指令,也是编程时经常使用的一组指令。它 包括:加、减、乘、除及其相关的辅助指令。 该组指令的操作数可以是8位、16位和32位(80386+)。当存储单元是该类指令的操作数 时,该操作数的寻址方式可以是任意一种存储单元寻址方式。 1、加法指令 、加法指令 ADD(ADD Binary Numbers Instruction) 指令的格式:ADD Reg/Mem, Reg/Mem/Imm 受影响的标志位:AF、CF、OF、PF、SF 和 ZF 指令的功能是把源操作数的值加到目的操作数中。 、带进位加指令 ADC(ADD With Carry Instruction) 指令的格式:ADC Reg/Mem, Reg/Mem/Imm 受影响的标志位:AF、CF、OF、PF、SF 和 ZF 指令的功能是把源操作数和进位标志位 CF 的值(0/1)一起加到目的操作数中。 、加1指令 INC(Increment by 1 Instruction) 指令的格式:INC Reg/Mem 受影响的标志位:AF、OF、PF、SF 和 ZF,不影响 CF 指令的功能是把操作数的值加1。 、交换加指令 XADD(Exchange and Add) 指令的格式:XADD Reg/Mem, Reg ;80486+ 受影响的标志位:AF、CF、OF、PF、SF 和 ZF 指令的功能是先交换两个操作数的值,再进行算术“加”法操作。 例5.3 已知有二个32位数 d1和 d2(用数据类型 DD 说明),编写程序片段把 d2的值加到 d1中。 解:32位数 d1和 d2在内存中如下所示。 … … … … …… 方法1:用16位寄存器编写程序 MO V AX, word ptr d1 ;由于 d1是双字类型,必须使用强制类型说明符。以下同。 MO V DX, word ptr d1+2 ;(DX,AX)构成一个32位数据 AD D AX, word ptr d2 ;低字相加 AD C DX, word ptr d2+2 ;高字相加。在低字相加时,有可能会产生“进位” MO V word ptr d1, AX ;低字送给 d1的低字 MO V word ptr d1+2, DX ;高字送给 d1的高字 方法2:用32位寄存器编写程序 MO V EAX, d1 AD D EAX, d2 MO V d1, EAX 从上面两段程序不难看出:用32位寄存器来处理32位数据显得简单、明了,而16位微机 虽然也能处理32位数据,但做起来就要复杂一些。 下面是学习和掌握加法类指令的控件,可模拟执行 ADD、ADC、INC、XADD、CLC、 STC 和 CMC 等指令。用鼠标左键单击寄存器列表框中指定的寄存器,则可修改其值。后面 其它控件的有关操作与此相一致,不再说明。 2、减法指令 、减法指令 SUB(Subtract Binary Values Instruction) 指令的格式:SUB Reg/Mem, Reg/Mem/Imm 受影响的标志位:AF、CF、OF、PF、SF 和 ZF 指令的功能是从目的操作数中减去源操作数。 、带借位减 SBB(Subtract with Borrow Instruction) 指令的格式:SBB Reg/Mem, Reg/Mem/Imm 受影响的标志位:AF、CF、OF、PF、SF 和 ZF 指令的功能是把源操作数和标志位 CF 的值从目的操作数中一起减去。 、减1指令 DEC(Decrement by 1 Instruction) 指令的格式:DEC Reg/Mem 受影响的标志位:AF、OF、PF、SF 和 ZF,不影响 CF 指令的功能是把操作数的值减去1。 、求补指令 NEG(Negate Instruction) 指令的格式:NEG Reg/Mem 受影响的标志位:AF、CF、OF、PF、SF 和 ZF 指令的功能:操作数=0-操作数,即改变操作数的正负号。 例5.4 已知有二个32位数 d1和 d2,编写程序片段从 d1中减去 d2的值。 解: 方法1:用16位寄存器编写程序 MO V AX, word ptr d1 ;取低字 MO V DX, word ptr d1+2 ;取高字,(DX,AX)构成一个32位数据 SUB AX, word ptr d2 ;低字相减 SBB DX, word ptr d2+2 ;高字相减。在低字相减时,有可能会产生“借位” MO V word ptr d1, AX ;低字送给 d1的低字 MO V word ptr d1+2, DX ;高字送给 d1的高字 方法2:用32位寄存器编写程序 MO V EAX, d1 SUB EAX, d2 MO V d1, EAX 下面是学习和掌握减法类指令的控件,可模拟执行 SUB、SBB、DEC、NEG、CLC、 STC 和 CMC 等指令。 3、乘法指令 计算机的乘法指令分为无符号乘法指令和有符号乘法指令,它们的唯一区别就在于:数 据的最高位是作为“数值”参与运算,还是作为“符号位”参与运算。 乘法指令的被乘数都是隐含操作数,乘数在指令中显式地写出来。CPU 会根据乘数是8 位、16位,还是32位操作数,来自动选用被乘数:AL、AX 或 EAX。 指令的功能是把显式操作数和隐含操作数相乘,并把乘积存入相应的寄存器中。 、无符号数乘法指令 MUL(Unsigned Multiply Instruction) 指令的格式:MUL Reg/Mem 受影响的标志位:CF 和 OF(AF、PF、SF 和 ZF 无定义) 指令的功能是把显式操作数和隐含操作数(都作为无符号数)相乘,所得的乘积按表 5.2的对应关系存放。 表5.2 乘法指令中乘数、被乘数和乘积的对应关系 乘数位数 隐含的被乘数 乘积的存放位置 举例 8位 AL AX MUL BL 16位 AX DX-AX MUL BX 32位 EAX EDX-EAX MUL ECX 、有符号数乘法指令 IMUL(Signed Integer Multiply Instruction) IMU L Reg/Mem IMU L Reg, Imm ;80286+ IMU L Reg, Reg, Imm ;80286+ 指令的格 式: IMU L Reg, Reg/Mem ;80386+ 受影响的标志位:CF 和 OF(AF、PF、SF 和 ZF 无定义) 1)、指令格式1——该指令的功能是把显式操作数和隐含操作数相乘,所得的乘积按表 5.2的对应关系存放。 2)、指令格式2——其寄存器必须是16位/32位通用寄存器,其计算方式为: Reg ← Reg × Imm 3)、指令格式3——其寄存器只能是16位通用寄存器,其计算方式为: Reg1 ← Reg2×Imm 或 Reg1 ← Mem×Imm 4)、指令格式4——其寄存器必须是16位/32位通用寄存器,其计算方式为: Reg1 ← Reg1×Reg2 或 Reg1 ← Reg1×Mem 在指令格式2~4中,各操作数的位数要一致。如果乘积超过目标寄存器所能存储的范围, 则系统将置溢出标志 OF 为1。 下面是学习和掌握乘法类指令的控件,可模拟执行 MUL 和 IMUL 等指令。 4、除法指令 除法指令的被除数是隐含操作数,除数在指令中显式地写出来。CPU 会根据除数是8位、 16位,还是32位,来自动选用被除数 AX、DX-AX,还是 EDX-EAX。 除法指令功能是用显式操作数去除隐含操作数,可得到商和余数。当除数为0,或商超 出数据类型所能表示的范围时,系统会自动产生0号中断。 、无符号数除法指令 DIV(Unsigned Divide Instruction) 指令的格式:DIV Reg/Mem 指令的功能是用显式操作数去除隐含操作数(都作为无符号数),所得商和余数按表 5.3的对应关系存放。指令对标志位的影响无定义。 、有符号数除法指令 IDIV(Signed Integer Divide Instruction) 指令的格式:IDIV Reg/Mem 受影响的标志位:AF、CF、OF、PF、SF 和 ZF 指令的功能是用显式操作数去除隐含操作数(都作为有符号数),所得商和余数的对应关 系见表5.3。 表5.3 除法指令除数、被除数、商和余数的对应关系 除数位数 隐含的被除数 商 余数 举例 8位 AX AL AH DIV BH 16位 DX-AX AX DX DIV BX 32位 EDX-EAX EAX EDX DIV ECX 5、类型转换指令 在作有符号除法时,有时需要把短位数的被除数转换成位数更长的数据类型。比如,要 用 BL 中的数据去除 AL,但根据除法指令的规定:除数是8位,则被除数必须是 AX,于是 就涉及到 AH 的取值问题。 为了方便说明,假设:(AH)=1H,(AL)=90H=-112D,(BL)=10H。 1)、在作除法运算前,必须处理 AH 的原有内容 假设在作除法时,不管 AH 中的值,这时,(AH、AL)/BL 的商是19H,但我们知道: AL/BL 的商应是-7,这就导致:计算结果不是所预期的结果,所以,在作除法运算前,程序 员必须要处理 AH 中的值。 2)、作无符号数除法时 可强置 AH 的值为0,于是,可得到正确的结果。 3)、作有符号数除法时 如果强置 AH 为0,则 AX=0090H,这时,AX/BL 的商为9,显然结果也不正确。 如果把 AL 的符号位1,扩展到 AH 中,得:AX=0FF90H=-112D,这时,AX/BL 的商就 是我们所要的正确结果。 综上所述,因为在进行有符号数除法时存在隐含操作数数据类型转换的问题,所以,系 统提供了四条数据类型转换指令:CBW、CWD、CWDE 和 CDQ。 、字节转换为字指令 CBW(Convent Byte to Word) 指令的格式:CBW 该指令的隐含操作数为 AH 和 AL。其功能是用 AL 的符号位去填充 AH,即:当 AL 为正数,则 AH=0,否则,AH=0FFH。 指令的执行不影响任何标志位。 、字转换为双字指令 CWD(Convent Word to Doubleword) 指令的格式:CWD 该指令的隐含操作数为 DX 和 AX,其功能是用 AX 的符号位去填充 DX。指令的 执行不影响任何标志位。 、字转换为扩展的双字指令 CWDE(Convent Word to Extended Doubleword) 指令的格式:CWDE ;80386+ 该指令的隐含操作数为 DX 和 AX,其功能是用 AX 的符号位填充 EAX 的高字位。 指令的执行不影响任何标志位。 、双字转换为四字指令 CDQ(Convent Doubleword to Quadword) 指令的格式:CDQ ;80386+ 该指令的隐含操作数为 EDX 和 EAX,指令的功能是用 EAX 的符号位填充 EDX。 指令的执行不影响任何标志位。 下面是学习和掌握除法类指令的控件,可模拟执行 DIV、IDIV、CBW、CWD、CWDE 和 CDQ 等指令。 例5.5 编写程序段,完成下面计算公式,并把所得的商和余数分别存入 X 和 Y 中(其中:A, B,C,X 和 Y 都是有符号的字变量)。 (C - 120 + A*B) / C 解: … A DW ? B DW ? C DW ? X DW ? Y DW ? … MO V AX, C SUB AX, 120D ;书写指令“ADD AX, -120D”也可以 CWD MO V CX, DX MO V BX, AX ;(CX, BX)←(DX, AX),调度寄存器,为作乘法准备必要的寄存器 MO V AX, A IMU L B ;(DX, AX)←A*B AD D AX, BX ;计算32位二进制之和,为作除法作准备 AD C DX, CX IDI V C ;AX 是商,DX 是余数 MO X, AX ;分别保存商和余数到指定的字变量单元里 V MO V Y, DX … 汇编入门(8讲) 时间:2009-5-16 8:19:24 核心提示:5.2.4 逻辑运算指令逻辑运算指令是另一组重要的指令,它包括:逻辑与 (AND)、逻辑或(OR)、逻辑非(NOT)和异或指令(XOR),逻辑运算指令也是经常 使用的指令。 1 、逻辑与操作指令 AND(Logical AND Instruction) 指令的格式:AND Reg/Mem, Reg/Mem/Im... 5.2.4 逻辑运算指令 逻辑运算指令是另一组重要的指令,它包括:逻辑与(AND)、逻辑或(OR)、逻辑非(NOT) 和异或指令(XOR),逻辑运算指令也是经常使用的指令。 1、逻辑与操作指令 AND(Logical AND Instruction) 指令的格式:AND Reg/Mem, Reg/Mem/Imm 受影响的标志位:CF(0)、OF(0)、PF、SF 和 ZF(AF 无定义) 指令的功能是把源操作数中的每位二进制与目的操作数中的相应二进制进行逻辑 “与操作”,操作结果存入目标操作数中。 例5.6 已知(BH)=67H,要求把其的第0、1和5位置为0。 解:可以构造一个立即数,其第0、1和5位的值为0,其它位的值 为1,该立即数即为:0DCH 或11011100B,然后用指令"AND BH, 0DCH"来实现此功能。 其计算过程如右图所示。 2、逻辑或操作指令 OR(Logical OR Instruction) 指令的格式:OR Reg/Mem, Reg/Mem/Imm 受影响的标志位:CF(0)、OF(0)、PF、SF 和 ZF(AF 无定义) 指令的功能是把源操作数中的每位二进制与目的操作数中的相应二进制进行逻辑" 或操作",操作结果存入目标操作数中。 例5.7 已知(BL)=46H,要求把其的第1、3、4和6位置为1。 解:构造一个立即数,使其第1、3、4和6位的值为1,其它位的 值为0,该立即数即为:5AH 或01011010B,然后用指令"OR BL, 5AH"来实现此功能。 其计算过程如右图所示。 3、逻辑非操作指令 NOT(Logical NOT Instruction) 指令的格式:NOT Reg/Mem 其功能是把操作数中的每位变反,即:1←0,0←1。指令的执行不影响任何标志位。 例5.8 已知(AL)=46H,执行指令“NOT AL”后,AL 的值是什么? 解:执行该指令后,(AL)=0B9H。其计算过程如下所示。 4、逻辑异或操作指令 XOR(Exclusive OR Instruction) 指令的格式:XOR Reg/Mem, Reg/Mem/Imm 受影响的标志位:CF(0)、OF(0)、PF、SF 和 ZF(AF 无定义) 指令的功能是把源操作数中的每位二进制与目的操作数中的相应二进制进行逻辑" 异或操作",操作结果存入目标操作数中。 例5.9 已知(AH)=46H,要求把其的第0、2、5和7位的二进制值变反。 解:构造一个立即数,使其第0、2、5和7位的值为1,其它位的 值为0,该立即数即为:0A5H 或10100101B,然后再用指令 "XOR AH, 0A5H"来实现此功能。 其计算过程如右图所示。 5、逻辑操作指令的小结 下面是学习和掌握逻辑类指令的控件,可模拟执行 AND、OR、NOT 和 XOR 等指令。 5.2.5 移位操作指令 移位操作指令是一组经常使用的指令,它包括算术移位、逻辑移位、双精度移位、循环 移位和带进位的循环移位等五大类。 移位指令都有指定移动二进制位数的操作数,该操作数可以是立即数或CL的值。在8086 中,该立即数只能为1,但在其后的 CPU 中,该立即数可以是1··31之内的数。 1、算术移位指令 算术移位指令有:算术左移 SAL(Shift Algebraic Left)和算术右移 SAR(Shift Algebraic Right)。它们的指令格式如下: SAL/SAR Reg/Mem, CL/Imm 受影响的标志位:CF、OF、PF、SF 和 ZF(AF 无定义)。 算术移位指令的功能描述如下,具体功能下图(a)、(b)所示。 算术左移 SAL 把目的操作数的低位向高位移,空出的低位补0; 算术右移 SAR 把目的操作数的高位向低位移,空出的高位用最高位(符号位)填补。 例5.10 已知(AH)=12H,(BL)=0A9H,试给出分别用算术左移和右移指令移动1位后,寄存 器 AH 和 BL 的内容。 解:用算术左移和右移指令移动1位后,寄存器 AH 和 BL 的结果如下表所示。 操作数的初值 执行的指令 执行后操作数的内容 (AH)=12H SAL AH, 1 (AH)=24H (BL)=0A9H SAL BL, 1 (BL)=52H (AH)=12H SAR AH, 1 (AH)=09H (BL)=0A9H SAR BL, 1 (BL)=0D4H 下面是学习和理解算术移位指令的控件。它简单、直观地表达了该移位指令的功能,通 过它,学习者可准确地掌握计算机系统中该移位指令的含义。 在该控件中,操作者可随机生成第一操作数,也可自行输入之。为了便于比较,在执行 指令前,把原操作数的内容存入“操作前的数据”中。 思考题:下面有两组指令序列,问每组指令执行后,寄存器 AX 的不会变化吗? SAL AX, 1 SAR AX, 1 或 SAR AX, 1 SAL AX, 1 2、逻辑移位指令 此组指令有:逻辑左移 SHL(Shift Logical Left)和逻辑右移 SHR(Shift Logical Right)。它 们的指令格式如下: SHL/SHR Reg/Mem, CL/Imm 受影响的标志位:CF、OF、PF、SF 和 ZF(AF 无定义)。 逻辑左移/右移指令只有它们的移位方向不同,移位后空出的位都补0。它们的具体功能 下图(a)、(b)所示。 例5.11 已知(AH)=12H,(BL)=0A9H,试给出分别用逻辑左移和右移指令移动1位后,寄存 器 AH 和 BL 的内容。 解:用算术左移和右移指令移动1位后,寄存器 AH 和 BL 的结果如下表所示。 操作数的初值 执行的指令 执行后操作数的内 容 (AH)=12H SHL AH, 1 (AH)=24H (BL)=0A9H SHL BL, 1 (BL)=52H (AH)=12H SHR AH, 1 (AH)=09H (BL)=0A9H SHR BL, 1 (BL)=54H 学习和理解逻辑移位指令的控件。 3、双精度移位指令 此组指令有:双精度左移 SHLD(Shift Left Double)和双精度右移 SHRD(Shift Right Double)。它们都是具有三个操作数的指令,其指令的格式如下: SHLD/SHRD Reg/Mem, Reg, CL/Imm ;80386+ 其中:第一操作数是一个16位/32位的寄存器或存储单元;第二操作数(与前者具有相同 位数)一定是寄存器;第三操作数是移动的位数,它可由 CL 或一个立即数来确定。 在执行 SHLD 指令时,第一操作数向左移 n 位,其“空出”的低位由第二操作数的高 n 位来填补,但第二操作数自己不移动、不改变。 在执行 SHRD 指令时,第一操作数向右移 n 位,其“空出”的高位由第二操作数的低 n 位来填补,但第二操作数自己也不移动、不改变。 SHLD 和 SHRD 指令的移位功能示意图如图5.8所示。 受影响的标志位:CF、OF、PF、SF 和 ZF(AF 无定义) 下面是几个双精度移位的例子及其执行结果。 双精度移位指令 指令操作数的初值 指令执行后的结果 SHLD AX, BX, 1 (AX)=1234H,(BX)=8765H (AX)=2469H SHLD AX, BX, 3 (AX)=1234H,(BX)=8765H (AX)=91A4H SHRD AX, BX, 2 (AX)=1234H,(BX)=8765H (AX)=448DH SHRD AX, BX, 4 (AX)=1234H,(BX)=8765H (AX)=5123H 学习和理解双精度移位指令的控件。 4、循环移位指令 循环移位指令有:循环左移 ROL(Rotate Left)和循环右移 ROR(Rotate Right)。 指令的格式:ROL/ROR Reg/Mem, CL/Imm 受影响的标志位:CF 和 OF 循环左移/右移指令只是移位方向不同,它们移出的位不仅要进入 CF,而且还要填补空 出的位。具体功能如下图(a)、(b)所示。 下面是几个循环移位的例子及其执行结果。 循环移位指令 指令操作数的初值 指令执行后的结果 ROL AX, 1 (AX)=6789H (AX)=0CF12H ROL AX, 3 (AX)=6789H (AX)=3C4BH ROR AX, 2 (AX)=6789H (AX)=59E2H ROR AX, 4 (AX)=6789H (AX)=9678H 学习和理解不带进位的循环移位指令的控件。 5、带进位的循环移位指令 带进位的循环移位指令有:带进位的循环左移 RCL(Rotate Left Through Carry)和带进位 的循环右移 RCR(Rotate Right)。 指令的格式:RCL/RCR Reg/Mem, CL/Imm 受影响的标志位:CF 和 OF 带进位的循环左移/右移指令只有移位的方向不同,它们都用原 CF 的值填补空出的位, 移出的位再进入 CF。具体功能如下图(a)、(b)所示。 下面是几个带进位循环移位的例子及其执行结果。 双精度移动指令 指令操作数的初值 指令执行后的结 果 RCL AX, 1 CF=0,(AX)=0ABCDH (AX)=579AH RCL AX, 1 CF=1,(AX)=0ABCDH (AX)=579BH RCR AX, 2 CF=0,(AX)=0ABCDH (AX)=AAF3H RCR AX, 2 CF=1,(AX)=0ABCDH (AX)=EAF3H 例5.12 编写指令序列把由 DX 和 AX 组成的32位二进制算术左移、循环左移1位。 解: (DX,AX)算术左移1位指令序列 (DX,AX)循环左移1位指令序列 SHL AX, 1 RCL DX, 1 SHLD DX, AX, 1 RCL AX, 1 学习和理解带进位的循环移位指令的控件。 6、移位指令的学习和理解 下面的控件是把各种移位指令的模拟功能组合在一起。它不仅简单、直观地表达了各种 移位指令的功能,而且能通过对照,理解它们之间的区别。该控件还结合了其它指令(CLC、 STC 和 CMC)来增加指令的组合变化。 5.2.6 位操作指令 1、位扫描指令(Bit Scan Instruction) 指令的格式:BSF/BSR Reg, Reg/Mem ;80386+ 受影响的标志位:ZF 位扫描指令是在第二个操作数中找第一个“1”的位置。如果找到,则该“1”的位置保存在 第一操作数中,并置标志位 ZF 为1,否则,置标志位 ZF 为0。 根据位扫描的方向不同,指令分二种:正向扫描指令和逆向扫描指令。 、正向扫描指令 BSF(Bit Scan Forward)从右向左扫描,即:从低位向高位扫描; 、逆向扫描指令 BSR(Bit Scan Reverse)从左向右扫描,即:从高位向低位扫描。 例如: MOV AX, 1234H BSF CX, AX ;指令执行后,(CX)=2 BSR CX, AX ;指令执行后,(CX)=12 2、位检测指令(Bit Test Instruction) 指令的格式:BT/BTC/BTR/BTS Reg/Mem, Reg/Imm ;80386+ 受影响的标志位:CF 位检测指令是把第一个操作数中某一位的值传送 给标志位 CF,具体的哪一位由指令的第二操作数来确 定。 根据指令中对具体位的处理不同,又分一下几种指 令: BT:把指定的位传送给 CF; BTC:把指定的位传送给 CF 后,还使该位变 反; BTR:把指定的位传送给 CF 后,还使该位变 为0; BTS:把指定的位传送给 CF 后,还使该位变 为1; 图5.11 位检测指令的功能示意图 例如:假设(AX)=1234H,分别执行下面指令。 BT AX, 2 ;指令执行后,CF=1,(AX)=1234h BTC AX, 6 ;指令执行后,CF=0,(AX)=1274h BTR AX, 10 ;指令执行后,CF=0,(AX)=1234h BTS AX, 14 ;指令执行后,CF=0,(AX)=5234h 3、检测位指令 TEST(Test Bits Instruction) 检测位指令是把二个操作数进行逻辑“与”操作,并根据运算结果设置相应的标志位,但 并不保存该运算结果,所以,不会改变指令中的操作数。在该指令后,通常用 JE、JNE、JZ 和 JNZ 等条件转移指令。 指令的格式:TEST Reg/Mem, Reg/Mem/Imm 受影响的标志位:CF(0)、OF(0)、PF、SF 和 ZF(AF 无定义) 例如: TEST AX, 1 ;测试 AX 的第0位 TEST CL, 10101B ;测试 CL 的第0、2、4位 下面是学习和掌握乘法类指令的控件,可模拟执行 BSF、BSR、BT、BTC、BTR、BTS 和 TEST 等指令。 5.2.7 比较运算指令 在程序中,我们要时常根据某个变量或表达式的取值去执行不同指令,从而使程序表现 出有不同的功能。为了配合这样的操作,在 CPU 的指令系统中提供了各种不同的比较指令。 通过这些比较指令的执行来改变有关标志位,为进行条件转移提供依据。 1、比较指令 CMP(Compare Instruction) 指令的格式:CMP Reg/Mem, Reg/Mem/Imm 受影响的标志位:AF、CF、OF、PF、SF 和 ZF 指令的功能:用第二个操作数去减第一个操作数,并根据所得的差设置有关标志位, 为随后的条件转移指令提供条件。但并不保存该差,所以,不会改变 指令中的操作数。 2、比较交换指令(Compare And Exchange Instruction) 在数据传送类指令中,我们介绍了交换指令 XCHG,它不管二个操作数的值是什么, 都无条件地进行交换。而比较交换指令,是先进行比较,再根据比较的结果决定是否进行操 作数的交换操作。 比较交换指令的功能:当二个操作数相等时,置标志位 ZF 为1;否则,把第一操作数 的值赋给第二操作数,并置标志位 ZF 为0。 、8位/16位/32位比较交换指令 指令的格式:CMPXCHG Reg/Mem, AL/AX/EAX ;80486+ 受影响的标志位:AF、CF、OF、PF、SF 和 ZF MASM 6.11中指令的描述与此不同,它没有限定第二操作数的要求。 、64位比较交换指令 该指令只有一个操作数,第二个操作数 EDX:EAX 是隐含的。 指令的格式:CMPXCHG8B Reg/Mem ;Pentium+ 受影响的标志位:ZF 例如:假设(AX)=1234H,(BX)=1234H,(CX)=4321H。 CMPXCHG BX, AX ;指令执行后,ZF=1 CMPXCHG CX, AX ;指令执行后,ZF=0,(AX)=4321H,CX 的值不变 3、字符串比较指令(Compare String Instruction) 参见后面第5.2.11节——字符串操作类指令——的叙述。 5.2.8 循环指令 循环结构是程序的三大结构之一。为了方便构成循环结构,汇编语言提供了多种循环指 令,这些循环指令的循环次数都是保存在计数器 CX 或 ECX 中。除了 CX 或 ECX 可以决定 循环是否结束外,有的循环指令还可由标志位 ZF 来决定是否结束循环。 在高级语言中,循环计数器可以递增,也可递减,但汇编语言中,CX 或 ECX 只能递 减,所以,循环计数器只能从大到小。在程序中,必须先把循环次数赋给循环计数器。 汇编语言的循环指令都是放在循环体的下面,在循环时,首先执行一次循环体,然后把 循环计数器 CX 或 ECX 减1。当循环终止条件达到满足时,该循环指令下面的指令将是下一 条被执行的指令,否则,程序将向上转到循环体的第一条指令。 在循环未终止,而向上转移时,规定:该转移只能是一个短转移,即偏移量不能超过128, 也就是说循环体中所有指令码的字节数之和不能超过128。如果循环体过大,可以用后面介 绍的“转移指令”来构造循环结构。 循环指令本身的执行不影响任何标志位。 1、循环指令(Loop Until Complete) 循环指令 LOOP 的一般格式: LOOP 标号 LOOPW 标号 ;CX 作为循环计 数器,80386+ LOOPD 标号 ;ECX 作为循环 计数器,80386+ 循环指令的功能描述: (CX)=(CX)-1或(ECX)=(ECX)-1; 如果(CX)≠0或(ECX)≠0,转向“标号”所指向的指 令,否则,终止循环,执行该指令下面的指令。 例5.13 编写一段程序,求1+2+…+1000之和,并把结果存入 AX 中。 解: 方法1:因为计数器 CX 只能递减,所以,可把求和式子改变为:1000+999+…+2+1。 … XO R AX, AX MO V CX, 1000D AD D AX, CX ;计算过程:1000+999+…+2+1 LOO P again again: … 方法2:不用循环计数器进行累加,求和式子仍为:1+2+…+999+1000。 … XO R AX, AX MO V CX, 1000D MO V BX, 1 AD D AX, BX ;计算过程:1+2+…+999+1000 INC BX LOO P again again: … 从程序段的效果来看:方法1要比方法2好。 2、相等或为零循环指令(Loop While Equal or Loop While Zero) 相等或为零循环指令的一般格式: LOOPE/LOOPZ 标号 LOOPEW/LOOPZW 标号 ;CX 作为循环 计数器,80386+ LOOPED/LOOPZD 标号 ;ECX 作为循 环计数器,80386+ 这是一组有条件循环指令,它们除了要受 CX 或 ECX 的影响外,还要受标志位 ZF 的影响。其具体规定 如下: (1)、(CX)=(CX)-1或(ECX)=(ECX)-1; (不改变 任何标志位) (2)、如果循环计数器≠0且 ZF=1,则程序转到循环 体的第一条指令,否则,程序将执行该循环指令下面的 指令。 3、不等或不为零循环指令(Loop While Not Equal or Loop While Not Zero) 不等或不为零循环指令的一般格式: LOOPNE/LOOPNZ 标号 LOOPNEW/LOOPNZW 标号 ;CX 作为循环 计数器,80386+ LOOPNED/LOOPNZD 标号 ;ECX 作为循 环计数器,80386+ 这也是一组有条件循环指令,它们与相等或为零循 环指令在循环结束条件上有点不同。其具体规定如下: (1)、(CX)=(CX)-1或(ECX)=(ECX)-1; (不改变任 何标志位) (2)、如果循环计数器≠0且 ZF=0,则程序转到循环体 的第一条指令,否则,程序将执行该循环指令下面的指令。 4、循环计数器为零转指令(Jump if CX/ECX is Zero) 在前面的各类循环 指令中,不管循环计数器的初值为何,循环体至少会被执行一次。 当循环计数器的初值为0时,通常的理解应是循环体被循环0次,即循环体一次也不被执行。 其实 不然,循环体不是不被执行,而是会被执行65536次(用 CX 计数)或4294967296次(几乎 是死循环,用 ECX 计数)。 为了解决指令的执行和常规思维之间差异,指令系统又提供了一条与循环计数器有关的 指令——循环计数器为零转指令。该指令一般用于循环的开始处,其指令格式如下: JCXZ 标号 ;当 CX=0时,则程序转移标号处执行 JECXZ 标号 ;当 ECX=0时,则程序转移标号处执行,80386+ 例5.14 编写一段程序,求1+2+…+k(K≥0)之和,并把结果存入 AX 中。 解: … K DB ? ;变量定义 … XOR AX, AX MOV CX, K JCXZ next ADD AX, CX ;计算过程: K+(K-1)+…+2+1 again: LOO P again next: … 思考题:假设变量 K 的值为0,并且在循环体的前面没有写指令“JCXZ next”,这时求出 的“和”AX 的值是什么? 5.2.9 转移指令 转移指令是汇编语 言程序员经常使用的一组指令。在高级语言中,时常有“尽量不要使 用转移语句”的劝告,但如果在汇编语言的程序中也尽量不用转移语句,那么该程序要么无 法编 写,要么没有多少功能,所以,在汇编语言中,不但要使用转移指令,而且还要灵活 运用,因为指令系统中有大量的转移指令。 转移指令分无条件转移指令和有条件转移指令两大类。 1、无条件转移指令(Transfer Unconditionally) 无条件转移指令包括:JMP、子程序的调用和返回指令、中断的调用和返回指令等。 下面只介绍无条件转移指令 JMP(Unconditional Jump)。 JMP 指令的一般形式: JMP 标号/Reg/Mem JMP 指令是从程序当前执行的地方无条件转移到另一个地方执行。这种转移可以是一 个短(short)转移(偏移量在[-128, 127]范围内),近(near)转移(偏移量在[-32K, 32K]范围内)或远 (far)转移(在不同的代码段之间转移)。 短和近转移是段内转移,JMP 指令只把目标指令位置的偏移量赋值指令指针寄存器 IP, 从而实现转移功能。但远转移是段间转移,JMP 指令不仅会改变指令指针寄存器 IP 的值, 而且还会改变代码段寄存器 CS 的值。 该转移指令的执行不影响任何标志位。 例如: … … JMP next1 ;向前转移,偏移量之差为负数 … JMP next2 ;向后转移,偏移量之差为正数 next1: … next2: … 在目前流行的汇编系统中,当段内转移时,有些软件把该转移指令默认为近转移,从而 使指令的偏移量用一个字来表示,于是生成3个字节的指令代码,但如果程序员自己清楚转 移的幅度在一个短转移的范围之内,那么,可用前置 short 的办法来告诉汇编程序,让它产 生2个字节的指令代码。 比如:如果程序员知道在上例中的标号 next2离“JMP next2”指令的偏移量不会超过127, 那么,可用下面的转移方式来省掉一个字节的指令代码。 … JMP short next2 ;生成2个字节的转移指令,从而节省一个字节 … next2: … 2、条件转移指令(Transfer Conditionally) 条件转移指令是一组极其重要的转移指令,它根据标志寄存器中的一个(或多个)标志位 来决定是否需要转移,这就为实现多功能程序提供了必要的手段。微机的指令系统提供了丰 富的条件转移指令来满足各种不同的转移需要,在编程序时,要对它们灵活运用。 条件转移指令又分三大类:基于无符号数的条件转移指令、基于有符号数的条件转移指 令和基于特殊算术标志位的条件转移指令。 、无符号数的条件转移指令(Jumps Based on Unsigned (Logic) Data) 指令的助忆符 检测的转移条件 功能描述 JE/JZ ZF=1 Jump Equal or Jump Zero JNE/JNZ ZF=0 Jump Not Equal or Jump Not Zero JA/JNBE CF=0 and ZF=0 Jump Above or Jump Not Below or Equal JAE/JNB CF=0 Jump Above or Equal or Jump Not Below JB/JNAE CF=1 Jump Below or Jump Not Above or Equal JBE/JNA CF=1 or AF=1 Jump Below or Equal or Jump Not Above 、有符号数的条件转移指令(Jumps Based on Signed (Arithmetic) Data) 指令的助忆符 检测的转移条件 功能描述 JE/JZ ZF=1 Jump Equal or Jump Zero JNE/JNZ ZF=0 Jump Not Equal or Jump Not Zero JG/JNLE ZF=0 and SF=OF Jump Greater or Jump Not Less or Equal JGE/JNL SF=OF Jump Greater or Equal or Jump Not Less JL/JNGE SF≠OF Jump Less or Jump Not Greater or Equal JLE/JNG ZF=1 or SF≠OF Jump Less or Equal or Jump Not Greater 、特殊算术标志位的条件转移指令(Jumps Based on Special Arithmetic Tests) 指令的助忆符 检测的转移条件 功能描述 JC CF=1 Jump Carry JNC CF=0 Jump Not Carry JO OF=1 Jump Overflow JNO OF=0 Jump Not Overflow JP/JPE PF=1 Jump Parity or Jump Parity Even JNP/JPO PF=0 Jump Not Parity or Jump Parity Odd JS SF=1 Jump Sign (negative) JNS SF=0 Jump No Sign (positive) 例5.15 编写一程序段,它把寄存器 AX-BX 的绝对值存入 BX 中。 解: … next: S U B BX, AX JN S next N E G BX … 例5.16 已知一个字节变量 char,试编写一程序段,把其所存的大写字母变成小写字母。 解: … cha r DB 'F' ;变量说明 … MO V AL, char CM P AL, 'A' JB next ;注意:字符是无符号数,不要使用指令 JL CM P AL, 'Z' JA next AD D char, 20H ;小写字母比大写字母的 ASCII 码大20H next: … 如果不知道(或忘了)大小写字母 ASCII 码之间的关系,那么,可用数值表达式'a'-'A'、 'b'-'B'、…、'z'-'Z'等来代替具体的数值20H。 例5.17 编写一段程序,完成下面计算公式,其中:变量 X 和 Y 都是字类型。 解: … X DW ? ;变量说明 Y DW ? … MO V AX, X MO BX, AX ;用 BX 来临时存放计算结果 V CM P AX, 0 JLE setdata CM P AX, 500 JG case3 AD D BX, 100D ;BX=X+100 JM P setdata next: SU B BX, 50D ;BX=X-50 MO V Y, B X ;把计算结果赋给变量 Y setda ta: … 例5.18 下面循环体的指令代码字节数超过128,试改写该循环。 … MOV CX, COUNT ;给循环计数器赋初值(>0) 循环体指令序列 ;循环体的首地址偏移量大于128 again: LOOP again 解: … MOV CX, COUNT 循环体指令序列 DEC CX again: JNZ again ;把 LOOP 指令改为条件转移指令 汇编入门(9讲) 时间:2009-5-16 8:30:04 核心提示:5.2.10 条件设置字节指令条件设置字节指令(Set Byte Conditionally)是80386 及其以后 CPU 所具有的一组指令。它们在测试条件方面与条件转移是一致的,但在功能方 面,它们不是转移,而是根 据测试条件的值来设置其字节操作数的内容为1或0。条件设置 字节指令的一般格式如下:SETnn... 5.2.10 条件设置字节指令 条件设置字节指令(Set Byte Conditionally)是80386及其以后 CPU 所具有的一组指令。它 们在测试条件方面与条件转移是一致的,但在功能方面,它们不是转移,而是根据测试条件 的值来设置其字节操作数的内容为1或0。 条件设置字节指令的一般格式如下: SETnn Reg/Mem ;80386+ 其中:nn 是表示测试条件的(见表5.4),操作数只能是8位寄存器或一个字节单元。 这组指令的执行不影响任何标志位。 表5.4 条件设置字节指令列表 指令的助忆符 操作数和检测条件之间的关系 SETZ/SETE Reg/Mem = ZF SETNZSETNE Reg/Mem = not ZF SETS Reg/Mem = SF SETNS Reg/Mem = not SF SETO Reg/Mem = OF SETNO Reg/Mem = not OF SETP/SETPE Reg/Mem = PF SETNP/SETPO Reg/Mem = not PF SETC/SETB/SETNAE Reg/Mem = CF SETNC/SETB/SETAE Reg/Mem = not CF SETNA/SETBE Reg/Mem = (CF or ZF) SETA/SETNBE Reg/Mem = not (CF or ZF) SETL/SETNGE Reg/Mem = (SF xor OF) SETNL/SETGE Reg/Mem = not (SF xor OF) SETLE/SETNG Reg/Mem = (SF xor OF) or ZF SETNLE/SETG Reg/Mem = not ((SF xor OF) or ZF) 例5.19 编写程序段:检测寄存器 EAX 的8个16进制中有几个0H,并把统计结果存入 BH 中。 解: 方法1:用条件转移指令来实现 XOR BH, BH MOV CX, 8 ;测试寄存器 EAX——8次 TEST AL, 0FH ;测试低四位二进制是否为0H again: JNZ next INC BH ROR EAX, 4 ;循环向右移四位,为测试高四位作准备 LOO P again 方法2:用条件设置字节指令来实现 XOR BH, BH next: MOV CX, 8 ;测试寄存器 EAX——8次 TEST AL, 0FH ;测试低四位二进制是否为0H SETZ BL ;如果 AL 的低四位是0,则 BL 置为1,否则,BL 为0 ADD BH, BL ROR EAX, 4 again: LOO P again 5.2.11 字符串操作指令 字符串操作指令的实质是对一片连续存储单元进行处理,这片存储单元是由隐含指针 DS:SI 或 ES:DI 来指定的。字符串操作指令可对内存单元按字节、字或双字进行处理,并能 根据操作对象的字节数使变址寄存器 SI(和 DI)增减1、2或4。具体规定如下: (1)、当 DF=0时,变址寄存器 SI(和 DI)增加1、2或4; (2)、当 DF=1时,变址寄存器 SI(和 DI)减少1、2或4。 在后面各指令中,有关变址寄存器都按上述规定进行增减,不再一一说明。 1、取字符串数据指令(Load String Instruction) 从由指针 DS:SI 所指向的内存单元开始,取一个字节、字或双字进入 AL、AX 或 EAX 中,并根据标志位 DF 对寄存器 SI 作相应增减。该指令的执行不影响任何标志位。 指令的格式:LODS 地址表达式 LODSB/LODSW LODSD ;8 0386+ 在指令 LODS 中,它会根据其地址表达式的属性来 决定读取一个字节、字或双字。即:当该地址表达式的 属性为字节、字或双字时,将从指针 DS:SI 处读一个字 节到 AL 中,或读一个字到 AX,或读一个双字到 EAX 中,与此同时,SI 还将分别增减1,2或4。 其它字符串指令中的“地址表达式”作用与此类似,将不再说明。 2、置字符串数据指令(Store String Instruction) 该指令是把寄存器 AL、AX 或 EAX 中的值存于 以指针 ES:DI 所指向内存单元为起始的一片存储单元 里,并根据标志位 DF 对寄存器 DI 作相应增减。该指 令不影响任何标志位。 指令的格式:STOS 地址表达式 STOSB/STOSW STOSD ;80386+ 3、字符串传送指令(Move String Instruction) 该指令是把指针 DS:SI 所指向的字节、字或双字传送给指针 ES:DI 所指向内存单元,并 根据标志位 DF 对寄存器 DI 和 SI 作相应增减。该指令的执行不影响任何标志位。 指令的格式:MOVS 地址表达式1, 地址表达式2 MOVSB/MOVSW MOVSD ;80386+ 4、输入字符串指令(Input String Instruction) 该指令是从某一指定的端口接受一个字符串,并存入一片存储单元之中。输入端口由 DX 指定,存储单元的首地址和读入数据的个数分别由 ES:DI 和 CX 来确定。在指令的执行 过程中,还根据标志位 DF 对寄存器 DI 作相应增减。该指令不影响任何标志位。 与指令有关的操作数 ES、DI、DX 和 CX 等都是隐含操作数。 指令的格式:INS 地址表达式 INSB/INSW INSD ;80286+ 5、输出字符串指令(Output String Instruction) 该指令是把一个字符串输入到指定的输出端口中。输出端口由 DX 指定,其输出数据的 首地址和个数分别由 DS:SI 和 CX 来确定。在指令的执行过程中,还根据标志位 DF 对寄存 器 SI 作相应增减。该指令的执行不影响任何标志位。 与指令有关的操作数 DS、SI、DX 和 CX 等都是隐含操作数。 指令的格式:OUTS 地址表达式 OUTSB/OUTSW OUTSD ;80286+ 6、字符串比较指令(Compare String Instruction) 该指令是把指针 DS:SI 和 ES:DI 所指向字节、字或 双字的值相减,并用所得到的差来设置有关的标志位。 与此同时,变址寄存器 SI 和 DI 也将根据标志位 DF 的 值作相应增减。 指令的格式:CMPS 地址表达式1, 地址表达式 2 CMPSB/CMPSW CMPSD ;80386+ 受影响的标志位:AF、CF、OF、PF、SF 和 ZF 7、字符串扫描指令(Scan String Instruction) 该指令是用指针 ES:DI 所指向字节、字或双字的值与相应的 AL、AX 或 EAX 的值相减, 用所得到的差来设置有关标志位。与此同时,变址寄存器 DI 还将根据标志位 DF 的值进行增 减。 指令的格式:SCAS 地址表达式1 SCASB/SCASW SCASD ;80386+ 受影响的标志位:AF、CF、OF、PF、SF 和 ZF 8、重复字符串操作指令(Repeat String Instruction) 前面介绍了七种不 同的字符串操作指令:取字符串数据、置字符串数据、字符串传送、 输入字符串、输出字符串、字符串比较和字符串扫描等指令,所叙述是这些指令执行一次所 具有 的功能。但我们知道:每个字符串通常会有多个字符的,所以,就需要重复执行这些 字符串操作指令。为了满足这种需求,指令系统提供了一组重复前缀指令。 虽然在这些字符串指令的前面都可以添加一个重复前缀指令,但由于指令执行结果的差 异,对某个具体的字符串指令又不用重复前缀指令而改用其它循环来实现重复的需要。 重复字符串操作指令对标志位的影响是由被重复的字符串操作指令来决定。 重复前缀指令 REP(Repeat String Instruction) 重复前缀指令是重复其后的字符串操作指令,重复的次数由 CX 来决定。其一般格式为: REP LODS/LODSB/LODSW/LODSD REP STOS/STOSB/STOSW/STOSD REP MOVS/MOVSB/MOVSW/MOVSD REP INS/ INSB/INSW/INSD REP OUTS/OUTSB/OUTSW/OUTSD 重复前缀指令的执行步骤如下: (1)、判断:CX=0; (2)、如果 CX=0,则结束重复操作,执行程序中的下一条指令; (3)、否则,CX=CX-1(不影响有关标志位),并执行其后的字符串操作指令,在该指 令执行完后,再转到步骤(1)。 从上面的重复前缀指令格式来看,虽然我们可以使用重复取字符串数据指令(第一组指 令),但可能会因为指令的执行结果而在程序中几乎不被使用。 例5.20 编写一段程序,计算字符串“12345abcdefgh”中字符的 ASCII 之和。 解: … M ES S DB '12345abcdefgh' ;在数据段中进行变量说明 … M O V AX, SEG MESS M O V DS, AX LE A SI, MESS ;用 DS:SI 来指向字符串的首地址 M O V CX, 13D ;重复次数 X OR BX, BX ;置求和的初值为0 RE P LODSB … 虽然指令“REP LODSB”能从字符串中取出每个字符,但它是在一条指令中完成的,程 序的其它指令根本无法处理每次取出的数据,指令的执行结果是:AL 只保存最后一次所取 出的字符'h'的 ASCII 码。 所以,为了实现本例的要求,不能使用重复前缀指令,而要把指令“REP LODSB”改写 成如下四条指令: X OR AH, AH ;为后面的累加作准备 LODSB A D D BX, AX ;AL 是被取出的字符,AH 已被清0 again: LO OP again 条件重复前缀指令(Repeat String Conditionally) 条件重复前缀指令与前面的重复前缀指令功能相类似,所不同的是:其重复次数不仅由 CX 来决定,而且还会由标志位 ZF 来决定。根据 ZF 所起的作用又分为二种:相等重复前缀 指令 REPE/REPZ 和不等重复前缀指令 REPE/REPZ。 A、相等重复前缀指令的一般格式为: REPE/REPZ SCAS/SCASB/SCASW/SCASD REPE/REPZ CMPS/CMPSB/CMPSW/CMPSD 该重复前缀指令的执行步骤如下: (1)、判断条件:CX≠0 且 ZF=1; (2)、如果条件不成立,则结束重复操作,执行程序中的下一条指令; (3)、否则,CX=CX-1(不影响有关标志位),并执行其后的字符串操作指令,在该指 令执行完后,再转到步骤(1)。 B、不等重复前缀指令的一般格式为: REPNE/REPNZ SCAS/SCASB/SCASW/SCASD REPNE/REPNZ CMPS/CMPSB/CMPSW/CMPSD 该重复前缀指令的执行步骤如下: (1)、判断条件:CX≠0 且 ZF=0; (2)、如果条件不成立,则结束重复操作,执行程序中的下一条指令; (3)、否则,CX=CX-1(不影响有关标志位),并执行其后的字符串操作指令,在该指 令执行完后,再转到步骤(1)。 5.2.12 ASCII--BCD 码运算调整指令 前面介绍的算术运算指令都是针对二进制数进行操作的指令,但对绝大多数人来说,十 进制是最简单、熟悉的。为了方便按十进制数进行算术运算,指令系统专门提供了一组十进 制运算调整指令。 虽然人们会觉得按十进制进行算术运算很自然,但计算机要化更多的时间来完成相应操 作。在通常情况下,这组指令很少被程序员运用在实际的程序之中。所以,这组指令的使用 率较低,可以根据需要有选择性地学习。 1、ASCII 码加调整指令 AAA(Ascii Adjust After Addition) 该指令是用于调整 AL 之值,该值是二个 ASCII 码字节相加之和。具体的调整规则如下: 、若 AL 的低四位大于9,或标志位 AF=1,则,AH=AH+1,AL=AL+6,并置 AF 和 CF 为1,否则,只置 AF 和 CF 为0; 、清除 AL 的高四位。 指令的格式:AAA 受影响的标志位:AF 和 CF(OF、PF、SF 和 ZF 等都是无定义) 例5.21 编写一段程序,完成二个15位十进制数 X 和 Y 之和,并把计算结果存入 X 之中。假 设数据 X 和 Y 都是以字符串形式表示的。 解: … X db "456407983123186" ;任意假设二个15位的大数 Y db "326676709587211" … CLC MO V SI, 14 ;用变址寄存器 SI 来从字符串的后面向前访问 MO V CX, 15 ;因为它们是二个15位十进制数 MO V AL, X[SI] loop1: AD C AL, Y[SI] ;把被加数加上 AAA MO V X[SI], AL DEC SI LOO P loop1 ;15位十进制数相加完毕 LEA BX, X ;下面5条指令是把 X 中的数据变成对应的字符 MO V CX, 15 AD D byte ptr [BX], '0' INC BX LOO P loop2 loop2: … 从上例可以看出,其实任意位的十进制数也都是可以的,只要改变 CX 的值即可。 2、ASCII 码减调整指令 AAS(Ascii Adjust After Subtraction) 该指令是用于调整 AL 之值,该值是二个 ASCII 码字节相减之差。具体的调整规则如下: 、若 AL 的低四位大于9,或标志位 CF=1,则,AH=AH-1,AL=AL-6,并置 AF 和 CF 为1,否则,只置 AF 和 CF 为0; 、清除 AL 的高四位。 指令的格式:AAS 受影响的标志位:AF 和 CF(OF、PF、SF 和 ZF 等都是无定义) 3、ASCII 码乘调整指令 AAM(Ascii Adjust After Multiplication) 该指令是用于调整寄存器 AL 之值,该值是由二个单 BCD 码字节用无符号乘指令 MUL 所得的积。其调整规则如下: AH←AL/10(商),AL←AL%10(余数) 指令的格式:AAM 受影响的标志位:PF、SF 和 ZF(AF、CF 和 OF 等都是无定义) 例如: MOV AL, 9 MOV BL, 8 MUL BL ;AL=72D AAM ;AH=7, AL=2 4、ASCII 码除调整指令 AAD(Ascii Adjust After Division) 该指令是在作除法前用于调整寄存器 AH 和 AL 之值,它是把二个寄存器中单 BCD 码 组成一个十进制数值,为下面的除法作准备的。其调整规则如下: AL←AH*10+AL,AH←0 指令的格式:AAD 受影响的标志位:PF、SF 和 ZF(AF、CF 和 OF 等都是无定义) 例如: MOV AX, 0502H MOV BL, 10D AAD ;AH=0, AL=52D DIV BL ;AH=2(余数), AL=5(商) 5、十进制数加调整指令 DAA(Decimal Adjust After Addition) 该指令是用于调整 AL 的值,该值是由指令 ADD 或 ADC 运算二个压缩型 BCD 码所得 到的结果。压缩型 BCD 码是一个字节存放二个 BCD 码,低四位和高四位都是一个 BCD 码。 其调整规则如下: 、如果 AL 的低四位大于9,或标志位 AF=1,那么,AL=AL+6,并置 AF=1; 、如果 AL 的高四位大于9,或 CF=1,那么,AL=AL+60H,并置 CF=1; 、如果以上两点都不成立,则,清除标志位 AF 和 CF。 经过调整后,AL 的值仍是压缩型 BCD 码,即:二个压缩型 BCD 码相加,并进行调整 后,得到的结果还是压缩型 BCD 码。 指令的格式:DAA 受影响的标志位:AF、CF、PF、SF 和 ZF(OF 无定义) 例如: MOV AL, 43H MOV BL, 29H ADD AL, BL ;AL=6BH,其不是压缩型的 BCD 码,因为低四位'B'不是 BCD 码 DAA ;调整后,AL=72H,这是压缩型的 BCD 码,也有:43+29=72 6、十进制数减调整指令 DAS(Decimal Adjust After Subtraction) 该指令也是用于调整 AL 的值,该值是由指令 SUB 或 SBB 运算二个压缩型 BCD 码所 得到的结果。其调整规则如下: 、如果 AL 的低四位大于9,或标志位 AF=1,那么,AL=AL-6,并置 AF=1; 、如果 AL 的高四位大于9,或 CF=1,那么,AL=AL-60H,并置 CF=1; 、如果以上两点都不成立,则,清除标志位 AF 和 CF。 经过调整后,AL 的值仍是压缩型 BCD 码,即:二个压缩型 BCD 码相减,并进行调整 后,得到的结果还是压缩型 BCD 码。 指令的格式:DAS 受影响的标志位:AF、CF、PF、SF 和 ZF(OF 无定义) 例如: MOV AL, 43H MOV BL, 29H SUB AL, BL ;AL=1AH,其不是压缩型的 BCD 码,因为低四位'A'不是 BCD 码 DAS ;调整后,AL=14H,这是压缩型的 BCD 码,也有:43-29=14 5.2.13 处理器指令 处理器指令是一组控制 CPU 工作方式的指令。这组指令的使用频率不高。 1、空操作指令 NOP(No Operation Instruction) 该指令没有的显式操作数,主要起延迟下一条指令的执行。通常用执行指令“XCHG AX, AX”来代表它的执行。NOP 指令的执行不影响任何标志位。 指令的格式:NOP 2、等待指令 WAIT(Put Processor in Wait State Instruction) 该指令使 CPU 处于等待状态,直到协处理器(Coprocessor)完成运算,并用一个重启信 号唤醒 CPU 为止。该指令的执行不影响任何标志位。 指令的格式:WAIT 3、暂停指令 HLT(Enter Halt State Instruction) 在等待中断信号时,该指令使 CPU 处于暂停工作状态,CS:IP 指向下一条待执行的指 令。当产生了中断信号,CPU 把 CS 和 IP 压栈,并转入中断处理程序。在中断处理程序执 行完后,中断返回指令 IRET 弹出 IP 和 CS,并唤醒 CPU 执行下条指令。 指令的格式:HLT 指令的执行不影响任何标志位。 4、封锁数据指令 LOCK(Lock Bus Instruction) 该指令是一个前缀指令形式,在其后面跟一个具体的操作指令。LOCK 指令可以保证是 在其后指令执行过程中,禁止协处理器修改数据总线上的数据,起到独占总线的作用。该指 令的执行不影响任何标志位。 指令的格式:LOCK INSTRUCTION 汇编入门(10讲) 时间:2009-5-16 8:34:03 核心提示:第 6章 程序的基本结构在前面几章,我们分别介绍了用汇编语言进行程序 设计所需要的几个最基本的知识:内存单元的寻址方式,变量定义和各种汇编指令格式。在 掌握了 这些基本内容之后,就需要学习如何把它们组成一个完整的汇编语言程序。6.1 源 程序的基本组成汇编语言源程序的组成部分有:模块、段、子程序和宏等。一个模... 第6章 程序的基本结构 在前面几章,我们分别介绍了用汇编语言进行程序设计所需要的几个最基本的知识:内 存单元的寻址方式,变量定义和各种汇编指令格式。在掌握了这些基本内容之后,就需要学 习如何把它们组成一个完整的汇编语言程序。 6.1 源程序的基本组成 汇编语言源程序的组成部分有:模块、段、子程序和宏等。一个模块对应一个目标文件, 当开发较大型的应用程序时,该程序可能由若干个目标文件或库结合而成的。有关模块和子 程序的知识和宏在第7章介绍,有关宏的知识将在第9章中叙述。 6.1.1 段的定义 微机系统的内存是 分段管理的,为了与之相对应,汇编语言源程序也分若干个段来构 成。8086CPU 有四个段寄存器,在该系统环境下运行的程序在某个时刻最多可访问四个段, 而80386及其以后的 CPU 都含有六个段寄存器,于是,在这些系统环境下开发的运行程序在 某个时刻最多可访问六个段。 不论程序在某个时刻最多能访问多少个段,在编程序时,程序员都可以定义比该段数更 多的段。在通常情况下,一个段的长度不能超过64K,在80386及其以后系统的保护方式下, 段基地址是32位,段的最大长度可达4G。 段的长度是指该段所占的字节数: 、如果段是数据段,则其长度是其所有变量所占字节数的总和; 、如果段是代码段,则其长度是其所有指令所占字节数的总和。 在定义段时,每个段都有一个段名。在取段名时,要取一个具有一定含义的段名。 段定义的一般格式如下: SEGMEN T [对齐类型] [组合类型] [类别] … ;段内的具体内容 段名 … 段名 ENDS 其中:“段名”必须是一个合法的标识符,前后二个段名要相同。可选项“对齐类型”、“组合 类型”和“类别”的说明作用请见6.3节中的叙述。 一个数据段的定义例子: SEGMENT word1 DW 1, 9078H, ? DB 21, 'World' DATA 1 byte1 DD 12345678H DATA 1 ENDS 一个代码段的例子: SEGMENT MOV AX, DATA1 ;把数据段 DATA1的段值送 AX MOV DS, AX ;把 AX 的值送给 DS,即:DS 存储数据段的段值 … MOV AX, 4C00H CODE 1 INT 21H ;调用 DOS 功能,结束程序的运行 CODE 1 ENDS 6.1.2 段寄存器的说明语句 在汇编语言源程序中可以定义多个段,每个段都要与一个段寄存器建立一种对应关系。 建立这种对应关系的说明语句格式如下: ASSUME 段寄存器名:段名[, 段寄存器名:段名, ……] 其中:段寄存器是 CS、DS、ES、SS、FS 和 GS,段名是在段定义语句说明时的段名。 在一条 ASSUME 语句中可建立多组段寄存器与段之间的关系,每种对应关系要用逗号 分隔。例如, ASSUME CS:CODE1, DS:DATA1 上面的语句说明了:CS 对应于代码段 CODE1,DS 对应于数据段 DATA1。 在 ASSUME 语句中,还可以用关键字 NOTHING 来说明某个段寄存器不与任何段相对 应。下面语句说明了段寄存器 ES 不与某段相对应。 ASSUME ES:NOTHING 在通常情况下,代 码段的第一条语句就是用 ASSUME 语句来说明段寄存器与段之间 的对应关系。在代码段的其它位置,还可以用另一个 ASSUME 语句来改变前面 ASSUME 语 句所说明的对应关系,这样,代码段中的指令就用最近的 ASSUME 语句所建立的对应关系 来确定指令中的有关信息。 例6.1 汇编语言段及其段说明语句的作用。 SEGMENT ;定义数据段 DATA1 word1 DW 5678h DAT A1 byte1 DB "ABCDEFG" DAT A1 ENDS SEGMENT ;定义数据段 DATA2 word2 DW 1234h DAT A2 word3 DW 9876h DAT A2 ENDS SEGMENT ;定义数据段 DATA3 DAT A3 byte2 DB ? DAT A3 ENDS SEGMENT ;编写代码段 CODE1 COD E1 ASSU CS:CODE1, DS:DATA1, ES:DATA2 ;(1) ME MOV AX, DATA1 ;(2) MOV DS, AX ;(3) MOV AX, DATA2 ;(4) MOV ES, AX ;(5) … MOV AX, word1 ;访问段 DATA1中的字变量 word1 MOV word2, AX ;访问段 DATA2中的字变量 word2 … ASSU ME DS:DATA3, ES:NOTHING ;(6) MOV AX, DATA3 MOV DS, AX MOV BL, byte2 ;访问段 DATA3中的字节变量 byte2 … MOV AX, 4C00H ;(7) INT 21H ;(8) COD E1 ENDS 语句(1)和(6)分别说明了段和段寄存器之间的对应关系,其中语句(6)重新说明语句(1)所 指定的对应关系。 二组语句(2)和(3)、(4)和(5)实现对段寄存器 DS 和 ES 赋初值。ASSUME 说明语句只起 说明作用,它不会对段寄存器赋值,所以,必须对有关段寄存器赋值。在以后的其它源程序 中也都是用此方法来实现对数据段寄存器赋值的。 语句(7)和(8)是调用中断21H 的4CH 号功能来结束本程序的执行,程序的返回代码由寄 存器 AL 来确定。结束本程序执行的指令是所有主模块必须书写的语句。 注意:代码段寄存器不能由程序员在源程序中对其赋值,其值是由操作系统在装入它进 入系统运行时自动赋值的。 6.1.3 堆栈段的说明 堆栈段是一个特殊的段,在程序中可以定义它,也可以不定义。除了要生成 COM 型执 行文件的源程序外,一个完整的源程序一般最好定义堆栈段。如果在程序中不定义堆栈段, 那么,操作系统在装入该执行程序时将自动为其指定一个64K 字节的堆栈段。 在程序没有定义堆栈段的情况下,在由连接程序生成执行文件时,将会产生一条如下的 警告信息,但程序员可以不理会它,所生成的执行文件是可以正常运行的。 warning xxxx: no stack segment (其中:xxxx 是错误号) 在源程序中,可用以下方法来定义堆栈段。 方法1: SEGMENT DB 256 DUP(?) ;256是堆栈的长度,可根据需要进行改变 STACK 1 TOP LABEL WORD STACK 1 ENDS 以上堆栈段的定义如图6.1所示。由于堆栈是按地址从大 到小的存储单元顺序来存放内容的,所以,在堆栈存储单元 说明语句之后,再说明一个栈顶别名,这样,对栈顶寄存器 SP 的赋值就很方便。 在源程序的代码段中,还要添加如下程序段,才能把段 STACK1当作堆栈段来使用。 … ASSUM E SS:STACK1 ;可在代码段的段指定语句中一起说明 CLI ;禁止响应可屏蔽中断 MOV AX, STACK1 MOV SS, AX MOV SP, offset TOP ;给堆栈段的栈顶寄存器 SP 赋初值 STI ;恢复响应可屏蔽中断 … 方法2: SEGMENT STACK;定义一个堆栈段,其段名为 STACK1 STACK 1 DB 256 DUP(?) STACK 1 ENDS 上述段定义说明了该段是堆栈段,系统会自动把段寄存器 SS 和栈顶寄存器 SP 与该堆 栈段之间建立相应的关系,并设置其初值,而不用在代码段对它们进行赋值。 除了以上二种方法外,还有一种更简洁的方法来定义堆栈段,有关内容请见第6.4.2节中 的叙述。 6.1.4 源程序的结构 下面的程序是一个完整的源程序,其功能是在屏幕上显示字符串“Hello, World.”。读者 可参考此结构编写自己的简单程序。 例6.2 在屏幕上显示字符串“HELLO,WORLD.” 解:可运行下面的控件,用鼠标左键单击程序中的某一行,可阅读其含义;单击“内存”可切 换内存内容的显示方式。 伪指令 END 表示源程序到此为止,汇编程序对该语句之后的任何内容都不作处理,所 以,通常情况下,伪指令 END 是源程序的最后一条语句。 伪指令 END 后面可附带一个在程序中已定义的标号,由该标号指明程序的启动位置。 如果源程序是一个独立的程序或主模块,那么,伪指令 END 后面一定要附带一个标号; 如果源程序仅是一个普通模块,那么,其 END 后面就一定不能附带标号。 汇编入门(11讲) 时间:2009-5-16 8:36:16 核心提示:6.2 程序的基本结构在学习高级语言程序设计时,我们知道了程序的三大主 要结构:顺序结构、分支结构和循环结构。在汇编语言的源程序也同样有此三大结构,所不 同 的是它们的表现形式不同。用高级语言编写程序时,由于不使用“转移语句”而使这三种 结构清晰明了。但在汇编语言的源程序中,很难不使用“转移语句”(除非 是一... 6.2 程序的基本结构 在学习高级语言程序设计时,我们知道了程序的三大主要结构:顺序结构、分支结构和 循环结构。在汇编语言的源程序也同样有此三大结构,所不同的是它们的表现形式不同。用 高级语言编写程序时,由于不使用“转移语句”而使这三种结构清晰明了。 但在汇编语言的源 程序中,很难不使用“转移语句”(除非是一些只有简单功能的程序), 有时甚至会有各种各样的“转移语句”。由于存在这些转移语句,就使得:汇编语言源程序 的 基本结构显得不太明确。如果源程序的编写者思维混乱,编写出来的源程序在结构上就会显 得杂乱无章,反之,如果编写者条理清晰,安排的操作井然有序,那 么,编写出来的程序 在结构上就会一目了然。 总之,不论是高级语言的源程序,还是汇编语言的源程序,其程序的三大基本结构也还 是万变不离其宗的。 6.2.1 顺序结构 顺序结构是最简单的程序结构,程序的执行顺序就是指令的编写顺序,所以,安排指令 的先后次序就显得至关重要。另外,在编程序时,还要妥善保存已得到的处理结果,为后面 的进一步处理直接提供前面的处理结果,从而避免不必要的重复操作。 例6.3 编写程序段,完成下面公式的计算(其中:变量 X 和 Y 是32位有符号数,变量 A,B 和 Z 是16位有符号数)。 A←(X-Y+24)/Z 的商,B←(X-Y+24)/Z 的余数 解: SEGMENT X DD ? Y DD ? Z DW ? A DW ? B DW ? DATA 1 … DATA 1 ENDS SEGMENT … MO V AX, X MO V DX, X+2 ;用(DX:AX)来保存32位变量 X 的数值 SUB AX,Y SBB DX, Y+2 ;(DX:AX)-(Y+2:Y) AD D AX, 24D AD C DX, 0 ;(DX:AX)+24 IDI V Z MO V A, AX MO V B, DX CODE 1 … CODE 1 ENDS 在编程序时,常常需要交换二变量之值。假设需要交换值的变量名为:var1和 var2,临 时增加的变量名为 temp。常用的算法如下: temp = var1 var1 = var2 var2 = temp 例6.4 假设有二个字变量 word1和 word2,编写程序段实现交换其值的功能。 解: 方法1:用汇编语言指令简单“直译”上面的 交换数据方法 SEGMENT … word1 DW ? word2 DW ? temp DW ? DATA 1 … DATA 1 ENDS SEGMENT … MOV AX, word1 MOV temp, AX ;上二语句实现语句“temp=word1” MOV AX, word2 MOV word1, AX ;上二语句实现语句“word1=word2” MOV AX, temp MOV word2, AX ;上二语句实现语句“word2=temp” CODE 1 … CODE 1 ENDS 这种方法虽然也能完成功能,但显然其不能充分利用汇编语言的特点,程序效率很低。 方法2:用汇编语言指令的特点来直接编译 SEGMENT … word1 DW ? word2 DW ? DATA 1 … DATA 1 ENDS SEGMENT … MOV AX, word1 XCH G AX, word2 MOV word1, AX ;能 XCHG word1, word2来代替这三条指令吗? CODE 1 … CODE 1 ENDS 该方法充分利用了汇编语言的特点,不仅省去了中间变量 temp 的定义,而且程序的效 率也提高了。 6.2.2 分支结构 分支结构是一种非常重要的程序结构,也是实现程序功能选择所必要的程序结构。由于 汇编语言需要书写转移指令来实现分支结构,而转移指令肯定会破坏程序的结构,所以,编 写清晰的分支结构是掌握该结构的重点,也是用汇编语言编程的基本功。 在程序中,当需要进行逻辑分支时,可用每次分二支的方法来达到程序最终分多支的要 求,也可是用地址表的方法来达到分多支的目的。 一、显示转移指令实现的分支结构 在高级语句中,分支结构一般用 IF 语句来实现,在汇编语言中,课用无条件转移指令 或条件转移指令实现的分支结构。如图6.2给出了二种常用的分支结构。 在编写分支程序时,要尽可能避免编写“头重脚轻”的结构,即:当前分支条件成立时, 将执行一系列指令,而条件不成立时,所执行的指令很少。这样就使后一个分支离分支点较 远,有时甚至会遗忘编写后一分支程序。这种分支方式不仅不利于程序的阅读,而且也不便 将来的维护。 所以,在编写分支结构时,一般先处理简单的分支,再处理较复杂的分支。对多分支的 情况,也可遵循“由易到难”的原则。因为简单的分支只需要较少的指令就能处理完,一旦处 理完这种情况后,在后面的编程过程中就可集中考虑如何处理复杂的分支。 例6.5 已知字节变量 CHAR1,编写一程序段,把它由小写字母变成大写字母。 解: SEGMENT … CHAR 1 DB ? DATA1 … DATA1 ENDS SEGMENT … MOV AL, CHAR1 CODE 1 CMP AL, ‘a’ JB next CMP AL, ‘z’ JA next SUB CHAR1, 20H ;指令 AND CHAR1, 0DFH 也可以 … next: … CODE 1 ENDS 例6.6 编写一程序段,计算下列函数值。其中:变量 X 和 Y 是有符号字变量。 解: SEGMENT … X DW ? Y DW ? DATA1 … DATA1 ENDS SEGMENT … MOV AX, X CMP AX, 0 JGE case23 ADD AX, 10 ;第一种情况的计算结果 CODE 1 JMP result CMP AX, 10D JG case3 MOV BX, 30D IMUL BX ;第二种情况的计算结果 case23: JMP result case3: SUB AX, 9 ;第三种情况的计算结果 MOV Y, AX ;把计算结果保存到变量 Y 中 result: … CODE 1 ENDS 例6.7 把下列 C 语言的语句改写成等价的汇编语言程序段(不考虑运算过程中的溢出)。 If (a+b > 0 && c%2 == 0) a = 62; else a = 21; 其中:变量 a,b 和 c 都是有符号的整型(int)变量。 解: SEGMENT … A DW ? B DW ? C DW ? DATA1 … DATA1 ENDS SEGMENT … MOV AX, A ADD AX, B JLE _ELSE ;ADD 指令会改变算术标志位 TEST C, 1 ;C%2==0,也就是:看 C 的最低位是否为0 JNZ _ELSE MOV A, 62D CODE 1 JMP NEXT _ELSE: MOV A, 21D NEXT: … CODE 1 ENDS 例6.8 用地址转移表实现下列 C 语言的 switch 语句,其中:变量 A 和 B 是有符号的整型(int) 变量。 switch (a%8) b = 32; {case 0: break; case 1: b = a + 43; case 2: break; b = 2*a; case 3: break; b--; case 4: break; case 5: case 6: case 7: printf(“Function 5_6_7”); break; 解: SEGMENT … A DW ? B DW ? Tabl e DW case0. case12, case12, case3 DW case4, case567, case567, case567 MS G DB 'Function 5_6_7$' DATA 1 … DATA 1 ENDS SEGMENT CODE 1 … } MO V AX, A MO V BX, AX AN D BX, 7 ;得到 BX 的低三位,实现 a%8的 计算 SHL BX, 1 ;由于地址表是字类型,其下标要 乘2 JMP Table[BX] ;利用地址表实现多路转移 MO V B, 32D case0: JMP next AD D AX, 43D MO V B, AX case12 : JMP next SHL AX, 1 MO V B, AX case3: JMP next DE C B case4: JMP next LEA DX, MSG MO V AH, 9 INT 21H case56 7: JMP next next: … CODE 1 ENDS 用地址表实现多路转移的关键在于:转移入口的地址表和转移情况可整数化。如果这二 个要求有一个不满足,或很难构造,则无法使用该方法。 二、用伪指令实现的分支结构 为了改善汇编语言 源程序的结构,减少显式转移语句所带来混乱,在宏汇编 MASM 6.11系统中,增加了表达分支结构的伪指令。该伪指令的书写格式与高级语言的书写方式相 类似,汇编程序在汇编时会自动增加转移指令和相应的标号。理解并 掌握该知识,对将来 学习《编译原理》课程也有一定的帮助。 分支伪指令的具体格式如下: 格式1: .IF condition ;以英文“句号”开头 指令序列 ;条件"condition"成立时所执行的指令序列 .ENDIF 格式2: .IF condition 指令序列1 .ELSE 指令序列2 ;条件"condition"不成立时所执行的指令序列 .ENDIF 格式3: .IF condition1 指令序列1 .ELSEIF condition2 指令序列2 ;条件"condition2"成立时所执行的指令序列 .ENDIF 其中:条件表达式“condition”的书写方式与 C 语言中条件表达式的书写方式相似,也可 用括号来组成复杂的条件表达式。 条件表达式中可用的操作符有:==(等于)、!=(不等)、>(大于)、>=(大于等于)、<(小于)、 <=(小于等于)、&(位操作与)、!(逻辑非)、&&(逻辑与)、||(逻辑或)等。 若在条件表达式中检测标志位的信息,则可以使用的符号名有:CARRY?(相当于 CF==1)、OVERFLOW?(OF==1)、PARITY?(PF==1)、SIGN?(SF==1)、ZERO?(ZF==1)等。例 如: .IF CARRY? && AX != BX ;检测 CF==1且 AX!=BX 是否成立 ;汇编语言指令序列 .ENDIF 在指令序列中,还可再含有其它的.IF 伪指令,即:允许嵌套。伪指令.ELSEIF 引导出 另一个二叉分支,但它不能作伪指令块的第一个伪指令。 汇编程序在对“条件表达式”进行代码转换时将进行代码优化处理,以便尽可能生成最好 的指令代码。如: .IF ax == 0 汇编程序会把它转换为指令“OR ax, ax”,而不是“CMP ax, 0”,因为前者比后者更好, 而不是简单直接地转换为后者。 如果用伪指令来书写分支结构,那么,例6.5的代码段部分就可写成如下程序段: … MOV AL, CHAR1 .IF AL>='a' && AL<='z' ;语句象 C 语言语句吗? SUB CHAR1, 20H .ENDIF … 也可把例6.6的代码段部分就可写成如下程序段: … MOV AX, X .IF AX < 0 ADD AX, 10 ;计算第一种情况的结果 .ELSEIF AX <= 10 MOV BX, 30D IMUL BX ;计算第二种情况的结果 .ELSE SUB AX, 9 ;计算第三种情况的结果 .ENDIF MOV Y, AX … ;把计算结果保存到变量 Y 中 例6.9 根据当前计算机的时间和日期,显示上午(AM)或下午(PM),以及所在的季节。 解:显示解答 6.2.3 循环结构 循环结构是一个重要的程序结构,它具有重复执行某段程序的功能。通常,循环结构包 括以下四个组成部分: 1、循环初始化部分——初始化循环控制变量、循环体所用到变量; 2、循环体部分——循环结构的主体; 3、循环调整部分——循环控制变量的修改、或循环终止条件的检查; 4、循环控制部分——程序执行的控制转移。 以上四部分可以在程序中用各种不同的形式体现出来,有时也并非清析地表达出来。常 用的循环结构如图6.3所示。 一、用循环指令构成循环结构 在编写循环结构的程序片段时,我们可以多种方法来循环结构。如:循环次数是已知的, 可用 LOOP 指令来构造循环;当循环次数是未知或不定的,则可用条件转移或无条件转移 来构成循环结构。 例6.10 分类统计字数组 data 中正数、负数和零的个数,并分别存入内存字变量 Positive、 Negative 和 Zero 中,数组元素个数保存在其第一个字中。 解:显示解答 例6.11 计算数组 score 的平均整数,并存入内存字变量 Average 中,数组以-1为结束标志。 解: SEGMENT data DW 90, 95, 54, 65, 36, 78, 66, 0, 99, 50, -1 DATA 1 Avera ge DW 0 DATA 1 ENDS SEGMENT CODE 1 ASSUME CS:CODE1, DS:DATA1 MOV AX, DATA1 MOV DS, AX XOR AX, AX XOR DX, DX ;用(DX,AX)来保存数组元素之和 XOR CX, CX ;用 CX 来保存数组元素个数 STAR T: LEA SI, data ;用指针 SI 来访问整个数组 MOV BX, word ptr [SI] CMP BX, 0 JL over ADD AX, BX ADC DX, 0 ;把当前数组元素之值加到(DX,AX)中 INC CX ;数组元素个数加1 ADD SI, 2 again: JMP again JCXZ exit ;防止零作除数,即数组是空数组 DIV CX over: MOV Average, AX exit: MOV AX, 4C00H INT 21H ENDS CODE 1 END START 二、用伪指令实现的循环结构 在宏汇编 MASM 6.11系统中,还增加了表达循环结构的伪指令,以便更清晰地表达 WHILE 循环、REPEAT-UNTIL 循环。另外,还增加两个辅助循环的伪指令。这 些伪指令 的书写格式和含义与高级语言中相应语句的书写格式和含义相一致,所以,这些伪指令是很 容易掌握的,也是非常有用的。 循环伪指令的格式和含义如下: 1、WHILE 型循环伪指令 .WHILE condition 循环体的指令序列 ;条件"condition”成立时所执行的指令序列 .ENDW 其中:.ENDW 与前面的.WHILE 相匹配,它标志着其循环体到此结束。 如果条件表达式“condition”在循环开始时,就为“假”(false),那么,该循环体一次也不 会被执行。 2、REPEAT 型循环伪指令 .REPEAT 循环体的指令序列 .UNTIL condition .REPEAT 循环体的指令序列 .UNTILCXZ [condition] REPEAT 型循环在执行完循环体后,才判定逻辑表达式 condition 的值。若该表达式的 值为真,则终止该循环,并将执行伪指令.UNTIL[CXZ]后面的指令,否则,将向上跳转到伪 指令.REPEAT 之后的指令,为继续执行其循环体作准备。 如果.UNTILCXZ 后面没有写逻辑表达式,那么,由.REPEAT-.UNTILCXZ 所构成的循 环与用 LOOP 指令所过程的循环是一致的,它们都是以“CX=0”为循环终止条件。 如 果.UNTILCXZ 后面书写了逻辑表达式,那么,该逻辑表达式的形式只能是: “EXP1==EXP2”或“EXP1!=EXP2”。所以,这时由 “.REPEAT-.UNTILCXZ condition”所构成 的循环就与用 LOOPNE/LOOPE 指令所过程的循环是一致的,它们都是以“condition || CX=0” 为循环终止条件。 和高级语言的 REPEAT 型的循环一样,.REPEAT-.UNTIL[CXZ]的循环体也会至少被执 行一次。 .WHILE-.ENDW 和.REPEAT-.UNTIL[CXZ]的循环体内还可再含有循环伪指令,这样 就构成了循环结构的嵌套。 汇编程序在生产指令代码时会进行代码优化,以便尽可能得到最优化的指令序列。 3、辅助循环伪指令 (1)、终止循环伪指令 .BREAK .BREAK .IF condition 该伪指令用来终止包含它的最内层循环。前者是无条件终止循环,后者是仅当逻辑表达 式 condition 为真时,才终止循环。 .WHILE 1 .REPEAT … .BREAK .IF condition … … .BREAK .IF condition … ENDW .UNTIL 0 对于以上二个循环,如果没有指令来终止循环的话,它们都将进入死循环状态,但如果 在该层循环体内,存在伪指令“.BREAK .IF condition”的话,那么,当逻辑表达式 condition 为真时,该循环就会被终止了。 (2)、循环继续伪指令 .CONTINUE .CONTINUE .IF condition 该伪指令用于直接跳转到包含它的最内层循环的计算循环条件表达式的代码处。前者是 无条件转移到计算循环条件表达式的代码处,后者是仅当条件表达式 condition 为真时,才 进行这样的跳转。 辅助循环伪指令.BREAK 和.CONTINUE 只能在伪指令.WHILE-.ENDW 和.REPEAT -.UNTIL 的循环体内使用。 例6.12 显示9个数字字母'1'~'9',26个大写字母,和显示任意输入的数字字符,并用按“回车” 键来结束本程序的运行。 解: SEGMENT DATA1 MSG1 DB 13, 10, "Iteration: " NUM 1 DB '1', "$" MSG2 DB 13, 10, "Alphabet: $" NUM 2 DB 'A', " $" MSG3 DB 13, 10, "Type digits, then press ENTER: $" DATA1 ENDS SEGMENT CODE 1 ASSUME CS:CODE1, DS:DATA1 MOV AX, DATA1 MOV DS, AX MOV CX, 9 MOV AH, 09H MOV DX, OFFSET MSG1 .REPEAT INT 21H INC NUM1 ;显示 Iteration: 1,2,~,9 .UNTILCXZ MOV DX, OFFSET MSG2 INT 21H ;显示字符串"Alphabet:" MOV AH, 09H MOV DX, OFFSET NUM2 .REPEAT INT 21H INC NUM2 ;显示当前字母 ;当前字母向后移 .UNTIL NUM2 > 'Z' ;显示整个大写字母表 MOV AH, 09H MOV DX, OFFSET MSG3 INT 21H .WHILE 1 ;循环条件为永真的循环 MOV AH, 07H INT 21H ;不带回显地从键盘读一个字符 .BREAK .IF AL == 13 ;如果输入“回车”键,则终止循环 .CONTINUE .IF (AL<'0') || (AL>'9') ;如果字符不是数字字符,则继续循环 MOV DL, AL MOV AH, 02H INT 21H ;显示所输入的数字字母 .ENDW MOV AX, 4C00H START : INT 21H ENDS CODE 1 END START 6.3 段的基本属性 在通常情况下,一个复杂的应用程序会由若干个模块组成,一个模块又会含有多个段。 而不同模块的段之间、同一模块的段之间往往存在某种联系,这种联系就要体现在段属性的 说明上。 段定义的一般格式如下: SEGMENT [对齐类型] [组合类型] [类别] 段名 … 段名 ENDS 段属性“对齐类型”、“组合类型”和“类别”要按此顺序说明,但这些可选项可根据需要选 择书写。如果源程序中不指定某个属性,那么,汇编程序将使用该属性的缺省值。 程序中的段名可以是唯一的,也可以与其它段同名。在同一模块中,如果有二个段同名, 则后者被认为是前段的后续,这样,它们就属同一段。 当同一模块出现二个同名段时,则后者的可选项属性要么与前者相同,要么不写其属性 而选用前者的段属性。 例6.13 同段名的作用 SEGMENT ;第一个数据段 DATA1 MSG DB "Hello, " DATA1 ENDS SEGMENT ;第一个代码段 CODE 1 ASSUME CS:CODE1, DS:DATA1 MOV AX, DATA1 MOV DS, AX MOV DX, offset MSG MOV AH, 9 START : INT 21H CODE 1 ENDS SEGMENT ;第二个数据段 DATA1 DB "World.$" DATA1 ENDS SEGMENT ;第二个代码段 MOV AX, 4C00H CODE 1 INT 21H ENDS END START CODE 1 END 在上面的例子中,第二个数据段是第一个数据段的后续,汇编程序把它们是合二为一, 上述的代码段也如此。 下面,详细说明段属性的含义及其作用。 6.3.1 对齐类型(ALIGN) 对齐类型表示当前段对起始地址的要求,连接程序(LINK.EXE)按表6.1的地址格式来定 位段的起始地址。在进行段定位时,会根据其定位类型进行定位的,所以,各段之间就有可 能出现一些空闲字节,即可能浪费几个字节单元。 段对齐类型 PARA 是一个适用于所有段类型的对齐类型,它也是缺省的对齐类型。对 齐类型 BYTE 和 WORD 通常用于数据段的定位,对齐类型 DWORD 通常用于80386及其以 后 CPU 代码段的定位。 表6.1 段对齐类型与段起始地址之间的对应关系 对齐类型 起始地址(二进制) 功能说明 最多的空闲字节数 BYTE xxxx xxxx xxxx xxxx xxxx 下一个字节地址 0 WORD xxxx xxxx xxxx xxxx xxx0 下一个字地址 1 DWORD xxxx xxxx xxxx xxxx xx00 下一个双字地址 3 PARA xxxx xxxx xxxx xxxx 0000 下一个节地址 15 PAGE xxxx xxxx xxxx 0000 0000 下一个页地址 127 6.3.2 组合类型(COMBINE) 组合类型是告诉连接程序如何把不同模块中段名相同的段合并在一起。具体的组合类型 如下: NONE 表示当前段在逻辑上独立于其它模块,并有其自己的基地址。NONE 是缺 省的组合类型。 PUBLIC 表示当前段与其它模块中同段名的 PUBLIC 类型段组合成一个段。组合的 先后次序取决于 LINK 程序中目标模块排列的次序。在组合时,后续段的起 始地址要按其对齐类型进行定位,所以,同名段之间可能有间隔。 COMMON 表示当前段与其它模块中同名段重叠,也就是说,它们的起始地址相同。 最终段的长度是同名段的最大长度。由于段覆盖,所以,前一同名段中的 初始化数据被后续段的初始数据覆盖掉。 STACK 组合类型 STACK 表示当前段是堆栈栈,其组合情况与 PUBLIC 相同。 AT 数值表 达式 该数值表达式是当前段所指定的绝对起始地址的段地址。 6.3.3 类别(CLASS) 类别是一个由程序员指定的用单引号括起来的字符串。如果一个段没有给出类别,那么, 这个段的类别就为空。类别是用于段的分类,连接程序利用该类别来调整同名、同类别的段, 并使它们相邻。典型的类别是"Data"和"Code"。如果指定某段的类别是"Code",那么,该段 最好是代码段,这样,有的调试程序(如:CodeView)就可以顺序工作。 例如: SEGMENT WORD PUBLIC "Data" DATA1 … DATA1 ENDS 上述段定义说明了该段的起始地址是下一个字地址、组合类型为 PUBLIC、段类别是 "Data"。 6.3.4 段组(GROUP) 段组伪指令 GROUP 是用于把源程序模块中若干个段结合成一个组,并对该段组定义一 个段组名。段组伪指令的格式如下: 段组名 GROUP 段名[, 段名, ……] 其中:段名之间要用逗号间隔,段名也可以用表达式“SEG 变量”或“SEG 标号”。 下面举例说明段组伪指令的使用方法和作用。 例6.12 段组的作用 方法1:用一个段寄存器对应二个数据段 SEGMENT ;第一个数据段 DATA1 b1 DB 10h DATA1 ENDS SEGMENT ;第二个数据段 DATA2 b2 DB 23h DATA2 ENDS SEGMENT CODE 1 ASSUME CS:CODE1, DS:DATA1 ;(1) MOV AX, DATA1 MOV DS, AX ;(2)把数据段 DATA1的段值赋给段寄存器 DS … MOV BL, b1 ;(3)引用 DS 来访问 DATA1中的变量 b1 … ASSUME DS:DATA2 ;(4) MOV AX, DATA2 MOV DS, AX ;(5)把数据段 DATA2的段值赋给段寄存器 DS … START : MOV AL, b2 ;(6)引用 DS 来访问 DATA2中的变量 b2 … ENDS CODE 1 END START 在上例中,语句(1)说明 DS 与 DATA1建立联系,语句(2)对 DS 赋值,语句(3)用 DS 来 访问 DATA1段的变量名。语句(4)说明 DS 与 DATA2建立联系,语句(5)对 DS 赋值,语句(6) 用 DS 来访问 DATA2段的变量名。 在该例子中,因为只使用一个段寄存器 DS 来对应二个数据段,所以,需要切换 DS 的 对应关系(如:语句(4))。但我们也可以用段寄存器 DS 和 ES 来分别对应段 DATA1和 DATA2, 这样,方法1就可变成方法2。 方法2:用二个段寄存器对应二个数据段 SEGMENT DATA1 b1 DB 10h DATA1 ENDS SEGMENT DATA2 b2 DB 23h DATA2 ENDS SEGMENT CODE 1 ASSUME CS:CODE1, DS:DATA1, ES:DATA2 MOV AX, DATA1 MOV DS, AX ;把数据段 DATA1的段值赋给段寄存器 DS MOV AX, DATA2 MOV ES, AX ;把数据段 DATA2的段值赋给段寄存器 ES … MOV BL, b1 ;引用 DS 来访问 DATA1中的变量 b1 … MOV AL, b2 ;引用 ES 来访问 DATA2中的变量 b2 START : … ENDS CODE 1 END START 我们还可以用段组来简化段寄存器的使用,把段 DATA1和 DATA2组成一个数据段。所 以,把方法2再改写成方法3的形式。 方法3:用一个段组组成二个数据段 GSEG GRO UP DATA1, DATA2 ;把段 DATA1和 DATA2定义成一个段组 DATA1 SEGMENT b1 DB 10h DATA1 ENDS SEGMENT DATA2 b2 DB 23h DATA2 ENDS SEGMENT CODE 1 ASSUME CS:CODE1, DS:GSEG MOV AX, GSEG MOV DS, AX ;把段组 GSEG 的段值赋给段寄存器 DS … MOV BL, b1 ;引用 DS 来访问 DATA1中的变量 b1 … MOV AL, b2 ;引用 DS 来访问 DATA2中的变量 b2 START : … ENDS CODE 1 END START 定义段组后,段组内各段所定义的标号和变量,除了与定义它们的段起始点相关外,还 与段组的起始点相关。规定如下: 、 如果在 ASSUME 伪指令中说明段组与段寄存器相对应,那么,有关标号或变量的偏 移量就相对于段组起点计算; 、 如果在 ASSUME 伪指令中说明段组内的某各段与段寄存器相对应,那么,有关标号 或变量的偏移量就相对于该段的起点。 所以,在使用段组后,程序员要谨慎使用 ASSUME 伪指令,并保证段寄存器的值与段 组或段相一致。 6.4 简化的段定义 前面,我们介绍了完整的段定义格式,用完整的段定义格式虽然可以控制段的各种属性, 但程序员很少会这样做。现在的汇编程序提供了一种简化的段定义方式,它使定义段更简单、 方便。 6.4.1 存储模型说明伪指令 在使用简化的段定义方式之前,必须使用存储模式说明伪指令来描述源程序所采用的存 储模式。该伪指令说程序所使用的存储模式,汇编程序将用该存储模式生成相应的 ASSUME 和 GROUP 语句,同时也为其它的简化段创建等价的预定义。 程序存储模式说明伪指令的格式如下: .MODEL 存储模式[,语言类型] [,操作系统类型] [,堆栈类型] 程序可选的存储模式有:TINY、SMALL、COMPACT、MEDIUM、LARGE、HUGE 和 FLAT。 伪指令.MODEL 必须写在源程序的首部,且只能出现一次,其前内容只能是注释。 如果用伪指令来指定程序所遵循的语言类型,那么,将不允许子程序的嵌套定义。与子 程序定义有关的内容请见第7.5节。 一、存储模式 如果要用汇编语言编写被高级语言调用的子程序,那么,该汇编程序的存储模式必须与 该高级语言编译(或解释)程序所使用的存储模式相匹配。汇编语言程序所能使用的存储模 式、符号及其相关信息如表6.2所列。 在程序中,还可伪指令 OPTION SEGMENT 和 SEGMENT 来指定段的规模。 有关存储模式的具体规定如下: 、TINY 在汇编程序 MASM 6.11和 TASM 4.0,该存储类型是为编写 COM 文件类型而设置的。 程序员还可用汇编命令行选项/AT 和连接命令选项/TINY 来达到此目的。 表6.2 存储模式的符号及其相关含义 代码的位距 数据的位距 段的宽度 数据段和代码段能否合 并 Code Distance Data Distance Segment Width Data & Code Combined? Tiny Small Compact Medium Large Huge Flat NEAR NEAR 16-bit Yes NEAR NEAR 16-bit No NEAR FAR 16-bit No FAR NEAR 16-bit No FAR FAR 16-bit No FAR FAR 16-bit No NEAR NEAR 32-bit Yes 、SMALL 所有的数据变量必须在一个数据段之内,所有的代码也必须在一个代码段之内。在这种 模型下,数据段寄存器的内容保持不变,所有转移也都是段内转移。 该存储类型是独立汇编语言源程序常用的存储模型。 、MEDIUM 所有的数据变量必须在一个数据段之内,但代码段可以有多个。在这种模型下,数据段 寄存器的内容保持不变,转移可以是段间转移。 、COMPACT 数据段可以有多个,但代码段只能有一。 、LARGE 数据段和代码段都可以有多个,但一个数组的字节数不能超过64KB。 、HUGE 数据段和代码段都可以有多个,一个数组的字节数也可以超过64KB。 、FLAT FLAT存储模式在创建执行文件时,将使该程序仅含一个包括程序数据和代码的32位段, 并且只能在80386及其以后的计算机系统中运行。该程序的文件类型为 EXE。 在使用该存储模式之前,必须先用伪指令.386、.486或其它伪指令来说明更高性能的 CPU 类型。也就是说:FLAT 模式仅在386及其以后 CPU 模式下才能使用。 在该程序中,所有代码和数据位距的缺省值都是 NEAR,子程序的类型也是 NEAR,并 且标识符@CodeSize,@DataSize 和@Model 的值分别为:0、0和7。 在 FLAT 存储模式下,程序将不使用段寄存器 FS 和 GS。汇编程序在处理说明语句 “.MODEL FLAT”时,将自动生成下列段寄存器说明语句: ASSUME CS:FLAT, DS:FLAT, SS:FLAT, ES:FLAT, FS:ERROR, GS:ERROR 当然,程序员也可把该段寄存器说明语句写在其指令序列之中。 二、语言类型 其详细说明请见7.5.3节中所述。 三、操作系统类型 OS_DOS 是当前唯一支持的选项值,也是该选项的缺省值。 四、堆栈类型 堆栈类型的值主要影响伪指令.STARTUP 所生成的指令序列。该选项有二个可选值: NEARSTACK 和 FARSTACK。其中:NEARSTACK 是该选项的缺省堆栈类型。 、NEARSTACK——堆栈段和数据段是同一段; 、FARSTACK——堆栈段和数据段是不同的段,且堆栈不在段组 DGROUP 中。 例如: .MODEL SMALL, C, OS_DOS, FARSTACK 6.4.2 简化段定义伪指令 简化段定义伪指令在说明一个新段即将开始的同时,也说明了上一个段的结束。在本段 定义结束时,也不必用伪指令“ENDS”来标识。 具体的伪指令说明形式及其功能描述如下: 1、代码段定义 .CODE 作用:说明其下面的内容是代码段中内容。 2、堆栈段定义 .STACK [堆栈字节数] 其中,“堆栈字节数”可以不写,其缺省值为1024B。 3、数据段定义 .DATA / .DATA? / .CONST 作用:说明其下面的内容是数据段中的变量定义。 在一个源程序中,可以有多个伪指令.DATA 定义的数据段,这就好象在源程序中定义 多个同段名的数据段一样。 伪指令.DATA?说明下面是一个未初始化数据段的开始,伪指令.CONST 说明下面是一 个常数数据段的开始。这二条伪指令很少使用,除非在与高级语言编写的程序相结合时,为 了遵守高级语言的某些约定,而需要特殊说明时才使用。 汇编程序在处理简化的堆栈段和数据段定义时,它会自动地把伪指 令.STACK、.DATA、.DATA?和.CONST 所定义的段组合成一个段组。如果想定义一个独立 的、不与其它段组合在一起的数据段的话,那么,就可选用下面的数据段定义方式。 4、远程数据段定义 .FARDATA [段名] / .FARDATA? [段名] 其中:“段名”是可选项,如果不指定的话,则该段名就取其缺省段名。 作用:说明一个独立的数据段。 伪指令.FARDATA?说明下面是一个未初始化的、独立数据段的开始。通常情况下,很 少使用该伪指令。 6.4.3 简化段段名的引用 当使用简化的段定义时,一般情况下,程序员可以不知道这些段的段名、段地址堆齐类 型和组合类型等。但当把简化定义的段和标准定义的段混合使用时,就需要知道简化定义段 的基本属性。表6.3是在小模式下段的基本属性对应表。 表6.3 小模式下简化段定义的缺省属性表 伪指令 缺省段名 对齐类型 组合类型 类别 段组名 .CODE _TEXT WORD PUBLIC 'CODE' .FARDATA FAR_DATA PARA NONE 'FAR_DATA' .FARDATA? FAR_BSS PARA NONE 'FAR_BSS' .STACK STACK PARA STACK 'STACK' DGROUP .DATA DATA WORD PUBLIC 'DATA' DGROUP .DATA? BSS WORD PUBLIC 'BSS' DGROUP .CONST CONST WORD PUBLIC 'CONST' DGROUP 在其它存储模型下,由伪指令".CODE"说明的代码段段名在"_TEXT"之前还要加上其模 块名(源程序名)。假设,某模块名为 ABC,则其缺省的代码段段名就为 ABC_TEXT。因此, 在这种情况下,程序的模块名或源程序名不要以数字开头。 例6.15 简化段定义的方法 .MODEL SMALL .STACK 128 .DATA MSG DB "Simplified Segment Directives.$" .CODE MOV AX, @DATA ;取数据段的段值 MOV DS, AX ;把给段寄存器 DS 赋值 MOV DX, offset MSG MOV AH, 9H INT 21h MOV AX, 4C00H INT 21h END 另外,在汇编程序 MASM 中,还提供了二组简化的代码伪指令:.STARTUP 和.EXIT。 、.STARTUP——在代码段的开始,用于自动初始化寄存器 DS、SS 和 SP; 、.EXIT——用于结束程序的运行,它等价于下列二条语句: MOV AH, 4CH INT 21h 当使用汇编程序 TASM 时,以上二条伪指令分别改为:STARTUPCODE 和 EXITCODE。 假设使用汇编程序 MASM,那么,例6.15可改写成例6.16的形式。 例6.16 .MODEL SMALL .STACK 128 .DATA MSG DB "Simplified Segment Directives.$" .CODE .STARTUP ;自动初始化寄存器 DS、SS 和 SP MO V DX, offset MSG MO V AH, 9H INT 21h .EXIT END 6.5 源程序的辅助说明伪指令 除了以上一些使用率较高的伪指令外,还有一些使用频率不太高的其它伪指令。下面仅 列举几个这样的伪指令。 6.5.1 模块名定义伪指令 NAME 模块名定义伪指令 NAME 说明该源程序的模块名。该伪指令的一般格式如下: NAME [模块名字符串] 6.5.2 页面定义伪指令 PAGE 在源程序的开始,可用伪指令 PAGE 说明每页的最大行数、每行的字符数。该伪指令的 一般格式为: PAGE [[行数], 宽度] 其中:“行数”的取值范围为[10, 255],“宽度”的取值范围为[60, 132]。 如:伪指令“PAGE 60, 80”说明每页最多有60行,每行最多有80个字符。 如果要在某指定行之后强行换页的话,那么,可在该行的下面书写不带操作数的伪指令 PAGE。 6.5.3 标题定义伪指令 TITLE 标题定义伪指令 TITLE 说明打印的标题,该标题可有60个字符。该伪指令在源程序头 部只能书写一次,其一般格式如下: TITLE [标题字符串] 如果程序中没有使用 NAME 伪操作,则汇编程序将用“标题字符串”的前六个字符串作 为其模块名。如果程序中既无 NAME 伪操作,也无 TITLE 伪指令,那么,源文件名将作为 模块名。 在汇编程序 TASM 环境下,标题定义伪指令是%TITLE。 6.5.4 子标题定义伪指令 SUBTTL/SUBTITLE 子标题定义伪指令 SUBTTL/SUBITLE 说明打印页上的子标题,该子标题也可有60个字 符,它在每页的第三行打印。该伪指令的一般格式如下: SUBTTL/SUBTITLE [标题字符串] 汇编入门(12讲) 时间:2009-5-18 19:03:56 核心提示:第 7章 子程序和库子程序是程序设计所常见的基本概念,汇编语言也提供 了编写子程序的方法。本章主要介绍子程序的定义、调用和返回、子程序的参数传递等知识。 此 后,还将讲解如何构造自己的子程序库。7.1 子程序的定义如果某程序段在源程序内反 复出现,那么,就可把该程序段定义为子程序。这样可以缩短源程序长度、节省... 第7章 子程序和库 子程序是程序设计所常见的基本概念,汇编语言也提供了编写子程序的方法。 本章主要介绍子程序的定义、调用和返回、子程序的参数传递等知识。此后,还将讲解 如何构造自己的子程序库。 7.1 子程序的定义 如果某程序段在源程序内反复出现,那么,就可把该程序段定义为子程序。这样可以缩 短源程序长度、节省目标程序的存储空间,也可提高程序的可维护性和共享性。 定义子程序的一般格式如下: PROC [NEAR | FAR] 子程序 名 … ;子程序体 子程序 名 ENDP 对子程序定义的具体规定如下: 、“子程序名”必须是一个合法的标识符,并前后二者要一致; 、PROC 和 ENDP 必须是成对出现的关键字,它们分别表示子程序定义开始和结束; 、子程序的类型有近(NEAR)、远(FAR)之分,其缺省的类型是近类型; 、如果一个子程序要被另一段的程序调用,那么,其类型应定义为 FAR,否则,其类 型可以是 NEAR。显然,NEAR 类型的子程序只能被与其同段的程序所调用; 、子程序至少要有一条返回指令,也可有多条返回指令。返回指令是子程序的出口语 句,但它不一定是子程序的最后一条语句; 、子程序名有三个属性:段值、偏移量和类型。其段值和偏移量对应于子程序的入口 地址,其类型就是该子程序的类型。 编写子程序除了要考虑实现子程序功能的方法外,还要养成书写子程序说明信息的好习 惯。其说明信息一般包括以下几方面内容: 、功能描述 、入口和出口参数 、所用寄存器 ;可选项,最好采用寄存器的保护和恢复方法,使之使用透 明化 、所用额外存储单元 ;可选项,可以减少为子程序定义自己的局部变量 、子程序的所采用的算 法 ;可选项,如果算法简单,可以不写 、调用时的注意事项 ;可选项,尽量避免除入口参数外还有其它的要求 、子程序的编写者 ;可选项,为将来的维护提供信息 、子程序的编写日期 ;可选项,用于确定程序是否是最新版本 这些说明性信息虽然不是子程序功能的一部分,但其他程序员可通过它们对该子程序的 整体信息有一个较清晰认识,为准确地调用它们提供直接的帮助,与此同时,也为实现子程 序的共享提供了必要的资料。 7.2 子程序的调用和返回指令 子程序的调用和返回是一对互逆操作,也是一种特殊的转移操作。 一方面,之所以说是转移,是因为当调用一个子程序时,程序的执行顺序被改变,CPU 将转而执行子程序中的指令序列,在这方面,调用子程序的操作含有转移指令的功能,子程 序的返回指令的转移特性与此类似; 另一方面,转移指令是一种“一去不复返”的操作,而当子程序完后,还要求 CPU 能转 而执行调用指令之下的指令,它是一种“有去有回”的操作。 为了满足子程序调用和返回操作的特殊性,在指令系统中设置了相应的特定指令。 7.2.1 调用指令(CALL) 调用子程序指令的格式如下: CALL 子程序名/Reg/Mem 子程序的调用指令分为近(near)调用和远(far)调用。如果被调用子程序的属性是近的, 那么,CALL 指令将产生一个近调用,它把该指令之后地址的偏移量(用一个字来表示的)压 栈,把被调用子程序入口地址的偏移量送给指令指针寄存器 IP 即可实现执行程序的转移。 近调用指令的堆栈操作如图7.1所示。 如果被调用子程序的属性是远的,那么,CALL 指令将产生一个远调用。这时,调用指 令不仅要把该指令之后地址的偏移量压进栈,而且也要把段寄存器 CS 的值压进栈。在此之 后,再把被调用子程序入口地址的偏移量和段值分别送给 IP 和 CS,这样完成了子程序的远 调用操作。远调用指令的堆栈操作如图7.2所示。 子程序调用指令本身的执行不影响任何标志位,但子程序体中指令的执行会改变标志 位,所以,如果希望子程序的执行不能改变调用指令前后的标志位,那么,就要在子程序的 开始处保护标志位,在子程序的返回前恢复标志位。 例如: CALL DISPLAY ;DISPLAY 是子程序名 CALL BX ;BX 的内容是子程序的偏移量 CALL WORD1 ;WORD1是内存字变量,其值是子程序的偏移量 CALL DWORD1 ;DWORD1是双字变量,其值是子程序的偏移量和段值 CALL word ptr [BX] ;BX 所指内存字单元的值是子程序的偏移量 CALL dword ptr [BX] ;BX 所指内存双字单元的值是子程序的偏移量和段值 7.2.2 返回指令(RET) 当子程序执行完时,需要返回到调用它的程序之中。为实现此功能,指令系统提供了一 条专用的返回指令。其格式如下: RET/RETN/RETF [Imm] 子程序的返回在功能上是子程序调用的逆操作。为了与子程序的远、近调用相对应,子 程序的返回也分:远返回和近返回。返回指令在堆栈操作方面是调用指令的逆过程(如图7.3 所示)。其具体规定如下: 、在近类型的子程序中,返回指令 RET 是近返回,其功能是把栈顶之值弹出到指令指针 寄存器 IP 中,SP 会被加2(如图7.3所示); 、在远类型的子程序中,返回指令 RET 是远返回,其功能是:先弹出栈顶之值到 IP 中, 再弹出栈顶之值到 CS 之中,SP 总共会被加4(如图7.4所示)。 如果返回指令后面带有立即数(其值通常为偶数),则表示在得到返回地址之后,SP 还要增加 的偏移量,它不是类似于高级语言中子程序的返回值(如图7.5所示)。 在 MASM 5.0及其以后版本中,可用指令 RETN 或 RETF 来显式地告诉汇编程序是本子 程序的返回是近返回,还是远返回。 例如: RET ;可能是近返回,也可能是远返回 RETN ;近返回指令 RETF ;远返回指令 RET 6 ;子程序返回后,(SP)←(SP) + 6 例7.1 编写一个子程序 UPPER,实现把寄存器 AL 中存放的字符变大写。 解: ;子程序功能:把 AL 中存放的字符变大写 ;入口参数:AL ;出口参数:AL ;算法描述:判断 AL 中字符必须在'a'~'z'之间才能把该字符变为大写 PROC CMP AL, 'a' ;书写'a'的 ASCII 码61H 也可以 JB over CMP AL, 'z' JA over UPPER SUB AL, 20H ;书写指令 AND AL, 0DFH 也可以 over: RET UPPER ENDP 例7.2 编写一个求字符串长度的子程序 StrLen,该字符串以0为结束标志,其首地址存放在 DS:DX,其长度保存在 CX 中返回。 解: ;子程序功能:求字符串的长度 ;入口参数:DS:DX 存放字符串的首地址,该字符串以0为结束标志 ;出口参数:CX 存放该字符串的长度 ;算法描述:用 BX 来指针来扫描字符串中的字符,如果遇到其结束标志,则停止扫描字 符串操作 PROC PUSH AX PUSH BX ;用堆栈来保存子程序所用到的寄存器内容 XOR CX, CX XOR AL, AL StrLen MOV BX, DX CMP [BX], AL JZ over INC CX ;增加字符串的长度 INC BX ;访问字符串的指针向后移 again: JMP again POP BX ;恢复在子程序开始时所保存的寄存器内容 POP AX over: RET StrLen ENDP 7.3 子程序的参数传递 子程序一般都是完成某种特定功能的程序段。当一个程序调用一个子程序时,通常都向 子程序传递若干个数据让它来处理;当子程序处理完后,一般也向调用它的程序传递处理结 果,我们称这种在调用程序和子程序之间的信息传递为参数传递。 用程序向子程序传递的参数称为子程序的入口参数,子程序向调用它的程序传递的参数 称为子程序的出口参数。子程序的入口参数和出口参数都是任意项,对某个具体的子程序来 说,要根据具体情况来确定其入口和出口参数,也可以二者都没有。 程序和被调用子程 序之间的参数传递方法是程序员自己或和别人事先约定好的信息传 递方法。这种信息传递方法可以是多种多样的,在本节,我们只介绍常用的、行之有效的参 数传递 方法有:寄存器传递参数、约定存储单元传递参数和堆栈传递参数等。如果对其它 的参数传递方法感兴趣的话,可参考其它《汇编语言程序设计》书籍。 7.3.1 寄存器传递参数 一方面,由于 CPU 中的寄存器在任何程序中都是“可见”的,一个程序对某寄存器赋值 后,在另一个程序中就能直接使用,所以,用寄存器来传递参数最直接、简便,也是最常 用 的参数传递方式。但另一方面,CPU 中寄存器的个数和容量都是非常有限,所以,该方法 适用于传递较少的参数信息。 例7.1是用寄存器传递参数的例子,子程序处理的数据被保存在寄存器 AL 中。假设有 下列的程序段: … MOV AL, 'b' CALL UPPER ;子返回时,(AL)='B' … MOV AL, '2' CALL UPPER ;子返回时,AL 的值不变,因为'2'不是字母 … 例7.3 按五位十进制的形式显示寄存器 BX 中的内容,如果 BX 的值小于0,则应在显示数值 之前显示负号'-'。 例如:(BX)=123,显示:00123;(BX)=-234,显示:-00234; 解: ;子程序功能:把寄存器 BX 的内容按十进制有符号数显示出来 ;入口参数:BX ;出口参数:无,只有显示信息 ; 算法描 述: 1、定义6个字节的存储单元 2、先判断 BX 是否小于零,如果是,则先显示负号'-',再取 BX 的绝对值; 3、采用除10,得余数的方法,从低位向高位求出每位十进制位; 4、输出数据的字符串。 SEGMENT SubData DB 5 DUP('0'), 0ah, 0dh, '$' ;0ah、0dh:换行、回车 SubData ENDS PROC ASS UMEDS:SubData PUS H DS PUS H DX PUS H CX PUS H AX MO V AX, SubData ;取子程序所用的数据区段地址 MO V DS, AX CMP BX, 0 JGE next MO V DL, '-' MO V AH, 2 INT 21H ;显示负号'-' DISPBX NEG BX ;求-BX,使其值为正数 MO V SI, 4 MO V AX, BX next: MO V CX, 10D XOR DX, DX IDIV CX ;DX 存放余数,AX 存放商 ADD DL, '0' MO V [SI], DL DEC SI JGE again XOR DX, DX again: MO V AH, 9 INT 21H ;调用中断21的功能9,显示 DS:DX 指向的字符串 POP AX POP CX POP DX POP DS RET DISPBX ENDP 7.3.2 约定存储单元传递参数 在调用子程序时,当需要向子程序传递大量数据时,因受到寄存器容量的限制,就不能 采用寄存器传递参数的方式,而要改用约定存储单元的传送方式。这种参数传递方式有点象 情报人员和联络人员之间的传递信息方式,一个向指定地点放情报,另一个从指定地点取情 报。 例7.2是采用约定存储单元传递参数的例子,所处理的数据不是直接传给子程序,而是 把存储它们的地址告诉子程序。 例7.4:编写一个子程序分类统计出一个字符串中数字字符、字母和其它字符的个数。该字 符串的首地址用 DS:DX 来指定(以0为字符串结束),各类字符个数分别存放 BX、CX 和 DI 中。 解: ;子程序功能:分类统计出字符串中数字字符、字母和其它字符的个数 ;入口参数:DS:DX 指向被统计的字符串 ;出口参数:BX、CX 和 DI 分别保存数字字符、字母和其它字符的个数 ; 算法描 述: 1、当字符在'0'~'9'范围时,数字字符个数 BX 加1; 2、为了判断简单,先把字字母变成大写字母; 3、当字符在'A'~'Z'范围时,字母个数 CX 加1; 4、否则,其它字符个数 DI 加1。 PROC PUS H AX PUS H SI XOR BX, BX XOR CX, CX XOR DI, DI ;上三条指令使各类字符计数清零 COUNT MO V SI, DX MO V AL, [SI] again: INC SI CMPAL, 0 JE over CMP AL, '0' JL other CMP AL, '9' JG next INC BX ;数字字符个数加1 JMP again CAL L UPPER ;调用子程序把 AL 中的字母变成大写字母 CMPAL, 'A' JL other CMPAL, 'Z' JG other INC CX ;字母个数加1 next: JMP again INC DI ;其它字符个数加1 other: JMP again POP SI POP AX over: RET COUNT ENDP 例7.5 显示出任意字符串中数字字符、字母和其它字符的个数。 解: .MOD EL SMALL .DATA MSG DB 'KSDJ L0984/[]3oiu OIU OIU (*&(5341', 0 .CODE .STARTUP LEA DX, MSG ;DS:DX 指向待统计的字符串 CAL L COUNT ;调用子程序统计出各类字符的个数 CAL L DISPBX ;调用子程序显示数字字符的个数 MOVBX, CX CAL L DISPBX ;调用子程序显示字母的个数 MOVBX, DI CAL L DISPBX ;调用子程序显示其它字符的个数 .EXIT 0 END 7.3.3 堆栈传递参数 堆栈是一个特殊的数据结构,它通常是用来保存程序的返回地址。当用它来传递参数时, 势必会造成数据和返回地址混合在一起的局面,用起来要特别仔细。 具体做法如下: (1)、当用堆栈传递入口参数时,要在调用子程序前把有关参数依次压栈,子程序从堆 栈中取到入口参数; (2)、当用堆栈传递出口参数时,要在子程序返回前,把有关参数依次压栈(这里还需要 做点额外操作,要保证返回地址一定在栈顶),调用程序就可以从堆栈中取到出口 参数。 在通常情况下,我们用堆栈传入口参数,用寄存器传出口参数。 1、用堆栈传递入口参数的调用方法: … PUSH Para1 … PUSH Paran ;把 n 个字的参数压栈 CALL SUBPRO ;调用子程序 SUBPRO … 2、在子程序中取入口参数的方法: 、段内调用子程序 由于是段内调用,所以,CALL 指令只把返回地址的偏移量(即 IP 的内容)压栈,如图7.6(a) 所示。在进入子程序后,为了能读取传递过来的参数,需要用 BP 来访问堆栈,所以要先保 护 BP 原来的值,再把当前 SP 的值传送给 BP。 于是,当前 BP 所指向的堆栈单元与最后一个参数 Paran 之间隔着 BP 的原值和返回地 址的偏移量,也就是说:二者之间相差4个字节。具体情况如图7.6(b)所示。 在子程序中读取用堆栈传递参数的一般方法如下程序片段所示。 PROC NEAR PUS H BP ;保护寄存器 BP MO V BP, SP ;用寄存器 BP 来访问堆栈,读取参数 … ;保护其它寄存器的指令 MO V Paran, [BP+4] ;保护其它寄存器的指令 … MO V Para1, [BP+4+2*(n-1)] SUBP RO … SUBP RO ENDP 、段间调用子程序 在段间调用子程序时,CALL 指令会把返回地址的偏移量和段寄存器 CS 的内容都压栈, 如图7.7(a)所示。在进入子程序后,与前面“段内调用子程序”一样,也需要用 BP 来读取传递 过来的参数,所以,也要先保护 BP 原来的值,再把当前 SP 的值传送给 BP。 这时,当前 BP 所指向的堆栈单元与最后一个参数 Paran 之间隔着 BP 的原值、返回地 址的偏移量和段地址,所以,二者之间相差6个字节。具体情况如图7.7(b)所示。 在段间调用时,除了多一个返回段地址外,其它的内容与“段内调用”的情况完全一致, 所以,在读取第 i 个参数时,只要用[BP+6+4*(n-i)]代替[BP+4+2(n-i)]即可(假设每个参数都 是字类型)。 7.4 寄存器的保护与恢复 由于计算机的硬件 资源只有一套,当子程序修改了寄存器的内容后,返回到调用它的 程序时,这些寄存器的内容也就不会是调用子程序前的内容。这样,子程序修改寄存器内容 就可能 变成了调用它的副作用,这种副作用常常会导致调用程序的出错。为此,在编写子 程序时,除了能对作为入口和出口参数的寄存器进行修改外,对其它寄存器的修改 对调用 程序来说都要是透明的,也就是说,在调用子程序指令的前后,除了作为入口和出口参数的 寄存器内容可以不同外,其它寄存器的内容要保持不变。有时,也 要求作为入口参数的寄 存器内容保持不变。 在子程序中,保存和恢复寄存器内容的主要方法是:在子程序的开始把它所用到的寄存 器压进栈,在返回前,再把它们弹出栈。这样编写的好处是该子程序可以被任何其它程序来 调用。在调用指令前,不需要保存寄存器,在调用指令后,也无需恢复寄存器。 利用堆栈来保存和恢复寄存器内容方法的一般形式如下: PROC PUS H REG1 … PUS H REGn ;把子程序要使用的寄存器压栈,REGi 代 表某个寄存器 … … … ;子程序的处理功能语句 POP REGn ;把前面压栈的寄存器弹出,注意它们的 次序 … POP REG1 XXX XX RET XXX XX ENDP 例7.2就是一个在子程序中利用堆栈来保存和恢复寄存器内容的例子。利用堆栈来实现 此项功能时,应注意以下几点: 、用堆栈保存和恢复寄存器的内容,要注意堆栈“先进后出”的操作特点; 、通常情况下不保护入口参数寄存器的内容,当然,也可以根据事先的约定而对它 们加以保护; 、如果用寄存器带回子程序的处理结果,那么,这些寄存器就一定不能加以保护; 、整个子程序的执行几乎肯定要改变标志位,可用 PUSHF 和 POPF 来保护和恢复标 志位,但一般在子程序中不保护标志位,除非有此特殊需要; 汇编入门(13讲) 时间:2009-5-18 19:08:52 点击:12 核心提示:7.5 子程序的完全定义在7.1节所给出的子程序定义格式是一个最基本的、最 简单的定义格式,它不能为子程序提供更简洁的调用方式。在宏汇编 MASM 6.11系统中, 为微机汇编语言的子程序提供了更加丰富的定义方式。虽然子程序的这种定义方式显得稍微 有点复杂,但它不仅为子程序的调用带来了极大的方 便,而且其调用... 7.5 子程序的完全定义 在7.1节所给出的子程序定义格式是一个最基本的、最简单的定义格式,它不能为子程 序提供更简洁的调用方式。在宏汇编 MASM 6.11系统中,为微机汇编语言的子程序提供了 更加丰富的定义方式。 虽然子程序的这种定义方式显得稍微有点复杂,但它不仅为子程序的调用带来了极大的 方便,而且其调用方式与高级语言中子程序的调用方式相一致,这就大大地降低了程序员熟 练掌握它的难度。 7.5.1 子程序完全定义格式 [distance] [langtype] [visibility] [] PRO C [USES 寄存器列表] [,参数[:数据类型]]... [LOCAL varlist] 子程 序名 子程序的程序体 子程 序名 ENDP 定义子程序时,可使用参数表来直接指明其所要的参数,但程序员必须先用.MODEL 伪指令,或使用参数来说明本子程序所使用的程序设计语言类型。 程序员在定义子程 序时,最好能象在高级语言(如:C/C++)定义过程那样,先说明该子 程序的原型(用伪指令 PROTO),这样,在调用时,系统可以自动进行类型检查,也 可以使 用更方便的调用伪指令 INVOKE 来调用该子程序。有关子程序的原型说明伪指令和调用伪 指令在随后第7.5.8和7.5.9小节中加以介绍。 子程序通常用 RET 指令来结束其执行,也可用指令“RET n”来指明在结束子程序执行后 从堆栈弹出 n 个字节。有关返回指令请参阅7.2.2节中的叙述。 汇编程序在处理子程序时能自动产生“起始”代码(PROLOGUE Code)和“结束”代码 (EPILOGUE code)。这两段特殊的代码分别完成:在调用子程序时,能把传递给子程序的参 数压栈,在子程序结束时能把先前压栈的参数弹出。有了这两段代码,程序员在调用子程序 时就不用自行考虑子程序的参数传递问题。 若子程序用指令 RETN、RETF 或 IRETF 作为子程序的结束指令,那么,汇编程序将不 生成“结束”代码。 程序员可以用自己定义宏来替代缺省的“起始”和“结束”的代码段。这种替代方法是使用 伪指令:OPTION PROLOGUE 和 OPTION EPILOGUE。 若子程序没有参数、局部变量,没使用 USES 子句,也不会产生新的段或段组,那么, 子程序是可以嵌套定义的。程序员也可以使用返回指令 RETN 和 RETF 来避免子程序的嵌 套。 在子程序内部,可以在指令之前使用伪指令 LOCAL 来说明其局部变量,有关规定在随 后的第7.5.10节中有详细的说明。 下面就来介绍该定义格式中各个说明项的作用。 7.5.2 子程序的位距 子程序的位距(Distance)有:Near、Far、Near16、Far16、Near32和 Far32。 子程序位距描述符 告诉汇编程序该子程序是在本段之内(Near),还是在本段之外(Far)。 Near 和 Far 描述符表示使用当前的段规模(Segment Size),Near16、Far16、Near32和 Far32 描述符是告诉汇编程序忽略当前的段规模,而使用指定16位或32位的段规模。 若选用类型 Near 或 Far,那么,汇编程序将根据当前段的规模来决定选用16位,还是32 位的 Near 或 Far。 若程序员不指定该选项,那么汇编程序将根据当前的存储模式(由.MODEL 来决定)和处 理机类型来决定子程序类型。若不使用伪指令.MODEL,那么,Near 是缺省的类型。 7.5.3 子程序的语言类型 子程序语言类型 (Language Type)可以是任何一种有效的程序设计语句类型,由它来告 诉汇编程序将使用什么样的标识符的命名风格、子程序的调用和返回约定。该语言类型说明 可使汇编 语言程序与其它语言程序达到共享的目的。所有有效的语言类型及其书写规定如 表7.1所列。 表7.1 语言类型及其书写规定 C SYSC ALL STDC ALL Basic Fortran Pascal 前导下划线 X X 字母大写化 X X X 参数从左到右 X X X 参数从右到左 X X X 调用程序清空堆 栈 X * 保存指针寄存器 BP X X X 使用 VARARG 参 数 X X X *若使用:VARARG 参数,则调用程序清空堆栈,否则,被调用的子程序清空堆栈。 程序员可用另外三种方法来设置程序的语言类型:.MODEL、OPTION LANGTYPE:和 命令行选项/Gx。若在程序和命令行中都说明了语言类型,那么,前者的说明优先。 另外,程序员也可用命令行选项/H 来限定标识符的最大长度。 例如: Pascal 语言风格:OPTION LANGUAGE:PASCAL、/Gc C 语言风格:OPTION LANGUAGE:C、/Gd 7.5.4 子程序的可见性 子程序的可见性(Visibility)决定该子程序对其它模块是否可用。它共有三个属性值: PRIVATE、PUBLIC 和 EXPORT。 PUBLIC 属性是子程序标准的缺省属性,但该缺省属性可以用伪指令 OPTION PROC 来 修改。EXPORT 属性意味着该子程序是一个“远”的、具有 PUBLIC 属性的子程序,并要求连 接程序在生成可执行文件时把其入口地址放入导出入口地址表中。 例如: OPTION PROC : PRIVATE ;说明子程序的可见性为:PRIVATE OPTION PROC : EXPORT ;说明子程序的可见性为:EXPORT 7.5.5 子程序的起始和结束操作 当程序员想用自己定义的宏来替代缺省的“起始”和“结束”的代码段时,可用下列说明语 句来实现: OPTION PROLOGUE : MacroName1 OPTION EPILOGUE : MacroName2 PROLOGUE 和 EPILOGUE 分别指定 MacroName1和 MacroName2为“起始”和“结束”代 码段的宏名。 汇编程序对用户定义的宏 MacroName1和 MacroName2的形式有较严格的规定,要求宏 的定义形式如下: MacroName MACRO ProcName, flags, argbytes, localbytes, , userparms:VARARG 该宏定义的每个参数都有详细的说明,感兴趣的读者可看有关技术资料或 MASM 6.11 中的帮助,详细的说明在此从略,但建议使用缺省的宏。 如果想取消当前指定的宏名,而恢复使用缺省的“起始”和“结束”代码段的宏名,那么, 可用下列说明语句,即指定二个缺省的宏名 PrologueDef 和 EpilogueDef。 OPTION PROLOGUE : PrologueDef OPTION EPILOGUE : EpilogueDef 若程序员不要汇编程序自动产生“起始”和“结束”代码,则可用 NONE 来代替说明语句 中的宏名,即: OPTION PROLOGUE : NONE OPTION EPILOGUE : NONE 7.5.6 寄存器的保护和恢复 保护寄存器说明子句的说明格式: USES 寄存器列表 该说明子句要求汇编程序为其生成保护和恢复寄存器的指令序列,即:在进入子程序执 行指令之前,把寄存器列表中的寄存器压进堆栈,在结束子程序执行时,把先前压进堆栈的 寄存器弹出,以达到保护寄存器的目的。 寄存器列表:列举出在子程序中需要保护的寄存器名,即:在子程序开始时需要把内容 进栈的寄存器名。若有多个寄存器名,则在寄存器名之间要用“空格”来分开。 例如: D s i p PROC USES AX DX, FUNC:WORD, MSG:PTR BYTE M O V DX, MSG M O V AX, FUNC I N T 21H RET D i ENDP s p 汇编程序在处理该子程序时,会根据子句 USES 的作用,在第一条指令“MOV DX, MSG” 之前,插入把寄存器 AX 和 DX 进栈的指令序列,即: PUSH AX PUSH DX 而在返回指令 RET 之前插入把寄存器 DX 和 AX 的值弹出的指令序列,即: POP DX POP AX 注意:若子程序含有多个 RET 或 IRET 指令,那么,汇编程序在每个 RET 或 IRET 指 令前都将增加相应的弹出堆栈指令序列。 从子句 USES 的功能来看,它与前面7.4节“寄存器的包含与恢复”中所用的方法完全一 致,所不同的是:用 USE 子句进行寄存器保护和恢复的代码是由汇编程序自动产生的,程 序员不用关心如何去做,有点象高级语言的编程风格,而7.4节中的代码则是由程序员自己 来安排的。 7.5.7 子程序的参数传递 子程序参数是用来向子程序传递信息的数据。若有多个参数,则参数之间要用逗号分割。 为了能说明子程序的参数,程序员必须事先指定参数所遵循的语言类型或使用“语言类型” 参数。 参数的数据类型可以是任何一个有效的数据类型说明符或 VARARG。VARARG 数据类 型允许向子程序传递“个数”不定的参数,其参数之间要用逗号“,”来分开。 若参数表中含有 VA R A R G 说明的参数,那么,该参数一定是该子程序的最后一个参数。 其规定隐含地说明了在参数表中只能有一个用 VARARG 说明的参数。 当子程序的语言类型是 C、SYSCALL 和 STDCALL 时,在其参数表中才能使用 VA R A R G 数据类型的参数。见前面的表7.1中所列。 如果没有显式地指定某个参数的数据类型,那么,在16位段规模的情况下,其缺省的数 据类型是 WORD;在32位段规模的情况下,其缺省的数据类型是 DWORD。 7.5.8 子程序的原型说明 子程序原型的说明格式如下: 子程序名 PROTO [distance] [langtype] [,[parameter]:tag]... 该说明语句告诉汇编程序该子程序的若干属性,如:位距、语语言类型、参数个数及其 类型等。这样,汇编程序就可以对其定义进行适当的检查。 如果对所有基于堆栈的过程都定义一个原型,那么,就可把这些原型存放在一个独立的 包含文件(用伪指令 INCLDUE 来装入)中。使用这种方法对将来把所有子程序放入自定义的 库文件中是非常方便的。 该原型说明语句中参数 distance、langtype、parameter 和 tag 等的含义与前面的叙述相一 致,在此不再重复。 7.5.9 子程序的调用伪指令 子程序调用伪指令 INVOKE 与子程序的调用指令 CALL 在功能上是一致的,但它使汇 编语言的子程序调用方法高级语言化,程序员可不用理会一些调用细节问题。 调用伪指令 INVOKE 的使用格式如下: INVOKE expression [, arguments] 其中:expression—地址表达式,通常为子程序名; arguments—传递的各参数之间用逗号','分开,参数可以是寄存器、表达式或 ADDR 标识符等。 该伪指令是调用基于堆栈的子程序的方法,它把所有参数压栈,子程序结束时,又把参 数自动弹出堆栈。 在参数传递时,汇编程序将根据子程序的原型进行数据类型检查。若需要进行参数类型 转换的话,汇编程序则会自动生成一段代码来满足其数据类型转换的要求。 例如: INVOKE TEST, AX, 12+34, ADDR MSG 其中:TEST 是子程序名,寄存器 AX 和表达式“12+34”是参数,“ADDR MSG”是传递 变量 MSG 的地址。 例7.6 编写一个累加参数数值的子程序。其中参数的个数不定,参数的个数由第一个参数来 确定。 解: .MODEL SMALL .STACK 256 .CODE ;第一个参数 parmcount 确定其后面参数 parmvalues 中所含参数的个数 ADDUP PROC NEAR C, parmcount:WORD, parmvalues:VARARG XOR AX, AX XOR SI, SI MOV CX, parmcount .REPEAT ADD AX, parmvalues[SI] ADD SI, 2 .UNTILCXZ RET ENDP .STARTUP INVO KE ADDUP, 3, 5, 2, 4 ;调用子程序 ADDUP,计算5+2+4 INVO KE ADDUP, 4, 1, 2, 3, 4 ;调用子程序 ADDUP,计算1+2+3+4 .EXIT 0 ADDUP .END 7.5.10 局部变量的定义 局部变量的定义格式: LOCAL 变量名[[数量]] [:数据类型] [,变量名[[数量]] [:数据类型]]... 伪指令 LOCAL 的作用是说明一个或多个临时的局部变量(位于堆栈中)。局部变量必须 在任何指令之前加以说明,并可用多个 LOCAL 伪指令来说明其局部变量。 在子程序中,若说明了某个局部变量,则子程序体中的指令就可使用该局部变量。汇编 程序会把对它的引用转换成用指针寄存器 BP 来访问其在堆栈中的实际存储单元。 在局部变量的作用 域与高级语言中局部变量的作用域相一致,即:局部变量只能在当 前子程序中使用,离开该子程序,它们就不能再被引用。但在局部变量的命名规则上有所不 同,高 级语言中的局部变量可与外层变量同名,而汇编语言中的局部变量不能与其它任何 变量同名,否则,在汇编时,将会给出“重定义”(Symbol redefinition)的错误信息。 “数量”用来说明该变量所具有的元素个数。象高级语言的数组定义一样,该数量必须写 在括号“[ ]”之中。“数量”说明项是可选项。 局部变量的类型说明符可以是任何合法的数据类型说明符。在16位段环境下,该缺省的 数据类型是 WORD,而在32位段环境下,该缺省的数据类型是 DWORD。 此处伪指令 LOCAL 的作用与9.3.1节中伪指令 LOCAL 的作用是完全不同的,具体的差 异请见9.3.1节中的比较。 例如: LOCAL data[20]:BYTE, num:WORD 在上例的说明中,定义了二个局部变量:data 和 num。前者是字节类型,并有20个元素, 后者是字类型,只有其自身1个元素。 7.6 子程序库 库文件对学过 C/C++语言程序设计的读者来说应该是不会陌生的,该语言的程序设计环 境提供了大量的库文件,也就是说,提供了大量的标准函数或过程。在本节里,介绍读者如 何创建自己的库文件。 7.6.1 建立库文件命令 LIB 宏汇编 MASM 系统提供了建立库文件的命令文件 LIB.EXE。其通常是在命令行环境 (MS-DOS 方式)下使用的,当然,也可在 Windows 95/98等环境下利用其“开始”菜单下的“运 行”功能项来使用。 一、MS-DOS 系统 显示命令 LIB 用法的命令如下: …>lib /? 该命令的显示结果如图7.9中所示。 二、Windows 系统 命令 LIB 的使用方式和显示结果如图7.8和7.9所示。 三、命令显示内容的解释 1)、各选项的解释 选项 含义 /?、/HELP 显示 LIB 命令的用法,描述各命令行参数的含义 /IGNORECASE 忽略子程序名中的大小写 /NOIGNORECAS E 不忽略子程序名中的大小 写 在实践中,作用不明显 /NOEXTDICTION ARY 不建立扩展的目录 /NOLOGO 不显示版本号和版权信息 /PAGESIZE:n 设置库文件的每页字节数为 n 2)、命令项的解释: 选项 含义 +name 向库文件中加一个新的目标文件 -name 从库文件中删除一个指定的目标文件 -+name 用新的目标文件替换掉库文件中原有的目 标文件 *name 拷贝出指定的目标文件 -*name 从库文件中移出指定的目标文件 在弄懂了 LIB 的各项功能含义后,读者就可根据自己的需要来建立库文件了。 7.6.2建立库文件举例 假设现有目标文件 sub1.obj、sub2.obj 和 sub3.obj,要用它们建立库文件 mylib.lib。可用 下列方法来建立该库文件: 方法1:所有目标文件都准备好了,可一次性把它们加入到库文件中 …>lib mylib +sub1 +sub2 +sub3 方法2:随着目标文件的逐个生成,而依次把它们加入到库文件中 …>lib mylib +sub1 …>lib mylib +sub2 …>lib mylib +sub3 假如源文件 sub3.asm 已修改,并也生成了新的目标文件 sub3.obj,这时,就需要把库文 件 mylib.lib 中的 sub3.obj 替换成新的目标文件。于是,可用下面命令来实现替换: …>lib mylib -+sub3 当提示输入目标库文件名(Output library)时,可按“回车”用默认的原库文件名。 如果想查看库文件 mylib.lib 中各文件的大小和存放的先后次序,可用下列命令: …>lib mylib, list ;把库文件 mylib.lib 中的文件结构生成到文件 list 中 …>type list 7.6.3 库文件的应用 在开发一个功能较 弱的应用程序时,其执行文件通常可由一个目标文件连接而成,当 开发一个功能较强、关系较复杂的应用程序时,其执行文件很难由一个目标文件连接而成, 常常是 由多个目标文件(模块)连接而成的。各模块之间无疑会存在着相互调用、相互访问 数据单元等内在联系,各模块之间的相互联系就产生了这样的问题:程序员如何 在源程序 中来表达这种联系? 为了解决描述各模块之间的联系,汇编语言提供了二条伪指令 PUBLIC 和 EXTRN,它 们的作用有点象 C/C++语言说明变量、过程和函数是“全局的”或“外部的”。 这二条伪指令的具体用法和含义如下: 1、伪指令 PUBLIC 伪指令 PUBLIC 是用来说明:当前模块中哪些标识符是能被其它模块引用的公共标识 符。其说明的一般格式如下: PUBLIC 标识符1, 标识符2, …… 其中:“标识符”可以是变量名、过程名和程序标号,各标识符之间要用逗号分开。 上面说明语句说明了标识符1、标识符2等是公共标识符,可以被其它模块引用。在一个 模块中,可用多条 PUBLIC 伪指令来说明公共标识符。 2、伪指令 EXTRN 伪指令 EXTRN 是用来说明:在当前模块所使用的标识符中,哪些标识符是已在其它模 块中被定义为指定类型的标识符。如果当前模块使用了其它模块的标识符,而对它又不加以 说明的话,那么,在汇编时,汇编程序将会给出下列出错信息: error nnnnn: undefined symbol : XXXXXX 其中:“nnnnn”是错误号,“XXXXXX”是当前模块中没有定义的标识符。 伪指令 EXTRN 的一般说明格式如下: EXTRN 标识符1:类型1, 标识符2:类型2, …… 其中:“标识符”和“类型”之间要用冒号“:”连接。 上面语句说明了标识符1、标识符2等是外部标识符,它们在其它模块中已被分别定义为 类型1、类型2等,该类型说明符可以是:NEAR、FAR、BYTE、WORD、DWORD 等之一。 如果在一条说明伪指令中说明了多个标识符,那么,各标识符之间要用逗号分开。 在一个模块中,可用多条 EXTRN 伪指令来说明本模块所引用的外部标识符。 注意:伪指令 EXTRN 中所说明的标识符必须在其定义的模块中被 PUBLIC 伪指令说明 为公共标识符,并且其说明的标识符类型要与该标识符在定义是的类型相一致,否则,要么 不能生成其可执行文件,要么其执行文件不能正确运行。 例7.7 把例7.3、7.4和7.5合并在一起生成一个可执行文件,假设它们所对应的源程序名分别 为 Count.ASM、DispBX.ASM 和 Main.ASM。 解:由于在源文件 Count.ASM 中调用了子程序 UPPER,所以,例7.1的程序也必须加入到本 题中。假设其源文件名为 Upper.ASM。 由于生成本题的执行文件需要四个模块,模块之间存在着调用关系,所以,在有关源文 件中需要说明某些标识符为外部属性,或说明其为公共属性。 为了把前面例子中的子程序改写成可汇编的程序,需要添加一些简单的说明语句或进行 简单修改,其添加或改写的部分已在下面用“下划线”表示出来。 ;源文件 Upper.ASM ;子程序说明信息:…… PUBLIC UPPER SegUpr SEGMENT 'code' PROC FAR UPPER …… ;例7.1中的程序段,在此从略 UPPER ENDP ENDS SegUpr END ;源文件 DispBX.ASM ;子程序说明信息:…… PUBLIC DISPBX SEGMENT SubData DB 5 DUP('0'), 0ah, 0dh, '$' SubData ENDS SegDisp SEGMENT 'code' PROC FAR DISPBX …… ;例7.3中的程序段,在此从略 DISPBX ENDP ENDS SegDisp END ;源文件 Count.ASM ;子程序说明信息:…… PUBLIC COUNT EXTRN UPPER:FAR SegCou nt SEGMENT 'code' PROC FAR COUNT …… ;例7.4中的程序段,在此从略 COUNT ENDP ENDS SegCou nt END ;源文件 Main.ASM EXTRN COUNT:FAR, DISPBX:FAR .MODE L SMALL .DATA STR DB 'KSDJ L0984/[]3oiu OIU OIU (*&(5341', 0 .CODE .STARTUP LEA DX, STR CALL COUNT ;调用子程序统计出各类字符的个数 CALL DISPBX ;调用子程序显示数字字符的个数 MOV BX, CX CALL DISPBX ;调用子程序显示字母的个数 MOV BX, DI CALL DISPBX ;调用子程序显示其它字符的个数 .EXIT 0 END 经过以上改写后,可用下列命令把它们分别汇编成目标文件(假设已安装了 MASM 编程 环境): …>MASM upper …>MASM dispbx …>MASM count …>MASM main 有了这些目标文件后,可用以下二种方法来生成可执行文件。 方法1:把所有的目标文件连接在一起 …>link main+upper+count+dispbx 方法2:把目标文件 upper.obj、count.obj 和 dispbx.obj 加到自己开发的库文件中,然后 在连接时,与该库文件连接。 …>lib mylib +upper +count +dispbx …>link main Microsoft (R) Segmented Executable Linker Version 5.31.009 Jul 13 1992 Copyright (C) Microsoft Corp 1984-1992. All rights reserved. Run File [main.exe]: List File [nul.map]: Libraries [.lib]: mylib ;输入要连接的库文件,可用加号“+”连接多个库 文件 Definitions File [nul.def]: LINK : warning L4021: no stack segment …>main ;运行生成的文件 以上各步骤也可由集成开发环境 PWB 来完成,具体介绍请见附录1。另外,当模块的 指令条数较少时,也可以把几个子模块合在一个源文件中。 7.6.4 库文件的好处 程序员在编写源程 序时,通常采用模块化的思想来组织源程序:把各类不同的子程序 分别编写在不同的源程序中,在各源程序中说明所用到的在其它模块中定义,或说明本模块 的定义 子程序可被其它模块调用。这样组织后,就可以分别汇编它们而得到其相应的目标 文件,在有了这些目标文件后,就可生成最终的可执行文件,但可用不同的方法来 生成最 终的可执行目标文件。 方法1:直接连接目标文件而生成可执行文件(如上节例7.6中的方法1所示) 这种方法简单、方便,也是常用的一种方法,但在连接时,LINK 程序会把目标文件中 的所有代码都嵌入到执行文件中,从而使得:包含在某目标文件中、但并没有被调用的子程 序代码也出现在执行文件中。这种情况无疑增加了执行文件的字节数。 方法2:采用子程序库的方法(如上节例7.6中的方法2所示) 库文件可以把它看 成是子程序的集合。库文件中存储着子程序名、子程序的目标代码 以及连接所需要的重定位信息。当某目标文件与库文件相连接时,LINK 程序只把目标文件 所用 到的子程序从库文件中找出来,并合并到最终的可执行文件中,而不是把库中所含的 全部子程序都纳入最后的可执行文件。 对照方法1和2可知:用库文件来存储子程序可生成较短的执行文件。 汇编入门(14讲) 时间:2009-5-18 19:18:38 点击:10 核心提示:第 8章 输入输出和中断输入输出功能是计算机的重要组成部分,是人—机 交互功能的主要承担者。在早期的计算机系统中,通常把输入输出设备或功能作为次要的部 分,而 把 CPU 作为主要研究对象。但现在随着输入输出设备的日益丰富、功能要求越来越 复杂,输入输出部分在整个计算机系统中的地位也得到了进一步提高。本章先介 绍了... 第8章 输入输出和中断 输入输出功能是计 算机的重要组成部分,是人—机交互功能的主要承担者。在早期的 计算机系统中,通常把输入输出设备或功能作为次要的部分,而把 CPU 作为主要研究对象。 但现 在随着输入输出设备的日益丰富、功能要求越来越复杂,输入输出部分在整个计算机 系统中的地位也得到了进一步提高。 本章先介绍了 I/O 的基本概念和 I/O 指令,再叙述了中断的概念及其工作过程,并列举 出计算机系统中若干个常用的中断及其功能。 8.1 输入输出的基本概念 输入输出是一个完整应用程序的重要组成部分,是交互式应用程序不可缺少的组成部 分。 在用高级语言编程 时,程序员可直接用输入输出语句来完成键盘输入、屏幕显示或打 印输出等需求,而无需关心这些输入输出语句是如何实现的,因为编译程序会自动把这些语 句转换 成相应的输入输出指令。但如果用汇编语言编写程序的话,情况就不同了,因为汇 编语言是与机器有关的程序设计语言,要编写出具有输入输出功能的代码段就必须 清楚 CPU 为输入输出提供了哪些指令,或计算机系统提供了哪些可直接使用的功能调用。 8.1.1 I/O 端口地址 I/O 端口是 CPU 与输入输出设备的交换数据的场所,通过 I/O 端口,处理机可以接受从 输入设备输入的信息;也可向输出设备发送信息。在计算机系统中,为了区分各类不同的 I/O 端口,就用不同的数字给它们进行编号,这种对 I/O 端口的编号就称为 I/O 端口地址。 按照 每次可交换一个字节数据的端口称为字节端口,每次可交换一个字数据的端口称为字端 口。 在 Intel 公司的 CPU 家族中,I/O 端口的地址空间可达64K,即可有65536个字节端口, 或32768个字端口。这些地址不是内存单元地址的一部分,不能普通的访问内存指令来读取 其信息,而要用专门的 I/O 指令才能访问它们。虽然 CPU 提供了很大的 I/O 地址空间,但 目前大多数微机所用的端口地址都在0~3FFH 范围之内,其所用的 I/O 地址空间只占整个 I/O 地址空间的很小部分。表8.1列举了几个重要的 I/O 端口地址。 表8.1 几个重要的 I/O 端口地址 端口地址 端口名称 端口地址 端口名称 020H~023H 中断屏蔽寄存器 378H~37FH 并行口 LPT2 040H~043H 时针/计数器 3B0H~3BBH 单色显示器端口 060H 键盘输入端口 3BCH~3BFH 并行口 LPT1 061H 扬声器(0, 1位) 3C0H~3CFH VGA/EGA 200H~20FH 游戏控制口 3D0H~3DFH CGA 278H~27FH 并行口 LPT3 3F0H~3F7H 磁盘控制器 2F8H~2FFH 串行口 COM2 3F8H~3FFH 串行口 COM1 计算机在启动时,BIOS 程序(Basic Input/Output System)将检查计算机系统中有哪些端口地 址。当发现有串行端口地址时,BIOS 就把 该端口存放在以地址40:00H 开始的数据区内;当发 现有并行端口地址时,BIOS 会把它 存入以地址40:08H 开始的数据区内。 每类端口有4个字的空间,对有二个串行口、二个并行口的计算机系统,其 BIOS 程序将得 到如图8.1所示的部分数据表。 图中03F8H、02F8H、0378H 和0278H 分别为 COM1、COM2、LPT1和 LPT2的端口地址。 8.1.2 I/O 指令 由于 I/O 端口地址和内存单元地址是相互独立的,这些端口地址不能普通的访问内存指令 来访问其信息,所以,在 CPU 的指令系统中就专门设置了 I/O 指令来存取 I/O 端口的信息。按 功能分类来看,I/O 指令应属于数据传送指令。 1、输入指令 IN 输入指令 IN 的一般格式如下: IN AL/AX, PortNo/DX 该指令的作用是从端口中读入一个字节或字,并保存在寄存器 AL 或 AX 中。如果某输入 设备的端口地址在0~255范围之内,那么,可在指令 IN 中直接给出,否则,要把该端口地址先 存入寄存器 DX 中,然后在指令中由 DX 来给出其端口地址。 例如: IN AL, 60H ;从端口60H 读入一个字节到 AL 中 IN AX, 20H ;把端口20H、21H 按“高高低低”组成的字读入 AX MOV DX, 2F8H IN AL, DX ;从端口2F8H 读入一个字节到 AL 中 IN AX, DX ;把端口2F8H、2F9H 按“高高低低”组成的字读入 AX 2、输出指令 OUT 输出指令 OUT 的一般格式如下: OUT PortNo/DX, AL/AX 该指令的作用是把寄存器 AL 或 AX 的内容输出到指定端口。如果某输出设备的端口地址 在0~255范围之内,那么,可在指令 OUT 中直接给出,否则,要把该端口地址先存入寄存器 DX 中,然后在指令中由 DX 来给出其端口地址。 例如: OUT 61H, AL ;把 AL 的内容输出到端口61H 中 OUT 20H, AX ;把 AX 的内容输出到端口20H、21H 中 MOV DX, 3C0H OUT DX, AL ;把 AL 的内容输出到端口3C0H 中 OUT DX, AX ;把 AX 的内容输出到端口3C0H、3C1H 中 有关字符串的输入输出指令,请见5.2.11节中的介绍。 8.2 中断 在计算机系统中,引入中断的最初目的是为了提高系统的输入输出性能。随着计算机应用 的发展,中断技术也应用到计算机系统的许多领域,如:多道程序、分时系统、实时处理、程 序监视和跟踪等领域。 8.2.1 中断的基本概念 下面只简单介绍与汇编语言程序设计有关的中断知识,使本章的知识具有一定完整性。有 关中断的详细介绍可参阅《计算机组成原理》课程中的相关章节。 1、中断和中断源 所谓中断就是 CPU 暂停当前程序的执行,转而执行处理紧急事务的程序,并在该事务处 理完后能自动恢复执行原先程序的过程。在此,称引起紧急事务的事件为中断源,称处理 紧急 事务的程序为中断服务程序或中断处理程序。 计算机系统还根据紧急事务的紧急程度,把中断 分为不同的优先级,并规定:高优先级的中断能暂停低优先级的中断服务程序的执行。 计算机系统有上百种可以发出中断请求的中断源,但最常见的中断源是:外设的输入输出 请求,如:键盘输入引起的中断,通信端口接受信息引起的中断等;还有一些计算机内部的异 常事件,如:0作除数、奇偶校验错等。 CPU 在执行程序时,是否响应中断要取决于以下三个条件能否同时满足: (1)、有中断请求; (2)、允许 CPU 接受中断请求; (3)、一条指令执行完,下一条指令还没有开始执行。 条件(1)是响应中断的主体。除用指令 INT 所引起的软件中 断之外,其它中断请求信号是随机产生的,程序员是无法预见的。 程序员可用程序部分地控制条件(2)是否满足,即可用指令 STI 和 CLI 来允许或不允许 CPU 响应可屏蔽的外部中断。而对 于不可屏蔽中断和内部中断,CPU 一定会响应它们的,程序员 是无控制权的。CPU 一定会执行这些中断的中断服务程序。 2、断向量表和中断服务程序 中断向量表是一个特殊的线性表,它保存着系统所有中断服 务程序的入口地址(偏移量和段地址)。在微机系统中,该向量表 有256个元素(0~0FFH),每个元素占4个字节,总共1K 字节,其 在内存中的存储形式及其存储内容如图8.2所示。 图8.2中的“中断偏移量”和“中断段地址”是指该中断服务程 序入口单元的“偏移量”和“段地址”。由此不难看出:假如中断号 为 n,那么,在中断向量表中存储该中断处理程序的入口地址的 单元地址为:4n。 表8.2说明了前16个中断向量表中列举了部分常用的中断 号。 表8.2 部分常用的中断号及其含义 中断号 含义 中断号 含义 0 除法出错 8 定时器 1 单步 9 键盘 2 非屏蔽中断 A 未用 3 断点 B COM2 4 溢出 C COM1 5 打印屏幕 D 硬盘(并行口) 6 未用 E 软盘 7 未用 F 打印机 8.2.2 引起中断的指令 中断处理程序基本上是系统程序员编写好的,是为操作系统或用户程序服务的。为了在应 用程序中使用中断服务程序,程序员必须能够在程序中有目的地安排中断的发生。为此,指令 系统提供了各种引起中断的指令。 1、中断指令 INT 中断指令 INT 的一般格式如下: INT Imm 其中:立即数 Imm 是一个0~0FFH 范围内的整数。 指令执行的步骤: ◆、把标志寄存器压栈,清除标志位 IF 和 TF; ◆、把代码段寄存器 CS 的内容压栈,并把中断服务程序入口地址的高字部分送 CS; ◆、32位段,压32位 IP。 在该指令执行完后,CPU 将转去执行中断服务程序。由于有了指令 INT,程序员就能为满 足某种特殊的需要,在程序中有目的地安排中断的发生,也就是说,该中断不是随机产生的, 而是完全受程序控制的。 一般情况下,一个中断可有很多不同的功能,每个功能都有一个唯一的功能号,所以,在 安排中断之前, 程序员还要决定需要该中断的哪个功能,中断的功能号都是由 AH 来确定的。 有些中断还需要其它参数,常用中断的功能和参数如附录3所列。 2、溢出指令 INTO 当标志位 OF 为1时,引起中断。该指令的格式如下: INTO 该指令影响标志位:IF 和 TF。 8.2.3 中断返回指令 当一个中断服务程序执行完毕时,CPU 将恢复被中断的现场,返回到引起中断的程序中。 为了实现此项功能,指令系统提供了一条专用的中断返回指令。该指令的格式如下: IRET/IRETD 该指令执行的过程基本上是 INT 指令的逆过程,具体如下: ◆、从栈顶弹出内容送入 IP; ◆、再从新栈顶弹出内容送入 CS; ◆、再从新栈顶弹出内容送入标志寄存器; 对80386及其以后的 CPU,指令 IRETD 从栈顶弹出32位内容送入 EIP。 8.2.4 中断和子程序的比较 中断和子程序调用之间有其相似和不同之 处。它们的工作过程非常相似,即:暂停当前程 序的执行,转而执行另一程序段,当该程序段执 行完时,CPU 都自动恢复原程序的执行。 如图8.3所示。 它们的主要差异有: ◆、 子程序调用一定是程序员在编写源程序时事先安排好的,是可知的,而中断是由中断源 根据自身的需要产生的,是不可预见的(用指令 INT 引起的中断除外); ◆、 子程序调用是用 CALL 指令来实现的,但没有调用中断的指令,只有发出中断请求的 事件(指令 INT 是发出内部中断信号,而不要理解为调用中断服务程序); ◆、 子程序的返回指令是 RET,而中断服务程序的返回指令是 IRET/IRETD。 ◆、 在通常情况下,子程序是由应用系统的开发者编写的,而中断服务程序是由系统软件设 计者编写的。 8.3. 中断功能的分类 计算机系统有上百种中断,若按中断的性质来划分,则系统中的中断可分为:可屏蔽中断 和不可屏蔽中断。对不可屏蔽中断,程序员不能控制它,系统肯定会立即响应的,而对于可屏 蔽中断,汇编语言程序员可以通过指令 CLI 和 STI 来控制对它们的响应。 若按中断源来划分,则系统中的中断又可分为:硬件 中断和软件中断。对于硬件中断,程序员不能控制它,它 们基本上是随机产生的,而对于软件中断,汇编语言程序 员可通过指令 INT 和 INTO 来有目的安排它们的。 下面主要介绍汇编语言程序员能控制的软件中断的功 能及其使用方法,常用的这类中断有:DOS 功能调用(INT 21H)、BIOS 中断、硬件和外设的中断等。 图8.4给出了程序员可使用的各类中断之间的层次关 系。 在用户程序中,若直接通过端口来操作硬件或外设,那么,其处理过程没有额外的多余工 作,处理速度显然是最快的,但这样做,无疑使用户程序具有了很大的局限性。硬件环境的改 变将直接影响程序的正常运行。 若用户程序通过调用 DOS 功能来实现其所需功能,那么,应用程序与低层硬件相距较远, 操作最终的对象需要经过中间环节,处理速度肯定受到一定的损失,但这种应用程序适应性强, 应用范围广,对硬件的依赖性最小。 由于 BIOS 介于 DOS 和具体硬件之间,所以,调用 BIOS 的功能是一个很好的折中方案。 程序员可在以下三种情况下考虑使用 BIOS 的功能: 1)、BIOS 提供的功能,而 DOS 没有提供该功能的情况; 2)、不能利用 DOS 功能调用的情况(可能因为某些具体应用的限制); 3)、基于处理速度的考虑,需要绕过 DOS 层的情况。 综上所述,可以归纳出如下结论:使用中断的层次越高,它与硬件设备相关程度就越低, 处理速度也就越低,但用户程序的适用范围较广。反之也然。 有了上面的结论,程序员可根据应用程序的要求、对硬件环境的熟悉程度等因素来选用不 同层次的中断。 汇编入门(15讲) 时间:2009-5-18 19:27:40 点击:15 核心提示:8.3.1 键盘输入的中断功能键盘输入是绝大多数程序的主要输入方式,学习 和掌握有关键盘输入中断的使用方法对编写交互式程序是非常重要的,也能更进一步理解计 算机 是如何接受键盘输入的。1、键盘缓冲区键盘缓冲区是一个先进先出的环形队列,其所 占内存区域如下:KBHeadDW ?;其内存地址为 0000:041A... 8.3.1 键盘输入的中断功能 键盘输入是绝大多数程序的主要输入方式,学习和掌握有关键盘输入中断的使用方法对 编写交互式程序是非常重要的,也能更进一步理解计算机是如何接受键盘输入的。 1、键盘缓冲区 键盘缓冲区是一个先进先出的环形队列,其所占内存区域如下: KBHead DW ? ;其内存地址为0000:041AH,缓冲区头指针 KBTail DW ? ;其内存地址为0000:041CH,缓冲区尾指针 KBBuff DW 16 DUP(?) ;其内存地址为0000:041EH,该缓冲区的缺省长度为16个字 与键盘有关的其它地址请见附录6之键盘地址。 键盘缓冲区是一个 环形队列,其性质与《数据结构》课程中对“环形队列”所描述的性 质完全一致。虽然缓冲区的本身长度为16个字,但出于判断“对列满”的考虑,它最多只能保 存15个键盘信息。当缓冲区满时,系统将不再接受按键信息,而会发出“嘟”的声音,以示要 暂缓按键。当 KBHead=KBTail 时,表示无键盘输入。 2、键盘状态字 在计算机键盘上除 了可输入各种字符(字母、数字和符号等)的按键之外,还有一些功 能键(如:F1、F2、…等)、控制键(如:Ctrl、Alt、Shift 等)、双态键 (如:Num Lock、Caps Lock 等)和特殊请求键(如:Print Screen、Scroll Lock 等)。 键盘中的控制键和双态键是非打印按键,它们是起控制或转换作用的。当使用者按下控 制键或双态键时,系统要记住其所按下的按键。为此,在计算机系统中,特意安排的一个字 来标志这些按键的状态,我们称该字为键盘状态字。 8.3.2 屏幕显示的中断功能 显示器是一个重要 的输出设备,它通过显示卡与计算机系统相连。显示器的显示屏通 常称之为屏幕,现在常用的显示器有14"和17",常用的显示分辨率为800×600或 1024×768 等。常用的显示卡类型为 VGA、SVGA、EVGA 和 TVGA 等,显示卡上也都带有大量的显 示存储器,能快速显示精美的图象。 1、显示模式 计算机系统中的显示器都有二种显示方式:文本显示方式和图形显示方式。在 DOS 操 作系统环境下,其默认的显示方式为文本显示方式,而在 Windows 操作系统环境下,其显 示方式是图形显示方式,其绝大多数操作界面是以图形界面的窗口形式出现的。 可用的显示模式,请参阅附录3中的中断10H 之功能00H 中所列。 1)、文本显示方式 文本显示方式是指以字符为最小单位的显示方式,每个字符都是以矩形块形式显示的。 在 BIOS ROM 中存有多种不同大小的字符集,主要的显示字符集大小为:8×8(标准)、8×14 和8×16等。 在常用的文本显示 模式(模式3)下,屏幕被划分成25行,每行可显示80个字符,所以, 每屏最多可显示2000(80×25)个字符。为了便于标识屏幕上的每个显示位置, 我们就用其所 在行和列来表示之,并规定:屏幕的左上角坐标为(0, 0),右下角坐标为(24, 79)。 在显示字符时,用一个字节存储该字符的 ASCII 码,用另一个字节存储的显示属性,即: 显示颜色。彩色显示器的字符显示属性定义如图8.6所示,有关颜色值的定义,请参阅附录6中 的表4。 2)、图形显示方式 图形显示是目前最常用的一种显示方式,也是 Windows 操作系统的默认显示方式。在 该显示方式下,我们可以看到优美的图象、VCD、浏览丰富多彩的网页等。 图形显示的最小单位是象素,对每个象素可用不同的颜色来显示。所以,在显示缓冲区 内记录的信息是屏幕各象素的显示颜色。 由于各种图形显示模式所能显示的颜色和象素是不同的,它决定了显示缓冲区的存储方 式也是不同的。下面给出三个具体的图形显示模式及其存储方式,通过它们可看出各种显示 模式在显示缓冲区存储方式上的明显差异。 (1)、4色320×200图形显示模式 由于每个象素只能是四种颜色之一,而四种情况用2位二进制就可表示,所以,一个字 节可表示4个象素的显示颜色,存储一行上的所有象素信息就需要80个字节。 在具体存储过程 中,它又把偶数行象素和齐数行分开来存储。偶数行和齐数行的象素 总数各有32000个,也都需要8000个字节来存储,并规定:偶数行象素从 0B800:0000H 开始 存储,齐数行象素从0B800:2000H 开始存储。该显示模式的存储形式如图8.8所示。 (2)、16色640×480图形显示模式 640×480图 形显示模式共有307200个象素,每个象素可选用16种颜色,它需要用4位二 进制来表示。该显示模式在存储显示信息时,把该4位分在四个位平面 P1、 P2、P3和 P4上, 所以,位平面 Pi(i=1,2,3,4)共有307200个二进制位,即有38400个字节。其显示缓冲区的存储 形式如图8.9所 示。 由图8.9可看出:若要改变某个象素的显示颜色,则需要在每个位平面上修改其对应的 二进制位。所以,若用操作显示缓冲区的方法来改变象素的颜色,那么编程将困难得多。在 屏幕上显示一个点,必须遵循以下步骤: 读入要改变的内存单元,把位平面信息装入显示卡; 通过图形地址寄存器(GAR)和位屏蔽寄存器(BMR)选择并寻址到其二进制位; 寻址并设定映像屏蔽寄存器(MMR)为0FH,在对应象素地址填入0(黑色)来清除象素 的原有颜色; 通过 MMR 设定该象素当前所要颜色; 通过修改包含显示信息的内存单元来写象素。 对于该显示模式下,改变象素方法的详细描述和例子,请参阅参考书籍[2]、[4],或其 它有关技术资料,在此不再进一步展开叙述。 若用 BIOS 中断10H 之功能0DH/0CH 来读/写象素,则它可屏蔽掉各种显示模式的差异, 用同样的参数完成同样的功能。所以,在此建议:使用系统中断的方法来实现对图形象素的 操作。 (3)、256色320×200图形显示模式 表达256种不同 颜色需要8位二进制,即一个字节。在该模式下,其显示缓冲区的存储方式 是非常简单的,即:第一个字节存储第一个象素的颜色,第二个字节存储第二个象素的颜 色, 以此类推,所以,存储满屏象素所需要的字节数为:320×200×1=64000。其显示缓冲区的存储 方式如图8.10所示。 从上面三种不同图 形显示模式的介绍,不难看出:各种显示模式在显示缓冲区存储方式上 的明显差异,操作象素方法的难易程度相差也很大,所以,再次建议:程序员不要用直接操作 显 示缓冲区的办法来达到改变显示象素的目的,最好是通过 BIOS 内的中断功能来实现相应的功 能,这样,所编写的程序能很方便地适应不同的图形显示模式。 2、显示缓冲区 显示缓冲区是用来记录屏幕显示信息的。在文本显示方式下,这些显示信息包括:每个 显示字符的 ASCII 码及其显示属性,如图8.7所示。在图形显示方式下,显示缓冲区内存储 每个象素的显示颜色。 在图8.7中,我们并没有给出具体的段地址,只用"XXXX"来表示其段地址。在常用的 VGA 显示方式下,计算机系统规定: 文本显示方式下,单色显示器的显示缓冲区段地址为0B000H; 文本显示方式下,彩色显示器显示缓冲区段地址为0B800H; 图形显示方式下,其显示缓冲区段地址为0A000H。 3、DOS 功能中的屏幕输出 屏幕输出是最常用的一种输出形式,DOS 操作系统提供了几种实现屏幕输出的功能调 用。INT 21H 中的相关功能如下: 02H——显示的字符 06H——控制台的输入/输出:当 DL≠0FFH,表示显示字符 09H——在屏幕上显示一个字符串 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之字符功能调 用类。 4、BIOS 中的屏幕操作功能 BIOS 系统提供了中断10H 来实现各种屏幕处理功能。其具体的功能如下: 00H——设置显示器模式 01H——设置光标形状 02H——设置光标位置 03H——读取光标信息 05H——设置显示页 06H、07H——初始化或滚屏(向上滚屏和向下滚 屏) 08H——读光标处的字符及其 属性 09H——在当前光标处按指定属性显示字符 0AH——在当前光标处显示字 符 0CH——写图形象素 0DH——读图形象素 0EH——在 Teletype 模式下显示字符 0FH——读取显示器模式 10H——颜色 13H——在 Teletype 模式下显示字符串 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之显示服务。 5、屏幕输出的举例 例8.2 用直接写屏方式在屏幕第5行、第10列以黄色(0EH)显示字符串"Hello"。 解:在文本显示方式下,每行显示80个字符,每个字符占2个字节,所以,显示一行需要160 个字节。若在第 m 行、第 n 列位置显示字符,则该位置所对应存储单元的偏移量为: m×160 + n×2。 .MODEL SMALL .DATA Yellow EQU 0EH MSG1 DB "Welcome..." Count EQU $-MSG1 .CODE .STARTUP MOV AX, 0B800H MOV ES, AX ;彩色显示器的显示缓冲区段地址 MOV DI, 5 IMUL DI, 160 ;5行所跳过的显示存储单元 MOV CX, 10 SHL CX, 1 ;10列所跳过的显示存储单元 ADD DI, CX ;第5行、第10列之前所跳过的所有显示单元 MOV CX, Count LEA SI, MSG1 ;CX:字符个数,SI:显示字符首地址 MOV AH, Yellow ;设置显示属性,即显示颜色 .REPEAT LODSB ;取显示字符 STOSW ;向显示缓冲区设置显示字符和属性 .UNTILCXZ .EXIT 0 END 例8.3 用“霓虹灯”的显示方式显示字符串"Hello",按 ESC 键时结束程序的运行。 例8.4 编写一个输入密码的程序,该程序的具体要求如下: 1、每输入一个字符,显示字符"#"表示之; 2、密码最多只有10个字符,多余的按键被丢弃; 3、若输入的字符串为"HELLO",则以蓝色显示"Welcome…",否则,以闪烁、亮红色 在显示"Invalid Password"。 例8.5 在256色320×200的图形显示模式下,从屏幕最左边向最右边,依次画竖线(从顶到底), 线的颜色从1依次加1。要求用中断调用的方法来画线。 例8.6 在256色320×200的图形显示模式下,从屏幕顶到屏幕底依次画横线(从最左边到最右 边),线的颜色从1依次加1。要求用直接操作显示缓冲区的方法来画线。 3、键盘中断的处理过程 当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求。 键盘中断服务程序先从键盘接口取得按键的扫描码,然后根据其扫描码判断用户所按的键并 作相应的处理,最后通知中断控制器本次中断结束并实现中断返回。 若用户按下双态键(如:Caps Lock、Num Lock 和 Scroll Lock 等),则在键盘上相应 LED 指 示灯的状态将发生改变; 若用户按下控制键(如:Ctrl、Alt 和 Shift 等),则在键盘标志字中设置其标志位; 若用户按下功能键(如:F1、F2、…等),再根据当前是否又按下控制键来确定其系统扫描 码,并把其系统扫描码和一个值为0的字节存入键盘缓冲区; 若用户按下字符键(如:A、1、+、…等),此时,再根据当前是否又按下控制键来确定其 系统扫描码,并得到该按键所对应的 ASCII 码,然后把其系统扫描码和 ASCII 码一起存 入键盘缓冲区; 若用户按下功能请求键(如:Print Screen 等),则系统直接产生一个具体的动作。 有关键盘中各种单键、组合键的扫描码及其相应的 ASCII 码,请参阅本书的附录5。 4、DOS 功能中的键盘输入 键盘输入是一种最常用的输入方式,所以,在 DOS 操作系统中,提供了能实现各种键 盘输入的功能(Windows 操作系统中仍然能用)。INT 21H 中的相关功能如下: 01H——带回显的键盘输入 06H——控制台的输入/输出:当 DL=0FFH,表示键盘 输入 07H——不回显、不过滤的键盘输 入 08H——不回显的键盘输入 0AH——键盘输入字符串 0BH——检查键盘输入状态 0CH——清除输入缓冲区的输入功能 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之字符功能调 用类。 5、BIOS 中的键盘输入 在 BIOS 系统中,提供了中断16H 来实现键盘输入功能。其具体的功能如下: 00H、10H——从键盘读一个字符,输入字符不回显 01H、11H——判断键盘缓冲区内是否有字符可读 02H——读取当前键盘状态字 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之键盘服务。 6、直接操作端口的键盘输入 由表8.1可知:键盘输入端口的地址为60H,所以,我们可以用指令 IN 从该端口读取当 前按键的扫描码。 例如: MOV DX, 60H IN AL, DX 7、键盘输入的举例 例8.1 用键盘最多输入10个字符,并存入内存变量 Buff 中,若按“Enter”键,则表示输入结 束。 解: 1、方法1 .MODEL SMALL CR EQU 0DH ;定义“回车”键的符号名 .DATA Buff DB 10 DUP(?) .CODE .STARTUP MOV CX, 0AH LEA BX, Buff .REPEAT MOV AH, 0H INT 16H ;用 BIOS 中的中断功能 .BREAK .IF AL==CR MOV [BX], AL INC BX .UNTILCXZ .EXIT 0 END 2、方法2 .MODEL SMALL .DATA Buff DB 10, ?, 10 DUP(?) ;注意缓冲区的定义方式 .CODE .STARTUP LEA DX, Buff MOV AH, 0AH INT 21H ;用 DOS 中的功能调用 .EXIT 0 END 8.3.3 打印输出的中断功能 打印输出是一种硬 拷贝输出,也是一种常用的输出形式。随着计算机应用领域的不断 扩大,外围设备的重视程度也越来越高。目前,打印机的种类已日益丰富,有针式打印机、 喷墨打 印机和激光打印机等。从打印色彩来看,既有普通的黑色打印,也有彩色打印。总 之,计算机打印输出的品质是越来越高。 1、打印机状态字 打印机状态字记录着打印机的当前工 作情况(或状态),它相当于 CPU 中的程序 状态字和键盘状态字。打印机状态字的各 位定义如图8.11所示。 若在某个程序中要包含打印功能,那 么,程序员可通过打印状态字来了控制打 印过程,并向使用者提供各种有用信息。 比如:提示使用者“打印机无纸了”或打印 机处于脱机状态(Off Line)等。 2、打印控制命令 在打印时,程序员往往要控制文本的输出格式,比如:换页、换行、字体或字号等。若 要实现对打印格式的控制,就必须要知道控制打印机的控制字符和控制命令。一些常用的打 印控制字符如表8.3所示。 表8.3 常用的打印控制字符 字符值 功能描述 09H 水平制表符,跳到下一个制表 位置 0AH 换行 0CH 换页 0DH 回车 打印机还有其它的控制命令,如:ESC 命令序列,该序列由字符 ESC(其 ASCII 为1BH) 和一些数值组成。表8.4列举了 LQ-1600K 打印机的几个控制命令。对于其它各类打印机的 控制命令,要参阅其使用手册。 表8.4 LQ-1600K 打印机的几个控制命令 控制命令 命令的数值 功能描述 ESC @ 1BH 40H 初始化打印机 ESC 4/ESC 5 1BH 34H / 1BH 35H 设定/取消斜体打印 ESC S 0/ESC S 1 1BH 53 0/1BH 53 1 设定上/下角标打印 ESC T 1BH 54H 取消上/下角标打印 ESC l n 1BH 6CH n 设定左边界,n 为当前字符 的宽度 3、DOS 功能中的打印输出 在 DOS 操作系统中,INT 21H 提供了一种打印输出的功能调用。其具体描述如下: 05H——向连接在 LPT1端口上的打印机输出一个字符 40H——向先前打开的文件写入指定数量的字节,可以把打印机看作为标准的输出 文件 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之字符功能调 用类。 4、BIOS 中的打印输出 BIOS 系统提供了中断17H 来实现打印输出功能。其具体的功能如下: 00H——向指定的打印机输出一个字符 01H——初始化指定的打印机 02H——读取指定打印机的状态 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之并行口服务。 5、打印输出的举例 例8.7 在每页的开始处打印“Assember Language”字符串,并空一行才打印其它内容。 例8.8 当打印机不能正常打印(非硬件故障)时,提示使用者其原因,以便解决打印问题。 8.3.4 串行通信口的中断功能 计算机的通信功能是现代计算机网络的最基本功能。如果计算机没有通信功能,计算机 网络环境也就无从谈起。一台计算机与其它计算机(或设备)之间通信除了必要的物理连接之 外,它们之间通信接口的功能就是另一个极其重要的功能模块。 从计算机通信的本质来看,通信就是发送或接受具有一定格式的二进制位。这些二进制 位通过一些应用程序——如浏览器——的解释显示成具有不同表达形式的文字或图象。 为了方便实现计算 机的通信功能,在系统的低层或操作系统中都有不同程度的支持, 如:DOS、BIOS 和 NETBIOS 等系统中都相应通信功能或中断服务。下面仅仅介绍在汇 编 语言程序中如何使用通信功能,有关通信功能的详细内容在《计算机通信与接口技术》课程 会有更深入的论述。 1、DOS 中的通信功能 INT 21H 提供了对通信口 COM1操作的功能调用。其具体描述如下: 03H——从辅助设备读入一个字符,该辅助设备的缺省值为 COM1 04H——向辅助设备输出一个字符,该辅助设备的缺省值为 COM1 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之字符功能调 用类。 2、BIOS 中的通信功能 BIOS 系统提供了中断14H 来实现对通信端口的控制能。这些具体的功能如下: 00H——初始化通信口 01H——向通信口输出字符 02H——从通信口读入字符 03H——读取通信口状态 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之串行口服务。 3、通信功能的应用举例 例8.9 把字符串"Hello, World"从 COM1端口传输出去。在传输过程中,要求传输速率为9600 波特,字长为8位,1位停止位,无齐偶校验。 解: .MODEL SMALL .DATA MSG DB "Hello, World" MLen EQU $-MSG .CODE .STARTUP MOV AL, 0E3H ;0E3—9600、无齐偶校验、字长8位,1位停止位 MOV DX, 0 MOV AH, 0 INT 14H ;初始化通信口 COM1 MOV BX, OFFSET MSG MOV CX, MLen MOV DX, 0 again: MOV AL, [BX] MOV AH, 1 INT 14H TEST AH, 80H ;如果发送字符失败,继续发同一个字符,这里可能 JNZ again ;构成死循环。在实际工作过程中,还有其它考虑。 INC BX LOO P again .EXI T 0 END 例8.10 把从 COM1端口读入的字符显示在屏幕上。如果通信端口数据未准备好,则程序处 于等待状态;若传输出错,则用红色显示字符"?"。 解: …… again: MOV DX, 0 MOV AH, 3 INT 14H TEST AH, 1H ;检测状态字节 AH 的最后一位,看数据是否准备好 JZ again MOV DX, 0 MOV AH, 2 INT 14H ;从 COM1端口读字符 TEST AH, 80H ;检测读字符是否成功 .IF ZERO? ;读字符成功 AND AL, 7FH ;屏蔽掉最高位,使之为字符 MOV BL, 15 ;正常颜色:白色 .ELSE MOV AL, '?' ;显示字符"?" MOV BL, 12 ;错误颜色:红字 .ENDIF MOV BH, 0 MOV AH, 0EH INT 10H JMP again …… 8.3.5 鼠标的中断功能 鼠标是现在计算机 系统中的一个常用输入设备,它为使用计算机带来了很大的方便, 鼠标指针的各种表现形式还反映了系统(或应用程序)当前处于什么样的工作状态。许多计算 机使 用者为体现其个性化还选择各自喜欢的鼠标图形,所以,了解和掌握计算机系统对鼠 标所提供的功能对理解鼠标的各种操作也是有一定的帮助。 1、鼠标中断的常用功能 BIOS 系统提供了中断33H 来实现鼠标中断功能,其常用的功能如下: 00H—初始化鼠标 01H—显示鼠标指针 02H—隐藏鼠标指针 03H—读取鼠标位置及其按钮 状态 04H—设置鼠标指针位置 05H—读取鼠标按键信息 06H—读取鼠标按钮释放信息 07H—设置鼠标水平边界 08H—设置鼠标垂直边界 09H—设置图形鼠标形状 0AH—设置本文鼠标形状 0BH—读取鼠标移动计数 0CH—为鼠标事件设置处理程序 0FH—设置鼠标计数与象素比 10H—设置鼠标指针隐藏区域 14H—替换鼠标事件中断 18H—为鼠标事件设置替换处理程序 19H—读取替换处理程序的地址 1DH—为鼠标指针选择显示页 1EH—读取鼠标指针的显示页 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之鼠标功能中 断。 2、鼠标指针的设置 鼠标指针主要有二种表现形式:文本鼠标和图形鼠标。文本鼠标又分为软指针和硬指针。 软指针是用各种字符来作鼠标指针,而硬指针是用方块光标的大小来表示鼠标指针,其效果 类似于 INT 10H 之功能01H——设置光标类型。 图形鼠标的表现形式与文本鼠标相比就显得更加丰富多彩,可用各种不同的指针形状来 反映系统当前所处的工作状态和所能进行的操作。在 Windows 操作系统及其应用程序中使 用了很多指针形状的变化来表达各种有用信息。 鼠标的文本软指针与图形指针的形成过程相一致,它需要两部分信息:象素掩码和光标 掩码。其指针形成过程如下: 1)、用象素掩码与当前鼠标所处位置的象素信息进行“逻辑与”运算; 2)、光标掩码与步骤1的运算结果再进行“异或”操作,该操作所得到的16×16位的0/1信 息就构成了当前鼠标指针的形状。 假设象素掩码为全0。全 0的象素掩码与屏幕上的显示信息“逻辑与”后,所得结果仍为全 0,全0的运算结果再和“光标掩码”进行“异或”操作,这时,所得结果显然与“光标掩码”完全 一样,所以,看到的鼠标指针形状就是光标掩码所表达的指针形状。 综合上述,可得结论:若象素掩码为全0,那么,鼠标的形状就是16×16位光标掩码所 表示的指针形状,鼠标所到之处就看不到该区域内(16×16点阵范围)的其它显示信息。 在 Windows 操作系统及其应用程序中,在16×16点阵范围内,除了看不见被各种形状指 针覆盖的部分之外,还能看见其它小区域,这是因为鼠标的“象素掩码”取其“光标掩码”的反 相点阵所致。感兴趣的读者可验证或证明之。 (1)、硬指针的设置 设置鼠标硬指针的中断功能使用方式: MOV BX, 1 ;硬指针 MOV CX, 01H ;鼠标硬指针的起始扫描线 MOV DX, 0FH ;鼠标硬指针的结束扫描线 MOV AX, 0AH ;设置文本鼠标指针 INT 33H (2)、软指针的设置 设置鼠标软指针的中断功能使用方式: MOV BX, 0 ;软指针 MOV DL, 'A' ;用字符'A'作为鼠标指针符号 MOV DH, 07FH ;置鼠标的颜色 MOV CX, 0 ;置象素位掩码 MOV AX, 0AH INT 33H ;设置文本鼠标指针 (3)、图形指针的设置 设置鼠标软指针的中断功能使用方式: …… PMask DW 16 dup(?) ;象素位掩码,PMask 可以是~CMask CMas k DW 16 dup(?) ;光标掩码,二者紧相邻 …… MOV AX, DS MOV ES, AX LEA DX, PMask ;ES:DX=象素位掩码的起始地址 MOV BX, 0 MOV CX, 0 ;在鼠标指针范围内,(0,0)点为指示点 MOV AX, 09H INT 33H ;设置图形鼠标指针 3、鼠标功能的应用举例 例8.11 在屏幕的右上角动态显示文本鼠标的位置,即:鼠标的任何移动都将马上显示其所 处位置,按鼠标左键结束程序的运行。 例8.12 编写可随时修改文本鼠标指针符号的程序,即要求:在程序运行过程中,随时在键 盘上按什么字符,即以该字符为鼠标指针符号。 读者可把例8.11和8.12结合在一起,使之:一方面,可动态显示鼠标的位置,另一方面, 也可随时修改文本鼠标的指针符号。 改变图形鼠标指针是许多计算机使用者体现其个性的方法之一,读者可参考例8.12和前 面的“图形指针的设置”部分的叙述,编写一个显示自己设计的鼠标指针的程序。在学习了有 关文件操作知识之后,还可利用现有的鼠标指针文件来显示鼠标指针。 8.3.6 目录和文件的中断功能 中断21H 提供了许多有关目录和文件操作的功能,其中常用的功能如下: 1、操作目录的常用功能 39H——创建目录 3BH——设置当前目录 3AH——删除目录 47H——读取当前目录 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之目录控制功 能。 2、用文件句柄操作文件的常用功能 3CH——创建文件 4EH——查找到第一个文件 3DH——打开文件 4FH——查找下一个文件 3EH——关闭文件 56H——文件换名 3FH——读文件或设备 57H——读取/设置文件的日期和时间 40H——写文件或设备 5AH——创建临时文件 41H——删除文件 5BH——创建新文件 42H——设置文件指针 67H——设置文件句柄数(最多文件数) 43H——读取/设置文件属 性 6CH——扩展的打开文件功能 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之文件操作功 能。 3、用 FCB 操作文件的常用功能 0FH——打开文件 21H——随机读 10H——关闭文件 22H——随机写 13H——删除文件 23H——读取文件的大小 14H——顺序读 24H——设置相对记录数 15H——顺序写 27H——随机读块 16H——创建文件 28H——随机写块 17H——文件换名 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之文件操作功 能(FCB)和记录操作功能(FCB)。 4、磁盘绝对读写中断 中断25H——磁盘绝对读 中断 中断26H——磁盘绝对写中断 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之其它 DOS 中断。 5、系统标准设备的句柄 0000H——键盘 0001H——屏幕 0002H——错误显示(屏幕) 0003H——COM1 0004H——打印机 6、目录、文件功能的应用举例 例8.13 编写一个创建子目录的程序,具体要求如下: 1)、用键盘输入一个目录路径名,若输入的字符串为空,则程序运行结束; 2)、若目录创建成功,显示成功信息,否则,显示创建失败信息。 例8.14 编写一个类似 TYPE 命令的程序,其要求如下: 1)、用键盘输入文件名(可包含路径),若输入的字符串为空,则程序运行结束; 2)、若输入的文件存在,则显示其内容,否则,显示文件不存在的信息。 8.3.7 内存管理的中断功能 在 C/C++语言环境中,若事先不知道数据的容量,那么,可通过动态申请空间的方法来 解决数据的存储问题,这种动态数据结构可用中断21H 所提供的存储管理功能来实现。 中断21H 在内存管理方面所提供的主要功能如下: 48H——分配内存块 49H——释放内存块 4AH——重定义内存块的大小 58H——读取/设置内存分配策略,其最基本的分配策略有三种:第一满足、最好满 足和最后满足 有关中断功能的详细描述和调用参数在此从略,需要查阅者可参阅附录3之内存分配功 能。 有关存储分配策略的含义可参阅《操作系统》课程中“内存管理”部分的介绍。 8.3.8 读取和设置中断向量 中断向量是系统用来存放中断服务程序或系统参数的入口地址。在通常情况下,程序员 不需要对中断向量表作任何操作,但在开发程序时,若要为某种特殊需要而提供新的中断处 理程序时,则其就必须要操作中断向量表。 1、读取中断向量 (1)、DOS 功能调用的方法 DOS 提供了用系统调用的方法来读取中断向量,其中断21H 之功能35H 就能读取指定 中断号的入口地址。其使用参数如下: 入口参数:AH=35H,AL=中断号 出口参数:ES:BX=中断处理程序的入口地址 例8.15 用功能调用的方法把中断 n 的入口地址保存到双字变量 OldAddr 中。 解: …… OldAddr DD ? …… MOV AL, n ;这里的 n 要用具体的中断号来定 MOV AH, 35H INT 21H MOV word ptr OldAddr, BX MOV word ptr OldAddr+2, ES ;把其入口地址保存在存储单元中 …… (2)、直接访问存储单元的方法 由前面的图8.2不难看出:中断向量表是存储在内存的第0段。若中断号为 n,那么,其 入口地址在表内的偏移量为4n。 例8.16 用直接访问存储单元的方法把中断 n 的入口地址保存到双字变量 OldAddr 中。 解: …… OldAddr DD ? …… MOV AX, 0H MOV ES, AX ;中断向量表存储在第0段内 MOV BX, 4*n MOV AX, ES:[BX] ;读取中断入口地址的偏移量 MOV word ptr OldAddr, AX ;保存中断入口地址的偏移量 MOV AX, ES:[BX+2] ;读取中断入口地址的段地址 MOV word ptr OldAddr+2, ES ;保存中断入口地址的段地址 …… 2、设置中断向量 (1)、DOS 功能调用的方法 中断21H 之功能25H 可为指定的中断号设置新的入口地址。其使用方法如下: 入口参数:AH=中断号,DS:DX=中断处理程序的入口地址 出口参数:无 例8.17 用功能调用的方法把子程序 NewFunc 设置为中断 n 的中断处理程序。 解: …… NewFun PROC …… c IRET ;注意其返回指令 ENDP …… MOV AX, SEG NewFunc MOV DS, AX ;设置段地址寄存器 MOVDX, OFFSET NewFunc ;设置偏移量 MOVAL, n ;这里的 n 要用具体的中断号来定 MOVAH, 25H INT 21H NewFun c …… (2)、直接访问存储单元的方法 例8.18 用直接访问存储单元的方法把子程序 NewFunc 设置为中断 n 的中断处理程序。 解: …… NewFun c PROC …… IRET ;注意其返回指令 NewFun c ENDP …… MOVAX, 0H MOV DS, AX MOVBX, 4*n CLI MOVword ptr [BX], OFFSET NewFunc ;设置中断处理程序的偏移量 MOVword ptr [BX+2], SEG NewFunc ;设置中断处理程序的段地址 STI …… 在上面程序段中, 指令 CLI 是一条值得注意的指令,它用来确保随后二条 MOV 指令 被连续执行而不被打断。这是因为执行其第一条 MOV 指令后,原中断向量表中的入口地址 就被 破坏了,这时,该入口地址既不是指向原处理程序,也不指向新处理程序。如果此刻 正巧发生了该类型的中断请求,那么,系统将转向一个非法的位置。如果程序员 能确信此 时不会发生该类型的中断请求,当然也就不必插入关中断指令。 后面的指令 STI 是开中断指令,它允许 CPU 响应其后的中断请求。 3、修改中断向量的步骤 前面,分别介绍了读取和设置中断入口地址的方法。在编程的实际过程中,若要修改某 个具体的中断处理程序时,一般需按下面几步来完成: 1)、读取指定中断的中断处理程序入口地址,并把它保存在存储单元内; 2)、把用户编写的程序段设置为指定中断的新处理程序; 3)、在用户程序结束之前(或不需要新的处理程序时),把步骤1保存的入口地址恢复成处 理该中断的入口地址。 程序员按以上三步来编程,可保证:在其程序运行过程中,该指定的中断将按新的处理 程序来处理,程序结束后,中断系统又恢复成原来的处理方式。所以,这种中断向量的改变 对其它程序或使用者来说是透明的,当然也就不会影响它们的正常运作。 以上三步具体化的形式如下: …… INTNO EQU 40H ;假设被修改的中断号 OldAddr DD ? …… NewFun c PROC ;新的中断处理程序 …… IRET NewFun c ENDP …… MOVAL, INTNO MOVAH, 35H INT 21H MOVword ptr OldAddr, BX MOVword ptr OldAddr+2, ES ;步骤1:保存原入口地址 MOVDX, OFFSET NewFunc MOV AX, SEG NewFunc MOV DS, AX MOVAL, INTNO MOVAH, 25H INT 21H ;步骤2:设置新的入口地址 …… ;用户编写的主要程序 …… MOV DX, word ptr OldAddr MOVDS, word ptr OldAddr+2 ;本指令与上一条指令能交换吗? MOVAL, INTNO MOVAH, 25H INT 21H ;步骤3:恢复原入口地址 …… END 汇编入门(16讲) 时间:2009-5-19 8:01:23 点击:10 核心提示:第 9章 宏宏是程序设计的另一个基本概念,它是把一段程序代码用一个特 定标识符(即:宏名)来表示。这样,在编写源程序时,程序员就可以直接使用该标识符来代 替一 段代码的编写,从而减少了重复代码的编写工作,也为减少错误,提高程序的可维护 性提供了帮助。宏在高级语言(如:C/C++等)也有广泛的使用。9.1 宏的... 第9章 宏 宏是程序设计的另 一个基本概念,它是把一段程序代码用一个特定标识符(即:宏名) 来表示。这样,在编写源程序时,程序员就可以直接使用该标识符来代替一段代码的编写, 从而 减少了重复代码的编写工作,也为减少错误,提高程序的可维护性提供了帮助。宏在 高级语言(如:C/C++等)也有广泛的使用。 9.1 宏的定义和引用 通常情况下,宏是用来代表一个具有特定功能的程序段,它只需在源程序中定义一次, 但可在源程序中引用多次。只要在编写程序时需要,就可以直接使用它。 9.1.1 宏的定义 在使用宏之前必须先定义宏,定义宏一般格式如下: MACRO [形参1, 形参2, ……] … ;宏的定义体 宏名 ENDM 在书写宏定义时,必须遵照下列规定: 、MACRO 和 ENDM 是二个必须成对出现的关键字,它们分别表示宏定义的开始和结 束; 、MACRO 和 ENDM 之间的部分是宏的定义体,它是由指令、伪指令或引用其它宏所 组成的程序片段,是宏所包含的具体内容; 、“宏名”是由程序员指定的一个合法的标识符,它代表该宏; 、宏名可以与指令助忆符、伪指令名相同。在这种情况下,宏指令优先,而同名的指 令或伪指令都失效; 、在 ENDM 的前面不要再写一次宏名,这与段或子程序定义的结束方式有所不同; 、在宏定义的首部可以列举若干个形式参数,每个参数之间要用逗号分隔。 根据上述规定,我们提倡宏名尽可能不要与指令助忆符、伪指令相名,以免引起不必要 的误会。 例9.1 定义一个把16位数据寄存器压栈的宏。 MACRO PUSH AX PUSH BX PUSH CX PUSH DX PUSH R ENDM 例9.2 定义二个字存储变量相加的宏。 MACRO OPRD1, OPRD2 MOV AX, OPRD2 ADD OPRD1, AX MADD M ENDM 上述宏定义虽然能满足题目的要求,但由于在定义体中改变了寄存器 AX 的值,这就使 宏的引用产生了一定的副作用。为了使寄存器 AX 的使用变得透明,可把该宏定义改成如下 形式: MACRO OPRD1, OPRD2 PUSH AX MOV AX, OPRD2 ADD OPRD1, AX POP AX MADD M ENDM 通过在宏定义的开始和结尾分别增加对所用寄存器的保护和恢复指令,就使得:对该宏 的任意引用都不会产生任何副作用。 9.1.2 宏的引用 在源程序中,一旦定义了某宏,那么,在该程序的任何位置都可直接引用该宏,而不必 重复编写相应的程序段。引用宏的一般格式如下: 宏名 [实参1, 实参2, ……] 其中:实参的位置要与形参的位置要对应,但实参的个数可以与形参的个数不相等。 、当实参的个数多于形参的个数时,多出的实参被忽略; 、当实参的个数少于形参的个数时,没有实参对应的形参用“空”来对应。但在宏展开时, 所得到的指令必须是合法的汇编指令,否则,汇编程序将会给出出错信息。 例如:假设已有字变量 w1和 w2,并且也有例9.2中的宏 MADDM,那么,如果要把 w2 的内容加到 w1中的话,就可以在源程序的代码段中按下列方式来引用该宏(点击它,可显示 宏展开后的形式): MADDM w1, w2 注意:以上的宏展开形式可以在 LST 文件中看到,宏展开所得到的语句前面有一个数 字(如:1),它表示宏展开的层次。 如果在源程序中按下列方式来引用了宏 MADDM,那么,在宏展开时就会得到下列语 句: MADDM w1 9.1.3 宏的参数传递方式 在引用宏时,参数是通过“实参”替换“形参”的方式来实现传递的。参数的形式灵活多样, 参数可以是常数、寄存器、存储单元和表达式,还可以是指令的操作码。 例9.3 定义二个字存储变量相加和相减的宏。 解: 方法1:定义二个宏分别实现存储变量的加 操作和减操作 方法2:定义一个宏,把存储变量的加和减操作合 并在一起 MACRO OPRD1, OPRD2 MOV AX, OPRD2 ADD OPRD1, AX MADD M ENDM MACRO OPRD1, OPRD2 MOV AX, OPRD2 SUB OPRD1, AX MSUB M ENDM MACRO OP, OPRD1, OPRD2 MOV AX, OPRD2 OP OPRD1, AX MOPM ENDM 其中:参数 OP 是一个对应于操作码的形式 参数。 下面是引用宏 MOPM 的二个例子,它们在宏展开时将会得到下列语句: MOPM SUB, w1, w2 … MOPM ADD, w1, w2 汇编入门(17讲) 时间:2009-5-19 8:06:45 点击:9 核心提示:9.5 条件汇编伪指令条件汇编伪指令是告诉汇编程序:根据某种条件确定一 组程序段是否加入到目标程序中。使用条件汇编伪指令的主要目的是:同一个源程序能根据 不 同的汇编条件生成不同功能的目标程序,增强宏定义的使用范围。条件汇编伪指令与高 级语言(如:C/C++)的条件编译语句在书写形式上相似,在所起作用方 面是... 9.5 条件汇编伪指令 条件汇编伪指令是告诉汇编程序:根据某种条件确定一组程序段是否加入到目标程序 中。使用条件汇编伪指令的主要目的是:同一个源程序能根据不同的汇编条件生成不同功能 的目标程序,增强宏定义的使用范围。 条件汇编伪指令与高级语言(如:C/C++)的条件编译语句在书写形式上相似,在所起作 用方面是完全一致的。 9.5.1 条件汇编伪指令的功能 条件汇编伪指令的一般格式如下: 条件表达式 IFnnnn 语句组1 [ELSE 语句组2] ENDIF 其中:IFnnnn 是表9.2中的伪指令,“[…]”内的语句是可选的。 条件汇编伪指令是在汇编程序把源程序转换成目标程序时起作用,其一般含义是:若条 件汇编伪指令后面的“条件表达式”为真,那么,语句组1将被汇编;否则,语句组2将被汇编 (如果含有 ELSE 伪指令)。 语句组1或语句组2内还可以包有条件汇编伪指令,这时,就形成了嵌套的条件汇编伪指 令。一个嵌套的 ELSE 伪指令总是与最近的、还没有与其它 ELSE 伪指令相比配的 IFnnnn 伪指令相比配。 每条条件汇编伪指令的具体含义如表9.3所示。 表9.3 条件汇编伪指令及其功能一览表 伪指令 含义 IF exp 若数值表达式 exp 的值不为0,则语句组1包含在目标文件中 IFE exp 若数值表达式 exp 的值为0,则语句组1包含在目标文件中 IFDEF label 若标号 label 有定义或被说明为 EXTRN,则语句组1包含在目标文件 中 IFNDEF label 若标号 label 没有定义,也没被说明为 EXTRN,则语句组1包含在目 标文件中 IFB <参数> 在宏引用时,若该形参没有相应的实参相对应,则语句组1包含在目 标文件中 IFNB <参数> 在宏引用时,若该形参没有相应的实参相对应,则语句组1包含在目 标文件中 IFIDN <参数1>, <参数2> 若参数1=参数2,则语句组1包含在目标文件中 IFDIF <参数1>, <参数2> 若参数1≠参数2,则语句组1包含在目标文件中 IF1 若汇编程序在第一遍扫描时,则语句组1包含在目标文件中 IF2 若汇编程序在第二遍扫描时,则语句组1包含在目标文件中 9.5.2条件汇编伪指令的举例 例9.14 编写一个可用 DOS 或 BIOS 功能调用输入字符的宏定义。 解: 方法1:使用条件汇编伪指令 IF MACRO DOS ;当符号 DOS 不为0时,则使用 DOS 的功能调用 MOV AH, 1H IF INT 21H ELSE ;否则,将使用 BIOS 的功能调用 MOV AH, 10H INT 16H ENDIF INPU T ENDM 在引用宏 INPUT 时,汇编程序会根据 DOS 是否为0来生成调用不同输入功能的程序段。 方法2:使用条件汇编伪指令 IFDEF MACRO DOS ;当定义了 DOS,则使用 DOS 的功能调用 MOV AH, 1H IFD EF INT 21H ELSE ;否则,将使用 BIOS 的功能调用 MOV AH, 10H INT 16H ENDIF INPU T ENDM 在引用宏 INPUT 时,汇编程序会根据符号 DOS 是否已定义来生成调用不同输入功能的 程序段。 例9.15 编写一个可用功能调用输入字符的宏定义。 解: READC MACRO char MOV AH, 1H INT 21H ;接受一个字符,并存入 AL 中 ;若参数 char 有实参与之对应 IFDIF , ;若参数 char≠AL,则把所输入字符保存到实参中 MOV char, AL IFNB ENDIF ENDIF H ENDM 9.6 宏的扩充 MASM 6.11编程系统对宏定义及其相关语句进行了一定程度的扩充。虽然这些扩充给编 程带来了一些方便,但它们不一定能被其它的汇编语言编程系统所接受,所以,程序员在使 用这些方便的扩充功能时,要注意到可能带来的限制。 下面介绍 MASM 6.11编程系统对宏及其相关语句的扩充。 9.6.1 宏定义形式 在 MASM 6.11编程系统中,其宏定义的一般形式如下: 宏 名 MACRO [参数1[:tag]] [,参数2[:tag]...] [LOCAL varlist] … [EXITM [value]] ENDM ;宏定义体内的局部变量和标号 ;宏的定义体 对上述宏定义的说明与9.1.1节中的说明完全一致,其需要增加的说明如下: tag——其值可以是 REQ、=<缺省值>或 VARARG REQ 指定该参数是不可缺少。在宏引用时,若该参数不对应某个“实参”,那么, 汇编程序会报错; =<缺省值> 在宏引用时,若不指定该参数所对应的“实参”,那么,该参数就取其缺省 值; VARARG 该参数对应一个可变长的实参表,各实参之间用逗号分开;若参数的属性 指定为 VA R A R G,那么,该参数一定要是最后一个参数。 有关该属性的应用,请见随后9.6.7节中的举例。 value—— 宏功能的返回值,其为可选项。 9.6.2 重复伪指令 REPEAT 重复伪指令 REPEAT 与前面9.4.1节中伪指令 REPT 在功能和使用方式方面完全一致, 设置该伪指令的主要原因是保证与先前版本的兼容性。 伪指令 REPEAT 的使用方式如下: 数值表达式 语句序列 ;被重复的汇编语言语句 REPE AT ENDM 9.6.3 循环伪指令 WHILE 循环伪指令 WHILE 的使用方式如下: Exp 语句序列 ;被重复的汇编语言语句 WHI LE ENDM 其功能是先判断表达式 Exp 是否为假(或为0),若是,则终止该伪指令的功能,否则, 循环汇编下面的指令块。表达式 Exp 是能在汇编时计算出其值的数值表达式。 例9.16:编写一个带有参数 result 和 k 的宏,其功能是把1+2+…+k 的累加和存入 result 之中, 其中:result 是不可缺省的,k 的缺省值为1。 解: MACRO result:REQ, k:=<1> LOCAL n n = k mov result, 0 n WHIL E add result, n n = n - 1 ENDM SU M ENDM 有了上面的宏定义,就可书写下面的宏引用来实现其相应的功能: SUM ax, 10 ;寄存器 ax=1+2+3+…+10 SUM bh ;寄存器 bh=1,因为第二个形参取其缺省值 SUM ecx, 100 ;寄存器 ecx=1+2+3+…+100 SUM data, 20 ;存储单元 data=1+2+3+…+20 9.6.4 循环伪指令 FOR 循环伪指令 FOR 与9.4.2节中伪指令 IRP 在功能上完全一致,设置该伪指令的原因也是 为了保证与先前版本的兼容性。 伪指令 FOR 的使用方式如下: parameter[:REQ|:=], 语句序列 ;被重复的汇编语言语句 FOR ENDM 其中各参数的说明如下: parameter 一个合法的标识符,它依次取后面参数表中的值。在指令序列中,该变量的每次 出现都用其值所替换; :REQ说明该变量的取值不能为空; := 指定该变量的缺省值,若后面的参数表缺省某个参数(用连续的逗号),这时,该 循环变量将取其缺省值; Argument参数表中可含有文本、符号、字符串或数值常量,每个参数之间要用逗号分割。 例如: data:=, <"123", , 21, 0> DB data ENDM FOR …… reg:REQ, push reg FOR ENDM 该语句在宏展开时,将得到下列语句: DB "123" DB ? DB 21 DB 0 …… push ax push bx push dx 9.6.5 循环伪指令 FORC 循环伪指令 FOR 与9.4.3节中伪指令 IRPC 在功能上完全一致,它也是为保证与先前版 本的兼容性而设置的。 伪指令 FORC 的使用方式如下: parameter, 语句序列 ;被重复的汇编语言语句 FORC ENDM 其中各参数的说明如下: parameter:一个合法的标识符,它依次取字符串中的每个字符。在语句序列中,该变量的每 次出现都用其值所替换; String:一个字符串或被定义为字符串的符号名,字符串中的空格也被算为一个字符。括 号"<"、">"是必不可少的。 例如: data, <1?3> DB data ENDM FORC …… FORCreg, push reg&x ENDM 该语句在宏展开时,将得到下列语句: DB 1 DB ? DB 3 …… push ax push bx push dx 9.6.6 转移伪指令 GOTO 转移伪指令 GOTO 用于实现宏定义体内的转移功能,其使用方式如下: GOTO 标号 … :标号 ;标号后不能写指令,但可写注释 … 该伪指令的功能是使汇编程序转移到“标号”处汇编,它只能在宏定义 MACRO、 REPEAT、WHILE、FOR 和 FORC 等语句块内使用,该标号也只在该语句块内有效。 9.6.7 宏扩充的举例 例9.17:编写一个给任意寄存器或存储单元清零的宏定义。 解: list : VARARG data:REQ, FOR mov data, 0 ENDM Clear ENDM 有了上面的宏定义,下面的二个宏引用都是正确的,尽管宏引用时所带的参数个数是不 同的。这正是参数属性 VARARG 的作用所在。 Clear ax, bx …… Clear si, cl, MemVar, edx ;MemVar 是内存变量,任意类型都可以 这二个宏引用展开时所得到的指令如下: mov ax, 0 mov bx, 0 …… mov si, 0 mov cl, 0 mov MemVar, 0 mov edx, 0 9.6.8 系统定义的宏 MASM 6.11系统定义了大量的标准宏,程序员能很方便地使用它们。在使用这些系统宏 之前,要象 C 语言那样用伪指令 INCLUDE 把有关“宏库”文件包含在用户的源程序中。主要 的系统宏库文件有:DOS.INC 和 BIOS.INC,它们存放在系统的 include 子目录中。 例9.18:使用系统宏定义,编写从键盘上读取一个字符。 解: include dos.inc ;把系统宏定义文件包含在源程序之中 …… @getchar 1, 1 ;引用系统宏定义 …… 下面是系统宏@getchar 的使用参数描述和定义,其它系统宏的有关信息请参阅相关的 宏定义文件。 1、系统宏@GetChar 的使用说明 宏 的 功从键盘读字符 能: 使用语 法: @GetChar [echo] [,[break] [,clearbuf]] ;常量,非零表示“回显”,缺省值为“回显” ;常量,非零表示接受“^C”,缺省值为“接受” 参数说 明: ;常量,非零表示清键盘缓冲区,缺省值为“不清” 返回参 数: AL=ASCII 码 内容破 坏: AX,DL(若回显,且不接受^C) 参见内 容: INT 21h — 01h、07h、08h 和0Ch,@GetStr 2、系统宏@GetChar 的定义 该宏定义在宏库文件 dos.inc 中,其具体宏定义如下: MACRO ech:=<1>, cc:=<1>, clear:=<0> LOCAL funct, disp disp = 1 IF ech IF cc funct = 01h ;使用功能1 ELSE funct = 07h ;使用功能7 disp = 02h ;设置需要回显标志 ENDIF ELSE IF cc funct = 08h ;使用功能8 ELSE funct = 07h ;使用功能7 ENDIF ENDIF IFE clear mov ah, funct ;置功能号 ELSE mov ah, 0Ch ;先清输入缓冲区,再接受键盘输入 mov al, funct ENDIF Int 21h ;调用 DOS 功能中断 IF disp EQ 02h ;检查是否需要回显 @GetC har mov dl, al mov ah, 02h int 21h ENDIF ENDM 对于上面的宏定义,程序员完全可以把它修改成其它形式的宏定义。 汇编入门(18讲) 时间:2009-5-19 8:10:00 点击:11 核心提示:第 10章 应用程序的设计在前面各章节中,我们侧重介绍了汇编语言程序设 计中各组成部分的作用,本章的重点是对前面所学知识的综合运用。希望通过各种不同类型 的例 子,使读者能够掌握用汇编语言编程的基本技巧。10.1 字符串的处理程序字符或字符 串是一类重要的非数值计算的处理对象。许多编辑软件都具有字符串查找、替... 第10章 应用程序的设计 在前面各章节中,我们侧重介绍了汇编语言程序设计中各组成部分的作用,本章的重点 是对前面所学知识的综合运用。希望通过各种不同类型的例子,使读者能够掌握用汇编语言 编程的基本技巧。 10.1 字符串的处理程序 字符或字符串是一类重要的非数值计算的处理对象。许多编辑软件都具有字符串查找、 替换、大小写的转换、单词的自动识别等功能,网络上的信息搜索也是现在一种常用的功能 等,这些功能的实现无疑都要涉及到字符串的处理功能。 为了方便对字符串 的处理,各种常用的编程环境也都给予了足够的支持。如:C 语言 编程环境提供了大量处理字符串的标准函数,象 strlen、strcmp 和 strcpy 等函 数;C++、VC 或 VB 等编程环境提供了字符串类 String 等。这些函数或类大大方便了程序员的编程。 在计算机系统内,为了加快字符串的处理,在其指令系统中设置了多条处理字符串的指 令,其详细内容请参阅第5.2.11节中的介绍。 下面我们将通过几个例子来学习汇编语言处理字符串的方法。 例10.1 编写一个求字符串长度的子程序 Strlen,要求字符串的首地址为入口参数,且以 ASCII 码0为结束符,CX 为出口参数,其存放该字符串的长度。 解: .MODEL SMALL, C .DATA buff DB "This is a example.", 0 .CODE PROC USES AX BX, String:PTR BYTE MOV BX, String XOR CX, CX MOV AL, [BX] .WHILE AL!=0 INC CX INC BX MOVAL, [BX] .ENDW Strlen RET ENDP .STARTUP INVOKE Strlen, ADDR buff .EXIT 0 Strlen END 例10.2 编写一个把字符串中的所有小写字符转换成大写字符的子程序 Strupr,要求字符串 的首地址和结束符为其入口参数。 解: .MODEL SMALL, C .DATA buff DB "This is a example.", 0 .CODE PROC USES AX BX, String:PTR BYTE, Tail:BYTE MOV BX, String .REPEAT MOV AL, [BX] .IF AL>='a' && AL<='z' SUB AL, 20H MO V [BX], AL .ENDIF INC BX .UNTIL AL==Tail Strup r RET ENDP .STARTUP INVOKE Strupr, ADDR buff, 0 .EXIT 0 Strup r END 例10.3 编写一个从字符串中拷贝子串的子程序 Strncpy,它有四个参数 str1、str2、idx 和 num, 其具体功能为把字符串 str2中从第 idx 个(从 0开始记数)字符开始、num 个字符传送 给 str1,字符串 str1和 str2都是以 ASCII 码0为结束符。 解: .MOD EL SMALL, C .DATA str1 DB "12345ABCDEF", 0 str2 DB 20 DUP('A') .CODE PROC USES AX BX, String:PTR BYTE Strlen …… ;参见例10.1 Strlen ENDP PROC USES AX CX DI SI DS ES, str1:FAR PTR BYTE, str2:FAR PTR BYTE, idx:WORD, num:WORD LES DI, str1 LDS SI, str2 ;取两个字符串的首地址 INVO KE Strlen, SI ;计算源字符串的长度,在 CX 中 MOV AX, idx .IF AX >= CX ;若字符起点就超过源串的长度 MO V BYTE PTR ES:[DI], 0 ;拷贝的字符串为“空” JM P over .ENDIF ADD SI, AX ;定源串中字符的起点 SI MOV CX, num CLD .REPEAT LODSB STOSB .UNTILCXZ AL==0 .IF AL!=0 ;设置目标串的结束符 MOV BYTE PTR[DI], 0 Strncp y .ENDIF over: RET ENDP .STARTUP INVO KE Strncpy, ADDR str2, ADDR str1, 3, 5 .EXIT 0 Strncp y END 例10.4 编写一个把字符串中空格和TAB 压缩掉的子程序Compress,字符串String 是以ASCII 码0为结束符。 解: .MODE L SMALL, C .DATA SPAC E EQU 20H TAB EQU 9H Buff DB "12 3 4 Ab cdef", 0 .CODE PROC USES AX BX SI DS, String:FAR PTR BYTE LDS SI, String ;SI 用于扫描字符串的指针 MOV BX, SI ;BX 用于存放结果的指针 .REPEAT M O V AL, [SI] IN C SI .IF AL!=SPACE && AL!=TAB MOV [BX], AL INC BX .ENDIF .UNTIL AL==0 Compres s RET ENDP .STARTUP INVOKE Compress, ADDR Buff .EXI T 0 Compres s END 从上面四个例子,我们不难看出处理字符串的一般方法,感兴趣的读者可自行编写实现 字符串变小写、整体拷贝、逆转和查找等功能的子程序,甚至还可以建立起自己的字符串处 理库文件。 10.2 数据的分类统计程序 数据的分类和统计也是一类非数值计算,数据的分类统计方法在例 6.10中已介绍,下面通过一个例子介绍数据的分类存储问题。 例10.5 统计从地址0040H:0000H 开始的100个字中,把正数和负数按照它们先后出现的次序 分别存储在缓冲区 Data1和 Data2,并把每类的个数存入相应缓冲区的第一个字单元 中。 解:由于在指定地址之后的100个字中,可能存在全是正数或负数的情况,所以,缓冲区 Data1 和 Data2的容量都应是100个字。 .MODEL SMALL .DATA Num = 100 Data 1 DW ?, Num dup(?) Data 2 DW ?, Num dup(?) .CODE .STARTUP MO V AX, 40H MO V ES, AX LEA SI, Data1+2 ;指向存储正数的缓冲区 LEA DI, Data2+2 ;指向存储负数的缓冲区 XOR BX, BX ;BX 用于扫描存储单元 MO V CX, 100 ;字符个数 .REPEAT M OV AX, ES:[BX] AD D BX, 2 C MP AX, 0 .CONTINUE .IF ZERO? JL next1 M OV [SI], AX ;向正数缓冲区内存储数据 AD D SI, 2 .CONTINUE next1: MO [DI], AX ;向负数缓冲区内存储数据 V ADD DI, 2 .UNTILCXZ SUB SI, OFFSET Data1+2 SUB DI, OFFSET Data2+2 SHR SI, 1 SHR DI, 1 MO V Data1, SI MO V Data2, DI ;把每类的统计个数存入缓冲区的第一个字单元 .EXI T 0 END 例10.6 用键盘输入任意一字符串,分类统计该字符串中每个数字和字母的出现次数。 解: .MODEL SMALL .DATA N = 80 Buff DB N, ?, N DUP(?) Num DW 36 DUP(0) ;每个字用于存放'0'~'9','A'~'Z'出现的个数 .CODE .STARTUP LEA DX, Buff MOV AH, 0AH INT 21H ;输入一个字符串 XOR CH, CH MOV CL, Buff+1 ;CX=输入字符串的个数 LEA SI, Buff+2 XOR BX, BX .REPEAT MO V BL, [SI] ;考虑下面的思考题 INC SI .IF BL>='0' && BL<='9' ;分类统计'0'~'9'中的每个数字的个数 S U B BL, '0' A D D BX, BX I N C Num[BX] .CONTINUE .ENDIF .IF BL>='a' && BL<='z' S U B BL, 20H ;小写变大写 .ENDIF .IF BL>='A' && BL<='Z' ;分类统计'A'~'Z'中的每个字母的个数 S U B BL, 'A'-10 A D D BX, BX I N C Num[BX] .ENDIF .UNTILCXZ .EXIT 0 END 思考题:在本例中,用指令“MOV BL, [SI]”来把当前检测的字符存入 BL,当然,我 们也可以用 AL 来代替 BL,有关指令要作相应的改动,但这样做,会更方便吗?希望读者 能知道:为什么要用 BL,而不用 AL? 10.3 数据转换程序 数据类型转换是输入输出过程中经常遇到的问题。输入时,计算机系统要把用户从键盘 上输入的字符串转变成相应的数值,并存储在内存中;输出时,要把计算机内部的二进制数 据形式转换成相应的十进制字符串,然后再输出。 在高级语言编程环 境中,程序员能用各种输入输出语句,按一定的格式进行交互式操 作,很少或根本不关心输入输出是如何实现的。有的程序员甚至认为其输入的就是十进制数 值,输 出数据也就是把内存中存储的数据直接输出出来。其实,输入输出过程并不是如此 简单,计算机系统要进行复杂而又细致的数据类型转换和格式化等工作。 本节试图通过用汇编语言实现数据类型的转换来反映输入输出的本来面目,使程序员在 用高级语言编程时,对其输入输出语句的实现过程有所了解,也知道有别人(或编译程序)帮 他完成了输入输出的准备工作。 例10.7 编写一个程序,它能把字类型变量的数值以十进制形式输出出来。若该数值为负数, 则需要输出负号"-",否则,不输出符号。 例10.7是用“用16位除10”的方法从低向高依次得到每位的数值,但若待输出的数据是32 位,用10除之后,其商很可能会超过16位,所以,不能简单地引用例10.7的方法来输出32位 二进制。 假设:32位二进制数 Z 为 A×216+B,其中:A 和 B 都是16位二进制数。 用10去除 A, 得:A=A1×10+A2,于是, (1) 假设 A2×216+B 被10除后所得的商和余数分别为 B1和 C1(B1≥0,C1≥0)。 利用式(1)和“A2<10”,我们不难看出:Z 的个位就是 C1和 B1<216。 令 Z1=A1×216+B1,显然,Z1就是 Z/10所得到的商。 对于 Z1,再利用式(1)得到商 Z2和 C2。……,重复上面的步骤,直到所得商为0为止。 下面的例10.9就是利用上面方法来输出32位二进制数值。 例10.9 编写一个子程序,该子程序能把32位二进制变量的数值以十进制形式输出出来。若 该数值为负数,则需要输出负号"-",否则,不输出符号。 解: .MODEL SMALL, C .DATA CR = 13 LF = 10 Da ta1 DD 908976789 .CODE ;子程序 Display 是按十进制输出32位二进制数值 SOURCE PROC USES AX BX CX DX SI DI SOURCE:DWORD LOC AL FLAG:BYTE ;定义一个字节类型的局部变量 FLAG MO V BX, WORD PTR [SOURCE] Displ ay MO CX, WORD PTR [SOURCE+2] V MO V FLAG, 0 ;FLAG=0——正数 CMP CX, 0 JGE next INC FLAG ;FLAG=1——负数 NOT BX NOT CX ADD BX, 1 ;能否用指令 INC BX? ADC CX, 0 ;上四条指令把32位数 CX-BX 变为正数 next: XOR DI, DI ;压入堆栈字符的个数 MO V SI,10 ;用10来除 .REPEAT ;本循环把32位二进制数转换成十进制 X O R DX, DX ;数的字符串存入堆栈之中 M O V AX, CX D I V SI M O V CX, AX M O V AX, BX D I V SI A D D DL, '0' P U S H DX I N C DI M BX, AX O V .UN TIL BX==0 && CX==0 .IF FLAG==1 ;判断前面转换的数是否为负数 M O V AL, '-' ;若是,把符号'-'压入堆栈 P U S H AX I N C DI .EN DIF MO V CX, DI .REPEAT ;本循环把堆栈中的字符串显示出来 P O P DX M O V AH, 2 I N T 21H .UNTILCXZ MO V DL, CR ;下面六条指令显示回车、换行 MO V AH, 2 INT 21H MO V DL, LF MO V AH, 2 INT 21H RET ENDP .STARTUP Displ ay INV Display, Data1 OKE INV OKE Display, -123456789 .EXI T 0 END 例10.10 编写一个程序,它能把用键盘输入的字符串转化成相应的数值。具体功能如下: 1、 输入的数据字符串可以带正、负符号,如:1234、+1234或-1234; 2、 字符串的最后一个字符表示数据的进制,默认的进制为十进制,如:1234H 表示十六 进制数1234,1234为十进制数; 3、 对于任何进制的数据,当遇到一个非进制范围内的字符时,则显示出错信息,并以数 值为其转换结果来结束该类型转换过程。 汇编入门(19讲) 时间:2009-5-19 8:13:01 点击:10 核心提示:10.4 文件操作程序有关目录和顺序文件的操作在第8.3.6节中已有介绍和举 例,本节主要介绍对记录文件的读写方法。记录文件是指文件中的每个分量是一个结构的 文 件,如:Fox 系列数据库管理系统中的 DBF 文件,该文件除了文件头是由记录文件的整体 信息和各字段描述信息之外,文件的主体内容就是由同一个结构组成 的... 10.4 文件操作程序 有关目录和顺序文件的操作在第8.3.6节中已有介绍和举例,本节主要介绍对记录文件的 读写方法。记录文件是指文件中的每个分量是一个结构的文件,如:Fox 系列数据库管理系 统中的 DBF 文件,该文件除了文件头是由记录文件的整体信息和各字段描述信息之外,文 件的主体内容就是由同一个结构组成的。 下面通过二个例子来介绍记录文件的读写方法。 例10.11 假设有一个简单的学生结构类型 student,其包括:学号、姓名和年龄等信息,要求 编写一个程序,该程序接受从键盘输入的学生记录信息,并把它们保存在文件 students.dat 之中。 解: .MODEL SMALL, C .486 STRUCT id DW ? sname DB 10 DUP(?) student age DB ? student ENDS .DATA fnam e DB "Students.dat",0 msg 1 DB "Id:$" msg 2 DB "Name:$" msg 3 DB "Age:$" msg 4 DB "Continue?$" msg 5 DB "Fail to create file$" CRL F DB 0AH, 0DH, "$" buff DB ?, ?, 11 DUP(?) peas on STUDENT <> .CODE PROC USES AX DX, Msg:PTR BYTE ;显示字符串 Msg DispMs g …… ;参见例10.7 DispMs g ENDP ;程序功能:把字符串 Data 转化成数值,不考虑负数。当遇到非法字符时,则结束转换操 作; ;入口参数:Data 为字符串的首地址,Len 为该字符串的长度; ;出口参数:数值存放在 AX 中。 PROC USES BX CX SI, Len:BYTE, Data:PTR BYPE XOR CX, CX MOV CL, Len MOV SI, Data XOR AX, AX XOR BX, BX .REPEAT MO V BL, [SI] SUB BL, '0' GetDat a .BREAK .IF BL<0 || BL>9 ;判断当前数值是否在0~9之间 IMU L AX, 10 AD D AX, BX INC SI .UNTILCXZ RET GetDat a ENDP ;程序功能:读取指定长度的字符串,在输入前,显示有关输入内容的提示信息; ;入口参数:读入字符串的长度为 Len,提示信息的首地址为 MSG; ;出口参数:读入信息(字符串)存放缓冲区 buff 中。 PROC USES AX DX, Len:BYTE, Msg:PTR BYTE INVO KE DispMsg, Msg ;显示提示信息 MOV AL, Len MOV buff, AL MOV AH, 0AH LEA DX, buff INT 21H INVO KE DispMsg, ADDR CRLF ;显示回车、换行 GetInfo RET ENDP .STARTUP MOV AX, DS MOV ES, AX LEA DX, fname MOV CX, 20H MOV AH, 3CH INT 21H ;创建文件 .IF CARRY? ;若创建失败,则显示失败信息 INVOKE DispMsg, ADDR msg5 JMP over .ENDIF GetInfo MOV BX, AX ;把句柄存入 BX,为后面使用作准备 again: INVO KE DispMsg, ADDR CRLF ;显示回车、换行 INVO KE GetInfo, 5, ADDR msg1 ;读取学号(假定学号为4位整数) INVO KE GetData, 4, ADDR buff+2 ;把学号字符串转化成数值 MOV peason.id, AX ;把数值型学号存入学号字段 INVO KE GetInfo, 11, ADDR msg2 ;读取姓名(假定姓名为10个字符) MOV CX, 10 MOV AL, ' ' LEA DI, peason.sname REP STOSB ;先置姓名字段为10个空格 MOV CL, buff+1 MOV SI, OFFSET buff+2 LEA DI, peason.sname REP MOVSB ;把输入的姓名存入姓名字段 INVO KE GetInfo, 3, ADDR msg3 ;读取年龄(假定年龄为2位整数) INVO KE GetData, 2, ADDR buff+2 ;把年龄字符串转化成数值 MOV peason.age, AL ;把数值型年龄存入年龄字段 MOV CX, SIZE peason LEA DX, peason MOV AH, 40H INT 21H ;把学生记录写入文件 INVO KE DispMsg, ADDR msg4 ;提示是否继续输入 MOV AH, 1 INT 21H AND AL, 0DFH CMP AL, 'Y' JZ again ;若按 y 或 Y,则继续输入 MOV AH, 3EH INT 21H over: .EXIT 0 END 例10.12 编写一个程序显示由例10.11建立的记录文件 students.dat 中的学生信息。 解: .MODEL SMALL,C STRUCT id DW ? sname DB 10 DUP(?) studen t age DB ? studen t ENDS .DATA fna DB "Students.dat",0 me id1 DB "Id:", 4 dup(?), 0dh, 0ah, "$" nam e1 DB "Name:", 10 dup(' '), 0dh, 0ah, "$" age 1 DB "Age:", 2 dup(?), 0dh, 0ah, "$" msg 1 DB "Fail to open file$" pea son student <> .CODE PROC USES AX DX, Msg:PTR BYTE ;显示字符串 Msg Disp Msg …… ;参见例10.7 Disp Msg ENDP ;程序功能:把数据 Data 转换成长度为 Len 的字符串; ;入口参数:待转换数据 Data,转换成字符串的长度为 Len,存放字符串的 首地址为 PStr; ;出口参数:读入信息(字符串)存放缓冲区 buff 中。 PROC USES AX CX DX DI, Data:WORD, Len:WORD, PStr:PTR BYTE MOV CX, Len MOV DI, PStr MOV AL, ' ' REP STOSB ;把存放字符串的缓冲区填充为空 格 MOV DI, PStr ADD DI, Len DEC DI ;确定最后一个字符在缓冲区中的 位置 MOV AX, Data MOV CX, 10 .REPEAT XOR DX, DX IDIV CX ;除10,从低位向高位求得每一位 ADD DL, '0' ;把余数转变成字符,然后存放目 标单元 MOV [DI], DL DEC DI .UNTIL AX==0 GetStr RET GetStr ENDP .STARTUP MOV AX, DS MOV ES, AX LEA DX, fname MOV AL, 0H MOV AH, 3DH INT 21H ;以“只读”方式打开指定的文件 .IF CARRY? ;若创建失败,则显示失败信息 INVOKE DispMsg, ADDR msg1 JMP over .ENDIF MOV BX, AX ;把句柄存入 BX,为后面使用作 准备 again: MOV CX, SIZE peason LEA DX, peason MOV AH, 3FH INT 21H ;从文件中读出一个记录 .IF CARRY? || AX==0 ;若读记录出错或遇到文件尾,结 束 JM P close .ENDIF INVO KE GetStr, peason.id, 4, ADDR Id1+3 ;把“学号”转换成字符串 INVO KE DispMsg, ADDR Id1 ;显示“学号”字符串 MOV CX, 10 LEA SI, peason.sname LEA DI, Name1+5 REP MOVSB ;把“姓名”转移到显示区 INVO KE DispMsg, ADDR Name1 ;显示“姓名”字符串 INVO KE Getstr, peason.age, 2, ADDR Age1+4 ;把“年龄”转换成字符串 INVO KE Dispmsg, ADDR Age1 ;显示“年龄”字符串 JMP again close: MOV AH, 3EH INT 21H ;关闭当前打开的文件 over: .EXIT 0 END 从例10.11和10.12,我们不难掌握记录文件的读写方法。有兴趣的读者,还 可以利用文件指针的定位来指定读写某个具体的记录。 10.5 动态数据的编程 动态数据结构是一种常用的数据结构,在事先不知道所处理数据容量的情 况,用动态数据是一种行之有效的方法,也为许多 C 语言程序员所采用。在汇编 语言中,我们也可以采用动态数据的方式来存储数据,并进行链表的遍历。 为了使读者尽快理解本例的功能,我们把与之相似功能的 C 语言程序书写如 下: #include #include struct link { int data; struct link *next; }; void main( ) {struct link *head=NULL, *temp, *pt; int i; for (i = 20; i > 0; i--) { ;生成20个结点的链表 temp = (struct link *)calloc(1,sizeof(struct link)); ;申请一个结点的空间 if (temp == NULL) break; ;若分配内存失败 temp->data = i; temp->next = NULL; if (head == NULL) head = temp; else pt->next = temp; pt = temp; } while (head != NULL) { ;遍历结点并输出其数 据字段 printf("%d\n",head->data); pt = head; head = head->next; free(pt); } } 例10.13 编写一个程序用动态链表存储20,19,……,1,并用遍历链表的方法来 显示每个结点的数值。 解:显示解答 10.6 COM 文件的编程 COM 文件和 EXE 文件都是可执行文件,最典型的 COM 文件是 Command.COM。COM 文件的主要特点如下: 1、COM 文件只有一个段,其字节数不会超过64K; 2、当操作系统装入 COM 文件时,四个段寄存器(CS、DS、ES 和 SS)都 用 PSP 的段值来初始化; 3、必须用伪指令 ORG 100H 来说明空出前256个字节。 例10.14 编写一个显示字符串“Hello”的 COM 类型的程序。 CSEG SEGMENT 'CODE' OR G 100H ;空出前256个字节 LE A DX, MSG MO V AH, 09H INT 21H MO V AX, 4C00H INT 21H start: MSG DB "Hello$" ;定义字符串 ENDS 解: CSE G END start 对上面程序,其生成的 COM 文件只有23个字节,而其 EXE 文件的字节数会 超过1K。 在 PWB 编程环境下,可在 Option→Project Templates→Set Project Template→ 在列表框中选 DOS COM 来指定生成 COM 文件。在 Turbo Assember 系统中,可 用 TASM、TLINK /T 来指定生成 COM 文件。 10.7 驻留程序 驻留程序 TSR(Terminate but Stay Resident)是一种特殊应用程序,它在装入内 存运行后,其部分代码仍然驻留在内存,当该段代码被激活时,它又进入运行状 态。常用的驻留程序是作为某 个中断处理程序的一部分,其激活条件就是系统 产生了此中断的中断请求。 虽然驻留程序可根据具体的需要有不同的编写方式,但其典型结构包括以下 几部分: 1、保存、修改中断向量表; 2、程序第一次运行时的初始化部分: ◆用自己定义的地址来取代中断向量表中的原地址 ◆确定驻留代码部分的字节数 ◆用中断21H 之功能31H 把需要驻留代码部分驻留在内存 3、驻留内存的代码部分。 例10.15 在 NumLock 处于“开状态”时,每按小键盘(Numeric Keypad)上的数字键, 给出“啪啪”响声。 解:显示解答 例10.16 编写一个驻留程序,它可显示当前时间的秒数。 10.8 程序段前缀及其应用 程序段前缀 PSP(Program Segment Prefix)是一个具有256个字节的信息区,是 可执行文件(EXE 和 COM)所特有的,其内容在操作系统装入该文件运行时存入。 10.8.1 程序段前缀的字段含义 PSP 信息区的字段分布如下表10.1所列。 表10.1 PSP 信息区的字段分布表 偏移 量 内容含义 偏移 量 内容含义 00~01 H 程序结束指令中断 20H 2E~31 H 保留 02~03 H 分配给该程序的最后 段的段地址 32~33 H 文件句柄表的长度 04~09 H 保留 34~37 H 指向文件句柄表的远指针 0A~0 中断22H 的地址(处理 38~4F 保留 DH 终止程序) H 0E~11 H 中断23H 的地址(处理 ^Break) 50~51 H 中断21H 的功能调用 12~15 H 中断24H 的地址(处理 严重错误) 52~5B H 保留 16~17 H 保留 5C~6B H 参数区1 18~2B H 缺省的文件句柄表 6C~7F H 参数区2 2C~2D H 程序环境块的段地址 80~FF H 存储缺省 DTA 的缓冲区 PSP 信息区的字段说明: 18~2BH◆ 字 段: 该字段内共有20个字节,每个字节存储一个文件句柄,所以, 系统允许应用程序在某一时刻最多只能打开20个文件。 前5个字节存储系统标准设备的句柄,可参阅8.3.6节——系统标 准设备的句柄。 若某文件需要同时打开更多的文件,则需要调整文件句柄数。 即:先用中断21H 之功能4AH 释放内存,再用其功能67H 来设 置新的文件句柄数。 MOV BX, NewNum ;新的文件句柄最大 数(20~65535) MOV AH, 67H INT 21H 2C~2DH◆ 字段: 该字段存放程序运行环境的段地址,程序的缺省运行环境有160 个字节,最多可达32K。该环境含有系统命令,如:COMSPEC、 PATH、PROMPT 和 SET。 该字段存放命令行的第一个参数。假设要执行下列命令: Masm D:test.asm 5C~6BH◆ 字段: 这时,04H(驱动器 D)、8个字符的文件名和3个字符的后缀 存放在该区域,没有存放字符的单元用空格(20H)来填充。若省 缺驱动器和文件名,则第一个字节为00H,其它单元为20H。 该字段存放命令行的第二个参数。假设要执行下列命令: Masm D:test.asm, test.obj 6C~7FH◆ 字段: 这时,test.obj 作为第二参数存入该字段,存储方式如上字 段。 80~FFH◆ 字段: 该字段的第一个字节存放命令行参数的字符数,第二个字节为 空格,从第三个字节开始存放命令行参数字符。 10.8.2 程序段前缀的应用 了解和掌握 PSP 中的信息分布就是为了利用其信息。对于 EXE 文件,可用 中断21H 之功能51H 获取其段地址。如: MOV AH, 51H INT 21H ;BX=PSP 的段地址 MOV ES, BX CMP byte ptr ES:[80H], 0 ;检查 PSP 的长度 JE next … 对于 COM 文件,因为其只有一个段,所以,可用更简单的方式来检查 PSP 的内容。 MO V BX, 80H CMP byte ptr [BX], 0 ;检查 PSP 的长度 JE next … 例10.17 利用 PSP 中的信息来终止当前程序的运行。 解:显示解答 例10.18 编写程序,显示其命令行参数信息。 解:在使用命令行时,操作系统通常允许在被装入的程序名之后附加多达127个 字符(包括最后的回车符)作为其命令的参数。 本例子的目的就是要了解命令行参数的存储方法,为以后使用命令行参数作 准备。 显示解答 例10.19 编写一个类型于 DOS 内部命令 TYPE 的程序,该命令的使用方法如下: ……>type 文件1,文件2,… 其功能是分别显示其参数文件1、文件2等内容,在显示文件内容前先显示文 件名。显示完内容后,暂停,等待用户按任意键。若某文件不存在,则显示相应 的提示信息。 解:为了较好地解答本题,我们可以对该问题按以下步骤来解决: 1、若命令行参数中的字符数为0,则结束本程序的运行; 2、从命令行参数中分离出一个文件名; 3、调用子程序,显示当前文件名、文件内容和等待用户按键; 4、重复步骤1~3,直到所有参数被处理完。 显示解答 汇编入门(20讲) 时间:2009-5-19 8:16:26 点击:10 核心提示:第 11章 数值运算协处理器数值运算协处理器(简称协处理器)是特为与微处 理器协同工作而设计的,它是用于加速处理浮点数据的处理部件。对同样的浮点计算,使用 该部 件进行运算所花的执行时间要比用常规指令编写的最有效代码所花的时间还要少得 多。在早期的计算机系统中,该部件是可选部件,但现在一般都把协处理器直接内 置... 第11章 数值运算协处理器 数值运算协处理器 (简称协处理器)是特为与微处理器协同工作而设计的,它是用于加 速处理浮点数据的处理部件。对同样的浮点计算,使用该部件进行运算所花的执行时间要比 用常 规指令编写的最有效代码所花的时间还要少得多。在早期的计算机系统中,该部件是 可选部件,但现在一般都把协处理器直接内置在 CPU 之中。鉴于现在 Pentium 处理器内部 结构的特点,该处理器能同时执行一条协处理器指令和二条整数指令。 协处理器的主要产品序列有:8087、80287、80387SX、80387DX 和80487SX 等。 协处理器可处理的数据类型有:16位、32位和64位有符号整数,18位 BCD 码,32位、 64位和80位浮点数。 协处理器可处理的运算有:乘法、除法、加法、减法、求平方根、部分正切、部分反正 切和对数等运算。 11.1 协处理器的数据格式 在第4章,我们主要介绍了整数在内存中的存储形式,这显然不能满足实际编程的需要。 数据类型的另一大类就是浮点数,浮点数在内存中的存储形式就是本节所介绍的主要内容。 有关浮点数的存储格式在《计算机组成原理》中的有关章节也有详细说明,不太熟悉的读者 可进行辅助阅读。 11.1.1 有符号整数 有符号数在协处理器中的应用与前面章节中所描述的方法是一致的,它是各种数据类型 的基础。这些整数可分为:16位(字型)、32位(短整型)和64位(长整型),其最高位为符号位。 这些整数的数据格式如图11.1所示,它们所能表示的数据范围如表11.1所列。 表11.1 各类整型数据的表示范围 数据类型 范 围 字型 -32768 ~ 32767 短整型 -2147483648 ~ 2147483647 长整型 -9×1018 ~ 9×1018 在汇编语言环境下,这三种整型数据的定义符分别为:DW、DD 和 DQ。如: data1 DW 2, -340 ;16位整数 data2 DD 321, -320 ;短型整数 data3 DQ -1230, 9034 ;长型整数 11.1.2 BCD 码数据 一个 BCD 码数据在内存中占80位,共10个字节。其最高位字节用来表示正负号,其余 9个字节,每个字节内含有二个 BCD 码,所以,一个 BCD 码数据可表示18个 BCD 编码。 BCD 码的数据格式如图11.2所示。 9 … 3 2 1 0 符号 字节 … … B C D B C D B C D B C D 图11.2 BCD 码的数据格式 关于 BCD 码的正负数,有如下规定: 、若最高位字节的值为0H,则表示该 BCD 码的值为正数; 、若最高位字节的值为80H,则表示该 BCD 码的值为负数。 在汇编语言环境下,BCD 码数据的定义符为:DT。如: .387 BCD1 DT 1234, -340 该说明语句决定了数据在内存中的存储形式如下: 00000000000000001234,80000000000000000340 11.1.3 浮点数 在计算机中,浮点数一般由三部分组成:数值的符号位、阶码和有效数字(以后简称为 尾数)。这种浮点数是用科学记数法来表示的,即:浮点数=符号位.有效数字×2阶码。 Intel 系列的协处理器支持3种形式的浮点数:短型浮点数(32位)、长型浮点数(64位)和临 时浮点数(80位),它们分别对应单精度、双精度和扩展精度浮点数。这些浮点数的数据格式 都符合 IEEE-754标准,它们的具体格式如图11.3所示。 一、十进制数转换成浮点数的步骤 1、将十进制数转换成二进制数:整数部分用2来除,小数部分用2来乘; 2、规格化二进制数:改变阶码,使小数点前面仅有第一位有效数字; 3、计算阶码: ◆ 短型浮点数的阶码加上偏移量7FH ◆ 长型浮点数的阶码加上偏移量3FFH ◆ 扩展型浮点数的阶码加上偏移量3FFFH 4、以浮点数据格式存储。 把数值的符号位、阶码和尾数合在一起就得到了该数的浮点存储形式。 注意:尾数是带有一个隐含位的23位数,即:数“1.XXXX”的尾数是“XXXX”,前面的”1” 被隐含掉,它只在扩展精度的格式中才被显式表示出来。 例11.1 把十进制数100.25转换成协处理器中的浮点数 解: 1、进制转换:(100.25)10=(1100100.01)2 2、规格化:(1100100.01)2=1.10010001×26=1.10010001×2110 3、计算阶码:110+01111111=10000101 4、数值的符号位为0,阶码为:10000101,尾数为:1001 0001 0000 0000 0000 000 综合上述可得:(100.25)10的浮点形式为:0 10000101 10010001000000000000000 下面是学习和掌握十进制数转化为浮点数的控件,它可按步骤演示整个转换过程。 几个特殊数据的存储规则: 正0:所有的数据位都是0; 负0:最高位为1,其它的数据位是0; 正/负无穷:符号位为0/1,阶码位全为1,有效数字全为 0; NAN:非法的浮点数,阶码位全为1,有效数字不 全为0; 其中:NAN — Not-A-Number。 二、浮点数转换成十进制数的步骤 该步骤与前面“十进制数转换成浮点数”的步骤是互逆的,其具体步骤如下: 1、分割数字的符号、阶码和有效数字; 2、将偏移阶码减去偏移,得到真正的阶码; 3、把数字写成规格化的二进制数形式; 4、把规格化的二进制数改变成非规格化的二进制数; 5、把非规格化的二进制数转换成十进制数。 例11.2 把协处理器中的浮点数1100000111001001000000000000转换成十进制数 解 1、把浮点数1100000111001001000000000000分割成三部分,可得: 符号位是1,阶码是10000011,尾数是1001001000000000000 2、还原阶码:10000011 – 01111111=100 3、该浮点数的规格化形式:1.1001001×24 (其中前面的“1.”从隐含位而来) 4、该浮点数的非规格化形式:11001.001 5、该浮点数的十进制数为-25.125 (因为符号位为1,所以,该数是负数) 下面是学习和掌握十进制数转化为浮点数的控件,它可按步骤演示整个转换过程。 三、浮点数说明形式 在汇编语言中,可用 DD、DQ 和 DT 来分别说明单精度、双精度和扩展精度的浮点数。 在 MASM 6.0系统中,正浮点数前面不能书写‘+’,但 MASM 6.11系统更正了这种错误, 并提供了新的浮点数说明方法,即:可用 REAL4、REAL8和 REAL10来分别代替 DD、DQ 和 DT。 在定义浮点数时,要使用伪指令.8087、.287或.387等。 例如: .387 data 1 DD 123, -543 ;定义单精度浮点数 data 2 REAL 4 3.345E+3 ;定义单精度浮点数 data 3 REAL 8 321.545 ;定义双精度浮点数 data 4 REAL 10 254.555 ;定义扩展精度浮点数 11.2 协处理器的结构 协处理器,顾名思 义,是为与 CPU 协同工作而设计的,其主要用来提高进行数学和超 越函数计算的速度。在80486DX 和 Pentium 处理器中都内置一个与80387完全 兼容的协处理 器。CPU 执行所有的常规指令,协处理器则执行协处理器指令,它们能同时并行地执行各 自的指令。由于现在 Pentium 处理器内部结构的特 点,该处理器能同时执行一条协处理器 指令和二条整数指令。 11.2.1 协处理器的内部结构 协处理器80x87的内部结构如图11.4所示。它可分为二个主要部分:控制部件(CU)和数 值执行部件(NEU)。 控制部件(CU)把协处理器接到 CPU 的系统总线上,协处理器和 CPU 都监视正在执行的 指令流。如果当前将要执行的指令是协处理器指令(即:ESCape 指令),那么,协处理器会 自动执行它,否则,该指令将交给 CPU 来执行。 数值执行部件(NEU)复制执行所有的协处理器指令,它有一个用8个80位的寄存器组成 的堆栈,该堆栈用于以扩展精度的浮点数据格式来存放数学指令的操作数和运算结果。在协 处理器指令的执行过程中,要么指定该堆栈寄存器中的数据,要么使用压栈/出栈机制来从 栈顶存放或读取数据。 在 NEU 部件中,还有一些记录协处理器工作状态的寄存器,如:状态寄存器、控制寄 存器、标记寄存器和异常指针寄存器等。有关这些寄存器的作用将在后面给予分别介绍。 11.2.2 状态寄存器 状态寄存器是用来标识协处理器中指令执行情况的,它相当于 CPU 中的标志位寄存器。 80x87协处理器的状态寄存器如图11.5所示。 1 5 1 3 1 2 1 1 8 7 0 B C 3 TOP C 2 C 1 C 0 E S S F P E U E O E Z E D E IE 图11.5 80x87协处理器的状态寄存器示意图 状态寄存器各标志位(或组合位)的含义如下: ◆ B(Busy,忙) 忙标志位用来表明协处理器是否正在执行协处理器指令,它可用 FWAIT 指令来测试。 在80287及其以后的协处理器中,协处理器和 CPU 能自动实现同步,所以,现在在运行任务 时,无须测试忙标志。 ◆C3~C0(条件编码位) 四位条件编码位的组合含义如表11.2所列。 表11.2 状态寄存器中条件编码位的组合含义 指 令 C3 C2 C1 C0 功 能 0 0 X 0 ST >操作数或(0 FTST) 0 0 X 1 ST <操作数或(0 FTST) 1 0 X 0 ST =操作数或(0 FTST) FTST、FCOM 1 1 X 1 ST 不可比较 Q1 0 Q0 Q2 Q2Q1Q0是商的右边 3位 FPREM ? 1 ? ? 未完成 0 0 0 0 +unnormal 0 0 0 1 +NAN 0 0 1 0 -unnormal 0 0 1 1 -NAN 0 1 0 0 +normal 0 1 0 1 +∞ 0 1 1 0 -unnormal 0 1 1 1 -∞ 1 0 0 0 +0 FXAM 1 0 0 1 空 1 0 1 0 -0 1 0 1 1 空 1 1 0 0 +denormal 1 1 0 1 空 1 1 1 0 -denormal 1 1 1 1 空 其中,normal—标准的浮点数,unnormal—有效数字前面是0,如 :0.XXXX,denormal— 阶码是最大的负值,NAN—见11.1.3节中几个特殊数据的说明。 ◆ TOP(栈顶) 该三位二进制000~111用来表明当前作为栈顶的寄存器,通常其值为000。 ◆ ES(错误汇总) ES=PE+UE+OE+ZE+DE+IE(逻辑或运算),在8087协处理器中,当 ES 为1时,将发出 一个协处理器中断请求,但在其后的协处理器中,不再产生这样的协处理器中断申请。 ◆ SF(堆栈溢出错误) 该状态位用来表明协处理器内部的堆栈是否有上溢或下溢错误。 ◆ PE(精度错误) 该状态位用来表明运算结果或操作数是否超过先前设定的精度。 ◆UE(下溢错误) 该状态位用来表明一个非0的结果太小,不能用控制字节所选定的当前精度来表示。 ◆OE(上溢错误) 该状态位用来表明一个非0的结果太大,不能用控制字节所选定的当前精度来表示,即 超过了当前精度所能表示的数据范围。 如果在控制寄存器中屏蔽该错误标志,即设控制寄存器中的 OM 为1,那么,协处理器 把上溢结果定义为无穷大。 ◆ZE(除法错误) 该状态位用来表明当前执行了“0作除数”的除法运算。 ◆DE(非规格化错误) 该状态位用来表明当前参与运算的操作数中至少有一个操作数是没有规格化的。 ◆ IE(非法错误) 该状态位用来表明执行了一个错误的操作,如:求负数的平方根,也可用来表明堆栈的 溢出错误、不确定的格式(0/0,∞,-∞等)错误,或用 NAN 作为操作数。 对于协处理器中状 态寄存器的内容,程序员可用指令 FSTSW 把其值送到内存单元中。 如果当前使用的是80287及其以后的协处理器,那么,可用指令“FSTSW AX”把该状态寄存 器的值传送给通用寄存器 AX。一旦状态寄存器的值复制到内存或 AX 中,那么,就可对其 各位进行分析,并可检测出当前协处理器的工作状 态。 对于80287协处理器,它还可通过 I/O 地址00FAH~00FFH 来实现其与 CPU 之间的数据 交换,而80387~Pentium 系列芯片,则是通过 I/O 地址800000FAH~800000FFH 来实现这两 者之间的数据交换。 当状态寄存器的内容传给 AX 之后,一般可用下面二种方法来检测协处理器的状态。 方法1:用 TEST 指令来检测其相应的状态位。 例11.3 检测是否有“0作除数”的错误。 FDIV DATA1 ;用协处理器中堆顶数据去除 DATA1 FSTSW AX ;把状态寄存器的值传送给 AX TEST AX, 4 ;测试第2位,即:检测 ZE 是否为1 JNZ DIV_ERR 例11.4 检测是否有“非法操作数”的错误。 FSQRT ;求协处理器中堆顶数据的平方根 FSTS W AX TEST AX, 1 ;测试第0位,即:检测 IE 是否为1 JNZ SQRT_ERR 方法2:用 SAHF 指令把 AX 的低字节传送给 CPU 的标志位寄存器,然后再用条件转移 指令来完成相应的检测。 例11.5 检测内存单元的数据与协处理器堆顶数据之间的大小关系。 FCO M DATA1 ;内存单元 DATA1的值与协处理器堆顶数 据进行比较 FST SW AX SAHF ;把 AX 的低字节存入 CPU 的状态寄存器 JE ST_EQUAL ; 具体大小关系的决定可见表11.2 中的 “FCOM” JB ST_BELOW JA ST_ABOVE 11.2.3 控制寄存器 控制寄存器主要用于浮点数精度选择的控制、四舍五入的控制和无穷大的控制等,其低 6位还可用来决定是否屏蔽协处理器的异常。指令 FLDCW 可用来设置控制寄存器的值。控 制寄存器中控制位的分布如图11.6所示,其控制位的含义如表11.3所列。 1 5 1 4 1 3 1 2 1 1 1 0 9 8 7 6 5 4 3 2 1 0 I C RC PC P M U M O M Z M D M I M 图11.6 控制寄存器的控制位分布示意图 表11.3 控制寄存器中控制位的含义 控制位 控制功能说明 IC(无穷大控 制) 0—投影,假定是无符号无穷; 1—仿射,允许正、负无穷 RC(舍入控制) 00—最接近或偶数,01—舍入成负无穷, 11—舍入成负无穷,10—截成0 PC(精度控制) 00—单精度,01—保留,11—双精度,10— 扩展精度 PM 精度错误屏蔽位 UM 下溢出屏蔽位 OM 上溢出屏蔽位 ZM 除数为0屏蔽位 DM 非规格化操作数 屏蔽位 IM 非法操作屏蔽位 若屏蔽位的值为1, 则状态寄存器的相应位 被屏蔽。 11.2.4 标记寄存器 标记寄存器用来表明协处理器堆栈中各存储单元内容的状态,也就是说,该寄存器可表 明堆栈中的数据是合法的,还是非法的,是无穷,还是0或空等。该标记寄存器的结构如图 11.7所示。 15 13 11 9 7 5 3 1 0 TA G(7) TA G(6) TA G(5) TA G(4) TAG (3) TAG (2) TAG (1) TAG (0) 图11.7 标记寄存器结构示意图 其中:TAG(i)的取值含义:00—合法,01—0,10—非法或无穷,11—空 在协处理器中,查看标记寄存器的方法是使用指令 FSTENV、FSAVE 或 FRSTOR,它 们都能使标记寄存器与其它协处理器数据一起转存。 汇编入门(21讲) 时间:2009-5-19 8:21:10 点击:9 核心提示:11.3 协处理器的指令系统协处理器共有68条不同的指令,汇编程序在遇到 协处理器指令助忆时,都会将其转换成机器语言的ESC指令,ESC指令代表了协处理器的 操 作码。在协处理器指令在执行过程中,需要访问内存单元时,CPU 会为其形成内存地址。 协处理器在协处理器指令期间内利用数据总线来传递数据。80287 协... 11.3 协处理器的指令系统 协处理器共有68条不同的指令,汇编程序在遇到协处理器指令助忆时,都会将其转换成 机器语言的 ESC 指令,ESC 指令代表了协处理器的操作码。 在协处理器指令在 执行过程中,需要访问内存单元时,CPU 会为其形成内存地址。协 处理器在协处理器指令期间内利用数据总线来传递数据。80287协处理器利用 I/O 地址 00FAH~00FFH 来实现其与 CPU 之间的数据交换,而80387~Pentium 系列芯片,则是利用 I/O 地址 800000FAH~800000FFH 来实现这两者之间的数据交换。 11.3.1 指令操作符的命名规则 协处理器指令的操作符(或助忆符)在命名设计时,遵循了下列规则: 1、在操作符后面加上字母 P:表示该指令执行完后,还进行一次堆栈弹出操作。如: FADD 和 FADDP 等; 2、在操作符后面加上字母 R:表示该操作是反模式,它仅限于减法、除法指令。如: FSUB 和 FSUBR 等; 正模式 —— 栈顶数据=栈顶数据 op 指令操作数,或 OPN1=OPN1 – OPN2 反模式 —— 栈顶数据=指令操作数 op 栈顶数据,或 OPN1=OPN2 – OPN1 假设:栈顶数据为10,内存变量 data 的值为1,分别执行下列指令将有不同的结果。 FSUB data ;指令执行后,栈顶数据为9 FSUBR data ;指令执行后,栈顶数据为-9 FSUB ST, ST(1) ;指令执行后,ST=ST-ST(1) FSUBR ST, ST(1) ;指令执行后,ST=ST(1)-ST 3、操作符的第2个字母是 I:表示内存中数据是整数。它对加、减、乘除指令都有效。 例如:FADD data——浮点数加法; FIADD data——整数加法,它表示内存单元 data 是一个整数,把该整数加到栈 顶的浮点数上。 4、操作符的第2个字母是 N:表示在指令执行之前检查非屏蔽数值性错误。如:FSAVE 和 FNSAVE 等,前者称为等待形式(wait version),后者称为非等待形式(no-wait version)。 在使用.8087伪指令情况下,汇编程序会在等待形式的指令前面加上指令 WAIT,而在非 等待形式的指令前面加上空操作指令 NOP。 理解了上述操作符命名规则,就能很容易地区分同类指令之间的差异。 11.3.2 数据传送指令 为了满足协处理器和 CPU 之间进行数据交流的需求,就需要实现内存单元和协处理器 之间进行数据传送的指令。协处理器的指令系统中有三大类数据传送指令:BCD 传送指令、 浮点数传送和整数传送指令。 一、BCD 传送指令 1、FBLD 指令格式:FBLD MemBCD(*) 指令功能:将内存中的 BCD 数据压入协处理器的堆栈中; (*) MemType 是指定数据类型 Type 的内存单元,如:MemBCD 是 BCD 类型的存储单 元。此后不再说明。 2、FBSTP 指令格式:FBSTP MemBCD 指令功能:将协处理器中的 BCD 数据存入内存,并进行堆栈的弹出操作。 例如: .387 data1 DT 123, -543 data2 DT 2.5 …… FBLD data1 ;把 BCD 数据123压进栈 FBST P data2 ;把当前堆顶数据弹出,并传送给 BCD 型的内参单元 二、浮点数传送指令 1、FLD 指令格 式: FLD STReg(*)/MemReal 指令功 能: 将浮点数据压入协处理器的堆栈中。当进行内存单元内容压栈时,系统会自动决 定传送数据的精度。比如:用 DD 或 REAL4定义的内存单元数值是单精度数等。 (*) STReg 是协处理器堆栈寄存器 ST(0)~ST(7)。 例如: .387 data1 DD 123, -543 data2 REAL8 -321.5 data3 REAL10 2.5 …… FLD data1 ;压一个单精度数据进栈 FLD data2 ;压一个双精度数据进栈 FLD ST(0) ;把堆栈寄存器 ST(0)的值再压进栈 FLD data3 ;压一个扩展精度数据进栈 2、FST 指令格 式: FST STReg/MemReal 指令功 能: 将协处理器堆栈栈顶的数据传送到目标操作数中。在进行数据传送时,系统自动 根据控制寄存器中舍入控制位的设置把栈顶浮点数舍入成相应精度的数据。 3、FSTP 指令格 式: FSTP STReg/MemReal 指令功 能: 与 FST 相类似,所不同的是:指令 FST 执行完后,不进行堆栈的弹出操作,即: 堆栈不发生变化,而指令 FSTP 执行完后,则需要进行堆栈的弹出操作,堆栈将 发生变化。请见11.3.1节中的指令操作符命名规则的说明。 4、FXCH 指令格 FXCH [STReg] 式: 指令功 能: 将指定的寄存器中的浮点数与堆顶浮点数进行交换。如果不指定操作数,那么, 默认 ST 和 ST(1)二者之间交换数据。 例如:FXCH ST(2)——栈顶数据与堆栈寄存器 ST(2)进行数据交换。 三、整数传送指令 1、FILD 指令格式:FILD MemInt 其中:MemInt 是定义为整型数据类型的内存单元,但不能是用 DB 定义的存储单元。 下同,不再叙述。 2、FIST/FISTP 指令格 式: FIST MemInt FISTP MemInt 其中: Mem 是定义整型数据类型的内存单元,但不能是用 DB 定义的存储单元。 指令功 能: 将协处理器堆栈栈顶的数据传送到目标存储单元中。在进行数据传送时,系统自 动根据控制寄存器中舍入控制位的设置把栈顶浮点数舍入成整型数据。 指令 FIST 和 FISTP 的区别在于堆栈操作,详细请见11.3.1中的命名规则说明。 11.3.3 数学运算指令 在协处理器的指令系统中,有关数学运算指令有:加法指令、减法指令、乘法指令、除 法指令和求平方根指令等。涉及数学运算的指令有比例运算、舍入运算、求绝对值运算和改 变数值符号运算等指令。 1、加法指令 FADD [STReg1, STReg2](*) FADD MemReal FADD P STReg, ST 指令格 式: FIAD D MemInt (*) 在此指令格式下,如果同时指定了二个堆栈寄存器,那么,其中一个寄存器必须是 ST。其它指令的同类格式与此同理。 指令 FADD 含有二个隐含操作数 ST(1)和 ST,其运算功能是:从堆栈中弹出这二个操 作数,然后把计算的“和”压入堆栈,即:ST=ST(1)+ST。 指令“FADD MemReal”的功能:ST=ST+MemReal 指令“FADDP STReg, ST”的功能:STReg=STReg+ST,并弹出堆栈的栈顶 指令“FIADD MemInt”的功能:ST=ST+MemInt 2、减法指令 FSUB [STReg1, STReg2] FSUB MemReal FSUB P STReg, ST FISU B MemInt FSUB R [STReg1, STReg2] ;后四条指令是前四条指令的反模式形式 FSUB R MemReal FSUB RP STReg,ST 指令格 式: FISU BR MemInt 指令 FSUB 含有二个隐含操作数 ST(1)和 ST,其运算功能是:从堆栈中弹出这二个操作 数,然后把计算的“差”压入堆栈,即:ST=ST(1)-ST。 指令“FSUB MemReal”的功能:ST=ST-MemReal 指令“FSUBP STReg, ST”的功能:STReg=STReg-ST,并弹出堆栈的栈顶 指令“FISUB MemInt”的功能:ST=ST-MemInt 反模式的四条指令的功能在此从略,请参阅11.3.1中的有关说明。 3、乘法指令 FMUL[STReg1, STReg2] FMULMemReal FMUL P STReg, ST 指令格 式: FIMU L MemInt 4、除法指令 FDIV [STReg1, STReg2] FDIV MemReal 指令格 式: FDIV P STReg, ST FIDIV MemInt FDIV R [STReg1, STReg2] ;后四条指令是前四条指令的反模式形式 FDIV R MemReal FDIV RP STReg, ST FIDIV R MemInt 例如: .387 word1 DW 20 data1 REAL8 8 data2 REAL8 -2 data3 REAL8 -12 …… FLD data1 ;本例只是显示指令的使用方法,无具体的实际功能 FLD data2 FLD data3 FDIV ST(2), ST FDIV data1 FDIV P ST(2), ST FIDIV word1 5、其它数学运算指令 在协处理器中,除了完成具体的数学运算指令外,还设置了若干个与数学运算有关的运 算指令。具体的运算指令及其功能描述如表11.4所列。 表11.4 与数学运算有关的其它指令 指令格式 指令的功能 FSQRT 求栈顶数据的平方根。如果对负数求其平方 根,则会发生错误,并可通过检测状态寄存 器的标志位 IE 来确定。 FSCALE 将 ST(1)中的数(转换成整数)加上 ST 的阶 码,该指令能快速完成乘/除2n 的运算。ST(1) 中的数必须在2-15到215之间。 FPREM/FPREM1 ST=ST%ST(1),80387及其以后的协处理器 支持 FPREM1。 FRNDINT 对栈顶数据进行舍入运算,使之转换成整 数。 FXTRACT 将栈顶数据分成二部分:无偏阶码和尾数。 尾数存入栈顶,无偏阶码存入 ST(1)。它常 用将浮点数转换成小数形式打印输出。 FABS ST=|ST|,即:求栈顶数据的绝对值。 FCHS ST=-ST,即:改变栈顶数据的符号。 11.3.4 比较运算指令 使用比较指令是将栈顶中的数与其它操作数进行比较,比较结果存于状态寄存器的条件 编码位 C3~C0处(参阅表11.2)。具体的比较运算指令及其功能描述如表11.5所列。 表11.5 比较运算指令及其功能 指令格式 指令的功能 FCOM 将栈顶数据与另一个操作数进行比较,该操作数 可以存储在堆栈寄存器中,也可存储在内存中。 当在指令中不指定操作数时,其默认的操作数是 ST 和 ST(1)。 FCOMP/FCOMPP 此指令的比较功能与 FCOM 相一致,所不同的 是指出从堆栈中弹出一个,还是二个数据。 FICOM MemInt FICOMP MemInt 栈顶数据与内存单元进行整数比较。指令 FICOMP 还要弹出栈顶数据。 FUCOM [STReg] FUCOMP [STReg] FUCOMPP 反向比较,其功能与上面同类指令类似。 FTST 栈顶数据与0进行比较。比较结果对条件编码位 的影响如表11.2所示。 FXAM 检测栈顶数据是正数、负数,还是规格化数。比 较结果对条件编码位的影响如表11.2所示。 11.3.5 超越函数运算指令 超越函数运算指令是用来实现三角函数或一些特殊的算术表达式而设置的,具体的指令 及其功能说明如表11.6所列。 表11.6 超越函数运算指令及其功能 指令格式 指令的功能 FPTAN 求部分 Y/X=tanθ,角度 θ 存于栈顶,其结果: ST=X,ST(1)=Y (1)、在8087-80287中,角度 θ 的范围上是0~π/4; (2)、在80387以后的协处理器中,角度 θ 的范围上是 0~263; 如果交度超过其取值范围,则状态 IE 标定为非法错误。 FPATAN 求部分反正切值 θ=arctan(X/Y),X 取自 ST,Y 取自 ST(1); 其中:X 和 Y 必须满足:0≤Y<X<∞; 指令执行时进行一次弹出操作,结果存入栈顶。 F2XM1 求2X-1的值,X 取自栈顶,结果也存放栈顶,X 的取 值范围:-1~1。 FCOS 或 FSIN 求栈顶 ST 中角度的正弦或余弦值,ST 中存放的是弧 度。 运算结果也存于 ST。 FSINCOS 求栈顶 ST 中角度的正弦和余弦值,ST 中存放的是弧 度。 运算结果:ST=正弦值,ST(1)=余弦值。 FYL2X 计算 Ylog2X 的值,X=ST,Y=ST(1),其中:X>0。 结果存入有一个弹出操作后的栈顶。 FYL2XP1 计算 Ylog2(X+1)的值,X=ST,Y=ST(1),其中:0<X <1-2-0.5。 结果存入有一个弹出操作后的栈顶。 11.3.6 常数操作指令 为了计算的方便,协处理器提供了几个将常用常数压栈的指令。如表11.7所示。 表11.7 常数压栈指令及其常数值 指令格式 指令的功能 指令格式 指令的功能 FLDZ ST=+0.0 FLDL2T ST= log210 FLD1 ST=+1.0 FLDLG2 ST= log102 FLDPI ST=π FLDLN2 ST= loge2 FLDL2E ST=log2e 11.3.7 协处理器控制指令 协处理器控制指令是用来实现控制协处理器状态而设置的,它包括协处理器的初始化、 状态寄存器内容的存取、异常处理和任务切换等操作。具体的指令及其功能说明如表11.8所 列。 表11.8 比较运算指令及其功能 指令格式 指令的功能 FINIT/FNINIT 初始化协处理器,初始化后协处理器的状态如 表11.9所列。 FLDCW Mem16 将由操作数指定的字存储单元内容存储到控 制寄存器中。 FSTCW Mem16 FNSTCW Mem16 把控制寄存器的内容存储到由操作数指定的 字存储单元。与指令“FLDCW”相对应。 FSTSW Mem16 FSTSW AX FNSTSW Mem16 FNSTSW AX 将控制寄存器的内容传送到寄存器 AX 中。 在8087协处理器中无此指令。 FCLEX/FNCLE X 清除状态寄存器中的“错误”和“忙”标志。 FSAVE Mem FNSAVE Mem 将全部机器状态存储到内存中。 FRSTOR Mem 从内存复原机器状态,它可恢复由指令 “FSAVE/FNSAVE”保存的信息。 FSTENV Mem FNSTENVMem 存储协处理器环境。 FLDENV Mem 重新装入由指令 FSTENV/FNSTENV 存储的协 处理器环境 FINCST 堆栈指针加1。 FDECSTP 堆栈指针减1。 FFREE ST(i) 释放堆栈寄存器,即使其标记为空,但其内容 并没有改变。 FNOP 协处理器的空操作 FWAIT 使处理器处于等待状态,以便协处理器完成其 操作。该指令主要用于在 CPU 访问被协处理器 影响的内存数据之前。 表11.9 协处理器初始化的状态 控制项 值 状态含义 控制位 IC 0 投影 控制位 PC 10 扩展精度 控制位 RC 00 最近舍入或偶数 错误屏蔽 11111 错误位关闭 忙标志 0 不忙 C3~C0 ???? 未定 TOP 000 堆栈栈顶设定为寄存器 0 ES 0 无错误 错误位 00000 无错误 全部标记 11 空 寄存器 -- 不改变 11.4 协处理器的编程举例 本节提供几个利用协处理器进行编程的例子,从这些例子可看出使用协处理器指令编程 的方法和技巧。 例11.6 假设圆的半径存于数组 RAD 中,计算出每个圆的面积,并存于数组 AREA 中。 解: 例11.7 已知 L=0.0001,C=0.002,试计算下列公式的值,并存于单精度数 F 中。 解: 例11.8 已知 L=4.0,F 按每次递增10.0的幅度从10.0增加到1000.0,试按公式 Y=2πFL,计 算出100个 Y 值,并把它们存入数组 RES 中。 例11.9 把内存单元 DATA 中存放的单精度浮点数以小数的形式显示在屏幕上。 例11.10 从键盘上读入一个带小数的数字字符串,然后把它转换成单精度浮点数,并存入内 存单元 DATA 中。 例11.11 编写一个子程序,求解一元二次方程的根 ax2 + bx + c = 0的根。 汇编入门(22讲) 时间:2009-5-19 8:26:17 点击:11 核心提示:第 12章 汇编语言和 C 语言 C/C++语言是一个被广泛使用的程序设计语言, 它不仅具有良好的高级语言特征,而且还具有一些低级语言的特点,如:寄存器变量、位操 作 等。所以,C 语言的程序与汇编语言程序之间能很平滑地衔接。另外,目前主要的 C 语 言程序开发环境,如:Turbo C/C++、Borland C/C++... 第12章 汇编语言和 C 语言 C/C++语言是 一个被广泛使用的程序设计语言,它不仅具有良好的高级语言特征,而 且还具有一些低级语言的特点,如:寄存器变量、位操作等。所以,C 语言的程序与汇编语 言 程序之间能很平滑地衔接。另外,目前主要的 C 语言程序开发环境,如:Turbo C/C++、 Borland C/C++等,也都提供了很好的混合编程手段。 本章主要介绍汇编语言和 C 语言的混合编程和调用方法。虽然其它高级语言,如:Pascal、 Basic 等,也可与汇编语言混合使用,但出于其应用范围的考虑,不再对它们进行介绍,感 兴趣的读者可参阅有关技术资料。 12.1 汇编指令的嵌入 为了提高 C 语言程序内某特殊功能段的处理效率,我们可以在其源程序中嵌入一段汇 编语言程序段。这样做,虽然能达到提高了程序处理效率的目的,但它无疑以丧失源程序的 可移植性为代价。所以,当想用 C 语言和汇编语言混合编程时,程序员需要权衡采用这种 方法的利与弊。 在 C 语言中,嵌入汇编语言的语法如下(*): asm <; or newline> 注意:这里的分号';'不是汇编语言中起注释作用的分号,而是作为语句的分隔符。 若 C 语言源程序中嵌入一条汇编语句,则可按下列方式来做: asm mov ax, data 若要嵌入一组汇编语句,则需要用括号'{'和'}'把它们括起来。 asm { mov ax, data1 xchg ax, data2 mov data1, ax //实现整型变量 data1和 data2之值的交换 } 例12.1 在 C 语言源程序中嵌入汇编语言语句实现赋值语句 A=A+B+C,其中:A、B、C 都 是整型变量。 解: …… asm {push ax //实现整型变量 A=A+B+C mov ax, A add ax, B add ax, C mov A, ax pop ax } 12.2 C 语言程序的汇编输出 在 Turbo C++或 Borland C++编程环境下,我们可 TCC 或 BCC 行命令把一个 C 语言的 源程序转换成汇编语言的源程序。通过阅读汇编语言程序可以很准确地知道 C 语言语句的 功能是如 何实现的,这样,可为将来学习《编译原理》课程中的"寄存器调度"和"代码生成 "等相关知识打下良好的基础。 C 语言源程序转换的命令格式如下: TCC -S t1.cpp 或 BCC -S t1.cpp ;假设其文件名为 t1.cpp 若命令 TCC/BCC 不带参数的话,则将显示其使用方法。 下面是 C 语言程序及其相对应的汇编语言程序,希望读者能逐行对照理解它们语句之 间的转换关系,这将能进一步理解高级语言的语句功能。 1、C 语言程序清单 #include int sum(int a, int b, int c) { return (a+b+c); } void main() {int a, b, c; a = b = 12; c = 32; printf("%d", sum(a,b,c)); } 2、生成的汇编语言程序清单 …… ;一系列辅助说明信息 _TEXT segment byte public 'CODE' ;代码段的开始 ;int sum(int a, int b, int c) ;C 语言语句 ;{ ;return(a+b+c); ;} ;void main() ;{int a, b, c; ;局部变量是用堆栈来存储的,请 见第7.5.10节 ; a = b = 12; ;给局部变量赋值 ; c = 32; ; printf("%d", sum(a,b,c)); ;调用系统标准函数 ; } _TEXT ends ;代码段的结束 _DATA segment word public 'DATA' ;数据段的定义 s@ label byte db '%d' db 0 _DATA ends public _main ;下面说明函数的属性,请见第 7.6.3节 public @sum$qiii extrn _printf : near _s@ equ s@ end 12.3 简单的屏幕编辑程序 下面是一个简单的 屏幕编辑的 C 语言程序,它不仅涉及到键盘处理、光标定位、屏幕 输出、字符颜色等,而且还运用了 C 语言和汇编语言的混合编程方法。若读者能把它改写 成相同功 能的汇编语言程序,那么,可以说,你已基本掌握了中断的使用方法,也对计算 机输入输出的工作方式有了更进一步的认识。 该程序的功能: ◆ 可用移动光标键↑、↓、←和→移动光标1行或1列,也可用 TAB/Shift+TAB、Home 和 End 键跳跃地移动光标; ◆ 当光标已在第1行,再向上移动时,这时,光标被定位到第25行,反之也然; ◆ 当光标已在第0列,还要向左移动时,光标被定位到第79列,反之也然; ◆ 当按下^W 或^Z 时,屏幕将向上或向下滚动1行; ◆ 显示当前键盘的状态:大小写状态、数字键盘状态和插入/修改状态; ◆ 如果按普通的键,将在屏幕上显示该字符,如果按下用 Alt、Ctrl 或 Shift 组合的组 合键,则显示该按键的扫描码; ◆ 用 Esc 键来结束程序的运行。 C 语言的源程序清单: #define NUM_KEY 0x20 /* 键盘状态字宏定义 */ #define CAPS_KEY 0x40 #define ESCAPE 27 /* 几个功能键的宏定义 */ #define TAB_KEY 9 #define SHIFT_TAB 15 #define CTRL_W 23 #define CTRL_Z 26 #define UP_ARROW 72 #define DOWN_ARROW 80 #define LEFT_ARROW 75 #define RIGHT_ARROW 77 #define INSERT 82 #define END_KEY 79 #define HOME_KEY 71 #define UP_SCROLL 6 /* 屏幕滚动宏定义 */ #define DOWN_SCROLL 7 #include int insert, cap_key, num_key; /* up_down:屏幕滚动方式:6-向上滚; 7-向下滚 (l_row, l_col)-(r_row, r_col):滚动矩形的对角线坐标 num:屏幕滚动的行数,0-清屏 attr:滚动后所剩下行的属性 */ cls(int up_down, int l_row, int l_col, int r_row, int r_col, int num, int attr) get_cursor(int *x, int *y) /* 取当前光标的位置,并分别存入变量 x 和 y 中 */ locate(int row, int col) /* 把光标设置在(row, col)位置 */ disp_string(int row, int col, char string[]) /* 在(row, col)位置显示字符串 string */ check_key_state() /* 在(row, col)位置以属性 attr 显示字符 ch */ insert_key() /* 在最后一行显示插入/修改状态标志,并改变光标形状 */ move_right(int row, int col, int len) /* 在(row, col)位置之后的字符和属性向后移 len 个位置 */ read_char_attr(int row, int col, char *ch, int *attr) /* 在读(row, col)位置字符和属性,并 分别存入 ch 和 attr */ write_char_attr(int row, int col, char ch, int attr) /* 在(row, col)位置以属性 attr 显示字 符 ch */ ctos(char ascii, char str[]) /* 把字符的 ASCII 码转换成字符串 */ main() {int k, key, row, col; char ch1, ch2, str[]=" $"; /* 前面有3个空格 */ char msg1[]="This is a simple screen edidtor.$", msg2[]="You can move cursor by Arrow keys, TAB/Shift-TAB, Home and End.$", msg3[]="You can press ^W for scroll up or ^Z for scroll down.$", msg4[]="It has some functions, such as insert/modify a char.$", msg5[]="If you press a function key, or key combined with Alt, Ctrl, Shift, it will display the key's scan code.$", msg6[]="The program exits when you press ESCAPE.$"; cls(UP_SCROLL, 0, 0, 24, 79, 0, 7); disp_string(0, 0, msg1); disp_string(2, 0, msg2); disp_string(4, 0, msg3); disp_string(6, 0, msg4); disp_string(8, 0, msg5); disp_string(11, 0, msg6); row = col = ch1 = insert = 0; locate(row, col); while (ch1 != ESCAPE) {while (ch1 != ESCAPE) {if (!bioskey(1)) {check_key_state(); continue;} key = bioskey(0); ch1 = key; ch2 = key >> 8; if (ch1 != 0) {switch(ch1) {case TAB_KEY: col = ((col&0xFFF8) + 8) %80; break; case CTRL_W: cls(DOWN_SCROLL, 0, 0, 24, 79, 1, 7); row = row + 1; break; case CTRL_Z: cls(UP_SCROLL, 0, 0, 24, 79, 1, 7); break; default: if (ch1 == ESCAPE) continue; if (insert) move_right(row, col, 1); write_char_attr(row, col, ch1, 31); col = (col+1+80) % 80; break; } locate(row, col); continue; } switch (ch2) {case UP_ARROW: row = (row-1+25) % 25; break; case DOWN_ARROW: row = (row+1+25) % 25; break; case LEFT_ARROW: col = (col-1+80) % 80; break; case RIGHT_ARROW: col = (col+1+80) % 80; break; case SHIFT_TAB: k = col & 0xFFF8; col = (col - ((k==0)? 8:k+80)) % 80; break; case HOME_KEY: col = 0; break; case END_KEY: col = 79; break; case INSERT: insert_key(&insert); break; default: ctos(ch2, str); k = strlen(str)-1; if (insert) move_right(row, col, k); disp_string(row, col, str); col = (col + k + 80) % 80; break; } locate(row, col); } } cls(UP_SCROLL, 0, 0, 24, 79, 0, 7); }
还剩229页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

l277951

贡献于2015-05-05

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf