V8编译流水线-CPU执行二进制代码流程

958 阅读4分钟

V8运行时环境准备好后,juejin.cn/post/685457… V8开始执行JS代码,V8需要先将JS代码编译成字节码,然后再解释执行字节码,或者将需要优化的字节码编译成二进制,直接执行二进制代码。

1. 简介

系统硬件组织模型图结构:主要由CPU,主存储器,各种IO总线,外部设备(硬盘,显示器,USB等设备)组成的。

首先,在程序执行之前,先将我们要执行的程序装进内存。CPU可以通过 内存地址,从内存中读取数据或写入数据。CPU和内存通过内存地址来进行交互。

内存中的每个存储空间都有其对应的独一无二的地址,这个地址就是内存地址。代码被编译成可执行文件后,可执行文件包含二进制的机器码,二进制代码会被加载进内存。

二进制代码加载进内存后,CPU就从内存中取出一条指令,分析指令,执行指令,这三个过程称为CPU时钟周期。二进制代码装载进内存后,系统将二进制代码中的第一条指令的地址写入到PC寄存器中,到下一个时钟周期时,CPU就会根据PC寄存器中的内存地址,从内存中取出指令。PC寄存器中的指令取出来后,系统会做两件事:第一件是将下一条指令的内存地址更新到PC寄存器中;第二件就是分析取出来的指令,识别不同类型的指令,获取各种操作数的方法。接着CPU开始执行指令。

因为CPU访问内存速度较慢,所以CPU内部会添加小型的存储设备,比如通用寄存器。通用寄存器和内存之前的关系就类似于口袋和背包的关系。通用寄存器容量小,读写速度快,内存容量大,读写速度慢。 通用寄存器既可以存储小量的数据,也可以存储指针。比如PC寄存器用来存放下一条执行的指令等。

常用的指令类型:

  • 加载的指令,作用是从内存中复制指定长度的内容到通用寄存器中,并覆盖寄存器中原来的内容。
  • 存储的指令,和加载指令相反,作用是将寄存器中的内容复制到内存中的某个位置,并覆盖内存中这个位置的原有内容。
  • 更新指令,作用是复制两个寄存器中的内容到ALU中,也可以是一块寄存器和一块内存中的内容到ALU中,然后ALU将相加的结果存放到其中一个寄存器中,并覆盖原寄存器中的内容。
  • 跳转指令。如下图所示。

2. 例子说明

int main()
{  
    int x = 1;
    int y = 2;
    int z = x + y;
    return z;
}

上面C代码转成二进制机器码如下:

pushq   %rbp  // 将rbp寄存器中的值写到内存中的栈区域
movq    %rsp, %rbp // 将rsp寄存器中的值写到rbp寄存器中
movl    $0, -4(%rbp)  // 将0写到栈帧的第一个位置
movl    $1, -8(%rbp)  // 将常数1压入栈中,x变量赋值
movl    $2, -12(%rbp)  // 将常数2压入栈中,y变量赋值
movl    -8(%rbp), %eax  // x的值从栈中复制到eax寄存器中
addl    -12(%rbp), %eax  // eax中已经保存了x的值,此时将栈中的y值取出来,与x相加,相加的结果保存到eax中
movl    %eax, -16(%rbp)  // 将eax寄存器中保存下来的值,存放到内存中
movl    -16(%rbp), %eax  // 将内存中计算好的值加载到eax寄存器中,z变量赋值,eax寄存器中的值作为返回值
popq    %rbp      // 执行完成,弹出当前函数栈桢
retq              // 返回

3. 总结

  • V8需要先将JS编译成字节码或者二进制代码,然后执行。
  • CPU执行机器代码的逻辑:首先将编译之后的二进制代码加载进内存,然后CPU按照指令的顺序,一行一行的执行。在执行指令的过程中,CPU引入寄存器,将中间数据存储在寄存器中,从而加快CPU执行速度。
  • CPU使用寄存器的指令:加载指令,存储指令,更新指令。用来在寄存器和内存,寄存器和寄存器之间传输数据。

写在最后

V8相关的学习总结来自于极客时间李兵老师的课程《图解goole V8》,如果想了解更多细节,可以进课程查看。