其他分享
首页 > 其他分享> > 8086Assembly02——数据段、栈段

8086Assembly02——数据段、栈段

作者:互联网

镇楼图

Pixiv:初音ミクの休日

(我永远喜欢黄豆粉)



〇、数据与内存单元

我们知道CS、IP可以分别表示高地址的内存单元和低地址的内存单元。

高地址内存单元的值和低地址内存单元的值分别存储在AH、AL这一类的寄存器中。

我们把这种只占1个字节也就是8位的内存单元内的值称为字节型数据。

高地址的内存单元称为高位字节

占2个字节也就是16位的内存单元称为字单元,其存放的数据称为字型数据

如果内存地址单元的起始地址为N。我们则称这个字单元为N地址字单元。

比如

地址
0 20H
1 4EH
2 12H
3 00H

1地址字单元的字节型数据为4EH,2地址字单元的字型数据为0012H(其中00H为高位字节的数据,12H为低位字节的数据)

这些规定使得我们看数据时不再局限于只看寄存器


一、DS寄存器

在之前我们尝试通过修改CS、IP和jmp来访问不同内存的指令,现在我们想访问不同内存的数据该怎么做?

DS寄存器用来一块内存的段地址,通过DS我们可以很容易地访问不同内存的数据。

现在我们想获取内存1234:5678内的数据7B该怎么做?

DS既然是用来存储段地址1234的,我们需要先把1234给DS,然后再通过偏移地址5678来获取数据

但我们无法这样赋值给DS寄存器,我们需要这样做

mov bx,1234
mov ds,bx

这样我们就成功给DS赋值了,但现在还差偏移地址

我们需要学习一个新的语法

mov ax,[5678]

[]里面的数据代表了偏移地址,而这个操作就代表将DS:[address]这一块内存的数据转移给ax。

现在我们可以通过DS来指定数据的段地址,[]来指定数据的偏移地址,通过这样的操作我们就可以随意访问任意内存的数据了。

至于为什么无法直接用mov ds,1234这样的操作,你不需要了解,这属于硬件设计的问题。

例子:如何执行1234:5678的指令,如何获取2345:6789的数据?

a 073F:0000

mov ax,2345
mov ds,ax
mov ax,[6789]
/*设置DS来获取指定内存数据*/
/*我们也可以用mov [6789],6789修改指定内存的数据*/
jmp 1234:5678
/*设置CS、IP来跳转至指定内存指令*/

这种获取指定内存数据的操作也称为“字的传送”

例子:如何修改10000H地址的数据?

mov ax,1000
mov ds,ax
mov ax,2c34
mov [0],ax

结果:确实将ax的数据写进1000:0000地址字单元里了。

(34 2C)

注:我们编写代码时是从高位到低位,显示的时候却是从低位到高位

例子:问0000:0010H是不是DS=1时mov ax,[0]所要索引的内存地址?


二、为什么要有数据段、代码段?(怎样布局内存)

我们可以用CS:IP来访问某一内存的指令,也可以用DS来访问某一内存的数据

为了方便管理,我们将连续的一块内存专门用来存放指令(毕竟指令执行完后CS:IP会自动增加),用另一块内存专门用来存放数据(通过add指令同样可以实现数据自动索引的操作)

专门存储数据的一块连续内存称为数据段

专门存储指令的一块连续内存称为代码段

至于这一块内存怎么设置,完全取决于你。

像这样设置内存区域的操作我们叫作布局内存

只有低级语言才需要你这样从0开始布局内存,像C语言它已经布局好了。

我们知道C语言从低地址到高地址的内存分为代码段、数据段、堆、栈等其他内存区域。这是帮你设计好的,你不再为了设计程序而需要先布局一下内存。


三、栈机制的实现

正常来说,一块内存地址的访问是从低地址到高地址。(比如CS:IP的地址是逐渐增大的)

但栈是具有特殊访问方式的一块内存空间,在某些情况下我们需要这样做。

栈的访问规则是后进先出LIFO (Last In First Out)

栈空间的内存分布(举例)
1000FH (栈空间的地址是从高到低的)
1000EH
1000DH
1000CH
... (栈空间的有数据的位置最低的数据称为栈顶)

栈空间遵循LIFO规则,先访问栈顶,最先写入的数据反而是最后访问

8086CPU提供了实现栈机制的一些汇编指令

入栈、出栈操作

我们把将数据写入栈空间的操作称为入栈,我们用push指令来将数据写入到栈空间里。

把栈空间的数据拿出的操作称为出栈,我们用pop指令来获取栈空间的数据。

这里我用e指令修改了2000:000x的数据,如何将这一数据段的数据保存至栈空间里再拿出来?

e 2000:0
00 01 03 06 0A 0F 15 1C 24 2D 3D 4E 60 73 88 9E 12
a
mov ax,2000
mov ds,ax
mov ax,[0]
push ax
mov ax,[1]
push ax
mov ax,[2]
push ax
mov ax,[3]
push ax
mov ax,[4]
push ax
mov ax,[5]
push ax
mov ax,[6]
push ax
mov ax,[7]
push ax
mov ax,[8]
push ax
mov ax,[9]
push ax
mov ax,[A]
push ax
mov ax,[B]
push ax
mov ax,[C]
push ax
mov ax,[D]
push ax
mov ax,[E]
push ax
mov ax,[F]
push ax
pop bx
pop bx
pop bx
pop bx
pop bx
pop bx
pop bx
pop bx
pop bx
pop bx
pop bx
pop bx
pop bx
pop bx
pop bx
pop bx

从最后执行的结果来看,push进去的数据确实pop出来了,而且也确实遵循了LIFO。

栈空间在哪?

我们知道数据段可以用DS寄存器找到位置,代码段可以用CS、IP找到位置,我们自然会想栈空间是不是也是如此。

事实上也是如此,栈空间也提供了两个寄存器,一个是SS,一个是SP

SS寄存器用来存储栈顶的段地址,SP寄存器用来存储栈顶的偏移地址

在我将所有数据输入至栈空间后得到了这个结果,首个ax应该是0001H,我在073F:00FBH处(根据寄存器SS、SP的值)找到栈空间的首个元素

PUSH指令具体过程

push指令后可以跟一块内存或寄存器,可以将指定的内存里的数据push进栈空间里。

其具体过程有二

①先移动SS:SP(SP -= 2),因为我们一旦push数据,栈顶就会被改变,在改变栈顶的数据之前,我们要先改变指向栈顶的内存地址

②将数据写入至SS:SP指向的内存

从内存地址的角度来看,我们可以发现push会将SS:SP不断减小,让栈顶的内存地址趋向于低地址

POP指令具体过程

pop指令后可以跟一块内存或寄存器,可以栈顶的数据pop至指定的内存里。

其具体过程有二

①将SS:SP内的数据写入至指定的内存里

②移动SS:SP(SP += 2),因为我们一旦pop数据后,就意味着这个栈顶里的数据被拿掉了,需要一个新栈顶

(实际上,pop出的数据在栈空间那边依然存在)

从内存地址的角度来看,我们可以发现push会将SS:SP不断减小,让栈顶的内存地址趋向于高地址

栈空状态

当我们push了3次后就意味着栈空间里有3个字的数据,当我们再pop三次后就意味着没数据了,这时候我们称栈空间处于“栈空状态”。

不过栈空状态是我们人为定义的,栈空状态更准确的定义是栈空间在逻辑上不存在数据了。(因为我们知道即使pop也不会消除数据)

\[一个判断方法就是: \\ |count_{push}-count_{pop}|=0则认为处于栈空状态 \\ count_{push}为执行push的次数 \\ count_{pop}为执行pop的次数 \]

\[或者有另外一个方法就是: \\ SS:SP=栈空间初始地址 \]

栈机制的本质是什么?

上图我设置了栈顶的位置,但我还没push就直接pop,而且可行。

(由于这一块是显存区,所以我执行pop ax后是直接显示一些奇怪东西)

但这确实说明了即使是在栈空状态下我依然可以pop操作,那么什么是栈?

首先我们要明确实现栈机制的只有3种基本要素:栈顶地址SS:SP、push操作、pop操作。

■栈顶地址是用来确定你要实现栈机制的内存在哪

■push操作是用来从高地址向低地址存储数据的

■pop操作则是用来从低地址向高地址拿数据的

因此我们可以得出结论:栈实际上是为了实现一种操作固定且简洁的访问内存模式

只要使用栈这种机制,我们就只能只能向下写入,向上读取。

那栈空间、栈空状态是要做什么,明明不需要这些概念也可以?

在我们实现程序就底层而言,我们还需要进行一步内存布局,在内存物理上不存在所谓的代码段、数据段、栈空间这些概念,但我们引入这些概念是为了更好的设计程序。

如果同样功能的内存这一块那一块就实在是太乱了,因此把同样功能的内存集中起来方便我们管理、设计。

栈空间也是如此,我们自己在逻辑上引入了栈空间这个概念,栈空概念也是基于此逻辑引入的。(在物理上不存在所谓的栈空)

我们把确定好的首个栈顶地址在逻辑上定为这个栈空间的起始位置

在逻辑上设定好的另外一个地址作为这个栈空间的结束位置

从起始位置到结束位置的内存区域称为栈空间

我们常常用这块栈空间能容纳多少字作为栈空间的大小

比如

我把7000:0010H作为栈空间的起始位置
我把7000:0000H作为栈空间的结束位置
则我定义的栈空间为7000:0010~7000:0000H

这一块栈空间能容纳10H大小的内存空间,换算成字则是8Word(字)
这也意味着我能在栈空间存储8个数据
    
如果SS:SP=7000:0010H那么就处于栈空状态
在栈空状态下我尝试pop,SS:SP会跑到7000:0012H
这时候超出了我们预设的范围,可能这一块内存区域是我们另外设定的数据段
这样就不太好了
因此我们引入栈空状态这个术语来避免我们pop出原本设定的内存区域

栈满状态

如果我们因为pop会在栈空状态跑到栈空间外,那么栈满状态则是我们会因为push跑到栈空间外的一个警戒线。

还是上述例子,在SS:SP=7000:0000H时我还尝试着用push会怎么样?

SS:SP会跑到7000:FFFE,这种更具有致命性,因为push操作会修改7000:FFFEH内存的数据,这并不属于栈空间,假如这是代码段的数据,那么我们相当于修改了指令,而且这种操作是不可逆的。

栈空状态的溢出我们还可以设置SP来挽回,而一旦发生栈满问题的溢出则是无法挽回的。

栈空状态、栈满状态下的溢出问题

某种内存布局 地址
代码段(栈段要想溢出到这,则在栈满状态下使用push) 7000:0010~7000:001FH
栈段 7000:0020~7000:0030H
数据段(栈段要想溢出到这,则在栈空状态下使用pop) 7000:0031~7000:0040H

注:栈空间也成为栈段

由于补码的关系,即使SP±2也不会导致SS进位或借位,所以一个栈段最大只能设置到64KB,也就是最多存储32KB字。

是如何设定栈空间的?

为了方便,我们把SP=0000作为一个栈空间的结束位置,另外设定一个SP作为初始位置

mov ax,0010
mov sp,ax
/*我们设置了sp=0010
因为当sp=0000时处于了栈满状态
如果我们再去push
sp就会变成00FE
这样子我们就能很容易地判断sp是否是栈满状态
因为我们栈满状态是我们第一警惕的

栈段有什么作用?

①栈段也是一个内存空间,理所当然地可以用来存储数据

②栈段具有特殊的机制LIFO,因此可以作为一个交互数据的功能

例如

mov ax,2000
mov bx,3000
push ax
push bx
pop ax
pop bx
/*这是交换数据功能

一个疑问

mov ax,3000
mov ss,ax

/*在我未执行指令之前使用d指令查看3000:0开始的数据
将ss移动到3000H时,我用d指令查看数据
为什么会改变?

这个问题得要等我们学到很后面很后面才能知道,现在就不用考虑了


总结

数据段如何实现存储数据

什么是栈

如何布局内存空间,如何防止溢出


参考书籍

《汇编语言 第四版》——王爽

参考网站

https://fishc.com.cn/forum-39-1.html

https://b23.tv/qUfphX

标签:8086Assembly02,mov,pop,栈段,内存,push,ax,数据
来源: https://www.cnblogs.com/AlienfronNova/p/14766390.html