上文,我们主要从CPU 执行指令的角度聊寄存器,我们这次深入寄存器的内存,从内存的角度再理解寄存器。
我们介绍过,在(8086)CPU 内,我们用16bit 寄存器存储一个“字”,即字型数据,低 8 位存放低位数据,高 8 位存放高位数据,注意,低位字节排在高位字节前(从栈的角度而言,低位字节较为接近栈顶),总结一下,字单元的概念就是:存放一个 16bit 数据的内存单元(有两块地址连续的 8bit 地址单元组成,为什么这样设计上期有叙),而高低字节是可以转化的,并非定死的,这个后文再述,以后,我们将起始地址为 N 的字单元简称为 N 地址字单元,包含(N 与 N+1 的内存单元)。
CPU 要读取数据必先知其地址,而 8086 中的地址前文有述,是段地址加上偏移地址,8086 中有 ds 寄存器,用于存放即将要访问的数据的地址。例如,要读取 1000H 单元格的内容,我们可以这样进行
mov bx,1000H
mov ds,bx
mov al,[0]
解读:
mov al,[0]
前面我们使用 mov 指令,不但可以将数据直接送入寄存器,还可以实现数据在寄存器间的传送,当然也可以实现内存单元到寄存器的传送。当然,mov 指令有其使用语法:
mov target, source
target 指移动的目标,source 指被移动数据的源;当然这里只是 mov 指令的其中一种语法
还有几种语法:
mov | 寄存器,数据 | 比如:mov ax,8 |
mov | 寄存器,寄存器 | 比如:mov ax,bx
|
mov | 寄存器,内存单元 | 比如:mov ax,[0] |
mov | 内存单元,寄存器 | 比如:mov [0],ax |
mov | 段寄存器,寄存器 | 比如:mov ds,ax |
我们知道,只有偏移地址是不能定位一个内存单元的,那么内存单元的段地址在哪里获取呢?在看到 mov al,[0] 这行指令的时候,你是不是也有这个疑问?事实上,指令执行时,8086CPU 自动取 ds 中的段地址为内存地址中的段地址。
那么 CPU如何用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 ,所以有这两条指令。
读到这里,你有没有开始思考,为什么要用两天指令完成对 ds 寄存器的赋值?你前面不是说了可以用 mov 指令直接完成对寄存器的赋值吗?这里为什么不可以 mov 直接对 ds 赋予一个内存单元的地址呢?
从理论上讲,我们可以用相似的方式:mov ds,1000H,来将1000H送入ds。可是,现实却无法这样做,因为8086CPU不支持将数据直接送入段寄存器的操作,注意,补充一个知识点,ds是一个段寄存器,所以 mov ds, 1000H这条指令是非法的。所以我们只好用一个寄存器(ax,bx,cx,dx 任一,四者无明显功能分别)来进行中转。
为什么8086CPU不支持将数据直接送入段寄存器的操作?这属于8086CPU硬件设计的问题,我们只要知道这一点就行了。(大佬可以去 intel 官网查找 8086 的细节,似乎下架了?)
问题3.2
写几条指令,将al中的数据送入内存单元10000H中,思考后看分析。
分析:
怎样将数据从寄存器送入内存单元?从内存单元到寄存器的格式是:“mov寄存器 名,内存单元地址”,从寄存器到内存单元则是:“mov内存单元地址,寄存器名”。 10000H可表示为1000:0,用ds存放段地址1000H,偏移地址是0,则mov [0],al可完成 从al到10000H的数据传送。完整的几条指令是:
mov bx,1000H
mov ds,bx
mov [0],al
我们之前说过,CPU 通过数据总线获取数据,有多少条数据线就可以传输多少位的数据,8086 是 16bit 的 CPU,因此拥有 16 条数据线,可以一次性传输一个字 ,只要在 mov 内给出 16bit 的地址,即可进行 16bit 数据的传输。
mov bx,1000H
mov ds,bx
mov ax,[0]//这里成功将 1000:0 的字型数据传入 ax
mov [0],cx// 这里将 16bit 寄存器 cx 中的数据传入 1000:0 处
问题3.3
内存中的情况如图3.2所示,写出下面的指令执行后寄存器ax,bx,cx中的值。
mov ax,1000H
mov ds, ax
mov ax, [0]
mov bx, [2]
mov ex, [1]
add bx, [1]
add ex, [2]
表3.1指令执行与寄存器中的内容(1)
指令 | 执行后相关寄存器中的内容 | 说明 |
mov ax,1000H | ax=1000H | 前两条指令的目的是将ds设为1000H |
mov ds, ax | ds=1000H | |
mov ax, [0] | ax=1123H | 1000:0处存放的字型数据送入ax: 1000:1单元存放字型数据的高8位:11H, 1000:0单元存放字型数据的低8位:23H, 所以1000:0处存放的字型数据为1123Ho 指令执行时,字型数据的高8位送入ah,字 型数据的低8位送入al,则ax中的数据为 1123H |
mov bx, [2] | bx=6622H | 这里说明了,低字节是由【】决定的! |
mov cx, [1] | cx=2211H | |
add bx, [1] | bx=8833H | |
add ex, [2] | cx=8833H |
看完了字的传送,我们来看看 add 和 sub 指令,以及前文已经提过的 mov 指令。
到现在,我们已知的 mov 通路为:1、数据——>寄存器;2、寄存器——>寄存器;3、内存单元——>寄存器;4、寄存器——>内存单元; 5、寄存器——>段寄存器;
我们学习 CPU 地址总线,数据总线等等的时候,知道地址总线或者数据总线都是双向联通的,那么 mov 的通路是否是互通的呢?(理论上,是的)【不验证,只给结论】
6、mov 寄存器,段寄存器;(即段寄存器——>寄存器)7、mov 内存单元,段寄存器;(段寄存器——>内存单元) 8、mov 段寄存器, 内存单元;(内存单元——>段寄存器)
而 add 与 sub 指令与 mov 指令一样,都要有两个操作对象,他们的语法形式如下:add 就是+,sub 就是-(减)的意思。
add寄存器,数据 | 比如:add ax,8 |
add寄存器,寄存器 | 比如:add ax,bx |
add寄存器,内存单元 | 比如:add ax,[0] |
add内存单元,寄存器 | 比如:add [0],ax |
sub寄存器,数据 | 比如:sub ax,9 |
sub寄存器,寄存器 | 比如:subax,bx |
sub寄存器,内存单元 | 比如:sub ax5[0] |
sub内存单元,寄存器 | 比如:sub [0],ax |
前面讲过,对于搭载8086CPU的计算机,在编程时,可以根据需要,将一组内存单元定义为一个段。
我们可以将一组长度为N(NW64KB)、地址连续、起始地址为16的倍数 的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。比如用123B0H〜 123B9H这段内存空间来存放数据,我们就可以认为,123B0H〜123B9H这段内存是一个 数据段,它的段地址为123BH,长度为10个字节。 ——《汇编语言》 王爽 著
我们如何定义数据段?这个我们后文再叙,我们现在需要了解如何使用相关指令访问数据段中具体单元,例如将123B0H〜123B9H的内存单元定义为数据段。现在要累加这个数据段中的前 3个单元中的数据,代码如下:
mov ax,123BH
mov ds,ax ;将123BH送入ds中,作为数据段的段地址
mov al,0 ;用al存放累加结果
add alr [0] ;将数据段第一个单元(偏移地址为0)中的数值加到al中
add al,[1] ;将数据段第二个单元(偏移地址为1)中的数值加到al中
add al,[2] ;将数据段第三个单元(偏移地址为2)中的数值加到al中
注意,调用不同类型数据时,要注意数据类型大小与偏移量的关系,如写几条指令,累加数据段中的前3个字型数据:
mov ax,123BH
mov ds,ax ;将123BH送入ds中,作为数据段的段地址
mov ax,0 ;用ax存放累加结果
add ax,[0] ;将数据段第一个字(偏移地址为0)加到ax中
add ax,[2] ;将数据段第二个字(偏移地址为2)加到ax中
add ax,[4] ;将数据段第三个字(偏移地址为4)加到ax中
注意,一个字型数据占两个单元,所以偏移地址是0、2、4。
聊了那么多内存有关的东西,我们也许有个疑问,那个在 C 和 C++出镜率比较高的“栈”,去哪了?我们这就来介绍“栈” 。
我们在之前就知道了:栈是一种具有特殊的访问方式的存储空 间。它的特殊性就在于,最后进入这个空间的数据,最先岀去。并且明晰了栈顶的概念。栈作为一种受限线性表,仅在表尾进行插入和删除操作的线性表。进行数据操作的这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作入栈或压入(PUSH),它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈(POP),它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。具体关于栈的内容可参考我的文章:2021-7-15 从“栈”的角度看程序安排(炉边小坐)-Menou16。
现如今的 CPU 都有栈的设计,8086 作为老前辈也是有的。8086CPU 有相关的指令,可以将一段内存作为“栈”来使用。这里明晰一个概念,出栈和入栈的数据操作都是以“字”byte 为单位的。
mov ax,0123H
push ax
mov bx,2266H
push bx 上图描述了这样一段指令
mov cx,1122H
push cx
pop ax
pop bx
pop cx
但是我们还有疑问:其一、CPU 如何知道要将这一段内存当做栈使用?其二、如何确定栈顶?
我们之前讨论过,CPU如何知道当前要执行的指令所在的位置?我们现在知道,CS、IP中存放着当前指令的段地址和偏移地址。现在的问题是:CPU如何知道栈顶的位置? 也有相应的寄存器来解决这个问题,这就是 SS:SP 道理与 CS:IP相同,CPU 从 SS:SP 得到栈顶地址,且任意时刻,SS:SP指向栈顶元素。push指令和pop指 令执行时,CPU从SS和SP中得到栈顶的地址。
注意注意注意:栈顶在地址较大一端!!!!(栈从高地址向低地址分配内存)
在了解了栈顶的规定后,我们可以理解 push 和 pop 指令的作用:
push ax的执行,由以下两步完成。
- SP=SP-2, SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
- 将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶。
有个很有趣的问题,我们关注一下
如果将10000H〜1000FH这段空间当作栈,初始状态栈是空的, SP=?
答:SP=0010H ,分析如下:
将10000H〜1000FH这段空间当作栈段,SS=1000H,栈空间大小为16字节,栈最底部的字单元地址为1000:000E。任意时刻,SS:SP指向栈顶,当栈中只有一个元素的时候SS=1000H, SP=000EH。栈为空,就相当于栈中唯一的元素出栈,出栈后, SP=SP+2, SP原来000EH,加2后SP=10H,所以,当栈为空的时候,SS=1000H, SP=10H。
换一个角度看,任意时刻,SS:SP指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素,所以SS:SP只能指向栈的最底部单元下面的单元,该单元的偏移地 址为栈最底部的字单元的偏移地址+2,栈最底部字单元的地址为1000:000E,所以栈空时,SP=0010H。
pop ax的执行过程和push ax刚好相反,由以下两步完成。(弹出)
- 将SS:SP指向的内存单元处的数据送入ax中;
- SP=SP+2, SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
注意:出栈后,SS:SP指向新的栈顶1000EH, pop操作前的栈顶元素, 1000CH处的2266H依然存在,但是,它已不在栈中。当再次执行push等入栈指令后, SS:SP移至1000CH,并在里面写入新的数据,它将被覆盖。