汇编 堆栈 变量存储 指针

本文章系作者原创,未经许可,不得转载。

汇编 堆栈 变量存储 指针

简单来说的话,可以把计算机理解成由

CPU,内存,硬盘组成,

而CPU内部又包括一种叫做内部寄存器的东西,包括
数据寄存器: AX,BX,CX,DX;
段寄存器: CS,DS,ES,SS;
指针与变址寄存器SP,BP,SI,DI;
指令指针与标志寄存器IP,FLAGS。

CPU与内存(存储器)通过总线来连接
这里写图片描述

内存通常会被分配为:代码段,数据段,堆栈段,附加段。

那么这几个段是如何划分的呢?答案就是通过CPU内部的段寄存器来划分。:

“”“此处放置图片”
我们在CS寄存器中存储一个地址,该地址就是内存中代码段的起始地址,同样的我们在SS寄存器中存储一个地址,改地址就是内存中堆栈段的起始地址。

也就是说不管是代码,还是堆栈,或者是数据段都是在内存中存储。

那么为何堆栈段如此特殊能,原因是在CPU内部的堆栈指针寄存器DP,SP

其中DP是基址,一般不变动,SP是变址,随着PUSH以及POP指令而变化:
“此处当一张图片,显示PUSH前后SP的变化以及其所指向的内存的变化”

由上图可以看出,堆栈段的特殊是源于DP,SP寄存器的特殊。那么为什么要创造这种堆栈类型的空间呢?我们来看一段汇编代码,反应了在调用子函数是堆栈可以做什么:

“”“此处放一段代码,用来反应子函数调用时都PUSH跟POP了什么”

注意这时候的堆栈跟我们在高级语言中所说的堆栈还不一样,因为此处的汇编代码是直接在硬件上运行的底层代码,其可以之间处理CPU内部的寄存器,而我们编写的高级语言大都运行在操作系统之上,肯定是接触不到这些内部寄存器的,那么我们所理解的堆栈是什么,其又有什么作用呢?跟栈空间与堆空间的关系又如何?我们理解如下:

首先,我们在学习高级语言时理解的栈,其PUSH,POP原理跟上面的堆栈一样,但是作用是完全不一样的。我们来看看平时所说的变量存储于栈空间是什么意思。

先上一段代码,看看变量定义时发生了什么:

c语言:

 #include <stdio.h>
 void def(void)
 {
     int a=3;
 }
  void test(void)
 {
     int a=3;
 }
 int main(void)
 {
     int a=1;
     int b=2;
     int c=6;
     int d=8;
     int e=10;
     def();
     test();

 }

汇编:

.file   "firstc.c"
    .text
    .globl  _def
    .def    _def;   .scl    2;  .type   32; .endef
_def:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $16, %esp
    movl    $3, -4(%ebp)
    leave
    ret
    .globl  _test
    .def    _test;  .scl    2;  .type   32; .endef
_test:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $16, %esp
    movl    $3, -4(%ebp)
    leave
    ret
    .def    ___main;    .scl    2;  .type   32; .endef
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $32, %esp
    call    ___main
    movl    $1, 28(%esp)
    movl    $2, 24(%esp)
    movl    $6, 20(%esp)
    movl    $8, 16(%esp)
    movl    $10, 12(%esp)
    call    _def
    call    _test
    leave
    ret

esp,ebp 都是寄存器,存储的是栈空间目前的栈顶地址,与栈底地址,两个地址地址都指向内存空间(数据段),含义是目前该函数里所用的变量就存储于数据段这两个地址所指向的位置之间。这一块有点拗口,需要仔细的理解。可以使用下图帮助理解
这里写图片描述

上面的汇编代码,我们从movl %esp, %ebp开始解析,前面的pushl %ebp是将上一个函数的ebp寄存器入栈,这么做的作用可以在本例调用子函数的时候进行解析。 movl %esp, %ebp的意思是将esp中的数传入ebp,我们假设该数值为0XFF,那么这步以后,两个寄存器中数值相等,都是0XFF。然后执行代码
subl $32, %esp 这个操作将esp中的数值减少32,然后执行
movl $1, 28(%esp)
movl $2, 24(%esp)
movl $6, 20(%esp)
movl $8, 16(%esp)
movl $10, 12(%esp)

这一串操做就是我们C语言中的变量赋值,我们发现,我们定义的a,b,c,d,e五个变量存储的区域都在ebp与esp所指向的地址之间,a的存储位置是ebp至28+esp(也就是32-28)之间的4个字节,b的存储位置是24-28中间的4个字节,以此类推,我们发现我们在main()函数中定义的变量都被约束在ebp与esp之间。

好,接着往下走,这时候我们调用了子函数def(),首先执行的代码段就是:
pushl %ebp
movl %esp, %ebp
subl $16, %esp

此时我们分析PUSH ebp的作用,这个操作是什么结果呢?就是将ebp中的数据压入栈段,这个操作执行完成之后会变成这样:
这里写图片描述
也就是将主函数的变量存储空间的基地址压入栈段保护了起来,后执行操作
movl %esp, %ebp
subl $16, %esp

这个操作首先将esp中的数据给了ebp,然后将esp中的数据减去了16,执行完成后是这个样子:
这里写图片描述
相信大家已经看出来了,这个操作的目的就是给def开辟一段空间来存储def中的变量,这样做可以防止main中的变量收到影响。
在def函数结尾会发生什么呢?我们注意到执行了一个leave指令,这个指令等价于movl %ebp, %esp
pop %ebp

这个指令是将ebp中的数值传给esp,然后从将栈顶的数取出来赋值给ebp。
执行后是这个样子:
这里写图片描述
这个时候我们看到,ebp跟esp中的值又回到了调用函数之前的样子,这就意味着def函数中定义的空间被释放了,然后在执行调用test函数后,会变成这个样子:
这里写图片描述

此时test函数使用的变量空间已经将def函数空间给覆盖掉了,这也就是局部变量作用域在函数内部的原因。
至此,变量存储过程分析完毕,下面总结一下:

  • 1.计算机可以简单理解为CPU与内存两部分,二者通过总线连接
  • 2.栈段,数据段,代码段都存储于内存中,但是CPU可以通过其内部的寄存器赋予不同的内存空间不同的功能。
  • 3.我们在高级语言中定义变量时,其存储范围在编译的时候通过esp减去某值(编译器计算)来确定,在调用函数的过程中可以通过将ebp压栈来保存上一函数的变量其实地址,然后将esp中的值赋予ebp,来保障不会影响上一函数的变量空间,同时,再函数退出时,通过将ebp的值赋予esp以及将栈顶值pop到ebp来释放空间,同时确定上一函数的变量存储空间。
  • 4.我们在高级语言学习中常说的栈空间,堆空间,按照上面的论述来看,其实都是内存空间,并且都存在于内存空间里面的数据段,只不过栈空间存储的变量会随着函数的执行而创建与释放,堆空间不会,因为堆空间不受栈段以及esp,ebp的控制,可以通过malloc函数来创建,但是需要程序员编写代码来释放(有可能操作系统会帮忙释放,但不一定)。
  • 5.我们在学习C#中常说的值类型变量应该指的就是存在栈空间里的变量,引用类型变量应该就是通过malloc函数创建的值位于堆空间,但是地址位于栈空间的变量。
  • 6.Python中的变量类型随着其指向的内存空间的数据类型变化而变化,与上面的论述过程不一样,其原理需要进一步分析。

猜你喜欢

转载自blog.csdn.net/whynat/article/details/82595791