《 第四章 内存访问 》

1. 内存中字的存储

图 4-1

数字:20000,十六进制:4E20H。
4E20 一共 16 位,我们称之为一个字。
CPU中,使用 16 位寄存器存储一个字。并用高 8 位寄存器来存放高位字节(4E),低 8 位寄存器存放低位字节(20)。如图 4-2 ,使用 寄存器 AX 存放4E20H, 图 4-1 右侧显示了寄存器低位和高位存储情况


图 4-2

如图 4-1 左侧,在内存中,每个内存单元是字节单元(一个单元存储一个字, 8 位),那么一个字要两个地址连续的内存单元来存放。同理,低地址单元存放低位字节(20),高地址单元存放高位字节(4E)。

图 4-3

如图 4-3 黄色区域中的的两个连续的内存单元组成了一个字单元。高地址内存单元存放字型数据的高位字节,低地址内存单元存放字型数据的低位字节。

问题分析:
对于图 4-1:
1)0 地址单元中存放的字节型数据是多少?
2)0 地址字单元中存放的字型数据是多少?
3)2 地址单元中存放的字节型数据是多少?
4)2 地址单元中存放的字型数据是多少?
5)1 地址子单元中存放的字型数据是多少?

分析:
1)12H
2) 4E12H
3) 12H
4) 0012H

  1. 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 中。如图演示:


CPU 读取内存单元 10000H 数据到 al 寄存器中

如上图演示,最终 ax 寄存器中存放了 0012H 也就是内存地址 10000H 中的数据。
对于 mov 指令功能:
① 将数据直接送入寄存器内 (mov bx,1000,将 1000H 送入寄存器 bx 中)
② 将一个寄存器中的内容送入另一个寄存器 (mov ds,bx 将 bx 内容送入到 ds 中)

除此之外,也可以通过 mov 指令将一个内存单元中的内容送入到一个寄存器中。

mov ax, [0]

上面这条命令中 [0] 表示的是偏移地址,但是仅仅有偏移地址是不能定位一个内存单元的,对于 8086CPU 而言会自动从去取 DS 寄存器中的数组作为内存单元的段地址。

再来看一下,如何用 mov 指令从 10000H 中读取数据:

  1. 10000H 用段地址 和 偏移地址表示为: 1000:0
  2. 通过 mov bx,1000 mov ds,bx 将段地址 1000H送入寄存器 ds 中
  3. 然后通过 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
将 AL 寄存器中数据 12H 送入内存 10000H 中

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 所示:


图 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-2 操作

问题 4-4:
内存情况如图 4-5:


图 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

通过上面执行 mov ax,ds 可以看到词条命令是合法的, 所以寄存器和段寄存器之间是相互通路的。

2)mov 内存单元,段寄存器

mov ax,1000H
mov ds,ax
mov [0],cs

操作如下:CS 中内容 073F,将其放入的 10000H 内存单元内,这里需要注意的是 CS 是一个 16 位寄存器,所以需要两个内存单元 存放其中去除的数据,低地址存放 3F 高地址存放 07:


mov 内存单元,段寄存器

3)mov 段寄存器,内存单元

mov ax, 1000H
mov ds,ax
mov ds,[0]

将内存单元中 10000H 字型数据送入 DS 寄存器中:


mov 段寄存器,内存单元

下面这些以供练习:

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 位寄存器,当然这里可以自行验证,当地位寄存器数据超过了之后是否会进位到高位寄存器。

数据段 123B0H ~ 123B9 操作累加

问题 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 所示:


图 4- 6

现在,一次只允许取一本,如何将 3 本书从盒子中取出?
显然,必须从盒子最上边取,这样取出的顺序就是: 《软件工程》《C 语言》《高等数学》,和放入的顺序正好相反,如图 4-7 所示:


图 4-7

从程序化角度,我们通过绿色箭头做一个标记,这个标记一直指着盒子最顶端的书籍。
总结:
如果说盒子就是一个栈,那么这个栈有两个操作,分别是入栈和出栈。
入栈,就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈,如果出栈的话,栈顶元素也是第一个被取出(LIFO)。


7 CPU 提供的栈机制

现在的 CPU 中都有栈的设计, 8086CPU 也不例外。8086CPU 提供相关的指令来以栈的方式访问内存空间。这意味着,在基于 8086CPU 编程的时候,可以将一段内存当做栈来使用。
8086CPU 提供入栈和出栈指令,最基本的两个是 PUSH 和 POP ,比如, push ax 表示将寄存器 ax 中的数据送入到栈中, pop ax 表示从栈顶取出数据送入到 ax。 8086CPU 的入栈和出栈操作都是以为单位进行。
举例说明,我们可以将 10000H~1000FH这段内存当做栈来使用,如图 4-8 所示:

图 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 两个寄存器存放着当前指令的段地址和偏移地址。

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 所示:
图 4-8

从图中,可以看出,8086CPU中入栈时,栈顶从高地址向低地址方向增长。

问题 4-6

如果将 10000H ~ 1000FH 这段地址当做栈,初始状态是空栈,此时 SS = 1000H,那么 SP=?
分析

图 4-9

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 axpush ax 正好相反,由以下两步组成:
(1)将 SS:SP 指向的内存单元处的数据送入 ax 中;
(2)SP=SP+2,SS:SP指向当前栈顶下面的一个单元,以当前栈顶下面的单元为新的栈顶。

如下图 4-10 所示, POP AX 的过程:


图4-10

这里需要注意第三步,尽管当前栈指针指向 1000E H 但是内存单元 1000DH 和 1000CH 中的数据任然存在,只不过不在栈中,直到下一次 PUSH 操作将其覆盖为止。

8 栈顶越界问题


这一小节,介绍越界问题。
比如,我们将内存地址空间 10010H ~ 1001FH 作为栈空间,该栈空间容量为 16 字节(8个字)。

栈空间

初始状态栈为空, SS=1000H,SP=0020H,SS:SP 指向 10020H。


初始状态

ax=0123H


寄存器 ax

执行 8 次 PUSH AX 操作,向栈中压入 8 个字,栈满,SS=1000H SP=0010, SS:SP 指向 10010H

向当前内存写入 8 条 push ax 指令

执行完 8 条指令,寄存器和内存情况

执行 8 次 PUSH AX 操作之后,栈满,如果在执行一次 PUSH AX,那么 SP=SP-2,SP=000E, SP=1000E

指向第九次 PUSH AX 操作

此时,PUSH 操作已经超过了栈空间,将栈外空间数据覆盖。

POP 操作类似。

主要操作细节是:
通过 -a 将命令写入当前指令寄存器指向的内存地址空间,然后,通过 -r 指令修改寄存器内容,包括栈寄存器 SP 和 SS 以及数据寄存器 AX 内容。
这里需要注意,POP 和 PUSH 都会出现越界,但是 8086CPU 并没有机制保证我们不越界,所以在编程的时候需要自己操作栈顶越界的问题。

9 push 、pop 指令


前面我们一直在使用 PUSH AXPOP AX 这两条指令,他们可以将数据在内存和寄存器之间进行传送。(栈空间是内存的一部分,它只是一段可以以一种特殊方式进行访问的内存空间)
除此之外, PUSH 和 POP 这两条指令的目标地址还可以是段寄存器或者内存空间。

练习 1


将 10000H~1000F 这段空间当做栈,初始状态栈空,将 ax,bx,ds 中的数据送入栈。

  1. 设置栈寄存器
    SS=1000H SP=0010 SS:SP=10010


  2. 将数据送入寄存器
    ax=1234H
    bx=5678H
    ds=9ABCH


  3. 将指令 push ax ,push bx, push ds 写入内存单元


  4. 执行第 3 步写入的指令


    执行 PUSH 操作
查看 PUSH 操作之后内存情况

练习 2


编程:
(1) 将 10000H~1000FH 这段空间当作栈,初始状态是空的;
(2)设置 AX=001AH , BX=001BH;
(3)将 AX、BX 中的数据入栈;
(4)然后将 AX、BX 清零;
(5)从栈中恢复 AX、BX 原来的内容。
解析:

  1. 首先设置栈寄存器值
    SS=1000H SP=0010H SS:SP=10010H

  2. 设置 AX BX 的值
    AX=001AH
    BX=001BH

  3. 入栈,执行 PUSH AX 和 PUSH BX 操作
    a 写入 push ax 和 push bx
    t 执行命令

  4. 清零
    ax=0000H
    bx=0000H

  5. 执行 POP BX POP AX 操作

10 栈段

实验:用机器指令和汇编指令编程

推荐阅读更多精彩内容