更多课程 选择中心

C/C++培训
达内IT学院

400-111-8989

堆栈是C语言里函数调用的基础,总结堆栈的具体作用

  • 发布:C++培训
  • 来源:学习笔记
  • 时间:2017-06-29 15:21

最近在弄嵌入式系统移植时,一直有一个相关概念就是堆栈。很多资料都说堆栈是C语言里函数调用的基础。具体,堆栈在c语言里和在系统里有哪些具体的作用。在网上和书上查了一下资料,现在总结一下,以供大家参考。

我们一般说堆栈的时候都强调堆栈是函数调用时把被调用的函数所需参数压入栈中,以实现函数的调用。

但是,函数调用具体是怎么实现的,我们一般不讲。现在总结一下堆栈的具体作用:

1,传递参数(为被调用函数提供参数)

2,保存局部变量

3,保存中间变量

4,在系统中用堆栈保存任务的状态(例如各个寄存器的值)

堆栈有后进先出的特性,所以能帮我们做很多事情。一会我们通过实例分析时你就会有感触。

下面先说一下看下面的具体事例分析时所需要的基础知识:

 

1.什么是堆栈


编译器一般使用堆栈实现函数调用。堆栈是存储器的一个区域,嵌入式环境有时需要程序员自己定义一个数组作为堆栈。Windows为每个线程自动维护一个堆栈,堆栈的大小可以设置。编译器使用堆栈来堆放每个函数的参数、局部变量等信息。

函数调用经常是嵌套的,在同一时刻,堆栈中会有多个函数的信息,每个函数占用一个连续的区域。一个函数占用的区域被称作帧(frame)。【每个函数都占有一个帧区。就是为了区分开这个函数的框架,一会我们分析具体实例时,我们就会有体会了】


编译器是从高地址开始使用堆栈。 

在多线程(任务)环境,CPU的堆栈指针指向的存储器区域就是当前使用的堆栈。切换线程的一个重要工作,就是将堆栈指针设为当前线程的堆栈栈顶地址。 

不同CPU,不同编译器的堆栈布局、函数调用方法都可能不同,但堆栈的基本概念是一样的。【我们说的是x86机器的,因为我们比较容易做实验】

1.1堆栈相关寄存器:


esp:堆栈指针(stack pointer),指向系统栈最上面一个栈帧的栈顶
ebp: 基址指针(base pointer),指向系统栈最上面一个栈帧的底部
cs:eip:指令寄存器(extended instruction pointer),指向下一条等待执行的指令地址
注:ebp在C语言中用作记录当前函数调用基址。

1.2堆栈操作


push:以字节为单位将数据(对于32位系统可以是4个字节)压入栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4的地址单元。
pop: 过程与PUSH相反。
call: 用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复执行下条指令。

leave:当调用函数调用时,一般都有这两条指令:pushl %ebp   movl %esp, %ebp  而,leave是这两条指令的反操作。
ret: 从一个函数或过程返回,之前call保存的下条指令地址会从栈内弹出到EIP寄存器中,程序转到CALL之前下条指令处执行。
注:
call指令的两个作用
①将下一条指令的地址A保存在栈顶
②设置eip指向被调用程序代码开始处

1.3函数堆栈框架的形成(C语言中)


①执行call XXX之前
cs : eip原来的值指向call下一条指令,该值被保存到栈顶
然后cs : eip的值指向xxx的入口地址


②进入 XXX
第一条指令: pushl %ebp                  //意为保存调用者的栈帧地址
第二条指令: movl %esp, %ebp        //初始化XXX的栈帧地址
然后函数体中的常规操作,可能会压栈、出栈


③退出XXX
movl %ebp,%esp
popl %ebp
ret

2.函数调用约定


函数调用约定包括传递参数的顺序,谁负责清理参数占用的堆栈等,如下面这个主要的函数约定表显示的 :
 

      

调用函数的代码和被调函数必须采用相同的函数的调用约定,程序才能正常运行。在Windows上,__cdecl是C/C++程序的缺省函数调用约定。在Linux下gcc默认用的规则是__stdcall ( 一会我们分析的函数就是在linux下用c语言源码和反汇编语言对比分析一下函数调用的具体实现)

在有的cpu上,编译器会用寄存器传递参数,函数使用的堆栈由被调函数分配和释放。这种调用约定在行为上和__cdecl有一个共同点:实参和形参数目不符不会导致堆栈错误。

不过,即使用寄存器传递参数,编译器在进入函数时,还是会将寄存器里的参数存入堆栈指定位置。参数和局部变量一样应该在堆栈中有一席之地。参数可以被理解为由调用函数指定初值的局部变量。

############################################3

好了,说了这么多,都是针对这个问题的基础知识。还有,你要完全看懂下面要举得具体例子还要有一定x86汇编语言的基础(当然是很简单的)。

看这么长的文章一定要有耐心,希望看完之后对你有所帮助。

好了,我们开始列举具体事例来说明问题:

c语言源码如下:

 

 

我们要把c语言源码编译成对应的汇编语言有以下两种方法,

一:使用gcc -S 文件名【我这里是diaoyong.c】 这样可以生成一个和源文件同名的汇编文件。

二:使用gdb工具调试。在gdb工具里用disassemble +文件名来反汇编c语言文件。

下面是我用gcc -S 命令编译出的汇编代码:

function:
        pushl   %ebp //保存原来的函数帧的栈底(当然这里是main函数的帧栈底部)
        movl    %esp, %ebp//此命令是把esp的值传送到ebp中去,还记得esp是什么吗,对时堆栈栈顶指针。
        movl    8(%ebp), %eax//把ebp的地址加上8里的值取出赋给eax
        sall    $1, %eax//左移一位实现乘2操作
        leave//相当于movl %esp, %ebp  pushl %ebp   
        ret//返回到调用函数
.Lfe1:
        .size   function,.Lfe1-function
.globl main
        .type   main,@function
main:
        pushl   %ebp 保存原来的函数帧的栈底(这里当然是调用main函数的帧栈底部)
        movl    %esp, %ebp//此命令是把esp的值传送到ebp中去。和function一样,这也说明main()函数没有什么特殊的,只是层次稍微高了一点点而已。
        subl    $8, %esp//堆栈指针向下移动8个字节,为局部变量i j 分配空间
        andl    $-16, %esp//
        movl    $0, %eax
        subl    %eax, %esp
        subl    $12, %esp//红色部分是为了让堆栈指针是16 的倍数。至于为什么要是16的倍数,Linux下的编译器GCC默认的堆栈是16字节对齐的,可能有些人要问为什么要对齐,对齐其实为了加快CPU的访问效率,这里你记住这点就可以了。

        pushl   $10//把需要的参数压入栈中
        call    function//呼叫被调用函数
        addl    $16, %esp
        movl    %eax, -4(%ebp)//把function返回值送到eax中
        movl    $0, %eax//把eax清零
        leave
        ret

下面做详细分析:

看看上面的汇编代码,和前面一样的不分析。但是其中有句不一样:subl $8 %esp ; 因为主函数里有两个临时变量i, j;给临时变量腾出8个字节空间。在看看下面的代码:

movl $10, (%esp) #====> %esp = 800, (800) = 10 ,其中800是我们假设的地址值,(800)表示地址800的内容这里的(%esp)指的是%esp地址里的内容,刚才我们假设这时候%esp的值是800, 那么地址为800的内容就是10了。执行函数调用了,注意在调用函数前其实是先把函数调用指令 call之后的地址压栈,也就是call之后那条指令的IP值压栈,所以这时候 %esp = 796;这里要弄明白为什么要把下条指令地址压栈,假设如果不把IP值压栈,那么当函数调用完毕后怎么能找到函数调用时的地址呢?也就是说如果没把IP压栈,那么函数调用完之后就回不到原来的执行地址了,就会造成程序执行顺序的错误!

下面列出函数function的汇编代码:

function:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
sall    $1, %eax
popl %ebp

leave
ret
pushl %ebp; 经过这条指令后 %esp值减4,所以这时候%esp值是792。下面这句:

movl %esp, %ebp #==============> %ebp = 792, %esp = 792, (792) = %ebp ;

其中(792)表示地址792的内容

movl 8(%ebp), %eax #========> %eax = 10

上面这句很多人可能不明白了,8(%ebp)指的是什么?8(%ebp)等于 : (%ebp + 8) ,这里注意,%ebp + 8 是表示一个地址值,加上括号表示存储在该地址上的内容。所以8(%ebp)其实就是地址为800的值,看前面地址800的值刚好是10!所以这句其实是把10复制给%eax寄存器.

 sall    $1, %eax#======> %eax = 20//左移相当于乘2

相当于2 * %eax, %eax这时候等于20了,刚好是实现了C代码中的 (2 * i);

popl %ebp #=========> 恢复%ebp寄存器的值, %esp这时候等于796

ret #=========> 函数调用完毕返回,这句其实是把刚才压栈的IP值弹出栈,执行这条指令后 %esp = 800

# 800!想想我们在调用函数的时候%esp也是800啊!这就是实现了“清栈”了,就是把调用函数所在的栈清除了!

好了,函数 function的汇编代码分析完了,现在回头继续看看main函数里的下一条指令了。接下来是这句:

movl %eax, -4(%ebp)

%eax寄存器存放的是什么?看function函数的代码,可以知道其实就是(2 * i)的值,所以返回值其实是通过%eax来传递的!传递到-4(%ebp)里去了,-4(%ebp) = (%ebp - 4); -4(%ebp)到底是什么呢?看看C代码,返回值传给变量j,那么-4(%ebp)会不会就是j呢?答案是肯定的!我们先看看%ebp的值是什么。看看 main函数的汇编代码,可以得出,%ebp其实指向了main函数的栈底部,但记不记得前面说的subl $24, %esp是为临时变量而留出的空间?没错,-4(%ebp) 就是存储在临时变量区域!也就是变量 j 了。

好了,说到这是不是有点晕了,没事,我画个图你就很明白了。 

  

即 ,调用函数时c语言会利用堆栈来做一个函数调用框架。压入栈中的每一个函数都有一个帧栈,每个帧栈都以ebp为分界线。

如最上层的函数,堆栈指针指向栈顶,而ebp指向帧栈底。而其他帧栈底部的指针ebp都压入到了栈中。当最外层的函数调用结束后,把ebp的值给dsp作为下一个函数的帧栈顶,而把压在栈中的ebp弹出做当前的帧栈底。如此往复。

预约申请免费试听课

填写下面表单即可预约申请免费试听!怕钱不够?可就业挣钱后再付学费! 怕学不会?助教全程陪读,随时解惑!担心就业?一地学习,可全国推荐就业!

上一篇:C语言必背18个经典程序
下一篇:C++漫谈:良好的编程习惯与编程要点

C语言创建windows窗口实例

C++回调函数是什么?

C++ shared_ptr和动态数组

C语言有哪些关键词,C语言44个关键词大全

  • 扫码领取资料

    回复关键字:视频资料

    免费领取 达内课程视频学习资料

  • 搜索抖音号

    搜索抖音号:1821685962

    免费领取达内课程视频学习资料

Copyright © 2021 Tedu.cn All Rights Reserved 京ICP备08000853号-56 京公网安备 11010802029508号 达内时代科技集团有限公司 版权所有

选择城市和中心
黑龙江省

吉林省

河北省

湖南省

贵州省

云南省

广西省

海南省