这章涉及了一个重点概念——栈(还好有栈,让系统有了那么多的漏洞)。
1、内存中字的存储
1个字=2个字节,如从0开始存放20000(4E20H),20、4E分别表示1个字节,内存中字的存储如下图所示:
这里,0号单元对应的是低地址单元,1号单元对应的是高地址单元。
任何两个地址连续的内存单元(0、1、2......),N号单元和N+1号单元,可以看成两个内存单元。比如说上图中的0
内存单元(字节单元),存放的是字节型数据,就是20H(十进制的32);0地址
字单元,存放字型数据,就是4E20H。
任何两个地址连续的内存单元,N号单元和N+1号单元,也可以看成一个地址为N的字单元中的高位字节单元和低位字节单元。比如上图中1
字单元中,所代表的字是124EH,高位字节单元是
2内存单元——12H,低位字节单元是
1内存单元——4EH。
2、DS和[address]
CPU读取内存单元时,必须先读取到这个内存单元的地址。8086CPU中,内存地址是由段地址和偏移地址组成(第二章中有介绍),并且有一个DS寄存器,用来存放要访问的数据的段地址。地址很重要,就像生活当中,要前往一个地方,我们必须要知道这个地方的地址,才能够决定我们如何去这个地方。
8086CPU中,通常有一个DS寄存器,用来存放要访问的数据的段地址。
例子:读取10000H单元的内容,可以用这样的一段代码来实现:
mov bx,1000H
mov ds,bx
mov al,[0]
解释:上面的代码是将10000H(1000:0)中的数据读取到al中。这里为什么不直接mov 10000H到ds中呢?这是因为8086CPU的设 计问题,其实没有什么原因,工程师就是这样设计的,不必深究(感觉废话连篇,哈哈)。
mov al,[0] 这一段代码又是什么意思呢?首先我们要知道,mov指令,就目前而言我们所知道的功能有两种:第一种就是直接将数据放到寄存器中(mov ax,2);第二种,将一个寄存器中的内容传递到另一个寄存器当中(mov bx,ax)。除去这两种功能,mov指令还能够将一个内存单元中的内容送入一个寄存器。那么mov al,[0] 这句代码就好理解了,就是将内存单元中,偏移地址为0的内存单元中的内容放入到ax通用寄存器中的低八位al(8位)中。一个段地址对应一个偏移地址,上面的代码中,段地址为1000,对应的偏移地址为0。CPU是如何自己找到段地址对应的偏移地址的呢?CPU中有其自身的索引机制,感兴趣的同学可以自行去百度或者google一下。
综上所述,上面的代码就可以理解为cpu从内存单元10000H(1000:0)中读取到数据,然后把值传递给ax寄存器中的al。
3、如何将1000H送入到DS(段寄存器)中?
我们既然可以 "mov ax,1000" ,那么可以“mov ds,1000H”吗?
答案是NO。8086CPU不支持直接将数据放入段寄存器中,ds是一个段寄存器。至于为什么不能,这在上面也说了是设计上的问题 。所以CPU从内存中读取内容,就是这样一个过程:数据--->通用寄存器--->段寄存器。
4、如何将数据从寄存器送入到内存中呢?
比如要把al中的数据送入到内存单元10000H中,我们可以通过以下的代码完成:
mov bx,1000H
mov ds,bx
mov [0],al
这里就是一个逆向思维的体现。
5、字的传送
8086CPU是16位结构,有16根数据线,所以一次性可以传送16位的数据,也就是一次性传送1个字(1个字=2个字节=16位)。
如下代码:
mov bx,1000H
mov ds,bx
mov ax,[0] ---->这里是将1000:0处的字型数据(1个字,16位)送入到ax(16位)当中
mov [0],cx ---->这里是将cx(16位)中的数据放入到1000:0(16位)内存单元中。
6、已知mov指令的几种形式
mov 寄存器,数据 ---->mov ax,2C3B
mov 寄存器,寄存器 ---->mov bx,ax
mov 寄存器,内存单元 ---->mov ax,[0]
mov 内存单元,寄存器 ----->mov [0],bx
mov 段寄存器,寄存器 ---->mov ds,ax
既然内存单元和寄存器之间可以互相赋值,那么寄存器和段寄存器之间可以互相赋值吗?(也就是可以执行 mov 寄存器,段寄存器吗?)答案是YES。同学们可以自行debug一下。
这么多指令,没必要去背,多敲一敲就记住了。
这里能不能像mov指令一样,对段寄存器进行操作呢?(比如 add 寄存器,段寄存器)答案是no。这个也可以自行debug一下。我电脑上这样做,是会报错的:
7、数据段
前面一章说过,对于8086CPU,我们可以根据需要,将一组内存单元定义为一个段(可以是代码段,也可以是数据段)
我们可以将一组长度为N(N<=64)、地址连续、起始地址为16的倍数的内存单元用于存放数据的内存空间,这样我们就定义了一个数据段。比如我们使用内存单元123B0H--123B9H这样一个连续内存单元来存放数据,它的段地址就是:123BH,偏移地址是0H到9H之间,长度就是10个字节。
那么我们应该如何来读取数据段呢?将一段内存当成数据段,这是在编程时候的一种安排,在具体操作的时候,我们可以使用ds来存储数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
这里我们定义123B0H到123BAH为数据段,现在要累加数据段中的前3个单元中的数据,应该这样写代码:
mov ax,123BH ---->把段地址存入到ax中
mov ds,ax ---->通过ax这个中介,将段地址123BH转交给ds段寄存器中
mov al,0 ---->使用al来存放累加结果(为什么用al?因为问题中说的是单元,没有说前3个字,默认字节,所以用al(占8位))
mov al,[0] ---->将数据段第一个单元(偏移地址为0)加到al中
mov al,[1] ---->将数据段第二个单元(偏移地址为1)加到al中
mov al,[2] ---->将数据段第三个单元(偏移地址为2)加到al中
上面的问题假如修改为累加数据段中的前3个字型数据数据呢?代码应该如何修改?其实很简单,只需要将al修改为ax即可,字型数据占16位嘛,ax寄存器也就是16位的嘛,对应一下,替换一下就ok了。还有就是将偏移地址修改为[0]、[2]、[4],一个字占两个单元嘛。
8、小结
9、栈的概念
栈是具有特殊访问方式的存储空间,特殊性就在于其中的数据是后进先出(LIFO)。
两个基本操作:入栈(将一个新元素放到栈顶)和出栈(从栈顶取出一个元素)。
栈顶的元素总是最后入栈,需要出栈时,又最先被取出来。
现今的CPU中,都有栈的设计,8086CPU提供相关指令来以栈的方式访问内存,这意味着,我们在基于8086CPU编程的时候,可以将一段内存当做栈来使用。
入栈指令:push 出栈指令:pop
push ax:就是将寄存器ax中的数据存入栈中 pop ax:就是将数据从栈中取出,放到ax寄存器中。8086CPU对栈的操作,数据都是以字为单位的(因为是16位)。
示例代码:
mov ax,0123H
push ax
mov bx,2266H
push bx
mov cx,1122H
push cx
pop ax
pop bx
pop cx
10、两个问题
CPU怎么知道一段空间被当做栈来使用呢?执行push和pop指令时,如何知道哪个单元是栈顶呢?
首先我们回顾一下CPU是如何指导当前要执行的指令的位置。寄存器CS和IP中存放着当前要执行的指令的段地址和偏移地址。
在8086CPU中,有两个寄存器,一个是
段寄存器SS,用来存放栈顶的段地址;一个是
寄存器SP,存放栈顶的偏移地址。
任何时刻,SS:SP指向栈顶元素。
在push一个元素时,首先
SP会减2,然后再将ax中的数据送入到SS:SP指向的内存单元处,SS:SP指向新的栈顶。如下图所示:
此时,我们思考一个问题,当栈为空的时候,SS:SP又会指向哪里呢?SS:SP指向最高地址的下一个单元,在执行完第一个push命令后,SS:SP指向栈中的第一个元素。如下图所示:
pop的操作则是相反的,SS:SP就是加2。如下图所示:
注意,pop之后的数据并不是真的从栈中移除掉,pop之后的数据仍然存在于栈中,只不过SS:SP不再指向被pop的那个内存单元。栈中的数据只是在不断的被新的数据所覆盖。
11、栈顶越界的问题
如何保证入栈、出栈不越界呢?(从另外一个角度来看,就是黑客经常使用的攻击手段——内存溢出攻击)
8086CPU不保证我们的栈操作不越界,所以要我们自己来操心栈顶越界的问题。
12、push和pop指令可以在寄存器和内存之间传送数据
push和pop分别可以进行寄存器的数据入栈和出栈,也可以进行段寄存器的数据入栈和出栈,亦可以进行内存单元之间的数据传递。
目前我们十分清楚的是,push和pop指令和mov指令不同,CPU执行mov指令只需要一个步骤,执行push和pop需要两步:
执行push时,首先sp加2,后向ss:sp处传送;执行pop时,先读取ss:sp处的数据,后改变sp(sp-2)。push和pop等栈的操作指令,修改的只是sp,也就是说对于16位的CPU而言,栈顶的变化范围在0~FFFFH之间。
13、栈段
前面一章说过,对于8086CPU,我们可以根据需要,将一组内存单元定义为一个段(可以是代码段,也可以是数据段)
我们可以将一组长度为N(N<=64)、地址连续、起始地址为16的倍数的内存单元当作栈来使用,这样我们就定义了一个栈段。将一段内存当作栈段来使用,仅仅是我们编程时的一种设置,CPU并不会由于这种设置,就在执行push、pop等栈操作指令时,就自动将我们定义的栈段当作栈空间来访问。CPU只认识SS:SP指向的栈顶,不会知道栈空间是从哪里到哪里。
一个栈段最大可以设置为多少呢?答案是2^16,即64KB(对于16位的8086CPU而言)。
14、段的综述
要如何定义一个段,这个段用来干什么,完全是编程人员的想法,CPU只认识地址。