函数调用与栈

文章目录

  • 函数调用与栈
    • 内存栈
    • 调用函数前
    • 调用函数时
      • 关于寄存器%ebp
      • 栈帧
    • 函数调用将要结束时
    • 函数调用结束后

函数调用与栈

内存栈

  在C语言中函数的调用必须借助于栈。
  关于栈是什么就不再做笔记了,但需要注意的是,这里的栈与数据结构中的栈虽然原理相同但并不是一个东西。在这里,栈就是一段计算机内存,只不过这段内存满足数据后进先出的规则。
  另外还需要注意的是,内存栈是向下增长的,即栈顶在下、栈底在上。之所以这样子,是因为我们将内存的高地址视为在上面、低地址在下面,而栈是由高地址向低地址推进的。如下图所示:
函数调用与栈_第1张图片

调用函数前

  在C程序调用函数前,它会现将函数所要用到的参数值以 逆序 的方式压入栈中。当调用函数时,函数所使用的参数值就是这些被压入栈中的值。此时栈中的情况如下图所示:
函数调用与栈_第2张图片

这里要注意的是逆序压入,这是C语言调用的约定。比如调用函数fun(a, b, c, d),则压栈的顺序为d、c、b、a 。

这里也解释了为什么C语言调用函数后,作为参数的变量,它们的值并没有发生改变。其原因就在于它们本身并没有参与被调用函数中的运算,真正参与运算的是它们在栈中的拷贝。

  在压入参数值后,将会一条call跳转指令。call指令会将调用完函数后将要返回的地址压入栈中,然后跳转到函数起始处(即将%eip寄存器的值修改为函数的起始地址)。此时栈中情况如图所示:
函数调用与栈_第3张图片

调用函数时

  现在eip指针(即%eip寄存器)已经指向了函数的起始地址,但是函数的起始部分并不是立马开始执行那些C语言写的代码,它还有一些准备工作。
  调用函数时,函数会先将%ebp寄存器的值压入栈中保存起来,然后用movl指令将栈指针寄存器%esp的值复制给%ebp。此时栈中情况如图所示:
函数调用与栈_第4张图片
  接下来准备工作完成,开始执行真正的函数代码了。在函数中免不了会定义局部变量,这些局部变量同样会依次压入栈中。假设函数中已经定义了n个局部变量,定义的顺序是局部变量1、局部变量2、…、局部变量n,此时栈中的情况如下:
函数调用与栈_第5张图片

关于寄存器%ebp

  %ebp寄存器被称为基址指针寄存器,在C语言的约定中它被用于访问函数的参数和局部变量。如何访问?当然是将%ebp作为基地址,然后通过偏移地址来访问栈中的各个值,所以ebp才叫作 基址 指针寄存器。

栈帧

  栈帧就是一个函数所对应的栈中所有的栈变量。就以上图为例,上图中整个栈就是一个栈帧,里面的栈变量包括了参数、局部变量和要返回的地址。

函数调用将要结束时

  函数调用将要结束时,函数会做以下几件事:

  • 将返回值存储到%eax寄存器(这个同样是C语言的约定)
  • 通过栈保存的内容来恢复到调用前的状态
  • 跳转回调用该函数的程序,即跳转到之前保存在栈中的地址。

  下面是返回主程序的汇编指令:

mov %ebp, %esp
popl %ebp
ret
  1. 先将%esp寄存器指向%ebp所指的地方,注意%ebp所指的内容一直保存着 旧的%ebp的值
  2. 再用一条popl %ebp指令即可将%ebp恢复到调用函数前的状态。这里要注意的是,当执行完栈弹出指令后,栈指针%esp就会指向栈中的下一条内容,即要返回的地址
  3. 最后用一条ret指令便跳回到主程序。ret指令会弹出栈顶的值,然后将该值复制到%eip寄存器。注意上一条说的,会发现现在复制到%eip的值正是要返回的地址。

函数调用结束后

  调用结束后主程序还有做一些事,就是将栈中剩余的参数部分弹出,当所有参数被弹出后,%esp的值也就恢复到调用函数之前的状态了。

  1. 从前面的分析便不难理解C语言为何无法改变传入函数的参数变量了;另外C语言中也不将函数中局部变量的地址作为返回值也可以轻易理解(函数返回时,其栈帧中的局部变量已经全部被舍弃了,该地址的内容已经无法预知)。
  2. 注意C语言中,%ebp和%esp寄存器都会被恢复,但其它寄存器情况就不一定了,而%eax寄存器的内容一定会被刷新(因为它保存了返回值)。
  3. 如果完全自己写汇编的话,完全可以不用%ebp保存栈帧的基地址,但是这是C语言的约定,并且由于硬件的结构该寄存器就是专门派这个用处的,效率是更高的。
  4. 这里只是讨论了C语言调用函数的一般情况,并没有讨论全局变量、返回值为一个结构(如果结构较大时,很明显之前说的%eax就会出现保存不下返回值的情况)等情况。
  5. 在提醒一下内存中栈是向下推进的,不是向上!!

你可能感兴趣的:(LINUX_C笔记)