内存访问与栈
- 1 内存分段
- 2 DS和[address]
- 3 mov、add、sub指令形式
- 4 栈
- 4.1 入栈与出栈
- 4.2 SS与SP
- 4.3 空栈
- 4.4 栈顶超界
- 4.5 push、pop指令
- 5 小结
本文属于《 X86指令基础系列教程》之一,欢迎查看其它文章。
1 内存分段
在x86程序执行时,内存会被分段,每个段(segment)都有其特定的用途。以下是常见的几个段:
- 代码段(Code Segment):代码段,也被称为文本段,是存储程序代码的地方。这通常包括二进制指令,也就是CPU执行的机器代码。
- 数据段(Data Segment):数据段是用于存储程序中定义的数据的位置,包括变量、数组等。这些数据在编译时被确定并存储在可执行文件或DLL中。当程序运行时,这些数据被加载到内存中,并分配给对应的内存地址。
- 堆(Heap):堆是动态分配内存的地方,也就是说,当程序运行时,它可以在需要更多内存时向操作系统请求额外的内存,并在不再需要时将其释放。
- 栈(Stack):栈用于存储局部变量和函数调用的信息。每当函数被调用时,都会在堆栈上创建一个新的栈帧,包含函数的局部变量、参数和返回地址。当函数返回时,其对应的栈帧被从堆栈中弹出。
- 未初始化数据段(Uninitialized Data Segment):也被称为BSS段,在程序开始执行之前,操作系统会将此段的内容初始化为零或空指针。这个段用于存储全局变量和静态变量,这些变量在程序开始运行之前不需要明确赋值。
以上这些分段的方式是操作系统和编译器为了更好地组织和管理内存而采取的一种策略。
2 DS和[address]
- DS (Data Segment):数据段寄存器,保存了数据段地址。
- [address]:内存单元的偏移地址。
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中。
看看以下指令执行结果,与寄存器值变化,如下所示:
3 mov、add、sub指令形式
到现在,我们知道, 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
4 栈
4.1 入栈与出栈
栈有两个基本的操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。栈的这种操作规则被称为: LIFO(Last InFirst Out,后进先出)。
现今的 CPU中都有栈的设计, 8086CPU也不例外。8086CPU提供相关的指令来以栈的方式访问内存空间。这意味着,在基于8086CPU编程的时候,可以将一段内存当作栈来使用。
8086CPU提供入栈和出栈指令,最基本的两个是PUSH(入栈)和POP(出栈)。比如:
- push ax表示将寄存器ax中的数据送入栈中;
- pop ax表示从栈顶取出数据送入ax。
8086CPU的入栈和出栈操作都是以字为单位进行的。
查看以下,入栈出栈指令执行过程:
4.2 SS与SP
8086CPU中,有两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中。
任意时刻, SS:SP指向栈顶元素。push指令和pop指令执行时, CPU从SS和SP中得到栈顶的地址。
现在,我们可以完整地描述push和pop指令的功能了,例如push ax。
push ax 的执行,由以下两步完成。
- (1) SP=SP-2, SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
- (2)将ax中的内容送入SS:SP指向的内存单元处, SS:SP此时指向新栈顶。
下图,描述了8086CPU对push指令的执行过程。
从图中我们可以看出,8086CPU中,入栈时,栈顶从高地址向低地址方向增长。
4.3 空栈
栈为空,就相当于栈中唯一的元素出栈,出栈后,SP=SP+2, SP原来为000EH,加2后SP=10H,所以,当栈为空的时候, SS=1000H,SP=10H。
接下来,我们描述pop指令的功能,例如pop ax。
pop ax的执行过程和push ax刚好相反,由以下两步完成。
- (1)将SS:SP指向的内存单元处的数据送入ax中;
- (2) SP=SP+2, SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
4.4 栈顶超界
下图,描述了在执行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指令出栈,都将发生栈顶超界问题。
我们在编程的时候,要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
4.5 push、pop指令
push 和 pop 指令的格式可以是如下形式:
- push 寄存器:将一个寄存器中的数据,入栈;
- pop 寄存器:出栈,用一个寄存器接收出栈的数据;
- push 段寄存器:将一个段寄存器中的数据入栈;
- pop 段寄存器:出栈,用一个段寄存器接收出栈的数据;
push和pop也可以在内存单元和内存单元之间传送数据,我们可以:
- push 内存单元:将一个内存字单元处的字入栈(注意:栈操作都是以字为单位);
- pop 内存单元:出栈,用一个内存字单元接收出栈的数据。
比如:
mov ax,1000H
mov ds,ax ;内存单元的段地址要放在ds中
push [0] ;将 1000:0 处的字压入栈中
pop [2] ;出栈,出栈的数据送入1000:2处
5 小结
mov、add、sub和push、pop指令形式。
栈:
- (1)8086CPU提供了栈操作机制,方案如下。
在SS、SP 中存放栈顶的段地址和偏移地址;提供入栈和出栈指令,它们根据SS:SP指示的地址,按照栈的方式访问内存单元。 - (2)push指令的执行步骤: ①SP=SP-2;②向SS:SP 指向的字单元中送入数据。
- (3)pop指令的执行步骤:①从SS:SP指向的字单元中读取数据;②SP=SP+2。
- (4)任意时刻, SS:SP 指向栈顶元素。
- (5)8086CPU只记录栈顶,栈空间的大小我们要自己管理。
- (6)用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反。
- (7)push、pop实质上是一种内存传送指令,注意它们的灵活应用。
内存分段有数据段、代码段、堆、栈、未初始化数据段:
- 我们可以用一个段存放数据,将它定义为“数据段”;
- 我们可以用一个段存放代码,将它定义为“代码段”;
- 我们可以用一个段当作栈,将它定义为“栈段”。
我们可以这样安排,但若要让CPU按照我们的安排来访问这些段,就要:
- 对于数据段,将它的段地址放在DS中,用mov、add、sub 等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当作数据来访问;就是DS和[address]。
- 对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令;就是CS和IP。
- 对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU在需要进行栈操作的时候,比如执行push、pop指令等,就将我们定义的栈段当作栈空间来用;就是SS和SP。
可见,不管我们如何安排, CPU将内存中的某段内容当作代码,是因CS:IP指向了那里; CPU将某段内存当作栈,是因为SS:SP指向了那里。我们一定要清楚,什么是我们的安排,以及如何让CPU按我们的安排行事。要非常清楚CPU的工作机理,才能在控制CPU按照我们的安排运行的时候做到游刃有余。
参考文档:
- 《汇编语言 第3版》 王爽 清华大学出版社