函数运行时在内存中是什么样子?( 二 )


A Box
函数调用也是同样的道理,你把上面的ABCD换成函数ABCD,本质不变 。因此,现在我们知道了,使用栈这种结构就可以用来保存函数调用信息 。和游戏中的每个任务一样,当函数在运行时每个函数也要有自己的一个“小盒子”,这个小盒子中保存了函数运行时的各种信息,这些小盒子通过栈这种结构组织起来,这个小盒子就被称为栈帧,stack frames,也有的称之为call stack,不管用什么命名方式,总之,就是这里所说的小盒子,这个小盒子就是函数运行起来后占用的内存,这些小盒子构成了我们通常所说的栈区 。那么函数调用时都有哪些信息呢?
控制转移
我们知道当函数A调用函数B的时候,控制从A转移到了B,所谓控制其实就是指CPU执行属于哪个函数的机器指令,CPU从开始执行属于函数A的指令切换到执行属于函数B的指令,我们就说控制从函数A转移到了函数B 。控制从函数A转移到函数B,那么我们需要有这样两个信息:

  • 我从哪里来 (返回)
  • 要到去哪里 (跳转)
是不是很简单,就好比你出去旅游,你需要知道去哪里,还需要记住回家的路 。函数调用也是同样的道理 。当函数A调用函数B时,我们只要知道:
  • 函数A对于的机器指令执行到了哪里 (我从哪里来,返回)
  • 函数B第一条机器指令所在的地址 (要到哪里去,跳转)
有这两条信息就足以让CPU开始执行函数B对应的机器指令,当函数B执行完毕后跳转回函数A 。那么这些信息是怎么获取并保持的呢?现在我们就可以打开这个小盒子,看看是怎么使用的了 。假设函数A调用函数B,如图所示:
函数运行时在内存中是什么样子?

文章插图
 
?
 
当前,CPU执行函数A的机器指令,该指令的地址为0x400564,接下来CPU将执行下一条机器指令也就是:
call 0x400540这条机器指令是什么意思呢?这条机器指令对应的就是我们在代码中所写的函数调用,注意call后有一条机器指令地址,注意观察上图你会看到,该地址就是函数B的第一条机器指令,从这条机器指令后CPU将跳转到函数B 。现在我们已经解决了控制跳转的“要到哪里去”问题,当函数B执行完毕后怎么跳转回来呢?原来,call指令除了给出跳转地址之外还有这样一个作用,也就是把call指令的下一条指令的地址,也就是0x40056a push到函数A的栈帧中,如图所示:
函数运行时在内存中是什么样子?

文章插图
 
?
 
现在,函数A的小盒子变大了一些,因为装入了返回地址:
函数运行时在内存中是什么样子?

文章插图
 
?
 
现在CPU开始执行函数B对应的机器指令,注意观察,函数B也有一个属于自己的小盒子(栈帧),可以往里面扔一些必要的信息 。
函数运行时在内存中是什么样子?

文章插图
 
?
 
如果函数B中又调用了其它函数呢?道理和函数A调用函数B是一样的 。让我们来看一下函数B最后一条机器指令ret,这条机器指令的作用是告诉CPU跳转到函数A保存在栈帧上的返回地址,这样当函数B执行完毕后就可以跳转到函数A继续执行了 。至此,我们解决了控制转移中“我从哪里来”的问题 。
 
传递参数与获取返回值
函数调用与返回使得我们可以编写函数,进行函数调用 。但调用函数除了提供函数名称之外还需要传递参数以及获取返回值,那么这又是怎样实现的呢?
在x86-64中,多数情况下参数的传递与获取返回值是通过寄存器来实现的 。假设函数A调用了函数B,函数A将一些参数写入相应的寄存器,当CPU执行函数B时就可以从这些寄存器中获取参数了 。同样的,函数B也可以将返回值写入寄存器,当函数B执行结束后函数A从该寄存器中就可以读取到返回值了 。我们知道寄存器的数量是有限的,当传递的参数个数多于寄存器的数量该怎么办呢?这时那个属于函数的小盒子也就是栈帧又能发挥作用了 。原来,当参数个数多于寄存器数量时剩下的参数直接放到栈帧中,这样被调函数就可以从前一个函数的栈帧中获取到参数了 。现在栈帧的样子又可以进一步丰富了,如图所示:
函数运行时在内存中是什么样子?

文章插图
 
从图中我们可以看到,调用函数B时有部分参数放到了函数A的栈帧中,同时函数A栈帧的顶部依然保存的是返回地址 。


推荐阅读