寄存器(内存访问)
3.1 内存中字的存储
CPU 中,用 16 位寄存器来存储一个字。高 8 位存放高位字节,低 8 位存放低位字节。在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字需要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
字单元的概念:字单元,即存放一个字型数据(16 位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。
N 地址字单元:起始地址为 N 的字单元。
任何两个地址连续的内存单元,N 号单元和 N+1 号单元,可以将它们看成两个内存单元,也可看成是一个地址为 N 的字单元中的高位字节单元和低位字节单元。
3.2 DS 和 [address]
CPU 要读写一个内存单元的时候,必须先给出这个内存单元的地址,在 8086PC 中,内存地址由段地址和偏移地址组成。8086CPU 中有一个 DS 寄存器,通常用来存放要访问数据的段地址。比如,我们要读取 10000H 单元的内容,可以用如下的程序段进行。
mov bx,1000H mov ds,bx mov al,[0]
上面的 3 条指令将 10000H(1000:0) 中的数据读到 al 中。
mov al,[0]
前面我们使用 mov 指令,可以完成两种传送:①将数据直接送入寄存器;②将一个寄存器中的内容送入另一个寄存器。
也可以使用 mov 指令将一个内存单元中的内容送入一个寄存器中。从哪一个内存单元送到哪一个寄存器中呢?在指令中必须指明。寄存器用寄存器名来指明,内存单元则需用内存单元的地址来指明。显然,此时 mov 的格式应该是 mov 寄存器名, 内存单元地址 。
“[···]”表示一个内存单元,“[···]”中的 0 表示内存单元的偏移地址。我们知道,只有偏移地址是不能定位一个内存单元的,那么内存单元的段地址是多少呢?指令执行时,8086CPU 自动取 ds 中的数据为内存单元的段地址。
再来看一下,如何使用 mov 指令从 10000H 中读取数据。10000H 用段地址和偏移地址表示为 1000:0,我们先将段地址 1000H 放入 ds,然后用 mov al,[0] 完成传送。mov 指令中的 [] 说明操作对象是一个内存单元,[] 中的 0 说明这个内存单元的偏移地址是 0,它的段地址默认存放在 ds 中,指令执行时,8086CPU 会自动从 ds 中取出。
mov bx, 1000H
mov ds, bx
若要用 mov al,[0] 完成数据从 1000:0 单元到 al 的传送,这条指令执行时,ds 中的内容为应为段地址 1000H,所以在这条指令之前应该将 1000H 送入 ds。
如何把一个数据送入寄存器呢?一般的寄存器可以使用如“mov ax,1”的方式送入数据,但是 ds 是一个段寄存器,8086CPU 不支持将数据直接送入段寄存器。那么如何将 1000H 送入 ds 呢?我们可以使用一个寄存器来进行中转,即先将 1000H 送入一个一般寄存器,如 bx,再将 bx 中的内容送入 ds。
3.3 字的传送
前面我们用 mov 指令在寄存器和内存之间进行字节型数据的传送。因为 8086CPU 是 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.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 寄存器,段寄存器 比如:mov ax,ds
add 和 sub 指令同 mov 一样,都有两个操作对象。
add 寄存器,数据 比如:add ax,8
add 寄存器,寄存器 比如:add ax,ax
add 寄存器,内存单元 比如:add ax,[0]
add 内存单元,寄存器 比如:add [0],ax
sub 寄存器,数据 比如:sub ax,9
sub 寄存器,寄存器 比如:sub ax,bx
sub 寄存器,内存单元 比如:sub ax,[0]
sub 内存单元,寄存器 比如:sub [0],ax
3.5 数 据 段
前面讲过,对于 8086PC 机,在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将一组长度为 N(N≤64KB)(因为偏移地址最大是64K)、地址连续、起始地址为 16 的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。比如用 123B0H~123B9H 这段内存空间来存放数据,我们就可以认为,123B0H~123B9H 这段内存是一个数据段,它的段地址为 123BH,长度为 10 个字节。
如何访问数据段中的数据呢?将一段内存当作数据段,是我们在编程时的一种安排,可以在具体操作的时候,用 ds 存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
比如,将 123B0H~123B9H 的内存单元定义为数据段。现在要累加这个数据段中的前 3 个单元中的数据,代码如下。
mov ax,123BH
mov ds,ax ;将 123BH 送入 ds 中,作为数据段的段地址
mov al,0 ;用 al 存放累加结果
add al,[0] ;将数据段第一个单元(偏移地址为 0)中的数值加到 al 中
add al,[1] ;将数据段第二个单元(偏移地址为 1)中的数值加到 al 中
3.1~3.5 小 结
(1)字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节放在低地址单元中,高位字节存放在高位地址单元中。
(2)用 mov 指令访问内存单元,可以在 mov 指令中只给出内存单元的偏移地址,此时,段地址默认在 DS 寄存器中。
(3)[address] 表示一个偏移地址为 address 的内存单元。
(4)在内存和寄存器之间传送字型数据时,高地址单元和高 8 位寄存器、低地址单元和低 8 位寄存器相对应。
(5)mov、add、sub 是具有两个操作对象的指令。jmp是具有一个操作对象的指令。
3.6 栈
在这里,我们对栈的研究仅限于这个角度:栈是一种具有特殊的访问方式的存储空间。它的特殊性就在于,最后进入这个空间的数据,最先出去。
栈有两个基本操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。栈的这种操作规则被称为:LIFO(Last In First Out,后进先出)。
3.7 CPU 提供的栈机制
现今的 CPU 中都有栈的设计,8086CPU 也不例外。8086CPU 提供相关的指令以栈的方式访问内存空间。这意味着,在基于 8086CPU 编程的时候,可以将一段内存当作栈来使用。
8086CPU 提供最基本的两个命令是 push(入栈) 和 pop(出栈)。比如,push ax 表示将寄存器 ax 中的内容送入栈中,pop ax 表示从栈顶取出数据送入ax。8086CPU 的入栈和出栈操作都是以字为单元进行的,高地址单元存放高 8 位,低地址单元存放低 8 位。
CPU 如何知道我们把哪一段内存当作栈呢?在 8086CPU 中,有两个寄存器,段寄存器 SS 和寄存器 SP,栈顶的段地址存放在 SS 中,偏移地址存放在 SP 中。任意时刻,SS:SP 指向栈顶元素。push 指令和 pop 指令执行时,CPU 从 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 栈顶超界的问题
8086CPU 不保证我们对栈的操作不会超界。这也就是说,8086CPU 只知道栈顶在何处(由 SS:SP 指示),而不知道我们安排的栈空间有多大。因此我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
3.9 push、pop 指令
push 和 pop 指令可以是如下形式:
push 寄存器 ;将一个寄存器中的数据入栈
pop 寄存器 ;出栈,用一个寄存器接收出栈的数据
当然也可以是如下形式:
push 段寄存器 ;将一个段寄存器中的数据入栈
pop 段寄存器 ;出栈,用一个段寄存器接收出栈的数据
push 和 pop 也可以在内存单元和内存单元之间传送数据,我们可以:
push 内存单元 ;将一个内存字单元的字入栈(注意:栈操作都是以字为单元的)
pop 内存单元 ;出栈,用一个内存单元接收出栈的数据
比如:
mov ax,1000H
mov ds,ax ;内存单元的段地址要存放在 ds 中
push [0] ;将 1000:0 处的字压入栈中
pop [2] ;出栈,出栈的数据送入 1000:2 处
指令执行时,CPU 要知道内存单元的地址,可以在 push、pop 指令中只给出内存单元的偏移地址,段地址在指令执行时,CPU 从 ds 中取得。
push、pop 实质上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与 mov 指令不同的是,push 和 pop 指令访问的内存单元的地址不是在指令中给出的,而是由 SS:SP 指出的。同时,push 和 pop 指令还要改变 SP 中的内容。
CPU 执行 mov 指令只需一步操作,就是传送,而执行 push、pop 指令却需要两步操作。执行 push 操作时,CPU 的两步操作是:先改变 SP,后向 SS:SP 处传送。执行 pop 时,CPU 的两步操作是:先读取 SS:SP 处的数据,后改变 SP。
注意:push、pop 等栈操作指令,修改的只是 SP。也就是说,栈顶的变化范围最大为:0~FFFFH。
提供:SS、SP指示栈顶;改变 SP 后写内存的是 push 指令;读内存后改变 SP 的是 pop 指令。
栈的综述
(1)8086CPU 提供了栈操作机制,方案如下。
在 SS、SP 中存放栈顶的段地址和偏移地址;
提供入栈和出栈指令,它们根据 SS:SP 指示的地址,按照栈的方式访问内存单元。
(2)push 指令的执行步骤:①SP=SP-2;②向SS:SP指向的字单元中送入数据。
(3)pop 指令的执行步骤:①从SS:SP指向的字单元中读取数据;②SP=SP+2。
(4)任意时刻,SS:SP 指向栈顶元素。
(5)8086CPU 只记录栈顶,栈空间的大小要我们自己管理。
(6)用栈来暂存以后需要恢复的寄存器的内容时,寄存器的出栈顺序要和入栈的顺序相反。
(7)push、pop 实质上是一种内存传送指令。
3.10 栈段
我们可以将长度为 N(N≤64KB) 的一组地址连续,起始地址为 16 的倍数的内存单元,当作栈空间来使用,从而定义了一个栈段。将一段内存当作栈段,仅仅是我们编程时的一种安排,CPU 并不会由于这种安排,就在执行 push、pop 等栈操作指令时自动地将我们定义的栈段当作栈空间来访问,我们要将 SS:SP 指向我们定义的栈段。
段的综述
我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。这完全是我们自己的安排。
我们可以用一个段存放数据,将它定义为“数据段”;ds
我们可以用一个段存放代码,将它定义为“代码段”;cs
我们可以用一个段当作栈,将它定义为“栈段”。ss
我们可以这样安排,但若要让 CPU 按照我们的安排来访问这些段,就要:
对于数据段,将它的段地址放在 DS 中,用 mov、add、sub 等访问内存单元的指令时,CPU 就将我们定义的数据段中的内容当做数据来访问;
对于代码段,将它的段地址放在 CS 中,将段中第一条指令的偏移地址放在 IP 中,这样 CPU 就将执行我们定义的代码段中的指令;
对于栈段,将它的段地址放在 SS 中,将栈顶单元的偏移地址放在 SP 中,这样 CPU 在需要进行栈操作的时候,比如执行 push、pop 指令等,就将我们定义的栈段当作栈空间来使用。
可见,不管我们如何安排,CPU 将内存中的某段内容当做代码,是因 CS:IP 指向了那里;CPU 将某段内存当作栈,是因为 SS:SP 指向了那里。
Debug 的 T 命令在执行修改寄存器 SS 的指令时,下一条指令也紧接着被执行。(也就是说一次 T 指令了两条指令)