编程语言
首页 > 编程语言> > 图解CPU执行一段程序

图解CPU执行一段程序

作者:互联网

程序执行

从打印出 Hello World 开始,程序如何运行起来,大家都很清楚。那么底层如何执行的呢,让我们一探究竟。

long main(){
  long a = 1;
  long b = 2;
  return a + b;
}

来一段 C 语言作为例子, gcc -S 生成汇编代码,简化如下。

pushq   %rbp
movq    %rsp, %rbp
movq    $1, -8(%rbp)
movq    $2, -16(%rbp)
movq    -16(%rbp), %rax
movq    -8(%rbp), %rdx
addq    %rdx, %rax
popq    %rbp
ret

有点晕,让我们补个知识。在所有 cpu 体系架构中,每个寄存器通常都是有建议的使用方法的,而编译器也通常依照CPU架构的建议来使用这些寄存器,因而我们可以认为这些建议是编译器都遵守的。

image

这里还要区分一下 “Caller Save” 和 ”Callee Save” 寄存器,即寄存器的值是由”调用者保存“ 还是由 ”被调用者保存“。当产生函数调用时,子函数内通常也会使用到通用寄存器,那么这些寄存器中之前保存的调用者(父函数)的值就会被覆盖。为了避免数据覆盖而导致从子函数返回时寄存器中的数据不可恢复,CPU 体系结构中就规定了通用寄存器的保存方式。

如果一个寄存器被标识为”Caller Save”, 那么在进行子函数调用前,就需要由调用者提前保存好这些寄存器的值,保存方法通常是把寄存器的值压入堆栈中,调用者保存完成后,在被调用者(子函数)中就可以随意覆盖这些寄存器的值了。如果一个寄存被标识为“Callee Save”,那么在函数调用时,调用者就不必保存这些寄存器的值而直接进行子函数调用,进入子函数后,子函数在覆盖这些寄存器之前,需要先保存这些寄存器的值,即这些寄存器的值是由被调用者来保存和恢复的。

看了这些文档介绍,回想下 图解CPU为何要乱序执行。CPU 执行单元并没有函数概念,只要指令和取值都准备好了,就可以执行。不过我们写程序一般都是一段程序(比如一个函数),外加信息作为上下文。翻译成指令,那肯定茫茫多,因此操作系统为了更好管理,设计了 Frame 用来封装一段相关联的代码。那么现在执行逻辑抽象如下。

程序执行模拟栈调用,我们结合图在看看刚才代码。

  1. 先把当前帧的信息保存起来, pushq %rbp
  2. 接着把要执行的帧赋给 %rpb, movq %rsp, %rbp
  3. 处理参数,先把参数读取入栈,然后再赋值给寄存器
  4. 取存和指令都满足了,那么就可以执行
  5. 把返回结果放入寄存器 %rax,和上一步可以一起完成, addq %rdx, %rax
  6. 执行完毕,把原来的栈信息取出,再放入 %rpb, popq %rbp
  7. 最后 ret 指令相当于 popq %rip,指向下一条指令

从栈调用来看,结合栈地址是从高到低,栈内参数位数对齐,紧密排布,那么相对的偏移量也是固定的。因此直接通过 %rbp 执行偏移取值,就可以完成变量的处理,因此我们也可以得出一个抽象栈内分布图如下。

总结

栈执行主要看 %rbp 指向谁,而 %rbp 又是通过 %rsp 赋值的,因此只要我们想办法把 %rsp 切换了就相当于换了执行单元的上下文环境。现在协程很流行,按照这个思路,大家是不是可以构思一下协程如何实现了~

标签:调用者,程序,函数调用,rbp,寄存器,图解,CPU,rax,movq
来源: https://www.cnblogs.com/maycap/p/15703766.html