第2章内容,主要从CPU如何执行指令的角度讲解了8086CPU的逻辑结构、形成物理地址的方法、相关的寄存器以及一些指令。
这一章,从访问内存的角度继续学习几个寄存器。
一、内存中字的存储
CPU中,用16位寄存器来存储一个字。高8位存放高位字节,低8位存放低位字节。在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。比如,从0地址开始存放20000,这种情况如下所示:
进制转换
此时的4E20 H一共有16位数据
现在要将这16位数据放到内存单元中,一个内存单元存放8位,即2个字节,放进去后如下图所示
现在,再提出一个概念,字单元:一个存放字型数据(16位)的内存单元,有两个地址连续的内存单元组成。
在以后的课程中,将起始地址为N的字单元简称为N地址字单元。比如,上面的例子中,字单元的起始地址为0,可以说这是0地址字单元。
二、DS和[address]
CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址,在8086PC机中,内存地址由段地址和偏移地址组成。8086CPU中有一个DS寄存器,通常用来存放要访问数据的段地址。
比如,我们要读取10000 H单元的内容,可以用如下的程序段进行。
; 下面的3条指令将10000H(1000:0)中的数据读到al中
mov bx,1000H
mov ds,bx
mov al,[0]
对上面的指令讲解,首先是mov
指令的用法
第3种用法中,[0 ]
中存放的是内存单元的偏地址0
但是,我们知道,只有一个偏移地址是不能定位一个内存单元的,内存单元的段地址是多少?指令执行时,8086CPU自动取DS中的数据为内存单元的段地址。
另外,上面第1条语句和第2条语句
mov bx,1000H
mov ds,bx
可不可以合成如下的一句话
mov ds,1000H
答案是:不能!因为不可以给段寄存器直接赋值,只能先赋值给一个寄存器吗,然后利用这个寄存器给段寄存器赋值。
上面是将1000:0
的物理地址送到al
中,如果将al
中的数据从到1000:0
中,应该怎么做
; 下面的3条指令将al中的数据送到10000H(1000:0)
mov bx,1000H
mov ds,bx
mov [0],al
三、字的传送
前面使用mov
指令在寄存器和内存之间进行字节型数据的传送。因为8086CPU是16位结构,有16根数据线,所以可以一次性传送16位数据(即1个字,2个字节)。只要在mov
指令中给出16位的寄存器就可以进行16位数据的传送了。
mov bx,1000H
mov ds,bx
mov ax,[0] ; 将1000:0处的字型数据送入ax
mov [0],cx ; cx中的16位数据送到1000:0处
例子:《汇编语言》第3版P62
四、mov、add、sub指令
前面用到了mov
、add
和sub
指令,它们都带有2个操作对象,对于mov
指令,它有如下几种形式
对于add
和sub
指令,这两个应该差不多,下面给出几个指令,至于其指令能否像mov
指令那样使用,需要验证一下
五、数据段
对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。
可以将一组长度#card=math&code=N%28N%5Cleqslant%2064%5Ctext%7BKB%7D%29&id=Bnmca)、地址连续、起始地址为16的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。
比如用123B0H~123B9H这段内存空间存放数据,就可以认为,123B0H - 123B9H这段内存是一个数据段,它的段地址为123BH,长度为10个字节。
PS:这段和原来的代码段的定义是一样的,所以我就可以使用原来的图了
现在将123B0H - 123B9H的内存单元定义为数据段,现在要累加这个数据段中前3个单元中的数据,代码如下
mov ax,123BH
mov ds,ax ; 将123BH送入DS中,作为数据段的首地址
mov al,0 ; 用AL存放累加结果
add al,[0] ; 将123B:0中的数据加到AL中
add al,[1] ; 将123B:1中的数据加到AL中
add al,[2] ; 将123B:2中的数据加到AL中
六、栈
这里对栈的理解就很浅显了:最先进的最后出
七、CPU提供的栈机制
现今的CPU都有栈的设计,8086CPU也不例外。8086CPU提供相关的指令来以栈的方式访问内存空间。这意味着在8086CPU编程的时候,可以将一段内存当作栈来使用。
8086提供入栈和出栈指令
- PUSH:入栈
- POP:出栈
; 注意:8086CPU的入栈和出栈操作都是以字为单位进行的
push ax ; 将寄存器ax中的数据送入栈中
pop ax ; 从栈顶取出数据送入ax
下面举例说明,下面可以将10000H-1000FH这段内存当作栈来使用,使用下面这个动图来描述一段指令的执行过程
指令代码如下
mov ax,1023H
push ax
mov bx,2266H
push bx
mov cx,1122H
pushcx
pop ax
pop bx
pop cx
上面可以看出push
和pop
指令的执行过程,但是其中有两个问题
- 我们将10000H-1000FH这段内存当作栈来使用,一个重要的问题是,CPU怎么知道这段内存是栈?
- 我们使用
push ax
指令的时候,要将寄存器内容放在当前栈顶的上方;使用pop ax
指令的时候,要从栈顶取出数据,送入寄存器。然而,pop
和push
指令在执行的时候必须知道哪个单元是栈顶单元,怎么知道呢?
之前学习内存地址的时候,使用CS和IP来告诉CPU其物理地址,这里同样有对应的寄存器SS和SP来告诉栈顶的地址
现在,可以完整描述push
和pop
指令的功能了,例如push ax
的执行,有下面两步完成
SP=SP-2
,SS:SP
指向当前栈顶前面的单元,以当前栈顶前面的单元作为新的栈顶- 将
ax
中的内容送入SS:SP
指令的内存单元出,ss:sp
此时指向新栈顶。
下图描述了8086CPU对push
指令的执行过程
从图中,可以看出8086CPU入栈时,栈顶从高地址向低地址方向增长。
接下来,描述pop
指令的功能,例如pop ax
有以下两步完成
- 将
SS:SP
指向的内存单元处的的数据送入ax
中 SP=SP+2
,SS:SP
指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
注意:上图中,出栈后,SS:SP
指向新的栈顶1000EH,pop
操作前的栈顶元素,1000CH处的2266H依然存在。但是,它以不在栈中。当再次执行push
等入栈指令后,SS:SP
移至1000CH,并在里面写新的数据,它将被覆盖。
八、栈顶超界的问题
通过上述的过程,我们现在知道了8086CPU用SS和SP指示栈顶的地址,并提供push
和pop
指令实现入栈和出栈。
但是,还有另外一个问题,SS和SP只是记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈时找到栈顶。可是,如何保证入栈、出栈时,栈顶不会超出栈空间?
栈顶超界是很危险的,既然我们将一段空间安排为栈,那么在栈空间之外的空间里可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们程序之中的,也可能是别的程序中的(毕竟一个计算机系统并不是只有我们自己的程序在运行)。但是,由于我们在入栈出栈时不小心,将这些数据、代码意外改写会引起一连串的错误。
这个时候,我们希望CPU中有记录栈顶上限和栈底的寄存器,我们通过这些寄存器来指定栈空间的范围,从而保证不会超界。
但是,8086CPU不保证我们对栈的操作不会超界。也就是说,8086CPU只知道栈顶在何处(由SS : SP指示),而不知道我们安排的栈空间有多大。这点就好像CPU只知道当前要执行的命令在何处(由CS : IP指示),而不知道要执行的指令有多少条。
所以,在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致超界;执行出栈操作的时候也要注意,防止栈空的时候继续出栈而导致超界。
九、push、pop指令
push
和pop
指令是可以在寄存器和内存之间(栈空间是内存空间的一部分,是一段可以以一种特殊的方式进行访问的内存空间)传递的数据。当然,push
和pop
指令也可在内存单元之间传递数据
比如,下面这个例子:
指令执行时,CPU要知道内存单元的地址,可以在push
和pop
指令中只给出内存单元的偏移地址,段地址在指令执行时从ds中取得。
mov ax,1000H
mov ds,ax ; 内存单元的段地址要放在ds中
push [0] ; 将1000:0处的字压入栈中
pop [2] ; 出栈,出栈的数据送入1000:2处
9.1 练习题
问题3.7
问题3.8
答案以及其分析:
问题3.9
答案以及分析
问题3.10
答案以及分析
9.2 综述
十、栈段
之前知道了数据段和代码段的定义,其实栈段也是一样的
对于8086PC机,在编程时,根据需要将一组内存单元定义为一个段。将长度为#card=math&code=N%28N%5Cleqslant%20%5Ctext%7B64KB%7D%29&id=PC7SF)一组地址连续、起始地址为16的倍数的内存单元,当作栈空间来使用,从而定义了一个栈段。
将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU并不会这种安排,就在执行push
和pip
等栈操作指令时自动地将我们定义的栈段当作栈空间来访问?
只能通过SS:SP
指向我们定义的栈段
10.1 问题1
如果将10000H-1FFFFH这段空间当作栈段,初始状态栈是空的,此时SS=1000H,SP= 0 ?
当整个栈为空的时候,其指向的是整个栈的下一个内存单元:20000H,这个时候SS:2000H, SP:0
10.2 问题2
一个栈段最大可以设为多少?为什么?SP的寻址大小
通过定义我们就可以到了,既然一个栈段大小为,这个大小是由偏移地址来确定的,所以对应的SP的大小为
检测
检测3.1
内存中的情况如图3.6所示:
各寄存器的初始值:CS=2000H
、IP=0
、DS=1000H
、AX=0
、BX=0
- 写出CPU执行的指令序列
```assembly ; 因为CS=2000H IP=0,所以执行2000:0处的代码 mov ax,6622H
; 跳转到下一个命令 jmp 0ff0:0100 ; 修改为CS:0ff0 IP:0100,即地址10000H
mov ax,2000H
mov ds,ax
mov ax,[0008] ; 将2000:8处的数据给ax,即AX=C389H,注意这里当作数据处理 mov ax,[0002] ; 同上,这里只是当作数据处理,如果当作代码处理的话,只能用jmp命令
2. 写出CPU执行每条指令后,CS、IP和相关寄存器中的数值
3. 再次体会:数据和程序有区别吗?如何确定内存中的信息哪些是数据,哪些是程序?<br />其实,通过上面的例子我们也可以知道,要想确定哪些是程序,只能通过`CS`和`IP`的大小来确定,**如果**`**CS**`**和**`**IP**`**大小有已经确定地指向了某一区域,那么这块区域就是代码**,其余的就当作数据处理。
<a name="cda56704"></a>
## 检测3.2
下面的3条指令执行后,CPU几次修改IP?都是在什么时候,最后IP中的值是多少?
```assembly
mov ax,bx
sub ax,ax
jmp ax ; 只有这次修改IP,IP的值为0