第三章 寄存器(内存访问)
# 第三章 寄存器(内存访问)
本章从内存访问的角度学习相关寄存器。
# 3.1 内存中字的存储
字单元:存放一个字型数据(16 位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元存放字型数据的高位字节,低地址单元存放字型数据的低位字节。
这种存储方式也被称为小端存储,Intel 系列的处理器一般都是小端存储。
# 3.2 DS 和 [Address]
上一章我们学习了 CS 段寄存器,用于存放代码段段地址。这里我们再引入另外一个段寄存器 DS,用于存放数据段段地址。
需要特别注意的是,8086CPU 不支持将数据直接送入段寄存器。 包括所有的段寄存器 CS、DS、SS、ES 都不支持将数据从内存直接送入。内存中的数据必须先送入其他中间寄存器,然后在从中间寄存器送入段寄存器。(此处描述有误:栈操作 "pop 段寄存器" 实际上就是将数据从内存中直接送入段寄存器,此处应该更正为无法通过 move 指令将数据从内存中直接送入段寄存器)
"[address]" 表示一个内存单元,中括号中的 address 表示内存单元的偏移地址。默认情况下,8686CPU 取 DS 中的数据作为该内存单元的段地址。
# 3.3 字的传送
使用 move 指令一次可以传送一个字。move 指令可以将数据从内存送入寄存器,也可以将数据从寄存器送入内存,也可以将数据从寄存器送入寄存器。但 move 指令不支持内存到内存的传送。
# 3.4 mov、add、sub 指令
add 指令和 sub 指令与 mov 指令用法类似,他们都有两个操作对象。这两个操作对象可以是如下格式:
寄存器, 数据
寄存器, 寄存器
寄存器, 内存单元
内存单元, 寄存器
有两点需要注意:
(1) mov、add、sub 指令的两个操作对象不能同时为内存单元。
(2) 段寄存器只能接收 mov 指令传送数据,不可以进行算术运算。如 add ds, ax 指令是违法的。(此处描述不够严谨,实际上段寄存器也可以接收来自操作栈的 pop 指令传递的数据)
# 3.5 数据段
数据段是一段长度为 N (N <= 64KB)、地址连续、其实地址为 16 的倍数的内存单元。我们用段寄存器 DS 存放数据段的段地址。
# 3.1~3.5 小结
(1) 字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
(2) 用 mov 指令访问内存单元,可以在 mov 指令中只给出单元的偏移地址,此时,段地址默认在 DS 寄存器中。
(3)[address] 表示一个偏移地址为 address 的内存单元。
(4) 在内存和寄存器之间传送数据时,高地址单元和高 8 位寄存器、低地址单元和低 8 位寄存器相对应。
(5) mov、add、sub 是具有两个操作对象的指令。jmp 是具有一个操作对象的指令。
(6) 可以根据自己的推测,在 debug 中实验指令的新格式。
# 3.6 栈
栈就是一种先进后出的数据结构。LIFO (Last In First Out)。
# 3.7 CPU 提供的栈机制
8086CPU 对栈提供两个基本操作指令:PUSH(入栈)和 POP(出栈)。 PUSH 是将数据送入栈中,POP 是将数据移出栈中。
前面我们已经学习了 CS 和 DS 两个段寄存器。并且知道 CS:IP 指向的内存单元被当做指令,DS:[address] 指向的内存单元被当做数据。这里我们引入另外一个段寄存器 SS,SS 中保存的是栈顶元素的段地址,此外使用 SP 保存栈顶元素的偏移地址。故在任意时刻 SS:SP 都指向栈顶元素。
PUSH AX 的操作详情:
(1)SP=SP-2,SS:SP 指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
(2)将 ax 中的内容送入 SS:SP 指向的内存单元,SS:SP 此时指向新的栈顶。
POP AX 的操作详情:
(1)将 SS:SP 指向的内存单元处的数据送入 ax 中;
(2)SP=SP+2,SS:SP 指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
# 3.8 栈顶超界问题
当栈满的时候进行 PUSH 操作或者栈空的时候使用 POP 操作,都将引发栈顶超界问题。
8086CPU 并未对栈顶超界做任何处理,程序员在编程的时候应当避免使得栈顶超界的情况发生。
# 3.9 push、pop 指令
push 指令和 pop 指令支持如下形式:
push 寄存器
push 段寄存器
push 内存单元
pop 寄存器
pop 段寄存器
pop 内存单元
2
3
4
5
6
7
2
3
4
5
6
7
push、pop 实际上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与 mov 指令不同的是,push 和 pop 指令访问的内存单元的地址不是在指令中给出的,而是由 SS:SP 指出的。同时,push 和 pop 还要改变 sp 中的值。CPU 执行 mov 指令仅需一步,CPU 执行 push 和 pop 指令需要两步:传送数据和修改 sp 的值。
需要注意的是,push、pop 等栈操作指令,修改的只是 SP,也就是说,栈顶的变化范围最大为:0~FFFFH。
栈的综述
(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 实际上是一种内存传送指令,注意它们的灵活应用。
栈是一种非常重要的机制,一定要深入理解,灵活掌握。 (P67)
# 3.10 栈段
与代码段、数据段类似,我们在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将长度为 N (N<=64KB) 的一组地址连续、起始地址为 16 的倍数的内存单元,当做栈空间来用。只需要使用 SS:SP 指向它们。
一个栈最大为 64KB,即偏移地址所能指向的最大范围。当一个大小为 64KB 的栈,其 SP=0 时则表示该栈为空或者栈满。
段的综述
我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元(通过偏移地址的移动来访问段内的单元)。这完全是我们自己的安排。我们可以用一个段存放数据,将它定义为 “数据段”;
我们可以用一个段存放代码,将它定义为 “代码段”;
我们可以用一个段当做栈,将它定义为 “栈段”;我们可以这样安排,但若要让 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
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
设置 CS=1000H,IP=0,这段代码将得到执行。可以看到,在这段代码中,我们又将 10000H1001FH 安排为栈段和数据段。10000H1001FH 这段内存,即是代码段,又是栈段和数据段。
一段内存,可以即是代码的存储空间,又是数据的存储空间,还可以是栈空间,也可以什么也不是。关键在于 CPU 中寄存器的设置,即 CS、IP,SS、SP,DS 的指向。
(p69)