细说:程序运行的环境和运行过程,再看不懂请自行面壁( 三 )


目前我们用栈来管理内存,所以可以把活动记录等价于栈桢 。栈桢是活动记录的实现方式,我们可以自由设计活动记录或栈桢的结构,下图是一个常见的设计:

细说:程序运行的环境和运行过程,再看不懂请自行面壁

文章插图
 
  • 返回值:一般放在最顶上,这样它的地址是固定的 。foo() 函数返回以后,它的调用者可以到这里来取到返回值 。在实际情况中,我们会优先通过寄存器来传递返回值,比通过内存传递性能更高 。
  • 参数:在调用 foo 函数时,把参数写到这个地址里 。同样,我们也可以通过寄存器来传递,而不是内存 。
  • 控制链接:就是上一级栈桢的地址 。如果用到了上一级作用域中的变量,就可以顺着这个链接找到上一级栈桢,并找到变量的值 。
  • 返回地址:foo 函数执行完毕以后,继续执行哪条指令 。同样,我们可以用寄存器来保存这个信息 。
  • 本地变量:foo 函数的本地变量 b 的存储空间 。
  • 寄存器信息:我们还经常在栈桢里保存寄存器的数据 。如果在 foo 函数里要使用某个寄存器,可能需要先把它的值保存下来,防止破坏了别的代码保存在这里的数据 。这种约定叫做被调用者责任,也就是使用寄存器的人要保护好寄存器里原有的信息 。某个函数如果使用了某个寄存器,但它又要调用别的函数,为了防止别的函数把自己放在寄存器中的数据覆盖掉,要自己保存在栈桢中 。这种约定叫做调用者责任 。

细说:程序运行的环境和运行过程,再看不懂请自行面壁

文章插图
 
你可以看到,每个栈桢的长度是不一样的 。
用到的参数和本地变量多,栈桢就要长一点 。但是,栈桢的长度和结构是在编译期就能完全确定的 。这样就便于我们计算地址的偏移量,获取栈桢里某个数据 。
总的来说,栈桢的设计很自由 。但是,你要考虑不同语言编译形成的模块要能够链接在一起,所以还是要遵守一些公共的约定的,否则,你写的函数,别人就没办法调用了 。
在之前的文章中我提到过栈桢,这次我们用了更加贴近具体实现的描述:栈桢就是一块确定的内存,变量就是这块内存里的地址 。在下一讲,我会带你动手实现我们的栈桢 。
2.从全局角度看整个运行过程了解了栈桢的实现之后,我们再来看一个更大的场景,从全局的角度看看整个运行过程中都发生了什么 。
细说:程序运行的环境和运行过程,再看不懂请自行面壁

文章插图
 
代码区里存储了一些代码,main 函数、bar 函数和 foo 函数各自有一段连续的区域来存储代码,我用了一些汇编指令来表示这些代码(实际运行时这里其实是机器码) 。
假设我们执行到 foo 函数中的一段指令,来计算“b+c”的值,并返回 。这里用到了mov、add、jmp 这三个指令 。mov 是把某个值从一个地方拷贝到另一个地方,add 是往
某个地方加一个值,jmp 是改变代码执行的顺序,跳转到另一个地方去执行(汇编命令的细节,我们下节再讲,你现在简单了解一下就行了) 。
mov b的地址寄存器1add c的地址寄存器1mov寄存器1 foo的返回值地址jmp返回地址//或ret指令执行完这几个指令以后,foo 的返回值位置就写入了 6,并跳转到 bar 函数中执行 foo 之后的代码 。
这时,foo 的栈桢就没用了,新的栈顶是 bar 的栈桢的顶部 。理论上讲,操作系统这时可以把 foo 的栈桢所占的内存收回了 。比如,可以映射到另一个程序的寻址空间,让另一个程序使用 。但是在这个例子中你会看到,即使返回了 bar 函数,我们仍要访问栈顶之外的一个内存地址,也就是返回值的地址 。
所以,目前的调用约定都规定,程序的栈顶之外,仍然会有一小块内存(比如 128K)是可以由程序访问的,比如我们可以拿来存储返回值 。这一小段内存操作系统并不会回收 。
我们目前只讲了栈,堆的使用也类似,只不过是要手工进行申请和释放,比栈要多一些维护工作 。
总结本文带你了解了程序运行的环境和过程,我们的程序主要跟 CPU、内存,以及操作系统打交道 。你需要了解的重点如下: