返回

从汇编的角度谈函数调用与返回

IOS

在上一篇文章中,我们探索了 CPU 与寄存器的一些原理和内在联系。我们知道任何高级语言方法的执行,它们的底层都是调用函数方法。但有没有想过函数的底层是怎么实现的呢?函数的调用一定涉及到了函数的存储位置和函数返回等一系列问题。本文将从汇编的角度带领大家一探究竟。

函数调用

在汇编中,函数调用通常使用 call 指令。call 指令的作用是将当前正在执行的指令地址压入栈中,然后跳转到被调用函数的地址。当被调用函数执行完毕后,它使用 ret 指令返回到调用函数。

函数调用的过程可以分为以下几个步骤:

  1. 压入返回地址。 当 call 指令被执行时,当前正在执行的指令地址被压入栈中。这确保了在被调用函数执行完毕后,调用函数可以继续执行。
  2. 跳转到被调用函数。 call 指令还将跳转到被调用函数的地址。这使得被调用函数可以开始执行。
  3. 保存寄存器。 在被调用函数开始执行之前,它通常会保存一些寄存器。这主要是为了防止这些寄存器在被调用函数中被修改,从而影响到调用函数的执行。
  4. 分配栈空间。 被调用函数还需要为它的局部变量分配栈空间。局部变量是在函数内部声明的变量,它们只能在函数内部使用。
  5. 执行函数体。 当所有准备工作都完成后,被调用函数就可以开始执行它的函数体了。函数体是函数的主要部分,它包含了函数要执行的代码。
  6. 释放栈空间。 当被调用函数执行完毕后,它会释放为它的局部变量分配的栈空间。
  7. 恢复寄存器。 被调用函数还会恢复它之前保存的寄存器。
  8. 返回调用函数。 最后,被调用函数使用 ret 指令返回到调用函数。

函数返回

函数返回是指函数执行完毕后,它将控制权交还给调用函数的过程。函数返回可以使用两种方式实现:

  1. 使用 ret 指令。 ret 指令是函数返回最常用的方式。当 ret 指令被执行时,栈顶的地址被弹出并加载到程序计数器中。这使得调用函数可以继续执行。
  2. 使用 jmp 指令。 jmp 指令也可以用于函数返回。当 jmp 指令被执行时,程序计数器直接跳转到指定的地址。这使得调用函数无法继续执行。

栈帧和局部变量

栈帧是函数在栈中分配的一块空间,它用于存储函数的局部变量和参数。栈帧的大小在函数被调用时确定,它由以下部分组成:

  • 局部变量区。 局部变量区用于存储函数的局部变量。
  • 参数区。 参数区用于存储函数的参数。
  • 返回地址区。 返回地址区用于存储函数返回时要返回的地址。

参数传递和返回值获取

在函数调用过程中,参数需要从调用函数传递给被调用函数。参数可以通过以下几种方式传递:

  • 按值传递。 按值传递是指将参数的值复制到被调用函数的栈帧中。这样,被调用函数就可以访问参数的值,但它不能修改调用函数的原始参数。
  • 按引用传递。 按引用传递是指将参数的地址复制到被调用函数的栈帧中。这样,被调用函数就可以访问参数的值,并且它可以修改调用函数的原始参数。
  • 按结果传递。 按结果传递是指将参数的结果复制到调用函数的栈帧中。这样,调用函数就可以访问参数的结果,但它不能修改被调用函数的原始参数。

函数的返回值可以通过以下几种方式获取:

  • 使用 ret 指令。 ret 指令可以将指定的值压入栈中,然后返回到调用函数。
  • 使用 jmp 指令。 jmp 指令可以将程序计数器跳转到指定的地址。如果指定的地址是调用函数的地址,那么就可以将返回值传递给调用函数。

结语

函数调用和函数返回是汇编编程中非常重要的两个概念。理解了函数调用和函数返回的机制,我们就可以更好地理解函数是如何在计算机中工作的。