函数调用中堆栈的个人理解【转】

转自:http://www.cnblogs.com/MyLove-Summer/p/5034209.html

这是我的第一篇博客,由于公司项目需要,将暂时告别C语言一段时间。所以在此记录一下自己之前学习C语言的一些心得体会,希望可以分享给大家,也可以记录下自己学习过程中遇到的问题以及存在的疑惑(其实就是自己学习过程中不解的地方)。好了,废话不多说,开始微博内容了,O(∩_∩)O哈哈~

      接下来将通过下面几个问题解析函数调用中对堆栈理解:

  • 函数调用过程中堆栈在内存中存放的结构如何?
  • 汇编语言中call,ret,leave等具体操作时如何?
  • linux中任务的堆栈,数据存放是如何?

      1. 函数调用过程中堆栈在内存中存放的结构如何?

      计算机,嵌入式设备,智能设备等其实都是有软件和硬件两部分组成,具体实现也许复杂,但整体的结构也就如此。软件运行在硬件上,告诉硬件该干什么。操作系统软件是在启动过程中经过BIOS,bootloarder等(如果有这些过程的话)从磁盘加载到内存中,而自定义软件则是编写存放到磁盘中,只有通过加载才会到内存中运行。

      首先我们来看一下什么是堆、栈还有堆栈,我们经常说堆栈其实它是等同于栈的概念。

      可以通俗意义上这样理解堆,堆是一段非常大的内存空间,供不同的程序员从其中取出一段供自己使用,使用之后要由程序员自己释放,如果不释放的话,这部分存储空间将不能被其他程序使用。堆的存储空间是不连续的,因为会因为不同时间,不同大小的堆空间的申请导致其不连续性。堆的生长是从低地址向高地址增长的。

      对栈的理解是,栈是一段存储空间,供系统或者操作系统使用,对程序员来说一般是不可见的,除非从一开始由程序员自己通过汇编等自己构建栈,栈会由系统管理单元自己申请释放。栈是从高地址向低地址生长的,既栈底在高地址,栈顶低地址。

      其次我们看一下应用程序的加载,应用程序被加载进内存后,由操作系统为其分配堆栈,程序的入口函数会是main函数。不过main函数也不是第一个被调用的函数,我们通过简单的例子讲解。

#include <stdio.h>
#include <string.h>

int function(int arg)
{
    return arg;
}
int main(void)
{
    int i = 10;
    int j;
    j = function(i);
    printf("%d\n",j);
    return 0;
}

 

用gcc -S main.c 生成汇编文件main.s, 其中function的汇编代码如下:

function:
.LFB0:
    .cfi_startproc
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    movl    -4(%rbp), %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

 

看以看到当函数被调用时,首先会把调用函数的栈底压栈到自己函数的栈中(pushq %rbp),然后将原来函数栈顶rsp作为当前函数的栈底(movq %rsp, %rbp)。函数运行完成时,会将压入栈中的rbp重新出栈到rbp中(popq %rbp)。当前function汇编函数没有显示出栈顶的变化(rsp的变化),我们可以通过main函数来看栈顶的变化,汇编代码如下:

main:
.LFB1:
    .cfi_startproc
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    $10, -4(%rbp)
    movl    -4(%rbp), %eax
    movl    %eax, %edi
    call    function
    movl    %eax, -8(%rbp)
    movl    -8(%rbp), %eax
    movl    %eax, %esi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    printf
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

 

从上面的汇编代码可以看到首先也是压栈和设置新栈底的过程,从此可以看出main函数也是被调用的函数,而不是第一个调用函数。代码中的黄色部分是当前栈顶变化,从使用的subq可以知道,栈顶的地址要小于栈底的地址,所以栈是从高地址向低地址生长。

      接下来可能有点绕,慢慢读,将用语言描述函数调用过程,调用函数会将被调用函数的实参从右往左的顺序压入调用函数的栈中,通过call指令调用被调用函数,首先将return address(也就是call指令的后一条指令的地址)压入调用函数栈中,这时rsp寄存器中存储的地址是存放return address内存地址的下一地址值,这时调用函数的栈结构形成,然后就会进入被调用函数的作用域中。被调用函数首先将调用函数的rbp压入被调用函数栈中(其实这个地址就是rsp寄存器中存储的地址),接下来将会将这个地址作为被调用函数的rbp地址,才会有movq %rsp, %rbp指令设置被调用函数的栈底。如上所描述的构成了函数调用的堆栈结构如下图所示。

此图来自http://www.cnblogs.com/taek/archive/2012/02/05/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/11842553,

2. 用户态堆栈

      对于32位的linux操作系统,每个任务都会有4G的寻址空间,其中0-3G为用户寻址空间,3G-4G为内核寻址空间。每个任务的创建都会有0-3G的用户寻址空间,但是3G-4G的内核寻址空间是属于所有任务共享的。这些地址都属于线性地址,需要通过地址映射转换成物理地址。为了实现每个任务在访问0-3G的用户空间时不至于混淆地址,每个任务的内存管理单元都会有一个属于自身的页目录pgd,在任务创建之初会创建新的pgd,任务会通过地址映射为0-3G空间映射物理地址。用户态的堆栈就在这0-3G的用户寻址空间中分配,和之前的main函数以及function函数构建堆栈一样,但是具体映射到哪个物理地址,还需要内存管理单元去做映射操作。总之,linux任务用户态的堆栈和普通应用程序一样,由操作系统分配和释放,对程序员来说不可见,不过因为操作系统的原因,任务用户程序寻址有限制。如果有机会之后介绍一下linux内存管理的个人理解。

时间: 2024-11-08 19:42:14

函数调用中堆栈的个人理解【转】的相关文章

Linux环境中堆栈和堆相关内存损坏的基本原理和调试方法介绍

本文讲的是Linux环境中堆栈和堆相关内存损坏的基本原理和调试方法介绍, 前言 建议阅读本文之前,你对ARM组件的有个基本了解,本文会先为你介绍32位Linux环境中进程的内存布局,然后再介绍堆栈和堆相关内存损坏的基本原理以及调试方法. 本文中使用的示例是在ARMv6 32位处理器上编译的,如果你无法访问ARM设备,可以点击这里https://azeria-labs.com/emulate-raspberry-pi-with-qemu/创建自己的实验环境并在虚拟机中模拟Raspberry Pi发

汇编-c/c++ 函数调用中形参为指针或者引用对栈操作问题

问题描述 c/c++ 函数调用中形参为指针或者引用对栈操作问题 问题引出: 当我们的函数参数为普通变量或指针时,我们在调用过程中会拷贝一个副本,而当形参为引用时不会拷贝一个副本. 当形参为普通变量时,会拷贝一个变量备份,当为指针时会拷贝一个指针备份,指针指向的内容不会拷贝 问题来了: 查看使用指针和使用引用的方式调用的函数的汇编代码,会发现在汇编代码层面实现方式是一模一样的,都是: lea eax,[i](假设i是整形变量) push eax 而使用值传递方式是: mov eax,dword p

函数调用导致堆栈不对称。原因可能是托管的 PInvoke 签名与非托管的目标签名不匹配。

原文:函数调用导致堆栈不对称.原因可能是托管的 PInvoke 签名与非托管的目标签名不匹配. 在dllimport中加入CallingConvention参数就行了,[DllImport(PCAP_DLL, CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)] 要注意C++与NET中数据类型的对应:   //c++:char * ---- c#:string //传入参数   //c++:char * ---

深入浅析C语言中堆栈和队列_C 语言

1.堆和栈 (1)数据结构的堆和栈 堆栈是两种数据结构. 栈(栈像装数据的桶或箱子):是一种具有后进先出性质的数据结构,也就是说后存放的先取,先存放的后取.这就如同要取出放在箱子里面底下的东西(放入的比较早的物体),首先要移开压在它上面的物体(放入的比较晚的物体). 堆(堆像一棵倒过来的树):是一种经过排序的树形数据结构,每个结点都有一个值.通常所说的堆的数据结构,是指二叉堆.堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆.由于堆的这个特性,常用来实现优先队列,堆的存取是随意,

Domino中的“代理”正确理解是什么?

问题描述 Domino中的"代理"正确理解是什么? 解决方案 解决方案二:在服务器端运行处理事务的程序解决方案三:一个或者多个数据库中执行特定任务的独立程序.服务器端,客户端都可以执行.解决方案四:Servlet解决方案五:....引用2楼cape114的回复: 一个或者多个数据库中执行特定任务的独立程序.服务器端,客户端都可以执行. 解决方案六:引用4楼zyy8023ych的回复: ....引用2楼cape114的回复:一个或者多个数据库中执行特定任务的独立程序.服务器端,客户端都可

class-java中this.new怎么理解

问题描述 java中this.new怎么理解 在java中,一个类A的内部声明一个非静态内部类.在这个类A的某个方法中要实现一个类的实例,实例代码中用到了this.new这里的this怎么理解. public class MyClass{ public void method1(){ ... MyInterface iclass = this.new MyClassInner(); ... } private class MyClassInner implements MyInterface{

对Java中传值调用的理解分析_java

本文实例分析了Java中的传值调用.分享给大家供大家参考.具体分析如下: Java以引用的方式操作对象实例 可以确认的是Java中操作对象的方式是以引用的方式操作对象.为了更深刻的了解这点我写了如下代码: 首先定义一个自定义类型 复制代码 代码如下: public class Person {            String name;            Person(String name){          this.name = name;      }  } 这里name默认是

服务器-java中这个参数如何理解&amp;amp;quot;goodsAction.action?type=type&amp;amp;amp;gtype=&amp;amp;quot;

问题描述 java中这个参数如何理解"goodsAction.action?type=type&gtype=" java中这个参数如何理解"goodsAction.action?type=type&gtype=" 是服务器类目下的goodsAction类的action方法的什么什么吗 解决方案 goodsAction.action应该是struts2框架的一个叫goodsAction的控制器方法 后面的type gtype是参数 解决方案二: goo

深入浅析JavaScript中with语句的理解_javascript技巧

JavaScript 有个 with 关键字, with 语句的原本用意是为逐级的对象访问提供命名空间式的速写方式. 也就是在指定的代码区域, 直接通过节点名称调用对象. with语句的作用是暂时改变作用域链.减少的重复输入. 其语法结构为: with(object){ //statements } 举一个实际例子吧: with(document.forms[]){ name.value = "lee king"; address.value = "Peking";