1. 内存中字的存储
数字:20000,十六进制:4E20H。
4E20 一共 16 位,我们称之为一个字。
CPU中,使用 16 位寄存器存储一个字。并用高 8 位寄存器来存放高位字节(4E),低 8 位寄存器存放低位字节(20)。如图 4-2 ,使用 寄存器 AX 存放4E20H, 图 4-1 右侧显示了寄存器低位和高位存储情况
如图 4-1 左侧,在内存中,每个内存单元是字节单元(一个单元存储一个字, 8 位),那么一个字要两个地址连续的内存单元来存放。同理,低地址单元存放低位字节(20),高地址单元存放高位字节(4E)。
如图 4-3 黄色区域中的的两个连续的内存单元组成了一个字单元。高地址内存单元存放字型数据的高位字节,低地址内存单元存放字型数据的低位字节。
问题分析:
对于图 4-1:
1)0 地址单元中存放的字节型数据是多少?
2)0 地址字单元中存放的字型数据是多少?
3)2 地址单元中存放的字节型数据是多少?
4)2 地址单元中存放的字型数据是多少?
5)1 地址子单元中存放的字型数据是多少?
分析:
1)12H
2) 4E12H
3) 12H
4) 0012H
- 124EH
任何两个地址连续的内存单元, N 号单元和 N+1 号单元,可以将它们看成两个内存单元,也可以看成一个地址为 N 的字单元的高位字节单元和低位字节单元
2. DS 和 [address]
CPU 要读写一个内存单元中的数据,就必须先给出这个内存单元的第一种,而 8086CPU 内存地址有段地址和偏移地址组成。 在 8086CPU 中有个寄存器 DS,这个寄存器就是用来存放要访问的内存单元的段地址。
比如,我们要读取 10000H 单元的内容,可以用如下的程序段进行:
mov bx, 1000
mov ds, bx
mov al,[0]
上面的三条命令将 10000H(1000:0)中的数据读到 al 中。如图演示:
如上图演示,最终 ax 寄存器中存放了 0012H 也就是内存地址 10000H 中的数据。
对于 mov 指令功能:
① 将数据直接送入寄存器内 (mov bx,1000,将 1000H 送入寄存器 bx 中)
② 将一个寄存器中的内容送入另一个寄存器 (mov ds,bx 将 bx 内容送入到 ds 中)
除此之外,也可以通过 mov 指令将一个内存单元中的内容送入到一个寄存器中。
mov ax, [0]
上面这条命令中 [0] 表示的是偏移地址,但是仅仅有偏移地址是不能定位一个内存单元的,对于 8086CPU 而言会自动从去取 DS 寄存器中的数组作为内存单元的段地址。
再来看一下,如何用 mov 指令从 10000H 中读取数据:
- 10000H 用段地址 和 偏移地址表示为: 1000:0
- 通过 mov bx,1000 mov ds,bx 将段地址 1000H送入寄存器 ds 中
- 然后通过 mov al,[0] 将数据从内存单元 1000:0 取出并送入寄存器al(ax低位寄存器)中
这里可能看出一个问题,就是第 2 步的时候,为什么不直接通过 mov ds,1000 将 1000H 直接送入寄存器: DS ?
这属于 8086CPU 硬件设计问题,DS 是一个段寄存器,8086CPU 不支持直接将数据送入段寄存器的操作。
问题:写几条指令将 al 中的数据送入内存单元 10000H 中(具体操作见下面的gif):
mov bx,1000
mov ds,bx
mov [0],al
3. 字的传送
上面通过 mov 指令在寄存器和内存之间进行字节型数据的传送。因为 8086CPU 是 16 位结构,有 16 根数据线,所以, 可以一次性传送 16 位的数据,也就是说可以一次性传送一个字。只要在 mov 指令中给出 16 位的寄存器就可以进行 16 位数据的传送了。
指令样板如下:
mov bx, 1000
mov ds,bx
mov ax,[0]
mov [0],cx
问题 4-3:
内存情况如图 4-4 所示:
执行如下命令后寄存器 ax, bx, cx 值各是多少?
mov ax,1000
mov ds,ax
mov ax,[0]
mov bx,[2]
mov cx,[1]
add bx,[1]
add cx,[2]
分析:
指令 | 执行后寄存器内容 | 指令说明 |
---|---|---|
mov ax,1000 | ax=1000H | 向寄存器 ax 中送入内容 1000 |
mov ds,ax | ds=1000H | 段地址寄存器内容设置为 1000 |
mov ax,[0] | ax=1123H | 这里 ax 寄存器是 16 位,取内存 10000 处 一个字型数据 1123H 送入 ax 中 |
mov bx,[2] | bx=6622H | bx 16 位寄存器,取内存 10002H 处一个字型数据 6622H 送入 bx 寄存器中 |
mov cx,[1] | cx=2211H | cx 16 位寄存器,取内存 10001H 处一个字型数据 2211H 送入 bx 寄存器中 |
add bx,[1] | bx=8833H | bx 内容为 6622H 取内存 10001H 中 2211H 执行 6622H + 2211H = 8833H,将计算结果送入 bx 寄存器中 |
add cx,[2] | cx=8833H | cx 内容 2211H 取出内存 10002H 中 6622H 相加得出 8833H 送入 cx 寄存器中 |
具体操作:
通过 e 命令已经将内存数据写入,然后向内存中写入上述指令,通过修改 cs:ip ,执行指令
问题 4-4:
内存情况如图 4-5:
执行下面命令后内存中的值:
mov ax,1000
mov ds,ax
mov ax,11316(十进制)
mov [0],ax
mov bx,[0]
sub bx,[2]
mov [2],bx
指令 | 执行后寄存器内容 | 指令说明 |
---|---|---|
mov ax,1000 | ax=1000H | 向寄存器 ax 中送入内容 1000 |
mov ds,ax | ds=1000H | 段地址寄存器内容设置为 1000 |
mov ax,11316 | ax=2C34H | 11316 的十六进制 2C34H |
mov [0],ax | 内存 10000H:34 10001H:2C | ax 中低位送入 10000H,高位送入 10001H |
mov bx,[0] | bx=2C34 | 取出内存 10000H 出字型数据 2C34 放入 bx 寄存器 |
sub bx,[2] | bx=1B12 | 2C34H - 1122H=1B12H |
mov [2],bx | 10003H:1B 10002:12 其他不变 | 寄存器 bx 中 1B12H 送入内存 |
4 mov add sub 指令
前面我们用到的 mov 指令形式有以下几种:
mov 寄存器,数据
mov 寄存器,寄存器
mov 寄存器,内存单元
mov 内存单元,寄存器
mov 段寄存器,寄存器
基于前面使用的指令,猜想:
1) mov 寄存器,段寄存器(验证寄存器和段寄存器之间是否有相反的通路)
通过上面执行 mov ax,ds 可以看到词条命令是合法的, 所以寄存器和段寄存器之间是相互通路的。
2)mov 内存单元,段寄存器
mov ax,1000H
mov ds,ax
mov [0],cs
操作如下:CS 中内容 073F,将其放入的 10000H 内存单元内,这里需要注意的是 CS 是一个 16 位寄存器,所以需要两个内存单元 存放其中去除的数据,低地址存放 3F 高地址存放 07:
3)mov 段寄存器,内存单元
mov ax, 1000H
mov ds,ax
mov ds,[0]
将内存单元中 10000H 字型数据送入 DS 寄存器中:
下面这些以供练习:
add 寄存器, 数据
add 寄存器,寄存器
add 寄存器,内存单元
add 内存单元,寄存器
sub 寄存器,数据
sub 寄存器,寄存器
sub 寄存器,内存单元
sub 内存单元,寄存器
5 数据段
对于 8086PC 机,在编程时,可以根据需要,将一组内存单元定义为一个端。我们可以将一组产孤单为N(N <= 64KB) 地址连续,起始地址为 16 的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。
比如 123B0H ~ 123B9H 这段内存空间来存放数据,我们就可以认为, 123B0H ~ 123B9H 这段内存是一个数据段,它的段地址为 123BH,长度为 10 个字节。
那么,如何访问数据段中的数据呢?
将一段内存当作数据段,是我们在编程时的一种安排,可以在具体操作的时候,用 ds 寄存器存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
比如,将 123B0H ~ 123B9H 的内存单元定义为数据段。现在要累加这个数据段中的前 3 个单元中的数据,代码如下:
mov ax, 123bH
mov ds,ax
mov al,0
add al,[0]
add al,[1]
add al,[2]
这里需要强调一点,是前3 单元内容,内存单元每个大小为 8 位(即一个字节),所以只需要的是 8 位寄存器,当然这里可以自行验证,当地位寄存器数据超过了之后是否会进位到高位寄存器。
问题 4-5
写几条指令,累加数据段中前 3 个字型数据:
mov ax,123BH
mov ds,ax
mov ax,0
mov ax,[0]
mov ax,[2]
mov ax,[4]
小结:
1) 字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中;
2)用 mov 指令访问内存单元,可以再 mov 指令中只给出单元的偏移地址,此时,段地址默认在 DS 寄存器中;
3)[address] 表示一个偏移地址为 address 的内存单元;
4)在内存和寄存器之间传送字型数据时,高地址单元和高 8 位寄存器、低地址单元和低 8 位寄存器相互对应;
5)mov、add、sub 是具有两个操作对象的指令。 jmp 是具有一个操作对象的指令;
6)可以根据自己的推测,在 Debug 中实验指令的新格式
6 栈
这了对栈的研究限于:栈是一种具有特殊访问方式的存储空间(LIFO)。
这里通过盒子放书的操作过程来描述栈的操作,如图 4-6 所示:
现在,一次只允许取一本,如何将 3 本书从盒子中取出?
显然,必须从盒子最上边取,这样取出的顺序就是: 《软件工程》《C 语言》《高等数学》,和放入的顺序正好相反,如图 4-7 所示:
从程序化角度,我们通过绿色箭头做一个标记,这个标记一直指着盒子最顶端的书籍。
总结:
如果说盒子就是一个栈,那么这个栈有两个操作,分别是入栈和出栈。
入栈,就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈,如果出栈的话,栈顶元素也是第一个被取出(LIFO)。
7 CPU 提供的栈机制
现在的 CPU 中都有栈的设计, 8086CPU 也不例外。8086CPU 提供相关的指令来以栈的方式访问内存空间。这意味着,在基于 8086CPU 编程的时候,可以将一段内存当做栈来使用。
8086CPU 提供入栈和出栈指令,最基本的两个是 PUSH 和 POP ,比如, push ax 表示将寄存器 ax 中的数据送入到栈中, pop ax 表示从栈顶取出数据送入到 ax。 8086CPU 的入栈和出栈操作都是以字为单位进行。
举例说明,我们可以将 10000H~1000FH这段内存当做栈来使用,如图 4-8 所示:
指令如下:
mov ax,0123H
push ax
mov bx,2266H
push bx
mov cx,1122H
push cx
pop ax
pop bx
pop cx
注意,字型数据用两个单元存放,高地址单元存放高 8 位,低地址单元存放低 8 位。
如图 4-8 所示,有两个疑惑在这里说明一下:
第一点疑惑,上面我们将内存的 10000H~1000FH 这段作为栈来来使用,然后执行 push 和 pop 指令。但是,CPU 是如何知道 10000H~1000FH 这段空间被当作栈来使用的呢 ?
第二点疑惑,push ax , pop ax 等指令执行的时候,要访问栈顶单元,那么这两个指令又是如何知道那个内存单元是栈顶单元呢 ?
这里,我们回顾下,CPU 如何知道当前要执行指令所在的位置呢 ?
在前面的操作中,我们也已经知道,那就是 CS 和 IP 两个寄存器存放着当前指令的段地址和偏移地址。
现在的问题是: CPU 如何知道栈顶位置 ?
显然,也应该有相应的寄存器来存放栈顶地址, 806CPU 中,有两个寄存器,段寄存器 SS 和 SP,栈顶段地址存放在 SS 中,偏移地址存放在 SP 中。
在任意时候, SP:IP指向栈顶元素
push 和 pop 指令执行时,CPU 从 SS 和 SP 中得到栈顶的地址。
现在,我们可以完整地描述 push 和 pop 指令的功能了,例如 push ax。
push ax 的执行,由一下两步完成:
(1)SP=SP-2, SS:SP 指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
(2)将 ax 中的内容送入 SS:SP 指向的内存单元处,SS:SP此时指向新栈顶。
如图 4-8 所示:
从图中,可以看出,8086CPU中入栈时,栈顶从高地址向低地址方向增长。
问题 4-6
如果将 10000H ~ 1000FH 这段地址当做栈,初始状态是空栈,此时 SS = 1000H,那么 SP=?
分析
SP = 0010H,如图 4-8 所示,
将 10000H ~ 1000FH 这段空间当做栈段,SS = 1000H,栈空间大小为 16 字节,栈最底部的字单元地址为 1000:000E。 任意时刻,SS:SP 指向栈顶,当栈中只有一个元素的时候,SS=10000H, SP=1000EH。
栈为空的话,也就是说,相当于栈中唯一的元素出栈,出栈后,SP=SP+2,SP 原来为 000EH,加 2 后,SP = 10H,所以当栈空的时候,SS=1000H,SP=10H。
换一个角度看,任意时刻,SS:SP 指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素,所以 SS:SP 指向栈最底部单元下面的单元,该单元的偏移地址为最底部字单元偏移地址 +2 ,栈最底部的字单元地址为 1000:000E, 所以栈空时, SP=0010H。
接下来,看一下 pop 操作:
pop ax
pop ax 和 push ax 正好相反,由以下两步组成:
(1)将 SS:SP 指向的内存单元处的数据送入 ax 中;
(2)SP=SP+2,SS:SP指向当前栈顶下面的一个单元,以当前栈顶下面的单元为新的栈顶。
如下图 4-10 所示, POP AX 的过程:
这里需要注意第三步,尽管当前栈指针指向 1000E H 但是内存单元 1000DH 和 1000CH 中的数据任然存在,只不过不在栈中,直到下一次 PUSH 操作将其覆盖为止。
8 栈顶越界问题
这一小节,介绍越界问题。
比如,我们将内存地址空间 10010H ~ 1001FH 作为栈空间,该栈空间容量为 16 字节(8个字)。
初始状态栈为空, SS=1000H,SP=0020H,SS:SP 指向 10020H。
ax=0123H
执行 8 次 PUSH AX 操作,向栈中压入 8 个字,栈满,SS=1000H SP=0010, SS:SP 指向 10010H
执行 8 次 PUSH AX 操作之后,栈满,如果在执行一次 PUSH AX,那么 SP=SP-2,SP=000E, SP=1000E
此时,PUSH 操作已经超过了栈空间,将栈外空间数据覆盖。
POP 操作类似。
主要操作细节是:
通过 -a 将命令写入当前指令寄存器指向的内存地址空间,然后,通过 -r 指令修改寄存器内容,包括栈寄存器 SP 和 SS 以及数据寄存器 AX 内容。
这里需要注意,POP 和 PUSH 都会出现越界,但是 8086CPU 并没有机制保证我们不越界,所以在编程的时候需要自己操作栈顶越界的问题。
9 push 、pop 指令
前面我们一直在使用 PUSH AX 和 POP AX 这两条指令,他们可以将数据在内存和寄存器之间进行传送。(栈空间是内存的一部分,它只是一段可以以一种特殊方式进行访问的内存空间)
除此之外, PUSH 和 POP 这两条指令的目标地址还可以是段寄存器或者内存空间。
练习 1
将 10000H~1000F 这段空间当做栈,初始状态栈空,将 ax,bx,ds 中的数据送入栈。
-
设置栈寄存器
SS=1000H SP=0010 SS:SP=10010
-
将数据送入寄存器
ax=1234H
bx=5678H
ds=9ABCH
-
将指令 push ax ,push bx, push ds 写入内存单元
-
执行第 3 步写入的指令
练习 2
编程:
(1) 将 10000H~1000FH 这段空间当作栈,初始状态是空的;
(2)设置 AX=001AH , BX=001BH;
(3)将 AX、BX 中的数据入栈;
(4)然后将 AX、BX 清零;
(5)从栈中恢复 AX、BX 原来的内容。
解析:
首先设置栈寄存器值
SS=1000H SP=0010H SS:SP=10010H设置 AX BX 的值
AX=001AH
BX=001BH入栈,执行 PUSH AX 和 PUSH BX 操作
a 写入 push ax 和 push bx
t 执行命令清零
ax=0000H
bx=0000H执行 POP BX POP AX 操作