寄存器主要 功能汇总
AX(累加器寄存器):主要用于算术和逻辑运算,以及存储函数返回值。
BX(基址寄存器):通常用作指针寄存器,存储内存访问的基地址。
CX(计数器寄存器):用于控制循环和计数操作。
DX(数据寄存器):用于存储一般数据,也可用于存储除法和乘法运算的结果。
SI(源变址寄存器):用于存储源数据的地址,通常与数据传输和字符串操作相关。
DI(目标变址寄存器):用于存储目标数据的地址,通常与数据传输和字符串操作相关。
SP(栈指针寄存器):指向栈的顶部,用于管理函数调用和局部变量。
BP(基址指针寄存器):在过程中用作堆栈帧指针,指向函数的参数和局部变量。
IP(指令指针寄存器):存储当前执行指令的内存地址。
CS(代码段寄存器):存储当前执行指令所在的代码段的基地址。
SS(堆栈段寄存器):存储堆栈的段基地址。
DS(数据段寄存器):存储数据段的基地址,用于访问数据。
ES(附加段寄存器):附加段寄存器,可用于访问额外的数据段。
PSW(程序状态字寄存器):存储程序运行的状态信息,包括标志位,如零标志、进位标志等。
1. 预备知识
1.1 进位记数制及不同数制间的转换
1.1.1什么是进位记数制
- 基数:在进位记数制中,一种记数制允许选用基本数字符号的个数叫做基数。
- 权:在一个数中,数字在不同的数位所代表的值是不同的,每个数字所表示的数值等于它本身乘以与所在数位有关的常数,把这个常数叫做位权,简称权。
1.1.2 计算机中常用的进位记数制
十进制:0 1 2 3 4 5 6 7 8 9
二进制:0 1
八进制:0 12 3 4 5 6 7
十六进制:0 1 2 3 4 5 6 7 8 9 a b c d e f
1.1.3 不同进位记数制之间的转换
二、八、十六进制→十进制
其他数制的数按各数位的权值展开求和,即采用“乘权求和”法。
十进制→二、八、十六进制
整数部分转换采用除基数取余法。小数部分转换采用乘基数取整法
其他数制数之间的转换
- 二进制数与八进制数之间的转换
- 以小数点为界,整数部分向左、小数部分向右每三位二进制数组成一位八进制数,不足三位者以o补荠(整数部分差补0,小数部分右补0。八进制数转换成三进制数,只需把每位八进制数用兰位二进制数表示即可。
- 例1.5 把二进制数10110.1转换成八进制数。
- ( 10110.1 ) 2 (10110.1)_2 (10110.1)2= ( 010110.100 ) 2 (010110.100)_2 (010110.100)2= ( 26.4 ) 8 (26.4)_8 (26.4)8
- 二进制数与十六进制数之间的转换
- 以小数点为界,整数部分向左、小数部分向右每四位二进制数组成一位十六进制数,不足四位者以0补齐(整数部分左补0,小数部分右补0)。十六进制数转换成二进制数,只需把每位十六进制数用四位二进制数表示即可。
- 例1.8 把十六进制数3A.5转换成二进制数
- ( 3 A . 5 ) 1 (3A.5)_1 (3A.5)1 6 _6 6= ( 00111010.0101 ) 2 (00111010.0101)_2 (00111010.0101)2
1.2 二进制数算术和逻辑运算
- 加法运算
- 二进制加法运算规则是逢2进1。即:
- 0+0=0
- 0+1=1
- 1+0=1
- 1+1=10
- 二进制加法运算规则是逢2进1。即:
- 减法运算
- 二进制减法运算规则是借1当2。即:
- 0-0=0
- 1-0=1
- 1-1=0
- 0-1=1(向高位借1)
- 二进制减法运算规则是借1当2。即:
- 乘法运算
- 二进制乘法运算规则是0乘以任何数得0,1乘以任何数得该数。即:
- 0×0=0
- 0×1=0
- 1×0=0
- 1×1=1
- 二进制乘法运算规则是0乘以任何数得0,1乘以任何数得该数。即:
- 除法运算
- 二进制除法运算规则是0除以1得0,1除以1得1,0做除数无意义。即:
- 0÷1=0
- 1÷1=1
- 二进制除法运算规则是0除以1得0,1除以1得1,0做除数无意义。即:
- 逻辑或运算
- 逻辑或运算规则是1和任何数相或得1,只有0与0相或得0。其运算符号为V或十。
- 0V0=0
- 0V1=1
- 1V0=1
- 1V1=1
- 例1.13 1100 V 1001=1101
- 逻辑或运算规则是1和任何数相或得1,只有0与0相或得0。其运算符号为V或十。
- 逻辑与运算
- 逻辑与运算规则是0和任何数相与得0,1和任何数相与该数值不变。其运算符号为
∧
\wedge
∧或X。
- 0 ∧ \wedge ∧ 0=0
- 0 ∧ \wedge ∧ 1=0
- 1 ∧ \wedge ∧ 0=0
- 1 ∧ \wedge ∧ 1=1或者表示为:
- 例1.14
- 1100 ∧ \wedge ∧ 1001=1000
- 逻辑与运算规则是0和任何数相与得0,1和任何数相与该数值不变。其运算符号为
∧
\wedge
∧或X。
- 逻辑异或运算
- 逻辑异或运算规则是0和任何数相异或该数值不变,1和任何数相异或该数值变反。其运算符号为:
- 0 ⨁ \bigoplus ⨁ 0=0
- 0 ⨁ \bigoplus ⨁ 1=1
- 1 ⨁ \bigoplus ⨁ 0=1
- 1 ⨁ \bigoplus ⨁ 1=0
- 例1.16 1101 ⨁ \bigoplus ⨁ 1001=0100
- 逻辑异或运算规则是0和任何数相异或该数值不变,1和任何数相异或该数值变反。其运算符号为:
1.3 数和字符在计算机中的表示方法
1.3.1整数在计算机中的表示
- 无符号整数
- 无符号整数包括整数0和正整数,在表示无符号整数时,全部数位均用来表示数值的大小,n位二进制数能够表示的无符号整数的范围是:0~( 2 n 2^n 2n-1)。
- 带符号整数
- 带符号整数包括整数0和正整数、负整数,在表示带符号整数时,一般用最高有效位表示数的符号,“O”表示正号,“1”表示负号,其余的数位是数值位。r位二进制数能够表示的带符号整数的范围是:-2n1~(2n1-1) 。
补码、反码
- 带符号整数包括整数0和正整数、负整数,在表示带符号整数时,一般用最高有效位表示数的符号,“O”表示正号,“1”表示负号,其余的数位是数值位。r位二进制数能够表示的带符号整数的范围是:-2n1~(2n1-1) 。
2. 微处理器的基础知识
2.1 80x86系列微处理器
- 实模式
- CPU复位(Reset)或加电(Power On)的时候以实模式启动,处理器以实模式工作。在实模式下,内存寻址方式和8086相同,由16位段寄存器的内容乘以16当做基地址,加上16位偏移地址形成20位的物理地址。在实模式下,所有的段都是可以读、写和可执行的。
- 保护模式
- 在保护模式下,CPU提供了多任务、内存分段、分页管理和特权级保护等功能这些功能是Windows/Linux等现代操作系统的基石。如果没有CPU的支持,操作系统的许多功能根本无法实现。例如,在实模式下,应用程序可以执行在何的CPU指令,读写所有的内存,DOS操作系统就不能控制应用程序的行为,应用程序可以做任何事情,没有在何限制。而在保护模式下,通过设置特权级和内存的分段分贡应用程序只能读写属于它首己的内存空间,而不能被坏其他应用程序和操作系统。
- 实模式下没有特权级的概念,相当于所有的指令都工作在特权级0,即最高的特权级。它可以执行所有特权指令,包括读写控制寄存器CRO等。Windows/Linux操作系统就是通过在实模式下初始化控制寄存器、GDTR、LDTR、IDTR、TR等寄存器以及页表,然后再通过置CRO的保护模式位(PE位)为1而进入保护模式的。实模式下不支持硬件上的多任务切换,所有的指令都在同一个环境下执行。
- 保护模式下提供的主要功能有:
- 段的大小可以设置为4GB,段内的偏移量为32位
- 特权级保护
- 支持内存分页机制,支持虚拟内存
- 支持多任务
- 虚拟86模式
- 虚拟86模式是以任务形式在保护模式下执行的,在CPU上可以同时支持由多个真正的CPU任务和多个虚拟86任务。在虚拟86模式下,CPU支持任务切换和内存分页。
2.2 程序可见寄存器组
程序可见寄存器组包括多个8位、16位和32位寄存器
- 通用寄存器
- 段寄存器
- 控制寄存器
2.3.1 通用寄存器
- 数据寄存器
- 四个16位寄存器:AX、BX、CX、DX (64K)
- 八个8位寄存器:AH、AL、BH、BL、CH、CL、DH、DL (256字)
- 四个32位寄存器:EAX、EBX、ECX、EDX
- 指针寄存器
- 堆栈指针寄存器:SP、ESP
- 功能:存放当前堆栈顶偏移量,总是与SS堆栈段寄存器配合存取堆栈中的数据
- 说明:实模式使用SP,保护模式使用ESP
- 基址指针寄存器:BP、EBP
- 功能:存放地址的偏移量部分或数据。若存放偏移量时,缺省情况与SS配合
- 说明:实模式使用BP,保护模式使用EBP
- 变址寄存器:SI DI ESI EDI
- 功能:存放地址的偏移量部分或数据,若存放偏移量时,缺省情况与DS配合
- 说明:实模式SI DI,保护模式ESI EDI
注意:除SP、ESP堆栈指针不能随意修改、需要慎用外,其他通用寄存器都可以直接在指令中使用,用以存放操作数,这是它们的通用之处。其他专用用途在具体指令中介绍
- 堆栈指针寄存器:SP、ESP
2.3.2 段寄存器
-
简介:IBM PC机的存储器采用分段管理方法组织,因此一个物理地址用段基址和偏移量表示。一个程序可以由多个段组成。
-
段寄存器功能:段寄存器存放段基址。在实模式下存放段基地址,在保护模式下存放段选择子。
-
代码段寄存器CS: 指定当前代码段,代码段中存放当前正在运行的程序段
-
堆栈段寄存器SS:指定当前堆栈段
说明:堆栈段是在内存开辟的一块特殊区域,其中的数据访问原则是后进先出(LIFO),允许插入和删除的一端叫做栈顶。IBM PC机中SP(或ESP)指向栈顶,SS指向堆栈段基地址。
- 数据段寄存器DS:指定当前运行程序所使用的数据段
- 附加数据段寄存器ES:指定当前运行程序所使用的附加数据段
- 段寄存器FS和GS:指定当前运行程序的另外两个存放数据的存储段
说明:
虽然DS、ES、FS、GS(甚至于CS、sS)所指定的段中都可以存放数据,但DS是主要的数据段寄存器,在默认情况下使用DS所指向段的数据。若要引用其他段中的数据,通常需要显式说明。
2.3.3 控制寄存器
控制寄存器包括指令指针寄存器和标志寄存器
注意:在程序中不能直接引用控制寄存器名
-
指令指针寄存器:IP EIP
- 总是与CS段寄存器配合指出下一条要执行的指令 的地址,其中存放偏移量部分,实模式使用IP,保护模式使用EIP
-
标志寄存器(FLAGS)
- 标志寄存器也被称为状态寄存器,由运算结果特征标志和控制标志组成
- 各档CPU均有标志。即8086拥有的九个标志
- 运算结果特征标志
- 用于记录程序中运行结果的特征。
- CF、PF、AF、AF、SF、OF六个标志
- CF:进位标志,记录运算结果的最高位向前产生的进位或借位。可用于检测无符号数运算时是否发生溢出。
- CF=1 有进位或借位
- CF=0 无进位或借位
- PF:奇偶标志,记录运算结果最低8位中含1的个数。可用于检测数据传送过程中是否发生错误。
- PF=1 个数为偶数
- PF=0 个数为奇数
- AF:辅助进位标志,记录运算结果最低4位向前产生的进位或借位。只有在执行十进制运算指令时才关心此位
- AF=1 有进位或借位
- AF=0 无进位或借位
- ZF:零标志位,记录运算结果是否为0
- ZF=1 运算结果为0
- ZF=0 结果非0
- SF:符号标志,记录运算结果的符号
- SF=1 运算结果为负
- SF=0 结果为非负
- OF :溢出标志,记录运算结果是否超出了及其所能表示的范围。可用于检测带符号数运算时是否发生溢出
- OF=1 运算结果超出范围
- OF=0 结果未超出
- CF:进位标志,记录运算结果的最高位向前产生的进位或借位。可用于检测无符号数运算时是否发生溢出。
- 控制标志
- 控制标志控制处理器的操作,要通过专门的指令才能使控制标志发生变化
- IF、DF、TF
- IF:中断允许标志。IF的控制只对外部可屏蔽中断请求(INTR)起作用
- IF=1 允许CPU响应INTR
- IF=0 禁止响应INTR
- DF:方向标志,专服务于字符串操作指令,指示串操作数地址的增减方向
- DF=1 串操作时操作数地址为自动减量
- DF=0 串操作时操作数地址为自动增量
- TF:陷阱标志,用于程序调试
- TF=1 CPU处于单步方式
- TF=0 CPU处于连续方式
- NT:嵌套任务标志。
- 保护模式在执行中断返回指令IRET时要测试NT值。当NT一1时,表示当前执行的任务嵌套于另一任务之中,执行完该任务后要返回到另一任务,IRET指令的执行是通过任务切换实现的。当NT0时,用堆栈中保存的值恢复FLAGS、cST 及IP寄存器的内容,以执行常规的IRET中断返回操作。
- IF:中断允许标志。IF的控制只对外部可屏蔽中断请求(INTR)起作用
2.3 存储器
基本概念
- 二进制位:存储信息的基本单位,可用小写字母b表示。
- 字节:存取信息的基本单位,可用大写字母B表示。一个字节由八位二进制数组成,占用一个存储单元。其位编号自左至右为 b 7 b 6 b 5 b 4 b 3 b 2 b 1 b 0 b_7b_6b_5b_4b_3b_2b_1b_0 b7b6b5b4b3b2b1b0。
- 字:一个字16位,占用两个存储单元。其位编号为 b 1 b_1 b1 5 _5 5~b 0 _0 0。
- 双字:一个双字32位,占用四个存储单元。其位编号为 b 3 b_3 b3 1 _1 1~b 0 _0 0
- 四字:一个四字64位,占用八个存储单元。其位编号为 b 6 b_6 b6 4 _4 4~b 0 _0 0。
- 逆序存放
- 照Intel公司的习惯,对于字、双字、四字数据类型,其低地址中存放低位字节数据,高地址中存放高位字节数据,这就是有些资料中称为“逆序存放”的含义。
- 存储器分段管理
- IBM PC机的存储器采用分段管理的方法。存储器采用分段管理后,一个内存单元地址要用段基地址和偏移量两个逻辑地址来描述,表示为段基址:偏移量,其段基址和偏移量的限定、物理地址的形成视CPU工作模式决定。
- 对段基址的限定
- 只要工作在实模式,段基址必须定位在地址为16的整数倍上,这种段起始边界通常称做节或小段。段基址有了这样的规定,1M字节空间的20位地址的低4位可以不表示出,而高16位就可以完全放入段寄存器了。
- 对段长的限定
- 在实模式下段长不能超过64K。
存储器采用分段管理后,其物理地址的计算方法为:
- 10H X 段基址 + 偏移量
- (其中H表示的是十六进制数)
- 简便的计算方法:因为段基址和偏移量一般用十六进制数表示,物理地址简便的计算方法是在段基址的最低位补以OH,再加上偏移量
2.4 汇编语言概述
程序设计语言包括
- 机器语言:直接用二进制代码的机器指令表示的语言。
- 用机器语言书写的程序->机器语言源程序
- 计算机可以直接运行的机器语言源程序。
- 机器语言程序的执行效率高
- 汇编语言:用助记符、符号地址、标号等符号书写程序的语言.
- 用汇编语言书写的程序->汇编语言源程序
- 计算机不能nche直接运行汇编语言源程序
- 汇编语言程序的执行效率高
- 高级语言:类似于人类语言的语言
3. CPU概述
- 一个典型的CPU由运算器、控制器、寄存器等器件组成,这些器件靠内部总线相连。
- 内部总线实现CPU内部各个器件之间的联系。
- 外部总线实现CPU和主板上其它器件的联系。
3.2寄存器概述
- n8086CPU有14个寄存器 它们的名称为:
- AX、BX、CX、DX、SI、DI、SP、BP、 IP、CS、SS、DS、ES、PSW。
3.3 几条汇编指令
汇编指令不区分大小写
CPU执行下表中的程序段的每条指令后,对寄存器中的数据进行的改变。
3.4 物理地址
- CPU访问内存单元时要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间。
- 每一个内存单元在这个空间中都有唯一的地址,这个唯一的地址称为物理地址。
3.5 16位结构的CPU
- 概括的讲,16位结构描述了一个CPU具有以下几个方面特征:
- 运算器一次最多可以处理16位的数据。
- 寄存器的最大宽度为16位。
- 寄存器和运算器之间的通路是16位的。
3.6 8086CPU给出物理地址的方法
- 8086有 20位地址总线,可传送20位地址,寻址能力为1M。
- 2的20次方为1048576,大约为1M
- 8086内部为16位结构,它只能传送16位的地址,表现出的寻址能力却只有64K。
- 2 1 2^1 21 6 ^6 6 =65536
- 8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址
3.7 地址加法器
- 地址加法器合成物理地址的方法:
物理地址=段地址×16+偏移地址
观察移位次数和各种形式数据的关系:
- 一个数据的二进制形式左移1位,相当于该数据乘以2;
- 一个数据的二进制形式左移N位,相当于该数据乘以2的N次方;
- 地址加法器如何完成段地址×16的运算?
以二进制形式存放的段地址左移4位。
3.8 段的概念
- 错误认识:
- 内存被划分成了一个一个的段,每一个段有一个段地址。
- 其实:内存并没有分段,段的划分来自于CPU,由于8086CPU用“(段地址×16)+偏移地址=物理地址”的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存。(分段管理整块的内存)
- 我们可以认为:地址10000H~100FFH的内存单元组成一个段,该段的起始地址( 基础地址)为10000H,段地址为1000H,大小为100H。
- 我们也可以认为地址10000H1007FH、10080H100FFH 的内存单元组成两个段,它们的起始地址( 基础地址 )为10000H和10080H,段地址为:1000H 和1008H,大小都为80H。
- 以后,在编程时可以根据需要,将若干地址连续的内存单元看作一个段,用段地址×16定位段的起始地址(基础地址),用偏移地址定位段中的内存单元。
两点需要注意
- 段地址×16 必然是 16的倍数,所以一个段的起始地址也一定是16的倍数;
- 偏移地址为16位,16 位地址的寻址能力为 64K,所以一个段的长度最大为64K。
小结
- CPU访问内存单元时,必须向内存提供内存单元的物理地址。
- 8086CPU在内部用段地址和偏移地址移位相加的方法形成最终的物理地址。
Question: - 观察下面的地址,读者有什么发现?
- 结论:CPU可以用不同的段地址和偏移地址形成同一个物理地址。
- 如果给定一个段地址,仅通过变化偏移地址来进行寻址,最多可以定位多少内存单元?
- 结论:偏移地址16位,变化范围为0~FFFFH,仅用偏移地址来寻址最多可寻64K个内存单元。
- 比如:给定段地址1000H,用偏移地址寻址,CPU的寻址范围为:10000H~1FFFFH。
- 2的四次方的四次方->2的16次方->16位->64K
内存单元地址小结
- 在8086PC机中,存储单元的地址用两个元素来描述。即段地址和偏移地址。
- “数据在21F60H内存单元中。”对于8086PC机的两种描述:
- (a)数据存在内存2000:1F60单元中;
- (b)数据存在内存的2000H段中的1F60H单元中。
- 可根据需要,将地址连续、起始地址为16的倍数的一组内存单元定义为一个段。
3.9 段寄存器
- 段寄存器就是提供段地址的, 8086CPU有4个段寄存器:
- CS、DS、SS、ES
- 当8086CPU要访问内存时,由这4个段寄存器提供内存单元的段地址。
3.10 CS和IP
- CS和IP是8086CPU中最关键的寄存器,它们指示了CPU当前要读取指令的地址。
- CS为代码段寄存器;(code)
- IP为指令指针寄存器。(intr)
8086PC读取和执行指令相关部件
8086PC工作过程的简要描述
(1)从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲器;
(2)IP = IP + 所读取指令的长度,从而指向下一条指令;
(3)执行指令。 转到步骤 (1),重复这个过程。
在 8086CPU 加电启动或复位后( 即 CPU刚开始工作时)CS和IP被设置为
CS=FFFFH,IP=0000H,即在8086PC机刚启动时,CPU从内存FFFF0H单元中读取指令执行,FFFF0H单元中的指令是8086PC机开机后执行的第一条指令。
- 内存中指令和数据没有任何区别,都是二进制信息,CPU在工作的时候把有的信息看作指令,有的信息看作数据。
- CPU根据什么将内存中的信息看作指令?
CPU将CS:IP指向的内存单元中的内容看作指令。 - 在任何时候,CPU将CS、IP中的内容当作指令的段地址和偏移地址,用它们合成指令的物理地址,到内存中读取指令码,执行。
- 如果说,内存中的一段信息曾被CPU执行过的话,那么,它所在的内存单元必然被CS:IP指向过。
3.11 修改CS、IP的指令
- 在CPU中,程序员能够用指令读写的部件只有寄存器,程序员可以通过改变寄存器中的内容实现对CPU的控制。
- CPU从何处执行指令是由CS、IP中的内容决定的,程序员可以通过改变CS、IP中的内容来控制CPU执行目标指令。
- 我们如何改变CS、IP的值呢?
- 先回想我们如何修改AX中的值?
- mov 指令
如:mov ax,123
mov指令可以改变8086CPU大部分寄存器的值,被称为传送指令。
能够通过mov 指令改变CS、IP的值吗? - mov指令不能用于设置CS、IP的值 8086CPU没有提供这样的功能。
- mov 指令
- 8086CPU为CS、IP提供了另外的指令来改变它们的值:转移指令
- 同时修改CS、IP的内容jmp 段地址:偏移地址(占用六位)
jmp 2AE3:3
jmp 3:0B16 - 功能:用指令中给出的段地址修改CS,偏移地址修改IP。
- 仅修改IP的内容:(占用四位)
jmp 某一合法寄存器
jmp ax (类似于 mov IP,ax)
jmp bx- 功能:用寄存器中的值修改IP。
- 同时修改CS、IP的内容jmp 段地址:偏移地址(占用六位)
- 先回想我们如何修改AX中的值?
3.12 代码段
- 对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。
- 可以将长度为 N( N≤64KB )的一组代码,存在一组地址连续、起始地址为 16的倍数的内存单元中,这段内存是用来存放代码的,从而定义了一个代码段。
- mov ax,0000 (B8 00 00)
add ax,0123H (05 23 01)
mov bx,ax (8B D8)
jmp bx (FF E3)
- mov ax,0000 (B8 00 00)
- 如何使得代码段中的指令被执行呢?
- 将一段内存当作代码段,仅仅是我们在编程时的一种安排,CPU 并不会由于这种安排,就自动地将我们定义得代码段中的指令当作指令来执行。
- CPU 只认被 CS:IP 指向的内存单元中的内容为指令。
- 所以要将CS:IP指向所定义的代码段中的第一条指令的首地址。
- CS = 123BH,IP = 0000H。
4. 内存访问
- 存储器
- 栈
4.1 内存中字的存储
- 在0地址处开始存放20000:
- 0号单元是低地址单元,1号单元是高地址单元。
- 问题:
(1)0地址单元中存放的字节型数据是多少?
(2)0地址字单元中存放的字型数据是多少?
(3)2地址字单元中存放的字节型数据是多少
(4)2地址单元中存放的字型数据是多少?
(5)1地址字单元中存放的字型数据是多少?
- 问题:
- 结论:任何两个地址连续的内存单元,N号单元和 N+1号单元,可以将它们看成两个内存单元 ,也可以看成一个地址为N的字单元中的高位字节单元和低位字节单元。
4.2 DS和[address]
- CPU要读取一个内存单元的时候,必须先给出这个内存单元的地址;
- 在8086PC中,内存地址由段地址和偏移地址组成。
- 8086CPU中有一个 DS寄存器,通常用来存放要访问的数据的段地址。
例如:我们要读取10000H单元的内容可以用如下程序段进行:
mov bx,1000H
mov ds,bx
mov al,[0]
上面三条指令将10000H(1000:0)中的数据读到al中。
mov al,[0]
已知的mov指令可完成的两种传送功能:
(1)将数据直接送入寄存器;
(2)将一个寄存器中的内容送入另一个寄存器中。
mov 指令 还可以将一个内存单元中的内容送入一个寄存器。
- 从哪个内存单元送到哪个寄存器中呢?
- mov指令的格式:
- mov 寄存器名,内存单元地址
- “[…]”表示一个内存单元, “[…]”中的0表示内存单元的偏移地址。
那么内存单元的段地址是多少呢?
- mov指令的格式:
- 执行指令时,8086CPU自动取DS中的数据为内存单元的段地址。
- 如何用mov指令从10000H中读取数据?
- 10000H表示为1000:0(段地址:偏移地址)
- 将段地址1000H放入ds
- 用mov al,[0]完成传送(mov指令中的[]说明操作对象是一个内存单元,[]中的0说明这个内存单元的偏移地址是0,它的段地址默认放在ds中)
- 如何用mov指令从10000H中读取数据?
- 如何把1000H送入ds?
- 传送指令 mov ax,1
- 相似的方式 mov ds,1000H?
- 8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器。
- mov ds,1000H 是非法的。
- 数据->一般的寄存器->段寄存器
- 因为8086CPU是16位结构,有16根数据线,所以,可以一次性传送16位的数据,也就是一次性传送一个字。
4.3 字的传送
因为8086CPU是16位结构,有16根数据线,所以,可以一次性传送16位的数据,也就是一次性传送一个字。(两个存储单元)
问题3.4:内存中的情况如右图,写出下面指令执行后寄存器ax,bx,cx中的值。
一次传输16位的字,占用了两个存储单元(10000H、10001H)
4.4 mov、add、sub指令
已学mov指令的几种形式:
mov 寄存器,数据
mov 寄存器,寄存器
mov 寄存器,内存单元
mov 内存单元,寄存器
mov 段寄存器,寄存器
根据已知指令进行推测:
- mov 段寄存器,寄存器
- mov 寄存器,段寄存器(验证)
- mov 内存单元,寄存器
- mov 内存单元,段寄存器
- mov 段寄存器,内存单元
nadd和sub指令同mov一样,都有两个操作对象。
4.5 数据段
- 将一组长度为N(N≤64K)、地址连续、起始地址为16的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。
- 比如我们用123B0H~123B9H这段空间来存放数据:
- 段地址:123BH
- 长度:10字节
小结
(1)字在内存中存储时 ,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放再高地址单元中。
(2)用 mov 指令要访问内存单元,可以在mov指令中只给出单元的偏移地址,此时,段地址默认在DS寄存器中。
(3)[address]表示一个偏移地址为address的内存单元。
(4)在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。
(5)mov、add、sub是具有两个操作对象的指令。jmp是具有一个操作对象的指令。
4.6 栈
栈是一种具有特殊的访问方式的存储空间。它的特殊性就在于,最后进入这个空间的数据,最先出去。(后进先出)
4.7 CPU提供的栈机制
- 8086CPU提供相关的指令来以栈的方式访问内存空间。
- 这意味着,我们在基于8086CPU编程的时候,可以将一段内存当作栈来使用。
- 8086CPU提供入栈和出栈指令: (最基本的)
- PUSH(入栈)
- POP (出栈)
- push ax:将寄存器ax中的数据送入栈中;
- pop ax :从栈顶取出数据送入ax。
- 8086CPU的入栈和出栈操作都是以字为单位进行的。
我们可以将10000H~1000FH这段内存当作栈来使用。
下面一段指令的执行过程:
mov ax,0123H
push ax
mov bx,2266H
push bx
mov cx,1122H
push cx
pop ax
pop bx
pop cx
Question
- CPU如何知道一段内存空间被当作栈使用?
- 执行push和pop的时候,如何知道哪个单元是栈顶单元?
回想:CPU如何指导当前要执行的指令所在的位置?
寄存器CS和IP中存放着当前指令的段地址和偏移地址。
8086CPU中,有两个寄存器:
段寄存器SS 存放栈顶的段地址
寄存器SP 存放栈顶的偏移地址
任意时刻,SS:SP指向栈顶元素。
push指令的执行过程
push ax
(1)SP=SP–2;
(2)将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶。
4.6 栈
如果我们将10000H~1000FH 这段空间当作栈,初始状态栈是空的,此时,SS=1000H,SP=?
A:SP = 0010H
占满一个
- 我们将10000H~1000FH 这段空间当作栈段,SS=1000H,栈空间大小为16 字节 ,栈最底部的字单元地址为1000:000E。
任意时刻,SS:SP指向栈顶,当栈中只有一个元素的时候,SS = 1000H,SP=000EH。 (参考图二) - 栈为空,就相当于栈中唯一的元素出栈,出栈后,SP=SP+2 ,SP 原来为 000EH,加 2 后SP=10H,所以,当栈为空的时候,SS=1000H,SP=10H。
pop 指令的执行过程
pop ax
(1)将SS:SP指向的内存单元处的数据送入ax中;
(2)SP = SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
注意:
出栈后,SS:SP指向新的栈顶 1000EH,pop操作前的栈顶元素,1000CH 处的2266H 依然存在 ,但是,它已不在栈中。
当再次执行push等入栈指令后,SS:SP移至1000CH,并在里面写入新的数据,它将被覆盖。
4.8 栈顶超界的问题
- 栈满的时候再使用push指令入栈,
- 栈空的时候再使用pop指令出栈,
都将发生栈顶超界问题。
栈顶超界是危险的:
因为我们既然将一段空间安排为栈 ,那么在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己的程序中的,也可能是别的程序中的。
结论:
我们在编程的时候要自己操心栈顶超界的问题 ,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
4.9 push、pop指令
push和pop指令是可以在寄存器和内存之间传送数据的。
栈空间当然也是内存空间的一部分,它只是一段可以以一种特殊的方式进行访问的内存空间。
- push和pop指令的格式(1)
- push 寄存器:将一个寄存器中的数据入栈
- pop寄存器:出栈,用一个寄存器接收出栈的数据
例如:push ax
pop bx
- push和pop指令的格式(2)
- push 段寄存器:将一个段寄存器中的数据入栈
- pop 段寄存器:出栈,用一个段寄存器接收出栈的数据
例如:push ds (数据段寄存器)
pop es(附加数据段寄存器)
- push和pop指令的格式(3)
- push内存单元:将一个内存单元处的字入栈(栈操作都是以字为单位)
- pop 内存单元:出栈,用一个内存字单元接收出栈的数据
例如:push [0]
pop [2]
指令执行时 ,CPU 要知道内存单元的地址,可以在 push、pop 指令中给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得。
栈的综述:
(1)8086CPU提供了栈操作机制,方案如下:
在SS,SP中存放栈顶的段地址和偏移地址;
提供入栈和出栈指令,他们根据SS:SP指示的地址,按照栈的方式访问内存单元。
(2)push指令的执行步骤:
1)SP=SP-2;
2)向SS:SP指向的字单元中送入数据。
(3)pop指令的执行步骤:
1)从SS:SP指向的字单元中读取数据;
2)SP=SP-2。
(4)任意时刻,SS:SP指向栈顶元素。
(5)8086CPU只记录栈顶,栈空间的大小我们要自己管理。
(6)用栈来暂存以后需要恢复的寄存器的内容时 ,寄存器出栈的顺序要和 入栈的顺序相反。
(7)push、pop实质上是一种内存传送指令,注意它们的灵活应用。
4.10 栈段
- 前面讲过,对于8086PC机,在编程时,我们可以根据需要 ,将一组内存单元定义为一个段。
- 我们可以将长度为 N(N ≤64K )的一组地址连续、起始地址为16的倍数的内存单元,当作栈来用,从而定义了一个栈段。
- 比如我们将10010H~1001FH 这段长度为 16 字节的内存空间当作栈来用,以栈的方式进行访问。
- 这段空间就可以成为栈段,段地址为1000H,大小为16字节。
一个栈段最大可以设为多少?
栈顶的变化范围是0~FFFFH,从栈空时候的SP=0,一直压栈,直到栈满时SP=0;如果再次压栈,栈顶将环绕,覆盖了原来栈中的内容。
所以一个栈段的容量最大为64KB
我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。这完全是我们自己的安排。
我们可以用一个段存放数据,将它定义为“数据段”;
我们可以用一个段存放代码,将它定义为“代码段”;
我们可以用一个段当作栈,将它定义为“栈段”;
5 第一个程序
5.1 一个源程序从写出到执行的过程
- 使用文本编辑器(如Edit、记事本等),用汇编语言编写汇编源程序。
- 使用汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件;再用连接程序对目标文件进行连接,生成可在操作系统中直接运行的可执行文件。
- 可执行文件中包含两部分内容:
- 程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
- 相关的描述信息(比如:程序有多大、要占多少内存空间等)
- 在操作系统中,执行可执行文件中的程序。
- 操作系统依照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化(比如:设置CS:IP指向第一条要执行的指令),然后由CPU执行程序。
5.2 源程序
-
assume ->伪指令,只有再编译时生效
-
segment、end相当于{}
-
一个汇编程序是由多个段组成的,这些段被用来存放代码、数据或当作栈空间来使用。
-
End 是一个汇编程序的结束标记,编译器在编译汇编程序的过程中,如果碰到了伪指令 end,就结束对源程序的编译。
-
如果程序写完了,要在结尾处加上伪指令end 。否则,编译器在编译程序时,无法知道程序在何处结束。注意:不要搞混了end和ends。
-
assume:含义为“假设”。
- 它假设某一段寄存器和程序中的某一个用 segment … ends 定义的段相关联。
- 通过assume说明这种关联,在需要的情况下 ,编译程序可以将段寄存器和某一个具体的段相联系。
-
源程序中的“程序”
- 汇编源程序:
- 伪指令 (编译器处理)
- 汇编指令(编译为机器码)
- 汇编源程序:
-
codesg:放在segment的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。
- exe的执行过程:
(1)我们在提示符“C:\masm”后面输入可执行文件的名字“1”,按Enter键。
(2)1.exe中的程序运行;
(3)运行结束,返回,再次显示提示符“C:\masm”。
5.5 程序执行过程的跟踪
用U命令查看一下其他指令:
用T命令担不执行程序中的每一条指令,并观察每条指令的执行结果,到了 int 21,我们要用P命令执行:
6. 寻址一
6.1 [bx]和内存单元的描述
[bx]是什么呢?
和[0]有些类似,[0]表示内存单元,它的偏移地址是0。
完整地描述一个内存单元,需要两种信息:
(1)内存单元的地址;
(2)内存单元的长度(类型)。
我们用[0]表示一个内存单元时,0 表示单元的偏移地址,段地址默认在ds中,单元的长度(类型)可以由具体指令中的其他操作对象(比如说寄存器)指出。
[bx]同样也表示一个内存单元,它的偏移地址在bx中,比如下面的指令:
- mov ax,[bx]
- mov al,[bx]
6.2 loop
英文单词“loop”有循环的含义,显然这个指令和循环有关。
6.3 描述性符号“()”
“() ”来表示一个寄存器或一个内存单元中的内容。
我们看一下(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)
;
(5)对于 add ax,2 的功能,我们可以这样来描述:(ax)=(ax)+2;
(6)对于add ax,bx的功能,我们可以这样来描述:(ax)=(ax)+(bx);
(7)对于push ax的功能,我们可以这样来描述: (sp) = (sp)-2
((ss)*16+(sp))=(ax)
(8)对于pop ax 的功能,我们可以这样来描述:
(ax)=((ss)*16+(sp)) (sp)=(sp)+2
6.3 约定符号idata表示常量
我们在Debug 中写过类似的指令:mov ax,[0],表示将 ds:0 处的数据送入ax中。指令中,在“[…]”里用一个常量0表示内存单元的偏移地址。以后,我们用idata表示常量。
比如:
- mov ax,[idata]就代表mov ax,[1]、mov ax,[2]、mov ax,[3]等。
- mov bx,idata就代表mov bx,1、mov bx,2、mov bx,3等。
- ax 和 bx 是通用寄存器可以直接操作
- mov ds,idata就代表mov ds,1、mov ds,2等,它们都是非法指令。
- 段寄存器不能直接加载一个立即数(如 1 或 2)作为地址。
6.4 寄存器间接寻址
下面指令的功能:
- mov ax,[bx]
功能:bx 中存放的数据作为一个偏移地址EA ,段地址SA 默认在ds 中,将SA:EA处的数据送入ax中。
即:(ax)=(ds *16 +(bx));
- mov [bx],ax
功能:bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将ax中的数据送入内存SA:EA处。
即:(ds *16 +(bx)) = (ax)。
- bx,si,di,bp用法及区别
bp默认的段地址是ss
6.5 Loop指令
- 指令的格式是:loop 标号,CPU 执行loop指令的时候,要进行两步操作:
① (cx)=(cx)-1;
② 判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。 - 从上面的描述中,可以看到,cx中的值影响着loop指令的执行结果。
- 通常(注意,我们说的是通常)用loop指令来实现循环功能,cx 中存放循环次数。
Question:任务3:编程计算 2 1 2^1 21 2 ^2 2。
(ax)= (ax)*2*2*2*2*2*2*2*2*2*2*2,最后(ax)中为2∧12的值。N*2可用N+N 实现。
程序代码:
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
6.6 loop和[bx]的联合应用
考虑这样一个问题:
计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
我们还是先分析一下
计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
分析:
(1)运算后的结果是否会超出 dx 所能存储的范围?
- ffff:0~ffff:b内存单元中的数据是字节型数据,范围在0~255(
2
8
2^8
28)之间,12个这样的数据相加,结果不会大于 65535 ,可以在dx中存放下。
(2)我们是否将 ffff:0~ffff:b中的数据直接累加到dx中?
当然不行,因为ffff:0~ffff:b中的数据是8位的,不能直接加到16位寄存器dx中。
(3)我们能否将ffff:0~ffff:b中的数据累加到dl中,并设置(dh=0,从而实现累加到dx中的目标?
这也不行,因为dl是8位寄存器,能容纳的数据的范围在小 255 之间,ffff : 0~ffff:b中的数据也都是 8 位,如果仅向dl中累加12个 8 位数据,很有可能造成进位丢失。
(4)我们到底怎样将用ffff:0~ffff:b中的8位数据,累加到16位寄存器dx中
从上面的分析中,我们可以看到,这里面有两个问题:类型的匹配和结果的不超界。
具体的说,就是在做加法的时候,我们有两种方法:
(dx)=(dx)+内存中的8位数据:
(dl)=(dl)+内存中的8 位数据;
第一种方法中的问题是两个运算对象的类型不匹配,第二种方法中的问题是结果有可能超界。
怎样解决这两个看似矛盾的问题?
目前的方法(在后面的课程中我们还有别的方法)就是我们得用一个16位寄存器来做中介
我们将内存单元中的 8 位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx上,从而使两个运算对象的类型匹配并且结果不会超界。
mov al,ds:[x]
mov ah,0
add dx,ax
我们可以看到,12个相似的程序段中,只有mov al,ds:[X]
指令中的内存单元的偏移地址是不同的,其他都一样。
而这些不同的偏移地址是可在0≤X≤0bH的范围内递增变化的。
我们可以用数学语言来描述这个累加的运算
从程序实现上,我们将循环做:
(al )=((ds)*16+X)
(ah)=0
(dx)=(dx)+(ax)
一共循环12次,在循环开始前(dx)=0ffffh,X=0,ds:X指向第一个内存单元。每次循环后,X递增,ds:X指向下一个内存单元。
6.7 一段安全的空间
随意向一段内存空间写入内容是很危险的 ,因为这段空间中可能存放着重要的系统数据或代码。
比如下面的指令:
mov ax,1000h
mov ds,ax
mov al,0
mov ds:[0],al
在一般的PC机中,DOS方式下,DOS和其他合法的程序一般都不会使用0:200~0:2FF( 0:200h~0:2FFh)的256 个字节的空间。所以,我们使用这段空间是安全的。
7. 寻址二
7.1 and和or指令
(1)and 指令:逻辑与指令,按位进行与运算。
如 mov al, 01100011B
and al, 00111011B
执行后:al = 00100011B
通过该指令可将操作对象的相应位设为0,其他位不变。
(2)or 指令:逻辑或指令,按位进行或运算。
如 mov al, 01100011B
or al, 00111011B
执行后:al = 01111011B
通过该指令可将操作对象的相应位设为1,其他位不变。
7.2 ASCII码大小写转换的问题
同一个字母的大写字符和小写字符对应的 ASCII 码是不同的,比如 “A” 的 ASCII 码是41H,“a”的ASCII码是61H。
要改变一个字母的大小写,实际上就是要改变它所对应的ASCII 码。
将所有的字母的大写字符和小写字符所对应的ASCII码列出来,进行对比,从中找到规律。
大写 二进制 小写 二进制
A 01000001 a 01100001
B 01000010 b 01100010
C 01000011 c 01100011
D 01000100 d 01100100
一个字母,我们不管它原来是大写还是小写:
我们将它的第5位 置0(从零开始的计算,第五位),它就必将变为大写字母;
将它的第5 位 置1,它就必将变为小写字母。
7.3 寄存器相对寻址
在前面,我们可以用[bx]的方式来指明一个内存单元, 我们还可以用一种更为灵活的方式来指明内存单元:
[bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata(bx中的数值加上idata)。
- mov ax,[bx+200]的含义:
- 将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上200,段地址在ds中。
- 数学化的描述为:
(ax)=((ds)*16+(bx)+200)
- 指令mov ax,[bx+200]也可以写成如下格式(常用):
mov ax,[200+bx]
mov ax,200[bx]
mov ax,[bx].200
7.4 用[bx+idata]的方式进行数组的处理
在codesg中填写代码,将datasg中定义的第一个字符串,转化为大写,第二个字符串转化为小写。
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'MinIX'
datasg ends
codesg segment
start: ……
codesg ends
end start
方法一:
mov ax,datasg
mov ds,ax
mov bx,0 ;存放地址偏移量
mov cx,5
s: mov al,[bx] ;读取一个字节数据到AL中
and al,11011111b ;从零开始处理
mov [bx],al ;将处理后的数据存回偏移位置
inc bx ;inc:偏移量加一
loop s ;重复s
mov bx,5
mov cx,5
s0: mov al,[bx]
or al,00100000b
mov [bx],al
inc bx
loop s0
改进的程序:
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] ;定位第二个字符串的字符(通过+5)
or al,00100000b
mov [5+bx],al
inc bx
loop s
- C语言定位方式:a[i],b[i]
- 汇编语言定位方式:0[bx],5[bx]
7.5 SI和DI
- SI和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]
- 下面的三组指令实现了相同的功能:
- 下面的三组指令也实现了相同的功能:
(1) mov bx,0
mov ax,[bx+123]
(2) mov si,0
mov ax,[si+123]
(3) mov di,0
mov ax,[di+123]
Question:
用寄存器SI和DI实现将字符串‘welcome to masm!’复制到它后面的数据区中。
assume cs:codesg,ds:datasg
datasg segment
db 'welcome to masm!'
db '................'
datasg ends
‘welcome to masm!’:一共十六个字
Answer:
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
注意,在程序中,我们用16位寄存器进行内存单元之间的数据传送,一次复制 2 个字节,一共循环8次。
用更少的代码,实现前面的程序。
可以利用[bx(si或di)+idata]
的方式,来使程序变得简洁。
只通过一个偏移量,偏移量+16即可以得到需要复制到的位置
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
7.6 基址 变址 寻址
- 在前面,我们用
[bx(si或di)]
和[bx(si或di)+idata]
的方式来指明一个内存单元,我们还可以用更灵活的方式:- [bx+si]
- [bx+di]
- [bx+si]和[bx+di]的含义相似,我们以[bx+si]为例进行讲解。
- [bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)(即bx中的数值加上si中的数值)。
- 我们看下指令mov ax,[bx+si]的含义:
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值,段地址在ds中。- 指令mov ax,[bx+si]的数学化的描述为:
(ax)=( (ds)*16+(bx)+(si) )
- 该指令也可以写成如下格式(常用):
mov ax,[bx][si]
- 指令mov ax,[bx+si]的数学化的描述为:
- 与寻址有关的寄存器:bx,si,di,bp
- 基址变址寻址方式的4种组合:jsakdljl
- bx+si
- bx+di
- bp+si
- bp+di
- 基址变址寻址方式的4种组合:jsakdljl
- 寻址方式的意义
- [bx+si+idata]和[bx+di+idata]的含义相似,我们以[bx+si+idata]为例进行讲解。
- [bx+si+idata]表示一个内存单元
- 它的偏移地址为(bx)+(si)+idata。 (即bx中的数值加上si中的数值再加上idata)
- [bx+si+idata]表示一个内存单元
- [bx+si+idata]和[bx+di+idata]的含义相似,我们以[bx+si+idata]为例进行讲解。
- 指令mov ax,[bx+si+idata]的含义:
- 将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值再加上idata,段地址在ds中。
数学化的描述为:(ax)=( (ds)*16+(bx)+(si)+idata )
- 将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值再加上idata,段地址在ds中。
8. 转移指令
8086CPU的转移指令分为以下几类:
无条件转移指令 (如:jmp)
条件转移指令
循环指令(如:loop)
过程
中断
8.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
Question:
有如下程序段,添写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
问题分析
(1)s和s0处的指令所在的内存单元的地址是多少?
cs:offset s 和cs:offset s0。
(2)将s处的指令复制到s0处,就是将cs:offeet s 处的数据复制到cs:offset s0处;
(3)段地址已知在cs中,偏移地址offset s和offset s0已经送入si和di中;
(4)要复制的数据有多长?
mov ax,bx指令的长度为两个字节,即1个字。
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
8.2 jmp指令
- jmp为无条件转移,可以只修改IP,也可以同时修改CS和IP;
- jmp指令要给出两种信息:
- 转移的目的地址
- 转移的距离(段间转移、段内短转移,段内近转移)
8.3 依据位移进行转移的jmp指令
- jmp short 标号(转到标号处执行指令)
这种格式的 jmp 指令实现的是段内短转移,它对IP的修改范围为 -128~127,也就是说,它向前转移时可以最多越过128个字节,向后转移可以最多越过127个字节。
对照汇编源程序,我们可以看到,Debug 将 jmp short s 中的 s 表示为inc ax 指令的偏移地址 8 ,并将jmp short s 表示为 jmp 0008 ,表示转移到cs:0008处。
没有了目的地址,CPU如何知道转移到哪里?
程序:
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
-
CPU执行指令的过程:
(1)从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲区;
(2)(IP) = (IP)+所读取指令的长度,从而指向下一条指令;
(3)执行指令。转到1,重复这个过程。
-
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。
8.3 依据位移进行转移的jmp指令
转移位移具体的计算方法如下图:
CPU执行 jmp short 标号 指令时并不需要转移的目的地址,只需要知道转移的位移就行了。
实际上,指令“jmp short 标号”的功能为(IP)=(IP)+8位位移。
(1)8位位移=“标号”处的地址-jmp指令后的第一个字节的地址;
(2)short指明此处的位移为8位位移;
(3)8位位移的范围为-128~127,用补码表示
(4)8位位移由编译程序在编译时算出。
-
还有一种和指令“jmp short 标号”功能相近的指令格式:
jmp near ptr 标号
它实现的时段内近转移。 -
指令“jmp near ptr 标号”的功能为:
(IP)=(IP)+16位位移。
指令“jmp near ptr 标号”的说明:
(1)16位位移=“标号”处的地址-jmp指令后的第一个字节的地址;
(2)near ptr指明此处的位移为16位位移,进行的是段内近转移;
(3)16位位移的范围为
-32769~32767,用补码表示;
(4)16位位移由编译程序在编译时算出。 -
指令 “jmp far ptr 标号”
实现的是段间转移,又称为远转移。
指令 “jmp far ptr 标号” 功能如下:
(CS)=标号所在段的段地址;
(IP)=标号所在段中的偏移地址。
far ptr指明了指令用标号的段地址和偏移地址修改CS和IP。
程序9.3:
assume cs:codesg
codesg segment
start:mov ax,0
mov bx,0
jmp far ptr s
db 256 dup (0)
s: add ax,1
inc ax
codesg ends
end start
如图中所示:
jmp far ptr s所对应的机器码:EA 0B 01 BD 0B ,其中包含转移的目的地址。
- “0B 01 BD 0B” 是目的地址在指令中的存储顺序,高地址的“BD 0B”是转移的段地址:0BBDH,低地址的“0B 01” 是偏移地址:010BH。
- 对于“jmp X 标号”格式的指令的深入分析请参看附注3。
8.5 转移地址在寄存器中的jmp指令
指令格式:jmp 16位寄存器
功能:IP =(16位寄存器)
先前说过,在如下链接
[[#3.11 修改CS、IP的指令]]
8.6 转移地址在内存中的jmp指令
转移地址在内存中的jmp指令的两种格式:
(1) jmp word ptr 内存单元地址(段内转移)
功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。
内存单元地址可用寻址方式的任一格式给出。
示例:
mov ax,0123H
mov ds:[0],ax
jmp word ptr ds:[0]
执行后,(IP)=0123H
mov ax,0123H
mov [bx],ax
jmp word ptr [bx]
执行后,(IP)=0123H
(2) jmp dword ptr 内存单元地址(段间转移)
功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。
(CS)=(内存单元地址+2)
(IP)=(内存单元地址)
内存单元地址可用寻址方式的任一格式给出。
示例
mov ax,0123H
mov ds:[0],ax
mov word ptr ds:[2],0
jmp dword ptr ds:[0]
执行后,
(CS)=0
(IP)=0123H
CS:IP指向0000:0123。
在mov word ptr ds:[2],0
中 word ptr
是汇编语言中的操作数大小说明符,用于指定操作数的大小。在 x86 架构中,它用于指示操作数的字长为 16 位,即一个字。
mov ax,0123H
mov [bx],ax
mov word ptr [bx+2],0
jmp dword ptr [bx]
执行后,
(CS)=0
(IP)=0123H
CS:IP指向0000:0123。
8.7 条件转移指令
- jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为-128~127。(存在范围限制,但jmp不存在)
- 指令格式:jcxz 标号
(如果(cx)=0,则转移到标号处执行。) - jcxz 标号 指令操作:
- 当(cx)=0时,(IP)=(IP)+8位位移)
8位位移=“标号”处的地址-jcxz指令后的第一个字节的地址;
8位位移的范围为-128~127,用补码表示;
8位位移由编译程序在编译时算出。 - 当(cx)!=0时,什么也不做(程序向下执行)。
- 当(cx)=0时,(IP)=(IP)+8位位移)
8.8 loop指令
- loop 标号 指令操作:
(1)(cx)=(cx)-1;
(2)如果(cx)≠0,(IP)=(IP)+8位位移。- 8位位移=“标号”处的地址-loop指令后的第一个字节的地址;
- 8位位移的范围为-128~127,用补码表示;
- 8位位移由编译程序在编译时算出。
- 当(cx)=0,什么也不做(程序向下执行)。
8.9 根据位移进行转移的意义
前面我们讲到:
jmp short 标号
jmp near ptr 标号
jcxz 标号
loop 标号
等几种汇编指令,它们对 IP的修改是根据转移目的地址和转移起始地址之间的位移来进行的。在它们对应的机器码中不包含转移的目的地址,而包含的食道目的地址的位移。
这样设计,方便了程序段在内存中的浮动装配。
- 注意,根据位移进行转移的指令,它们的转移范围受到转移位移的限制,如果在源程序中出现了转移范围超界的问题,在编译的时候,编译器将报错。
示例一个错误的信息:
assume cs:code
code segment
start: jmp short s
db 128 dup(0)//赋值给db128次,使得start和s相隔128个单位
s: mov ax,0ffffh
code ends
end start
jmp short s的转移范围是-128~127,IP最多向后移动127个字节。
9.转移类指令-2
call和ret 指令都是转移指令,它们都修改IP,或同时修改CS和IP。
它们经常被共同用来实现子程序的设计。
9.1 ret 和 retf
-
ret指令用栈中的数据,修改IP的内容,从而实现近转移;
-
retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移;
-
CPU执行ret指令时,进行下面两步操作:
(1)(IP)=((ss)*16+(sp))
(2)(sp)=(sp)+2
-
CPU执行retf指令时,进行下面两步操作:
(1)(IP)=((ss)*16+(sp))
(2)(sp)=(sp)+2
(3)(CS)=((ss)*16+(sp))
(4)(sp)=(sp)+2
9.2 call 指令
- CPU执行call指令,进行两步操作:
(1)将当前的 IP 或 CS和IP 压入栈中;
(2)转移。 - call 指令不能实现短转移,除此之外,call指令实现转移的方法和 jmp 指令的原理相同。
9.3 依据位移进行转移的call指令
- call 标号(将当前的 IP 压栈后,转到标号处执行指令)
- CPU执行此种格式的call指令时,进行如下的操作:
(1) (sp) = (sp) – 2
((ss)*16+(sp)) = (IP)
(2) (IP) = (IP) + 16位位移 - call 标号
- 16位位移=“标号”处的地址-call指令后的第一个字节的地址;
- 16位位移的范围为 -32768~32767,用补码表示;
- 16位位移由编译程序在编译时算出。
- CPU 执行指令“call 标号”时,相当于进行:
push IP
jmp near ptr 标号
9.4 转移的目的地址在指令中的call指令
- 指令“call far ptr 标号”实现的是段间转移。
- CPU执行“call far ptr 标号”这种格式的call指令时的操作:
(1) (sp) = (sp) – 2
((ss) ×16+(sp)) = (CS)
(sp) = (sp) – 2
((ss) ×16+(sp)) = (IP)
(2) (CS) = 标号所在的段地址
(IP) = 标号所在的偏移地址 - CPU 执行指令 “call far ptr 标号” 时,相当于进行:
push CS
push IP
jmp far ptr 标号
9.5 转移地址在寄存器中的call指令
- 指令格式:call 16位寄存器
- 功能:
- (sp) = (sp) – 2
((ss)*16+(sp)) = (IP)
- (IP) = (16位寄存器)
- 功能:
9.6 转移地址在内存中的call指令
- 转移地址在内存中的call指令有两种格式:
- (1) call word ptr 内存单元地址
- 汇编语法解释:
push IP
jmp word ptr 内存单元地址
- 汇编语法解释:
- (2) call dword ptr 内存单元地址
- 汇编语法解释:
push CS
push IP
jmp dword ptr 内存单元地址
- 汇编语法解释:
- (1) call word ptr 内存单元地址
9.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
- 写一个具有一定功能的程序段,我们称其为子程序,在需要的时候,用call指令转去执行。
- 可是执行完子程序后,如何让CPU接着call指令向下执行?
- call指令转去执行子程序之前,call指令后面的指令的地址将存储在栈中,所以可以在子程序的后面使用 ret 指令,用栈中的数据设置IP的值,从而转到 call 指令后面的代码处继续执行
- 这样,我们可以利用call和ret来实现子程序的机制。
9.8 mul 指令
无符号数乘法,与al(小于255)/ax(大于255)相乘
-
格式如下:
mul reg
mul 内存单元 -
内存单元可以用不同的寻址方式给出,比如:
- mul byte ptr ds:[0]
含义为: (ax)=(al)*((ds)*16+0);
- mul byte ptr ds:[0]
-
mul word ptr [bx+si+8]
含义为:
(ax)=(ax)*((ds)*16+(bx)+(si)+8)
结果的低16位;
(dx)=(ax)*((ds)*16+(bx)+(si)+8)
结果的高16位;
例如:
(1)计算100*10
100和10小于255,可以做8位乘法,程序如下:
mov al,100
mov bl,10
mul bl
结果: (ax)=1000(03E8H)
1)计算100*10000
100小于255,可10000大于255,所以必须做16位乘法,程序如下:
mov ax,100
mov bx,10000
mul bx
结果: (ax)=4240H,(dx)=000FH
(F4240H=1000000)
9.9 dup
dup是一个操作符,在汇编语言中同db、dw、dd 等一样,也是由编译器识别处理的符号。
它是和db、dw、dd 等数据定义伪指令配合使用的,用来进行数据的重复。
10. 中断
- 中断的意思是指,CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。
- 概念
- 中断源:引起中断的事件
- 内中断(软中断):
- INT 指令 / CPU 错(除法错、溢出)/
- 为调试程序设置的中断
- 外中断(硬中断):
- 外设的 I/O 请求 —— 可屏蔽中断
- 电源掉电 / 奇偶错 —— 非屏蔽中断
80x86 中断源:
-
内中断的产生
-
中断处理程序
- CPU的设计者必须在中断信息和其处理程序的入口地址之间建立某种联系,使得CPU根据中断信息可以找到要执行的处理程序。
- 中断信息中包含有标识中断源的类型码。根据CPU的设计,中断类型码的作用就是用来定位中断处理程序。
-
中断向量表
- 中断向量表就是中断向量的列表。
- 中断向量表在内存中保存,其中存放着 256个中断源所对应的中断处理程序的入口:
- 中断向量表在内存中存放,对于8086PC机,中断向量表指定放在内存地址0处。
- 从内存0000:0000到0000:03FF的1024个单元中存放着中断向量表。
-
中断过程
- 用中断类型码找到中断向量,并用它设置CS和IP,这个工作是由CPU的硬件自动完成的。
- CPU 硬件完成这个工作的过程被称为中断过程。
- 8086CPU的中断过程:
- 1)(从中断信息中)取得中断类型码;
(2)标志寄存器的值入栈( 因为在中断过程中要改变标志寄存器的值,所以先将其保存在栈中。);
(3)设置标志寄存器的第8位TF 和第9位IF的值为0;(这一步的目的后面将介绍)
(4)CS的内容入栈;
(5)IP的内容入栈;
(6)从内存地址为中断类型码*4 和中断类型码 *4+2 的两个字单元中读取中断处理程序的入口地址设置IP和CS。
- 1)(从中断信息中)取得中断类型码;
- 简洁的描述中断过程,如下:
(1)取得中断类型码N;
(2) pushf
(3) TF = 0,IF = 0
(4) push CS
(5) push IP
(6)(IP) = (N*4),(CS) = (N*4+2)
-
中断处理程序和iret指令
- 中断处理程序的编写方法和子程序的比较相似,下面是常规的步骤:
(1)保存用到的寄存器。
(2)处理中断。
(3)恢复用到的寄存器。
(4)用 iret 指令返回。 - iret指令的功能用汇编语法描述为:
pop IP
pop CS
popf
- 中断处理程序的编写方法和子程序的比较相似,下面是常规的步骤:
-
编程处理 0 号中断
- 改变一下0号中断处理程现在序的功能,即重新编写一个0号中断处理程序,它的功能是在屏幕中间显示“overflow!”后,然后返回到操作系统。
- 编程:当发生除法溢出时,在屏幕中间显示“overflow!”,返回DOS。
- 分析(1)当发生除法溢出的时候,产生0号中断信息,从而引发中断过程。
此时,CPU将进行以下工作:
① 取得中断类型码0;
② 标志寄存器入栈,TF、IF设置为0;
③ CS、IP入栈;
④(IP) = (0*4),(CS) = (0*4+2)
- 分析(2)可见 ,当中断 0 发生时,CPU将转去执行中断处理程序。
只要按如下步骤编写中断处理程序,当中断0发生时,即可显示“overflow!”。
① 相关处理。
② 向显示缓冲区送字符串“overflow!”。
③ 返回DOS - 我们将这段程序称为do0。
分析(3)现在的问题是:do0 应放在内存中。
因为除法溢出随时可能发生,CPU随时都可能将 CS:IP指向 do0的入口,执行程序。- 问题变得简单而直接,我们只需找到一块别的程序不会用到的内存区,将do0传送到其中即可。
- 前面讲到,内存0000:0000~0000:03FF,大小为1KB的空间是系统存放中断处理程序入口地址的中断向量表。
- 问题变得简单而直接,我们只需找到一块别的程序不会用到的内存区,将do0传送到其中即可。
- 8086 支持 256 个中断,但是,实际上,系统中要处理的中断事件远没有达到256 个 。
所以在中断向量表中,有许多单元是空的。 - 中断向量表是PC系统中最重要的内存区,只用来存放中断处理程序的入口地址,DOS 系统和其他应用程序都不会随便使用这段空间。
- 一般情况下:
从0000:0200至0000:02FF的256个字节的空间所对应的中断向量表项都是空的,操作系统和其他应用程序都不占用。- 根据以前的编程经验,可以估计出,do0的长度不超过256个字节。
- 结论:将do0传送到内存0000:0200处。
-
总结上面的分析,要做以下几件事情:
(1)编写可以显示“overflow!”的中断处理程序:do0;
(2)将do0送入内存0000:0200处;
(3)将do0的入口地址0000:0200存储在中断向量表0号表项中。
程序框架