1、内存中字的存储
字单元:存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。
起始地址为N的字单元简称为N地址字单元。比如一个字单元由2、3两个内存单元组成,则这个字单元的起始地址为2,我们可以说这是2地址字单元。
示例如下:在0地址处开始存放20000(4E20H)
注意:0号单元是低地址单元,1号单元是高地址单元。
问题:
1、0地址单元中存放的字节型数据是多少?答:20H
2、0地址字单元中存放的字型数据是多少?答:4E20H
3、2地址单元中存放的字节型数据是多少?答:12H
4、2地址字单元中存放的字型数据是多少?答:0012H
5、1地址字单元中存放的字型数据是多少?答:124EH
可以看出:任何两个地址连续的内存单元,N号单元和 N+1号单元,可以将它们看成两个内存单元 ,也可以看成一个地址为N的字单元中的高位字节单元和低位字节单元。
2、DS和[address]
CPU要读取一个内存单元的时候,必须先给出这个内存单元的地址;在8086PC中,内存地址由段地址和偏移地址组成。8086CPU中有一个 DS寄存器,通常用来存放要访问的数据的段地址。
例如:我们要读取10000H单元的内容可以用如下程序段进行:
mov bx, 1000H
mov ds, bx
mov a1,[0]
上面的三条指令将10000H(1000:0)中的数据读到al中。
前面我们使用mov指令,可完成两种传送:
(1)将数据直接送入寄存器;
(2)将一个寄存器中的内容送入另一个寄存器;
除此之外,mov指令还可以将一个内存单元中的内容送入一个寄存器;
从哪一个内存单元送到哪一个寄存器中呢?格式应该为:mov 寄存器名, 内存单元地址。
“[…]”表示一个内存单元, “[…]”中的 0 表示内存单元的偏移地址。那么段地址是多少呢?指令执行时,8086 CPU自动取ds(段寄存器)中的数据为内存单元的段地址。
如何用mov指令从10000H中读取数据?
- 10000H表示为1000:0(段地址:偏移地址)
- 将段地址1000H放入ds
- 用mov al,[0]完成传送(mov指令中的[ ]说明操作对象是一个内存单元,[ ]中的0说明这个内存单元的偏移地址是0,它的段地址默认放在ds中)
- 如何把1000H送入ds?见下:
如何把1000H送入ds?
传送指令 mov ax,1
相似的方式 mov ds,1000H?
8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器。(硬件设计的问题)
mov ds,1000H 是非法的。
数据->通用寄存器->段寄存器
问题:写几条指令,将al 中的数据送入内存单元 10000H。思考后看分析:
分析:怎样将数据从寄存器送入内存单元?从内存单元到寄存器的格式是:“mov 寄存器名,内存单元地址”,从寄存器到内存单元则是:“mov 内存单元地址,寄存器名”。10000H可表示为 1000:0,用ds存放段地址:1000H,偏移地址是0,则:mov[0],al 可完成从 al 到10000H的数据传送。完整的几条指令是:
mov bx,1000H
mov ds,bx
mov [0],al
3、字的传送
8086 CPU是16位结构,有16根数据线,所以,可以一次性传送16位的数据,也就是说可以一次性传送一个字。我们只要在mov指令中给出16位的寄存器就可以进行16位数据的传送了,比如:
mov bx, 1000H
mov ds, bx
mov ax, [0] //1000:0 处的字型数据送入ax
mov [0], cx //cx中的16位数据送到1000:0处
问题3.3:
内存中的情况如图所示,写出下面的指令执行后寄存器ax、bx、cx中的值。
mov ax, 1000H //执行后为:ax=1000H
mov ds, ax //执行后为:ds=1000H
mov ax, [0] //执行后为:1123H
mov bx, [2] //执行后为:6622H
mov cx, [1] //执行后为:2211H
add bx, [1] //执行后为:8833H
add cx, [2] //执行后为:8833H
分析:
前两条指令的目的是将ds设为 1000H,
1000:0处存放的字型数据送入ax;
1000:1单元存放字型数据的高8位:11H;1000:0单元存放字型数据的低8位:23H;所以1000:0处存放的字型数据为1123H。
指令执行时,字型数据的高8位送入ah,字型数据的低8位送入al,则ax中的数据为1123H
以下原理同上。
问题 3.4
内存中的情况如图所示,写出下面的指令执行后内存中的值:
分析:进行单步跟踪,看一下每条指令执行后相关寄存器或内存单元中的值。
4、mov、add、sub指令
MOV指令的形式:
mov 寄存器,数据 // mov ax,8
mov 寄存器,寄存器 // mov ax,bx
mov 寄存器,内存单元 // mov ax,[0]
mov 内存单元,寄存器 // mov [0],ax
mov 段寄存器,寄存器 // mov ds,ax
mov 寄存器,段寄存器
add 和 sub 指令同mov 一样,都有两个操作对象。使用形式如下:
5、数据段
对于8086PC机,我们可以根据需要将一组内存单元定义为一个段(可以是代码段、数据段等)。
我们可以将一组长度为N(N≤64K)、地址连续、起始地址为16的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。
比如我们用123B0H~123B9H这段空间来存放数据:
- 段地址:123BH
- 长度:10字节
将一段内存当作数据段,是我们在编程时的一种安排,我们可以在具体操作的时候 ,用 ds 存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
例如:将123B0H - 123BAH 的内存单元定义为数据段。我们现在要累加这个数据段中的前3个单元中的数据,代码如下:
mov ax, 123BH
mov ds, ax //将123BH送入ds中,作为数据段的段地址
mov al, [0] // 用al存放累加结果,将数据段第一个单元(偏移地址为0)中的数值加到al中
add al, [1] //将数据段第二个单元(偏移地址为1)中的数值加到al中
add a1, [2] //将数据段第三个单元(偏移地址为2)中的数值加到al中
问题3.5
写几条指令,累加数据段中的前3个字型数据。
分析:代码如下
mov ax, 123BH
mov ds, ax //将123BH送入ds中,作为数据段的段地址
mov ax, 0 //用ax寄存器存放累加结果
add ax, [0] //将数据段第一个字(偏移地址为0)加到ax中
add ax, [2] //将数据段第二个字(偏移地址为2)加到ax中
add ax, [4] //将数据段第三个字(偏移地址为4)加到ax中
注意:一个字型数据占两个单元,所以偏移地址是0、2、4
1-5 小结
- 字在内存中存储时 ,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放再高地址单元中。
- 用 mov 指令要访问内存单元,可以在mov指令中只给出单元的偏移地址,此时,段地址默认在DS寄存器中。
- [address]表示一个偏移地址为 address 的内存单元。
- 在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。
- mov、add、sub是具有两个操作对象的指令。jmp是具有一个操作对象的指令。
检测点 3.1
(1)在Debug中,用“d 0:0 1f”查看内存,结果如下:
0000:0000 70 80 F0 30 EF 60 30 E2 - 00 80 80 12 66 20 22 60
0000:0010 62 26 E6 D6 CC 2E 3C 3B - AB BA 00 00 26 06 66 88
下面的程序执行前,AX=0,BX=0,写出每条汇编指令执行完后相关寄存器中的值。
mov ax,1
mov ds,ax
mov ax,[0000] AX=2662H
mov bx,[0001] BX=E626H
mov ax,bx AX=E626H
mov ax,[0000] AX=2662H
mov bx,[0002] BX=D6E6H
add ax,bx AX=FD48H
add ax,[0004] AX=2C14H
mov ax,0 AX=0000H
mov al,[0002] AX=00E6H
mov bx,0 BX=0000H
mov bl,[000C] BX=0026H
add al,bl AX=000CH
提示:注意CS的设置
(2)内存中的情况如下所示:
各寄存器的初始值:CS=2000H,IP=0,DS=1000H,AX=0,BX=0;
(1)写出CPU执行的指令序列(用汇编指令写出)
(2)写出CPU执行每条指令后,CS、IP和相关寄存器中的数值
(3)再次体会:数据和程序有区别吗?如何确定内存中的信息哪些是数据,哪些是程序?数据和程序在内存中都是二进制/十六进制存储,根据寄存器进行区别数据、程序
答案:
指令序列 | CS | IP | DS | AX | BX | |
初始值 | 2000h | 0 | 0 | 0 | 0 | |
1 | mov ax,6622h | 2000h | 3h | 0 | 6622h | 0 |
2 | jmp 0ff0:0100 | ff0h | 100h | 0 | 6622h | 0 |
3 | mov ax,2000h | ff0h | 103h | 0 | 2000h | 0 |
4 | mov ds,ax | ff0h | 105h | 2000h | 2000h | 0 |
5 | mov ax,[8] | ff0h | 108h | 2000h | c189h | 0 |
6 | mov ax,[2] | ff0h | 10bh | 2000h | ea66h | 0 |
6、栈
栈是一种具有特殊的访问方式的存储空间。它的特殊性就在于,最后进入这个空间的数据,最先出去。
栈有两个基本的操作:入栈和出栈。
- 入栈:将一个新的元素放到栈顶;
- 出栈:从栈顶取出一个元素。
栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。
栈的操作规则:LIFO(Last In First Out,后进先出)
现今的CPU中都有栈的设计,8086 CPU 提供相关的指令来以栈的方式访问内存空间。这意味着,我们在基于8086CPU编程的时候,可以将一段内存当作栈来使用。
8086CPU提供入栈和出栈指令: (最基本的)
PUSH(入栈)
POP (出栈)
push ax:将寄存器ax中的数据送入栈中
pop ax :从栈顶取出数据送入ax。
8086CPU的入栈和出栈操作都是以字为单位进行的
注意:字型数据用两个单元存放,高地址单元放高 8 位,低地址单元放低8 位。
CPU如何指导当前要执行的指令所在的位置?
答:寄存器CS和IP中存放着当前指令的段地址和 偏移地址。
8086CPU中,有两个寄存器:
- 段寄存器SS 存放栈顶的段地址
- 寄存器SP 存放栈顶的偏移地址
任意时刻,SS:SP指向栈顶元素。
PUSH指令的执行过程:由两步完成:
(1) SP=SP-2; SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
(2) 将 ax 中的内容送入 SS:SP指向的内存单元处,SS:SP此时指向新栈顶;
如下为:8086CPU对push指令的执行过程:
从上可以看出,8086CPU中,入栈时,栈顶从高地址向低地址方向增长。
问题 3.6
如果我们将 10000H - 1000FH 这段空间当作栈,初始状态是空的,此时,SS=1000H,SP=?
思考后看分析:
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。
换一个角度看:任意时刻,SS:SP 指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素。所以SS:SP 只能指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址+2。栈最底部字单元的地址为1000:000E,所以栈空时,SP=0010H。
POP指令的执行过程: 由两步完成:
(1) 将SS:SP指向的内存单元处的数据送入ax中;
(2) SP = SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
如下为 8086CPU对pop指令的执行过程:
注意:
出栈后,SS:SP指向新的栈顶 1000EH,pop操作前的栈顶元素,1000CH 处的2266H 依然存在 ,但是,它已不在栈中。
当再次执行push等入栈指令后,SS:SP移至1000CH,并在里面写入新的数据,它将被覆盖。
7、栈顶超界的问题
8086CPU用SS和SP指示栈顶的位置,并提供push和pop指令实现入栈和出栈。SS和SP只记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈时找到栈顶。
当栈满的时候再使用push指令入栈,或栈空的时候再使用pop指令出栈,都将发生栈顶超界问题。
8086CPU的工作机理,只考虑当前的情况:
- 当前栈顶在何处;
- 当前要执行的指令是哪一条。
结论:
- 我们在编程的时候要自己操心栈顶超界的问题 ,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;
- 执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
8、push、pop指令
push和pop指令是可以在寄存器和内存之间传送数据的。(栈空间当然也是内存空间的一部分,它只是一段可以以一种特殊的方式进行访问的内存空间。)
push和pop指令的格式可以是如下形式:
push和pop指令的格式一:
- push 寄存器:将一个寄存器中的数据入栈
- pop寄存器:出栈,用一个寄存器接收出栈的数据
例如:push ax ; pop bx;
push和pop指令的格式二
- push 段寄存器:将一个段寄存器中的数据入栈
- pop段寄存器:出栈,用一个段寄存器接收出栈的数据
例如:push ds; pop es;
push和pop指令的格式三
- push内存单元:将一个内存单元处的字入栈(栈操作都是以字为单位)
- pop 内存单元:出栈,用一个内存字单元接收出栈的数据
例如:
mov ax, 1000H
mov ds, ax //内存单元的段地址要放在ds中;
push [0]; //将1000:0 处的字压入栈中;
pop [2]; //出栈,出栈的数据送入1000:2处;
指令执行时,CPU要知道内存单元的地址,可以在push、pop指令中只给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得。
问题 3.7
编程,将10000H - 1000FH 这段空间当作栈,初始状态栈是空的,将AX、BX、DS中的数据入栈。
分析:代码如下
mov ax, 1000H
mov ss, ax //设置栈的段地址,ss=1000H,不能直接向段寄存器ss送入数据,所以用ax中转。
mov sp, 0010H //设置栈顶的偏移地址,因栈为空,所以sp=0010H。
//上面的三条指令设置栈顶地址,编程时要自己注意栈的大小。
push ax
push bx
push ds
问题 3.8
编程 :(1)将 10000H - 1000FH 这段空间当作栈,初始状态栈是空的;
(2)设置AX=001AH,BX=001BH;
(3)将AX、BX中的数据入栈;
(4)然后将AX、BX清零;
(5)从栈中恢复AX、BX原来的内容;
分析:代码如下
mov ax, 1000H
mov ss, ax
mov sp, 0010H //初始化栈顶,
mov ax, 001AH
mov bx, 001BH
push ax
push bx //ax、bx入栈,
mov ax, 0H
mov bx, 0H
pop bx //从栈中恢复ax、bx原来的数据,当前栈顶的内容是bx
pop ax
图解:
从上面的程序我们看到,用栈来暂存以后需要恢复的寄存器中的内容时,出栈的顺序要和入栈的顺序相反,因为最后入栈的寄存器的内容在栈顶,所以在恢复时,要最先出栈。
问题 3.9
编程:(1)将10000H - 1000FH 这段空间当作栈,初始状态栈是空的:
(2)设置 AX=002AH,BX=002BH;
(3)利用栈,交换AX和BX中的数据;
分析:代码如下
mov ax, 1000H
mov ss, ax
mov sp, 0010H //初始化栈顶,
mov ax, 002AH
mov bx, 002BH
push ax
push bx //ax、bx入栈,
pop ax
pop bx
图解:
问题 3.10
我们如果要在 10000H处写入字型数据 2266H,可以用以下的代码完成:
mov ax, 1000H
mov ds, ax
mov ax, 2266H
mov [0], ax
补全下面的代码,使它能够完成同样的功能;在10000H处写入字型数据2266H.
要求:不能使用 “ mov 内存单元,寄存器 ”这类指令。
mov ax, 1000H
mov ss, ax
mov sp, 0002H
mov ax, 2266H
push ax
从上述分析中可以看出,push、pop实质上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与mov指令不同的是,push 和 pop 指令访问的内存单元的地址不是在指令中给出的,而是由SS:SP指出的。同时,push 和 pop 指令还要改变SP中的内容。
我们十分清楚的是,push和pop指令同mov指令不同,CPU执行mov指令只需一步操作,就是传送,而执行push、pop指令却需要两步操作。执行push时,CPU的两步操作是:先改变SP,后向SS:SP处传送,执行pop时,CPU的两步操作是:先读取SS:SP处的数据,后改变SP。
注意:push、pop等栈操作指令,修改的只是SP。也就是说,栈顶的变化范围最大为:0 - FFFFH。
栈的综述:
8086CPU提供了栈操作机制,方案如下:
- 在SS,SP中存放栈顶的段地址和偏移地址;
- 提供入栈和出栈指令,他们根据SS:SP指示的地址,按照栈的方式访问内存单元。
push指令的执行步骤:
- SP=SP-2;
- 向SS:SP指向的字单元中送入数据。
pop指令的执行步骤:
- 从SS:SP指向的字单元中读取数据;
- SP=SP-2。
任意时刻,SS:SP指向栈顶元素。
8086CPU只记录栈顶,栈空间的大小我们要自己管理。
用栈来暂存以后需要恢复的寄存器的内容时 ,寄存器出栈的顺序要和 入栈的顺序相反。
push、pop实质上是一种内存传送指令,注意它们的灵活应用。
9、栈段
问题 3.11
如果我们将 10000H - 1FFFFH 这段空间当作栈段,初始状态栈是空的,此时,SS=1000H,SP=? 答:SP=0H
分析:任意时刻,SS:SP指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素,所以SS:SP只能指向栈的最底部单元下面的单元,该单元的地址为栈最底部的字单元的地址+2。栈最底部字单元的地址为 1000:FFFE,所以栈空时,SP=0000H。
问题 3.12
一个栈段最大可以设为多少?为什么?答:64KB
段的综述:
我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。这完全时我们自己的安排。
我们可以用一个段存放数据,将它定义为“数据段”
我们可以用一个段存放代码,将它定义为“代码段”
我们可以用一个段当作栈,将它定义为“栈段”
我们可以这样安排,但若要让CPU按照我们的安排来访问这些段,就要:
对于数据段,将它的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当作数据来访问;
对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令;
对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU在需要进行栈操作的时候,比如执行 push、pop指令等,就将我们定义的栈段当作栈空间来用。
可见,不管我们如何安排,CPU将内存中的某段内容当作代码,是因CS:IP指向了那里;CPU将某段内存当作栈,是因为SS:SP指向了那里。我们一定要清楚,什么是我们的安排,以及如何让CPU按我们的安排行事。要非常地清楚CPU的工作机理,才能在控制CPU来按照我们的安排运行的时候做到游刃有余。
比如我们将 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
设置 CS=1000H,IP=0,这段代码将得到执行。可以看到,在这段代码中,我们又将 10000H - 1001FH 安排为栈段和数据段。10000H - 1001FH这段内存,既是代码段,又是栈段和数据段。
一段内存,可以既是代码的存储空间,又是数据的存储空间,还可以是栈空间,也可以什么也不是,关键在于CPU中寄存器的设置,即:CS、IP、SS、DS的指向。
监测点 3.2
(1)补全下面的程序,使其可以将 10000H- 1000FH中的8个字,逆序拷贝到 20000H - 2000FH中。逆序拷贝的含义如图所示(图中内存里的数据均为假设):
答案:
mov ax, 1000H
mov ds, ax
mov ax, 2000H
mov ss, ax
mov sp, 10H
push [0]
push [2]
push [4]
push [6]
push [8]
push [A]
push [C]
push [E]
(2)补全下面的程序,使其可以将 10000H - 1000FH 中的8个字,逆序拷贝到 20000H - 2000FH中。(感觉题目出错了,应该为:将20000H - 2000FH 中的8个字,逆序拷贝到 10000H - 1000FH中)
mov ax,2000H
mov ds,ax
mov ax,1000H
mov ss,ax
mov sp,0
pop [e]
pop [c]
pop [a]
pop [8]
pop [6]
pop [4]
pop [2]
pop [0]
实验 2 用机器指令和汇编指令编程