在x86程序执行时,内存会被分段,每个段(segment)都有其特定的用途。以下是常见的几个段:
以上这些分段的方式是操作系统和编译器为了更好地组织和管理内存而采取的一种策略。
CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址,在8086PC中,内存地址由段地址和偏移地址组成。8086CPU中有一个DS寄存器,通常用来存放要访问数据的段地址。比如我们要读取10000H单元的内容,可以用如下的程序段进行。
mov bx,1000H
mov ds,bx
mov al, [0]
“[…]”表示一个内存单元, “[…]”中的0 表示内存单元的偏移地址。指令执行时,8086CPU 自动取 ds 中的数据为内存单元的段地址。
mov ds,1000H 这条指令是非法的。因此,先将1000H放入bx寄存器,再把bx寄存器放入ds,执行“mov al, [0]”时,从DSx16+0地址,也就是从10000H读取数据,放入al中。由于al为8位,因此从10000H读取的长度为1字节,放入al中。
看看以下指令执行结果,与寄存器值变化,如下所示:
到现在,我们知道, mov指令可以有以下几种形式。
mov 寄存器,数据 比如:mov ax,8
mov 寄存器,寄存器 比如: mov ax,bx
mov 寄存器,内存单元 比如:mov ax,[0]
mov 段寄存器,内存单元 比如:mov ds,[0]
mov 内存单元,寄存器 比如:mov [0],ax
mov 内存单元,段寄存器 比如:mov [0],cs
mov 段寄存器,寄存器 比如:mov ds,ax
mov 寄存器,段寄存器 比如:mov ax,ds
add和sub指令同mov一样,都有两个操作对象。它们也可以有以下几种形式。
add 寄存器,数据 比如:add ax,8
add 寄存器,寄存器 比如:add ax,bx
add 寄存器,内存单元 比如: add ax,[0]
add 内存单元,寄存器 比如: add [0],ax
sub 寄存器,数据 比如:sub ax,9
sub 寄存器,寄存器 比如:sub ax,bx
sub 寄存器,内存单元 比如: sub ax,[0]
sub 内存单元,寄存器 比如: sub [0],ax
栈有两个基本的操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。栈的这种操作规则被称为: LIFO(Last InFirst Out,后进先出)。
现今的 CPU中都有栈的设计, 8086CPU也不例外。8086CPU提供相关的指令来以栈的方式访问内存空间。这意味着,在基于8086CPU编程的时候,可以将一段内存当作栈来使用。
8086CPU提供入栈和出栈指令,最基本的两个是PUSH(入栈)和POP(出栈)。比如:
8086CPU的入栈和出栈操作都是以字为单位进行的。
查看以下,入栈出栈指令执行过程:
8086CPU中,有两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中。
任意时刻, SS:SP指向栈顶元素。push指令和pop指令执行时, CPU从SS和SP中得到栈顶的地址。
现在,我们可以完整地描述push和pop指令的功能了,例如push ax。
push ax 的执行,由以下两步完成。
下图,描述了8086CPU对push指令的执行过程。
从图中我们可以看出,8086CPU中,入栈时,栈顶从高地址向低地址方向增长。
栈为空,就相当于栈中唯一的元素出栈,出栈后,SP=SP+2, SP原来为000EH,加2后SP=10H,所以,当栈为空的时候, SS=1000H,SP=10H。
接下来,我们描述pop指令的功能,例如pop ax。
pop ax的执行过程和push ax刚好相反,由以下两步完成。
下图,描述了在执行push指令后,栈顶超出栈空间的情况。
将10010H-1001FH当作栈空间,该栈空间容量为16字节(8字),初始状态为空, SS=1000H,SP=0020H, SS:SP指向10020H;在执行8次push ax后,向栈中压入8个字,栈满, SS:SP指向10010H;再次执行push ax: sp-sp-2, SS:SP 指向 1000EH,栈顶超出了栈空间, ax 中的数据送入1000EH单元处,将栈空间外的数据覆盖。
出栈时,也是类似的情况。可以看到,当栈满的时候再使用push指令入栈,或栈空的时候再使用pop指令出栈,都将发生栈顶超界问题。
我们在编程的时候,要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
push 和 pop 指令的格式可以是如下形式:
push和pop也可以在内存单元和内存单元之间传送数据,我们可以:
比如:
mov ax,1000H
mov ds,ax ;内存单元的段地址要放在ds中
push [0] ;将 1000:0 处的字压入栈中
pop [2] ;出栈,出栈的数据送入1000:2处
mov、add、sub和push、pop指令形式。
栈:
内存分段有数据段、代码段、堆、栈、未初始化数据段:
我们可以这样安排,但若要让CPU按照我们的安排来访问这些段,就要:
可见,不管我们如何安排, CPU将内存中的某段内容当作代码,是因CS:IP指向了那里; CPU将某段内存当作栈,是因为SS:SP指向了那里。我们一定要清楚,什么是我们的安排,以及如何让CPU按我们的安排行事。要非常清楚CPU的工作机理,才能在控制CPU按照我们的安排运行的时候做到游刃有余。
参考文档: