- 1. 第1章 基础知识1.1 机器语言 1. 9 数据总线
1.2 汇编语言的产生 1.10 控制总线
1.3 汇编语言的组成 1.11 内存地址空间(概述)
1.4 存储器 1.12 主板
1.5 指令和数据 1.13 接口卡
1.6 存储单元 1.14 各类存储器芯片
1.7 CPU对存储器的读写 1.15 内存地址空间
1.8 地址总线
- 2. 引言汇编语言是直接在硬件之上工作的编程语言,首先要了解硬件系统的结构,才能有效的应用汇编语言对其编程。
在本章中,对硬件系统结构的问题进行一部分的探讨,以使后续的课程可在一个好的基础上进行。
- 3. 引言当课程进行到需要补充新的基础知识(关于编程结构或其他的)时候,再对相关的基础知识进行介绍和探讨。
本书的原则是,以后用到的知识,以后再说。
- 4. 引言汇编课程的研究重点放在如何利用硬件系统的编程结构和指令集有效灵活的控制系统进行工作。
- 5. 1.1 机器语言
机器语言是机器指令的集合。
机器指令展开来讲就是一台机器可以正确执行的命令。
- 6. 1.1 机器语言指令:01010000 (PUSH AX)
电平脉冲:
- 7. 1.1 机器语言以后我们提到的计算机是指由CPU 和其他受CPU 直接或间接控制的芯片、器件、设备组成的计算机系统;
比如我们最常见的PC 机。
- 8. 1.1 机器语言程序员们将 0、1 数字编程的程序代码打在纸带或卡片上,1打孔,0不打孔,再将程序通过纸带机或卡片机输入计算机,进行运算。
示例
应用8086CPU完成运算:
S = 768 + 12288 – 1280
- 9. 1.1 机器语言S = 768 + 12288 - 1280
机器码:
101100000000000000000011
000001010000000000110000
001011010000000000000101
假如将程序错写成以下这样,请找处错误:
101100000000000000000011
000001010000000000110000
000101101000000000000101
- 10. 1.1 机器语言在显示器上输出“welcome to masm”。
机器码
看到这样的程序,你会有什么感想?
如果程序里有一个“ 1 ”被误写为“ 0 ”,又如何去查找呢?
- 11. 1.2 汇编语言的产生汇编语言的主体是汇编指令。
汇编指令和机器指令的差别在于指令的表示方法上。汇编指令是机器指令便于记忆的书写格式。
汇编指令是机器指令的助记符。
- 12. 1.2 汇编语言的产生机器指令: 1000100111011000
操作:寄存器BX的内容送到AX中
汇编指令:MOV AX,BX
这样的写法与人类语言接近,便于阅读和记忆。
- 13. 寄存器寄存器:简单的讲是CPU中可以存储数据的器件,一个CPU中有多个寄存器。
AX是其中一个寄存器的代号,
BX是另一个寄存器的代号。
更详细的内容我们在以后的课程中将会讲到。
- 14. 1.2 汇编语言的产生
计算机能读懂的只有机器指令,那么如何让计算机执行程序员用汇编指令编写的程序呢?
- 15. 用汇编语言编写程序的工作过程
- 16. 1.3 汇编语言的组成汇编语言由以下3类组成:
1、汇编指令(机器码的助记符)
2、伪指令 (由编译器执行)
3、其它符号(由编译器识别)
汇编语言的核心是汇编指令,它决定了汇编语言的特性。
- 17. 1.4 存储器CPU 是计算机的核心部件.它控制整个计算机的运作并进行运算,要想让一个CPU 工作,就必须向它提供指令和数据。
指令和数据在存储器中存放,也就是平时所说的内存。
- 18. 1.4 存储器
在一台PC机中内存的作用仅次于CPU。
离开了内存,性能再好的CPU也无法工作。
- 19. 1.4 存储器
磁盘不同于内存,磁盘上的数据或程序如果不读到内存中,就无法被CPU 使用。
- 20. 1.5 指令和数据
指令和数据是应用上的概念。
在内存或磁盘上,指令和数据没有任何区别,都是二进制信息。
- 21. 1.5 指令和数据二进制信息:
1000100111011000
─> 89D8H (数据)
1000100111011000
─> MOV AX,BX (程序)
- 22. 1.6 存储单元存储器被划分为若干个存储单元,每个存储单元从0开始顺序编号;
例如:
一个存储器有128个存储单元,
编号从0~127。
如右图示:
- 23. 1.6 存储单元对于大容量的存储器一般还用以下单位来计量容量(以下用B来代表Byte):
1KB=1024B
1MB=1024KB
1GB=1024MB
1TB=1024GB
磁盘的容量单位同内存的一样,实际上以上单位是微机中常用的计量单位。
- 24. 1.7 CPU对存储器的读写CPU要想进行数据的读写,必须和外部器件(标准的说法是芯片)进行三类信息的交互:
存储单元的地址(地址信息)
器件的选择,读或写命令(控制信息)
读或写的数据(数据信息)
- 25. 1.7 CPU对存储器的读写那么CPU是通过什么将地址、数据和控制信息传到存储芯片中的呢?
电子计算机能处理、传输的信息都是电信号,电信号当然要用导线传送。
- 26. 1.7 CPU对存储器的读写在计算机中专门有连接CPU和其他芯片的导线,通常称为总线。
物理上:一根根导线的集合;
逻辑上划分为:
地址总线
数据总线
控制总线
图示
- 27. 1.7 CPU对存储器的读写总线在逻辑上划分的图示:
- 28. 1.7 CPU对存储器的读写CPU在内存中读或写的数据演示:
读演示
写演示
从上面我们知道CPU是如何进行数据读写的。可是我们如何命令计算机进行数据的读写呢?
- 29. 1.7 CPU对存储器的读写
- 30. 1.7 CPU对存储器的读写
- 31. 1.7 CPU对存储器的读写对于8086CPU,下面的机器码能够完成从3号单元读数据:
机器码: 101000000000001100000000
含义:从3号单元读取数据送入寄存器AX
CPU接收这条机器码后将完成上面所述的读写工作。
- 32. 1.7 CPU对存储器的读写机器码难于记忆,用汇编指令来表示,情况如下:
机器码:101000000000001100000000
对应的汇编指令:MOV AX,[3]
含义:传送3号单元的内容到AX
- 33. 1.8 地址总线CPU是通过地址总线来指定存储单元的。
地址总线上能传送多少个不同的信息,CPU就可以对多少个存储单元进行寻址。
- 34. 1.8 地址总线
地址总线发送地址信息演示
- 35. 1.8 地址总线
- 36. 1.8 地址总线一个CPU有N根地址总线,则可以说这个CPU的地址总线的宽度为N。
这样的CPU最多可以寻找2的N次方个内存单元。
- 37. 1.9 数据总线CPU与内存或其它器件之间的数据传送是通过数据总线来进行的。
数据总线的宽度决定了CPU和外界的数据传送速度。
- 38. 1.9 数据总线我们来分别看一下它们向内存中写入数据89D8H时,是如何通过数据总线传送数据的:
8088CPU数据总线上的数据传送情况
8086CPU数据总线上的数据传送情况
- 39. 1.9 数据总线 8位数据总线上传送的信息
- 40. 1.9 数据总线 16位数据总线上传送的信息
- 41. 1.10 控制总线CPU对外部器件的控制是通过控制总线来进行的。在这里控制总线是个总称,控制总线是一些不同控制线的集合。
有多少根控制总线,就意味着CPU提供了对外部器件的多少种控制。
所以,控制总线的宽度决定了CPU对外部器件的控制能力。
控制总线上发送的控制信息
- 42. 1.10 控制总线
- 43. 1.10 控制总线前面所讲的内存读或写命令是由几根控制线综合发出的:
其中有一根名为读信号输出控制线负责由CPU 向外传送读信号,CPU 向该控制线上输出低电平表示将要读取数据;
有一根名为写信号输出控制线负责由CPU向外传送写信号。
- 44. 1.1节~1.10节 小结
(1)汇编指令是机器指令的助记符,同机器指令一一对应。
(2)每一种CPU都有自己的汇编指令集。
- 45. 1.1节~1.10节 小结
(3)CPU可以直接使用的信息在存储器中存放。
(4)在存储器中指令和数据没有任何区别,都是二进制信息。
- 46. 1.1节~1.10节 小结(5)存储单元从零开始顺序编号。
(6)一个存储单元可以存储 8 个 bit (用作单位写成“b”),即 8 位二进制数。
(7)1B = 8b 1KB = 1024B
1MB = 1024KB 1GB = 1024MB
- 47. 1.1节~1.10节 小结(续)(8)每一个CPU芯片都有许多管脚,这些管脚和总线相连。也可以说,这些管脚引出总线。一个CPU可以引出三种总线的宽度标志了这个CPU的不同方面的性能:
地址总线的宽度决定了CPU的寻址能力;
数据总线的宽度决定了CPU与其它器件进行数据传送时的一次数据传送量;
控制总线宽度决定了CPU对系统中其它器件的控制能力。
- 48. 1.1节~1.10节 小结(续)
在汇编课程中,我们从功能的角度介绍了这三类总线,对实际的连接情况不做讨论。
特别提示
- 49. 特别提示
检测点 1.1(Page 8)
没有通过检测点请不要向下学习!
- 50. 1.11 内存地址空间(概述)什么是内存地址空间呢?
一个CPU的地址线宽度为10,那么可以寻址1024个内存单元,这1024个可寻到的内存单元就构成这个CPU的内存地址空间。下面深入讨论。
首先需要介绍两部分基本知识,主板和接口卡。
- 51. 1.12 主板在每一台PC机中,都有一个主板,主板上有核心器件和一些主要器件。
这些器件通过总线(地址总线、数据总线、控制总线)相连。
- 52. 1.13 接口卡计算机系统中,所有可用程序控制其工作的设备,必须受到CPU的控制。
CPU对外部设备不能直接控制,如显示器、音箱、打印机等。直接控制这些设备进行工作的是插在扩展插槽上的接口卡。
- 53. 1.14 各类存储器芯片从读写属性上看分为两类:
随机存储器(RAM)和只读存储器(ROM)
从功能和连接上分类:
随机存储器RAM
装有BIOS的ROM
接口卡上的RAM
PC机中各类存储器的逻辑连接情况
- 54. 1.14 各类存储器芯片装有BIOS的ROM
BIOS:Basic Input/Output System,基本输入输出系统。
BIOS是由主板和各类接口卡(如:显卡、网卡等)厂商提供的软件系统,可以通过它利用该硬件设备进行最基本的输入输出。在主板和某些接口卡上插有存储相应BIOS的ROM。
- 55. (本页无文本内容)
- 56. 1.15 内存地址空间上述的那些存储器在物理上是独立的器件。
但是它们在以下两点上相同:
1、都和CPU的总线相连。
2、CPU对它们进行读或写的时候都通过控制线发出内存读写命令。
- 57. 1.15 内存地址空间将各各类存储器看作一个逻辑存储器:
所有的物理存储器被看作一个由若干存储单元组成的逻辑存储器;
每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间;
CPU在这段地址空间中读写数据,实际上就是在相对应的物理存储器中读写数据。
- 58. (本页无文本内容)
- 59. 1.15 内存地址空间假设,上图中的内存空间地址段分配如下:
地址0~7FFFH的32KB空间为主随机存储器的地址空间;
地址8000H~9FFFH的8KB空间为显存地址空间;
地址A000H~FFFFH的24KB空间为各个ROM的地址空间。
- 60. 1.15 内存地址空间不同的计算机系统的内存地址空间分配情况是不同的。
8086PC机内存地址空间分配的基本情况
- 61. 8086PC机的内存地址空间分配
- 62. 1.15 内存地址空间内存地址空间:
最终运行程序的是CPU,我们用汇编编程的时候,必须要从CPU角度考虑问题。
对CPU来讲,系统中的所有存储器中的存储单元都处于一个统一的逻辑存储器中,它的容量受CPU寻址能力的限制。这个逻辑存储器即是我们所说的内存地址空间。
- 63. 小结
- 64. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 65. 第2章 寄存器(CPU工作原理)2.1 通用寄存器
2.2 字在寄存器中的存储
2.3 几条汇编指令
2.4 物理地址
2.5 16位结构的CPU
2.6 8086CPU给出物理地址的方法
2.7 “段地址×16+偏移地址
=物理地址”的本质含义
2.8 段的概念
2.9 段寄存器
2.10 CS和IP
2.12 代码段
- 66. CPU概述一个典型的CPU由运算器、控制器、寄存器等器件组成,这些器件靠内部总线相连。
内部总线实现CPU内部各个器件之间的联系。
外部总线实现CPU和主板上其它器件的联系。
- 67. 寄存器概述8086CPU有14个寄存器 它们的名称为:
AX、BX、CX、DX、SI、DI、SP、BP、
IP、CS、SS、DS、ES、PSW。
这些寄存器以后会陆续介绍
- 68. 2.1 通用寄存器8086CPU所有的寄存器都是16位的,可以存放两个字节。
AX、BX、CX、DX 通常用来存放一般性数据被称为通用寄存器。
下面以AX为例,我们看一下寄存器的逻辑结构。
- 69. 一个16位寄存器可以存储一个16位的数据。(数据的存放情况)
一个16位寄存器所能存储的数据的最大值为多少?
答案:216-1。2.1 通用寄存器
- 70. 16位数据在寄存器中的存放情况数据:18
二进制表示:10010
在寄存器AX中的存储:
- 71. 16位数据在寄存器中的存放情况数据:20000
二进制表示:0100111000100000
在寄存器AX中的存储:
- 72. 2.1 通用寄存器8086上一代CPU中的寄存器都是8位的;
为保证兼容性,这四个寄存器都可以分为两个独立的8位寄存器使用。
AX可以分为AH和AL;
BX可以分为BH和BL;
CX可以分为CH和CL;
DX可以分为DH和DL。
8086CPU的8位寄存器存储逻辑
- 73. 2.1 通用寄存器以AX为例,8086CPU的16位寄存器分为两个8位寄存器的情况:
- 74. 2.1 通用寄存器AX的低8位(0位~7位)构成了AL寄存器,高8位(8位~15位)构成了AH寄存器。
AH和AL寄存器是可以独立使用的8位寄存器。
8086CPU的8位寄存器数据存储情况
一个8位寄存器所能存储的数据的最大值是多少?
答案:28-1。
- 75. 2.1 通用寄存器
- 76. 2.2 字在寄存器中的存储一个字可以存在一个16位寄存器中,这个字的高位字节和低位字节自然就存在这个寄存器的高8位寄存器和低8位寄存器中。
- 77. 关于数制的讨论由于一个内存单元可以存放 8位数据,CPU中的寄存器又可存放 n 个 8位数据。也就是说,计算机中的数据大多
是由1~N个8位数据构成的。
用十六进制来表示数据可以直观的看
出这个数据是由哪些8位数据构成的。
示例
- 78. 2.3 几条汇编指令汇编指令不区分大小写
- 79. 2.3 几条汇编指令CPU执行下表中的程序段的每条指令后,对寄存器中的数据进行的改变。
- 80. 2.3 几条汇编指令
- 81. 2.3 几条汇编指令
这里的丢失,指的是进位制不能在 8 位寄存器中保存,但是 CPU 不是并真的不丢弃 这个进位值,这个问题会在后面的课程中讨论。
- 82. 特别提示
检测点2.1(Page 18)
没有通过检测点请不要向下学习!
- 83. 2.4 物理地址CPU访问内存单元时要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间。
每一个内存单元在这个空间中都有唯一的地址,这个唯一的地址称为物理地址。
- 84. 2.5 16位结构的CPU概括的讲,16位结构描述了一个CPU具有以下几个方面特征:
1、运算器一次最多可以处理16位的数据。
2、寄存器的最大宽度为16位。
3、寄存器和运算器之间的通路是16位的。
- 85. 2.6 8086CPU给出物理地址的方法8086有20位地址总线,可传送20位地址,寻址能力为1M。
8086内部为16位结构,它只能传送16位的地址,表现出的寻址能力却只有64K。
- 86. 2.6 8086CPU给出物理地址的方法
8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。
8086CPU相关部件的逻辑结构
- 87. 在8086CPU内部用两个16位地址合成的方法来形成一个20位的物理地址
- 88. 地址加法器地址加法器合成物理地址的方法:
物理地址=段地址×16+偏移地址
例如:
8086CPU访问地址为123C8H的内存单元
由段地址×16引发的讨论
- 89. (本页无文本内容)
- 90. 观察移位次数和各种形式数据的关系:
(1)一个数据的二进制形式左移1位,相当于该数据乘以2;
(2)一个数据的二进制形式左移N位,相当于该数据乘以2的N次方;
(3)地址加法器如何完成段地址×16的运算?
以二进制形式存放的段地址左移4位。由段地址×16引发的讨论
- 91. 2.7 “段地址×16+偏移地址=物理地址”的本质含义两个比喻说明:
说明“基础地址+偏移地址 = 物理地址” 的思想:第一个比喻
说明“段地址×16+偏移地址=物理地址”的思想:第二个比喻
8086CPU就是这样一个只能提供两张3位数据纸条的CPU。
- 92. “基础地址+偏移地址 = 物理地址”比如说,学校、体育馆同在一条笔直的单行路上(学校位于路的起点0米处)。
读者在学校,要去图书馆,问我那里的地址,我可以用几种方式描述这个地址?
- 93. “基础地址+偏移地址 = 物理地址”(1)从学校走2826m到图书馆。这2826可以认为是图书馆的物理地址。
(2)从学校走2000m到体育馆,从体育馆再走826m到图书馆。
第一个距离2000m是相对于起点的基础地址;
第二个距离826m是将对于基础地址的偏移地址。
- 94. “段地址×16+偏移地址=物理地址”比如我们只能通过纸条来通信,读者问我图书馆的地址,我只能将它写在纸上告诉读者。
显然我必须有一张可以容纳 4 位数据的纸条才能写下2826这个数据:
- 95. “段地址×16+偏移地址=物理地址”不巧的是,没有能容纳4位数据的纸条,仅有两张可以容纳3位数据的纸条。
这样我只能以这种方式告诉读者2826这个数据:
- 96. 2.8 段的概念错误认识:
内存被划分成了一个一个的段,每一个段有一个段地址。
其实:
内存并没有分段,段的划分来自于CPU,由于8086CPU用“(段地址×16)+偏移地址=物理地址”的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存。
- 97. 2.8 段的概念我们可以认为:地址10000H~100FFH的内存单元组成一个段,该段的起始地址( 基础地址)为10000H,段地址为1000H,大小为100H。
- 98. 2.8 段的概念我们也可以认为地址10000H~1007FH、10080H~100FFH 的内存单元组成两个段,它们的起始地址( 基础地址 )为10000H和10080H,段地址为:1000H 和1008H,大小都为80H。
- 99. 2.8 段的概念以后,在编程时可以根据需要,将若干地址连续的内存单元看作一个段,用段地址×16定位段的起始地址(基础地址),用偏移地址定位段中的内存单元。
两点需要注意
内存单元地址小结
特别提示
- 100. 两点需要注意(1)段地址×16 必然是 16的倍数,所以一个段的起始地址也一定是16的倍数;
(2)偏移地址为16位,16 位地址的寻址能力为 64K,所以一个段的长度最大为64K。
- 101. 内存单元地址小结CPU访问内存单元时,必须向内存提供内存单元的物理地址。
8086CPU在内部用段地址和偏移地址移位相加的方法形成最终的物理地址。
思考两个问题
- 102. (1)观察下面的地址,读者有什么发现?
结论:CPU可以用不同的段地址和偏移地址形成同一个物理地址。内存单元地址小结
- 103. 内存单元地址小结(2)如果给定一个段地址,仅通过变化偏移地址来进行寻址,最多可以定位多少内存单元?
结论:偏移地址16位,变化范围为0~FFFFH,仅用偏移地址来寻址最多可寻64K个内存单元。
比如:给定段地址1000H,用偏移地址寻址,CPU的寻址范围为:10000H~1FFFFH。
- 104. 内存单元地址小结在8086PC机中,存储单元的地址用两个元素来描述。即段地址和偏移地址。
“数据在21F60H内存单元中。”对于8086PC机的两种描述:
(a)数据存在内存2000:1F60单元中;
(b)数据存在内存的2000段中的1F60H单元中。
可根据需要,将地址连续、起始地址为16的倍数的一组内存单元定义为一个段。
- 105. 特别提示
检测点2.2(Page 23)
没有通过检测点请不要向下学习!
- 106. 2.9 段寄存器段寄存器就是提供段地址的。
8086CPU有4个段寄存器:
CS、DS、SS、ES
当8086CPU要访问内存时,由这4个段寄存器提供内存单元的段地址。
- 107. 2.10 CS和IPCS和IP是8086CPU中最关键的寄存器,它们指示了CPU当前要读取指令的地址。
CS为代码段寄存器;
IP为指令指针寄存器。
- 108. 8086PC读取和执行指令相关部件8086PC读取和执行指令演示
8086PC工作过程的简要描述
- 109. (本页无文本内容)
- 110. 8086PC工作过程的简要描述(1)从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲器;
(2)IP = IP + 所读取指令的长度,从而指向下一条指令;
(3)执行指令。 转到步骤 (1),重复这个过程。
- 111. 8086PC工作过程的简要描述在 8086CPU 加电启动或复位后( 即 CPU刚开始工作时)CS和IP被设置为CS=FFFFH,IP=0000H,即在8086PC机刚启动时,CPU从内存FFFF0H单元中读取指令执行,FFFF0H单元中的指令是8086PC机开机后执行的第一条指令。
- 112. 2.10 CS和IP内存中指令和数据没有任何区别,都是二进制信息,CPU在工作的时候把有的信息看作指令,有的信息看作数据。
CPU根据什么将内存中的信息看作指令?
CPU将CS:IP指向的内存单元中的内容看作指令。
- 113. 2.10 CS和IP在任何时候,CPU将CS、IP中的内容当作指令的段地址和偏移地址,用它们合成指令的物理地址,到内存中读取指令码,执行。
如果说,内存中的一段信息曾被CPU执行过的话,那么,它所在的内存单元必然被CS:IP指向过。
- 114. 2.11 修改CS、IP的指令在CPU中,程序员能够用指令读写的部件只有寄存器,程序员可以通过改变寄存器中的内容实现对CPU的控制。
CPU从何处执行指令是由CS、IP中的内容决定的,程序员可以通过改变CS、IP中的内容来控制CPU执行目标指令。
我们如何改变CS、IP的值呢?
- 115. 2.11 修改CS、IP的指令8086CPU必须提供相应的指令
先回想我们如何修改AX中的值?
mov指令不能用于设置CS、IP的值,
8086CPU没有提供这样的功能。
8086CPU为CS、IP提供了另外的指令来改变它们的值:转移指令
- 116. 如何修改AX中的值?mov 指令
如:mov ax,123
mov指令可以改变8086CPU大部分寄存器的值,被称为传送指令。
能够通过mov 指令改变CS、IP的值吗?
- 117. 2.11 修改CS、IP的指令同时修改CS、IP的内容:
jmp 段地址:偏移地址
jmp 2AE3:3
jmp 3:0B16
功能:用指令中给出的段地址修改CS,偏移地址修改IP。
- 118. 2.11 修改CS、IP的指令仅修改IP的内容:
jmp 某一合法寄存器
jmp ax (类似于 mov IP,ax)
jmp bx
功能:用寄存器中的值修改IP。
- 119. 内存中存放的机器码和对应汇编指令情况: (初始:CS=2000H,IP=0000H)
请写出指令执行序列:问题分析
- 120. 问题分析结果:(1)mov ax,6622
(2)jmp 1000:3
(3)mov ax,0000
(4)mov bx,ax
(5)jmp bx
(6)mov ax,0123H
(7)转到第(3)步执行
- 121. 2.12 代码段对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。
可以将长度为 N( N≤64KB )的一组代码,存在一组地址连续、起始地址为 16的倍数的内存单元中,这段内存是用来存放代码的,从而定义了一个代码段。
例如
- 122. 2.12 代码段这段长度为 10 字节的字节的指令,存在从123B0H~123B9H的一组内存单元中,我们就可以认为,123B0H~123B9H这段内存单元是用来存放代码的 ,是一个代码段 ,它的段地址为123BH,长度为10字节。
- 123. 2.12 代码段如何使得代码段中的指令被执行呢?
将一段内存当作代码段,仅仅是我们在编程时的一种安排,CPU 并不会由于这种安排,就自动地将我们定义得代码段中的指令当作指令来执行。
CPU 只认被 CS:IP 指向的内存单元中的内容为指令。
所以要将CS:IP指向所定义的代码段中的第一条指令的首地址。
CS = 123BH,IP = 0000H。
- 124. 2.9节~2.12节 小结1、段地址在8086CPU的寄存器中存放。当8086CPU要访问内存时,由段寄存器提供内存单元的段地址。8086CPU有4个段寄存器,其中CS用来存放指令的段地址。
2、CS存放指令的段地址,IP存放指令的偏移地址。
8086机中,任意时刻,CPU将CS:IP指向的内容当作指令执行。
- 125. 2.9节~2.12节 小结(续)3、8086CPU的工作过程:
(1)从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲器;
(2)IP指向下一条指令;
(3)执行指令。(转到步骤(1),重复这个过程。)
4、8086CPU提供转移指令修改CS、IP的内容。
- 126. 特别提示
检测点2.3(Page 33)
没有通过检测点请不要向下学习!
- 127. 小结
- 128. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 129. 第3 章寄存器(内存访问)3.1 内存中字的存储
3.2 DS和[address]
3.3 字的传送
3.4 mov、add、sub指令
3.5 数据段
3.6 栈
3.7 CPU提供的栈机制
3.8 栈顶超界的问题
3.9 push、pop指令
3.10 栈段
- 130. 引言在第2章中,我们主要从 CPU 如何执行指令的角度讲解了8086CPU的逻辑结构、形成物理地址的方法、相关的寄存器以及一些指令。
这一章中,我们从访问内存的角度继续学习几个寄存器。
- 131. 在0地址处开始存放20000:
0号单元是低地址单元,1号单元是高地址单元。3.1 内存中字的存储
- 132. 3.1 内存中字的存储问题:
(1)0地址单元中存放的字节型数据是多少?
(2)0地址字单元中存放的字型数据是多少?
(3)2地址字单元中存放的字节型数据是多少?
- 133. 3.1 内存中字的存储问题(续):
(4)2地址单元中存放的字型数据是多少?
(5)1地址字单元中存放的字型数据是多少?
结论
- 134. 3.1 内存中字的存储结论:
任何两个地址连续的内存单元,N号单元和 N+1号单元,可以将它们看成两个内存单元 ,也可以看成一个地址为N的字单元中的高位字节单元和低位字节单元。
- 135. 3.2 DS和[address]CPU要读取一个内存单元的时候,必须先给出这个内存单元的地址;
在8086PC中,内存地址由段地址和偏移地址组成。
8086CPU中有一个 DS寄存器,通常用来存放要访问的数据的段地址。
例如
- 136. 3.2 DS和[address]例如:我们要读取10000H单元的内容可以用如下程序段进行:
mov bx,1000H
mov ds,bx
mov al,[0]
上面三条指令将10000H(1000:0)中的数据读到al中。
- 137. 3.2 DS和[address] mov al,[0]
已知的mov指令可完成的两种传送功能:
(1)将数据直接送入寄存器;
(2)将一个寄存器中的内容送入另一个寄存器中。
mov 指令 还可以将一个内存单元中的内容送入一个寄存器。
- 138. 3.2 DS和[address]从哪个内存单元送到哪个寄存器中呢?
mov指令的格式:
mov 寄存器名,内存单元地址
“[…]”表示一个内存单元, “[…]”中的0表示内存单元的偏移地址。
那么内存单元的段地址是多少呢?
- 139. 3.2 DS和[address]执行指令时,8086CPU自动取DS中的数据为内存单元的段地址。
如何用mov指令从10000H中读取数据?
10000H表示为1000:0(段地址:偏移地址)
将段地址1000H放入ds
用mov al,[0]完成传送(mov指令中的[]说明操作对象是一个内存单元,[]中的0说明这个内存单元的偏移地址是0,它的段地址默认放在ds中)
如何把1000H送入ds?
- 140. 3.2 DS和[address]如何把1000H送入ds?
传送指令 mov ax,1
相似的方式 mov ds,1000H?
8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器。
(硬件设计的问题)
mov ds,1000H 是非法的。
数据一般的寄存器段寄存器
- 141. 3.2 DS和[address]问题:
写几条指令,将al中的数据送入内存单元10000H?(思考后分析)
分析问题本质:
怎样将数据从寄存器送入内存单元?
结论:mov bx,1000H
mov ds,bx
mov [0],al (一种合理的回答)
- 142. 3.3 字的传送因为8086CPU是16位结构,有16根数据线,所以,可以一次性传送16位的数据,也就是一次性传送一个字。
- 143. 问题3.3:内存中的情况如右图,写出下面指令执行后寄存器ax,bx,cx中的值。
思考后看分析。(单步跟踪)3.3 字的传送
- 144. 问题3.3分析
- 145. 问题3.4:内存中的情况如右图,写出下面指令执行后寄存器ax,bx,cx中的值。
思考后看分析。(单步跟踪)3.3 字的传送
- 146. 问题3.4分析
- 147. 3.4 mov、add、sub指令已学mov指令的几种形式:
mov 寄存器,数据
mov 寄存器,寄存器
mov 寄存器,内存单元
mov 内存单元,寄存器
mov 段寄存器,寄存器
根据已知指令进行推测
- 148. 3.4 mov、add、sub指令根据已知指令进行推测:
mov 段寄存器,寄存器
mov 寄存器,段寄存器(验证)
mov 内存单元,寄存器
mov 内存单元,段寄存器
mov 段寄存器,内存单元
- 149. 验证(Debug)mov 段寄存器,寄存器
mov 寄存器,段寄存器
- 150. add和sub指令同mov一样,都有两个操作对象。
它们可以对段寄存器进行操作吗?
(请自行在Debug中试验)3.4 mov、add、sub指令
- 151. 3.5 数据段前面讲过,对于8086PC机,我们可以根据需要将一组内存单元定义为一个段。
我们可以将一组长度为N(N≤64K)、地址连续、起始地址为16的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。
比如我们用123B0H~123B9H这段空间来存放数据:
段地址:123BH
长度:10字节
- 152. 3.5 数据段如何访问数据段中的数据呢?
将一段内存当作数据段,是我们在编程时的一种安排,我们可以在具体操作的时候 ,用 ds 存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
示例
- 153. 3.5 数据段我们将123B0H~123BAH的内存单元定义为数据段,我们现在要累加这个数据段中的前3个单元中的数据,代码如下:
- 154. 3.5 数据段问题3.5
写几条指令,累加数据段中的前3个字型数据。
思考后看分析。
- 155. 问题3.5分析注意:一个字型数据占两个单元,所以偏移地址是0、2、4。
- 156. 3.1节~3.5节 小结(1)字在内存中存储时 ,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放再高地址单元中。
(2)用 mov 指令要访问内存单元,可以在mov指令中只给出单元的偏移地址,此时,段地址默认在DS寄存器中。
(3)[address]表示一个偏移地址为address的内存单元。
- 157. 3.1节~3.5节 小结(续)(4)在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。
(5)mov、add、sub是具有两个操作对象的指令。jmp是具有一个操作对象的指令。
(6)可以根据自己的推测,在Debug中实验指令的新格式。
特别提示
- 158. 特别提示
检测点3.1 (p52)
没有通过检测点请不要向下学习!
- 159. 3.6 栈我们研究栈的角度:
栈是一种具有特殊的访问方式的存储空间。它的特殊性就在于,最后进入这个空间的数据,最先出去。
可以用一个盒子和3本书来描述
栈的操作方式
- 160. (本页无文本内容)
- 161. (本页无文本内容)
- 162. 3.6 栈栈有两个基本的操作:入栈和出栈。
入栈:将一个新的元素放到栈顶;
出栈:从栈顶取出一个元素。
栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。
栈的操作规则:LIFO
(Last In First Out,后进先出)
- 163. 3.7 CPU提供的栈机制现今的CPU中都有栈的设计。
8086CPU提供相关的指令来以栈的方式访问内存空间。
这意味着,我们在基于8086CPU编程的时候,可以将一段内存当作栈来使用。
- 164. 3.7 CPU提供的栈机制8086CPU提供入栈和出栈指令: (最基本的)
PUSH(入栈)
POP (出栈)
push ax:将寄存器ax中的数据送入栈中;
pop ax :从栈顶取出数据送入ax。
8086CPU的入栈和出栈操作都是以字为单位进行的。
- 165. 3.6 栈下面举例说明,我们可以将10000H~1000FH这段内存当作栈来使用。
下面一段指令的执行过程:
mov ax,0123H
push ax
mov bx,2266H
push bx
mov cx,1122H
push cx
pop ax
pop bx
pop cx
- 166. 3.6 栈指令序列的执行过程演示
注意:字型数据用两个单元存放,高地址单元放高 8 位,低地址单元放低8 位。
是否有疑惑?
两个疑问
- 167. (本页无文本内容)
- 168. 两个疑问1、CPU如何知道一段内存空间被当作栈使用?
2、执行push和pop的时候,如何知道哪个单元是栈顶单元?
分析
结论:任意时刻,SS:SP指向栈顶元素。
- 169. 对于两个疑问的分析回想:CPU如何指导当前要执行的指令所在的位置?
寄存器CS和IP中存放着当前指令的段地址和偏移地址。
8086CPU中,有两个寄存器:
段寄存器SS 存放栈顶的段地址
寄存器SP 存放栈顶的偏移地址
任意时刻,SS:SP指向栈顶元素。
- 170. push 指令的执行过程push ax
(1)SP=SP–2;
(2)将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶。
图示
- 171. push 指令的执行过程
- 172. 3.6 栈问题3.6:如果我们将10000H~1000FH 这段空间当作栈,初始状态栈是空的,此时,SS=1000H,SP=?
思考后看分析。
- 173. 问题3.6分析SP = 0010H
- 174. 问题3.6分析(续)我们将10000H~1000FH 这段空间当作栈段,SS=1000H,栈空间大小为16 字节 ,栈最底部的字单元地址为1000:000E。
任意时刻,SS:SP指向栈顶,当栈中只有一个元素的时候,SS = 1000H,SP=000EH。
- 175. 问题3.6分析(续)栈为空,就相当于栈中唯一的元素出栈,出栈后,SP=SP+2 ,SP 原来为 000EH,加 2 后SP=10H,所以,当栈为空的时候,SS=1000H,SP=10H。
换个角度看
- 176. 问题3.6分析(续)换个角度看:
任意时刻,SS:SP 指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素,所以SS:SP 只能指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址+2,栈最底部字单元的地址为1000:000E,所以栈空时,SP=0010H。
- 177. pop 指令的执行过程pop ax
(1)将SS:SP指向的内存单元处的数据送入ax中;
(2)SP = SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
图示
- 178. pop 指令的执行过程注意
- 179. pop 指令的执行过程注意:
出栈后,SS:SP指向新的栈顶 1000EH,pop操作前的栈顶元素,1000CH 处的2266H 依然存在 ,但是,它已不在栈中。
当再次执行push等入栈指令后,SS:SP移至1000CH,并在里面写入新的数据,它将被覆盖。
- 180. 3.8 栈顶超界的问题SS和SP只记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈时找到栈顶。
可是,如何能够保证在入栈、出栈时,栈顶不会超出栈空间?
- 181. 3.8 栈顶超界的问题当栈满的时候再使用push指令入栈,
栈空的时候再使用pop指令出栈,
都将发生栈顶超界问题。
栈顶超界是危险的。
- 182. (本页无文本内容)
- 183. (本页无文本内容)
- 184. 3.8 栈顶超界的问题栈顶超界是危险的:
因为我们既然将一段空间安排为栈 ,那么在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己的程序中的,也可能是别的程序中的。
(毕竟一个计算机系统并不是只有我们自己的程序在运行)
- 185. 3.8 栈顶超界的问题但是由于我们在入栈出栈时的不小心,而将这些数据、代码意外地改写,将会引发一连串的错误。
我们当然希望CPU 可以帮我们解决这个问题,
- 186. 3.8 栈顶超界的问题比如说在CPU中有记录栈顶上限和下限的寄存器,我们可以通过填写这些寄存器来指定栈空间的范围 ,然后 ,CPU 在执行push指令的时候靠检测栈顶上限寄存器,在执行pop 指令的时候靠检测栈顶下限寄存器保证不会超界。
实际情况:8086CPU中并没有这样的寄存器。
- 187. 3.8 栈顶超界的问题8086CPU不保证对栈的操作不会超界。
这就是说, 8086CPU 只知道栈顶在何处(由SS:SP指示),而不知道读者安排的栈空间有多大。这点就好像 ,CPU 只知道当前要执行的指令在何处(由CS:SP指示)而不知道读者要执行的指令有多少。
从这两点我们可以看出
- 188. 3.8 栈顶超界的问题8086CPU的工作机理,只考虑当前的情况:
当前栈顶在何处;
当前要执行的指令是哪一条。
结论
- 189. 3.8 栈顶超界的问题结论:
我们在编程的时候要自己操心栈顶超界的问题 ,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
- 190. 3.9 push、pop指令 push和pop指令是可以在寄存器和内存之间传送数据的。
push和pop指令的格式
- 191. 栈与内存栈空间当然也是内存空间的一部分,它只是一段可以以一种特殊的方式进行访问的内存空间。
- 192. 3.9 push、pop指令push和pop指令的格式(1)
push 寄存器:将一个寄存器中的数据入栈
pop寄存器:出栈,用一个寄存器接收出栈的数据
例如:push ax
pop bx
- 193. 3.9 push、pop指令push和pop指令的格式(2)
push 段寄存器:将一个段寄存器中的数据入栈
pop段寄存器:出栈,用一个段寄存器接收出栈的数据
例如:push ds
pop es
- 194. 3.9 push、pop指令push和pop指令的格式(3)
push内存单元:将一个内存单元处的字入栈(栈操作都是以字为单位)
pop 内存单元:出栈,用一个内存字单元接收出栈的数据
例如:push [0]
pop [2]
指令执行时 ,CPU 要知道内存单元的地址,可以在 push、pop 指令中给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得。
- 195. 3.9 push、pop指令问题3.7
编程:
将10000H~1000FH 这段空间当作栈,初始状态是空的,将 AX、BX、DS中的数据入栈。
思考后看分析。
- 196. 问题3.7分析
- 197. 3.9 push、pop指令问题3.8
编程:
(1)将10000H~1000FH 这段空间当作栈,初始状态是空的;
(2)设置AX=001AH,BX=001BH;
(3)将AX、BX中的数据入栈;
(4)然后将AX、BX清零;
(5)从栈中恢复AX、BX原来的内容。
思考后看分析。
- 198. 问题3.8分析结论
- 199. 问题3.8分析从上面的程序我们看到,用栈来暂存以后需要恢复的寄存器中的内容时 ,出栈的顺序要和入栈的顺序相反,因为最后入栈的寄存器的内容在栈顶 ,所以在恢复时,要最先出栈。
- 200. 3.9 push、pop指令问题3.9
编程:
(1)将10000H~1000FH 这段空间当作栈,初始状态是空的;
(2)设置AX=002AH,BX=002BH;
(3)利用栈 ,交换 AX 和 BX 中的数据。
思考后看分析。
- 201. 问题3.9分析
- 202. 3.9 push、pop指令问题3.10
我们如果要在10000H处写入字型数据2266H,可以用以下的代码完成:
mov ax,1000H
mov ds,ax
mov ,ax,2266H
mov [0],ax
补全下面的代码
- 203. 3.9 push、pop指令补全下面的代码,完成同样的功能:
在10000H处写入字型数据2266H。
__________
__________
__________
mov ax,2266H
push ax
要求:不能使用“mov 内存单元,寄存器”这类指令
思考后看分析。
- 204. 问题3.10分析我们看需补全代码的最后两条指令,将ax中的2266H压入栈中,也就是说,最终应由push ax将2266H写入10000H处。
问题的关键就在于:如何使push ax访问的内存单元是10000H。
Push指令是入栈指令。(注意执行过程)
完成程序
- 205. 问题3.10分析(续)完成的程序:
mov ax,1000H
mov ss,ax
mov sp,2
mov ax,2266H
push ax
结论
- 206. 问题3.10分析(续)结论
push、pop 实质上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与mov指令不同的是,push和pop指令访问的内存单元的地址不是在指令中给出的,而是由SS:SP指出的。
同时,push和pop指令还要改变 SP 中的内容。
- 207. 问题3.10分析(续)我们要十分清楚的是,push和pop指令同mov指令不同,CPU执行mov指令只需一步操作,就是传送,而执行push、pop指令却需要两步操作。
执行push时:
先改变SP,后向SS:SP处传送。
执行pop时:
先读取SS:SP处的数据,后改变SP。
- 208. 注意push、pop 等栈操作指令,修改的只是SP。也就是说,栈顶的变化范围最大为:0~FFFFH。
提供:SS、SP指示栈顶;改变SP后写内存的入栈指令;读内存后改变SP的出栈指令。
这就是8086CPU提供的栈操作机制。
- 209. 栈的综述(1)8086CPU提供了栈操作机制,方案如下:
在SS,SP中存放栈顶的段地址和偏移地址;
提供入栈和出栈指令,他们根据SS:SP指示的地址,按照栈的方式访问内存单元。
(2)push指令的执行步骤:
1)SP=SP-2;
2)向SS:SP指向的字单元中送入数据。
(3)pop指令的执行步骤:
1)从SS:SP指向的字单元中读取数据;
2)SP=SP-2。
- 210. 栈的综述(续)(4)任意时刻,SS:SP指向栈顶元素。
(5)8086CPU只记录栈顶,栈空间的大小我们要自己管理。
(6)用栈来暂存以后需要恢复的寄存器的内容时 ,寄存器出栈的顺序要和 入栈的顺序相反。
(7)push、pop实质上是一种内存传送指令,注意它们的灵活应用。
栈是一种非常重要的机制,一定要深入理解,灵活掌握。
- 211. 3.10 栈段前面讲过,对于8086PC机,在编程时,我们可以根据需要 ,将一组内存单元定义为一个段。
我们可以将长度为 N(N ≤64K )的一组地址连续、起始地址为16的倍数的内存单元,当作栈来用,从而定义了一个栈段。
- 212. 3.10 栈段比如我们将10010H~1001FH 这段长度为 16 字节的内存空间当作栈来用,以栈的方式进行访问。
这段空间就可以成为栈段,段地址为1000H,大小为16字节。
- 213. 3.10 栈段将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU 并不会由于这种安排,就在执行push、pop 等栈操作指令时就自动地将我们定义的栈段当作栈空间来访问。
如何使的如push、pop 等栈操作指令访问我们定义的栈段呢?
将SS:SP指向我们定义的栈段。
- 214. 3.10 栈段问题3.11
如果我们将10000H~1FFFFH这段空间当作栈段,初始状态是空的,此时,SS=1000H,SP=?
思考后看分析。
- 215. 问题3.11分析我们将10000H~1FFFFH这段空间当作栈段 ,SS=1000H ,栈空间大小为64KB ,栈最底部的字单元地址为1000:FFFE。
任意时刻,SS:SP指向栈顶,当栈中只有一个元素的时候,SS=1000H,SP=FFFEH。
- 216. 问题3.11分析栈为空,就相当于栈中唯一的元素出栈,出栈后,SP=SP+2。
SP原来为FFFEH,加2后SP=0,所以,当栈为空的时候,SS=1000H,SP=0。
换个角度看
- 217. 问题3.11分析(续)换个角度看
任意时刻,SS:SP指向栈顶元素,当栈为空的时候 ,栈中没有元素 ,也就不存在栈顶元素,所以SS:SP只能指向栈的最底部单元下面的单元 ,该单元的偏移地址为栈最底部的字单元的偏移地址+2 ,栈最底部字单元的地址为1000:FFFE,所以栈空时,SP=0000H。
- 218. 问题3.12一个栈段最大可以设为多少?为什么?
思考后看分析。
- 219. 问题3.12分析一个栈段最大可以设为多少?
分析:这个问题显而易见,提出来只是为了提示我们将相关的知识融会起来。
首先从栈操作指令所完成的功能的角度上来看,push、pop等指令在执行的时候只修改SP;
- 220. 问题3.12分析所以栈顶的变化范围是0~FFFFH,从栈空时候的SP=0,一直压栈,直到栈满时SP=0;如果再次压栈,栈顶将环绕,覆盖了原来栈中的内容。
所以一个栈段的容量最大为64KB。
- 221. 段的综述我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。这完全是我们自己的安排。
我们可以用一个段存放数据,将它定义为“数据段”;
我们可以用一个段存放代码,将它定义为“代码段”;
我们可以用一个段当作栈,将它定义为“栈段”;
- 222. 段的综述(续)我们可以这样安排,但若要让CPU按照我们的安排来访问这些段,就要:
对于数据段,将它的段地址放在 DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当作数据段来访问;
- 223. 段的综述(续)对于代码段,将它的段地址放在 CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令;
- 224. 段的综述(续)对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地置放在 SP 中,这样CPU在需要进行栈操作的时候,比如执行 push、pop 指令等,就将我们定义的栈段当作栈空间来用。
- 225. 段的综述(续)可见,不管我们如何安排 ,CPU 将内存中的某段内存当作代码 ,是因为CS:IP指向了那里;CPU将某段内存当作栈 ,是因为 SS:IP 指向了那里。
- 226. 段的综述(续)我们一定要清楚 ,什么是我们的安排,以及如何让CPU按我们的安排行事。
要非常的清楚CPU的工作机理,才能在控制CPU来按照我们的安排运行的时候做到游刃有余。
- 227. 段的综述(续)比如我们将10000H~1001FH安排为代码段,并在里面存储如下代码:
mov ax,1000H
mov ss,ax
mov sp,0020H ;初始化栈顶
mov ax,cs
mov ds,ax ;设置数据段段地址
mov ax,[0]
add ax,[2]
mov bx,[4]
add bx,[6]
push ax
push bx
pop ax
pop bx
- 228. 段的综述(续)设置CS=1000H,IP=0,这段代码将得到执行。
可以看到,在这段代码中,我们又将10000H~1001FH 安排为栈段和数据段。
10000H~1001FH这段内存,既是代码段,又是栈段和数据段。
- 229. 段的综述(续)一段内存,可以既是代码的存储空间,又是数据的存储空间,还可以是栈空间,也可以什么也不是。
关键在于CPU中寄存器的设置,即:
CS、IP、SS、SP、DS的指向。
- 230. 特别提示
检测点3.2 (p66)
没有通过检测点请不要向下学习!
- 231. 小结
- 232. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 233. 第4章 第1个程序4.1 一个源程序从写出到执行的过程
4.2 源程序
4.3 编辑源程序
4.4 编译
4.5 连接
4.6 以简化的方式进行编译和连接
4.7 1.exe的执行
4.8 可执行文件中的程序装入内存并运行的原理
4.9 程序执行过程的跟踪
- 234. 引言现在我们将开始编写完整的汇编语言程序,用编译器将它们编译成为可执行文件(如:*.exe文件),在操作系统中运行。
这一章,我们将编写第一个这样的程序。
- 235. 4.1 一个源程序从写出到执行的过程一个汇编语言程序从写出到最终执行的简要过程:
编写--〉编译--〉连接--〉执行
演示
- 236. 编写汇编源程序
使用文本编辑器(如Edit、记事本等),用汇编语言编写汇编源程序。
- 237. 对源程序进行编译连接使用汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件;再用连接程序对目标文件进行连接,生成可在操作系统中直接运行的可执行文件。
- 238. 可执行文件可执行文件中包含两部分内容:
程序(从原程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
相关的描述信息(比如:程序有多大、要占多少内存空间等)
- 239. 执行可执行文件中的程序在操作系统中,执行可执行文件中的程序。
操作系统依照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化(比如:设置CS:IP指向第一条要执行的指令),然后由CPU执行程序。
- 240. (本页无文本内容)
- 241. 4.2 源程序汇编指令
伪指令
XXX segment
XXX ends
end
assume
- 242. 4.2 源程序汇编指令
有对应的机器码的指令,可以被编译为机器指令,最终为CPU所执行。
- 243. 4.2 源程序伪指令
没有对应的机器码的指令,最终不被CPU所执行。
谁来执行伪指令呢?
伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作。
- 244. 定义一个段 segment和ends是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令。
segment和ends的功能是定义一个段,segment说明一个段开始,ends 说明一个段结束。
一个段必须有一个名称来标识,使用格式为:
段名 segment
段名 ends
- 245. 定义一个段一个汇编程序是由多个段组成的,这些段被用来存放代码、数据或当作栈空间来使用。
一个有意义的汇编程序中至少要有一个段,这个段用来存放代码。
- 246. 程序结束标记End 是一个汇编程序的结束标记,编译器在编译汇编程序的过程中,如果碰到了伪指令 end,就结束对源程序的编译。
如果程序写完了,要在结尾处加上伪指令end 。否则,编译器在编译程序时,无法知道程序在何处结束。
注意:不要搞混了end和ends。
- 247. 寄存器与段的关联假设assume:含义为“假设”。
它假设某一段寄存器和程序中的某一个用 segment … ends 定义的段相关联。
通过assume说明这种关联,在需要的情况下 ,编译程序可以将段寄存器和某一个具体的段相联系。
- 248. 4.2 源程序源程序中的“程序”
汇编源程序:
伪指令 (编译器处理)
汇编指令(编译为机器码)
程序:源程序中最终由计算机执行、处理的指令或数据。
注意
- 249. 注意我们可以将源程序文件中的所有内容称为源程序,将源程序中最终由计算机执行处理的指令或数据 ,成为程序。
程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机器码,存储在可执行文件中,
图示
- 250. 程序经编译连接后变为机器码
- 251. 4.2 源程序标号
一个标号指代了一个地址。
codesg:放在segment的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。
- 252. 4.2 源程序程序的结构
任务:编程运算 2∧3。
定义一个段
实现处理任务
程序结束
段与段寄存器关联
汇编程序
assume cs:abc
abc segment
mov ax,2
add ax,ax
add ax,ax
abc ends
end
- 253. 4.2 源程序程序返回
我们的程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机器码,存储在可执行文件中,那么,它怎样得到运行呢?(讨论)
现在,我们知道,一个程序结束后,将CPU的控制权交还给使它得以运行的程序,我们称这个过程为:程序返回。
如何返回呢?
- 254. DOS中的程序运行DOS是一个单任务操作系统。
一个程序P2在可执行文件中,则必须有一个正在运行的程序P1,将P2从可执行文件中加载入内存后,将CPU的控制权交给P2,P2才能得以运行。P2开始运行后,P1暂停运行。
而当P2运行完毕后,应该将CPU的控制权交还给使它得以运行的程序P1,此后,P1继续运行。
- 255. 4.2 源程序程序返回
应该在程序的末尾添加返回的程序段。
mov ax,4c00H
int 21H
这两条指令所实现的功能就是程序返回。
几个和结束相关的内容
- 256. 段结束、程序结束、程序返回
- 257. 语法错误和逻辑错误 语法错误
程序在编译时被编译器发现的错误;
容易发现。
aume cs:abc
abc segment
mov ax,2
add ax,ax
add ax,ax
end
- 258. 语法错误和逻辑错误 逻辑错误
程序在编译时不能表现出来的、在运行时发生的错误;
不容易发现。 assume cs:abc
abc segment
mov ax,2
add ax,ax
add ax,ax
mov ax,4c00H
int 21H
abc ends
end
- 259. 4.4 编辑源程序进入DOS方式,运行Edit,在其中编辑程序,如下图所示:
- 260. 4.4 编译进入DOS方式,进入 C:\masm 目录,运行masm.exe。
如果源程序文件不是以 asm 为扩展名的话,就要输入它的全名。比如p1.txt。
在输入源程序文件名的时候一定要指明它所在的路径。如果文件就在当前路径下,只输入文件名就可以。
- 261. 4.4 编译输入要编译的源文件文件名后,按 Enter键。
目标文件(*.obj)是我们对一个源程序进行编译要得到的最终结果。
编译程序默认要输出的目标文件名为1.obj,所以可以不必再另行指定文件名。
- 262. 4.4 编译列表文件是编译器将源程序编译为目标文件的过程中产生的中间结果。
可以不生成这个文件,直接按 Enter键即可。
- 263. 4.4 编译编译程序提示输入交叉引用文件的名称。
这个文件同列表文件一样,是编译器将源程序编译为目标文件过程中产生的中间结果。
可以不生成这个文件,直接按 Enter 键即可。
- 264. 4.4 编译对源程序的编译结束,编译器输出的最后两行告诉我们这个源程序没有警告错误和必须要改正的错误。
- 265. 4.4 编译一般来说,有两类错误使我们得不到所期望的目标文件:
(1)我们程序中有“Severe Errors”;
(2)找不到所给出的源程序文件。
- 266. 4.5 连接在对源程序进行编译得到目标文件后,我们需要对目标文件进行连接,从而得到可执行文件。
继续上一节的过程,我们再将C:\masm\1.obj连接为C:\masm\1.exe。
- 267. 4.5 连接进入DOS方式,进入C:\masm目录,运行link.exe。
如果目标文件不是以obj为扩展名的话,就要输入它的全名。比如:p1.bin。
在输入目标文件名的时候,要注意指明它所在的路径。这里,我们要连接的文件是当前路径下1.obj,所以此处输入“1”。
- 268. 4.5 连接输入要连接的目标文件名后,按Enter键。
可执行文件是我们对一个程序进行连接要得到的最终结果。
连接程序默认要输出的可执行文件名为 1.EXE ,所以可以不必再另行指定文件名。
我们直接按 Enter 键,使用连接程序设定的可执行文件名。
- 269. 4.5 连接映像文件是连接程序将目标文件连接为可执行文件过程中产生的中间结果。
可以不生成这个文件,直接按 Enter 键即可。
- 270. 4.5 连接连接程序提示输入库文件的名称。
库文件里包含了一些可以调用的子程序,如果我们的程序中调用了某一个库文件中的子程序,就需要在连接的时候,将这个库文件和我们的目标文件连接到一起,生成可执行文件。
如果没有调用任何子程序,直接按Enter键即可。
- 271. 4.5 连接对目标文件的连接结束,连接程序输出的最后一行告诉我们,这个程序有一个警告错误:“没有栈段” ,这里我们不理会这个错误。
- 272. 4.5 连接前面我们通过对 1.obj 进行连接的过程,展示了使用连接器对目标文件进行连接的方法。
我们得到了一个新的文件 1.exe 。当然,如果连接过程中出现错误,那么我们将得不到这个可执行文件。
连接的作用是什么呢?
- 273. 4.5 连接这里再次强调一下,我们学习汇编的主要目的,就是通过用汇编语言进行编程而深入地理解计算机底层的基本工作机理,达到可以随心所欲地控制计算机的目的。
基于这种考虑,我们的编程活动,大都是直接对硬件进行的。我们希望直接对硬件编程,却并不希望用机器码编程。
- 274. 4.5 连接我们用汇编语言编程,就要用到 :编辑器(Edit)、编译器(masm)、连接器(link)、调试工具(debug)等所有工具,而这些工具都是在操作系统之上运行的程序,所以我们的学习过程必须在操作系统的环境中进行。
- 275. 4.5 连接我们在一个操作系统环境中,使用了许多工具,这势必要牵扯到操作系统、编译原理等方面的知识和原理。
我们只是利用这些环境、工具来方便我们的学习,而不希望这些东西分散了我们的注意力。
- 276. 4.5 连接所以,对于涉及到而又不在我们学习的主要内容之中的东西,我们只做简单的解释。
- 277. 4.5 连接连接的作用有以下几个:
当源程序很大时,可以将它分为多个源程序文件来编译,每个源程序编译成为目标文件后,再用连接程序将它们连接到一起,生成一个可执行文件;
程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件;
- 278. 4.5 连接连接的作用有以下几个(续):
一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接用来生成可执行文件,连接程序将这此内容处理为最终的可执行信息。
所以,在只有一个源程序文件,而又不需要调用某个库中的子程序的情况下,也必须用连接程序对目标文件进行处理,生成可执行文件。
注意,对于连接的过程,可执行文件是我们要得到的最终结果。
- 279. 4.6 以简化的方式进行编译和连接我们编译、连接的最终目的是用源程序文件生成可执行文件。
在这个过程中所产生的中间文件都可以忽略。我们可以用一种较为简捷的方式进行编译、连接。
- 280. 4.6 以简化的方式进行编译和连接编译:
- 281. 4.6 以简化的方式进行编译和连接连接:
- 282. 4.7 1.exe的执行现在,终于将我们的第一个汇编程序加工成了一个可在操作系统下执行的程序文件。1.exe的执行情况:
程序到底运行没有?
程序当然运行了,只是从屏幕上不可能看到任何运行结果。
- 283. 4.7 1.exe的执行我们的程序没有像显示器输出任何信息。程序只是做了一些将数据送入寄存器和加法的操作,而这些事情,我们不可能从显示屏上看出来。
程序执行完成后,返回,屏幕上再次出现操作系统的提示符。
- 284. 4.8 可执行文件中的程序装入内存并运行的原理在DOS中,可执行文件中的程序P1若要运行,必须有一个正在运行的程序P2 ,将 P1 从可执行文件中加载入内存,将CPU的控制权交给它,P1才能得以运行;
当P1运行完毕后,应该将CPU的控制权交还给使它得以运行的程序P2。
1.exe的执行过程
- 285. 4.8 可执行文件中的程序装入内存并运行的原理1.exe的执行过程:
(1)我们在提示符“C:\masm”后面输入可执行文件的名字“1”,按Enter键。
问题4.1
(2)1.exe中的程序运行;
(3)运行结束,返回,再次显示提示符“C:\masm”。
问题4.2
- 286. 问题4.1执行第(1)步操作时,有一个正在运行的程序将1.exe中的程序加载入内存,这个正在运行的程序是什么?它将程序加载入内存后,如何使程序得以运行?
- 287. 问题4.2执行第(3)步操作,程序运行结束后,返回到哪里?
参考内容
回答问题4.1和4.2
- 288. 操作系统的外壳操作系统是由多个功能模块组成的庞大 、复杂的软件系统。任何通用的操作系统 ,都要提供一个称为shell(外壳)的程序 ,用户(操作人员)使用这个程序来操作计算机系统工作。
DOS中有一个程序command.com ,这个程序在 DOS 中称为命令解释器,也就是DOS系统的shell。
- 289. 回答问题4.1和4.2(1)我们在DOS中直接执行 1.exe 时,是正在运行的command将1.exe中的程序加载入内存。
(2)command设置CPU的CS:IP指向程序的第一条指令(即程序的入口),从而使程序得以运行。
(3)程序运行结束后,返回到command中,CPU继续运行command。
- 290. 4.8 可执行文件中的程序装入内存并运行的原理汇编程序从写出到执行的过程:
- 291. 4.9 程序执行过程的跟踪为了观察程序的运行过程 ,我们可以使用Debug。
Debug 可以将程序加载入内存,设置CS:IP指向程序的入口,但Debug并不放弃对CPU 的控制,这样,我们就可以使用Debug 的相关命令来单步执行程序 ,查看每条指令指令的执行结果。
- 292. 4.9 程序执行过程的跟踪接下来可以用R命令看一下各个寄存器的设置情况:
可以看到,Debug将程序从可执行文件加载入内存后,cx中存放的是程序的长度。1.exe 中程序的机器码共有15个字节。
- 293. 4.9 程序执行过程的跟踪现在程序已从 1.exe 中装入内存,接下来我们查看一下它的内容,可是我们查看那里的内容呢?
程序被装入内存的什么地方?
我们如何得知?
在DOS系统中.EXE文件中的程序的加载过程如下
- 294. EXE文件中的程序的加载过程
- 295. EXE文件中的程序的加载过程注意:有一步称为重定位的工作我们在上面没有讲解,因为这个问题和操作系统的关系较大,我们不作讨论。
总结
- 296. EXE文件中的程序的加载过程总结
程序加载后,ds中存放着程序所在内存区的段地址,这个内存区的偏移地址为 0 ,则程序所在的内存区的地址为:ds:0;
这个内存区的前256 个字节中存放的是PSP,dos用来和程序进行通信。
从 256字节处向后的空间存放的是程序。
- 297. EXE文件中的程序的加载过程总结(续)
所以,我们从ds中可以得到PSP的段地址SA,PSP的偏移地址为 0,则物理地址为SA×16+0。
因为PSP占256(100H)字节,所以程序的物理地址是:
SA×16+0+256= SA×16+16×16=(SA+16)×16+0
可用段地址和偏移地址表示为:SA+10:0。
- 298. 4.9 程序执行过程的跟踪用U命令查看一下其他指令:
- 299. 4.9 程序执行过程的跟踪用T命令担不执行程序中的每一条指令,并观察每条指令的执行结果,到了 int 21,我们要用P命令执行:
- 300. 4.9 程序执行过程的跟踪int 21 执行后,显示“Program terminated normally”,返回到Debug中。
表示程序正常结束。
注意,要使用P命令执行int 21。
- 301. 4.9 程序执行过程的跟踪需要注意的是,在 DOS 中运行程序时,是command将程序加载入内存;
所以程序运行结束后返回到command中,而在这里是debug 将程序加载入内存,所以程序运行结束后要返回到Debug中。
- 302. 4.9 程序执行过程的跟踪使用Q命令退出Debug,将返回到command中,因为Debug是由command加载运行的。
- 303. 4.9 程序执行过程的跟踪我们在 DOS中用 “Debug 1.exe” 运行Debug对1.exe进行跟踪时,程序加载的顺序是:command加载Debug,Debug加载1.exe。
返回的顺序是:从1.exe中的程序返回到Debug,从Debug返回到command。
- 304. 小结
- 305. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 306. 第5章 [bx]和loop指令5.1 [bx]
5.2 Loop指令
5.3 在Debug中跟踪用loop指令实现的循环程序
5.4 Debug和汇编编译器Masm对指令的不同处理
5.5 loop和[bx]的联合应用
5.6 段前缀
5.7 一段安全的空间
5.8 段前缀的使用
- 307. [bx]和内存单元的描述
[bx]是什么呢?
和[0]有些类似,[0]表示内存单元,它的偏移地址是0。
- 308. [bx]和内存单元的描述我们要完整地描述一个内存单元,需要两种信息:
(1)内存单元的地址;
(2)内存单元的长度(类型)。
我们用[0]表示一个内存单元时,0 表示单元的偏移地址,段地址默认在ds中,单元的长度(类型)可以由具体指令中的其他操作对象(比如说寄存器)指出。
- 309. [bx]和内存单元的描述[bx]同样也表示一个内存单元,它的偏移地址在bx中,比如下面的指令:
mov ax,[bx]
mov al,[bx]
- 310. loop
英文单词“loop”有循环的含义,显然这个指令和循环有关。
- 311. 描述性符号“()”
为了描述上的简洁,在以后的课程中,我们将使用一个描述性的符号 “() ”来表示一个寄存器或一个内存单元中的内容。
- 312. 描述性符号“()”我们看一下(X)的应用,比如:
(1)ax中的内容为0010H,我们可以这样来描述:(ax)=0010H;
(2)2000:1000 处的内容为0010H,我们可以这样来描述:(21000H)=0010H;
(3)对于mov ax,[2]的功能,我们可以这样来描述:(ax)=((ds)*16+2);
(4)对于mov [2],ax 的功能,我们可以这样来描述:((ds)*16+2)=(ax);
- 313. 描述性符号“()”(5)对于 add ax,2 的功能,我们可以这样来描述:(ax)=(ax)+2;
(6)对于add ax,bx的功能,我们可以这样来描述:(ax)=(ax)+(bx);
(7)对于push ax的功能,我们可以这样来描述: (sp) = (sp)-2
((ss)*16+(sp))=(ax)
- 314. 描述性符号“()”(8)对于pop ax 的功能,我们可以这样来描述:
(ax)=((ss)*16+(sp)) (sp)=(sp)+2
- 315. 约定符号idata表示常量我们在Debug 中写过类似的指令:mov ax,[0],表示将 ds:0 处的数据送入ax中。指令中,在“[…]”里用一个常量0表示内存单元的偏移地址。以后,我们用idata表示常量。
比如
- 316. 约定符号idata表示常量比如:
mov ax,[idata]就代表mov ax,[1]、mov ax,[2]、mov ax,[3]等。
mov bx,idata就代表mov bx,l、mov bx,2、mov bx,3等。
mov ds,idata就代表mov ds,1、mov ds,2等,它们都是非法指令。
- 317. 5.1 [bx]我们看一看下面指令的功能:
mov ax,[bx]
功能:bx 中存放的数据作为一个偏移地址EA ,段地址SA 默认在ds 中,将SA:EA处的数据送入ax中。
即: (ax)=(ds *16 +(bx));
- 318. 5.1 [bx]mov [bx],ax
功能:bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将ax中的数据送入内存SA:EA处。
即:(ds *16 +(bx)) = (ax)。
- 319. 5.1 [bx]问题5.1
程序和内存中的情况如下图所示,写出程序执行后,21000H~21007H 单元中的内容。
思考后看分析。
- 320. 5.1 [bx]问题5.1分析
(1)先看一下程序的前三条指令:
mov ax,2000H
mov ds,ax
mov bx,1000H
这三条指令执行后
ds=2000H,
bx=1000H;
- 321. 5.1 [bx]问题5.1分析(续)
(2)再看第4条指令:
mov ax,[bx]
指令执行前:
ds=2000H,bx=1000H,则mov ax,[bx]将把内存2000:1000处的字型数据送入ax中。
该指令执行后:
ax=00beH。
- 322. 5.1 [bx]问题5.1分析(续)
(3)再看第5、6条指令:
inc bx
inc bx
指令执行前:
bx=1000H。
执行后:
bx=0002H。
- 323. 5.1 [bx]问题5.1分析(续)
(4)再看第7条指令:
mov [bx],ax
指令执行前:
ds=2000H,bx=1002H,则
mov [bx],ax将把ax中的数据
送入内存2000:1002处。
指令执行后:
2000:1002单元的内容为BE,2000:1003单元的内容为00。
- 324. 5.1 [bx]问题5.1分析(续)
(5)再看第8、9条指令:
inc bx
inc bx
指令执行前:
bx=1002H。
执行后:
bx=0004H。
- 325. 5.1 [bx]问题5.1分析(续)
(6)再看第10条指令:
mov [bx],ax
指令执行前:
ds=2000H,bx=1004H,则
mov [bx],ax将把ax中的数据
送入内存2000:1004处。
指令执行后:
2000:1004单元的内容为BE,2000:1005单元的内容为00。
- 326. 5.1 [bx]问题5.1分析(续)
(7)再看第11条指令:
inc bx
指令执行前:
bx=1004H。
执行后:
bx=0005H。
- 327. 5.1 [bx]问题5.1分析(续)
(8)再看第12条指令:
mov [bx],al
指令执行前:
ds=2000H,bx=1005H,则
mov [bx],ax将把al中的数据送
入内存2000:1005处。
指令执行后:
2000:1005单元的内容为BE。
- 328. 5.1 [bx]问题5.1分析(续)
(9)接下来是第13条指令:
inc bx
指令执行前:
bx=1005H,
指令执行后:
bx=0006H。
- 329. 5.1 [bx]问题5.1分析(续)
(10)再看第12条指令:
mov [bx],al
指令执行前:
ds=2000H,bx=1006H,则
mov [bx],ax将把al中的数据
送入内存2000:1006处。
指令执行后:
2000:1006单元的内容为BE。
- 330. 5.2 Loop指令指令的格式是:loop 标号,CPU 执行loop指令的时候,要进行两步操作:
① (cx)=(cx)-1;
② 判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。
- 331. 5.2 Loop指令从上面的描述中,我们可以看到,cx中的值影响着loop指令的执行结果。
通常(注意,我们说的是通常)我们用loop指令来实现循环功能,cx 中存放循环次数。
- 332. 5.2 Loop指令这里我们讲解loop指令的功能,关于loop指令如何实现转至标号处的细节,将在后面的课程中讲解。下面我们通过一个程序来看一下loop指令的具体应用:
任务1:编程计算2∧ 2,结果存放在ax中。
任务1分析
任务2:编程计算2∧ 3。
任务2分析
任务3:编程计算2∧ 12。
任务3分析
- 333. 5.2 Loop指令任务1:编程计算2∧2,结果存放在ax中。
分析:
设(ax)=2,可计算:(ax)= (ax)*2,最后(ax)中为2∧2的值。N*2可用N+N 实现。
程序代码
- 334. 5.2 Loop指令任务1:编程计算2∧2,结果存放在ax中。
程序代码:
assume cs:code
code segment
mov ax,2
add ax,ax
mov ax,4c00h
int 21h
code ends
end
- 335. 5.2 Loop指令任务2:编程计算2∧3。
分析:
2∧3=2*2*2,若设(ax)=2,可计算:(ax)= (ax)*2*2,最后(ax)中为2∧3的值。N*2可用N+N 实现。
程序代码
- 336. 5.2 Loop指令任务2:编程计算2∧3。
程序代码:
assume cs:code
code segment
mov ax,2
add ax,ax
add ax,ax
mov ax,4c00h
int 21h
code ends
end
- 337. 5.2 Loop指令任务3:编程计算2∧12。
分析:
2∧12=2*2*2*2*2*2*2*2*2*2*2*2,若设(ax)=2,可计算:
(ax)= (ax)*2*2*2*2*2*2*2*2*2*2*2,最后(ax)中为2∧12的值。N*2可用N+N 实现。
程序代码
- 338. 5.2 Loop指令任务3:编程计算2∧12。
程序代码:
assume cs:code
code segment
mov ax,2
;做11次add ax,ax
mov ax,4c00h
int 21h
code ends
end
按照我们的算法,计算2∧12需要11条重复的指令add ax, ax。我们显然不希望这样来写程序,这里,可用loop来简化我们的程序
- 339. 5.2 Loop指令任务3:编程计算2∧12。
程序代码:
assume cs:code
code segment
mov ax,2
mov cx,11
s: add ax,ax
loop s
mov ax,4c00h
int 21h
code ends
end
- 340. 5.2 Loop指令程序分析:
(1)标号在汇编语言中,标号代表一个地址,此程序中有一个标号s 。它实际上标识了一个地址,这个地址处有一条指令:add ax,ax。
(2)loop s
CPU 执行loop s的时候,要进行两步操作:
① (cx)=(cx)-1;
② 判断cx 中的值,不为0 则转至标号s 所标识的地址处执行(这里的指令是“add ax,ax),如果为零则执行下一条指令(下一条指令是mov ax,4c00h)。 编程计算2∧12。
程序代码:
assume cs:code
code segment
mov ax,2
mov cx,11
s: add ax,ax
loop s
mov ax,4c00h
int 21h
code ends
end
- 341. 5.2 Loop指令程序分析(续):
(3)以下三条指令
mov cx,11
s: ddd ax,ax
loop s
执行loop s时,首先要将(cx)减1,然后若(cx)不为0,则向前转至s处执行add ax,ax。所以,我们可以利用cx来控制add ax,ax的执行次数。
下面我们详细分析一下这段程序的执行过程,从中体会如何用cx 和loops 相配合实现循环功能。 编程计算2∧12。
程序代码:
assume cs:code
code segment
mov ax,2
mov cx,11
s: add ax,ax
loop s
mov ax,4c00h
int 21h
code ends
end
- 342. 5.2 Loop指令程序的执行过程:
mov cx,11
s: add ax,ax
loop s(1)执行mov cx,11,设置(cx)=11;
(2)执行add ax,ax(第1次);
(3)执行loop s 将(cx)减1,(cx)=10, (cx)不为0,所以转至s处;
(4)执行add ax,ax(第2次);
(5)执行loop s 将(cx)减1,(cx)=9, (cx)不为0,所以转至s处;
- 343. 5.2 Loop指令程序的执行过程:
mov cx,11
s: add ax,ax
loop s (6)执行add ax,ax(第3次);
(7)执行loop s 将(cx)减1,(cx)=8, (cx)不为0,所以转至s处;
(8)执行add ax,ax(第4次);
(9)执行loop s 将(cx)减1,(cx)=7, (cx)不为0,所以转至s处;
- 344. 5.2 Loop指令程序的执行过程:
mov cx,11
s: add ax,ax
loop s (10)执行add ax,ax(第5次);
(11)执行loop s 将(cx)减1,(cx)=6,(cx)不为0,所以转至s处;
(12)执行add ax,ax(第6次);
(13)执行loop s 将(cx)减1,(cx)=5,(cx)不为0,所以转至s处;
- 345. 5.2 Loop指令程序的执行过程:
mov cx,11
s: add ax,ax
loop s (14)执行add ax,ax(第7次);
(15)执行loop s 将(cx)减1,(cx)=4,(cx)不为0,所以转至s处;
(16)执行add ax,ax(第8次);
(17)执行loop s 将(cx)减1,(cx)=3,(cx)不为0,所以转至s处;
- 346. 5.2 Loop指令程序的执行过程:
mov cx,11
s: add ax,ax
loop s(18)执行add ax,ax(第9次);
(19)执行loop s 将(cx)减1,(cx)=2, (cx)不为0,所以转至s处;
(20)执行add ax,ax(第10次);
(21)执行loop s 将(cx)减1,(cx)=1, (cx)不为0,所以转至s处;
- 347. 5.2 Loop指令程序的执行过程:
mov cx,11
s: add ax,ax
loop s(22)执行add ax,ax(第11次);
(23)执行loop s 将(cx)减1,(cx)=0,(cx)为0,所以向下执行。
(结束循环)
- 348. 5.2 Loop指令从上面的过程中,我们可以总结出用cx和loop 指令相配合实现循环功能的三个要点:
(1)在cx中存放循环次数;
(2)loop 指令中的标号所标识地址要在前面;
(3)要循环执行的程序段,要写在标号和loop 指令的中间。
- 349. 5.2 Loop指令用cx和loop指令相配合实现循环功能的程序框架如下:
mov cx,循环次数 s: 循环执行的程序段 loop s
- 350. 5.2 Loop指令问题5.2
用加法计算123 x236 ,结果存在ax 中。
思考后看分析。
分析:可用循环完成,将123加236次。可先设(ax)=0,然后循环做236次(ax)=(ax)+123。
程序代码
- 351. 5.2 Loop指令问题5.2程序代码
assume cs:code
code segment
mov ax,0
mov cx,236
s:add ax,123
loop s
mov ax,4c00h
int 21h
code ends
end
- 352. 5.2 Loop指令问题5.3
改进问题5.2程序,提高123x236 的计算速度。
思考后看分析。
分析:问题5.2程序做了236 次加法,我们可以将236 加123次。可先设(ax)=0,然后循环做123次(ax)=(ax)+236,这样可以用123 次加法实现相同的功能。
程序代码请自行实现。(参考代码)
- 353. 5.3 在Debug中跟踪用loop指令实现的循环程序考虑这样一个问题,计算ffff:0006单元中的数乘以3,结果存储在dx中。我们分析一下:
(1)运算后的结果是否会超出dx所能存储的范围?
ffff:0006 单元中的数是一个字节型的数据,范围在0~255之间,则用它和3相乘结果不会大于65535,可以在dx 中存放下。
- 354. 5.3 在Debug中跟踪用loop指令实现的循环程序分析:
(2)我们用循环累加来实现乘法,用哪个寄存器进行累加?我们将ffff:0006单元中的数赋值给ax,用dx进行累加。先设(dx)=0,然后做3次(dx)=(dx)+(ax)。
(3) ffff:0006单元是一个字节单元,ax是一个 16 位寄存器,数据长度不一样,如何赋值?
- 355. 5.3 在Debug中跟踪用loop指令实现的循环程序注意,
我们说的是“赋值”,就是说,让 ax 中的数据的值(数据的大小)和ffff:0006 单元中的数据的值(数据的大小)相等。
8 位数据01H和16位数据0001H的数据长度不一样,但它们的值是相等的。
- 356. 5.3 在Debug中跟踪用loop指令实现的循环程序那么我们如何赋值?
设ffff:0006单元中的数据是XXH,若要ax中的值和ffff:0006单元中的相等,ax中的数据应为00XXH。
所以,若实现ffff:0006单元向ax 赋值,我们应该令(ah)=0,(al)=(ffff6H)。
实现计算ffff:0006单元中的数乘以3,结果存储在dx中的程序代码。
- 357. 5.3 在Debug中跟踪用loop指令实现的循环程序assume cs:code
code segment
mov ax,0ffffh
mov ds,ax
mov bx,6
mov al,[bx]
mov ah,0
mov dx,0
mov cx,3
s: add dx,ax
loop s
mov ax,4c00h
int 21h
code ends
end注意程序中的第一条指令mov ax,0ffffh。
我们知道大于9FFFH的十六进制数据A000H、A001H、…… 、C000H、C001H、……、FFFEH、FFFFH等,在书写的时候都是以字母开头的。
而在汇编源程序中,数据不能以字母开头,所以要在前面加0。
- 358. 5.3 在Debug中跟踪用loop指令实现的循环程序下面我们对程序的执行过程进行跟踪。
首先 ,我们将它编辑为源程序文件,文件名定为 p3.asm ;对其进行编译连接后生成p3.exe;然后再用Debug对p3.exe中的程序进行跟踪。
源程序p3.asm
- 359. 5.3 在Debug中跟踪用loop指令实现的循环程序上面,我们通过对一个循环程序的跟踪,更深入一步地讲解了 loop指令实现循环的原理。
下面,我们将前面的程序改一下,计算 ffff:0006单元中的数乘以 123,结果存储在dx中。
更改程序:只要将寻循环次数改为123。
- 360. 5.3 在Debug中跟踪用loop指令实现的循环程序程序源码 Debug加载 跟踪
- 361. 5.3 在Debug中跟踪用loop指令实现的循环程序Debug执行“g 0012”后,CS:0012前的程序段被执行,从各个相关的寄存器中的值,我们可以看出执行的结果:
- 362. 5.3 在Debug中跟踪用loop指令实现的循环程序我们跟踪了两次循环的过程,可以确定循环程序段在逻辑上是正确的。
我们不想再继续一步一步的观察循环的过程了,因为还要进行 121((cx)=79h)次循环,我们希望将循环一次执行完,显然 T命令不可行,我们可以使用P命令来达到目的。
再次遇到loop指令时,使用P命令来执行。Debug就会自动重复执行循环中的指令,直到(cx)=0为止。
- 363. 5.4 Debug和汇编编译器Masm对指令的不同处理 本节只试点为下面刻成的顺利进行提供一点预备知识。
我们在Debug中写过类似的指令:
mov ax,[0]
表示将ds:0处的数据送入al中。
但是在汇编元程序中,指令“mov ax,[0]”被编译器当作指令“mov ax,0”处理。
示例
- 364. 5.4 Debug和汇编编译器Masm对指令的不同处理示例任务:将内存2000:0、2000:1 、2000:2、2000:3单元中的数据送入al,bl,cl,dl中。
(1)在Debug中编程实现
(2)汇编程序实现
两种实现的实际实施情况
- 365. 5.4 Debug和汇编编译器Masm对指令的不同处理在Debug中编程实现:
mov ax,2000h
mov ds,ax
mov al,[0]
mov bl,[1]
mov cl,[2]
mov dl,[3]
- 366. 5.4 Debug和汇编编译器Masm对指令的不同处理汇编程序实现:assume cs:code
code segment
mov ax,2000h
mov ds,ax
mov al,[0]
mov bl,[1]
mov cl,[2]
mov dl,[3]
mov ax,4c00h
int 21h
code ends
end
- 367. 5.4 Debug和汇编编译器Masm对指令的不同处理Debug中的情况:
- 368. 5.4 Debug和汇编编译器Masm对指令的不同处理将汇编源程序存储为compare.asm用masm、link生成compare.exe,用debug加载compare.exe,如图:
- 369. 5.4 Debug和汇编编译器Masm对指令的不同处理比如我们可以这样访问2000:0单元:
mov ax,2000h
mov ds,ax
mov al,ds:[0]
- 370. 5.5 loop和[bx]的联合应用考虑这样一个问题:
计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
我们还是先分析一下
- 371. 5.5 loop和[bx]的联合应用计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
分析:
(1)运算后的结果是否会超出 dx 所能存储的范围?
ffff:0~ffff:b内存单元中的数据是字节型数据,范围在0~255之间,12个这样的数据相加,结果不会大于 65535 ,可以在dx中存放下。
- 372. 5.5 loop和[bx]的联合应用计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
分析:
(2)我们是否将 ffff:0~ffff:b中的数据直接累加到dx中?
当然不行,因为ffff:0~ffff:b中的数据是8位的,不能直接加到16位寄存器dx中。
- 373. 5.5 loop和[bx]的联合应用计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
分析:
(3)我们能否将ffff:0~ffff:b中的数据累加到dl中,并设置(dh=0,从而实现累加到dx中的目标?
这也不行,因为dl是8位寄存器,能容纳的数据的范围在小 255 之间,ffff : 0~ffff:b中的数据也都是 8 位,如果仅向dl中累加12个 8 位数据,很有可能造成进位丢失。
- 374. 5.5 loop和[bx]的联合应用计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
分析:
(4)我们到底怎样将用ffff:0~ffff:b中的8位数据,累加到16位寄存器dx中?
- 375. 5.5 loop和[bx]的联合应用从上面的分析中,我们可以看到,这里面有两个问题:类型的匹配和结果的不超界。
具体的说,就是在做加法的时候,我们有两种方法:
(dx)=(dx)+内存中的8位数据:
(dl)=(dl)+内存中的8 位数据;
第一种方法中的问题是两个运算对象的类型不匹配,第二种方法中的问题是结果有可能超界。
- 376. 5.5 loop和[bx]的联合应用计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
分析:
怎样解决这两个看似矛盾的问题?
目前的方法(在后面的课程中我们还有别的方法)就是我们得用一个16位寄存器来做中介。
- 377. 5.5 loop和[bx]的联合应用我们将内存单元中的 8 位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx上,从而使两个运算对象的类型匹配并且结果不会超界。
想清楚以上的问题之后,我们编写程序代码。
- 378. 5.5 loop和[bx]的联合应用问题5.4
应用loop 指令,改进程序5.5 ,使它的指令行数让人能够接受。
思考后看分析。
- 379. 5.5 loop和[bx]的联合应用分析:
可以看出,在程序中,有12个相似的程序段,我们将它们一般化地描述为:
mov al,ds:[x]
mov ah,0
add dx,ax
我们可以看到,12个相似的程序段中,只有mov al,ds:[X]指令中的内存单元的偏移地址是不同的,其他都一样。
- 380. 5.5 loop和[bx]的联合应用分析: (续)
而这些不同的偏移地址是可在0≤X≤0bH的范围内递增变化的。
我们可以用数学语言来描述这个累加的运算:
- 381. 5.5 loop和[bx]的联合应用分析: (续)
从程序实现上,我们将循环做: (al )=((ds)*16+X) (ah)=0 (dx)=(dx)+(ax)
一共循环12次,在循环开始前(dx)=0ffffh,X=0,ds:X指向第一个内存单元。每次循环后,X递增,ds:X指向下一个内存单元。
- 382. 5.5 loop和[bx]的联合应用分析:(续)完整的算法描述
初始化:(ds)=0ffffhX=0(dx)=0
循环12次:(al) =((ds)*16+X) (ah) = 0 (dx)=(dx)+(ax)X=X+1
- 383. 5.5 loop和[bx]的联合应用分析: (续)
可见,表示内存单元偏移地址的X应该是一个变量 ,因为在循环的过程中,偏移地址必须能够递增。
这样,在指令中,我们就不能用常量来表示偏移地址。我们可以将偏移地址放到 bx中,用[bx]的方式访问内存单元。
- 384. 5.5 loop和[bx]的联合应用分析: (续)
在循环开始前设(bx)=0,每次循环,将bx中的内容加1即可。
最后一个问题是,如何实现循环12次?
我们的loop指令该发挥作用了。
更详细的算法描述。
- 385. 5.5 loop和[bx]的联合应用分析: (续)更详细的算法描述初始化
(ds)=0ffffh
(bx)=0
(dx)=0
(cx)=12
循环12 次: s:(al)=((ds)*16+(bx))
(ah)=0
(dx)=(dx)+(ax)
(bx)=(bx)+1 loops
写出程序
- 386. 5.5 loop和[bx]的联合应用在实际编程中,经常会遇到,用同一种方法处理地址连续的内存单元中的数据的问题。
我们需要用循环来解决这类问题,同时我们必须能够在每次循环的时候按照同一种方法来改变要访问的内存单元的地址。
- 387. 5.5 loop和[bx]的联合应用这时,我们就不能用常量来给出内存单元的地址(比如[0]、[1]、[2]中,0、1、2是常量),而应用变量。
“mov al,[bx]”中的 bx就可以看作一个代表内存单元地址的变量,我们可以不写新的指令,仅通过改变bx中的数值,改变指令访问的内存单元。
- 388. 5.6 段前缀指令“mov ax,[bx]”中,内存单元的偏移地址由bx给出,而段地址默认在ds中。
我们可以在访问内存单元的指令中显式地给出内存单元的段地址所在的段寄存器。
- 389. 5.6 段前缀这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址的“ds:”、“cs:”、“ss:”或“es:”,在汇编语言中称为段前缀。
- 390. 5.7 一段安全的空间在8086模式中,随意向一段内存空间写入内容是很危险的 ,因为这段空间中可能存放着重要的系统数据或代码。
比如下面的指令:
mov ax,1000h
mov ds,ax
mov al,0
mov ds:[0],al
- 391. 5.7 一段安全的空间我们以前在Debug中,为了讲解上的方便,写过类似的指令。
但这种做法是不合理的 ,因为之前我们并没有论证过 1000:0中是否存放着重要的系统数据或代码。
如果1000:0中存放着重要的系统数据或代码,“mov ds:[0],al” 将其改写,将引发错误。
比如程序
- 392. 5.7 一段安全的空间上图是在Windows2000的DOS方式中,在Debug里执行“mov [0026],ax”的结果。如果在实模式(即纯DOS方式)下执行程序p7.exe,将会引起死机。
产生这种结果的原因是0:0026处存放着重要的系统数据,而“mov [0026],ax”将其改写。
- 393. 5.7 一段安全的空间可见,我们在不能确定一段内存空间中是否存放着重要的放据或代码的时候,不能随意向其中写入内容。
不要忘记,我们是在操作系统的环境中工作,操作系统管理所有的资源,也包括内存。
- 394. 5.7 一段安全的空间同样不能忘记,我们正在学习的是汇编语言,要通过它来获得底层的编程体验,理解计算机底层的基本工作机理。
所以我们尽量直接对硬件编程,而不去理会操作系统。
- 395. 5.7 一段安全的空间我们似乎面临一种选择,是在操作系统中安全、规矩地编程,还是自由、直接地用汇编语言去操作真实的硬件,了解那些早己被层层系统软件掩盖的真相?
在大部分的情况下,我们选择后者,除非我们就是在学习操作系统本身的内容。
注意
- 396. 5.7 一段安全的空间注意:
我们在纯DOS方式(实模式)下,可以不理会DOS,直接用汇编语言去操作真实的硬件,因为运行在CPU实模式下的DOS,没有能力对硬件系统进行全面、严格地管理。
- 397. 5.7 一段安全的空间注意: (续)
但在Windows 2000、UNIX这些运行于CPU保护模式下的操作系统中,不理会操作系统,用汇编语言去操作真实的硬件,是根本不可能的。硬件已被这些操作系统利用CPU保护模式所提供的功能全面而严格地管理了。
- 398. 5.7 一段安全的空间在后面的课程中,我们需要直接向内存中写入内容,可我们又不希望发生图5.17中的那种情况。所以要找到一段安全的空间供我们使用。
在一般的PC机中,DOS方式下,DOS和其他合法的程序一般都不会使用0:200~0:2FF( 0:200h~0:2FFh)的256 个字节的空间。所以,我们使用这段空间是安全的。
- 399. 5.7 一段安全的空间不过为了谨慎起见,在进入DOS后,我们可以先用Debug 查看一下,如果0:200~0:2FF单元的内容都是0的话,则证明DOS 和其他合法的程序没有使用这里。
为什么DOS和其他合法的程序一般都不会使用0:200~0:2FF这段空间?我们将在以后的课程中讨论这个问题。
- 400. 5.7 一段安全的空间好了,我们总结一下:
(1)我们需要直接向一段内存中写入内容;
(2)这段内存空间不应存放系统或其他程序的数据或代码,否则写入操作很可能引发错误。
(3)DOS方式下,一般情况, 0:200~0:2FF 空间中没有系统或其他程序的数据或代码;
(4)以后,我们需要直接向一段内存中写入内容时,就使用0:200~0:2FF这段空间。
- 401. 5.8 段前缀的使用我们考虑一个问题:
将内存ffff:0~ffff:b段元中的数据拷贝到 0:200~0:20b单元中。
分析一下
程序代码
改进的程序代码
- 402. 5.8 段前缀的使用分析一下:
(1) 0:200~0:20b单元等同于0020:0~0020:b单元,它们描述的是同一段内存空间:
(2)拷贝的过程应用循环实现,简要描述如下: 初始化:X=0
循环12次: 将ffff:X单元中的数据送入0020:X(需要用一个寄存器中转)
X=X+1
- 403. 5.8 段前缀的使用分析一下: (续)
(3)在循环中,源单元ffff:X和目标单元的0020:X的偏移地址X是变量。我们用bx来存放。
(4)我们用将0:200~0:20b用0020:0~0020:b描述,就是为了使目标单元的偏移地址和源始单元的偏移地址从同一数值0开始。
- 404. 5.8 段前缀的使用
- 405. 5.8 段前缀的使用因源单元ffff:X和目标单元0020:X 相距大于64KB,在不同的64KB段里,程序中,每次循环要设置两次ds。
这样做是正确的,但是效率不高。
我们可以使用两个段寄存器分别存放源单元ffff:X和目标单元0020:X的段地址,这样就可以省略循环中需要重复做12次的设置ds的程序段。
- 406. 5.8 段前缀的使用
- 407. 5.8 段前缀的使用改进的程序中,使用 es 存放目标空间0020:0~0020:b的段地址,用ds存放源空间ffff:0~ffff:b的段地址。
在访问内存单元的指令“mov es:[bx],al”中 ,显式地用段前缀 “es:” 给出单元的段地址,这样就不必在循环中重复设置ds。
- 408. 小结
- 409. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 410. 第6章 包含多个段的程序6.1 在代码段中使用数据
6.2 在代码段中使用栈
6.3 将数据、代码、栈放入不同的段
- 411. 引言前面我们写的程序中,只有一个代码段。
现在有一个问题是:
如果程序需要用其他空间来存放数据,我们使用哪里呢?
- 412. 引言第5章中,我们讲到要使用一段安全的空间。
可哪里安全呢?
第5章中,我们说0:200~0:300是相对安全的;
可这段空间的容量只有256个字节,如果我们需要的空间超过256个字节该怎么办呢?
- 413. 引言对于使用多个段的问题,我们先简单说到这里,下面我们将以这样的顺序来深入地讨论多个段的问题:
(1)在一个段中存放数据、代码、栈,我们先来体会一下不使用多个段时的情况;
(2)将数据、代码、栈放入不同的段中。
- 414. 6.1 在代码段中使用数据考虑这样一个问题,编程计算以下8个数据的和,结果存在ax 寄存器中:
0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H。
在前面的课程中,我们都是累加某些内存单元中的数据,并不关心数据本身。
可现在我们要累加的就是已经给定了数值的数据。
- 415. 6.1 在代码段中使用数据assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
mov bx,0
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
codesg ends
end程序6.1
- 416. 6.1 在代码段中使用数据解释一下,程序第一行中的 “dw”的含义是定义字型数据。dw即define word。
在这里,我们使用dw定义了8个字型数据(数据之间以逗号分隔),它们所占的内存空间的大小为16个字节。
- 417. 6.1 在代码段中使用数据程序中的指令就要对这8个数据进行累加,可这8个数据在哪里呢?
由于它们在代码段中,程序在运行的时候CS中存放代码段的段地址,所以我们可以从CS中得到它们的段地址。
- 418. 6.1 在代码段中使用数据这8个数据的偏移地址是多少呢?
因为用dw定义的数据处于代码段的最开始,所以偏移地址为0,这8 个数据就在代码段的偏移0、2、4、6、8、A、C、E处。
程序运行时,它们的地址就是CS:0、CS:2、CS:4、CS:6、CS:8、CS:A、CS:C、CS:E。
- 419. 6.1 在代码段中使用数据程序中,我们用bx存放加2递增的偏移地址,用循环来进行累加。
在循环开始前,设置(bx)=0,cs:bx指向第一个数据所在的字单元。
每次循环中(bx)=(bx)+2,cs:bx指向下一个数据所在的字单元。
- 420. 6.1 在代码段中使用数据我们将前面的程序6.1编译、连接为可执行文件p61.exe,先不要运行,我们用debug加载查看一下。
- 421. 6.1 在代码段中使用数据用 Debug加 载后,我们可以将 IP 设置为10h,从而使CS:IP指向程序中的第一条指令。然后再用T命令、P命令、或者是G 命令执行。
可是这样一来,我们就必须用Debug 来执行程序。
程序 6.1 编译成可执行文件后,在系统中直接运行可能会出现问题,因为程序的入口处不是我们所希望执行的指令。
- 422. 6.1 在代码段中使用数据如何让这个程序在编译后可以存系统中直接运行呢?我们可以在源程序中指明界序的入口所在,具体做法见下面的程序6.2。
程序代码
- 423. 6.1 在代码段中使用数据assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
start: mov bx,0
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
codesg ends
end start程序6.2
- 424. 6.1 在代码段中使用数据
注意在程序6.2中加入的新内容,我们在程序的第一条指令的前面加上了一个标号start,而这个标号在伪指令end的后面出现。
- 425. 6.1 在代码段中使用数据
探讨end的作用:
end 除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。
- 426. 6.1 在代码段中使用数据有了这种方法,我们就可以这样来安排程序的框架:assume cs:code
code segment
:
数据
:
start:
:
:
代码
:
:
code ends
end start
- 427. 6.2 在代码段中使用栈完成下面的程序,利用栈,将程序中定义的数据逆序存放。
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
?
code ends
end
程序大致思路
- 428. 6.2 在代码段中使用栈程序的思路大致如下:
程序运行时,定义的数据存放在cs:0~cs:15单元中,共8个字单元。依次将这8个字单元中的数据入栈,然后再依次出栈到这 8 个字单元中,从而实现数据的逆序存放。
- 429. 6.2 在代码段中使用栈程序的思路大致如下: (续)
问题是,我们首先要有一段可当作栈的内存空间。如前所述,这段空间应该由系统来分配。我们可以在程序中通过定义数据来取得一段空间,然后将这段空间当作栈空间来用。
- 430. 6.2 在代码段中使用栈程序6.3源码
注意程序6.3中的指令:
mov ax,cs
mov ss,ax
mov sp,32
我们要讲 cs:16 ~ cs:31 的内存空间当作栈来用,初始状态下栈为空,所以 ss:sp要指向栈底,则设置ss:sp指向cs:32。如果对这点还有疑惑,建议回头认真复习一下第三章。
- 431. 6.2 在代码段中使用栈比如对于:
dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
我们可以说,定义了8个字型数据,也可以说,开辟了8个字的内存空间,这段空间中每个字单元中的数据依次是:
0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H。
因为它们最终的效果是一样的。
- 432. 特别提示检测点6.1(Page 119)
没有通过检测点,请不要向下学习!
- 433. 6.3 将数据、代码、栈放入不同的段在前面的内容中,我们在程序中用到了数据和栈,我们将数据、栈和代码都放到了一个段里面。我们在编程的时候要注意何处是数据,何处是栈,何处是代码。
这样做显然有两个问题:
(1)把它们放到一个段中使程序显得混乱;
(2)前面程序中处理的数据很少,用到的栈空间也小,加上没有多长的代码,放到一个段里面没有问题。
- 434. 6.3 将数据、代码、栈放入不同的段但如果数据、栈和代码需要的空间超过64KB,就不能放在一个段中(一个段的容量不能大于64 KB,是我们在学习中所用的8086模式的限制,并不是所有的处理器都这样)。
所以,我们应该考虑用多个段来存放数据、代码和栈。
- 435. 6.3 将数据、代码、栈放入不同的段怎样做呢?
我们用和定义代码段一样的方法来定义多个段,然后在这些段里面定义需要的数据,或通过定义数据来取得栈空间。
具体做法如程序6.4所示,这个程序实现了和程序6.3 一样的功能,不同之处在于它将数据、栈和代码放到了不同的段中。
- 436. 6.3 将数据、代码、栈放入不同的段程序中“data”段中的数据“0abch”的地址就是:data:6。
我们要将它送入bx中,就要用如下的代码:
mov ax,data
mov ds,ax
mov bx,ds:[6]
我们不能用下面的指令:
mov ds,data
mov ax,ds:[6]
- 437. 6.3 将数据、代码、栈放入不同的段其中指令“mov ds,data” 是错误的,因为8086CPU不允许将一个数值直接送入段寄存器中。
程序中对段名的引用,如指令“mov ds,data”中的“data”,将被编译器处理为一个表示段地址的数值。
- 438. 6.3 将数据、代码、栈放入不同的段(3)“代码段”、“数据段”、“栈段”完全是我们的安排
现在,我们以一个具体的程序来再次讨论一下所谓的“代码段”、“数据段”、“栈段”。
- 439. 6.3 将数据、代码、栈放入不同的段② 我们在源程序中用伪指令
“assume cs:code,ds:data,ss:stack”将cs、ds和ss分别和code、data、stack段相连。
这样做了之后,CPU是否就会将 cs指向 code,ds 指向 data,ss 指向stack,从而按照我们的意图来处理这些段呢?
- 440. 6.3 将数据、代码、栈放入不同的段当然也不是,要知道 assume 是伪指令,是由编译器执行的,也是仅在源程序中存在的信息,CPU并不知道它们。
我们不必深究 assume 的作用,只要知道需要用它将你定义的具有一定用途的段和相关的寄存器联系起来就可以了。
- 441. 6.3 将数据、代码、栈放入不同的段③ 若要CPU按照我们的安排行事,就要用机器指令控制它,源程序中的汇编指令是CPU要执行的内容。
- 442. 6.3 将数据、代码、栈放入不同的段CPU如何知道去执行它们?
我们在源程序的最后用“end start”说明了程序的入口,这个入口将被写入可执行文件的描述信息,可执行文件中的程序被加载入内存后,CPU的CS:IP被设置指向这个入口,从而开始执行程序中的第一条指令。
标号“start”在“code”段中,这样CPU就将code段中的内容当作指令来执行了。
- 443. 6.3 将数据、代码、栈放入不同的段我们在code段中,使用指令: mov ax,stack
mov ss,ax
mov sp,16 设置ss指向stack,设置ss:sp指向stack:16, CPU 执行这些指令后,将把stack段当做栈空间来用。
CPU若要访问data段中的数据,则可用 ds 指向 data 段,用其他的寄存器(如:bx)来存放 data段中数据的偏移地址。
- 444. 6.3 将数据、代码、栈放入不同的段总之,CPU到底如何处理我们定义的段中的内容,是当作指令执行,当作数据访问,还是当作栈空间,完全是靠程序中具体的汇编指令,和汇编指令对CS:IP、SS:SP、DS等寄存器的设置来决定的。
我们完全可以将程序6.4 写成这样,实现同样的功能。程序6.4源码
- 445. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 446. 第7章 更灵活的定位内存地址的方法7.1 and和or指令
7.2 关于ASCII码
7.3 以字符形式给出的数据
7.4 大小写转换的问题
7.5 [bx+idata]
7.6 用[bx+idata]的方式进行数组的处理
7.7 SI和DI
7.8 [bx+si]和[bx+di]
7.9 [bx+si+idata]和[bx+di+idata]
7.10 不同的寻址方式的灵活应用
- 447. 引言前面,我们用[0]、[bx]的方法,在访问内存的指令中,定位内存单元的地址。
在这一章中,我们主要讲解一些更灵活的定位内存地址的方法和相关的编程方法。
我们的讲解将通过具体的问题来进行。
- 448. 7.1 and和or指令首先我们介绍两条指令and和or,因为我们下面的例程中要用到它们。
(1)and 指令:逻辑与指令,按位进行与运算。
如 mov al, 01100011B
and al, 00111011B
执行后:al = 00100011B
通过该指令可将操作对象的相应位设为0,其他位不变。示例
- 449. 7.2 关于ASCII码世界上有很多编码方案,有种方案叫做ASCII编码,是在计算机系统中通常被采用的。
简单地说,所谓编码方案,就是一套规则,它约定了用什么样的信息来表示现实对象。
比如说,在ASCII编码方案中,用 61H 表示“a”,62H表示“b”。
- 450. 7.2 关于ASCII码一种规则需要人们遵守才有意义。
一个文本编辑过程中,就包含着按照ASCII编码规则进行的编码和解码。
在文本编辑过程中,我们按一下键盘的a键,就会在屏幕上看到“a”。
这是怎样一个过程呢?
- 451. 7.3 以字符形式给出的数据我们可以在汇编程序中,用 “……”的方式指明数据是以字符的形式给出的,编译器将把它们转化为相对应的ASCII码。
例如程序7.1
- 452. 7.3 以字符形式给出的数据assume ds:data
data segment
db 'unIX'
db 'foRK'
data ends
code segment
start:mov al,'a'
mov bl,'b'
mov ax,4c00h
int 21h
code ends
end start 程序7.1
- 453. 7.3 以字符形式给出的数据上面的源程序中:
“db ‘unIX’ ” 相当于“db 75H,6EH,49H,58H”, “u”、 “n”、 “I”、 “X”的ASCII码分别为75H、6EH、49H、58H;
“db ‘foRK’ ” 相当于“db 66H,6FH,52H,4BH”, “u”、 “n”、 “I”、 “X”的ASCII码分别为66H、6FH、52H、4BH;
“mov al,’a’”相当于“mov al,61H”,”a”的ASCII码为61H;
“mov al,’b’”相当于“mov al,62H”,”b”的ASCII码为62H。
- 454. 7.4 大小写转换的问题首先分析一下,我们知道同一个字母的大写字符和小写字符对应的 ASCII 码是不同的,比如 “A” 的 ASCII 码是41H,“a”的ASCII码是61H。
要改变一个字母的大小写,实际上就是要改变它所对应的ASCII 码。
- 455. 7.4 大小写转换的问题我们可以将所有的字母的大写字符和小写字符所对应的ASCII码列出来,进行对比,从中找到规律。
大写 二进制 小写 二进制
A 01000001 a 01100001
B 01000010 b 01100010
C 01000011 c 01100011
D 01000100 d 01100100
- 456. 7.4 大小写转换的问题通过对比,我们可以看出来,小写字母的ASCII码值比大写字母的ASCII码值大20H 。
这样,我们可以想到,如果将 “a” 的ASCII码值减去20H,就可以得到“A”;如果将“A”的ASCII码值加上20H 就可以得到“a”。
- 457. 7.4 大小写转换的问题按照这样的方法,我们可以将 datasg段中:
第一个字符串“BaSiC”中的小写字母变成大写;
第二个字符串,“iNfOrMaTiOn”中的大写字母变成小写。
- 458. 7.4 大小写转换的问题要注意的是:
对于字符串“BaSic”,我们应只对其中的小写字母所对应的ASCII码进行减20H 的处理,将其转为大写,而对其中的大写字母不进行改变;
- 459. 7.4 大小写转换的问题要注意的是: (续)
对于字符串 “ iNforMaTIOn ” ,我们应只对其中的大写字母所对应的ASCII码进行加20H 的处理,将其转为小写;
而对于其中的小写字母不进行改变,这里面就存在着一个前提,程序必须要能够判断一个字母是大写还是小写。
- 460. 7.4 大小写转换的问题以“BaSiC”讨论,程序的流程将是这样的:assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'iNfOrMaTiOn'
datasg ends
codesg segment
start: mov ax,datasg
mov ds,ax
mov bx,0
mov cx,5
s: mov al,[bx]
如果(al)>61H,则为小写字母ASCII码,则:sub al,21H
mov [bx],al
inc bx
loop s
:
codesg ends
end start
- 461. 7.4 大小写转换的问题判断将用到一些我们目前还没有学习到的指令。现在面临的问题是,用己学的指令来解决这个问题,则我们不能对字母的大小写进行任何判断。
但是,现实的问题却要求程序必须要能区别对待大写字母和小写字母。
- 462. 7.4 大小写转换的问题可以看出,就ASCII码的二进制形式来看,除第5位(位数从0开始计算)外,大写字母和小写字母的其他各位都一样。
大写字母ASCII码的第5位(位数从0开始计算)为0,小写字母的第5位为1。
- 463. 7.4 大小写转换的问题这样,我们就有了新的方法:
一个字母,我们不管它原来是大写还是小写:
我们将它的第5位置0,它就必将变为大写字母;
将它的第5 位置1,它就必将变为小写字母。
- 464. 7.4 大小写转换的问题我们用什么方法将一个数据中的某一位置0还是置1?
当然是用我们刚刚学过的or和and指令。
完整的程序代码
- 465. 7.5 [bx+idata]在前面,我们可以用[bx]的方式来指明一个内存单元, 我们还可以用一种更为灵活的方式来指明内存单元:
[bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata(bx中的数值加上idata)。
- 466. 7.5 [bx+idata]我们看一下指令mov ax,[bx+200]的含义:
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上200,段地址在ds中。
数学化的描述为: (ax)=((ds)*16+(bx)+200)
- 467. 7.5 [bx+idata]指令mov ax,[bx+200]也可以写成如下格式(常用):
mov ax,[200+bx]
mov ax,200[bx]
mov ax,[bx].200
问题
- 468. 7.5 [bx+idata]问题7.1
用Debug查看内存,结果如下:
2000:1000 BE 00 06 00 00 00 ……
写出下面的程序执行后,ax、bx、cx中的内容。
思考后看分析。 mov ax,2000H
mov ds,ax
mov bx,1000H
mov ax,[bx]
mov cx,[bx+1]
add cx,[bx+2]
- 469. 7.5 [bx+idata]问题7.1分析
mov ax,[bx]
访问的字单元的段地址在ds中,(ds)=2000H;
偏移地址在bx 中,(bx)=1000H;
指令执行后(ax)=00BEH。
- 470. 7.5 [bx+idata]问题7.1分析
mov cx,[bx+1]
访问的字单元的段地址在ds中,(ds)=2000H;
偏移地址=(bx)+1=1001H;
指令执行后(cx)=0600H。
- 471. 7.5 [bx+idata]问题7.1分析
add cx,[bx+2]
访问的字单元的段地址在ds中,(ds)=2000H;
偏移地址=(bx)+2=1002H;
指令执行后(cx)=0606H。
- 472. 7.6 用[bx+idata]的方式进行数组的处理有了[bx+idata]这种表示内存单元的方式,我们就可以用更高级的结构来看待所要处理的数据。
我们通过下面的问题来理解这一点。
- 473. 7.6 用[bx+idata]的方式进行数组的处理在codesg中填写代码,将datasg中定义的第一个字符串,转化为大写,第二个字符串转化为小写。assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'MinIX'
datasg ends
codesg segment
start: ……
codesg ends
end start
- 474. 7.6 用[bx+idata]的方式进行数组的处理按照我们原来的方法,用[bx]的方式定位字符串中的字符。
代码段中的程序代码: mov ax,datasg
mov ds,ax
mov bx,0
mov cx,5
s: mov al,[bx]
and al,11011111b
mov [bx],al
inc bx
loop s
mov bx,5
mov cx,5
s0: mov al,[bx]
or al,00100000b
mov [bx],al
inc bx
loop s0
- 475. 7.6 用[bx+idata]的方式进行数组的处理现在,我们有了 [bx+idata]的方式,就可以用更简化的方法来完成上面的程序。
我们观察datasg段中的两个字符串,一个的起始地址为0,另一个的起始地址为5。
我们可以将这两个字符串看作两个数组,一个从0地址开始存放,另一个从5开始存放。
- 476. 7.6 用[bx+idata]的方式进行数组的处理那么我们可以用[0+bx]和[5+bx]的方式在同一个循环中定位这两个字符串中的字符。
在这里,0和5给定了两个字符串的起始偏移地址,bx中给出了从起始偏移地址开始的相对地址。
这两个字符串在内存中的起始地址是不一样的,但是,它们中的每一个字符,从起始地址开始的相对地址的变化是相同的。
- 477. 7.6 用[bx+idata]的方式进行数组的处理改进的程序: mov ax,datasg
mov ds,ax
mov bx,0
mov cx,5
s: mov al,[bx] ;定位第一个字符串的字符
and al,11011111b
mov [bx],al
mov al,[5+bx] ;定位第二个字符串的字符
or al,00100000b
mov [5+bx],al
inc bx
loop s
- 478. 7.6 用[bx+idata]的方式进行数组的处理程序还可以写成这样: mov ax,datasg
mov ds,ax
mov bx,0
mov cx,5
s: mov al,0[bx]
and al,11011111b
mov 0[bx],al
mov al,5[bx]
or al,00100000b
mov 5[bx],al
inc bx
loop s
- 479. 7.6 用[bx+idata]的方式进行数组的处理如果我们用高级语言,比如C语言来描述上面的程序,大致是这样的:
char a[5]=“BaSiC”;
char b[5]=“MinIX”;
main()
{
int i;
i=0;
do
{
a[i]=a[i]&0xDF;
b[i]=b[i]&0x20;
i++;
}while(i<5);
}
- 480. 7.6 用[bx+idata]的方式进行数组的处理如果读者熟悉C语言的话,可以比较一下这个C程序和上面的汇编程序的相似之处。尤其注意它们定位字符串中字符的方式:
C语言定位方式:a[i],b[i]
汇编语言定位方式:0[bx],5[bx]
- 481. 7.6 用[bx+idata]的方式进行数组的处理通过比较,我们可以发现:
[bx+idata]的方式为高级语言实现数组提供了便利机制。
- 482. 7.7 SI和DISI和DI是8086CPU中和bx功能相近的寄存器,SI和DI不能够分成两个8 位寄存器来使用。
下面的三组指令实现了相同的功能:
(1) mov bx,0
mov ax,[bx]
(2) mov si,0
mov ax,[si]
(3) mov di,0
mov ax,[di]
- 483. 7.7 SI和DI下面的三组指令也实现了相同的功能:
问题7.2(1) mov bx,0
mov ax,[bx+123]
(2) mov si,0
mov ax,[si+123]
(3) mov di,0
mov ax,[di+123]
- 484. 7.7 SI和DI问题7.2
用寄存器SI和DI实现将字符串‘welcome to masm!’复制到它后面的数据区中。
assume cs:codesg,ds:datasg
datasg segment
db 'welcome to masm!'
db '................'
datasg ends
思考后看分析。
- 485. 7.7 SI和DI问题7.2分析
我们编写的程序大都是进行数据的处理,而数据在内存中存放,所以我们在处理数据之前首先要搞清楚数据存储在什么地方,也就是说数据的内存地址。
现在我们要对datasg 段中的数据进行复制,我们先来看一下要复制的数据在什么地方,datasg:0,这是要进行复制的数据的地址。
- 486. 7.7 SI和DI问题7.2分析(续)
那么复制到哪里去呢?
它后面的数据区。
“welcome to masm!”从偏移地址0开始存放,长度为 16 个字节,所以,它后面的数据区的偏移地址为 16 ,就是字符串所要存放的空间。
- 487. 7.7 SI和DI问题7.2分析(续)
清楚了地址之后,我们就可以进行处理了。
我们用ds:si 指向要复制的源始字符串,用 ds:di 指向复制的目的空间,然后用一个循环来完成复制。
- 488. 7.7 SI和DI代码段:
注意,在程序中,我们用16位寄存器进行内存单元之间的数据传送,一次复制 2 个字节,一共循环8次。codesg segment
start: mov ax,datasg
mov ds,ax
mov si,0
mov di,16
mov cx,8
s: mov ax,[si]
mov [di],ax
add si,2
add di,2
loop s
mov ax,4c00h
int 21h
codesg ends
end start
- 489. 7.7 SI和DI问题7.3
用更少的代码,实现问题7. 2中的程序。
思考后看分析。
- 490. 7.7 SI和DI问题7.3分析:我们可以利用[bx(si或di)+idata]的方式,来使程序变得简洁。
程序如下:codesg segment
start: mov ax,datasg
mov ds,ax
mov si,0
mov cx,8
s: mov ax,0[si]
mov 16[si],ax
add si,2
loop s
mov ax,4c00h
int 21h
codesg ends
end start
- 491. 7.8 [bx+si]和[bx+di]在前面,我们用[bx(si或di)]和[bx(si或di)+idata] 的方式来指明一个内存单元,我们还可以用更灵活的方式:
[bx+si]
[bx+di]
[bx+si]和[bx+di]的含义相似,我们以[bx+si]为例进行讲解。
- 492. 7.8 [bx+si]和[bx+di][bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)(即bx中的数值加上si中的数值)。
我们看下指令mov ax,[bx+si]的含义:
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值,段地址在ds中。
- 493. 7.8 [bx+si]和[bx+di]指令mov ax,[bx+si]的数学化的描述为:
(ax)=( (ds)*16+(bx)+(si) )
该指令也可以写成如下格式(常用):
mov ax,[bx][si]
- 494. 7.8 [bx+si]和[bx+di]问题7.4
用Debug查看内存,结果如下:
2000:1000 BE 00 06 00 00 00 ……
写出下面的程序执行后,ax、bx、cx中的内容。
- 495. 7.8 [bx+si]和[bx+di] 用Debug查看内存,结果如下:
2000:1000 BE 00 06 00 00 00 ……
mov ax,2000H
mov ds,ax
mov bx,1000H
mov si,0
mov ax,[bx+si]
inc si
mov cx,[bx+si]
inc si
mov di,si
mov ax,[bx+di] 思考后看分析。
- 496. 7.8 [bx+si]和[bx+di]问题7.4分析
mov ax,[bx+si]
访问的字单元的段地址在ds中,(ds)=2000H;
偏移地址=(bx)+(si)=1000H;
指令执行后(ax)=00BEH。
- 497. 7.8 [bx+si]和[bx+di]问题7.4分析
mov cx,[bx+si]
访问的字单元的段地址在ds中,(ds)=2000H;
偏移地址=(bx)+(si)=1001H;
指令执行后(cx)=0600H。
- 498. 7.8 [bx+si]和[bx+di]问题7.4分析
add cx,[bx+di]
访问的字单元的段地址在ds中,(ds)=2000H;
偏移地址=(bx)+(di)=1002H;
指令执行后(cx)=0606H。
- 499. 7.9 [bx+si+idata]和[bx+di+idata][bx+si+idata]和[bx+di+idata]的含义相似,我们以[bx+si+idata]为例进行讲解。
[bx+si+idata]表示一个内存单元
它的偏移地址为(bx)+(si)+idata。
(即bx中的数值加上si中的数值再加上idata)
- 500. 7.9 [bx+si+idata]和[bx+di+idata]指令mov ax,[bx+si+idata]的含义:
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值再加上idata,段地址在ds中。
数学化的描述为:
(ax)=( (ds)*16+(bx)+(si)+idata )
- 501. 7.9 [bx+si+idata]和[bx+di+idata]指令mov ax,[bx+si+idata]:
该指令也可以写成如下格式(常用):
mov ax,[bx+200+si]
mov ax,[200+bx+si]
mov ax,200[bx][si]
mov ax,[bx].200[si]
mov ax,[bx][si].200
- 502. 7.9 [bx+si+idata]和[bx+di+idata]问题7.5
用Debug查看内存,结果如下:
2000:1000 BE 00 06 00 6A 22 ……
写出下面的程序执行后,ax、bx、cx中的内容。
- 503. 7.9 [bx+si+idata]和[bx+di+idata] 用Debug查看内存,结果如下:
2000:1000 BE 00 06 00 6A 22 ……
mov ax,2000H
mov ds,ax
mov bx,1000H
mov si,0
mov ax,[bx+2+si]
inc si
mov cx,[bx+2+si]
inc si
mov di,si
mov ax,[bx+2+di] 思考后看分析。
- 504. 7.9 [bx+si+idata]和[bx+di+idata]问题7.5分析
mov ax,[bx+2+si]
访问的字单元的段地址在ds中,(ds)=2000H;
偏移地址=(bx)+(si)+2=1002H;
指令执行后(ax)=0006H。
- 505. 7.9 [bx+si+idata]和[bx+di+idata]问题7.5分析
mov ax,[bx+2+si]
访问的字单元的段地址在ds中,(ds)=2000H;
偏移地址=(bx)+(si)+2=1003H;
指令执行后(cx)=006AH。
- 506. 7.9 [bx+si+idata]和[bx+di+idata]问题7.5分析
mov ax,[bx+2+si]
访问的字单元的段地址在ds中,(ds)=2000H;
偏移地址=(bx)+(si)+2=1004H;
指令执行后(cx)=226AH。
- 507. 7.10 不同的寻址方式的灵活应用如果我们比较一下前而用到的几种定位内存地址的方法(可称为寻址方式),就可以发现有以下几种方式:
(1)[iata] 用一个常量来表示地址,可用于直接定位一个内存单元;
(2)[bx]用一个变量来表示内存地址,可用于间接定位一个内存单元;
(3)[bx+idata] 用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元;
(4)[bx+si]用两个变量表示地址;
(5)[bx+si+idata] 用两个变量和一个常量表示地址。
- 508. 7.10 不同的寻址方式的灵活应用可以看到:
从[idata]一直到[bx+si+idata],我们可以用更加灵活的方式来定位一个内存单元的地址。
这使我们可以从更加结构化的角度来看待所要处理的数据。
- 509. 7.10 不同的寻址方式的灵活应用下面我们通过一个问题的系列来体会CPU提供多种寻址方式的用意,并学习一些相关的编程技巧。
- 510. 7.10 不同的寻址方式的灵活应用问题7.6
编程,将datasg段中每个单词的头一个字母改为大写字母。assume cs:codesg,ds:datasg
datasg segment
db '1. file '
db '2. edit '
db '3. search '
db '4. view '
db '5. options '
db '6. help '
datasg ends
codesg segment
start:……
codesg ends
end start
- 511. 7.10 不同的寻址方式的灵活应用问题7.6分析
datasg中的数据的存储结构,如图:
- 512. 7.10 不同的寻址方式的灵活应用我们可以看到:
在datasg中定义了6个字符串,每个长度为16字节。
(注意,为了直观,每个字符串的后面都加上了空格符,以使它们的长度刚好为16字节)
- 513. 7.10 不同的寻址方式的灵活应用因为它们是连续存放的,我们可以将这6个字符串看成一个6行16列的二维数组。
按照要求,我们需要修改每一个单词的第一个字母,即二维数组的每一行的第4列(相对于行首的偏移地址为3 )。
- 514. 7.10 不同的寻址方式的灵活应用我们需要进行6次循环,用一个变量R定位行,用常量3 定位列。处理的过程如下:
R=第一行的地址
mov cx,6
s: 改变R行,3列的字母为大写
R=下一行的地址
loop
- 515. 7.10 不同的寻址方式的灵活应用我们用bx作变量,定位每行的起始地址,用3定位要修改的列,用[bx+idata]的方式来对目标单元进行寻址,
程序如下: mov ax,datasg
mov ds,ax
mov bx,0
mov cx,6
s: mov al,[bx+3]
and al,11011111b
mov [bx+3],al
add bx,16
loop s
- 516. 7.10 不同的寻址方式的灵活应用问题7.7
编程:将datasg段中每个单词改为大写字母。
assume cs:codesg,ds:datasg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends
codesg segment
start: ……
codesg ends
end start
- 517. 7.10 不同的寻址方式的灵活应用问题7.7分析
datasg中数据的存储结构,如图:
- 518. 7.10 不同的寻址方式的灵活应用在datasg中定义了4个字符串,每个长度为16字节。
(注意,为了使我们在Debug 中可以直观地查看,每个字符串的后面都加上了空格符,以使它们的长度刚好为16byte)
- 519. 7.10 不同的寻址方式的灵活应用因为它们是连续存放的,我们可以将这 4 个字符串看成一个 4行16列的二维数组。
按照要求,我们需要修改每一个单词,即二维数组的每一行的前3列。
- 520. 7.10 不同的寻址方式的灵活应用我们需要进行4x3次的二重循环,用变量R 定位行,变量C定位列。
外层循环按行来进行;
内层按列来进行。
- 521. 7.10 不同的寻址方式的灵活应用我们首先用R 定位第1行,然后循环修改R行的前3列;
然后再用R定位到下一行,再次循环修改R行的前3列……,
如此重复直到所有的数据修改完毕。
- 522. 7.10 不同的寻址方式的灵活应用处理的过程大致如下:
R=第一行的地址;
mov cx,4
s0: C=第一列的地址
mov cx,3
s: 改变R 行,C列的字母为大写
C=下一列的地址;
loop s
R=下一行的地址
loop s0
- 523. 7.10 不同的寻址方式的灵活应用我们用bx来作变量,定位每行的起始地址,用si 定位要修改的列,用 [bx+si] 的方式来对目标单元进行寻址,
程序代码:
问题7.8 mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s0: mov si,0
mov cx,3
s: mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s
add bx,16
loop s0 源程序
- 524. 7.10 不同的寻址方式的灵活应用问题7.8
仔细阅读上面的程序,看看有什么问题?
思考后看分析。
- 525. 7.10 不同的寻址方式的灵活应用问题7.8分析
问题在于cx的使用,我们进行二重循环,却只用了一个循环计数器,造成在进行内层的时候覆盖了外层循环的循环计数值。
多用一个计数器又不可能,因为loop指令默认cx为循环计数器。
怎么办呢?
- 526. 7.10 不同的寻址方式的灵活应用我们应该在每次开始内层循环的时候,将外层循环的cx中的数值保存起来,在执行外层循环的loop指令前,再恢复外层循环的cx数值。
我们可以用寄存器dx来临时保存cx中的数值。
改进的程序
- 527. 7.10 不同的寻址方式的灵活应用 mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s0: mov dx,cx ;将外层循环的cx值保存在dx中
mov si,0
mov cx,3 ;cx设置为内存循环的次数
s: mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s
add bx,16
mov cx,dx ;用dx中存放的外层循环的计数值恢复cx
loop s0 ;外层循环的loop指令将cx中的计数值减 1
- 528. 7.10 不同的寻址方式的灵活应用上面的程序用dx来暂时存放cx中的值;
如果在内层循环中,dx寄存器也被使用,该怎么办?
我们似乎可以使用别的寄存器,但是CPU中的寄存器数量毕竟是有限的,如8086CPU只有14个寄存器。
- 529. 7.10 不同的寻址方式的灵活应用在上面的程序中:
si、cx、ax、bx,显然不能用来暂存cx中的值,因为这些寄存器在循环中也要使用;
cs、ip、ds也不能用,因为cs:ip时刻指向当前指令,ds指向datasg段;
可用的就只有:dx、di、es、ss、sp、bp等6个寄存器了。
- 530. 7.10 不同的寻址方式的灵活应用可是如果循环中的程序比较复杂,这些寄存器也都被使用的话,那么该如何?
我们在这里讨论的问题是,程序中经常需要进行数据的暂存,我们怎样做将更为合理。
- 531. 7.10 不同的寻址方式的灵活应用这些数据可能是寄存器中的,也可能是内存中的。
我们可以用寄存器暂存它们,但是这不是一个一般化的解决方案,因为寄存器的数量有限,每个程序中可使用的寄存器都不一样。
我们希望寻找一个通用的方案,来解决这种在编程中经常会出现的问题。
- 532. 7.10 不同的寻址方式的灵活应用显然,我们不能选择寄存器,那么可以使用的就是内存了。
我们可以考虑将需要暂存的数据放到内存单元中,需要使用的时候,再从内存单元中恢复。这样我们就需要开辟一段内存空间。
再次改进的程序
- 533. 7.10 不同的寻址方式的灵活应用上面的程序中,用内存单元来保存数据;
可是上面的作法却有些麻烦,因为如果需要保存多个数据的时候,读者必须要记住数据放到了哪个单元中,这样程序容易混乱。
- 534. 7.10 不同的寻址方式的灵活应用我们使用内存来暂存数据,这一点是确定了的,但是值得推敲的是,我们用怎样的结构来保存这些数据,而使得我们的程序更加清晰。
- 535. 7.10 不同的寻址方式的灵活应用一般来说,在需要暂存数据的时候,我们都应该使用栈,回忆一下,栈空间在内存中,采用相关的指令,如:push、pop等,可对其进行特殊的操作。
我们再次改进我们的程序
- 536. 7.10 不同的寻址方式的灵活应用问题7.9
编程,将datasg段中每个单词的前四个字母改为大写字母:
assume cs:codesg,ds:datasg,ss:stacksg
stacksg segment
dw 0,0,0,0,0,0,0,0
stacksg ends
datasg segment
db '1. display......'
db '2. brows........'
db '3. replace......'
db '4. modify.......'
datasg ends
codesg segment
start: ……
codesg ends
end start
- 537. 7.10 不同的寻址方式的灵活应用问题7.9分析
datasg中的数据的存储结构,如图:
- 538. 7.10 不同的寻址方式的灵活应用在datasg中定义了4个字符串,每个长度为16字节(注意,为了使我们在Debug中可以直观地查看,每个字符串的后面都加上了空格符,以使它们的长度刚好为16字节)。
因为它们是连续存放的,我们可以将这4个字符串看成一个4行16列的二维数组,按照要求,我们需要修改每个单词的前四个字母,即二维数组的每一行的3~6列。
- 539. 7.10 不同的寻址方式的灵活应用我们需要进行4x4次的二重循环,用变量R定位行,常量3定位每行要修改的起始列,变量C定位相对于起始列的要修改的列。
外层循环按行来进行,内层按列来进行。
- 540. 7.10 不同的寻址方式的灵活应用我们首先用R定位第1行,循环修改R行的3+C( 0≤C≤3 )列;
然后再用R 定位到下一行,再次循环修改R行的3+C(0≤C≤3)列……,
如此重复直到所有的数据修改完毕。
- 541. 7.10 不同的寻址方式的灵活应用处理的过程大致如下:
R=第一行的地址;
mov cx,4
s0: C=第一个要修改的列相对于起始列的地址
mov cx,4
s: 改变R行,3+C列的字母为大写
C=下一个要修改的列相对于起始列的地址
loop s
R=下一行的地址
loop s0
- 542. 7.10 不同的寻址方式的灵活应用我们用bx来作变量,定位每行的起始地址,用 si定位要修改的列,用 [ bx+3+si ]的方式来对目标单元进行寻址。
请在实验中白己完成这个程序。
- 543. 7.10 不同的寻址方式的灵活应用这一章中,我们主要讲解了更灵活的寻址方式的应用和一些编程方法,主要内容有:
寻址方式
[bx(或si、di)+idata]、
[bx+si(或di)]、
[bx+si(或di)+idata]的意义和应用;
二重循环问题的处理;
栈的应用;
大小写转化的方法;
and 、or 指令。
- 544. 7.10 不同的寻址方式的灵活应用下一章中,我们将对寻址方式的问题进行更深入地探讨。
之所以如此重视这个问题,是因为寻址方式的适当应用,使我们可以以更合理的结构来看待所要处理的数据。
而为所耍处理的看似杂乱的数据设计一种清晰的数据结构是程序设计的一个关键的问题。
- 545. 小结
- 546. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 547. 第8章 数据处理的两个基本问题8.1 bx、si、di、bp
8.2 机器指令处理的数据所在位置
8.3 汇编语言中数据位置的表达
8.4 寻址方式
8.5 指令要处理的数据有多长?
8.6 寻址方式的综合应用
8.7 div 指令
8.8 伪指令 dd
8.9 dup
- 548. 引言本章对前面的所有内容是具有总结性的。我们知道,计算机是进行数据处理、运算的机器,那么有两个基本的问题就包含在其中:(1)处理的数据在什么地方?(2)要处理的数据有多长?这两个问题,在机器指令中必须给以明确或隐含的说明,否则计算机就无法工作。
- 549. 引言本章中,我们就要针对8086CPU对这两个基本问题进行讨论。虽然讨论是在8086CPU的基础上进行的,但是这两个基本问题却是普遍的,对任何一个处理器都存在。
我们定义的描述性符号:reg和sreg
- 550. 引言为了描述上的简洁,在以后的课程中,我们将使用两个描述性的符号 reg来表示一个寄存器,用sreg表示一个段寄存器。
reg的集合包括:ax、bx、cx、dx、ah、al、bh、bl、ch、cl、dh、dl、sp、bp、si、di;
sreg的集合包括:ds、ss、cs、es。
- 551. 8.1 bx、si、di、bp前三个寄存器我们己经用过了,现在我们进行一下总结。
(1)在8086CPU 中,只有这4个寄存器(bx、bp、si、di)可以用在“[…]” 中来进行内存单元的寻址。
正确的指令
错误的指令
- 552. 8.1 bx、si、di、bp正确的指令
mov ax,[bx]
mov ax,[bx+si]
mov ax,[bx+di]
mov ax,[bp]
mov ax,[bp+si]
mov ax,[bp+di]
- 553. 8.1 bx、si、di、bp错误的指令
mov ax,[cx]
mov ax,[ax]
mov ax,[dx]
mov ax,[ds]
- 554. 8.1 bx、si、di、bp(2)在“[…]” 中,这4个寄存器(bx、bp、si、di)可以单个出现,或只能以四种组合出现:
bx和si、bx和di、bp和si、bp和di
正确的指令
错误的指令
- 555. 8.1 bx、si、di、bp正确的指令
mov ax,[bx]
mov ax,[si]
mov ax,[di]
mov ax,[bp]
mov ax,[bx+si]
mov ax,[bx+di] mov ax,[bp+si]
mov ax,[bp+di]
mov ax,[bx+si+idata]
mov ax,[bx+di+idata]
mov ax,[bp+si+idata]
mov ax,[bp+di+idata]
- 556. 8.1 bx、si、di、bp错误的指令
mov ax,[bx+bp]
mov ax,[si+di]
- 557. 8.1 bx、si、di、bp(3)只要在[…]中使用寄存器bp,而指令中没有显性的给出段地址,段地址就默认在ss中。比如: mov ax,[bp] 含义: (ax)=((ss)*16+(bp))
mov ax,[bp+idata] 含义:(ax)=((ss)*16+(bp)+idata)
mov ax,[bp+si] 含义:(ax)=((ss)*16+(bp)+(si))
mov ax,[bp+si+idata] 含义:(ax)=((ss)*16+(bp)+(si)+idata)
- 558. 8.2 机器指令处理的数据所在位置绝大部分机器指令都是进行数据处理的指令,处理大致可分为三类:
读取、写入、运算
在机器指令这一层来讲,并不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。
- 559. 8.2 机器指令处理的数据所在位置指令在执行前,所要处理的数据可以在三个地方:
CPU内部、内存、端口
(端口我们将在后面的课程中进行讨论)
指令举例
- 560. 8.2 机器指令处理的数据所在位置指令举例:
- 561. 8.3 汇编语言中数据位置的表达在汇编语言中如何表达数据的位置?
汇编语言中用三个概念来表达数据的位置。
1、立即数(idata)
2、寄存器
3、段地址(SA)和偏移地址(EA)
- 562. 8.3 汇编语言中数据位置的表达1、立即数(idata)
对于直接包含在机器指令中的数据(执行前在cPu 的指令缓冲器中),在汇编语言中称为:立即数(idata ) ,在汇编指令中直接给出。例如:
mov ax,1
add bx,2000h
or bx,00010000b
mov al,’a’
- 563. 8.3 汇编语言中数据位置的表达1、立即数(idata)
mov ax,1
对应机器码:
B80100
执行结果:(ax) = 1
- 564. 8.3 汇编语言中数据位置的表达2、寄存器
指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。例如:
mov ax,bx
mov ds,ax
push bx
mov ds:[0],bx
push ds
mov ss,ax
mov sp,ax
- 565. 8.3 汇编语言中数据位置的表达2、寄存器
mov ax,bx
对应机器码:89D8
执行结果:(ax) = (bx)
- 566. 8.3 汇编语言中数据位置的表达3、段地址(SA)和偏移地址(EA)
指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中。
存放段地址的寄存器可以是默认的。
示例
存放段地址的寄存器也可以显性的给出。
示例
- 567. 8.3 汇编语言中数据位置的表达存放段地址的寄存器是默认的
示例:
mov ax,[0]
mov ax,[bx]
mov ax,[bx+8]
mov ax,[bx+si]
mov ax,[bx+si+8]
段地址默认在ds中
- 568. 8.3 汇编语言中数据位置的表达存放段地址的寄存器是默认的
示例(续):
mov ax,[bp]
mov ax,[bp+8]
mov ax,[bp+si]
mov ax,[bp+si+8]
段地址默认在ss中
- 569. 8.3 汇编语言中数据位置的表达显性的给出存放段地址的寄存器
示例
mov ax,ds:[bp] 含义:(ax)=((ds)*16+(bp))
mov ax,es:[bx] 含义:(ax)=((es)*16+(bx))
mov ax,ss:[bx+si] 含义:(ax)=((ss)*16+(bx)+(si))
mov ax,cs:[bx+si+8] 含义:(ax)=((cs)*16+(bx)+(si)+8)
- 570. 8.3 汇编语言中数据位置的表达3、段地址(SA)和偏移地址(EA)
mov ax,[bx]
对应机器码:8B07
执行结果: (ax) = ( (ds)×16+(bx) )
- 571. 8.4 寻址方式当数据存放在内存中的时候,我们可以用多种方式来给定这个内存单元的偏移地址,这种定位内存单元的方法一般被称为寻址方式。
8086CPU有多种寻址方式,我们在前面的课程中都已经用到了,这里我们进行一下总结。
- 572. 8.4 寻址方式
- 573. 8.4 寻址方式寻址方式
演示1、直接寻址
演示2、寄存器间接寻址
演示3、寄存器相对寻址
演示4、基址变址寻址
演示5、相对基址变址寻址
- 574. (本页无文本内容)
- 575. (本页无文本内容)
- 576. (本页无文本内容)
- 577. (本页无文本内容)
- 578. (本页无文本内容)
- 579. 8.5 指令要处理的数据有多长?8086CPU的指令,可以处理两种尺寸的数据,byte和word。所以在机器指令中要指明,指令进行的是字操作还是字节操作。
- 580. 8.5 指令要处理的数据有多长?对于这个问题,汇编语言中用以下方法处理。
(1)通过寄存器名指明要处理的数据的尺寸。
(2)在没有寄存器名存在的情况下,用操作符X ptr指明内存单元的长度,X在汇编指令中可以为word或byte。
(3)其他方法
- 581. 8.5 指令要处理的数据有多长?下面的指令中,寄存器指明了指令进行的是字操作:
mov ax,1
mov bx,ds:[0]
mov ds,ax
mov ds:[0],ax
inc ax
add ax,1000
- 582. 8.5 指令要处理的数据有多长?下面的指令中,寄存器指明了指令进行的是字节操作:
mov al,1
mov al,bl
mov al,ds:[0]
mov ds:[0],al
inc al
add al,100
- 583. 8.5 指令要处理的数据有多长?下面的指令中,用word ptr指明了指令访问的内存单元是一个字单元:
mov word ptr ds:[0],1
inc word ptr [bx]
inc word ptr ds:[0]
add word ptr [bx],2
- 584. 8.5 指令要处理的数据有多长?下面的指令中,用byte ptr指明了指令访问的内存单元是一个字节单元:
mov byte ptr ds:[0],1
inc byte ptr [bx]
inc byte ptr ds:[0]
add byte ptr [bx],2
- 585. 8.5 指令要处理的数据有多长?在没有寄存器参与的内存单元访问指令中,用word ptr或byte ptr显性地指明所要访问的内存单元的长度是很必要的。
否则,CPU无法得知所要访问的单元是字单元,还是字节单元。
- 586. 8.5 指令要处理的数据有多长?假设我们用Debug查看内存的结果如下:
2000:1000 FF FF FF FF FF FF……
那么指令:
mov ax,2000H
mov ds,ax
mov byte ptr [1000H],1
将使内存中的内容变为:
2000:1000 01 FF FF FF FF FF……
- 587. 8.5 指令要处理的数据有多长?而指令:
mov ax,2000H
mov ds,ax
mov word ptr [1000H],1
将使内存中的内容变为:
2000:1000 01 00 FF FF FF FF……
为什么?
- 588. 8.5 指令要处理的数据有多长?这是因为
mov byte ptr [1000H],1访问的是地址为 ds:1000H的字节单元,修改的是 ds:1000H单元的内容;
而mov word ptr [1000H],1访问的是地址为 ds:1000H 的字单元,修改的是 ds:1000H和ds:1001H两个单元的内容。
- 589. 8.5 指令要处理的数据有多长?有些指令默认了访问的是字单元还是字节单元,
比如:push [1000H]就不用指明访问的是字单元还是字节单元,
因为push指令只进行字操作。
- 590. 8.6 寻址方式的综合应用下面我们通过一个问题来进一步讨论各种寻址方式的作用。
实际应用
- 591. 8.6 寻址方式的综合应用关于DEC公司的一条记录(1982年):
公司名称:DEC
总裁姓名:Ken Olsen
排 名:137
收 入:40
著名产品:PDP
1988年DEC公司的信息有了变化:
1、Ken Olsen 在富翁榜上的排名已升至38位;
2、DEC的收入增加了70亿美元;
3、该公司的著名产品已变为VAX系列计算机。
任务:编程修改内存中的过时数据。
- 592. 8.6 寻址方式的综合应用首先,我们应该分析一下要修改的数据:
(1)(DEC公司记录)的(排名字段)
(2)(DEC公司记录)的(收入字段)
(3)(DEC公司记录)的(产品字段)的(第一个字符)、(第二个字符)、(第三个字符)
- 593. 8.6 寻址方式的综合应用从要修改的内容,我们就可以逐步地确定修改的方法:
(1)我们要访问的数据是DEC公司的记录,所以,首先要确定DEC公司记录的位置:R=seg:60 确定了公司记录的位置后,我们下面就进一步确定要访问的内容在记录中的位置。
(2)确定排名字段在记录中的位置:0CH。
(3)修改R+0CH处的数据。
(4)确定收入字段在记录中的位置:0EH。
(5)修改R+0EH处的数据。
- 594. 8.6 寻址方式的综合应用从要修改的内容,我们就可以逐步地确定修改的方法: (续)
(6)确定产品字段在记录中的位置:10H。要修改的产品字段是一个字符串(或一个数组),需要访问字符串中的每一个字符。所以我们要进一步确定每一个字符在字符串中的位置。
(7)确定第一个字符在产品字段中的位置:P=0。
(8)修改R+10H+P处的数:P=P+1。
(9)修改R+10H+P处的数据: P=P+1。
(10)修改R+10H+P处的数据。
- 595. 8.6 寻址方式的综合应用根据上面的分析,程序如下: mov ax,seg
mov ds,ax
mov bx,60h
mov word ptr [bx+0ch],38
add word ptr [bx+0eh],70
mov si,0
mov byte ptr [bx+10h+si],’V’
inc si
mov byte ptr [bx+10h+si],’A’
inc si
mov byte ptr [bx+10h+si],’X’
;确定记录地址:ds:bx
;排名字段改为38
;收入字段增加70
;用si来定位产品字符串中的字符
- 596. 8.6 寻址方式的综合应用如果读者熟悉C语言的话,我们可以用C语言来描述这个程序,大致应该是这样的:C语言描述
我们再按照C语言的风格,用汇编语言写一下这个程序,注意和C语言相关语句的比对:汇编语言描述
- 597. 8.6 寻址方式的综合应用
- 598. 8.6 寻址方式的综合应用
- 599. 8.6 寻址方式的综合应用我们可以看到,8086CPU提供的如[bx+si+idata]的寻址方式为结构化数据的处理提供了方便。
使得我们可以在编程的时候,从结构化的角度去看待所要处理的数据。
从上面我们可以看到,一个结构化的数据包含了多个数据项,而数据项的类型又不相同,有的是字型数据,有的是字节型数据,有的是数组(字符串)。
- 600. 8.6 寻址方式的综合应用一般来说,我们可以用[bx+idata+si]的方式来访问结构体中的数据。
用bx定位整个结构体,用idata定位结构体中的某一个数据项,用 si 定位数组项中的每个元素 。
为此,汇编语言提供了更为贴切的书写方式。
如:[bx].idata、[bx].idata[si]。
- 601. 8.6 寻址方式的综合应用在C语言程序中我们看到,如:dec.cp[i],dec是一个变量名,指明了结构体变量的地址,cp 是一个名称,指明了数据项cp的地址,而i用来定位cp中的每一个字符。
汇编语言中的做法是:bx.10h[si]
看一下,是不是很相似?
- 602. 8.7 div 指令div是除法指令,使用div作除法的时候:
除数:8位或16位,在寄存器或内存单元中
被除数:(默认)放在AX 或 DX和AX中
结果:运算 8位 16位
商 AL AX
余数 AH DX
- 603. 8.7 div 指令div指令格式:
div reg
div 内存单元
现在我们可以用多种方法来表示一个内存单元了。
div指令示例
- 604. 8.7 div 指令div指令示例
div byte ptr ds:[0]
含义为:(al)=(ax)/((ds)*16+0)的商;
(al)=(ax)/((ds)*16+0)的余数
- 605. 8.7 div 指令div指令示例(续)
div word ptr es:[0]
含义为:(ax)=[(dx)*10000H+(ax)]/((ds)*16+0)的商;
(dx)=[(dx)*10000H+(ax)]/((ds)*16+0)的余数
- 606. 8.7 div 指令div指令示例(续)
div byte ptr [bx+si+8]
含义为:
(al)= (ax)/((ds)*16+(bx)+(si)+8)的商;
(ah)=(ax)/((ds)*16+(bx)+(si)+8)的余数
- 607. 8.7 div 指令div指令示例(续)
div word ptr [bx+si+8]
含义为:(ax)=[(dx)*10000H+(ax)]/((ds)*16+(bx)+(si)+8)的商;
(dx)=[(dx)*10000H+(ax)]/((ds)*16+(bx)+(si)+8)的余数
- 608. 8.7 div 指令编程:
利用除法指令计算100001/100。
我们首先分析一下,被除数 100001 大于65535,不能用ax寄存器存放,所以我们要用dx和ax两个寄存器联合存放100001,也就是说要进行16位的除法。
除数100小于255,可以在一个 8位寄存器中存放,但是,因为被除数是32位的,除数应为16位,所以要用一个16位寄存器来存放除数100。
- 609. 8.7 div 指令编程:
利用除法指令计算100001/100。(分析续)
因为要分别为dx和ax赋100001的高16位值和低16位值,所以应先将100001表示为十六进制形式:186A1H。
程序如下
- 610. 8.7 div 指令编程:
利用除法指令计算100001/100。(程序)
mov dx,1
mov ax,86A1H ;(dx)*10000H+(ax)=100001
mov bx,100
div bx
程序执行后,(ax)=03E8H(即1000),(dx)=1(余数为1)。
读者可自行在Debug中实践。
- 611. 8.7 div 指令编程:
利用除法指令计算1001/100。
我们首先分析一下被除数1001可用 ax寄存器存放,除数100可用 8位寄存器存放,也就是说,要进行8位的除法。程序如下:
mov ax,1001
mov bl,100
div bl
程序执行后,(al)=0AH(即10),(ah)=1(余数为1)。
读者可自行在Debug中实践。
- 612. 8.8 伪指令 dd前面我们用db和dw定义字节型数据和字型数据。
dd是用来定义dword (double word双字)型数据的。
示例
- 613. 8.8 伪指令 dd示例:data segment
db 1
dw 1
dd 1
data ends
在data段中定义了三个数据:
第一个数据为01H,在data:0处,占1个字节;
第二个数据为0001H,在data:1处,占1个字;
第三个数据为00000001H,在data:3处,占2个字节;
- 614. 8.8 伪指令 dd问题8.1
用div 计算data段中第一个数据除以第二个数据后的结果,商存放在第3个数据的存储单元中。
data segment
dd 100001
dw 100
dw 0
data ends
思考后看分析。
- 615. 8.8 伪指令 dd问题8.1分析
data segment
dd 100001
dw 100
dw 0
data ends
data段中的第一个数据是被除数,为dword(双字)型,32位,所以在做除法之前,用dx和ax存储。应将data:0字单元中的低16位存储在 ax中,data:2字单元中的高16位存储在dx中。
程序代码
- 616. 8.8 伪指令 dd问题8.1程序代码
mov ax,data
mov ds,ax
mov ax,ds:[0] ;ds:0字单元中的低16位存储在ax中
mov dx,ds:[2] ;ds:2字单元中的高16位存储在dx中
div word ptr ds:[4];用dx:ax中的32位数据除以ds:4字
;单元中的数据
mov ds:[6],ax ;将商存储在ds:6字单元中
- 617. 8.9 dupdup是一个操作符,在汇编语言中同db、dw、dd 等一样,也是由编译器识别处理的符号。
它是和db、dw、dd 等数据定义伪指令配合使用的,用来进行数据的重复。
示例
- 618. 8.9 dupdup示例
db 3 dup (0)
定义了3个字节,它们的值都是0,
相当于 db 0,0,0
- 619. 8.9 dupdup示例
db 3 dup (0,1,2)
定义了9个字节,它们是
0、1、2、0、1、2、0、1、2,
相当于 db 0,1,2,0,1,2,0,1,2
- 620. 8.9 dupdup示例
db 3 dup (‘abc’,’ABC’)
定义了18个字节,它们是
‘abcABCabcABCabcABC’,
相当于db ‘abcABCabcABCabcABC’
- 621. 8.9 dup可见,dup的使用格式如下:
db 重复的次数 dup (重复的字节型数据)
dw 重复的次数 dup (重复的字型数据)
dd 重复的次数 dup (重复的双字数据)
- 622. 8.9 dupdup是一个十分有用的操作符
比如我们要定义一个容量为 200 个字节的栈段,如果不用dup,则必须用这样的格式:
stack segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends
- 623. 8.9 dup当然,读者可以用dd,使程序变得简短一些,但是如果要求定义一个容量为1000字节或10000字节的呢?
如果没有dup,定义部分的程序就变得太长了;
有了dup就可以轻松解决。如下:
stack segment
db 200 dup (0)
stack ends
- 624. 小结
- 625. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 626. 第9章 转移指令的原理9.1 操作符offset
9.2 jmp指令
9.3 依据位移进行转移的jmp指令
9.4 转移的目的地址在指令中的jmp指令
9.5 转移地址在寄存器中的jmp指令
9.6 转移地址在内存中的jmp指令
9.7 jcxz指令
9.8 loop指令
9.9 根据位移进行转移的意义
9.10 编译器对转移位移超界的检测
- 627. 引言8086CPU的转移指令分为以下几类:
无条件转移指令 (如:jmp)
条件转移指令
循环指令(如:loop)
过程
中断
- 628. 9.1 操作符offset操作符offset在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址。
比如下面的程序:
assume cs:codesg
codeseg segment
start:mov ax,offset start ; 相当于 mov ax,0
s:mov ax,offset s ; 相当于mov ax,3
codesg ends
end start
- 629. 9.1 操作符offset问题9.1
有如下程序段,添写2条指令,使该程序在运行中将s处的一条指令复制到s0处。
思考后看分析。 assume cs:codesg
codesg segment
s: mov ax,bx ;(nop的机器码占一个字节)
mov si,offset s
mov di,offset s0
__________
__________
s0: nop ;(nop的机器码占一个字节)
nop
codesg ends
ends
- 630. 9.1 操作符offset问题9.1分析
(1)s和s0处的指令所在的内存单元的地址是多少?
cs:offset s 和cs:offset s0。
(2)将s处的指令复制到s0处,就是将cs:offeet s 处的数据复制到cs:offset s0处;
- 631. 9.1 操作符offset问题9.1分析(续)
(3)段地址已知在cs中,偏移地址offset s和offset s0已经送入si和di中;
(4)要复制的数据有多长?
mov ax,bx指令的长度为两个字节,即1个字。
完整程序
- 632. 9.1 操作符offset assume cs:codesg
codesg segment
s: mov ax,bx ;(mov ax,bx 的机器码占两个字节)
mov si,offset s
mov di,offset s0
mov ax,cs:[si]
mov cs:[di],ax
s0: nop ;(nop的机器码占一个字节)
nop
codesg ends
ends
问题9.1完整程序:
- 633. 9.2 jmp指令jmp为无条件转移,可以只修改IP,也可以同时修改CS和IP;
jmp指令要给出两种信息:
转移的目的地址
转移的距离(段间转移、段内短转移,段内近转移)
- 634. 9.3 依据位移进行转移的jmp指令jmp short 标号(转到标号处执行指令)
这种格式的 jmp 指令实现的是段内短转移,它对IP的修改范围为 -128~127,也就是说,它向前转移时可以最多越过128个字节,向后转移可以最多越过127个字节。
- 635. 9.3 依据位移进行转移的jmp指令比如:程序9.1
assume cs:codesg
codesg segment
start:mov ax,0
jmp short s
add ax,1
s:inc ax
codesg ends
end start左面的程序执行后, ax中的值为 1 ,因为执行 jmp short s 后 ,越过了add ax,1 ,IP 指向了标号 s处的 inc ax。也就是说,程序只进行了一次ax加1操作。
- 636. 9.3 依据位移进行转移的jmp指令汇编指令jmp short s 对应的机器指令应该是什么样的呢?
我们先看一下别的汇编指令和其对应的机器指令,(示例)
现在我们在Debug中将程序9.1翻译成为机器码,看看结果
- 637. 9.3 依据位移进行转移的jmp指令汇编指令与机器码的对应示例
可以看到,在一般的汇编指令中,汇编指令中的idata(立即数),不论它是表示一个数据还是内存单元的偏移地址,都会在对应的机器指令中出现,因为CPU执行的是机器指令,它必须要处理这些数据或地址。
- 638. 9.3 依据位移进行转移的jmp指令对照汇编源程序,我们可以看到,Debug 将 jmp short s 中的 s 表示为inc ax 指令的偏移地址 8 ,并将jmp short s 表示为 jmp 0008 ,表示转移到cs:0008处。
- 639. 9.3 依据位移进行转移的jmp指令这一切似乎合理,可是当我们查看jmp short s或jmp 0008所对应的机器码,去发现了问题。
- 640. 9.3 依据位移进行转移的jmp指令jmp 0008 ( Debug 中的表示)或jmp short s (汇编语言中的表示)所对应的机器码为EB 03,注意,这个机器码中竟不包含转移的目的地址。
这意味着,CPU 在执行EB 03的时候,并不知道转移目的地址。
- 641. 9.3 依据位移进行转移的jmp指令那么,CPU根据什么进行转移呢?
没有了目的地址,CPU如何知道转移到哪里呢?
- 642. 9.3 依据位移进行转移的jmp指令令人奇怪的是,汇编指令jmp short s中,明明是带有转移的目的地址(由标号 s 表示)的,可翻译成机器指令后,怎么目的地址就没了呢?
- 643. 9.3 依据位移进行转移的jmp指令没有了目的地址,CPU如何知道转移到哪里?
我们把程序9.1改写一下,变成这样:
程序9.2
- 644. 9.3 依据位移进行转移的jmp指令程序9.2
assume cs:codesg
codesg segment
start:mov ax,0
mov bx,0
jmp short s
add ax,1
s:inc ax
codesg ends
end start
我们在Debug中将程序9.2翻译为机器码,看看结果
- 645. 9.3 依据位移进行转移的jmp指令比较一下程序1和2用Debug查看的结果
- 646. 9.3 依据位移进行转移的jmp指令注意,两个程序中的 jmp指令都要使IP 指向 inc ax 指令,但是程序 1 的 inc ax 指令的偏移地址为 0008 ,而程序 2 的 inc ax 指令的偏移地址为000BH。
我们再来看两个程序中的jmp指令所对应的机器码,都是EB 03。
- 647. 9.3 依据位移进行转移的jmp指令这说明CPU在指令jmp指令的时候并不需要转移的目的地址。
两个程序中的jmp指令的转移目的地址并不一样,一个是cs:0008,另一个是cs:000B。
如果机器指令中包含了转移的目的地址的话,那么它们对应的机器码应该是不同的。
- 648. 9.3 依据位移进行转移的jmp指令可是它们对应的机器码都是 EB 03,这说明在机器指令中并不包含转移
的目的地址。
如果机器指令中不包含目的地址的话,那么,也就是说 CPU不需要这个目的地址就可以实现对IP的修改。
- 649. 9.3 依据位移进行转移的jmp指令这种信息是什么呢?
我们一步步地分析。
我们先简单回忆一下CPU执行指令的过程(如果你需要更多地回忆,可以复习一下2.10节的内容)。
- 650. 9.3 依据位移进行转移的jmp指令CPU执行指令的过程:
(1)从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲区;
(2)(IP) = (IP)+所读取指令的长度,从而指向下一条指令;
(3)执行指令。转到1,重复这个过程。
- 651. 9.3 依据位移进行转移的jmp指令按照这个步骤,我们参照程序9.2的图看一下:
jmp short s指令的读取和执行过程
- 652. 9.3 依据位移进行转移的jmp指令jmp short s指令的读取和执行过程:
(1)(CS)=0BBDH,(IP)=0006,CS:IP指向EB 03(jmp short s的机器码);
(2)读取指令码EB 03进入指令缓冲器;
(3)(IP)=(IP)+所读取指令的长度=(IP)+2=0008,CS:IP指向add ax,1;
(4)CPU指行指令缓冲器中的指令EB 03;
(5)指令EB 03执行后,(IP)=000BH,CS:IP指向inc ax。
- 653. 9.3 依据位移进行转移的jmp指令从上面的过程中我们看到,CPU 将指令EB 03 读入后,IP 指向了下一条指令,即 CS:0008 处的add ax,1,接着执行EB 03。
如果 EB 03 没有对 IP 进行修改的话,那么,接下来 CPU将执行 add ax,1。
- 654. 9.3 依据位移进行转移的jmp指令可是,CPU 执行的 EB 03确是一条修改IP的转移指令,执行后 (IP) = 000BH ,CS:IP指向inc ax,CS:0008处的add ax,1没有被执行。
CPU在执行EB 03的时候是根据什么修改的 IP,使其指向目标指令呢?就是根据指令码中的03。
- 655. 9.3 依据位移进行转移的jmp指令注意,要转移的目的地址是CS:000B,而CPU 执行 EB 03时,当前的(IP)=0008,如果将当前的IP值加3,使(IP)=000BH,CS:IP就可以指向目标指令。
在转移指令EB 03中并没有告诉CPU要转移的目的地址,却告诉了 CPU 要转移的位移,即将当前的IP向后移动3个字节。
- 656. 9.3 依据位移进行转移的jmp指令因为程序1、2中的jmp 指令转移的位移相同,都是向后 3 个字节,所以它们的机器码都是EB 03。
原来如此,在“jmp short 标号”指令所对应的机器码中,并不包含转移的目的地址,而包含的是转移的位移。
这个位移,使编译器根据汇编指令中的“标号”计算出来的,
- 657. 9.3 依据位移进行转移的jmp指令转移位移具体的计算方法如下图:
- 658. 9.3 依据位移进行转移的jmp指令结论:
CPU执行 jmp short 标号 指令时并不需要转移的目的地址,只需要知道转移的位移就行了。
具体过程演示
- 659. (本页无文本内容)
- 660. 9.3 依据位移进行转移的jmp指令实际上,指令“jmp short 标号”的功能为(IP)=(IP)+8位位移。
(1)8位位移=“标号”处的地址-jmp指令后的第一个字节的地址;
(2)short指明此处的位移为8位位移;
(3)8位位移的范围为-128~127,用补码表示
(如果你对补码还不了解,请阅读附注2)
(4)8位位移由编译程序在编译时算出。
- 661. 9.3 依据位移进行转移的jmp指令还有一种和指令“jmp short 标号”功能相近的指令格式:
jmp near ptr 标号
它实现的时段内近转移。
指令“jmp near ptr 标号”的功能为:
(IP)=(IP)+16位位移。
- 662. 9.3 依据位移进行转移的jmp指令指令“jmp near ptr 标号”的说明:
(1)16位位移=“标号”处的地址-jmp指令后的第一个字节的地址;
(2)near ptr指明此处的位移为16位位移,进行的是段内近转移;
(3)16位位移的范围为
-32769~32767,用补码表示;
(4)16位位移由编译程序在编译时算出。
- 663. 9.4 转移的目的地址在指令中的jmp指令前面讲的jmp指令,其对应的机器码中并没有转移的目的地址,而是相对于当前IP的转移位移。
指令 “jmp far ptr 标号”
实现的是段间转移,又称为远转移。
- 664. 9.4 转移的目的地址在指令中的jmp指令指令 “jmp far ptr 标号” 功能如下:
(CS)=标号所在段的段地址;
(IP)=标号所在段中的偏移地址。
far ptr指明了指令用标号的段地址和偏移地址修改CS和IP。
我们看下面的程序
- 665. 9.4 转移的目的地址在指令中的jmp指令程序9.3:
assume cs:codesg
codesg segment
start:mov ax,0
mov bx,0
jmp far ptr s
db 256 dup (0)
s: add ax,1
inc ax
codesg ends
end start
- 666. 9.4 转移的目的地址在指令中的jmp指令我们在Debug中将程序9.3翻译成为机器码,看到的结果如图:
- 667. 如图中所示:
源程序中的db 256 dup (0),被Debug解释为相应的若干条汇编指令 。这不是关键,关键是,我们要注意一下jmp far ptr s所对应的机器码:EA 0B 01 BD 0B ,其中包含转移的目的地址。9.4 转移的目的地址在指令中的jmp指令
- 668. “0B 01 BD 0B” 是目的地址在指令中的存储顺序,高地址的“BD 0B”是转移的段地址:0BBDH,低地址的“0B 01” 是偏移地址:010BH。
对于“jmp X 标号”格式的指令的深入分析请参看附注3。9.4 转移的目的地址在指令中的jmp指令
- 669. 9.5 转移地址在寄存器中的jmp指令指令格式:jmp 16位寄存器
功能:IP =(16位寄存器)
这种指令我们在前面的课程(参见2.11节)中已经讲过,这里就不再详述。
- 670. 9.6 转移地址在内存中的jmp指令转移地址在内存中的jmp指令有两种格式:
(1) jmp word ptr 内存单元地址(段内转移)
功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。
内存单元地址可用寻址方式的任一格式给出。
示例
- 671. 9.6 转移地址在内存中的jmp指令(1) jmp word ptr 内存单元地址(段内转移)
示例:
mov ax,0123H
mov [bx],ax
jmp word ptr [bx]
执行后,(IP)=0123Hmov ax,0123H
mov ds:[0],ax
jmp word ptr ds:[0]
执行后,(IP)=0123H
- 672. 9.6 转移地址在内存中的jmp指令转移地址在内存中的jmp指令的第二种格式:
(2) jmp dword ptr 内存单元地址(段间转移)
功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。
(CS)=(内存单元地址+2)
(IP)=(内存单元地址)
内存单元地址可用寻址方式的任一格式给出。
示例
- 673. 9.6 转移地址在内存中的jmp指令(2) jmp dword ptr 内存单元地址(段间转移)
示例: mov ax,0123H
mov [bx],ax
mov word ptr [bx+2],0
jmp dword ptr [bx]
执行后,
(CS)=0
(IP)=0123H
CS:IP指向0000:0123。mov ax,0123H
mov ds:[0],ax
mov word ptr ds:[2],0
jmp dword ptr ds:[0]
执行后,
(CS)=0
(IP)=0123H
CS:IP指向0000:0123。
- 674. 特别提示检测点9.1(p170)
没有完成此检测点,请不要向下进行。
- 675. 9.7 jcxz指令 jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为-128~127。
指令格式:jcxz 标号
(如果(cx)=0,则转移到标号处执行。)
操作
- 676. 9.7 jcxz指令jcxz 标号 指令操作:
当(cx)=0时,(IP)=(IP)+8位位移)
8位位移=“标号”处的地址-jcxz指令后的第一个字节的地址;
8位位移的范围为-128~127,用补码表示;
8位位移由编译程序在编译时算出。
当(cx)=0时,什么也不做(程序向下执行)。
- 677. 9.7 jcxz指令我们从 jcxz的功能中可以看出,指令“jcxz 标号”的功能相当于:
if((cx)==0)jmp short 标号;
(这种用C语言和汇编语言进行的综合描述,或许能使你对有条件指令理解得更加清楚。)
- 678. 特别提示检测点9.2(p171)
没有完成此检测点,请不要向下进行。
- 679. 9.8 loop指令loop指令为循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为-128~127。
指令格式:loop 标号
((cx))=(cx)-1,如果(cx)≠0,转移到标号处执行。
操作
- 680. 9.8 loop指令loop 标号 指令操作:
(1)(cx)=(cx)-1;
(2)如果(cx)≠0,(IP)=(IP)+8位位移。
8位位移=“标号”处的地址-loop指令后的第一个字节的地址;
8位位移的范围为-128~127,用补码表示;
8位位移由编译程序在编译时算出。
当(cx)=0,什么也不做(程序向下执行)。
- 681. 9.8 loop指令我们从loop的功能中可以看出,指令“loop 标号”的功能相当于:
(cx)--;
if((cx)≠0)jmpshort 标号
- 682. 特别提示检测点9.3(p172)
没有完成此检测点,请不要向下进行。
- 683. 9.9 根据位移进行转移的意义前面我们讲到:
jmp short 标号
jmp near ptr 标号
jcxz 标号
loop 标号
等几种汇编指令,它们对 IP的修改是根据转移目的地址和转移起始地址之间的位移来进行的。在它们对应的机器码中不包含转移的目的地址,而包含的食道目的地址的位移。
这样设计,方便了程序段在内存中的浮动装配。
- 684. 9.9 根据位移进行转移的意义例如:
这段程序装在内存中的不同位置都可正确执行,因为 loop s 在执行时只涉及到 s的位移( - 4,前移 4个字节,补码表示为FCH),而不是s的地址。
- 685. 9.9 根据位移进行转移的意义如果loop s的机器码中包含的是s的地址,则就对程序段在内存中的偏移地址有了严格的限制;
因为机器码中包含的是 s 的地址,如果 s 处的指令不在目的地址处,程序的执行就会出错。
- 686. 9.9 根据位移进行转移的意义而loop s的机器码中包含的是转移的位移,就不存在这个问题了;
因为,无论 s处的指令的实际地址是多少,loop指令的转移位移是不变的。
- 687. 9.10 编译器对转移位移超界的检测注意,根据位移进行转移的指令,它们的转移范围受到转移位移的限制,如果在源程序中出现了转移范围超界的问题,在编译的时候,编译器将报错。
示例
- 688. 9.10 编译器对转移位移超界的检测示例
下面的程序将引起编译错误:
jmp short s的转移范围是-128~127,IP最多向后移动127个字节。 assume cs:code
code segment
start: jmp short s
db 128 dup(0)
s: mov ax,0ffffh
code ends
end start
- 689. 9.10 编译器对转移位移超界的检测注意:
我们在第2章中讲到的形如
“jmp 2000:0100”的转移指令,是在 Debug 中使用的汇编指令,汇编编译器并不认识。
如果在源程序中使用,编译时也会报错。
- 690. 小结
- 691. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 692. 第10章 call 和 ret 指令10.1 ret 和 retf
10.2 call 指令
10.3 依据位移进行转移的call指令
10.4 转移的目的地址在指令中的call指令
10.5 转移地址在寄存器中的call指令
10.6 转移地址在内存中的call指令
10.7 call 和 ret 的配合使用
10.8 mul 指令
10.9 模块化程序设计
10.10 参数和结果传递的问题
10.11 批量数据的传递
10.12 寄存器冲突的问题
- 693. 引言call和ret 指令都是转移指令,它们都修改IP,或同时修改CS和IP。
它们经常被共同用来实现自程序的设计。
这一章,我们讲解call和ret 指令的原理。
- 694. 10.1 ret 和 retfret指令用栈中的数据,修改IP的内容,从而实现近转移;
操作
retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移;
操作
- 695. 10.1 ret 和 retfCPU执行ret指令时,进行下面两步操作:
(1)(IP)=((ss)*16+(sp))
(2)(sp)=(sp)+2
- 696. 10.1 ret 和 retfCPU执行retf指令时,进行下面两步操作:
(1)(IP)=((ss)*16+(sp))
(2)(sp)=(sp)+2
(3)(CS)=((ss)*16+(sp))
(4)(sp)=(sp)+2
- 697. 10.1 ret 和 retf可以看出,如果我们用汇编语法来解释ret和retf指令,则:
CPU执行ret指令时,相当于进行:
pop IP
CPU执行retf指令时,相当于进行:
pop IP
pop CS
- 698. 10.1 ret 和 retf示例程序
ret指令
程序中ret指令执行后,(IP)=0,CS:IP指向代码段的第一条指令。
retf指令
程序中retf指令执行后,CS:IP指向代码段的第一条指令。
- 699. 特别提示检测点10.1(p179)
没有完成此检测点,请不要向下进行。
- 700. 10.2 call 指令CPU执行call指令,进行两步操作:
(1)将当前的 IP 或 CS和IP 压入栈中;
(2)转移。
call 指令不能实现短转移,除此之外,call指令实现转移的方法和 jmp 指令的原理相同,下面的几个小节中 ,我们以给出转移目的地址的不同方法为主线,讲解call指令的主要应用格式。
- 701. 10.3 依据位移进行转移的call指令call 标号(将当前的 IP 压栈后,转到标号处执行指令)
CPU执行此种格式的call指令时,进行如下的操作:
(1) (sp) = (sp) – 2
((ss)*16+(sp)) = (IP)
(2) (IP) = (IP) + 16位位移
- 702. 10.3 依据位移进行转移的call指令call 标号
16位位移=“标号”处的地址-call指令后的第一个字节的地址;
16位位移的范围为 -32768~32767,用补码表示;
16位位移由编译程序在编译时算出。
演示
- 703. (本页无文本内容)
- 704. 10.3 依据位移进行转移的call指令从上面的描述中,可以看出,如果我们用汇编语法来解释此种格式的 call指令,则:
CPU 执行指令“call 标号”时,相当于进行:
push IP
jmp near ptr 标号
- 705. 特别提示检测点10.2(p181)
没有完成此检测点,请不要向下进行。
- 706. 10.4 转移的目的地址在指令中的call指令前面讲解的call指令,其对应的机器指令中并没有转移的目的地址 ,而是相对于当前IP的转移位移。
指令“call far ptr 标号”实现的是段间转移。
操作
- 707. 10.4 转移的目的地址在指令中的call指令CPU执行“call far ptr 标号”这种格式的call指令时的操作:
(1) (sp) = (sp) – 2
((ss) ×16+(sp)) = (CS)
(sp) = (sp) – 2
((ss) ×16+(sp)) = (IP)
(2) (CS) = 标号所在的段地址
(IP) = 标号所在的偏移地址
- 708. 10.4 转移的目的地址在指令中的call指令从上面的描述中可以看出,如果我们用汇编语法来解释此种格式的 call 指令,则:
CPU 执行指令 “call far ptr 标号” 时,相当于进行:
push CS
push IP
jmp far ptr 标号
- 709. 特别提示检测点10.3(p181)
没有完成此检测点,请不要向下进行。
- 710. 10.5 转移地址在寄存器中的call指令指令格式:call 16位寄存器
功能:
(sp) = (sp) – 2
((ss)*16+(sp)) = (IP)
(IP) = (16位寄存器)
- 711. 10.5 转移地址在寄存器中的call指令指令格式:call 16位寄存器
汇编语法解释此种格式的 call 指令,CPU执行call 16位reg时,相当于进行:
push IP
jmp 16位寄存器
- 712. 特别提示检测点10.4(p182)
没有完成此检测点,请不要向下进行。
- 713. 10.6 转移地址在内存中的call指令转移地址在内存中的call指令有两种格式:
(1) call word ptr 内存单元地址
(2) call dword ptr 内存单元地址
- 714. 10.6 转移地址在内存中的call指令(1) call word ptr 内存单元地址
汇编语法解释:
push IP
jmp word ptr 内存单元地址
示例
- 715. 10.6 转移地址在内存中的call指令(1) call word ptr 内存单元地址(示例)
比如下面的指令:
mov sp,10h
mov ax,0123h
mov ds:[0],ax
call word ptr ds:[0]
执行后,(IP)=0123H,(sp)=0EH
- 716. 10.6 转移地址在内存中的call指令(2) call dword ptr 内存单元地址
汇编语法解释:
push CS
push IP
jmp dword ptr 内存单元地址
示例
- 717. 10.6 转移地址在内存中的call指令(2) call dword ptr 内存单元地址(示例)
比如,下面的指令:
mov sp,10h
mov ax,0123h
mov ds:[0],ax
mov word ptr ds:[2],0
call dword ptr ds:[0]
执行后,(CS)=0,(IP)=0123H,(sp)=0CH
- 718. 特别提示检测点10.5(p183)
没有完成此检测点,请不要向下进行。
- 719. 10.7 call 和 ret 的配合使用前面,我们已经分别学习了 ret 和call指令的原理。现在我们看一下,如何将它们配合使用来实现子程序的机制。
问题10.1
- 720. 10.7 call 和 ret 的配合使用assume cs:code
code segment
start: mov ax,1
mov cx,3
call s
mov bx,ax ;(bx) = ?
mov ax,4c00h
int 21h
s: add ax,ax
loop s
ret
code ends
end start问题10.1
右面程序返回前,bx中的值是多少?
思考后看分析。
- 721. 10.7 call 和 ret 的配合使用问题10.1分析
我们来看一下 CPU 执行这个程序的主要过程:
(1)CPU 将call s指令的机器码读入,IP指向了call s后的指令mov bx,ax,然后CPU执行call s指令,将当前的 IP值(指令mov bx,ax的偏移地址)压栈,并将 IP 的值改变为标号 s处的偏移地址;
(2)CPU从标号 s 处开始执行指令,loop循环完毕,(ax)=8;
- 722. 10.7 call 和 ret 的配合使用问题10.1分析(续)
我们来看一下CPU执行这个程序的主要过程:
(3)CPU将ret指令的机器码读入,IP指向了ret 指令后的内存单元,然后CPU 执行 ret 指令 ,从栈中弹出一个值(即 call 先前压入的mov bx,ax 指令的偏移地址)送入 IP 中。则CS:IP指向指令mov bx,ax;
(4)CPU从 mov bx,ax 开始执行指令,直至完成。
- 723. 10.7 call 和 ret 的配合使用问题10.1分析(续)
程序返回前,(bx)=8 。我们可以看出,从标号s 到ret的程序段的作用是计算2的N次方,计算前,N的值由CX提供。
我们再来看下面的程序
- 724. 10.7 call 和 ret 的配合使用
- 725. 10.7 call 和 ret 的配合使用我们看一下程序的主要执行过程:
(1)前三条指令执行后,栈的情况如下:
- 726. 10.7 call 和 ret 的配合使用程序的主要执行过程:
(2)call 指令读入后,(IP) =000EH,CPU指令缓冲器中的代码为 B8 05 00;
CPU执行B8 05 00,首先,栈中的情况变为:
然后,(IP)=(IP)+0005=0013H。
- 727. 10.7 call 和 ret 的配合使用程序的主要执行过程:
(3)CPU从cs:0013H处(即标号s处)开始执行。
(4)ret指令读入后:(IP)=0016H,CPU指令缓冲器中的代码为 C3;CPU执行C3,相当于进行pop IP,执行后,栈中的情况为:
(IP)=000EH;
- 728. 10.7 call 和 ret 的配合使用程序的主要执行过程:
(5)CPU回到 cs:000EH处(即call指令后面的指令处)继续执行。
从上面的讨论中我们发现,可以写一个具有一定功能的程序段,我们称其为子程序,在需要的时候,用call指令转去执行。
- 729. 10.7 call 和 ret 的配合使用可是执行完子程序后,如何让CPU接着call指令向下执行?
call指令转去执行子程序之前,call指令后面的指令的地址将存储在栈中,所以可以在子程序的后面使用 ret 指令,用栈中的数据设置IP的值,从而转到 call 指令后面的代码处继续执行。
- 730. 10.7 call 和 ret 的配合使用这样,我们可以利用call和ret来实现子程序的机制。
子程序的框架
- 731. 10.7 call 和 ret 的配合使用子程序的框架:
标号:
指令
ret具有子程序的源程序的框架:
- 732. 10.7 call 和 ret 的配合使用现在,可以从子程序的角度,回过头来再看一下本节中的两个程序。
- 733. 10.8 mul 指令因下面要用到,我们介绍一下mul指令,mul是乘法指令,使用 mul 做乘法的时候:
(1)相乘的两个数:要么都是8位,要么都是16位。
8 位: AL中和 8位寄存器或内存字节单元中;
16 位: AX中和 16 位寄存器或内存字单元中。
- 734. 10.8 mul 指令使用mul座乘法的时候:
(2)结果
8位:AX中;
16位:DX(高位)和AX(低位)中。
格式如下:
mul reg
mul 内存单元
- 735. 10.8 mul 指令内存单元可以用不同的寻址方式给出,比如:
mul byte ptr ds:[0]
含义为: (ax)=(al)*((ds)*16+0);
mul word ptr [bx+si+8]
含义为:
(ax)=(al)*((ds)*16+(bx)+(si)+8)结果的低16位;
(dx)=(al)*((ds)*16+(bx)+(si)+8)结果的高16位;
- 736. 10.8 mul 指令例如:
(1)计算100*10
100和10小于255,可以做8位乘法,程序如下:
mov al,100
mov bl,10
mul bl
结果: (ax)=1000(03E8H)
- 737. 10.8 mul 指令例如:
(1)计算100*10000
100小于255,可10000大于255,所以必须做16位乘法,程序如下:
mov ax,100
mov bx,10000
mul bx
结果: (ax)=4240H,(dx)=000FH
(F4240H=1000000)
- 738. 10.9 模块化程序设计从上面我们看到 ,call 与 ret 指令共同支持了汇编语言编程中的模块化设计。在实际编程中,程序的模块化是必不可少的。
因为现实的问题比较复杂,对现实问题进行分析时,把它转化成为相互联系、不同层次的子问题,是必须的解决方法。
- 739. 10.9 模块化程序设计而call和ret 指令对这种分析方法提供了程序实现上的支持。利用 call和ret指令,我们可以用简洁的方法,实现多个互相联系、功能独立的子程序来解决一个复杂的问题。
下面的内容中,我们来看一下子程序设计中的相关问题和解决方法。
- 740. 10.10 参数和结果传递的问题子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者。
其实,我们讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值。
- 741. 10.10 参数和结果传递的问题我们设计一个子程序,可以根据提供的N,来计算N的3次方。
这里有两个问题:
(1)我们将参数N存储在什么地方?
(2)计算得到的数值,我们存储在什么地方?
- 742. 10.10 参数和结果传递的问题很显然,我们可以用寄存器来存储,可以将参数放到 bx 中 ;因为子程序中要计算 N×N×N ,可以使用多个 mul 指令,为了方便,可将结果放到 dx 和 ax中。
子程序
- 743. 10.10 参数和结果传递的问题子程序:
说明:计算N的3次方
参数: (bx)=N
结果: (dx:ax)=N∧3
cube:mov ax,bx
mul bx
mul bx
ret
- 744. 10.10 参数和结果传递的问题注意,我们在编程的时候要注意良好的风格,对于程序应有详细的注释。子程序的注释信息应该包含对子程序的功能、参数和结果的说明。
因为今天写的子程序,以后可能还会用到;自己写的子程序,也很可能要给别人使用,所以一定要有全面的说明。
- 745. 10.10 参数和结果传递的问题用寄存器来存储参数和结果是最常使用的方法。对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:
调用者将参数送入参数寄存器,从结果寄存器中取到返回值;
子程序从参数寄存器中取到参数,将返回值送入结果寄存器。
- 746. 10.10 参数和结果传递的问题编程:计算data段中第一组数据的 3 次方,结果保存在后面一组dword单元中。
assume cs:code
data segment
dw 1,2,3,4,5,6,7,8
dd 0,0,0,0,0,0,0,0
data ends
我们可以用到已经写好的子程序
程序代码
- 747. 10.11 批量数据的传递前面的例程中,子程序 cube 只有一个参数,放在bx中。如果有两个参数,那么可以用两个寄存器来放,可是如果需要传递的数据有3个、4个或更多直至 N个,我们怎样存放呢?
寄存器的数量终究有限,我们不可能简单地用寄存器来存放多个需要传递的数据。对于返回值,也有同样的问题。
- 748. 10.11 批量数据的传递在这种时候,我们将批量数据放到内存中,然后将它们所在内存空间的首地址放在寄存器中,传递给需要的子程序。
对于具有批量数据的返回结果,也可用同样的方法。
- 749. 10.11 批量数据的传递我们看一个例子,设计子程序
功能:将一个全是字母的字符串转化为大写。
分析
子程序
- 750. 10.11 批量数据的传递将一个全是字母的字符串转化为大写。
分析
这个子程序需要知道两件事,字符串的内容和字符串的长度。因为字符串中的字母可能很多,所以我们不便将整个字符串中的所有字母都直接传递给子程序。
- 751. 10.11 批量数据的传递将一个全是字母的字符串转化为大写。
分析(续)
但是,我们可以将字符串在内存中的首地址放在寄存器中传递给子程序。因为子程序中要用到循环,我们可以用 loop 指令,而循环的次数恰恰就是字符串的长度。出于方便的考虑,可以将字符串的长度放到cx中。
- 752. 10.11 批量数据的传递编程:将data段中的字符串转化为大写。
assume cs:code
data segment
db 'conversation'
data ends
code segment
start: ……
code ends
end start
- 753. 10.11 批量数据的传递编程:将data段中的字符串转化为大写。
源程序代码
注意:除了寄存器传递参数外,还有一种通用的方法使用栈来传递参数。关于这种技巧请参看附注4。
- 754. 10.12 寄存器冲突的问题设计一个子程序:
功能:将一个全是字母,以0结尾的字符串,转化为大写。
程序要处理的字符串以0作为结尾符,这个字符串可以如下定义:
db ‘conversation’,0
- 755. 10.12 寄存器冲突的问题设计一个子程序:
功能:将一个全是字母,以0结尾的字符串,转化为大写。
分析
应用这个子程序 ,字符串的内容后面定要有一个0,标记字符串的结束。子程序可以依次读取每个字符进行检测,如果不是0,就进行大写的转化,如果是0,就结束处理。
由于可通过检测0而知道是否己经处理完整个字符串 ,所以子程序可以不需要字符串的长度作为参数。我们可以用jcxz来检测0。
- 756. 10.12 寄存器冲突的问题子程序设计:
说明:将一个全是字母,以0结尾的字符串,转化为大写。
参数:ds:si指向字符串的首地址;
结果:没有返回值。
子程序代码
我们来看一下这个子程序的应用
- 757. 10.12 寄存器冲突的问题子程序代码
- 758. 10.12 寄存器冲突的问题程序的应用
(1)将data段中字符串转化为大写
assume cs:code
data segment
db 'conversation',0
data ends
代码段中相关程序段如下:
mov ax,data
mov ds,ax
mov si,0
call capital
- 759. 10.12 寄存器冲突的问题子程序的应用
(2)将data段中字符串全部转化为大写
assume cs:code
data segment
db ‘word',0
db ‘unix',0
db ‘wind',0
db ‘good',0
data ends
可以看到,所有字符串的长度都是5(算上结尾符 0 ),我们使用循环 ,重复调用子程序capital完成对4个字符串的处理。
完整的程序代码
- 760. 10.12 寄存器冲突的问题问题10.2
前面的程序在思想上完全正确,但在细节上却有些错误,把错误找出来。
思考后看分析。
- 761. 10.12 寄存器冲突的问题问题10.2分析
问题在于cx的使用,主程序要使用cx记录循环次数,可是子程序中也使用了cx,在执行子程序的时候,cx中保存的循环计数值被改变,使得主程序的循环出错。
从上而的问题中,实际上引出了个一般化的问题:子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的冲突。
- 762. 10.12 寄存器冲突的问题问题10.2分析(续)
那么我们如何来避免这种冲突呢 ?粗略地看,我们可以有两个方案:
(1)在编写调用子程序的程序时 ,注意看看子程序中有没有用到会产生冲突的寄存器,如果有,调用者使用别的寄存器;
(2)在编写子程序的时候,不要使用会产生冲突的寄存器。
- 763. 10.12 寄存器冲突的问题问题10.2分析(续)
我们来分析一下上面两个方案的可行性:
(1) 这将给调用子程序的程序的编写造成很大的麻烦,因为必须要小心检杳所调用的子程序中是否有将产生冲突的寄存器。
比如说,在上面的例子中,我们在编写主程序的循环的时候就得检查子程序巾是否用到了bx和cx,因为如果子程序中用到了这两个寄存器就会出现问题。
如果我们采用这种方案来解决冲突的话,那么在主程序的循环中,就小能使用cx寄存器,因为子程序中己经用到。
- 764. 10.12 寄存器冲突的问题问题10.2分析(续)
分析一下上面两个方案的可行性:
(2) 这个方案是不可能实现的,因为编写子程序的时候无法知道将来的调用情况。
可见,我们上面所设想的两个方案都不可行。
- 765. 10.12 寄存器冲突的问题问题10.2分析(续)
我们希望:
(1)编写调用了程序的程序的时候不必关心子程序到底使用了哪些寄存器;
(2)编写了程序的时候不必关心调用者使用了哪些寄存器;
(3)不会发生寄存器冲突。
- 766. 10.12 寄存器冲突的问题问题10.2分析(续)
解决这个问题的简捷方法是,在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复。我们可以用栈来保存寄存器中的内容。
- 767. 10.12 寄存器冲突的问题问题10.2分析(续)
以后,我们编写子程序的标准框架如下:
子程序开始:子程序中使用的寄存器入栈
子程序内容
子程序使用的寄存器出栈
返回(ret、retf)
我们改进一下子程序capital的设计
- 768. 10.12 寄存器冲突的问题问题10.2改进的子程序capital的设计capital: push cx
push si
change: mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change
ok: pop si
pop cx
ret
要注意寄存器入栈和出栈的顺序。
- 769. 小结
- 770. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 771. 第11章 标志寄存器 11.1 ZF标志
11.2 PF标志
11.3 SF标志
11.4 CF标志
11.5 OF标志
11.6 adc指令
11.7 sbb指令
11.8 cmp指令
11.9 检测比较结果的条件转移指令
11.10 DF标志和串传送指令
11.11 pushf和popf
11.12 标志寄存器在Debug中的表示
- 772. 引言8086CPU的标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW)。
我们己经使用过8086CPU的ax、bx、cx、dx、si、di、bp、sp、ip、cs、ss、ds、es等13个寄存器了。
本章中的标志寄存器(以下简称为flag)是我们要学习的最后一个寄存器。
- 773. 引言flag 和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义。
而flag寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。
- 774. 引言8086CPU的flag寄存器的结构:
flag的1、3、5、12、13、14、15位在8086CPU中没有使用,不具有任何含义。而0、2、4、6、7、8、9、10、11位都具有特殊的含义。
- 775. 引言在这一章中,我们学习标志寄存器中的CF、PF、ZF、SF、OF、DF 标志位,和一些与其相关的典型指令。
- 776. 11.1 ZF标志flag的第6位是ZF,零标志位。
它记录相关指令执行后,
结果为0 ,ZF = 1
结果不为0,ZF = 0
示例
- 777. 11.1 ZF标志比如:
mov ax,1
sub ax,1
指令执行后,结果为0,则ZF = 1。
mov ax,2
sub ax,1
指令执行后,结果为1,则ZF = 0。
- 778. 11.1 ZF标志对于ZF的值,我们可以这样来看,ZF标记相关指令的计算结果是否为0,如果为0,则在ZF要记录下“是0”这样的肯定信息。
在计算机中1 表示逻辑真,表示肯定,所以当结果为0的时候 ZF=1,表示“结果是0 ”。如果结果不为0,则ZF要记录下“不是0”这样的否定信息。
在计算机中0表示逻辑假,表示否定,所以当结果不为0 的时候ZF=0,表示“结果不是0”。
示例
- 779. 11.1 ZF标志示例
指令:mov ax,1
and ax,0
执行后,结果为0,则ZF=1,表示“结果是0”。
指令:mov ax,1
or ax,0
执行后,结果不为0,则ZF=0,表示“结果非0”。
- 780. 11.1 ZF标志注意:
在8086CPU的指令集中,有的指令的执行是影响标志寄存器的,比如:add、sub、mul、div、inc、or、and等,它们大都是运算指令(进行逻辑或算术运算);
有的指令的执行对标志寄存器没有影响,比如:mov、push、pop等,它们大都是传送指令。
- 781. 11.1 ZF标志注意:
我们在使用一条指令的时候,要注意这条指令的全部功能,其中包括,执行结果对标记寄存器的哪些标志位造成影响。
- 782. 11.2 PF标志flag的第2位是PF,奇偶标志位。
它记录指令执行后,结果的所有二进制位中1的个数:
为偶数,PF = 1;
为奇数,PF = 0。
示例
- 783. 11.2 PF标志示例
指令:mov al,1
add al,10
执行后,结果为00001011B,其中有3(奇数)个1,则PF=0;
指令:mov al,1
or al,10
执行后,结果为00000011B,其中有2(偶数)个1,则PF=1;
- 784. 11.3 SF标志flag的第7位是SF,符号标志位。
它记录指令执行后,
结果为负,SF = 1;
结果为正,SF = 0。
有符号数与补码
示例
mov al,10000001B
add al,1
结果: (al)=10000010B
- 785. 有符号数与补码我们知道计算机中通常用补码来表示有符号数据。计算机中的一个数据可以看作是有符号数,也可以看成是无符号数。
比如:
00000001B ,可以看作为无符号数 1 ,或有符号数+1;
10000001B ,可以看作为无符号数129,也可以看作有符号数-127。
- 786. 有符号数与补码这也就是说,对于同一个二进制数据,计算机可以将它当作无符号数据来运算,也可以当作有符号数据来运算。
- 787. 11.3 SF标志我们可以将add指令进行的运算当作无符号数的运算,那么add指令相当于计算129+1,结果为130(10000010B);
也可以将add指令进行的运算当作有符号数的运算,那么add指令相当于计算-127+1,结果为-126(10000010B)。
- 788. 11.3 SF标志不管我们如何看待,CPU 在执行add等指令的时候,就已经包含了两种含义,也将得到用同一种信息来记录的两种结果。
关键在于我们的程序需要哪一种结果。
- 789. 11.3 SF标志SF 标志,就是CPU对有符号数运算结果的一种记录 ,它记录数据的正负。
在我们将数据当作有符号数来运算的时候,可以通过它来得知结果的正负。
如果我们将数据当作无符号数来运算,SF的值则没有意义,虽然相关的指令影响了它的值。
- 790. 11.3 SF标志这也就是说,CPU在执行 add 等指令时,是必然要影响到SF标志位的值的。
至于我们需不需要这种影响,那就看我们如何看待指令所进行的运算了。
- 791. 11.3 SF标志比如:
mov al,10000001B add al,1
执行后,结果为10000010B,SF=1,
表示:如果指令进行的是有符号数运算,那么结果为负;
- 792. 11.3 SF标志再比如:
mov al,10000001B
add al,01111111B
执行后,结果为0,SF=0,
表示:如果指令进行的是有符号数运算,那么结果为非负。
- 793. 11.3 SF标志某此指令将影响标志寄存器中的多个标志位,这些被影响的标记位比较全面地记录了指令的执行结果,为相关的处理提供了所需的依据。
比如指令sub al,al执行后,ZF、PF、SF等标志位都要受到影响,它们分别为:1、1、0。
- 794. 特别提示检测点11.1(p205)
没有完成此检测点,请不要向下进行。
- 795. 11.4 CF标志flag的第0位是CF,进位标志位。
一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。
- 796. 11.4 CF标志对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N-1位,的最高有效位,而假想存在的第N位,就是相对于最高有效位的更高位。
- 797. 11.4 CF标志我们知道,当两个数据相加的时候,有可能产生从最高有效位向更高位的进位。
比如,两个8 位数据:98H+98H,将产生进位。
由于这个进位值在8位数中无法保存,我们在前面的课程中,就只是简单地说这个进位值丢失了。
- 798. 11.4 CF标志其实CPU在运算的时候,并不丢弃这个进位值,而是记录在一个特殊的寄存器的某一位上。
8086CPU 就用flag的CF位来记录这个进位值。
- 799. 11.4 CF标志比如,下面的指令:
mov al,98H
add al,al ;执行后: (al)=30H,CF=1,
;CF记录了最高有效位向更高位的进位值
add al,al ;执行后: (al)=30H,CF=1,
;CF记录了最高有效位向更高位的进位值
- 800. 11.4 CF标志而当两个数据做减法的时候,有可能向更高位借位。
比如,两个 8 位数据:97H-98H,将产生借位,借位后,相当于计算197H-98H。
而flag的CF位也可以用来记录这个借位值。
- 801. 11.4 CF标志比如,下面的指令:
mov al,98H
add al,al ;执行后: (al)=30H,CF=1,
;CF记录了最高有效位向更高位的进位值
add al,al ;执行后: (al)=30H,CF=1,
;CF记录了最高有效位向更高位的进位值
- 802. 11.5 OF标志我们先来谈谈溢出的问题。
在进行有符号数运算的时候,如结果超过了机器所能表示的范围称为溢出。
那么,什么是机器所能表示的范围呢?
- 803. 11.5 OF标志比如说,指令运算的结果用 8 位寄存器或内存单元来存放。
比如:add al,3 ,那么对于 8 位的有符号数据,机器所能表示的范围就是-128~127。
同理,对于16 位有符号数,机器所能表示的范围是-32768~32767。
- 804. 11.5 OF标志如果运算结果超出了机器所能表达的范围,将产生溢出。
注意,这里所讲的溢出,只是对有符号数运算而言。
下面我们看两个溢出的例子。
- 805. 11.5 OF标志示例指令
mov al,98 add al,99
执行后将产生溢出。
因为add al,99 进行的有符号数运算是:(al)=(al)+99=98+99=197
而结果197超出了机器所能表示的8位有符号数的范围:-128~127。
- 806. 11.5 OF标志示例指令:
mov al,0F0H ;0F0H,为有符号数-16的补码 add al,88H ;88H,为有符号数-120的补码
执行后将产生溢出。
因为add al,88H进行的有符号数运算是:
(al)=(al)+(-120)=(-16)+(-120)=-136
而结果-136超出了机器所能表示的8位有符号数的范围:-128~127。
- 807. 11.5 OF标志如果在进行有符号数运算时发生溢出,那么运算的结果将不正确。
就上面的两个例子来说: mov al,98 add al,99 add指令运算的结果是(al)=0C5H ,因为进行的是有符号数运算,所以 al中存储的是有符号数,而0C5H是有符号数-59的补码。
- 808. 11.5 OF标志如果我们用add 指令进行的是有符号数运算,则98+99=-59这样的结果让人无法接受。
造成这种情况的原因,就是实际的结果 197,作为一个有符号数,在 8 位寄存器al中存放不下。
- 809. 11.5 OF标志同样,对于:
mov al,0F0H ;0F0H,为有符号数-16的补码add al,88H ;88H,为有符号数-120的补码
add指令运算的结果是(al)=78H,
因为进行的是有符号数运算,所以 al中存储的是有符号数,而78H表示有符号数120。
- 810. 11.5 OF标志如果我们用add 指令进行的是有符号数运算,则-16-120=120这样的结果显然不正确。
造成这种情况的原因,就是实际的结果
-136,作为一个有符号数,在 8位寄存器al中存放不下。
- 811. 11.5 OF标志由于在进行有符号数运算时,可能发生溢出而造成结果的错误。
则CPU需要对指令执行后是否产生溢出进行记录。
flag的第11位是OF,溢出标志位。
- 812. 11.5 OF标志一般情况下,OF记录了有符号数运算的结果是否发生了溢出。
如果发生溢出,OF=1,
如果没有,OF=0。
- 813. 11.5 OF标志一定要注意CF和OF的区别:
CF是对无符号数运算有意义的标志位;
而OF是对有符号数运算有意义的标志位。
- 814. 11.5 OF标志比如:
mov al,98 add al,99
add指令执行后:CF=0,OF=1。
前面我们讲过,CPU在执行add等指令的时候,就包含了两种含义:无符号数运算和有符号数运算。
- 815. 11.5 OF标志对于无符号数运算,CPU用CF位来记录是否产生了进位;
对于有符号数运算,CPU 用 OF 位来记录是否产生了溢出,
当然,还要用SF位来记录结果的符号。
- 816. 11.5 OF标志对于无符号数运算,98+99没有进位,CF=0;
对于有符号数运算,98+99发生溢出,OF=1。
- 817. 11.5 OF标志 mov al,0F0H add al,88H add指令执行后:CF=1,OF=1。
对于无符号数运算,0F0H+88H有进位,CF=1;
对于有符号数运算,0F0H+88H发生溢出,OF=1。
- 818. 11.5 OF标志 mov al,0F0H add al,78H add指令执行后:CF=1,OF=0。
对于无符号运算,0F0H+78H有进位,CF=1;
对于有符号数运算,0F0H +78H不发生溢出,OF=0。
- 819. 11.5 OF标志我们可以看出,CF 和OF 所表示的进位和溢出,是分别对无符号数和有符号数运算而言的,它们之间没有任何关系。
- 820. 特别提示检测点11.2 (page207)
没有完成此检测点,请不要向下进行。
- 821. 11.6 adc指令adc是带进位加法指令 ,它利用了CF位上记录的进位值。
格式: adc 操作对象1,操作对象2
功能:
操作对象1=操作对象1+操作对象2+CF
比如:adc ax,bx 实现的功能是:
(ax)=(ax)+(bx)+CF
adc指令执行过程演示
- 822. (本页无文本内容)
- 823. 11.6 adc指令adc指令示例(一)
mov ax,2 mov bx,1 sub bx,ax adc ax,l
执行后,(ax)=4。
adc执行时,相当于计算: (ax)+1+CF=2+1+1=4。
- 824. 11.6 adc指令adc指令示例(二)
mov ax,1 add ax,ax adc ax,3
执行后,(ax)=5。
adc执行时,相当于计算: (ax)+3+CF=2+3+0=5。
- 825. 11.6 adc指令adc指令示例(三)
mov al,98H add al,aladc al,3
执行后,(ax)=34H。
adc执行时,相当于计算: (ax)+3+CF=30H+3+1=34H。
- 826. 11.6 adc指令可以看出,adc指令比add指令多加了一个CF位的值。
为什么要加上CF的值呢?
CPU为什么要提供这样一条指令呢?
我们先来看一下CF的值的含义。
- 827. 11.6 adc指令在执行 adc 指令的时候加上的 CF 的值的含义,由 adc指令前面的指令决定的,也就是说,关键在于所加上的CF值是被什么指令设置的。
显然,如果CF 的值是被sub指令设置的,那么它的含义就是借位值;如果是被add指令设置的,那么它的含义就是进位值。
- 828. 11.6 adc指令我们来看一下两个数据:0198H和0183H如何相加的:
01 98 + 01 83 1
----------------- 03 1B
可以看出,加法可以分两步来进行:
(1)低位相加;
(2)高位相加再加上低位相加产生的进位值。
- 829. 11.6 adc指令下面的指令和add ax , bx具有相同的结果: add al,bl adc ah,bh
看来CPU提供 adc 指令的目的,就是来进行加法的第二步运算的。
adc指令和add指令相配合就可以对更大的数据进行加法运算。
- 830. 11.6 adc指令编程计算1EF000H+201000H,结果放在ax(高16位)和bx(低16位)中。
分析
- 831. 11.6 adc指令分析:
因为两个数据的位数都大于16,用add 指令无法进行计算。我们将计算分两步进行,先将低16位相加,然后将高 16 位和进位值相加。
程序如下
- 832. 11.6 adc指令程序:
mov ax,001EH mov bx,0F000H add bx,1000H adc ax,0020H
adc 指令执行后,也可能产生进位值,所以也会对CF位进行设置。
- 833. 11.6 adc指令由于有这样的功能,我们就可以对任意大的数据进行加法运算。
看一个例子
- 834. 11.6 adc指令编程计算
1EF0001000H+2010001EF0H,结果放在ax(高16位),bx(次高16位),cx(低16位)中。
分析
- 835. 11.6 adc指令分析:
计算分3步进行:
(1)先将低16位相加,完成后,CF 中记录本次相加的进位值;
(2)再将次高16位和 CF(来自低16位的进位值)相加,完成后,CF中记录本次相加的进位值;
(3)最后高16 位和CF(来自次高16位的进位值)相加,完成后,CF中记录本次相加的进位值。
- 836. 11.6 adc指令程序代码
mov ax,001EH
mov bx,0F000H
mov cx,1000H
add cx,1EF0H
adc bx,1000H
adc ax,0020H
- 837. 11.6 adc指令下面我们,编写一个子程序,对两个128位数据进行相加。
名称:add128
功能:两个128位数据进行相加
参数
- 838. 11.6 adc指令参数:
ds:si指向存储第一个数的内存空间,因数据为128位,所以需要8个字单元,由低地址单元到高地址单元依次存放 128位数据由低到高的各个字。运算结果存储在第一个数的存储空间中。
ds:di指向存储第二个数的内存空间
程序代码 思考
- 839. 11.6 adc指令思考:
inc和loop指令不影响CF位,上面的程序中,能不能将4个inc指令,用:
add si,2
add si,2
取代?
- 840. 11.7 sbb指令sbb是带错位减法指令,它利用了CF位上记录的借位值。
格式:sbb 操作对象1,操作对象2
功能:
操作对象1=操作对象1–操作对象2–CF
比如:sbb ax,bx
实现功能: (ax) = (ax) – (bx) – CF
- 841. 11.7 sbb指令sbb指令执行后,将对CF进行设置。
利用sbb指令我们可以对任意大的数据进行减法运算。
- 842. 11.7 sbb指令比如,计算003E100OH–00202000H,结果放在ax,bx中,程序如下:
mov bx,1000H
mov ax,003EH
sub bx,2000H
sub ax,0020H
- 843. 11.7 sbb指令sbb和adc是基于同样的思想设计的两条指令,在应用思路上和adc类似。在这里,我们就不再进行过多的讨论。
通过学习这两条指令,我们可以进一步领会一下标志寄存器CF位的作用和意义。
- 844. 11.8 cmp指令cmp 是比较指令,功能相当于减法指令,只是不保存结果。
cmp 指令执行后,将对标志寄存器产生影响。
其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。
- 845. 11.8 cmp指令cmp指令
格式:cmp 操作对象1,操作对象2
功能:计算操作对象1–操作对象2 但并不保存结果,仅仅根据计算结果对标志寄存器进行设置。
- 846. 11.8 cmp指令比如:cmp ax,ax
做(ax)–(ax)的运算,结果为0,但并不在ax中保存,仅影响flag的相关各位。
指令执行后:
ZF=1,
PF=1,
SF=0,
CF=0,
OF=0。
- 847. 11.8 cmp指令下面的指令:
mov ax,8
mov bx,3
cmp ax,bx
执行后: (ax)=8,
ZF=0,
PF=1,
SF=0,
CF=0,
OF=0。
- 848. 11.8 cmp指令其实,我们通过cmp 指令执行后,相关标志位的值就可以看出比较的结果。
cmp ax,bx
- 849. 11.8 cmp指令现在我们可以看出比较指令的设计思路,即:通过做减法运算,影响标志寄存器,标志寄存器的相关位记录了比较的结果。
反过来看上面的例子 cmp ax,ax
- 850. 11.8 cmp指令指令 cmp ax,bx 的逻辑含意是比较ax和bx中的值,如果执行后:
- 851. 11.8 cmp指令同 add、sub 指令一样,CPU 在执行cmp指令的时候,也包含两种含义:
进行无符号数运算和进行有符号数运算。
所以利用cmp指令可以对无符号数进行比较,也可以对有符号数进行比较。
- 852. 11.8 cmp指令上面所讲的是用 cmp 进行无符号数比较时,相关标志位对比较结果的记录。
下面我们再来看一下如果用cmp来进行有符号数比较时,CPU用哪些标志位对比较结果进行记录。
- 853. 11.8 cmp指令我们以cmp ah,bh为例进行说明:
cmp ah,bh
如果(ah)=(bh) 则(ah)-(bh)=0,所以:ZF=1;
如果(ah)≠(bh) 则(ah)-(bh) ≠0,所以:ZF=0;
所以,我们根据cmp指令执行后ZF的值,就可以知道两个数据是否相等。
- 854. 11.8 cmp指令我们继续看,如果(ah)<(bh)则可能发生什么情况呢?
对于有符号数运算,在 (ah)<(bh) 情况下,(ah)-(bh)显然可能引起SF=1,即结果为负。
- 855. 11.8 cmp指令比如:
(ah)=1,(bh)=2:
则(ah)-(bh)=0FFH,0FFH 为 -1 的补码,因为结果为负,所以SF=1。
(ah)=0FEH,(bx)=0FFH:
则(ah)-(bh)=2-(-1)=0FFH,因为结果为负,所以SF=1。
- 856. 11.8 cmp指令通过上面的例子,我们是不是可以得到这样的结论:
cmp 操作对象1,操作对象2 指令执行后,SF=1,就说明操作对象1<操作对象2?
当然不是。
我们再看两个例子。
- 857. 11.8 cmp指令 (ah)=22H,(bh)=0A0H:
则(ah)-(bh)=34-(-96)=82H,82H是-126的补码,所以SF=1。
这里虽然SF=1,但是并不能说明(ah)<(bh),因为显然34>-96。
- 858. 11.8 cmp指令两个有符号数A 和B 相减,得到的是负数,那么可以肯定A
- 859. 11.8 cmp指令我们可以根据指令执行后,相关标志位的值来判断比较的结果。
单纯地考察SF 的值不可能知道结果的正负。因为SF 记录的只是可以在计算机中存放的相应位数的结果的正负。
比如add ah, al执行后,SF记录的是ah中的8位二进制信息所表示的数据的正负。
- 860. 11.8 cmp指令cmp ah,bh执行后,SF记录的是(ah) -(bh)所得到的8位结果数据的正负;
虽然这个结果没有在我们能够使用的寄存器或内存单元中保存,但是在指令执行的过程中,它暂存在CPU 内部的暂存器中。
- 861. 11.8 cmp指令所得到的相应结果的正负,并不能说明,运算所应该得到的结果的正负。
这是因为在运算的过程中可能发生溢出。
如果有这样的情况发生,那么,SF的值就不能说明任何问题。
- 862. 11.8 cmp指令比如: mov ah,22H mov bh,0A0H sub ah,bh
结果SF=1,运算实际得到的结果是(ah)=82H,但是在逻辑上,运算所应该得到的结果是:34-(-96)=130。
- 863. 11.8 cmp指令就是因为130 这个结果作为一个有符号数超出了-128~127这个范围,在ah 中不能表示,而ah中的结果被CPU当作有符号数解释为-126。
而SF被用来记录这个实际结果的正负,所以SF=1。
但SF=1不能说明在逻辑上运算所得的正确结果的正负。
- 864. 11.8 cmp指令又比如: mov ah,0A0H mov bh,0CBH cmp ah,bh
结果SF=1,运算 (ah)-(bh) 实际得到的结果是D5H,但是在逻辑上,运算所应该得到的结果是:160-(-53)=213。
- 865. 11.8 cmp指令但是逻辑上的结果的正负,才是cmp指令所求的真正结果,因为我们就是要靠它得到两个操作对象的比较信息。
所以cmp 指令所作的比较结果,不是仅仅靠SF就能记录的,因为它只能记录实际结果的正负。
- 866. 11.8 cmp指令我们考虑一下 ,两种结果之间的关系,实际结果的正负,和逻辑上真正结果的正负,它们之间有多大的距离呢?
从上面的分析中,我们知道,实际结果的正负,之所以不能说明逻辑上真正结果的正负,关键的原因在于发生了溢出。
- 867. 11.8 cmp指令如果没有溢出发生的话,那么,实际结果的正负和逻辑上真正结果的正负就一致了。
所以,我们应该在考察SF(得知实际结果的正负)的同时考察OF(得知有没有溢出),就可以得知逻辑上真正结果的正负,同时就可以知道比较的结果。
- 868. 11.8 cmp指令下面,我们以cmp ah,bh为例,总结一下CPU执行cmp指令后,SF和OF的值是如何来说明比较的结果的。
- 869. 11.8 cmp指令(1)如果SF=1,而OF=0
OF=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负;
因SF=1,实际结果为负,所以逻辑上真正的结果为负,所以(ah)<(bh)。
- 870. 11.8 cmp指令(2)如果SF=1,而OF=1
OF=1 ,说明有溢出,逻辑上真正结果的正负≠实际结果的正负;
因 SF=1 ,实际结果为负,实际结果为负,而又有溢出,这说明是由于溢出导致了实际结果为负,
简单分析一下,就可以看出,如果因为溢出导致了实际结果为负,那么逻辑上真正的结果必然为正。这样,SF=1,OF = 1 ,说明了(ah)>(bh)。
- 871. 11.8 cmp指令(3)如果SF=0,而OF=1
OF=1 ,说明有溢出,逻辑上真正结果的正负≠实际结果的正负;
因SF=0,实际结果非负,而OF=1说明有溢出,则结果非 0 ,所以,实际结果为正。实际结果为正,而又有溢出,这说明是由于溢出导致了实际结果非负,
简单分析一下,就可以看出,如果因为溢出导致了实际结果为正,那么逻辑上真正的结果必然为负。这样,SF=0,OF = 1 ,说明了(ah)<(bh)。
- 872. 11.8 cmp指令(4)如果SF=0,而OF=0
OF=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负;
因SF=0,实际结果非负,所以逻辑上真正的结果必然非负。所以(ah)≥(bh)。
- 873. 11.8 cmp指令上面,我们深入讨论了cmp指令在进行有符号数和无符号数比较时,对flag 相关标志位的影响,和CPU如何通过相关的标志位来表示比较的结果。
在学习中,要注意领会8086CPU这种工作机制的设计思想。实际上,这种设计思想对于各种处理机来说是普遍的。
- 874. 11.8 cmp指令下面的内容中我们将学习一些根据cmp指令的比较结果(即,cmp指令执行后,相关标志位的值)进行工作的指令。
- 875. 11.9 检测比较结果的条件转移指令“转移”指的是它能够修改IP,而“条件”指的是它可以根据某种条件 ,决定是否修改IP。
比如:jcxz就是一个条件转移指令,它可以检测 cx 中的数值,如果(cx)=0,就修改IP,否则什么也不做。
所有条件转移指令的转移位移都是
[-128,127]。
- 876. 11.9 检测比较结果的条件转移指令除了 jcxz 之外,CPU还提供了其他条件转移指令,大多数条件转移指令都检测标志寄存器的相关标志位,根据检测的结果来决定是否修改IP。
- 877. 11.9 检测比较结果的条件转移指令它们检测的是哪些标志位呢?
就是被cmp指令影响的那些,表示比较结果的标志位。
这些条件转移指令通常都和cmp相配合使用,就好像 call 和 ret 指令通常相配合使用一样。
- 878. 11.9 检测比较结果的条件转移指令因为 cmp 指令可以同时进行两种比较,无符号数比较和有符号数比较,所以根据 cmp 指令的比较结果进行转移的指令也分为两种,即:
根据无符号数的比较结果进行转移的条件转移指令,它们检测ZF、CF的值;
和根据有符号数的比较结果进行转移的条件转移指令,它们检测 SF、OF和 ZF的值。
- 879. 11.9 检测比较结果的条件转移指令我们看一下根据无符号数的比较结果进行转移的条件转移指令。
下表是常用的根据无符号数的比较结果进行转移的条件转移指令。
- 880. 11.9 检测比较结果的条件转移指令这些指令比较常用,它们都很好记忆,它们的第一个字母都是j,表示jump;后面的:
e:表示equal;
ne:表示not equal;
b:表示below;
nb:表示not below;
a:表示above;
na:表示not above。
- 881. 11.9 检测比较结果的条件转移指令注意观察一下它们所检测的标志位,都是cmp指令进行无符号数比较时候,记录比较结果的标志位。
比如je,检测 ZF位,当 ZF=1的时候进行转移,如果在 je 前面使用了 cmp 指令,那么je对ZF的检测,实际上就是间接地检测cmp的比较结果是否为两数相等。
- 882. 11.9 检测比较结果的条件转移指令编程实现如下功能:
如果(ah)=(bh)则(ah)=(ah)+(ah),否则(ah)=(ah)+(bh)。
cmp ah,bh
je s
add ah,bh
jmp short ok
s: add ah,ah
ok: ret
- 883. 11.9 检测比较结果的条件转移指令上面的程序执行时,如果(ah)=(bh) ,则cmp ah,bh使ZF=1,而je检测ZF是否为1,如果为1,将转移到标号s 处执行指令add ah,ah。
这也可以说,cmp 比较 ah、bh 后所得到的相等的结果使得je 指令进行转移。从而很好地体现了 je指令的逻辑含义,相等则转移。
- 884. 11.9 检测比较结果的条件转移指令虽然 je 的逻辑含义是“相等则转移”,但它进行的操作是,ZF=1时则转移。
“相等则转移”这种逻辑含义,是通过和 cmp 指令配合使用来体现的,因为是cmp 指令为“ZF=1”赋予了“两数相等”的含义。
- 885. 11.9 检测比较结果的条件转移指令至于究竟在je之前使不使用cmp指令,在于我们的安排。
je检测的是ZF位置,不管je 前面是什么指令,只要CPU执行je指令时,ZF=1,那么就会发生转移。
比如
- 886. 11.9 检测比较结果的条件转移指令比如:
mov ax,0
add ax,0
je s
inc ax
s: inc ax
执行后,(ax)=1。add ax,0使得ZF=1,所以je指令将进行转移。
- 887. 11.9 检测比较结果的条件转移指令可在这个时候发生的转移确不带有“相等则转移”的含义。因为此处的je指令检测到的ZF=1 ,不是由cmp等比较指令设置的,而是由add指令设置的,并不具有“两数相等”的含义。
但无论“ZF=1” 的含义如何,是什么指令设置的,只要是ZF=1,就可以使得je指令发生转移。
- 888. 11.9 检测比较结果的条件转移指令CPU提供了cmp指令,也提供了 je 等条件转移指令,如果将它们配合使用,可以实现根据比较结果进行转移的功能。
但这只是“如果”,只是一种合理的建议,和事实上常用的方法。
但究竟是否配合使用它们,完全是你自己的事情。
这就好像,call和ret指令的关系一样。
- 889. 11.9 检测比较结果的条件转移指令对于jne、jb、jnb、ja、jna等指令和cmp指令配合使用的思想和je相同,可以自己分析一下。
虽然我们分别讨论了cmp指令和与其比较结果相关的有条件转移指令,但是它们经常在一起配合使用。
所以我们在联合应用它们的时候,不必再考虑cmp指令对相关标志位的影响和je 等指令对相关标志位的检测。
- 890. 11.9 检测比较结果的条件转移指令因为相关的标志位,只是为cmp和je等指令传递比较结果。
我们可以直接考虑cmp和je等指令配合使用时,表现出来的逻辑含义。
它们在联合使用的时候表现出来的功能有些像高级语言中的IF语句。
- 891. 11.9 检测比较结果的条件转移指令我们来看一组程序:
data段中的8个字节如下:
data segment
db 8,11,8,1,8,5,63,38
data ends
(1)编程:统计data段中数值为8的字节的个数,用ax保存统计结果。
(2)编程:统计data段中数值大于8的字节的个数,用ax保存统计结果。
(3)编程:统计data段中数值小于8的字节的个数,用ax保存统计结果。
- 892. 11.9 检测比较结果的条件转移指令(1)编程:统计data段中数值为8的字节的个数,用ax保存统计结果。
编程思路:初始设置(ax)=0,然后用循环依次比较每个字节的值,找到一个和8相等的数就将ax的值加1。
程序如下
另一种实现方式
- 893. 11.9 检测比较结果的条件转移指令 mov ax,data
mov ds,ax
mov bx,0 ;ds:bx指向第一个字节
mov ax,0 ;初始化累加器
mov cx,0
s: cmp byte ptr [bx],8 ;和8进行比较
jne next ;如果不相等转到next,继续循环
inc ax ;如果相等就将计数值加1
next: inc bx
loop s ;程序执行后: (ax)=3
- 894. 11.9 检测比较结果的条件转移指令 mov ax,data
mov ds,ax
mov bx,0 ;ds:bx指向第一个字节
mov ax,0 ;初始化累加器
mov cx,0
s: cmp byte ptr [bx],8 ;和8进行比较
je ok ;如果相等就转到ok,继续循环
jmp short next ;如果不相等就转到next,继续循环
ok: inc ax ;如果相等就将计数值加1
next: inc bx
loop s
- 895. 11.9 检测比较结果的条件转移指令比起第一个程序,它直接的遵循了“等于8则计数值加1”的原则,用je指令检测等于8的情况,但是没有第一个程序精简。
第一个程序用 jne 检测不等于 8 的情况 ,从而间接地检测等于 8 的情况。
要注意在使用 cmp 和条件转移指令时的这种编程思想。
- 896. 11.9 检测比较结果的条件转移指令(2)编程:统计data段中数值大于8的字节的个数,用ax保存统计结果。
编程思路:初始设置(ax)=0,然后用循环依次比较每个字节的值,找到一个大于8的数就将ax的值加1。
程序如下
- 897. 11.9 检测比较结果的条件转移指令 mov ax,data
mov ds,ax
mov bx,0 ;ds:bx指向第一个字节
mov ax,0 ;初始化累加器
mov cx,0
s: cmp byte ptr [bx],8 ;和8进行比较
jne next ;如果不大于8转到next,继续循环
inc ax ;如果大于8就将计数值加1
next: inc bx
loop s ;程序执行后: (ax)=3
- 898. 11.9 检测比较结果的条件转移指令(3)编程:统计data段中数值小于8的字节的个数,用ax保存统计结果。
编程思路:初始设置(ax)=0,然后用循环依次比较每个字节的值,找到一个小于8的数就将ax的值加1。
程序如下
- 899. 11.9 检测比较结果的条件转移指令 mov ax,data
mov ds,ax
mov bx,0 ;ds:bx指向第一个字节
mov ax,0 ;初始化累加器
mov cx,0
s: cmp byte ptr [bx],8 ;和8进行比较
jnb next ;如果不小于8转到next,继续循环
inc ax ;如果小于8就将计数值加1
next: inc bx
loop s ;程序执行后: (ax)=2
- 900. 11.9 检测比较结果的条件转移指令上面讲解了根据无符号数的比较结果进行转移的条件转移指令。
根据有符号数的比较结果进行转移的条件转移指令的工作原理和无符号的相同,只是检测了不同的标志位。
- 901. 11.9 检测比较结果的条件转移指令我们在这里主要探讨的是cmp、标志寄存器的相关位、条件转移指令三者配合应用的原理,这个原理具有普遍性,而不是逐条讲解条件转移指令。
对这些指令感兴趣的学习者可以查看相关的指令手册。
- 902. 特别提示检测点11.3(p219)
没有完成此检测点,请不要向下进行。
- 903. 11.10 DF标志和串传送指令flag的第10位是DF,方向标志位。
在串处理指令中,控制每次操作后si,di的增减。
DF = 0:每次操作后si,di递增;
DF = 1:每次操作后si,di递减。
- 904. 11.10 DF标志和串传送指令格式1: movsb
功能:(以字节为单位传送)
(1) ((es)×16 + (di)) = ((ds) ×16 + (si))
(2) 如果DF = 0则: (si) = (si) + 1
(di) = (di) + 1
如果DF = 0则:(si) = (si) - 1
(di) = (di) - 1
- 905. 11.10 DF标志和串传送指令我们可以用汇编语法描述movsb的功能如下:
mov es:[di],byte ptr ds:[si];8086 并不支持这样的指令,这里只是个描述。
如果DF=0:inc si
inc di
如果DF=1:dec si
dec di
- 906. 11.10 DF标志和串传送指令可以看出,movsb 的功能是将 ds:si 指向的内存单元中的字节送入 es:di中,然后根据标志寄存器DF位的值,将 si和di递增或递减。
当然,也可以传送一个字, movsw指令
- 907. 11.10 DF标志和串传送指令格式2:movsw
功能:(以字为单位传送)
将 ds:si指向的内存字单元中word送入es:di中,然后根据标志寄存器DF位的值,将si和di递增2或递减2。
- 908. 11.10 DF标志和串传送指令我们可以用汇编语法描述movsw的功能如下:
mov es:[di],word ptr ds:[si];8086 并不支持这样的指令,这里只是个描述。
如果DF=0:add si,2
add di,2
如果DF=1:sub si,2
sub di,2
- 909. 11.10 DF标志和串传送指令movsb和movsw进行的是串传送操作中的一个步骤,一般来说,movsb 和 movsw 都和rep配合使用,格式如下:
rep movsb 用汇编语法来描述rep movsb的功能就是: s : movsb loop s
rep movsw用汇编语法来描述rep movsw的功能就是: s : movsw loop s
- 910. 11.10 DF标志和串传送指令可见,rep的作用是根据cx的值,重复执行后面的串传送指令。
由于每执行一次movsb指令si和di都会递增或递减指向后一个单元或前个单元,则rep movsb就可以循环实现(cx)个字符的传送。
- 911. 11.10 DF标志和串传送指令由于flag的DF位决定着串传送指令执行后,si和di改变的方向,
所以CPU应该提供相应的指令来对DF位进行设置,从而使程序员能够决定传送的方向。
- 912. 11.10 DF标志和串传送指令8086CPU提供下而两条指令对DF位进行设置:
cld指令:将标志寄存器的DF位置0
std指令:将标志寄存器的DF位置1
我们来看两个程序
编程1
编程2
- 913. 11.10 DF标志和串传送指令编程:
用串传送指令,将data段中的第一个字符串复制到它后面的空间中。
data segment
db ‘Welcome to masm!’
db 16 dup (0)
data ends
我们分析一下
- 914. 11.10 DF标志和串传送指令我们分析一下,使用串传送指令进行数据的传送,需要给它提供一些必要的信息,它们是:
① 传送的原始位置:ds:si;
② 传送的目的位置:es:di;
③ 传送的长度:cx;
④ 传送的方向:DF。
- 915. 11.10 DF标志和串传送指令在这个问题中,这些信息如下:
① 传送的原始位置:data:0;
② 传送的目的位置:data:16;
③ 传送的长度:16;
④ 传送的方向: 因为正向传送(每次串传送指令执行后,si和di 递增)比较方便,所以设置DF=0。
明确了这些信息之后,我们来编写程序
- 916. 11.10 DF标志和串传送指令 mov ax,data
mov ds,ax
mov si,0 ;ds:si指向data:0
mov es,ax
mov di,16 ;es:di指向data:16
mov cx ,16 ;(cx)=16,rep循环16次
cld ;设置DF=0,正向传送
rep movsb
- 917. 11.10 DF标志和串传送指令编程:用串传送指令,将F000H段中的最后16个字符复制到data段中。
data segment
db 16 dup (0)
data ends
我们分析一下
- 918. 11.10 DF标志和串传送指令我们还是先来看一下应该为串传送指令提供什么样的信息:
要传送的字符串位于F000H段的最后16个单元中,那么它的最后一个字符的位置:F000:FFFF,是显而易见的。
我们可以将ds:si指向 F000H段的最后一个单元,将es:di指向data段中的最后一个单元,然后逆向(即从高地址向低地址)传送16个字节即可。
- 919. 11.10 DF标志和串传送指令相关信息如下:
① 传送的原始位置:F000:FFFF;
② 传送的目的位置:data:15;
③ 传送的长度:16;
④ 传送的方向:因为逆向传送(每次串传送指令执行后,si 和 di 递减)比较方便,所以设置DF=1。
程序代码
- 920. 11.10 DF标志和串传送指令 mov ax,0f000h
mov ds,ax
mov si,0ffffh ;ds:si指向f000:ffff
mov ax,data
mov es,ax
mov di,15 ;es:di指向data:15
mov cx ,16 ;(cx)=16,rep循环16次
std ;设置DF=1,逆向传送
rep movsb
- 921. 11.11 pushf和popfpushf :将标志寄存器的值压栈;
popf :从栈中弹出数据,送入标志寄存器中。
pushf和popf,为直接访问标志寄存器提供了一种方法。
- 922. 特别提示检测点11.4(p223)
没有完成此检测点,请不要向下进行。
- 923. 11.12 标志寄存器在Debug中的表示在Debug中,标志寄存器是按照有意义的各个标志位单独表示的。
在Debug中,我们可以看到下面的信息:
- 924. 11.12 标志寄存器在Debug中的表示下面列出Debug对我们已知的标志位的表示:
- 925. 小结
- 926. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 927. 第12章 内中断12.1 内中断的产生
12.2 中断处理程序
12.3 中断向量表
12.4 中断过程
12.5 中断处理程序
12.6 除法错误中断的处理
12.7 编程处理 0 号中断
12.8 安装
12.9 do0
12.10 设置中断向量
12.11 单步中断
12.12 响应中断的特殊情况
- 928. 引言中断的意思是指,CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。
注意,我们这里所说的中断信息,是为了便于理解而采用的种逻辑上的说法。它是对几个具有先后顺序的硬件操作所产生的事件的统一描述。
- 929. 12.1 内中断的产生当CPU 的内部有什么事情发生的时候,将产生需要马上处理的中断信息呢?
对于8086CPU,当内部有下面情况发生的时候,将产生中断信息:
1、除法错误,比如:执行div指令产生的除法溢出;
2、单步执行;
3、执行int0指令;
4、执行int 指令。
- 930. 12.1 内中断的产生上述的4种中断源,在8086CPU中的中断类型码如下:
(1)除法错误:0
(2)单步执行:1
(3)执行 int0 指令:
(4)执行 int 指令 ,该指令的格式为 int n,指令中的n为字节型立即数,是提供给CPU的中断类型码。
- 931. 12.2 中断处理程序CPU的设计者必须在中断信息和其处理程序的入口地址之间建立某种联系,使得CPU根据中断信息可以找到要执行的处理程序。
我们知道,中断信息中包含有标识中断源的类型码。根据CPU的设计,中断类型码的作用就是用来定位中断处理程序。
- 932. 12.2 中断处理程序比如CPU 根据中断类型码 4,就可以找到4号中断的处理程序。
可随之而来的问题是,若要定位中断处理程序,需要知道它的段地址和偏移地址,而如何根据 8位的中断类型码得到中断处理程序的段地址和偏移地址呢?
- 933. 12.3 中断向量表CPU用 8 位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址。
那么什么是中断向量表呢?
中断向量表就是中断向量的列表。
- 934. 12.3 中断向量表中断向量表在内存中保存,其中存放着 256个中断源所对应的中断处理程序的入口,如下图所示:
- 935. 12.3 中断向量表中断向量表在内存中存放,对于8086PC机,中断向量表指定放在内存地址0处。
从内存0000:0000到0000:03FF的1024个单元中存放着中断向量表。
- 936. 特别提示检测点12.1(p227)
没有完成此检测点,请不要向下进行。
- 937. 12.4 中断过程从上面的讲解中,我们知道,可以用中断类型码,在中断向量表中找到中断处理程序的入口。
找到这个入口地址的最终目的是用它设置CS和IP,使CPU执行中断处理程序。
- 938. 12.4 中断过程用中断类型码找到中断向量,并用它设置CS和IP,这个工作是由CPU的硬件自动完成的。
CPU 硬件完成这个工作的过程被称为中断过程。
- 939. 12.4 中断过程在使用 call 指令调用子程序时有同样的问题,子程序执行后还要返回到原来的执行点继续执行,
所以,call 指令先保存当前 CS 的 IP值,然后再设置CS和IP。
- 940. 12.4 中断过程8086CPU的中断过程:
(1)(从中断信息中)取得中断类型码;
(2)标志寄存器的值入栈( 因为在中断过程中要改变标志寄存器的值,所以先将其保存在栈中。);
(3)设置标志寄存器的第8位TF 和第9位IF的值为0;(这一步的目的后面将介绍)
(4)CS的内容入栈;
(5)IP的内容入栈;
(6)从内存地址为中断类型码*4 和中断类型码 *4+2 的两个字单元中读取中断处理程序的入口地址设置IP和CS。
- 941. 12.4 中断过程CPU在收到中断信息之后,如果处理该中断信息,就完成一个由硬件自动执行的中断过程。
(程序员无法改变这个过程中所要做 的工作)
- 942. 12.4 中断过程中断过程的主要任务就是用中断类型码在中断向量表中找到中断处理程序的入口地址,设置CS和IP。
因为中断处理程序执行完成后,CPU还要回过头来继续执行被中断的程序,
所以要在设置CS、IP之前,先将它们的值保存起来。
- 943. 12.4 中断过程可以看到CPU将它们保存在栈中。
我们注意到,在中断过程中还要做的一个工作就是设置标志寄存器的TF、IF位。
对于这样做的目的,我们将在后面的内容和下一章中进行讨论。
- 944. 12.4 中断过程因为在执行完中断处理程序后,需要恢复在进入中断处理程序之前的CPU现场
(某一时刻,CPU中各个寄存器的值)。
所以应该在修改标记寄存器之前,将它的值入栈保存。
- 945. 12.4 中断过程我们更简洁的描述中断过程,如下:
(1)取得中断类型码N;
(2) pushf
(3) TF = 0,IF = 0
(4) push CS
(5) push IP
(6)(IP) = (N*4),(CS) = (N*4+2)
在最后一步完成后,CPU 开始执行由程序员编写的中断处理程序。
中断过程演示
- 946. (本页无文本内容)
- 947. 12.5 中断处理程序由于CPU随时都可能检测到中断信息,也就是说,CPU 随时都可能执行中断处理程序,所以中断处理程序必须一直存储在内存某段空间之中。
而中断处理程序的入口地址,即中断向量,必须存储在对应的中断向量表表项中。
- 948. 12.5 中断处理程序中断处理程序的编写方法和子程序的比较相似,下面是常规的步骤:
(1)保存用到的寄存器。
(2)处理中断。
(3)恢复用到的寄存器。
(4)用 iret 指令返回。
iret指令的功能用汇编语法描述为:
pop IP
pop CS
popf
- 949. 12.5 中断处理程序iret通常和硬件自动完成的中断过程配合使用。
可以看到,在中断过程中,寄存器入栈的顺序是标志寄存器、CS、IP ,而iret的出栈顺序是 IP、CS、标志寄存器,刚好和其对应,实现了用执行中断处理程序前的CPU现场恢复标志寄存器和CS、IP的工作。
- 950. 12.5 中断处理程序iret指令执行后,CPU回到执行中断处理程序前的执行点继续执行程序。
- 951. 12.6 除法错误中断的处理下面的内容中,我们通过对 0号中断,即除法错误的中断处理,来体会一下前面所讲的内容。
- 952. 12.6 除法错误中断的处理当CPU执行div等除法指令的时候,如果发生了除法溢出错误,将产生中断类型码为 0 的中断信息,CPU将检测到这个信息,然后引发中断过程,转去执行 0 号中断所对应的中断处理程序。
- 953. 12.6 除法错误中断的处理我们看一下下面的程序的执行结果:
mov ax,1000h
mov bh,1
div bh
- 954. 12.7 编程处理 0 号中断现在我们考虑改变一下0号中断处理程序的功能,即重新编写一个0号中断处理程序,它的功能是在屏幕中间显示“overflow!”后,然后返回到操作系统。
如下图所示
- 955. 12.6 除法错误中断的处理
- 956. 12.7 编程处理 0 号中断当CPU执行div bh时,发生了除法溢出错误,产生0号中断信息,从而引发中断过程,CPU执行我们编写的0号中断处理程序。
在屏幕中间显示提示信息“overflow!”后,返回到操作系统中。
- 957. 12.7 编程处理 0 号中断编程:当发生除法溢出时,在屏幕中间显示“overflow!”,返回DOS。
分析
- 958. 12.7 编程处理 0 号中断分析(1)当发生除法溢出的时候,产生0号中断信息,从而引发中断过程。
此时,CPU将进行以下工作:
① 取得中断类型码0;
② 标志寄存器入栈,TF、IF设置为0;
③ CS、IP入栈;
④ (IP) = (0*4),(CS) = (0*4+2)
- 959. 12.7 编程处理 0 号中断分析(2)可见 ,当中断 0 发生时,CPU将转去执行中断处理程序。
只要按如下步骤编写中断处理程序,当中断0发生时,即可显示“overflow!”。
① 相关处理。
② 向显示缓冲区送字符串“overflow!”。
③ 返回DOS
- 960. 12.7 编程处理 0 号中断我们将这段程序称为do0。
分析(3)现在的问题是:do0 应放在内存中。
因为除法溢出随时可能发生,CPU随时都可能将 CS:IP指向 do0的入口,执行程序。
- 961. 12.7 编程处理 0 号中断那么do0应该放在哪里呢?
由于我们是在操作系统之上使用计算机,所有的硬件资源都在操作系统的管理之下,所以我们要想得到一块内存存放do0,应该向操作系统申请。
- 962. 12.7 编程处理 0 号中断但在这里出于两个原因我们不想这样做:
过多地讨论申请内存将偏离问题主线;
我们学习汇编的一个重要目的就是要获得对计算机底层的编程体验。
所以,在可能的情况下,我们不去理会操作系统,而直接面向硬件资源。
- 963. 12.7 编程处理 0 号中断问题变得简单而直接,我们只需找到一块别的程序不会用到的内存区,将do0传送到其中即可。
前面讲到,内存0000:0000~0000:03FF,大小为1KB的空间是系统存放中断处理程序入口地址的中断向量表。
- 964. 12.7 编程处理 0 号中断8086 支持 256 个中断,但是,实际上,系统中要处理的中断事件远没有达到256 个 。
所以在中断向量表中,有许多单元是空的。
中断向量表是PC系统中最重要的内存区,只用来存放中断处理程序的入口地址,DOS 系统和其他应用程序都不会随便使用这段空间。
- 965. 12.7 编程处理 0 号中断我们可以利用中断向量表中的空闲单元来存放我们的程序。
一般情况下:
从0000:0200至0000:02FF的256个字节的空间所对应的中断向量表项都是空的,操作系统和其他应用程序都不占用。
我们在前面的课程中使用过这段空间(参见5.7节)。
- 966. 12.7 编程处理 0 号中断根据以前的编程经验,我们可以估计出,do0的长度不可能超过256个字节。
结论:我们可以将do0传送到内存0000:0200处。
- 967. 12.7 编程处理 0 号中断分析(4 - 1)
我们将中断处理程序do0放到 0000:0200 后,若要使得除法溢出发生的时候,CPU转去执行do0,则必须将do0的入口地址,即0000:0200登记在中断向量表的对应表项中。
- 968. 12.7 编程处理 0 号中断分析(4 - 2)
因为除法溢出对应的中断类型码为0,它的中断处理程序的入口地址应该从0×4地址单元开始存放,段地址存放在 0×4+2 字单元中,偏移地址存放在0×4字单元中。
- 969. 12.7 编程处理 0 号中断分析(4 - 3)
也就是说要将do0的段地址0存放在 0000:0002 字单元中 ,将偏移地址200H存放在0000:0000字单元中。
- 970. 12.7 编程处理 0 号中断总结上面的分析,我们要做以下几件事情:
(1)编写可以显示“overflow!”的中断处理程序:do0;
(2)将do0送入内存0000:0200处;
(3)将do0的入口地址0000:0200存储在中断向量表0号表项中。
程序框架
- 971. 12.7 编程处理 0 号中断程序12.1 assume cs:code
code segment
start: do0安装程序
设置中断向量表
mov ax,4c00h
int 21h
do0: 显示字符串“overflow!”
mov ax,4c00h
int 21h
code ends
end start
- 972. 12.7 编程处理 0 号中断我们可以看到,上面的程序分为两部分:
(1)安装do0,设置中断向量的程序
(2)do0
程序12.1执行时,do0的代码是不执行的,它只是作为do0安装程序所要传送的数据。
- 973. 12.7 编程处理 0 号中断程序12.1执行时,首先执行do0安装程序,将 do0 的代码拷贝到内存 0:200处,然后设置中断向量表,将do0的入口地址,即偏移地址200H和段地址0,保存在0号表项中。
这两部分工作完成后,程序就返回了。
- 974. 12.7 编程处理 0 号中断程序的目的就是在内存0:200处安装do0 的代码,将0号中断处理程序的入口地址设置为0:200。
do0的代码虽然在程序中,却不在程序执行的时候执行。它是在除法溢出发生的时候才得以执行的中断处理程序。
- 975. 12.7 编程处理 0 号中断do0部分代码的最后两条指令是依照我们的编程要求,用来返回DOS的。
现在,我们在反过来从CPU的角度看一下,什么是中断处理程序?
- 976. 12.7 编程处理 0 号中断我们来看一下do0是如何变成0号中断的中断处理程序的:
(1)程序12.1 在执行时,被加载到内存中,此时do0的代码在程序12.1 所在的内存空间中,它只是存放在程序12.1的代码段中的一段要被传送到其他单元中的数据,我们不能说它是0号中断的中断处理程序;
- 977. 12.7 编程处理 0 号中断do0是如何变成0号中断的中断处理程序的:
(2)程序12.1中安装do0 的代码执行完后,do0的代码被从程序12.1的代码段中拷贝到0:200处。此时,我们也不能说它是0号中断的中断处理程序,它只不过是存放在0:200处的一些数据;
- 978. 12.7 编程处理 0 号中断do0是如何变成0号中断的中断处理程序的:
(3)程序12.1中设置中断向量表的代码执行完后,在0号表项中填入了do0的入口地址0:200,此时0:200 处的信息,即do0 的代码,就变成了0号中断的中断处理程序。
因为当除法溢出(即0号中断)发生时,CPU将执行0:200处的代码。
- 979. 12.7 编程处理 0 号中断回忆以下:
我们如何让一个内存单元成为栈顶?
将它的地址放入SS、SP中;
我们如何让,一个内存单元中的信息被CPU当作指令来执行?
将它的地址放入CS、IP中;
那么,我们如何让一段程序成为N号中断的中断处理程序?
将它的入口地址放入中断向量表的N号表项中。
- 980. 12.7 编程处理 0 号中断下面的内容中,我们讨论每一部分程序的具体编写方法。
- 981. 12.8 安装我们可以使用movsb指令,将do0的代码送入0:0200处。
程序框架
- 982. 12.8 安装我们来看一下,用rep movsb指令的时候需要确定的信息:
(1)传送的原始位置,段地址:code,偏移地址:offset do0;
(2)传送的目的位置:0:200;
(3)传送的长度:do0部分代码的长度;
(4)传送的方向:正向。
更明确的程序
- 983. 12.8 安装 assume cs:code
code segment
start: 设置es:di指向目的地址
设置ds:si指向源地址
设置cx为传输长度
设置传输方向为正
rep movsb
设置中断向量表
mov ax,4c00h
int 21h
do0: 显示字符串“overflow!”
mov ax,4c00h
int 21h
code ends
end start
- 984. assume cs:code
code segment
start: mov ax,cs
mov ds,ax
mov si,offset do0;设置ds:si指向源地址
mov ax,0
mov es,ax
mov di,200h ;设置es:di指向目的地址
mov cx,do0部分代码的长度;设置cx为传输长度
cld ;设置传输方向为正
rep movsb
设置中断向量表
mov ax,4c00h
int 21h
do0: 显示字符串“overflow!”
mov ax,4c00h
int 21h
code ends
end start
- 985. 12.8 安装问题是,我们如何知道do0代码的长度?
最简单的方法是,计算一下do0 所有指令码的字节数。
但是这样做太麻烦了,因为只要do0的内容发生了改变,我们都要重新计算它的长度。
- 986. 12.8 安装我们可以利用编译器来计算do0的长度。
具体做法
- 987. assume cs:code
code segment
start: mov ax,cs
mov ds,ax
mov si,offset do0;设置ds:si指向源地址
mov ax,0
mov es,ax
mov di,200h ;设置es:di指向目的地址
mov cx,offset do0end-offset do0;设置cx为传输长度
cld ;设置传输方向为正
rep movsb
设置中断向量表
mov ax,4c00h
int 21h
do0: 显示字符串“overflow!”
mov ax,4c00h
int 21h
do0end:nop
code ends
end start
- 988. 12.8 安装“-”是编译器识别的运算符号,编译器可以用它来进行两个常数的减法。
比如:mov ax,8-4,被编译器处理为指令: mov ax,4。
编译器可以处理表达式,比如:
指令: mov ax,(5+3)*5/10,被编译器处理为指令: mov ax,4
- 989. 12.8 安装好了,知道了“-”的含义,对于用offset do0end-offset do0,得到do0代码的长度的原理,我们就不再多说了,相信到现在,读者已经可以自己进行分析了。
下面我们编写do0程序。
- 990. 12.9 do0do0程序的主要任务是显示字符串,程序如下: do0:设置ds:si指向字符串
mov ax,0b800h
mov es,ax
mov di,12*160+36*2;设置es:di指向显存空间的中间位置
mov cx,9 ;设置cx为字符串长度
s: mov al,[si]
mov es:[di],al
inc si
add di,2
loop s
mov ax,4c00h
int 21h
do0end:nop
- 991. 12.9 do0程序写好了,可要显示的字符串放在哪里呢?
我们看程序12.2源码
- 992. 12.9 do0程序12.2看似合理,可实际上却大错特错。
错误分析
- 993. 12.9 do0程序12.2错误分析
注意,“overflow!”在程序12.2的data段中。程序12.2执行完成后返回,它所占用的内存空间被系统释放,而在其中存放的“overflow!”也将很可能被别的信息覆盖;
- 994. 12.9 do0程序12.2错误分析(续)
而do0程序被放到了0:200处,随时都会因发生了除法溢出而被CPU 执行,很难保证 do0 程序从原来程序12.2所处的空间中取得的是要显示的字符串“overflow!”。
- 995. 12.9 do0因为 do0 程序随时可能被执行,而它要用到字符串“overflow”,所以该字符串也应该存放在一段不会被覆盖的空间中。
正确的程序如下:
程序12.3源码
- 996. 12.9 do0在程序12.3中,我们将“overflow!”放到do0程序中,程序12.3执行时,将标号do0到标号do0end之间的内容送到0000:0200处。
- 997. 12.9 do0注意,因为在do0程序开始处的“overflow!”不是可以执行的代码,所以在“overflow!”之前加上一条jmp 指令,转移到正式的do0 程序。
当除法溢出发生时,CPU 执行0:200 处的jmp 指令,跳过后面的字符串,转到正式的do0 程序执行。
- 998. 12.9 do0do0程序执行过程中必须要找到“overflow!”,那么它在哪里呢?
首先来看段地址,“overflow!”和do0的代码处于同一个段中,而除法溢出发生时,CS中必然存放do0的段地址,也就是“overflow!”的段地址;
- 999. 12.9 do0再来看偏移地址,0:200处的指令为jmp short do0start ,这条指令占两个字节,所以“overflow!”的偏移地址为202h 。
- 1000. 12.10 设置中断向量下面,我们将do0的入口地址0:200,写入中断向量表的 0 号表项中,使do0成为0 号中断的中断处理程序。
- 1001. 12.10 设置中断向量0号表项的地址为0:0,其中0:0字单元存放偏移地址,0:2字单元存放段地址。
程序如下:
mov ax,0
mov es,ax
mov word ptr es:[0*4],200h
mov word ptr es:[0*4+2],0
- 1002. 12.11 单步中断基本上,CPU在执行完一条指令之后,如果检测到标志寄存器的TF位为1,则产生单步中断,引发中断过程。单步中断的中断类型码为1,则它所引发的中断过程如下:
(1)取得中断类型码1;
(2)标志寄存器入栈,TF、IF设置为0;
(3) CS、IP入栈;
(4)(IP)=(1*4),(CS)=(1*4+2)。
- 1003. 12.11 单步中断如上所述,如果TF=1,则执行一条指令后,CPU就要转去执行1号中断处理程序。
CPU为什么要提供这样的功能呢?
- 1004. 12.11 单步中断我们在使用Debug的T命令的时候,有没有想过这样的问题,Debug如何能让CPU在执行一条指令后,就显示各个寄存器的状态?
我们知道,CPU在执行程序的时候是从CS:IP指向的某个地址开始,自动向下读取指令执行。
- 1005. 12.11 单步中断也就是说,如果CPU不提供其他功能的话,就按这种方式工作,只要CPU一加电,它就从预设的地址开始一直执行下去,不可能有任何程序能控制它在执行完一条指令后停止,去做别的事情。
- 1006. 12.11 单步中断可是,我们在Debug中看到的情况却是,Debug可以控制CPU执行被加载程序中的一条指令,然后让它停下来,显示寄存器的状态。
Debug有特殊的能力吗?
- 1007. 12.11 单步中断我们只能说Debug利用了CPU提供的一种功能。
只有CPU提供了在执行一条指令后就转去做其他事情的功能,Debug或是其他的程序才能利用CPU提供的这种功能做出我们使用T命令时的效果。
- 1008. 12.11 单步中断好了,我们来简要地考虑一下Debug是如何利用CPU所提供的单步中断的功能的。
首先,Debug提供了单步中断的中断处理程序,功能为显示所有寄存器中的内容后等待输入命令。
- 1009. 12.11 单步中断然后,在使用 T 命令执行指令时,Debug 将TF设置为 1,使得CPU在工作于单步中断方式下,则在CPU执行完这条指令后就引发单步中断,执行单步中断的中断处理程序,所有寄存器中的内容被显示在屏幕上,并且等待输入命令。
- 1010. 12.11 单步中断那么,接下来的问题是,当TF=1时,CPU在执行完一条指令后将引发单步中断,转去执行中断处理程序。
注意
- 1011. 12.11 单步中断注意:中断处理程序也是由一条条指令组成的,如果在执行中断处理程序之前,TF=1,则CPU在执行完中断处理程序的第一条指令后,又要产生单步中断,则又要转去执行单步中断的中断处理程序,在执行完中断处理程序的第一条指令后,又要产生单步中断,则又要转去执行单步中断的中断处理程序……
- 1012. 12.11 单步中断看来,上面的过程将陷入一个永远不能结束的循环,CPU永远执行单步中断处理程序的第一条指令。
- 1013. 12.11 单步中断CPU当然不能让这种情况发生,解决的办法就是,在进入中断处理程序之前,设置TF=0。
从而避免CPU在执行中断处理程序的时候发生单步中断。
这就是为什么在中断过程中有 TF=0这个步骤。
- 1014. 12.11 单步中断我们再来看一下中断过程
(1)取得中断类型码N;
(2)标志寄存器入栈,TF=0、IF=0;
(3)CS、IP入栈;
(4)(IP) = (N*4),(CS) = (N*4+2)
最后,CPU提供单步中断功能的原因就是,为单步跟踪的执行过程,提供了实现机制。
- 1015. 12.12 响应中断的特殊情况一般情况下,CPU在执行完当前指令后,如果检测到中断信息,就响应中断,引发中断过程。
可是,在有些情况下,CPU 在执行完当前指令后,即便是发生中断,也不会响应。
对于这些情况,我们不一一列举,只是用一种情况来进行说明。
- 1016. 12.12 响应中断的特殊情况在执行完向 ss寄存器传送数据的指令后,即便是发生中断,CPU 也不会响应。
这样做的主要原因是,ss:sp联合指向栈顶,而对它们的设置应该连续完成。
- 1017. 12.12 响应中断的特殊情况如果在执行完设置ss的指令后,CPU响应中断,引发中断过程,要在栈中压入标志寄存器、CS和IP的值。
而ss改变,sp并未改变,ss:sp指向的不是正确的栈顶,将引起错误。
- 1018. 12.12 响应中断的特殊情况所以CPU在执行完设置ss的指令后,不响应中断。
这给连续设置 ss和sp,指向正确的栈顶提供了一个时机。
即,我们应该利用这个特性,将设置ss和sp的指令连续存放,使得设置sp的指令紧接着设置ss的指令执行,而在此之间,CPU不会引发中断过程。
- 1019. 12.12 响应中断的特殊情况比如,我们要将栈顶设为1000:0,
应该 而不应该 mov ax,1000h
mov ss,ax
mov ax,0
mov sp,0 mov ax,1000h
mov ss,ax
mov ax,0
mov sp,0
- 1020. 12.12 响应中断的特殊情况好了,现在我们回过来看一下,实验2 中的“(3)下一条指令执行了吗?”。
现在你知道原因了吗?
- 1021. 12.12 响应中断的特殊情况Debug 利用单步中断来实现T命令的功能,
也就是说,用T命令执行一条指令后,CPU响应单步中断,执行Debug设置好的处理程序,才能在屏幕上显示寄存器的状态,并等待命令的输入。
- 1022. 12.12 响应中断的特殊情况而在mov ss,ax指令执行后,CPU根本就不响应任何中断,其中也包括单步中断,
所以Debug设置好的用来显示寄存器状态和等待输入命令的中断处理程序根本没有得到执行,所以我们看不到预期的结果。
- 1023. 12.12 响应中断的特殊情况CPU接着向下执行后面的指令mov sp,10h,然后响应单步中断,我们才看到正常的结果。
- 1024. 小结
- 1025. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 1026. 第13章 int指令13.1 int指令
13.2 编写供应用程序调用的中断例程
13.3 对int、iret和栈的深入理解
13.4 BIOS和DOS所提供的中断例程
13.5 BIOS和DOS中断例程的安装过程
13.6 BIOS中断例程应用
13.7 DOS中断例程应用
- 1027. 引言中断信息可以来自 CPU 的内部和外部,当CPU 的内部有需要处理的事情发生的时候,将产生需要马上处理的中断信息,引发中断过程。
- 1028. 引言在第12章中,我们讲解了中断过程和两种内中断的处理。
这一章中,我们讲解另一种重要的内中断,由int指令引发的中断。
- 1029. 13.1 int指令int格式: int n,n为中断类型码。它的功能是引发中断过程。
CPU 执行int n指令,相当于引发一个 n号中断的中断过程,执行过程如下:
(1)取中断类型码n;
(2)标志寄存器入栈,IF = 0,TF = 0;
(3)CS、IP入栈;
(4)(IP) = (n*4),(CS) = (n*4+2)。
从此处转去执行n号中断的中断处理程序。
- 1030. 13.1 int指令可以在程序中使用int指令调用任何一个中断的中断处理程序。
比如,下面的程序: assume cs:code
code segment
start: mov ax,0b800h
mov es,ax
mov byte ptr es:[12*160+40*2],’!’
int 0
code ends
end start
- 1031. 13.1 int指令这个程序在 Windows 2000中的 DOS方式下执行时,将在屏幕中间显示一个“!”,然后显示“Divide overflow”后返回到系统中。
“!”是我们编程显示的,而,“Divide overflow”是哪里来的呢?
我们的程序中又没有做除法,不可能产生除法溢出。
- 1032. 13.1 int指令程序是没有做除法,但是在结尾使用了int 0指令。
CPU执行int 0指令时,将引发中断过程,执行 0号中断处理程序,而系统设置的 0号中断处理程序的功能是显示“Divide overflow”,然后返回到系统。
- 1033. 13.1 int指令可见,int 指令的最终功能和call指令相似,都是调用一段程序。
一般情况下,系统将一些具有一定功能的子程序,以中断处理程序的方式提供给应用程序调用。
- 1034. 13.1 int指令我们在编程的时候,可以用int指令调用这些子程序。
当然,也可以自己编写一些中断处理程序供别人使用。
以后,我们可以将中断处理程序简称为中断例程。
- 1035. 13.2 编写供应用程序调用的中断例程前面,我们已经编写过中断0 的中断例程了,现在我们讨论可以供应用程序调用的中断例程的编写方法。
我们通过两个问题来讨论:
1、示例一
2、示例二
- 1036. 13.2 编写供应用程序调用的中断例程示例一 编写、安装中断7ch的中断例程:
功能:求一word型数据的平方。
参数: (ax)=要计算的数据。
返回值:dx、ax中存放结果的高16位和低16位。
应用举例:求2*3456^2
程序及分析
- 1037. 13.2 编写供应用程序调用的中断例程 assume cs:code
code segment
start: mov ax,3456;(ax)=3456
int 7ch;调用中断7ch的中断例程,计算ax中的数据的平方
add ax,ax;dx:ax存放结果,讲结果乘以2
mov ax,4c00h
int 21h
code ends
end start
分析一下
- 1038. 13.2 编写供应用程序调用的中断例程我们要做三部分工作:
(1)编程实现求平方功能的程序;
(2)安装程序,我们将其安装在0:200处;
(3)设置中断向量表,将程序的入口地址保存在7ch表项中,使其成为中断7ch的中断例程。
安装程序代码
- 1039. 13.2 编写供应用程序调用的中断例程注意,在中断例程 sqr的最后,要使用iret指令。
用汇编语法描述,iret指令的功能为:
pop IP
pop CS
popf
- 1040. 13.2 编写供应用程序调用的中断例程CPU执行int 7ch指令进入中断例程之前,标志寄存器、当前的CS和IP被压入栈中,在执行完中断例程后,应该用iret指令恢复int 7ch 执行前的标志寄存器和CS、IP的值,从而接着执行应用程序。
int指令和 iret指令的配合使用与call指令和ret指令的配合使用具有相似的思路。
- 1041. 13.2 编写供应用程序调用的中断例程示例二 编写、安装中断7ch的中断例程:
功能:将一个全是字母,以 0结尾的字符串,转化为大写。
参数:ds:si指向字符串的首地址。
应用举例:
将data段中的字符转化为大写。
- 1042. 13.2 编写供应用程序调用的中断例程应用举例:将data段中的字符转化为大写。
assume cs:code
data segment
db 'conversation',0
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0
int 7ch
mov ax,4c00h
int 21h
code ends
end start
- 1043. 13.2 编写供应用程序调用的中断例程安装程序源码
在中断例程capital中用到了寄存器 si和cx,编写中断例程和编写子程序的时候具有同样的问题,就是要避免寄存器的冲突。
应该注意例程中用到的寄存器的值的保存和恢复。
- 1044. 13.3 对int、iret和栈的深入理解问题:用7ch中断例程完成 loop指令的功能。
loop s 的执行需要两个信息,循环次数和到s的位移,所以,7ch中断例程要完成loop指令的功能,也需要这两个信息作为参数。
我们用cx存放循环次数,用bx存放位移。
- 1045. 13.3 对int、iret和栈的深入理解应用举例:在屏幕中间显示80个‘!’。
assume cs:code
code segment
start: mov ax,0b800h
mov es,ax
mov di,160*12
mov bx,offset s - offset se;设置从标号se到标号s的转移位移
mov cx,80
s: mov byte ptr es:[di],'!'
add di,2
int 7ch ;如果(cx)≠0,转移到标号s处
se: nop
mov ax,4c00h
int 21h
code ends
end start
- 1046. 13.3 对int、iret和栈的深入理解在上面的程序中,用int 7ch调用7ch中断例程进行转移,用 bx 传递转移的位移。
分析:为了模拟loop指令,7ch中断例程应具备下面的功能:
(1)dec cx
(2)如果(cx)≠0,转到标号s 处执行,否则向下执行。
- 1047. 13.3 对int、iret和栈的深入理解下面我们分析7ch中断例程如何实现到目的地址的转移:
(1)转到标号s显然应设(CS)=标号s的段地址,(IP)=标号s的偏移地址;
(2)那么,中断例程如何得到标号s的段地址和偏移地址呢? 分析
(3)现在知道,可以从栈中直接和间接地得到标号s的段地址和偏移地址,那么如何用它们设置CS:IP呢? 分析
- 1048. 13.3 对int、iret和栈的深入理解int 7ch引发中断过程后,进入 7ch中断例程,在中断过程中,当前的标志寄存器、CS和IP都要压栈,此时压入的CS和IP中的内容,分别是调用程序的段地址(可以认为是标号 s 的段地址)和int 7ch后一条指令的偏移地址(即标号se的偏移地址)。
- 1049. 13.3 对int、iret和栈的深入理解可见,在中断例程中,可以从栈里取得标号s 的段地址和标号 se的偏移地址,而用标号se的偏移地址加上bx中存放的转移位移就可以得到标号s的偏移地址。
- 1050. 13.3 对int、iret和栈的深入理解可以利用iret指令,我们将栈中的se的偏移地址加上 bx 中的转移位移,则栈中的se的偏移地址就变为了s的偏移地址。
我们再使用iret指令,用栈中的内容设置CS、IP,从而实现转移到标号s处。
- 1051. 13.3 对int、iret和栈的深入理解7ch中断例程如下:
lp: push bp
mov bp,sp
dec cx
jcxz lpret
add [bp+2],bx
lpret: pop bp
iret
- 1052. 13.3 对int、iret和栈的深入理解因为要访问栈,使用了 bp,在程序开始处将bp 入栈保存,结束时出栈恢复。
当要修改栈中se的偏移地址的时候,栈中的情况为;
栈顶处是bp 原来的数值,下面是se的偏移地址,再下面是s的段地址,再下面是标志寄存器的值。
- 1053. 13.3 对int、iret和栈的深入理解而此时,bp中为栈顶的偏移地址,所以((ss)*16+(bp)+2)处为se 的偏移地址,将它加上bx 中的转移位移就变为s的偏移地址。最后用iret出栈返回,CS:IP即从标号s处开始执行指令。
如果(cx)=0,则不需要修改栈中 se的偏移地址,直接返回即可。
CPU从标号se处向下执行指令。
- 1054. 特别提示 检测点13.1(page246)
没有通过此检测点,请不要向下进行!
- 1055. 13.5 BIOS和DOS中断例程的安装过程而BIOS和DOS提供的中断例程是如何安装到内存中的呢?
我们下面讲解它们的安装过程。
- 1056. 13.5 BIOS和DOS中断例程的安装过程(1) 开机后,CPU 一加电,初始化(CS)=0FFFFH,(IP)=0,自动从FFFF:0单元开始执行程序。FFFF:0处有一条转跳指令,CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。
- 1057. 13.5 BIOS和DOS中断例程的安装过程(2)初始化程序将建立BIOS 所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。
- 1058. 13.5 BIOS和DOS中断例程的安装过程(3) 硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。从此将计算机交由操作系统控制。
- 1059. 13.5 BIOS和DOS中断例程的安装过程(4)DOS 启动后,除完成其它工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。
- 1060. 特别提示 检测点13.2(page248 独立思考后可共同讨论)
没有通过此检测点,请不要向下进行!
- 1061. 13.6 BIOS中断例程应用下面我们举几个例子,来看一下BIOS中断例程的应用。
int 10h中断例程是BIOS提供的中断例程,其中包含了多个和屏幕输出相关的子程序。
- 1062. 13.6 BIOS中断例程应用一般来说,一个供程序员调用的中断例程中往往包括多个子程序,中断例程内部用传递进来的参数来决定执行哪个子程序。
BIOS 和DOS 提供的中断例程,都用ah来传递内部子程序的编号。
- 1063. 13.6 BIOS中断例程应用我们看一下int 10h中断例程的设置光标位置功能。
mov ah,2
mov bh,0
mov dh,5
mov dl,12
int 10h
- 1064. 13.6 BIOS中断例程应用(ah)=2表示调用第 10h号中断例程的 2号子程序,功能为设置光标位置,可以提供光标所在的行号(80*25字符模式下:0~24)、列号(80*25字符模式下:0~79),和页号作为参数。
(bh)=0,(dh)=5,(dl)=12,设置光标到第0页,第5行,第12列。
- 1065. 13.6 BIOS中断例程应用bh中页号的含义:内存地址空间中,B8000h~BFFFFh共 32K的空间,为80*25 彩色字符模式的显示缓冲区。
一屏的内容在显示缓冲区中共占4000个字节。
- 1066. 13.6 BIOS中断例程应用显示缓冲区分为8页,每页4K(≈4000),显示器可以显示任意一页的内容。一般情况下,显示第 0 页的内容。
也就是说,通常情况下,B8000~B8F9F中的4000个字节的内容将出现在显示器上。
- 1067. 13.6 BIOS中断例程应用再看一下int 10h中断例程的在光标位置显示字符功能。
mov ah,9 ;置光标
mov al,’a’ ;字符
mov bl,7 ;颜色属性
mov bh,0 ;第0页
mov cx,3 ;字符重复个数
int 10h
- 1068. 13.6 BIOS中断例程应用(ah)=9 表示调用第10h号中断例程的9号子程序;
功能为在光标位置显示字符,可以提供要显示的字符、颜色属性、页号、字符重复个数作为参数。
- 1069. 13.6 BIOS中断例程应用(bh)中的颜色属性格式如下:
可以看出,和显存中的属性字节的格式相同。
- 1070. 13.6 BIOS中断例程应用编程:在屏幕的5行12列显示3个红底高亮闪烁绿色的‘a’。
程序源码
注意:闪烁的效果必须在全屏DOS 方式下才能看到。
- 1071. 13.7 DOS中断例程应用int 21h 中断例程是DOS提供的中断例程,其中包含了DOS提供给程序员在编程时调用的子程序。
我们从前一直使用的是 int 21中断例程的4ch号功能,即程序返回功能,如下:
mov ah,4ch ;程序返回
mov al,0 ;返回值
int 21h
- 1072. 13.7 DOS中断例程应用(ah)=4ch表示调用第21h号中断例程的 4ch号子程序,功能为程序返回,可以提供返回值作为参数。
我们前面使用这个功能的时候经常写作:
mov ax,4c00h
int 21h
- 1073. 13.7 DOS中断例程应用我们看一下int 21h中断例程的在光标位置显示字符串的功能:
ds:dx指向字符串 ;要显示的字符串需用“$”作为结束符
mov ah ,9 ;功能号9,表示在光标位置显示字符串
int 21h
- 1074. 13.7 DOS中断例程应用(ah)=9表示调用第21h号中断例程的 9号子程序,功能为在光标位置显示字符串,可以提供要显示字符串的地址作为参数。
- 1075. 13.7 DOS中断例程应用编程:在屏幕的5列12行显示字符串“Welcome to masm!”。
源代码
- 1076. 13.7 DOS中断例程应用上述程序在屏幕的5列12行显示字符串“Welcome to masm!”,直到遇见“$”(“$” 本身并不显示,只起到边界的作用)。
如果字符串比较长,遇到行尾,程序会自动转到下一行开头处继续显示;如果到了最后一行,还能自动上卷一行。
- 1077. 13.7 DOS中断例程应用DOS为程序员提供了许多可以调用的子程序,都包含在 int 21h 中断例程中。
我们这里只对原理进行了讲解,对于DOS提供的所有可调用子程序的情况,读者可以参考相关的书籍。
- 1078. 小结
- 1079. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 1080. 第14章 端口14.1 端口的读写
14.2 CMOS RAM 芯片
14.3 shl和shr指令
14.4 CMOS RAM中存储的时间信息
- 1081. 引言CPU可以直接读写3 个地方的数据:
(1)CPU 内部的寄存器;
(2)内存单元;
(3)端口。
这一章,我们讨论端口的读写。
- 1082. 14.1 端口的读写对端口的读写不能用mov、push、pop等内存读写指令。
端口的读写指令只有两条:
in和out,
分别用于从端口读取数据和往端口写入数据。
- 1083. 14.1 端口的读写我们看一下CPU 执行内存访问指令和端口访问指令时候,总线上的信息:
(1)访问内存
(2)访问端口
- 1084. 14.1 端口的读写访问内存:
mov ax,ds:[8];假设执行前(ds)=0
执行时,与总线相关的操作:
① CPU通过地址线将地址信息8发出;
② CPU通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据;
③ 存储器将 8号单元中的数据通过数据线送入CPU。
- 1085. 14.1 端口的读写访问端口:
in al,60h;从60h号端口读入一个字节
执行时与总线相关的操作:
① CPU通过地址线将地址信息60h发出;
② CPU通过控制线发出端口读命令,选中端口所在的芯片,并通知它,将要从中读取数据;
③ 端口所在的芯片将60h端口中的数据通过数据线送入CPU。
- 1086. 14.1 端口的读写访问端口:
过程演示
注意:在in和out 指令中,只能使用 ax 或al 来存放从端口中读入的数据或要发送到端口中的数据。访问8 位端口时用 al ,访问16 位端口时用ax 。
- 1087. (本页无文本内容)
- 1088. 14.1 端口的读写对0~255以内的端口进行读写:
in al,20h ;从20h端口读入一个字节
out 20h,al ;往20h端口写入一个字节
对256~65535的端口进行读写时,端口号放在dx中:
mov dx,3f8h ;将端口号3f8送入dx
in al,dx ;从3f8h端口读入一个字节
out dx,al ;向3f8h端口写入一个字节
- 1089. 14.2 CMOS RAM 芯片PC机中有一个CMOS RAM芯片,其有如下特征:
(1)包含一个实时钟和一个有128个存储单元的RAM存储器。
(早期的计算机为64个字节)
- 1090. 14.2 CMOS RAM 芯片(2)该芯片靠电池供电。
所以,关机后其内部的实时钟仍可正常工作, RAM 中的信息不丢失。
- 1091. 14.2 CMOS RAM 芯片CMOS RAM芯片特征:
(3) 128 个字节的 RAM 中,内部实时钟占用 0~0dh单元来保存时间信息,其余大部分分单元用于保存系统配置信息,供系统启动时BIOS程序读取。
BIOS也提供了相关的程序,使我们可以在开机的时候配置CMOS RAM 中的系统信息。
- 1092. 14.2 CMOS RAM 芯片CMOS RAM芯片特征:
(4)该芯片内部有两个端口,端口地址为70h和71h。
CPU 通过这两个端口读写CMOS RAM。
- 1093. 14.2 CMOS RAM 芯片CMOS RAM芯片特征:
(5)70h为地址端口,存放要访问的CMOS RAM单元的地址;
71h为数据端口,存放从选定的CMOS RAM 单元中读取的数据,或要写入到其中的数据。
- 1094. 14.2 CMOS RAM 芯片可见,CPU对CMOS RAM的读写分两步进行。
比如:读CMOS RAM的2号单元:
1、将2送入端口70h
2、从71h读出2号单元的内容
- 1095. 特别提示检测点14.1(page256)
没有通过此检测点,请不要向下进行
- 1096. 14.3 shl和shr指令shl和shr 是逻辑移位指令,后面的课程中我们要用到移位指令,这里进行一下讲解。
- 1097. 14.3 shl和shr指令shl逻辑左移指令,功能为:
(1)将一个寄存器或内存单元中的数据向左移位;
(2)将最后移出的一位写入CF中;
(3)最低位用0补充。
- 1098. 14.3 shl和shr指令指令:
mov al,01001000b
shl al,1 ;将al中的数据左移一位
执行后(al)=10010000b,CF=0。
我们来看一下shl al,1的操作过程
- 1099. 14.3 shl和shr指令shl al,1的操作过程
(1)左移:
原数据: 01001000
左移后:01001000
(2)最后移出一位写入CF中:
原数据: 01001000
左移后: 1001000 CF=0
(3)最低为用0补充:
原数据: 01001000
左移后: 10010000
如果接着上面,继续执行一条shl al,1指令?
执行后:(al)=00100000b,CF=1 操作过程
- 1100. 14.3 shl和shr指令再执行一条shl al,1指令的操作过程:
(1)左移:
原数据: 10010000
左移后:10010000
(2)最后移出一位写入CF中:
原数据: 10010000
左移后: 0010000 CF=1
(3)最低为用0补充:
原数据: 10010000
左移后: 00100000
- 1101. 14.3 shl和shr指令如果移动位数大于1时,必须将移动位数放在cl中。
比如,指令:
mov al,01010001b
mov cl,3
shl al,cl
执行后(al)=10001000b,因为最后移出
一位是0,所以CF=0。
- 1102. 14.3 shl和shr指令可以看出,将X逻辑左移一位,相当于执行X=X*2。
比如:
- 1103. 14.3 shl和shr指令shr逻辑右移指令,它和shl所进行的操作刚好相反:
(1)将一个寄存器或内存单元中的数据向右移位;
(2)将最后移出的一位写入CF中;
(3)最高位用0补充。
- 1104. 14.3 shl和shr指令指令:
mov al,10000001b
shr al,1 ;将al中的数据右移一位
执行后(al)=01000000b,CF=1。
如果接着上面,继续执行一条shr al,1指令,则执行后:(al)=00100000b,CF=0。
- 1105. 14.3 shl和shr指令如果移动位数大于1时,必须将移动位数放在cl中。
比如,指令:
mov al,01010001b
mov cl,3
shr al,cl
执行后(al)=00001010b,因为最后移出
的一位是0,所以CF=0。
- 1106. 14.3 shl和shr指令可以看出,将X逻辑右移一位,相当于执行X=X/2。
shl和shr指令执行过程演示
- 1107. (本页无文本内容)
- 1108. 特别提示检测点14.2(p258)
没有通过此检测点,请不要向下进行
- 1109. 14.4 CMOS RAM中存储的时间信息在CMOS RAM中,存放着当前时间:
秒:00H
分:02H
时:04H
日:07H
月:08H
年:09H
这6个信息的长度长度都为1个字节。
- 1110. 14.4 CMOS RAM中存储的时间信息 这些数据以BCD码的方式存放:
数码: 0 1 2 3 4
BCD码:0000 0001 0010 0011 0100
数码: 5 6 7 8 9
BCD码:0101 0110 0111 1000 1001
- 1111. 14.4 CMOS RAM中存储的时间信息比如:
数值26,用BCD码表示为:0010 0110
可见,一个字节可表示两个BCD码。
则CMOS RAM存储时间信息的单元中,存储了用两个 BCD码表示的两位十进制数,高 4 位的BCD码表示十位,低4 位的BCD 码表示个位。
比如:00010100b表示14。
- 1112. 14.4 CMOS RAM中存储的时间信息编程:在屏幕中间显示当前的月份。
分析:这个程序主要做两部分工作:
(1)从CMOS RAM的8号单元读出当前月份的BCD码;
要读取 CMOS RAM的信息,我们首先要向地址端口70h写入要访问的单元的地址:
mov al,8
out 70h,al
然后从数据端口71h中取得指定单元中的数据:
in al,71h
- 1113. 14.4 CMOS RAM中存储的时间信息(2)将用BCD码表示的月份以十进制的形式显示到屏幕上。
我们可以看出 ,BCD 码值=十进制数码值,则BCD码值+30h=十进制数对应的ASCII码。
- 1114. 14.4 CMOS RAM中存储的时间信息从CMOS RAM的8号单元读出的一个字节中,包含了用两个 BCD 码表示的两位十进制数,高4位的 BCD码表示十位,低 4位的 BCD码表示个位。
比如:00010100b表示14。
- 1115. 14.4 CMOS RAM中存储的时间信息我们需要进行两步工作: (一)
将从CMOS RAM的8号单元中读取的一个字节,分为两个表示BCD码值的数据。
实现此功能的指令序列
- 1116. 14.4 CMOS RAM中存储的时间信息 mov ah,al ;al中为从CMOS RAM的 8
;号单元读出的数据
mov cl,4
shr ah,cl ;ah中为月份的十位数码值
and al,00001111b
;ah中为月份的个位数码值
- 1117. 14.4 CMOS RAM中存储的时间信息我们需要进行两步工作: (二)
(2)显示(ah)+30h 和 (al)+30 对应的ASCII码字符。
完整的程序源代码
- 1118. 小结
- 1119. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 1120. 第15章 外中断15.1 接口芯片和端口
15.2 外中断信息
15.3 PC机键盘的处理过程
15.4 编写int 9中断例程
15.5 安装新的int 9中断例程
- 1121. 引言以前我们讨论的都是CPU对指令的执行。我们知道,CPU 在计算机系统中,除了能够执行指令,进行运算以外,还应该能够对外部设备进行控制,接收它们的输入,向它们进行输出。
也就是说,CPU 除了有运算能力外,还要有 I/O( Input/Output ,输入/输出)能力。
- 1122. 15.1 接口芯片和端口第14 章我们讲过,在PC 系统的接口卡和主板上,装有各种接口芯片。这些外设接口芯片的内部有若干寄存器,CPU 将这些寄存器当作端口来访问。
- 1123. 15.1 接口芯片和端口外设的输入不直接送入内存和CPU ,而是送入相关的接口芯片的端口中;
CPU 向外设的输出也不是直接送入外设,而是先送入端口中,再由相关的芯片送到外设。
- 1124. 15.1 接口芯片和端口CPU 还可以向外设输出控制命令,而这些控制命令也是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设实施控制。
可见,CPU 通过端口和外部设备进行联系。
- 1125. 15.2 外中断信息CPU 在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入。
在PC 系统中,外中断源一共有两类:
1、可屏蔽中断
2、不可屏蔽中断
- 1126. 15.2 外中断信息可屏蔽中断是CPU 可以不响应的外中断。CPU 是否响应可屏蔽中断,要看标志寄存器的IF 位的设置。
当CPU 检测到可屏蔽中断信息时:
如果IF=1,则CPU 在执行完当前指令后响应中断,引发中断过程;
如果IF=0,则不响应可屏蔽中断。
- 1127. 15.2 外中断信息我们回忆一下内中断所引发的中断过程:
(1)取中断类型码n;
(2)标志寄存器入栈,IF=0,TF=0;
(3)CS 、IP 入栈;
(4)(IP)=(n*4),(CS)=(n*4+2)
由此转去执行中断处理程序。
- 1128. 15.2 外中断信息可屏蔽中断所引发的中断过程 ,除在第1步的实现上有所不同外,基本上和内中断的中断过程相同。
因为可屏蔽中断信息来自于CPU外部,中断类型码是通过数据总线送入CPU 的;
而内中断的中断类型码是在CPU内部产生的。
- 1129. 15.2 外中断信息现在,我们可以解释中断过程中将IF置为0的原因了。将IF置0的原因就是,在进入中断处理程序后,禁止其他的可屏蔽中断。
当然,如果在中断处理程序中需要处理可屏蔽中断,可以用指令将IF 置1 。
- 1130. 15.2 外中断信息8086CPU 提供的设置IF的指令如下:
sti,用于设置IF=1;
cli,用于设置IF=0。
- 1131. 15.2 外中断信息不可屏蔽中断是CPU 必须响应的外中断。当CPU 检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程。
对于8086CPU 不可屏蔽中断的中断类型码固定为2。所以中断过程中,不需要取中断类型码。
- 1132. 15.2 外中断信息不可屏蔽中断的中断过程:
1、标志寄存器入栈,IF=0,TF=0;
2、CS、IP入栈;
3、(IP)=(8),(CS)=(0AH)。
- 1133. 15.2 外中断信息几乎所有由外设引发的外中断,都是可屏蔽中断。当外设有需要处理的事件(比如说键盘输入)发生时,相关芯片向CPU 发出可屏蔽中断信息。
不可屏蔽中断是在系统中有必须处理的紧急情况发生时用来通知CPU 的中断信息。在我们的课程中,主要讨论可屏蔽中断。
- 1134. 15.3 PC机键盘的处理过程下面我们看一下键盘输入的处理过程,并以此来体会一下PC 机处理外设输入的基本方法。
1、键盘输入
2、引发9号中断
3、执行int 9中断例程
- 1135. 15.3 PC机键盘的处理过程键盘上的每一个键相当于一个开关,键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。
按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明了按下的键在键盘上的位置。扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60H 。
松开按下的键时,也产生一个扫描码,扫描码说明了松开的键在键盘上的位置。松开按键时产生的扫描码也被送入60H 端口中。
- 1136. 15.3 PC机键盘的处理过程一般将按下一个键时产生的扫描码称为通码,松开一个键产生的扫描码称为断码。
扫描码长度为一个字节,通码的第7 位为 0 ,断码的第7位为1,即: 断码=通码+80H
比如:g键的通码为22H,断码为a2H。
键盘上部分键的扫描码
- 1137. 15.3 PC机键盘的处理过程
- 1138. 15.3 PC机键盘的处理过程键盘的输入到达60H 端口时,相关的芯片就会向CPU 发出中断类型码为 9 的可屏蔽中断信息。
CPU检测到该中断信息后,如果IF=1,则响应中断,引发中断过程,转去执行int 9中断例程。
- 1139. 15.3 PC机键盘的处理过程BIOS 提供了int 9中断例程,用来进行基木的键盘输入处理,主要的工作如下:
(1)读出60H 端口中的扫描码;
- 1140. 15.3 PC机键盘的处理过程(2)如果是字符键的扫描码,将该扫描码和它所对应的字符码( 即 ASCII码)送入内存中的 BIOS 键盘缓冲区;
如果是控制键(比如 Ctrl )和切换键(比如 CapsLock)的扫描码,则将其转变为状态字节( 用二进制位记录控制键和切换键状态的字节 )写入内存中存储状态字节的单元。
- 1141. 15.3 PC机键盘的处理过程(3)对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。
PC机键盘的处理过程演示
- 1142. (本页无文本内容)
- 1143. 15.3 PC机键盘的处理过程BIOS键盘缓冲区是系统启动后,BIOS用于存放int 9 中断例程所接收的键盘输入的内存区。
该内存区可以存储15 个键盘输入,因为 int 9 中断例程除了接收扫描码外,还要产生和扫描码对应的字符码,所以在BIOS键盘缓冲区中,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。
- 1144. 15.3 PC机键盘的处理过程0040:17 单元存储键盘状态字节,该字节记录了控制键和切换键的状态。键盘状态字节各位记录的信息如下:
- 1145. 15.4 编写int 9 中断例程从上面的内容中,我们可以看出键盘输入的处理过程:
(1)键盘产生扫描码;
(2)扫描码送入60h 端口;
(3)引发9 号中断;
(4)CPU执行int 9中断例程处理键盘输入。
- 1146. 15.4 编写int 9 中断例程上面的过程中,第(1)、(2)、(3)步都是由硬件系统完成的。我们能够改变的只有int 9中断处理程序。
我们可以重新编写int 9中断例程,按照自己的意图来处理键盘的输入。
- 1147. 15.4 编写int 9 中断例程但是,在课程中,我们不准备完整地编写一个键盘中断的处理程序,因为要涉及到一些硬件细节,而这些内容脱离了我们的内容主线。
但是,我们却还要编写新的键盘中断处理程序,来进行一些特殊的工作,那么这些硬件细节如何处理呢?
- 1148. 15.4 编写int 9 中断例程这点比较简单,因为BIOS 提供的int 9中断例程已经对这些硬件细节进行了处理。
我们只要在自己编写的中断例程中调用BIOS 的int 9中断例程就可以了。
编程
- 1149. 15.4 编写int 9 中断例程编程:
在屏幕中间依次显示 “a”~“z” ,并可以让人看清。在显示的过程中,按下Esc键后,改变显示的颜色。
我们先来看一下如何依次显示“a”~“z”
- 1150. 15.4 编写int 9 中断例程assume cs:code
code segment
start: mov ax,0b800h
mov es,ax
mov ah,'a'
s: mov es:[160*12+40*2],ah
inc ah
cmp ah,'z'
jna s
mov ax,4c00h
int 21h
code ends
end start 依次显示”a”~”z”
- 1151. 15.4 编写int 9 中断例程在上面的程序的执行过程中,我们无法看清屏幕上的显示。
因为一个字母刚显示到屏幕上,CPU执行几条指令后,就又变成了另一个字母,字母之间切换得太快,无法看清。
- 1152. 15.4 编写int 9 中断例程我们应该在每显示一个字母后,延时一段时间,让人看清后,再显示下一个字母。
那么如何延时呢?
我们让CPU 执行一段时间的空循环。
- 1153. 15.4 编写int 9 中断例程因为现在CPU 的速度都非常快,所以循环的次数一定要大,我们用两个16 位寄存器来存放32 位的循环次数。如下:
mov dx,10h
mov ax,0
s: sub ax,1
sbb dx,0
cmp ax,0
jne s
cmp dx,0
jne s
- 1154. 15.4 编写int 9 中断例程上面的程序,循环100000h 次。
我们可以将循环延时的程序段写为一个子程序。
现在,我们看程序源代码
- 1155. 15.4 编写int 9 中断例程显示“a”~“z”,并可以让人看清,这个任务己经实现。
那么如何实现,按下 Esc 键后,改变显示的颜色呢?
键盘输入到达60h 端口后,就会引发 9号中断,CPU 则转去执行int 9中断例程。
- 1156. 15.4 编写int 9 中断例程我们可以编写int 9中断例程,功能如下:
(1)从60h 端口读出键盘的输入;
(2)调用BIOS 的int 9 中断例程,处理其他硬件细节;
(3)判断是否为Esc的扫描码,如果是,改 变显示的颜色后返回;如果不是则直接返回。
我们对这些功能的实现一一进行分析
- 1157. 15.4 编写int 9 中断例程1、从端口60h读出键盘的输入
in al,60h
- 1158. 15.4 编写int 9 中断例程2、调用BIOS的int 9中断例程
有一点要注意的是,我们写的中断处理程序要成为新的int 9中断例程,主程序必须要将中断向量表中的int 9中断例程的入口地址改为我们写的中断处理程序的入口地址。
- 1159. 15.4 编写int 9 中断例程那么在新的中断处理程序中调用原来的int 9中断例程时,中断向量表中的int 9中断例程的入口地址却不是原来的int 9 中断例程的地址。
所以我们不能使用int 指令直接调用。
- 1160. 15.4 编写int 9 中断例程要能在我们写的新中断例程中调用原来的中断例程,就必须在将中断向量表中的中断例程的入口地址改为新地址之前,将原来的入口地址保存起来。
这样,在需要调用的时候,我们才能找到原来的中断例程的入口。
- 1161. 15.4 编写int 9 中断例程对于我们现在的问题,假设我们将原来int 9中断例程的偏移地址和段地址保存在ds:[0]和ds:[2]单元中。
那么我们在需要调用原来的int 9中断例程时候,就可以在 ds:[0]、ds:[2] 单元中找到它的入口地址。
- 1162. 15.4 编写int 9 中断例程那么,有了入口地址后,我们如何进行调用呢?
当然不能使用指令int 9来调用。我们可以用别的指令来对int指令进行一些模拟,从而实现对中断例程的调用。
- 1163. 15.4 编写int 9 中断例程我们来看,int 指令在执行的时候,CPU 进行下面的工作:
(1)取中断类型码n;
(2)标志寄存器入栈;
(3) IF=0,TF=0;
(4) CS 、IP 入栈;
(5)(IP)=(n*4),(CS=(n*4+2)。
- 1164. 15.4 编写int 9 中断例程取中断类型码是为了定位中断例程的入口地址,在我们的问题中,中断例程的入口地址已经知道。
所以,我们用别的指令模拟int指令时候,不需要做第(1)步。
- 1165. 15.4 编写int 9 中断例程在假设要调用的中断例程的入口地址在ds:0和ds:2单元中的前提下,我们将int 过程用下面几步模拟:
(1)标志寄存器入栈;
(2)IF=0,TF=0;
(3)CS、IP入栈;
(4)(IP)=((ds)*16+0),
(CS)=((ds)*16+2)。
- 1166. 15.4 编写int 9 中断例程可以注意到第(3)、(4)步和call dword ptr ds:[0]的功能一样。
call dword ptr ds:[0]的功能也是:(1)CS 、IP 入栈;(2)(IP)=((ds)*16+0),
(CS)=((ds)*16+2)。
如果还有疑问,复习10.6节的内容。
- 1167. 15.4 编写int 9 中断例程所以int 过程的模拟过程变为:
(1)标志寄存器入栈;
(2)IF=0,TF=0;
(3)call dword ptr ds:[0]
对于(1),可用pushf实现。
对于(2),可用下面的指令实现。
- 1168. 15.4 编写int 9 中断例程实现IF=0,TF=0步骤:
pushf
pop ax
and ah,11111100b ;IF和OF为标志寄存器的第9位和第8位
push ax
popf
这样,模拟int指令的调用功能,调用入口地址在ds:0、ds:2中的中断例程的程序如下
- 1169. 15.4 编写int 9 中断例程 pushf ;标志寄存器入栈
pushf
pop ax
and ah,11111100b ;IF和OF为标志寄存器的第9位和第8位
push ax
popf ;IF=0、TF=0
call dword ptr ds:[0];CS、IP入栈:
;(IP)=((ds)*16+0),
;(CS)=((ds)*16+2)
- 1170. 15.4 编写int 9 中断例程3、如果是Esc键的扫描码,改变显示的颜色后返回
如何改变显示的颜色?
- 1171. 15.4 编写int 9 中断例程显示的位置是屏幕的中问,即第12行40列,显存中的偏移地址为:160*12+40* 2。
所以字符的ASCII码要送入b800:160*12+40*2处。
而b800:160*12+40*2+1处是字符的属性,我们只要改变此处的数据就可以改变在b800:160*12+40*2处显示的字符的颜色了。
- 1172. 15.4 编写int 9 中断例程该程序的最后一个问题是,要在程序返回前,将中断向量表中的ini 9中断例程的入口地址恢复为原来的地址。否则程序返回后,别的程序将无法使用键盘。
经过分析,整理得到完整的程序代码。
- 1173. 15.4 编写int 9 中断例程注意,本章中所有关于键盘的程序,因要直接访问真实的硬件,则必须在DOS实模式下运行。
在Windows 2000 的DOS 方式下运行,会出现一些和硬件工作原理不符合的现象。
- 1174. 特别提示检测点15.1(page271)
没有通过此检测点,请不要向下进行!
- 1175. 15.5 安装新的 int 9 中断例程下面,我们安装一个新的int 9中断例程,使得原int 9中断例程的功能得到扩展。
任务:安装一个新的int 9中断例程,功能:在DOS下,按F1键后改变当前屏幕的显示颜色,其他的键照常处理。
我们进行一下分析
- 1176. 15.5 安装新的 int 9 中断例程分析:
(1)改变屏幕的显示颜色
改变从B800开始的4000个字一节中的所有奇地址单元中的内容,当前屏幕的显示颜色即发生改变。
程序如下
- 1177. 15.5 安装新的 int 9 中断例程分析(续):
(1)改变屏幕的显示颜色程序 mov ax,0b800h
mov es,ax
mov bx,1
mov cx,2000
s: inc byte ptr es:[bx]
add bx,2
loop s
- 1178. 15.5 安装新的 int 9 中断例程 (2)其他键照常处理
可以调用原int 9中断处理程序,来处理其他的键盘输入。
- 1179. 15.5 安装新的 int 9 中断例程分析(续) :
(3)原int 9中断例程入口地址的保存
因为在编写的新int 9中断例程中要调用原int 9中断例程,所以,要保存原int 9中断例程的入口地址。
保存在哪里?
- 1180. 15.5 安装新的 int 9 中断例程分析(续) :
显然不能保存在安装程序中,因为安装程序返回后地址将丢失。
我们将地址保存在0:200单元处。
- 1181. 15.5 安装新的 int 9 中断例程分析(续) :
(4)新int 9中断例程的安装
这个问题在前面己经详细讨论过。
我们可将新的int 9中断例程安装在0:204 处。
完整的程序代码
- 1182. 15.5 安装新的 int 9 中断例程这一章中,我们通过对键盘输入的处理,讲解了CPU 对外设输入的通常处理方法。
即:
(1)外设的输入送入端口;
(2)向CPU 发出外中断(可屏蔽中断)信息;
(3)CPU检测到可屏蔽中断信息,如果IF=1,CPU在执行完当前指令后响应中断,执行相应的中断例程;
(4)可在中断例程中实现对外设输入的处理。
- 1183. 15.5 安装新的 int 9 中断例程端口和中断机制,是CPU 进行I/O的基础。
- 1184. 15.5 安装新的 int 9 中断例程指令系统总结
我们对8086CPU 的指令系统进行一下总结。
读者若要详细了解8086 指令系统中的各个指令的用法 ,可以查看有关的指令手册。
8086CPU 提供以下几大类指令
- 1185. 15.5 安装新的 int 9 中断例程1、数据传送指令
比如:
mov、push、pop、pushf、popf、xchg等都是数据传送指令,
这些指令实现寄存器和内存、寄存器和寄存器之间的单个数据传送。
- 1186. 15.5 安装新的 int 9 中断例程2、算术运算指令
比如:
add、sub、adc、sbb、inc、dec、cmp、imul、idiv、aaa等都是算术运算指令,
这些指令实现寄存器和内存中的数据的算数运算。
它们的执行结果影响标志寄存器的:sf、zf、of、cf、pf、af位。
- 1187. 15.5 安装新的 int 9 中断例程3、逻辑指令
比如:
and、or、not、xor、test、shl、shr、sal、sar、rol、ror、rcl、rcr 等都是逻辑指令。
除了not指令外,它们的执行结果都影响标志寄存器的相关标志位。
- 1188. 15.5 安装新的 int 9 中断例程4、转移指令
可以修改IP ,或同时修改CS 和IP 的指令统称为转移指令。
转移指令分为以下几类:
(1)无条件转移指令,比如:jmp;
(2)条件转移指令,比如:jcxz、je、jb、ja、jnb、jna等;
(3)循环指令,比如:loop;
(4)过程,比如:call、ret、retf;
(5)中断,比如int、iret。
- 1189. 15.5 安装新的 int 9 中断例程5、处理机控制指令这些指令对标志寄存器或其他处理机状态进行设置,
比如:cld、std、cli、sti、nop、clc、cmc、stc、hlt、wait、esc、lock等都是处理机控制指令。
- 1190. 15.5 安装新的 int 9 中断例程6、串处理指令
这些指令对内存中的批量数据进行处理
比如:movsb、movsw、cmps、scas、lods、stos等。
若要使用这些指令方便地进行批量数据的处理,则需要和rep、repe、repne等前缀指令配合使用。
- 1191. 小结
- 1192. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 1193. 第16章 直接定址表16.1 描述了单元长度的标号
16.2 在其他段中使用数据标号
16.3 直接定址表
16.4 程序入口地址的直接定址表
- 1194. 引言这一章,我们讨论如何有效合理地组织数据,以及相关的编程技术。
- 1195. 16.1 描述了单元长度的标号本章中,我们要用到这种标号,先进行如下介绍。
前面的课程中,我们一直在代码段中使用标号来标记指令、数据、段的起始地址。
- 1196. 16.1 描述了单元长度的标号比如:下面的程序将code 段中的a 标号处的8个数据累加,结果存储到b标号处的字中。
程序代码
- 1197. 16.1 描述了单元长度的标号assume cs:code
code segment
a : db 1,2,3,4,5,6,7,8
b : dw 0
start :mov si,offset a
mov bx,offset b
mov cx,8
s : mov al,cs:[si]
mov ah,0
add cs:[bx],ax
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
- 1198. 16.1 描述了单元长度的标号程序中,code、a、b、start、s都是标号。这些标号仅仅表示了内存单元的地址。
但是,我们还可以使用一种标号,这种标号不但表示内存单元的地址,还表示了内存单元的长度,即表示在此标号处的单元,是一个字节单元,还是字单元,还是双字单元。
上面的程序我们还可以写成这样:程序
- 1199. 16.1 描述了单元长度的标号assume cs:code
code segment
a db 1,2,3,4,5,6,7,8
b dw 0
start : mov si,0
mov cx,8
s : mov al,a[si]
mov ah,0
add b,ax
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
- 1200. 16.1 描述了单元长度的标号我们在code 段中使用的标号a、b后面没有“:” ,它们是同时描述内存地址和单元长度的标号。
标号a,描述了地址code:0,和从这个地址开始,以后的内存单元都是字节单元;
而标号b描述了地址code:8,和从这个地址开始,以后的内存单元都是字单元。
- 1201. 16.1 描述了单元长度的标号因为这种标号包含了对单元长度的描述,所以,在指令中,它可以代表一个段中的内存单元。
比如,对于程序中的b dw 0。
指令:mov ax,b
相当于:mov ax,cs:[8]
指令:mov b,2
相当于:mov word ptr cs:[8],2
指令:inc b
相当于:inc word ptr cs:[8]
在这些指令中,标号b 代表了一个内存单元,地址为code:8 ,长度为2 字节。
- 1202. 16.1 描述了单元长度的标号下面的指令会引起编译错误: mov al,b
因为b代表的内存单元是字单元,而al 是8 位寄存器。
如果我们将程序中的指令:add b,ax ,写为add b,al,将出现同样的编译错误。
- 1203. 16.1 描述了单元长度的标号对于程序中的a db 1,2,3,4,5,6,7,8 :
指令:mov al,a [si]
相当于:mov al,cs:0[si]
指令:mov al,a[3]
相当于:mov al,cs:0[3]
指令:mov al,a[bx+si+3]
相当于:mov al,cs:0[bx+si+3]
- 1204. 16.1 描述了单元长度的标号可见,使用这种包含单元长度的标号, 可以使我们以简洁的形式访问内存中 的数据。
以后,我们将这种标号称为数据标号。
它标记了存储数据的单元的地址和长度。
它不同于仅仅表示地址的地址标号。
- 1205. 特别提示检测点16.1(page278)
没有通过此检测点,请不要向下进行!
- 1206. 16.2 在其他段中使用数据标号一般来说,我们不在代码段中定义数据,而是将数据定义到其他段中。
在其他段中,我们也可以使用数据标号来描述存储数据的单元的地址和长度。
注意:在后面加有“:”的地址标号,只能在代码段中使用,不能在其他段中使用。
- 1207. 16.2 在其他段中使用数据标号右面的程序将data 段中 a标号处的 8 个数据累加,结果存储到 b标号处的字中。assume cs:code,ds:data
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0
mov cx,8
s: mov al,a[si]
mov ah,0
add b,ax
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
- 1208. 16.2 在其他段中使用数据标号注意,如果想在代码段中,直接用数据标号访问数据,则需要用伪指令assume 将标号所在的段和一个段寄存器联系起来。
否则编译器在编译的时候,无法确定标号的段地址在哪一个寄存器中。
- 1209. 16.2 在其他段中使用数据标号当然,这种联系是编译器需要的,但绝对不是说,我们因为编译器的工作需要,用 assume 指令将段寄存器和某个段相联系,段寄存器中就会真的存放该段的地址。
我们在程序中还要使用指令对段寄存器进行设置。
- 1210. 16.2 在其他段中使用数据标号比如:在上面的程序中,我们要在代码段code中用 data段中的数据标号 a、b 访问数据,则必须用 assume 将一个寄存器和data 段相联。
在程序中,我们用 ds寄存器和 data 段相联,则编译器对相关指令的编译如下:
指令:mov al,a[si]
编译为:mov al,[si+0]
指令:add b,ax
编译为:add [8],ax
- 1211. 16.2 在其他段中使用数据标号因为这些实际编译出的指令,都默认所访问单元的段地址在ds中,而实际要访问的段为data,所以,若要访问正确,在这些指令执行前,ds 中必须为 data 段的段地址。
则,我们在程序中使用指令: mov ax,data mov ds,ax
设置ds指向data段。
- 1212. 16.2 在其他段中使用数据标号我们可以将标号当作数据来定义,此时,编译器将标号所表示的地址当作数据的值。
比如:
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw a,b
data ends
数据标号c处存储的两个字型数据为标号a、b 的偏移地址。
- 1213. 16.2 在其他段中使用数据标号相当于:
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw offset a, offset b
data ends
- 1214. 16.2 在其他段中使用数据标号再比如:
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dd a,b
data ends
数据标号c处存储的两个双字型数据为标号a的偏移地址和段地址、标号b 的偏移地址和段地址。
- 1215. 16.2 在其他段中使用数据标号相当于:
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw offset a, seg a, offset b, seg b
data ends
seg操作符,功能为取得某一标号的段地址。
- 1216. 特别提示检测点16.2(page280)
没有通过此检测点,请不要向下进行!
- 1217. 16.3 直接定址表现在,我们讨论用查表的方法编写相关程序的技巧。
编写子程序,以十六进制的形式在屏幕中间显示给定的byte 型数据。
分析
- 1218. 16.3 直接定址表分析:
一个字节需要用两个十六进制数码来表示,所以,子程序需要在屏幕上显示两个ASCII 字符。
我们当然要用“0”、“1”、“2”、“3”、“4”、“5” 、“6” 、“7” 、“8” 、“9” 、“A”、“B”、“C”、“D”、“E”、“F”这16个字符来显示十六进制数码。
- 1219. 16.3 直接定址表我们可以将一个byte的高4位和低4 位分开,分别用它们的值得到对应的数码字符。
比如 2Bh ,我们可以得到高4 位的值为2,低4 位的值为11。
那么我们如何用这两个数值得到对应的数码字符“2”和“B”呢?
- 1220. 16.3 直接定址表最简单的办法就是一个一个地比较,如下:
如果数值为 0,则显示“0”;
如果数值为 1,则显示“1”;
:
如果数值为11,则显示“B”;
:
我们可以看出,这样做,程序中要使用多条比较、转移指令。
程序将比较长,混乱。
- 1221. 16.3 直接定址表显然,我们希望能够在数值0~15和字符“0”~“F”之间找到一种映射关系。
这样我们用0~15间的任何数值,都可以通过这种映射关系直接得到“0”~“F”中对应的字符。
- 1222. 16.3 直接定址表数值0~9和字符“0”~“9”之间的映射关系是很明显的,即:
数值+30h=对应字符的ASCII值:
0+30h=“0”的ASCII值
1+30h=“1”的ASCII值
2+30h=“2”的ASCII值
:
- 1223. 16.3 直接定址表但是,10~15和“A”~“F”之间的映射关系是:
数值+37h=对应字符的ASCII值:
10+37h=“A”的ASCII值
11+37h=“B”的ASCII值
12+37h=“C”的ASCII值
:
- 1224. 16.3 直接定址表可见,我们可以利用数值和字符之间的这种原本存在的映射关系,通过高 4 位和低4 位值得到对应的字符码。
但是由于映射关系的不同,我们在程序中必须进行一些比较,对于大于 9 的数值,我们要用不同的计算方法。
- 1225. 16.3 直接定址表这样做,虽然使程序得到了简化。但是,如果我们希望用更简捷的算法,就要考虑用同一种映射关系从数值得到字符码。
所以,我们就不能利用0~9和“0”~“9” 之间与 10~15 和 “A” ~“F ” 之间原有的映射关系。
- 1226. 16.3 直接定址表因为数值0~15 和字符“0”~“F ”之间没有一致的映射关系存在,所以,我们应该在它们之间建立新的映射关系。
具体的做法是,我们建立一张表,表中依次存储字符“0”~“F”,我们可以通过数值0~15直接查找到对应的字符。
子程序代码
- 1227. 16.3 直接定址表可以看出,在子程序中,我们在数值0~15和字符“0”~“F ” 之间建立的映射关系为:
以数值N为table 表中的偏移,可以找到对应的字符。
- 1228. 16.3 直接定址表利用表,在两个数据集合之间建立一种映射关系,使我们可以用查表的方法根据给出的数据得到其在另一集合中的对应数据。
这样做的目的一般来说有三个:
(1)为了算法的清晰和简洁。
(2)为了加快运算速度。
(3)为了使程序易于扩充。
- 1229. 16.3 直接定址表在上面的子程序中,我们更多的是为了算法的清晰和简洁,而采用了查表的方法。
下面我们来看一下,为了加快运算速度而采用查表的方法的情况。
- 1230. 16.3 直接定址表编写一个子程序,计算sin(x),x∈{0°,30°,60°,90°,120°,150°,180°},并在屏幕中间显示计算结果。
比如sin(30)的结果显示为“0.5”。
- 1231. 16.3 直接定址表我们可以利用麦克劳林公式来计算sin(x)。
x 为角度,麦克劳林公式中需要代入弧度,则:
y=x/180*3.1415926
- 1232. 16.3 直接定址表可以看出,计算sin(x)需要进行多次乘法和除法。
乘除是非常费时的运算,它们的执行时间大约是加法、比较等指令的5倍。
如何才能够不做乘除而计算sin(x)呢?
我们看一下需要计算的sin(x)的结果
- 1233. 16.3 直接定址表需要计算的sin(x)的结果:
sin(0)=0
sin(30)=0.5
sin(60)=0.866
sin(90)=1
sin(120)=0.866
sin(150)=0.5
sin(180)=0
- 1234. 16.3 直接定址表我们可以看出,其实用不着计算,可以占用一些内存空间来换取运算的速度。
将所要计算的sin(x) 的结果都存储到一张表中;然后用角度值来查表,找到对应的sin(x)的值。
我们用ax向子程序传递角度
程序代码
- 1235. 16.3 直接定址表在上面的子程序中,我们在角度值x和表示 sin(x) 的字符串集合table 之间建立的映射关系为:
以 角度值/30 为table 表中的偏移,可以找到对应的字符串的首地址。
- 1236. 16.3 直接定址表编程的时候要注意程序的容错性,即对于错误的输入要有处理能力。
在上面的子程序中,我们还应该在加上对提供的角度值是否超范围的检测。
- 1237. 16.3 直接定址表如果提供的角度值不在合法的集合中,程序将定位不到正确的字符串,出现错误。
对于角度值的检测,请读者自行完成。
- 1238. 16.3 直接定址表上面的两个子程序中,我们将通过给出的数据进行计算或比较而得到结果的问题,转化为用给出的数据作为查表的依据,通过查表得到结果的问题。
具体的查表方法 ,是用查表的依据数据 ,直接计算出所要查找的元素在表中的位置。
- 1239. 16.3 直接定址表像这种可以通过依据数据,直接计算出所要找的元素的位置的表,我们称其为:
直接定址表。
我们可以在直接定址表中存储子程序的地址,从而方便地实现不同子程序的调用。
- 1240. 16.4 程序入口地址的直接定址表我们看下面的问题:实现一个子程序setscreen ,为显示输出提供如下功能:
(1)清屏。
(2)设置前景色。
(3)设置背景色。
(4)向上滚动一行
入口参数说明
- 1241. 16.4 程序入口地址的直接定址表入口参数说明:
(1)用ah 寄存器传递功能号:
0 表示清屏,
1表示设置前景色,
2 表示设置背景色,
3 表示向上滚动一行;
(2)对于2、3号功能,用al传送颜色值,
(al)∈{ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 }
- 1242. 16.4 程序入口地址的直接定址表下面,我们讨论一下各种功能如何实现 :
(1)清屏:将显存中当前屏幕中的字符设为空格符;
(2)设置前景色:设置显存中当前屏幕中处于奇地址的属性字节的第0、1、2位;
- 1243. 16.4 程序入口地址的直接定址表
(3)设置背景色:设置显存中当前屏幕中处于奇地址的属性字节的第4、5、6位;
(4)向上滚动一行:依次将第 n+1行的内容复制到第n行处:最后一行为空。
- 1244. 16.4 程序入口地址的直接定址表我们将这4 个功能分别写为 4 个子程序,请读者根据编程思想,自行读懂下面的程序。
功能实现程序源码
- 1245. 16.4 程序入口地址的直接定址表我们可以将这些功能子程序的入口地址存储在一个表中,它们在表中的位置和功能号相对应。
对应关系为:
功能号*2=对应的功能子程序在地址表中的偏移。
程序源码
- 1246. 16.4 程序入口地址的直接定址表当然,我们也可以将子程序setscreen如下实现:程序源码
显然,用通过比较功能号进行转移的方法,程序结构比较混乱,不利于功能的扩充。
- 1247. 16.4 程序入口地址的直接定址表比如说,在 setscreen 中再加入一个功能,则需要修改程序的逻辑,加入新的比较、转移指令。
用根据功能号查找地址表的方法,程序的结构清晰,便于扩充。
如果加入一个新的功能子程序,那么只需要在地址表中加入它的入口地址就可以了。
- 1248. 小结
- 1249. 《汇编语言》课件 王爽 著-清华大学出版社制作工具:Microsoft PowerPoint2003本课件由汇编网(www.asmedu.net)制作提供
- 1250. 第17章 使用BIOS进行键盘输入和磁盘读写17.1 int 9中断例程对键盘输入的处理
17.2 使用int 16h中断例程读取键盘缓冲区
17.3 字符串的输入
17.4 应用int13h中断例程对磁盘进行读写
- 1251. 引言大多数有用的程序都需要处理用户的输入,键盘输入是最基本的输入。
程序和数据通常需要长期存储,磁盘是最常用的存储设备。
BIOS 为这两种外设的I/O提供了最基本的中断例程,在本章中,我们对它们的应用和相关的问题进行讨论。
- 1252. 17.1 int 9中断例程对键盘输入的处理我们已经讲过,键盘输入将引发9 号中断,BIOS 提供了int 9 中断例程。
CPU 在9 号中断发生后,执行int 9中断例程,从60h 端口读出扫描码,并将其转化为相应的ASCII 码或状态信息,存储在内存的指定空间(键盘缓冲区或状态字节)中。
- 1253. 17.1 int 9中断例程对键盘输入的处理一般的键盘输入,在CPU 执行完int 9 中断例程后,都放到了键盘缓冲区中。
键盘缓冲区中有16 个字单元,可以存储15个按键的扫描码和对应的入ASCII 码。
- 1254. 17.1 int 9中断例程对键盘输入的处理下面我们按照键盘缓冲区的逻辑结构,来看一下键盘输入的扫描码和对应 ASCII 码是如何写入键盘缓冲区的。
注意
- 1255. 17.1 int 9中断例程对键盘输入的处理注意:
在我们的课程中,仅在逻辑结构的基础上,讨论BIOS键盘缓冲区的读写问题。
其实键盘缓冲区是用环形队列结构管理的内存区,但我们不对队列和环形队列的实现进行讨论,因为那是另一门专业课《数据结构》的内容。
- 1256. 17.1 int 9中断例程对键盘输入的处理下面,我们通过下面几个键:
A、B、C、D、E、shift_A、A
的输入过程,简要地看一下int 9 中断例程对键盘输入的处理方法:
处理键盘输入演示
- 1257. (本页无文本内容)
- 1258. 17.2 使用int 16h中断例程读取键盘缓冲区BIOS提供了int 16h 中断例程供程序员调用。
int 16h 中断例程中包含的一个最重要的功能是从键盘缓冲区中读取一个键盘输入,该功能的编号为0。
- 1259. 17.2 使用int 16h中断例程读取键盘缓冲区下面的指令从键盘缓冲区中读取一个键盘输入,并且将其从缓冲区中删除:
mov ah,0 int 16h
结果:(ah)=扫描码,
(al)=ASCII码。
- 1260. 17.2 使用int 16h中断例程读取键盘缓冲区下面,我们接着上一节中的键盘输入过程,看一下 int 16h 如何读取键盘缓冲区。
int 16h读键盘缓冲区演示
- 1261. (本页无文本内容)
- 1262. 17.2 使用int 16h中断例程读取键盘缓冲区从上面我们可以看出,int 16h 中断例程的 0 号功能,进行如下的工作:
(1)检测键盘缓冲区中是否有数据;
(2)没有则继续做第1 步;
(3)读取缓冲区第一个字单元中的键盘输入;
(4)将读取的扫描码送入ah,ASCII 码送入al;
(5)将己读取的键盘输入从缓冲区中删除。
- 1263. 17.2 使用int 16h中断例程读取键盘缓冲区可见,B1OS 的int 9 中断例程和int 16h 中断例程是一对相互配合的程序,int 9 中断例程向键盘缓冲区中写入,int 16h 中断例程从缓冲区中读出。
它们写入和读出的时机不同,int 9 中断例程在有键按下的时候向键盘缓冲区中写入数据;
而int 16h 中断例程是在应用程序对其进行调用的时候,将数据从键盘缓冲区中读出。
- 1264. 17.2 使用int 16h中断例程读取键盘缓冲区我们在编写一般的处理键盘输入的程序的时候,可以调用int 16h 从键盘缓冲区中读取键盘的输入。
编程,接收用户的键盘输入,输入“r”,将屏幕上的字符设置为红色:输入“g”, 将屏幕上的字符设置为绿色;输入“b ”,将屏幕上的字符设置为蓝色。
程序源码
- 1265. 特别提示检测点17.1(page294)
没有通过此检测点,请不要向下进行!
- 1266. 17.3 字符串的输入用户通过键盘输入的通常不仅仅是单个字符而是字符串。
下面我们讨论字符串输入中的问题和简单的解决方法。
- 1267. 17.3 字符串的输入最基本的字符串输入程序,需要具备下面的功能:
(1) 在输入的同时需要显示这个字符串;
(2)一般在输入回车符后,字符串输入结束;
(3)能够删除已经输入的字符。
对于这三个功能,我们可以想象在DOS 中,输入命令行时的情况。
- 1268. 17.3 字符串的输入编写一个接收字符串的输入子程序,实现上面三个基本功能。因为在输入的过程中需要显示,子程序的参数如下:
(dh)、(dl)=字符串在屏幕上显示的行、列位置;
ds:si 指向字符串的存储空间,字符串以0为结尾符。
下面我们进行分析
- 1269. 17.3 字符串的输入(1)字符的输入和删除。每个新输入的字符都存储在前一个输入的字符之后,而删除是从最后面的字符进行的。
我们看下面的过程:
空字符串:
输入“a”:a
输入“b”:ab
输入“c”:abc
输入“d”:abcd
删除一个字符:abc
删除一个字符:ab
删除一个字符:a
删除一个字符:
- 1270. 17.3 字符串的输入可以看出在字符串输入的过程中,字符的输入和输出是按照栈的访问规则进行的,即后进先出。
这样,我们就可以用栈的方式来管理字符串的存储空间,也就是说,字符串的存储空间实际上是一个字符栈。
字符栈中的所有字符,从栈底到栈顶,组成一个字符串。
- 1271. 17.3 字符串的输入(2)在输入回车符后,字符串输入结束。
输入回车符后 ,我们可以在字符串中加入0,表示字符串结束。
- 1272. 17.3 字符串的输入(3) 在输入的同时需要显示这个字符串。
每次有新的字符输入和删除一个字符的时候,都应该重新显示字符串,即从字符栈的栈底到栈顶,显示所有的字符。
- 1273. 17.3 字符串的输入(4)程序的处理过程。现在我们可以简单地确定程序的处理过程如下:
① 调用int 16h读取键盘输入;
② 如果是字符,进入字符栈,显示字符栈中的所有字符;继续执行① ;
③ 如果是退格键,从字符栈中弹出一个字符,显示字符栈中的所有字符;继续执行① ;
④ 如果是Enter 键,向字符栈中压入0,返回。
- 1274. 17.3 字符串的输入从程序的处理过程中可以看出,字符栈的入栈、出栈和显示栈中的内容,是需要在多处使用的功能,我们应该将它们写为子程序。
- 1275. 17.3 字符串的输入子程序:字符栈的入栈、出栈和显示。
参数说明:
(ah)=功能号,0表示入栈,1表示出栈,2表示显示;
ds : si 指向字符栈空间;
对于0 号功能:(al)=入栈字符;
对于1 号功能:(al)=返回的字符;
对于2 号功能:(dh)、(dl) =字符串在屏幕上显示的行、列位置。
- 1276. 17.3 字符串的输入子程序代码
字符栈的访问规则如下所示:
(1)栈空
(2)“a”入栈
(3)“b”入栈
- 1277. 17.3 字符串的输入另外一个要注意的问题是:
显示栈中字符的时候,要注意清除屏幕上上一次显示的内容。
我们现在写出完整的接收字符串输入的子程序。
- 1278. 17.4 应用int13h中断例程对磁盘进行读写我们主要以 3.5 英寸软盘为例,进行讲解。
常用的3.5英寸软盘的结构:
分为上下两面,每面有80个磁道,每个磁道又分为18个扇区,每个扇区的大小为512B。
总容量为:2面×80磁道×18扇区×512B=1440KB≈1.44MB
- 1279. 17.4 应用int13h中断例程对磁盘进行读写磁盘的实际访问由磁盘控制器进行,我们可以通过控制磁盘控制器来访问磁盘。
只能以扇区为单位对磁盘进行读写。
在读写扇区的时候,要给出面号、磁道号和扇区号。
面号和磁道号从0开始,而扇区号从1开始。
- 1280. 17.4 应用int13h中断例程对磁盘进行读写如果我们通过直接控制磁盘控制器来访问磁盘,则需要涉及许多硬件细节。
BIOS提供了对扇区进行读写的中断例程,这些中断例程完成了许多复杂的和硬件相关的工作。
我们可以通过调用BIOS中断例程来访问磁盘。
- 1281. 17.4 应用int13h中断例程对磁盘进行读写BIOS 提供的访问磁盘的中断例程为int 13h 。
如下,读取0面0道1扇区的内容到0:200: mov ax,0
mov es,ax
mov bx,200h
mov al,1
mov ch,0
mov cl,1
mov dl,0
mov dh,0
mov ah,2
int 13h
- 1282. 17.4 应用int13h中断例程对磁盘进行读写入口参数:
(ah)=int 13h的功能号(2表示读扇区)
(al)=读取的扇区数
(ch)=磁道号
(cl)=扇区号
(dh)=磁头号(对于软驱即面号,因为一个面用一个磁头来读写)
(dl)=驱动器号
软驱从0开始,0:软驱A,1:软驱B;
硬盘从80h开始,80h:硬盘C,81h:硬盘D。
es:bx指向接收此扇区读入数据的内存区
- 1283. 17.4 应用int13h中断例程对磁盘进行读写返回参数:
操作成功: (ah)=0,(al)=读入的扇区数
操作失败: (ah)=出错代码
- 1284. 17.4 应用int13h中断例程对磁盘进行读写将0:200中的内容写入0面0道1扇区: mov ax,0
mov es,ax
mov bx,200h
mov al,1
mov ch,0
mov cl,1
mov dl,0
mov dh,0
mov ah,3
int 13h
- 1285. 17.4 应用int13h中断例程对磁盘进行读写入口参数:
(ah)=int 13h的功能号(3表示写扇区)
(al)=写入的扇区数
(ch)=磁道号
(cl)=扇区号
(dh)=磁头号(面)
(dl)=驱动器号
软驱从0开始,0:软驱A,1:软驱B;
硬盘从80h开始,80h:硬盘C,81h:硬盘D。
es:bx指向将写入磁盘的数据
- 1286. 17.4 应用int13h中断例程对磁盘进行读写返回参数:
操作成功: (ah)=0,(al)=写入的扇区数
操作失败: (ah)=出错代码
注意
- 1287. 17.4 应用int13h中断例程对磁盘进行读写注意:
下面我们要使用int 13h 中断例程对软盘进行读写。直接向磁盘扇区写入数据是很危险的,很可能覆盖掉重要的数据。
如果向软盘的0 面0 道1 扇区中写入了数据,要使软盘在现有的操作系统下可以使用,必须要重新格式化。
- 1288. 17.4 应用int13h中断例程对磁盘进行读写注意: (续)
在编写相关的程序之前,必须要找一张空闲的软盘。
在使用int 13h中断例程时一定要注意驱动器号是否正确,千万不要随便对硬盘中的扇区进行写入。
- 1289. 17.4 应用int13h中断例程对磁盘进行读写编程:
将当前屏幕的内容保存在磁盘上。
分析:
1 屏的内容占4000个字节,需要8 个扇区,我们用0面0道的1~8扇区存储显存中的内容。
程序代码
- 1290. 小结