在8086CPU中,地址总线宽度为20,可以传送20位的地址,达到1MB的寻址能力,但寄存器都是16位的,所以表现出来的寻址能力只有64KB。在CPU寻址过程中,CPU会根据16位的段地址和16位的偏移地址来进行地址的合成,生成20位的物理地址。

8086CPU中有以下段地址寄存器:

  1. DS:数据段地址寄存器,是默认的数据段地址,CPU根据该段地址和对应的偏移地址来获取内存数据
  2. SS:栈段地址寄存器,和SP(栈顶寄存器)表示的偏移地址配合使用,来进行栈的寻址和操作
  3. CS:代码段地址寄存器,和IP(指令指针寄存器)表示的编译地址配合使用,来进行内存代码数据的寻址操作
  4. ES:附加段地址寄存器,可以作为内存数据的段地址。如:当对内存数据寻址时,可以指定内存数据的段地址寄存器为ES, 若没有指定段地址,则默认DS中的数据为段地址。

CPU根据特定规则来进行最终地址的生成:段地址 × 16 + 偏移地址
这里的段地址和偏移地址常用16进制来表示,数值后边加H来表示,一个16位进制对应4位二进制,各个进制的转换关系不在此进行说明。

8086系列CPU存取数据是从低地址到高地址来进行的,如一段10000H~10005H的内存单元,每个内存单元表示一个字节即8位二进制:

内存单元

对应数据

10000H

10H

10001H

20H

10002H

30H

10003H

40H

10004H

50H

10005H

60H

当把内存单元10000H数据送入AX寄存器,需要获取2个内存单元的数据,即10000H为低地址存入AX的低8位,10001H为高地址存入AX的高8位,此时AX的值为2010H。反过来当把AX的值送入内存地址10000H时,需要2个内存单元存放,高地址10001H存放AX的高8位数据:20H,低地址10000H存放AX的低8位数据。

内存数据

内存数据的获取需要通过 数据段寄存器值 × 16 + 偏移地址 来进行寻址,偏移地址已“[]”符号表示。
表示规则: DS:[偏移地址]
段地址:DS可以省略,默认为DS寄存器,也可以指定附加段寄存器,如: ES:[偏移地址]
偏移地址:可以为数值,也可以为其他寄存器,如: [AX]

段地址×16:对于段数据 1000H 来说,1000H × 16 = 10000H,从二进制的角度来看,相当于段数据左移了4。假设此时给出的偏移地址为 0001H,那么最终的内存地址就是:10001H。

既然知道了内存地址的表示方法,但应该怎么去修改内存数据?

传送指令 MOV
可以通过该指令来对指定寄存器或内存数据进行修改,MOV指令后有两个操作对象,语法如下:

MOV 操作对象1,操作对象2

该指令把操作对象2表示的数据传送到操作对象1中,不修改操作对象2的数据。

注意:CS和IP寄存器不能通过mov指令来进行修改,CPU并未提供这样的操作,可用JMP指令来进行修改,后续将会介绍到。

根据该语法可以修改寄存器 AX 的值为 0 :

mov ax, 0H

对于段寄存器来说,不能直接使用mov指令来进行立即数的赋值,因为对于CPU硬件来说,没有提供这样的操作,但可以通过其它寄存器来修改,此时 DS = 1000H:

mov ax, 1000H
mov ds, ax

对于内存来说,也可以通过寄存器来实现数据传送:
把 1000H 数据送入内存地址为:1000H:0H 的位置:

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

这样就实现了对内存数据的修改,然后把该内存数据的数据,放入到 BX 中:

mov bx, [0]

对于mov指令来说,不支持对段地址和立即数,段地址和段地址之间的数据传送,但可以通过内存数据来赋值:

mov ds, [0]

加法指令 ADD
该指令接受和mov指令一样,接受两个操作对象,来对两个操作对象数据相加,结果送入到操作对象1中,语法如下:

add 操作数1,操作数2

因段地址的不同用途,add 指令不支持对包含段地址的运算指令。

计算两数之和,并把结果保存到 AX 中:

mov ax, 1
mov bx, 2
add ax, bx

减法指令 SUB
该指令和 ADD 语法一样,计算操作对象1减操作对象2的值,并把结果保存到操作对象1中。语法如下:

sub 操作对象1,操作对象2

计算两数之差,并把结果保持到 AX 中:

mov ax, 2
mov bx, 1
sub ax, bx

注意,以上的操作都是依赖于寄存器来实现的,因为寄存器指定了操作对象中数据的类型,如字型数据、字节型数据。在操作的过程中,我们需要保证两个操作对象数据类型的一致性,不然就是非法的,因为对于cpu来说,无法确认应该使用多少个内存单元来满足一次运算。除了寄存器外,我们还可以使用其它的方式来指定操作数据的类型,如word ptr(字型数据)、byte ptr(字节型数据)。后续将会对这些进行说明。

指令执行

在计算机中,所有的数据和指令都是通过二进制数值进行存放的,所以我们在内存数据中写入的汇编指令到计算机中也是翻译为二进制形式的机器指令,那么CPU是根据什么来判断某一块内存中的数据是不是要当成指令来执行呢?

刚刚介绍过,CPU把 DS:[偏移地址] 指向的内存内容当成数据处理,对于指令,CPU通过 CS (代码段寄存器)IP (指令指针寄存器) 指向内存内容当成机器指令来处理。

同样,执行指令的地址可以使用公式计算出来:CS× 16 + IP

CPU通过 CS:IP 指向的地址获取指令进行执行,指令执行过程如下:

  1. CPU通过CS和IP合成物理地址
  2. 读取该物理地址下的指令并放入指令缓冲器,在执行指令前使IP的值增加该条指令的长度
  3. 执行指令

很多情况下,我们所执行的指令并处于 CS:IP 的指向的地址,这个时候可以通过 无条件跳转指令 JMP 来更改寄存器 CS 和 IP 的值来指向我们指令所处的内存单元。格式如下:
段间转移:

jmp 段地址:偏移地址

也可以单独修改寄存器 IP 的值,格式如下:
段内转移:

jmp 操作对象

例一:修改寄存器 CS=1000H, IP=0001H:

jmp 1000H:0001H

例二:修改寄存器IP=0010H

mov ax, 0010
jmp ax

关于更多 JMP 指令和特性,将放在后续的学习中在进行介绍,在此章中只介绍CPU的指令执行过程。

栈操作

栈是一种具有特殊访问方式的存储空间,有栈底和栈顶之分,遵循先进后出的原则。栈一般有压栈和出栈两种操作。
CPU提供了支持栈操作的寄存器和指令。
SS(栈段寄存器)制定了一个栈的段地址,SP(栈顶指针寄存器)指向栈顶地址。通过 SS × 16 + SP 来寻址栈顶。

压栈指令 PUSH
push指令接受一个操作对象,该操作对象可以寄存器、段寄存器、内存单元,格式如下:

push 操作对象

如,把寄存器ax中的值压入栈中

mov ax, 1020H
push ax

在把一个操作对象压入栈中时,cpu实际做以下操作:

  1. CPU计算下一个栈顶位置,以便在该栈顶位置压入操作对象,计算如下:SP = SP - 2,此时SS:SP指向新栈顶。
  2. 在SS:SP指向的新栈顶处压入操作对象数据

对于栈顶指针寄存器 SP 来说,始终指向栈的栈顶位置,如:对于一个新栈,内存单元范围为10000H~10010H,若栈段寄存器SS = 1000H,则SP应为10010H内存单元的下一个内存单元,所以SP = 0011H。当对寄存器ax进行压栈后,SP指向新的栈顶位置,SP = 000EH。此时1000EH内存单元存放AX的低8为数据,即20H,10010H内存单元存放AX的高8位数据,即10H。

出栈指令 POP
pop指令接受一个操作对象,该操作对象可以寄存器、段寄存器、内存单元,格式如下:

pop 操作对象

如:把刚刚压入栈的数据出栈到bx中,此时bx=1020H:

pop bx

在把一个栈中数据出栈时,CPU实际做一下操作:

  1. 通过栈顶指针指向的内存单元取出对应的数据,并存放到操作对象中
  2. 计算出栈后的栈顶指针位置,计算如下:SP = SP + 2

对于8086CPU来说,因为寄存器都是16位的,所以栈顶指针寄存器最多可以使用2的16次方个内存单元,若在达到最大的栈顶位置还在进行压栈时,新压栈的数据会覆盖原先已入栈的数据。
如,一个无数据的栈空间:1000H~1FFFFH,假设SS = 1000H,此时栈顶指针寄存器SP=0000H,指向新栈的栈顶。第一次执行push后,SP = SP - 2 = FFFEH,当一直进行压栈操作时,SP会重新赋值位0000H,当在进行压栈时,就会对以前入栈的数据进行覆盖。
所以,对于栈的操作,要注意栈顶超界的问题,否则可能会造成对自己栈和其他内存单元的数据照成影响。