我要投搞

标签云

收藏小站

爱尚经典语录、名言、句子、散文、日志、唯美图片

当前位置:2019跑狗图高清彩图 > 栈机制 >

return机制

归档日期:07-13       文本归类:栈机制      文章编辑:爱尚语录

  才被分配内存单元。子函数运行结束时,所有局部变量的内存单元会被系统释放。形参和函数内部的局部变量的生命期和作用域都是在函数内部(

  (2)当所传实参内容比较庞大时,传址只是复制了整个实参的地址过去,指针依据同一个地址访问实参变量。而传值就会将实参内容整个拷贝过去,形参会跟实参占一样大的内存,栈空间是有限的。当然了,在弱小的程序中,传址的这个优点不会被体现出来。

  在函数中,可以随意的返回一个局部变量。但如果返回一个局部变量的地址(指针),编译器就会给出警告(编译器也不可能那么完美能够彻底的检查出段错误)。在函数内部返局部指针这的确是一个危险的操作。鄙人的笔记先将用return返回值(指针为地址值)的机制搞清楚后再分析一下。

  Linux等的C语言中return返回值的机制为:将返回值存入eax寄存器中,然后系统再将eax中的值赋给变量(i)。

  堆:由程序员分配和释放。如在C/C++中程序员使用malloc/new分配堆空间,使用free/delete释放所申请的堆空间。特点:释放内存块顺序随意。

  栈:栈是由系统自动分配和回收的内存。如一个子函数被调用时,系统会将函数内的局部变量的内存单元分配到栈上,当函数执行完毕时系统自动释放所分配的栈地址单元。特点:释放栈内存顺序为后进先出。

  【1】当程序执行到第8行调用子函数child_fun,程序转到到child_fun子函数入口地址处。

  【2】程序进入child_fun子函数(即此子函数开始运行),执行到”return

  1;”时,系统将返回的1存入寄存器eax中,然后经}标志后函数运行完毕。若子函数中有形参和局部变量,则在函数开始运行时,系统自动为局部变量分配栈空间,待函数运行完毕时系统自动释放在栈中为局部变量分配的内存单元中的数据。

  【3】child_fun子函数执行完毕,函数返回到调用子函数的地方即第8行处继续执行,将保存在寄存器eax中的值即1赋给变量i。

  编译C语言源文件时可不为gcc添加加-O2优化参数,不然在汇编代码中会看不到子函数调用的call指令。

  当初学习RAM汇编指令的时候没有清晰的动过手,对于这段汇编代码也是只认识push、move之系列英语单词,但是不会可以学习一下:

  【1】由于不同的CPU的汇编格式不一样,故首先了解一下当前操作系统使用的什么汇编格式。比如windows下采用的Intel的汇编格式,linux采取的是AT&T汇编格式。

  【2】收索一下AT&T汇编指令,浏览一下。明白一些基本指令的含义和编写格式后,只抓这个汇编代码的关键部分进行跟踪:

  [1]在main函数中,调用子函数child_fun之前的汇编代码就不用看了,是依函数地址,初始化栈、代码段之类的含义。从13行的”call

  child_fun”开始,程序就从红线箭头标识的方向跳到子函数child_fun处开始执行。

  [2]27行前的代码就不用看了,也根据将子函数地址初始化栈之类的。请我对照C语言源代码,第27行的代码”move1 &1,

  %eax”的含义是将常数1装入寄存器eax中。常数1对应C语言源代码中return后面的常数1。28行出栈,29行从子函数child_fun处返回到14行处。

  [3]14行代码”movel %eax, -8(%ebp)”的含义是将寄存器eax的值载入”-8(%ebp)”所寻址之处,而且这个地址就是变了i的地址(

  12行及16行对应语句之间的内容是main函数中的内容,12行之前是初始化代码,16行后是恢复未初始状态的代码,可单独写程序验证)。并且main函数中的返回值也是保存在寄存器eax中的[见图2汇编代码

  在最开始的未明白return机制前可能还是要纳闷:在子函数中返回一个局部变量,等子函数运行结束时,此局部变量会被释放掉。当在子函数中返回一个指针时,等子函数运行结束时,此地址中的值会被释放掉。有点找不出其中被释放的差别。根据返回局部变量的经验,

  可以这么分析:在执行return语句时,首先将return后面的地址值返回存入到比如eax寄存器中,然后系统再将eax中的地址值给接收函数返回地址的指针变量。这看起来都没什么问题,但问题在于两个方面:

  [2]子函数运行结束后,一切有关于局部变量的内存都已经释放回收。那么在用这个地址来操作就很危险:根本没有这个地址或者是地址中没有内容[没有内容是对的

  有的程序就能够将局部变量的地址放回回来,甚至在编译时警告都没有。例如以下程序例子:

  这个令人吃惊的结果不禁让人怀疑自己最开始对栈内存释放的理解。这个例子最起码验证了在子函数执行完毕后,原存在栈中的内容是没有被释放掉的。那么栈由系统自动分配和回收到底是怎么个情况呢?再整个不能输出正确结果的例子。

  根据程序代码和执行结果可见正如标题那个样子:栈内存还在,只是栈内存中的值被释放掉了。它不在被程序所占用。

  因为在子函数执行完毕时毕竟还是将栈内存(即局部变量的地址)返回到了父函数中。但是内存中的值已经被释放掉了。但是为什么第一个值依旧没有被释放掉呢?是正确的呢?可能是首地址所以一直都会给其它程序留个好印象吧。

  所以最后的结论是:子函数中的局部地址是能够被return到父函数中去的。只是在父函数中用这个地址去访问内容时,此地址中的内容已经被系统清除掉。这是很危险的操作:在父函数中用此地址访问其内容时,有可能刚被释放掉的这块栈内存又被系统分配另外的局部变量了,而此时你所访问的结果只是会导致程序结果不正确而已;但如果此地址中的内容还是不定状态,访问得到的值跟Figure

  栈的分配和释放可以这样子理解:栈内存块在计算机中不可能会移动,它的地址已经被固定。系统分不分配它,它就在那里。当为局部变量分配栈内存时,系统就将局部变量存入到栈的某个内存块中;当子函数运行结束局部变量应当被释放时,系统再将这些存入局部变量的栈内存中的数据清除掉,恢复原来没有被初始化的状态。

  不管是返回指针还是返回值,return将return之后的值存到eax寄存器中,回到父函数再将返回的值赋给变量。

  在函数内返回一个指针会出错的原因:子函数运行完毕时,存局部变量的所有栈地址的内容已经被释放。若在父函数中再访问这些地址中的内容时,因为这些地址的内容已经被释放,所访问到的值可能是乱的、不定的。

  分配内存,就是将某变量存入到某块内存中的一个地址中;释放内存,就是将此内存中的内容清除掉,恢复内存未被初始化的状态。

  start是一个ELF格式32位小端(Little Endian)的可执行文件,动态链接并且符号表没有去除。这正是Unix/Linux平台典型的可执行文件格式。

  注:这里得到的汇编语言语法格式与Intel的手册有很大不同,Unix/Linux采用AT&T汇编格式作为汇编语言的语法格式,如果想了解AT&T汇编可以参考文章 Linux 汇编语言开发指南.

  在C语言的层面来看,main函数是一个程序的起始入口点,而实际上,ELF可执行文件的入口点并不是main而是_start。

  实际上IA32并没有规定用哪个寄存器来保存返回值。但是,如果反汇编Solaris/Linux的二进制文件,就会发现,都用EAX保存函数返回值。

  1)IA32的栈是用来存放临时数据,而且是LIFO,即后进先出的。栈的增长方向是从高地址向低地址增长,按字节为单位编址。

  2) EBP是栈基址的指针,永远指向栈底(高地址),ESP是栈指针,永远指向栈顶(低地址)。

  3) PUSH一个long型数据时,以字节为单位将数据压入栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4的地址单元。

  5) CALL指令用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复执行下条指令。

  6) RET指令用来从一个函数或过程返回,之前CALL保存的下条指令地址会从栈内弹出到EIP寄存器中,程序转到CALL之前下条指令处执行。

  函数被调用时,返回时的EIP首先被压入堆栈;创建栈框架时,上级函数栈的EBP被压入堆栈,与EIP一道行成新函数栈框架的边界。

  栈帧建立后,EBP指向的栈的内容就是上一级函数栈的EBP,可以想象,通过EBP就可以把层层调用函数的栈都回朔遍历一遍,调试器就是利用这个特性实现backtrace功能的。

  栈分配空间给函数局部变量时的语句通常就是给ESP减去一个常数值,例如,分配一个整型数据就是 ESP-4。

  由于栈框架指针永远指向当前函数的栈基地址,参数和局部变量访问通常为如下形式:

  假如函数A调用函数B,函数B调用函数C ,则函数栈帧及调用关系如下图所示:(重点注意形参与局部变量的各自位置)

  表面来看,这条语句最直接的后果是使ESP的地址后4位为0,即16字节对齐,那么为什么这么做呢?

  原来,IA32 系列CPU的一些指令分别在4、8、16字节对齐时会有更加的运行速度,因此gcc编译器为提高生成代码在IA32上的运行速度,默认对产生的代码进行16字节对齐.

  andl $0xf0,%esp的意义很明显,那么subl$8,%esp呢,是必须的吗?这里假设在进入main函数之前,栈是16字节对齐的,那么,进入main函数后,EIP被压入堆栈后,栈地址最末4位必定是0100,esp-8则恰好使后4位地址为0。看来,这也是为保证栈16字节对齐的。

  默认情况下,n是等于4的,也就是说,默认情况下,gcc是16字节对齐,以适应IA32大多数指令的要求。

  可以看到,栈对齐指令没有了,因为,IA32的栈本身就是4字节对齐的,不需要用额外指令进行对齐。

  由于STP在调试器backtrace的指令中被使用到,因此没有STP该调试指令就无法使用。

  函数参数和局部变量的访问,在没有ebp的情况下,都只能通过+xx(esp)的方式访问,而很难区分两种方式,降低了程序的可读性。

  Calling Convention调用约定对以上问题作出了规定。CallingConvention也是ABI的一部分。因此,遵守相同ABI规范的操作系统,使其相互间实现二进制代码的互操作成为了可能。

  今后,将通过进一步的实验,来深入了解这些概念。通过掌握这些概念,使在汇编级调试程序产生的core dump、掌握C语言高级调试技巧成为了可能。

本文链接:http://apawoodbury.com/zhanjizhi/250.html