函数调用堆栈过程中栈到底是怎么压入和弹出的

1 问题描述
  在此之前,我对C中函数调用过程中栈的变化,仅限于了解有好几种参数的入栈顺序,其中的按照形参逆序入栈是比较常见的,也仅限于了解到这个程度,但到底在一个函数A里面,调用另一个函数B的过程中,函数A的栈是怎么变化的,实参是怎么传给函数B的,函数B又是怎么给函数A返回值的,这些问题都不能很明白的一步一步解释出来。下面,便是用一个小例子来解释这个过程,主要回答的问题是如下几个:
  1、函数A在执行到调用函数B的语句之前,栈的结构是什么样子?
  2、函数A执行调用函数B这一条语句的过程中,A的栈是怎样的?
  3、在执行调用函数B语句时,实参是调用函数A来传入栈,还是被调函数B来进行入栈?
  4、实参的入栈顺序是怎样的?
  5、执行调用函数B的过程中,函数A的栈又是怎样的,B的呢?
  6、函数B执行完之后,发生了什么事情,怎样把结果传给了函数A中的调用语句处的参数(比如:A中int c = B_fun(...)这样的语句)?
  7、调用函数的语句结束后,怎样继续执行A中之后的语句?
  大概的问题也就这些,其实也就是整个过程中一些自己认为比较重要的步骤。接下来详细描述这个过程,以下先给出自己的C测试代码,和对应的反汇编代码。
2 测试代码
  2.1&C测试代码
  C测试代码如下:(代码中自己关注的几个地方是L14 15 16 17)
2 fun(int *x, int *y)
int temp = *x;
return *x + *y;
12 main(void)
int a = 5;
int b = 9;
int c = 3;
c = fun(&a, &b);
  主要关注的地方是:
  1、main中定义int变量 a b c 时,是怎样的定义顺序?
  2、L17 的过程。
  3、进入fun之后,的整个栈的结构。
  2.2 汇编测试代码
$0x10,%esp
0x8(%ebp),%eax
(%eax),%eax
%eax,-0x4(%ebp)
0xc(%ebp),%eax
(%eax),%edx
0x8(%ebp),%eax
%edx,(%eax)
0xc(%ebp),%eax
-0x4(%ebp),%edx
%edx,(%eax)
0x8(%ebp),%eax
(%eax),%edx
0xc(%ebp),%eax
(%eax),%eax
$0x18,%esp
c7 45 f4 05 00 00 00
$0x5,-0xc(%ebp)
c7 45 f8 09 00 00 00
$0x9,-0x8(%ebp)
c7 45 fc 03 00 00 00
$0x3,-0x4(%ebp)
-0x8(%ebp),%eax
89 44 24 04
%eax,0x4(%esp)
-0xc(%ebp),%eax
%eax,(%esp)
e8 a5 ff ff ff
80483b4 &fun&
%eax,-0x4(%ebp)
c7 45 f4 07 00 00 00
$0x7,-0xc(%ebp)
c7 45 f8 11 00 00 00
$0x11,-0x8(%ebp)
b8 00 00 00 00
3 分析过程
  3.1 main栈
  1、L24 执行push %ebp:main函数先保存之前函数(在执行到main之前的初始化函数,具体的细节可以参考程序员的自我修养这本书有讲整个程序执行的流程)的帧指针%ebp。此时,即进入了main函数的栈,图标描述如下
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
  2、L25 执行mov %esp,%ebp:步骤1已经保存了之前函数的%ebp,接下来需要修改函数main的栈帧指针,指示main栈的开始,即修改%ebp,使其内容为寄存器%esp的内容(C描述为:%ebp = %esp),此时栈结构如下:
main:%esp(%ebp)
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
  3、L26 执行sub $0x18,%esp:此处即修改main函数栈的大小。由于linux里,栈增长的方向是从大到小,所以这里是%esp = %esp - $0x18;关于为什么减去$0x18,即十进制的24,深入理解计算机系统一书P154这样描述:&GCC坚持一个x86编程指导方针,也就是一个函数使用的所有栈空间必须是16字节的整数倍。包括保存%ebp值的4个字节和返回值的4个字节,采用这个规则是为了保证访问数据的严格对齐。&,所以这里main函数栈的大小 = 24 + 4 + 4 = 32(分配的24,保存%ebp的4,保存返回值的4)。此时栈结构如下:
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
  4、 L27 movl $0x5,-0xc(%ebp);L28 movl $0x9,-0x8(%ebp);L29 movl $0x3,-0x4(%ebp)这三行是定义的变量a b c。此时栈结构如下,可以看出来,变量的定义顺序不是按照在main里面声明的顺序定义的,这个我不是很懂,求指导。
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
%ebp - 0x4
%ebp - 0x8
%ebp - 0xc
&  5、L30 lea -0x8(%ebp),%eax; L31 mov %eax,0x4(%esp)这两行是把变量b的地址赋值到%esp + 4,栈结构如下:
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
%ebp - 0x4
%ebp - 0x8
%ebp - 0xc
%esp + 0x4
变量b的地址
  6、L32 lea -0xc(%ebp),%eax; L33 mov%eax,(%esp)这两行是把变量a的地址赋值到%esp,栈结构如下:
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
%ebp - 0x4
%ebp - 0x8
%ebp - 0xc
%esp + 0x4
变量b的地址
变量a的地址
  7、L34 call 80483b4 &fun&;可以看出这一行,即调用的是fun(int *, int *)函数,而且也从第6步知道实参是调用函数传入栈,且是逆序传入。这里call指令会把之后指令的地址压入栈,即L35的指令地址804840f。(从汇编代码看不出来这一步压栈的过程,但根据后续分析,这样是正确的,书上也是这么描述call指令的,怎样能直观的看到栈的变化,我不懂,哪位知道可以留言告诉我)此时栈的结构如下:
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
%ebp - 0x4
%ebp - 0x8
%ebp - 0xc
变量b的地址
变量a的地址
  到这一步,关于main函数栈的情况分析就到这里,接下来进入fun函数进行分析。
  3.2 fun函数栈
  1、L2 push%ebp:同main函数第一步一样,先保存之前函数的栈帧,即保存main函数的帧指针%ebp,此时栈情况如下:
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
%ebp - 0x4
%ebp - 0x8
%ebp - 0xc
变量b的地址
变量a的地址
被保存的main函数的%ebp
  2、L3 mov %esp,%ebp:同上述main描述里面步骤2,修改寄存器%ebp。栈如下:
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
变量b的地址
变量a的地址
fun栈开始(%esp与%ebp)
被保存的main函数的%ebp
  3、L4 sub $0x10,%esp:同上述main描述步骤3,修改函数fun的栈大小,(不明白的是这里怎么修改的大小为十进制16,这样加上其他的最后不是16的整数倍?)此时栈如下:
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
变量b的地址
变量a的地址
fun栈开始(%ebp)
被保存的main函数的%ebp
  4、L5 mov 0x8(%ebp),%eax;L6& mov (%eax),%eax
;L7 mov%eax,-0x4(%ebp):这三行功能分别是把%eax = &a; %eax = %ebp - 0x4 =对应的是fun函数语句int temp = *a;其中,L7会改变栈的情况,此时栈如下:
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
变量b的地址
变量a的地址
被保存的main函数的%ebp
&%ebp - 0x4
  5、L8 mov 0xc(%ebp),%eax;L9 mov (%eax),%edx;L10 mov 0x8(%ebp),%eax; L11 mov %edx,(%eax)对应功能分别是:get &b; get &a; a = b。其中,只有L11会修改栈内容,栈内容如下:
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
a = 9(修改了a的值)
变量b的地址
变量a的地址
被保存的main函数的%ebp
%ebp - 0x4
  6、L12 mov 0xc(%ebp),%eax; L13 mov-0x4(%ebp),%edx;L14 mov %edx, (%eax):功能分别对应get &b; %edx =b = a。其中L13会修改栈内容,具体栈情况更改如下:
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
b = 5(修改了b的值)
a = 9(修改了a的值)
变量b的地址
变量a的地址
被保存的main函数的%ebp
%ebp - 0x4
  7、然后就是L15,L16,L17,L18这4行分别得到&a, a, &b, b。这些都不会造成栈内容的变化。
  L19 add %edx, %eax会计算出a + b的值,并把结果保存在寄存器%eax,也即返回值在%eax(这里大家都清楚,函数如果有返回值,一般都是保存在%eax)
  8、L10 leave:深入理解计算机系统一书P151这样描述leave指令:
    movl %ebp, %esp
    popl %ebp
    以下分两步来描述:
      即先把寄存器%ebp赋值给%esp,其中%ebp保存的是之前main函数的%ebp,这一步修改了%esp的内容,即栈情况会发生变化。这一步之后栈情况为:
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
变量b的地址
变量a的地址
被保存的main函数的%ebp
      然后是popl %ebp,即把%ebp的内容恢复为之前main函数的帧指针,经过这一步之后%ebp指向了main栈的开始处:如下表示
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
变量b的地址
变量a的地址
%esp(%ebp)
被保存的main函数的%ebp
  9、L21 ret:从栈中弹出地址,并跳转到这个位置。栈即如下:
被保存的start函数的%ebp
每个函数开始前,先保存之前函数的帧指针%ebp
变量b的地址
变量a的地址
  到这里fun函数即执行完,然后又跳转到main函数开始执行后续指令。后续L35行用到的%eax即之前fun函数的返回值,L35 L36 L37都用到了%ebp,此时%ebp已经指向了main函数的帧指针,后面已经没有什么可以描述的了,最后还会修改变量a b c 的值,只需要相应的修改栈中内容即可,没有什么可说的了。
  到这里全部分析过程就结束了。希望能够帮助到跟我一样对过程调用不熟悉的朋友。
(作者)(译者).. 机械工业出版社. 2010.
阅读(...) 评论()这是我的第一篇博客,由于公司项目需要,将暂时告别C语言一段时间。所以在此记录一下自己之前学习C语言的一些心得体会,希望可以分享给大家,也可以记录下自己学习过程中遇到的问题以及存在的疑惑(其实就是自己学习过程中不解的地方)。好了,废话不多说,开始微博内容了,O(∩_∩)O哈哈~
接下来将通过下面几个问题解析函数调用中对堆栈理解:
函数调用过程中堆栈在内存中存放的结构如何?
汇编语言中call,ret,leave等具体操作时如何?
linux中任务的堆栈,数据存放是如何?
1. 函数调用过程中堆栈在内存中存放的结构如何?
计算机,嵌入式设备,智能设备等其实都是有软件和硬件两部分组成,具体实现也许复杂,但整体的结构也就如此。软件运行在硬件上,告诉硬件该干什么。操作系统软件是在启动过程中经过BIOS,bootloarder等(如果有这些过程的话)从磁盘加载到内存中,而自定义软件则是编写存放到磁盘中,只有通过加载才会到内存中运行。
首先我们来看一下什么是堆、栈还有堆栈,我们经常说堆栈其实它是等同于栈的概念。
可以通俗意义上这样理解堆,堆是一段非常大的内存空间,供不同的程序员从其中取出一段供自己使用,使用之后要由程序员自己释放,如果不释放的话,这部分存储空间将不能被其他程序使用。堆的存储空间是不连续的,因为会因为不同时间,不同大小的堆空间的申请导致其不连续性。堆的生长是从低地址向高地址增长的。
对栈的理解是,栈是一段存储空间,供系统或者操作系统使用,对程序员来说一般是不可见的,除非从一开始由程序员自己通过汇编等自己构建栈,栈会由系统管理单元自己申请释放。栈是从高地址向低地址生长的,既栈底在高地址,栈顶低地址。
其次我们看一下应用程序的加载,应用程序被加载进内存后,由操作系统为其分配堆栈,程序的入口函数会是main函数。不过main函数也不是第一个被调用的函数,我们通过简单的例子讲解。
#include &stdio.h&
#include &string.h&
int function(int arg)
int main(void)
int i = 10;
j = function(i);
printf("%d\n",j);
用gcc -S main.c 生成汇编文件main.s, 其中function的汇编代码如下:
.cfi_startproc
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
%rsp, %rbp
.cfi_def_cfa_register 6
%edi, -4(%rbp)
-4(%rbp), %eax
.cfi_def_cfa 7, 8
.cfi_endproc
看以看到当函数被调用时,首先会把调用函数的栈底压栈到自己函数的栈中(pushq %rbp),然后将原来函数栈顶rsp作为当前函数的栈底(movq %rsp, %rbp)。函数运行完成时,会将压入栈中的rbp重新出栈到rbp中(popq %rbp)。当前function汇编函数没有显示出栈顶的变化(rsp的变化),我们可以通过main函数来看栈顶的变化,汇编代码如下:
.cfi_startproc
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
%rsp, %rbp
.cfi_def_cfa_register 6
$10, -4(%rbp)
-4(%rbp), %eax
%eax, %edi
%eax, -8(%rbp)
-8(%rbp), %eax
%eax, %esi
$.LC0, %edi
.cfi_def_cfa 7, 8
.cfi_endproc
从上面的汇编代码可以看到首先也是压栈和设置新栈底的过程,从此可以看出main函数也是被调用的函数,而不是第一个调用函数。代码中的黄色部分是当前栈顶变化,从使用的subq可以知道,栈顶的地址要小于栈底的地址,所以栈是从高地址向低地址生长。
接下来可能有点绕,慢慢读,将用语言描述函数调用过程,调用函数会将被调用函数的实参从右往左的顺序压入调用函数的栈中,通过call指令调用被调用函数,首先将return address(也就是call指令的后一条指令的地址)压入调用函数栈中,这时rsp寄存器中存储的地址是存放return address内存地址的下一地址值,这时调用函数的栈结构形成,然后就会进入被调用函数的作用域中。被调用函数首先将调用函数的rbp压入被调用函数栈中(其实这个地址就是rsp寄存器中存储的地址),接下来将会将这个地址作为被调用函数的rbp地址,才会有movq %rsp, %rbp指令设置被调用函数的栈底。如上所描述的构成了函数调用的堆栈结构如下图所示。
此图来自,此图中MOV EBP,ESP与本文的movq指令操作不同。
2. 汇编语言中call,ret,leave等具体操作时如何?
  push:将数据压入栈中,具体操作是rsp先减,然后将数据压入sp所指的内存地址中。rsp寄存器总是指向栈顶,但不是空单元。
  pop:将数据从栈中弹出,然后rsp加操作,确保rsp寄存器指向栈顶,不是空单元。
  call:将下一条指令的地址压入当前调用函数的栈中(将PC指令压入栈中,因为在从内存中取出call指令时,PC指令已经自动增加),然后改变PC指令的为call的function的地址,程序指针跳转到新function。
  ret:当指令指到ret指令行时,说明一个函数已经结束了,这时候rsp已经从被调用函数的栈指到了调用函数构建的返回地址位置。ret是将rsp所指栈顶地址中的内容赋值给PC,接下来将执行call function的下一条指令。
  leave:相当于mov %esp, %ebp, pop ebp。头一条指令其实是把ebp所指的被调用函数的栈底作为新的栈顶,pop指令时相当于把被调用函数的栈底弹出,rsp指向返回地址。
  int:通过其后加中断号,实现软件引发中断,linux操作系统中系统调用多有此实现,其他实时操作系统中在操作系统移植时,会有tick心脏函数也有此实现。
  其他的汇编指令在此就不多讲了,因为汇编指令众多,硬件cpu寄存器也因硬件不同而不同,此节就讲了函数构建进入和离开函数时用到的几个汇编指令,这几条指令和栈变化有关。自己构建汇编函数,或者是在读linux操作系统的系统调用时会对其理解有帮助。硬件寄存器中rsp,和rbp用于指示栈顶和栈底。
3. linux中任务的堆栈,数据存放是如何?
linux的任务堆栈分为两种:内核态堆栈和用户态堆栈。接下来简单介绍一下这两个堆栈,如果以后有机会将详细介绍这两个堆栈。
1. 内核态堆栈
linux操作系统分为内核态和用户态。用户态代码访问代码和数据收到诸多限制,用户态主要是为程序员编写程序使用,处于用户态的代码不可以随便访问linux内核态的数据,这主要就是设置用户态的权限,安全考虑。但是用户态可以通过系统调用接口,中断,异常等访问指定内核态的内容。内核态主要是用于操作系统内核运行以及管理,可以无限制的访问内存地址和数据,权限比较大。
linux操作系统的进程是动态的,有生命周期,进程的运行和普通的程序运行一样,需要堆栈的帮助,如果在内核存储区域内为其提前分配堆栈的话,既浪费内核内存(任务地址大约3G的空间),也不能灵活的构建任务,所以linux操作系统在创建新的任务时,为其分配了8k的存储区域用于存放进程内核态的堆栈和线程描述符。线程描述符位于分配的存储区域的低地址区域,大小固定,而内核态堆栈则从存储区域的高地址开始向低地址延伸。如果之前版本为内核态堆栈和线程描述符分配4k的存储空间时,则需要为中断和异常分配额外的栈供其使用,防止任务堆栈溢出。
2. 用户态堆栈
对于32位的linux操作系统,每个任务都会有4G的寻址空间,其中0-3G为用户寻址空间,3G-4G为内核寻址空间。每个任务的创建都会有0-3G的用户寻址空间,但是3G-4G的内核寻址空间是属于所有任务共享的。这些地址都属于线性地址,需要通过地址映射转换成物理地址。为了实现每个任务在访问0-3G的用户空间时不至于混淆地址,每个任务的内存管理单元都会有一个属于自身的页目录pgd,在任务创建之初会创建新的pgd,任务会通过地址映射为0-3G空间映射物理地址。用户态的堆栈就在这0-3G的用户寻址空间中分配,和之前的main函数以及function函数构建堆栈一样,但是具体映射到哪个物理地址,还需要内存管理单元去做映射操作。总之,linux任务用户态的堆栈和普通应用程序一样,由操作系统分配和释放,对程序员来说不可见,不过因为操作系统的原因,任务用户程序寻址有限制。如果有机会之后介绍一下linux内存管理的个人理解。
转载from:
本文已收录于以下专栏:
相关文章推荐
本文共包含一下四个部分。C源代码
对应汇编代码:此汇编使用”gcc -S hello.c”命令编译生成,部分删减
栈空间的使用过程:包括5个部分,五张图C源码
int sayhello(int ...
之前看了很多关于uboot的分析,其中就有说要为C语言的运行,准备好堆栈。
而自己在Uboot的start.S汇编代码中,关于系统初始化,也看到有堆栈指针初始化这个动作。但是,从来只是看到有人说...
#include  using namespace void f1(int a,int b){}int main(){    f1(3,4);   &#...
C语言的程序运行可以说就是不断的调用函数,从主入口的main函数到各种各样的库函数,再到用户自定义的完成特定功能的函数。
程序中关于一个函数的操作主要包括三个方面。①函数声明,②函数定义,③函数调用...
简析C语言中的函数调用栈机制!
一、地址空间与物理内存
(1)地址空间与物理内存是两个完全不同的概念,真正的代码及数据都存在物理内存中。
物理储存器是指实际存在的具体储存器芯片,CPU在操纵物理储存器的时候都把他们当做内存来对待...
前几天碰到一个问题:一个进程运行过程中挂死了,把gdb挂上去之后bt打印的内容为空,后来通过查看 /proc 文件系统,查看程的调用栈,才发现是发消息给内核态程序时,内核态一直没有响应,导致用户态进程...
之前一直有这样的需求,当时问到,也没搜到方法,现在竟然既问到了,也搜到了,哎,世事真是不能强求啊! 在linux内核调试中,经常用到的打印函数调用堆栈的方法非常简单,只需在需要查看堆栈的函数中加入:d...
我们知道,栈溢出通常是因为递归调用层次太深导致,那么为什么递归调用层次太深回导致栈溢出呢,解决这个问题
之前我们先看一下与函数调用有关的栈的基本概念:
1. 每一个线程拥有一个调用栈结构(...
写一下关于函数调用栈的一些相关知识,对于在Linux下面进行c/c++开发,在问题定位时 查看调用栈信息是一个非常常用的定位方法,因为根据调用关系,可以知道程序的执行流程是什么样子。如果 不能查看调用...
他的最新文章
讲师:吴岸城
您举报文章:
举报原因:
原文地址:
原因补充:
(最多只允许输入30个字)& & & 这是我的第一篇博客,由于公司项目需要,将暂时告别C语言一段时间。所以在此记录一下自己之前学习C语言的一些心得体会,希望可以分享给大家,也可以记录下自己学习过程中遇到的问题以及存在的疑惑(其实就是自己学习过程中不解的地方)。好了,废话不多说,开始微博内容了,O(&_&)O哈哈~
& & & 接下来将通过下面几个问题解析函数调用中对堆栈理解:
函数调用过程中堆栈在内存中存放的结构如何?
汇编语言中call,ret,leave等具体操作时如何?
linux中任务的堆栈,数据存放是如何?
& & & 1. 函数调用过程中堆栈在内存中存放的结构如何?
& & & 计算机,嵌入式设备,智能设备等其实都是有软件和硬件两部分组成,具体实现也许复杂,但整体的结构也就如此。软件运行在硬件上,告诉硬件该干什么。操作系统软件是在启动过程中经过BIOS,bootloarder等(如果有这些过程的话)从磁盘加载到内存中,而自定义软件则是编写存放到磁盘中,只有通过加载才会到内存中运行。
& & & 首先我们来看一下什么是堆、栈还有堆栈,我们经常说堆栈其实它是等同于栈的概念。
& & & 可以通俗意义上这样理解堆,堆是一段非常大的内存空间,供不同的程序员从其中取出一段供自己使用,使用之后要由程序员自己释放,如果不释放的话,这部分存储空间将不能被其他程序使用。堆的存储空间是不连续的,因为会因为不同时间,不同大小的堆空间的申请导致其不连续性。堆的生长是从低地址向高地址增长的。
& & & 对栈的理解是,栈是一段存储空间,供系统或者操作系统使用,对程序员来说一般是不可见的,除非从一开始由程序员自己通过汇编等自己构建栈,栈会由系统管理单元自己申请释放。栈是从高地址向低地址生长的,既栈底在高地址,栈顶低地址。
& & & 其次我们看一下应用程序的加载,应用程序被加载进内存后,由操作系统为其分配堆栈,程序的入口函数会是main函数。不过main函数也不是第一个被调用的函数,我们通过简单的例子讲解。
#include &stdio.h&
#include &string.h&
int function(int arg)
int main(void)
int i = 10;
j = function(i);
printf("%d\n",j);
用gcc -S main.c 生成汇编文件main.s, 其中function的汇编代码如下:
.cfi_startproc
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
%rsp, %rbp
.cfi_def_cfa_register 6
%edi, -4(%rbp)
-4(%rbp), %eax
.cfi_def_cfa 7, 8
.cfi_endproc
看以看到当函数被调用时,首先会把调用函数的栈底压栈到自己函数的栈中(pushq %rbp),然后将原来函数栈顶rsp作为当前函数的栈底(movq %rsp, %rbp)。函数运行完成时,会将压入栈中的rbp重新出栈到rbp中(popq %rbp)。当前function汇编函数没有显示出栈顶的变化(rsp的变化),我们可以通过main函数来看栈顶的变化,汇编代码如下:
.cfi_startproc
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
%rsp, %rbp
.cfi_def_cfa_register 6
$10, -4(%rbp)
-4(%rbp), %eax
%eax, %edi
%eax, -8(%rbp)
-8(%rbp), %eax
%eax, %esi
$.LC0, %edi
.cfi_def_cfa 7, 8
.cfi_endproc
从上面的汇编代码可以看到首先也是压栈和设置新栈底的过程,从此可以看出main函数也是被调用的函数,而不是第一个调用函数。代码中的黄色部分是当前栈顶变化,从使用的subq可以知道,栈顶的地址要小于栈底的地址,所以栈是从高地址向低地址生长。
& & & 接下来可能有点绕,慢慢读,将用语言描述函数调用过程,调用函数会将被调用函数的实参从右往左的顺序压入调用函数的栈中,通过call指令调用被调用函数,首先将return address(也就是call指令的后一条指令的地址)压入调用函数栈中,这时rsp寄存器中存储的地址是存放return address内存地址的下一地址值,这时调用函数的栈结构形成,然后就会进入被调用函数的作用域中。被调用函数首先将调用函数的rbp压入被调用函数栈中(其实这个地址就是rsp寄存器中存储的地址),接下来将会将这个地址作为被调用函数的rbp地址,才会有movq %rsp, %rbp指令设置被调用函数的栈底。如上所描述的构成了函数调用的堆栈结构如下图所示。
此图来自/taek/archive//2338877.html,此图中MOV EBP,ESP与本文的movq指令操作不同。
& & &2. 汇编语言中call,ret,leave等具体操作时如何?
  push:将数据压入栈中,具体操作是rsp先减,然后将数据压入sp所指的内存地址中。rsp寄存器总是指向栈顶,但不是空单元。
  pop:将数据从栈中弹出,然后rsp加操作,确保rsp寄存器指向栈顶,不是空单元。
  call:将下一条指令的地址压入当前调用函数的栈中(将PC指令压入栈中,因为在从内存中取出call指令时,PC指令已经自动增加),然后改变PC指令的为call的function的地址,程序指针跳转到新function。
  ret:当指令指到ret指令行时,说明一个函数已经结束了,这时候rsp已经从被调用函数的栈指到了调用函数构建的返回地址位置。ret是将rsp所指栈顶地址中的内容赋值给PC,接下来将执行call function的下一条指令。
  leave:相当于mov %esp, %ebp, pop ebp。头一条指令其实是把ebp所指的被调用函数的栈底作为新的栈顶,pop指令时相当于把被调用函数的栈底弹出,rsp指向返回地址。
  int:通过其后加中断号,实现软件引发中断,linux操作系统中系统调用多有此实现,其他实时操作系统中在操作系统移植时,会有tick心脏函数也有此实现。
  其他的汇编指令在此就不多讲了,因为汇编指令众多,硬件cpu寄存器也因硬件不同而不同,此节就讲了函数构建进入和离开函数时用到的几个汇编指令,这几条指令和栈变化有关。自己构建汇编函数,或者是在读linux操作系统的系统调用时会对其理解有帮助。硬件寄存器中rsp,和rbp用于指示栈顶和栈底。
& & &&3. linux中任务的堆栈,数据存放是如何?
& & & linux的任务堆栈分为两种:内核态堆栈和用户态堆栈。接下来简单介绍一下这两个堆栈,如果以后有机会将详细介绍这两个堆栈。
1. 内核态堆栈
& & & linux操作系统分为内核态和用户态。用户态代码访问代码和数据收到诸多限制,用户态主要是为程序员编写程序使用,处于用户态的代码不可以随便访问linux内核态的数据,这主要就是设置用户态的权限,安全考虑。但是用户态可以通过系统调用接口,中断,异常等访问指定内核态的内容。内核态主要是用于操作系统内核运行以及管理,可以无限制的访问内存地址和数据,权限比较大。
& & & linux操作系统的进程是动态的,有生命周期,进程的运行和普通的程序运行一样,需要堆栈的帮助,如果在内核存储区域内为其提前分配堆栈的话,既浪费内核内存(任务地址大约3G的空间),也不能灵活的构建任务,所以linux操作系统在创建新的任务时,为其分配了8k的存储区域用于存放进程内核态的堆栈和线程描述符。线程描述符位于分配的存储区域的低地址区域,大小固定,而内核态堆栈则从存储区域的高地址开始向低地址延伸。如果之前版本为内核态堆栈和线程描述符分配4k的存储空间时,则需要为中断和异常分配额外的栈供其使用,防止任务堆栈溢出。
此图出自http://blog.csdn.net/bailyzheng/article/details/,
2. 用户态堆栈
& & & 对于32位的linux操作系统,每个任务都会有4G的寻址空间,其中0-3G为用户寻址空间,3G-4G为内核寻址空间。每个任务的创建都会有0-3G的用户寻址空间,但是3G-4G的内核寻址空间是属于所有任务共享的。这些地址都属于线性地址,需要通过地址映射转换成物理地址。为了实现每个任务在访问0-3G的用户空间时不至于混淆地址,每个任务的内存管理单元都会有一个属于自身的页目录pgd,在任务创建之初会创建新的pgd,任务会通过地址映射为0-3G空间映射物理地址。用户态的堆栈就在这0-3G的用户寻址空间中分配,和之前的main函数以及function函数构建堆栈一样,但是具体映射到哪个物理地址,还需要内存管理单元去做映射操作。总之,linux任务用户态的堆栈和普通应用程序一样,由操作系统分配和释放,对程序员来说不可见,不过因为操作系统的原因,任务用户程序寻址有限制。如果有机会之后介绍一下linux内存管理的个人理解。
Views(...) Comments()}

我要回帖

更多关于 函数调用堆栈 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信