图解 Google V8 # 10:机器代码:二进制机器码究竟是如何被CPU执行的?

Posted 凯小默

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图解 Google V8 # 10:机器代码:二进制机器码究竟是如何被CPU执行的?相关的知识,希望对你有一定的参考价值。

说明

图解 Google V8 学习笔记

在编译流水线中的位置

在执行代码时,V8 需要先将 javascript 编译成字节码,然后再解释执行字节码,或者将需要优化的字节码编译成二进制,并直接执行二进制代码。

CPU执行二进制代码:

将源码编译成机器码

下面是一段 C 代码

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

先通过 GCC 编译器将这段 C 代码编译成二进制文件,编译出来的机器码

每一行都是一个指令,该指令可以让 CPU 执行指定的任务。

  • 左边就是编译生成的机器码,用十六进制来展示,便于阅读
  • 中间的部分是汇编代码,汇编代码采用助记符(memonic)来编写程序

助记符(mnemonic)

助记符(mnemonic)是便于人们记忆、并能描述指令功能和指令操作数的符号,助记符是表明指令功能的英语单词或其缩写。汇编语言由于采用了助记符号来编写程序,比用机器语言的二进制代码编程要方便些,在一定程度上简化了编程过程。汇编语言的特点是用符号代替了机器指令代码,而且助记符与指令代码一一对应,基本保留了机器语言的灵活性。使用汇编语言能面向机器并较好地发挥机器的特性,得到质量较高的程序。按指令作用对象来分,可分为伪指令和真指令(硬指令)。伪指令也就是作用于汇编程序的命令;真指令就是作用于真正处理器的命令。

  • 汇编:将汇编语言编写的程序转换为机器语言的过程
  • 反汇编:将机器语言转化为汇编语言的过程

CPU 是怎么执行程序的?

计算机系统的硬件组织结构:


首先,在程序执行之前,需要被装进内存。

什么是内存?

内存(Memory)是计算机的重要部件,也称内存储器和主存储器,它用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。它是外存与CPU进行沟通的桥梁,计算机中所有程序的运行都在内存中进行,内存性能的强弱影响计算机整体发挥的水平。只要计算机开始运行,操作系统就会把需要运算的数据从内存调到CPU中进行运算,当运算完成,CPU将结果传送出来。

内存还是一个临时存储数据的设备,之所以是临时的存储器,是因为断电之后,内存中的数据都会消失。

内存中的每个存储空间都有其对应的独一无二的地址


当二进制代码被加载进了内存后,同一条指令,使用相同的颜色来标记:


二进制数据反汇编成一条条指令的形式:

CPU 便可以通过指定内存地址,从内存中取出一条指令,然后分析该指令,最后执行该指令,这三个过程称为一个 CPU 时钟周期

CPU 是怎么知道要取出内存中的哪条指令?

将混乱的二进制代码转换为有序的指令形式:

CPU 中有一个 PC 寄存器,它保存了将要执行的指令地址,当二进制代码被装载进了内存之后,系统会将二进制代码中的第一条指令的地址写入到 PC 寄存器中,到了下一个时钟周期时,CPU 便会根据 PC 寄存器中的地址,从内存中取出指令。

PC (program counter)是程序计数器的简称。PC 是一种特殊的寄存器,用于保存下一条待执行指令的存储地址。

PC 寄存器中的指令取出来之后,CPU 会做两件事:

1、将下一条指令的地址更新到 PC 寄存器中


2、分析该指令,并识别出不同的类型的指令,以及各种获取操作数的方法。

什么是通用寄存器?

通用寄存器是 CPU 中用来存放数据的设备,不同处理器中寄存器的个数也是不一样的,之所以要通用寄存器,是因为 CPU 访问内存的速度很慢,所以 CPU 就在内部添加了一些存储设备,这些设备就是通用寄存器。

通用寄存器跟内存的区别:

  • 通用寄存器容量小,读写速度快
  • 内存容量大,读写速度慢。

通用寄存器通常用来存放数据或者内存中某块数据的地址,把这个地址又称为指针

某些专用的数据或者指针存储在专用的通用寄存器中:比如

  • rbp 寄存器通常是用来存放栈帧指针的
  • rsp 寄存器用来存放栈顶指针的
  • PC 寄存器用来存放下一条要执行的指令

常用的指令类型

算术逻辑单元(arithmetic and logic unit) 是能实现多组算术运算和逻辑运算的组合逻辑电路,简称ALU。

  1. 加载的指令:其作用是从内存中复制指定长度的内容到通用寄存器中,并覆盖寄存器中原来的内容。

    上图使用了 movl 指令,指令后面跟着的第一个参数是要拷贝数据的内存的位置,第二个参数是要拷贝到 ecx 这个寄存器。

  2. 存储的指令:和加载类型的指令相反,其作用是将寄存器中的内容复制内存某个位置,并覆盖掉内存中的这个位置上原来的内容。

    上图也是使用 movl 指令,movl 指令后面的 %ecx 就是寄存器地址,-8(%rbp) 是内存中的地址,这条指令的作用是将寄存器中的值拷贝到内存中。

  3. 更新指令:其作用是复制两个寄存器中的内容到 ALU 中,也可以是一块寄存器和一块内存中的内容到 ALU 中,ALU 将两个字相加,并将结果存放在其中的一个寄存器中,并覆盖该寄存器中的内容。

    上图里的 addl 指令,将寄存器 eax 和 ecx 中的值传给 ALUALU 对它们进行相加操纵,并将计算的结果写回 ecx。

  4. 跳转指令:从指令本身抽取出一个字,这个字是下一条要执行的指令的地址,并将该字复制到 PC 寄存器中,并覆盖掉 PC 寄存器中原来的值。

    上图是通过 jmp 来实现的,jmp 后面跟着要跳转的内存中的指令地址。

  5. IO 读 / 写指令:这些指令可以从一个 IO 设备中复制指定长度的数据到寄存器中,也可以将一个寄存器中的数据复制到指定的 IO 设备。

CPU 是如何执行指令?

分析一段汇编代码的执行流程,以上面的 C 代码为例

1、在 C 程序中,CPU 会首先执行调用 main 函数,在调用 main 函数时,CPU 会保存上个栈帧上下文信息和创建当前栈帧的上下文信息,主要是通过下面这两条指令实现的:

pushq   %rbp
movq    %rsp, %rbp
  • 第一条指令:将 rbp 寄存器中的值写到内存中的栈区域
  • 第二条指令:将 rsp 寄存器中的值写到 rbp 寄存器中。

2、然后将 0 写到栈帧的第一个位置:

movl  $0, -4(%rbp)

3、接下来给 x 和 y 赋值:

movl  $1, -8(%rbp)
movl  $2, -12(%rbp)
  • 第一条指令:将常数值 1 压入到栈中
  • 第二条指令:将常数值 2 压入到栈中

4、x 的值从栈中复制到 eax 寄存器中

movl  -8(%rbp), %eax

5、再将内存中的 y 和 eax 中的 x 相加,相加的结果再保存在 eax 中

addl  -12(%rbp), %eax

6、CPU 会将结果保存到内存中

 movl  %eax, -16(%rbp)

7、将结果 z 加载到 eax 寄存器中,会被默认作为返回值

movl  -16(%rbp), %eax

8、最后执行一些恢复现场的操作

popq  %rbp 
retq

总结

下面是来自王楚然同学的总结:

  1. 二进制代码装载进内存,系统会将第一条指令的地址写入到 PC 寄存器中。
  2. 读取指令:根据pc寄存器中地址,读取到第一条指令,并将pc寄存器中内容更新成下一条指令地址。
  3. 分析指令:并识别出不同的类型的指令,以及各种获取操作数的方法。
  4. 执行指令:由于cpu访问内存花费时间较长,因此cpu内部提供了通用寄存器,用来保存关键变量,临时数据等。指令包括加载指令,存储指令,更新指令,跳转指令。如果涉及加减运算,会额外让ALU进行运算。
  5. 指令完成后,通过pc寄存器取出下一条指令地址,并更新pc寄存器中内容,再重复以上步骤。

参考资料

以上是关于图解 Google V8 # 10:机器代码:二进制机器码究竟是如何被CPU执行的?的主要内容,如果未能解决你的问题,请参考以下文章

图解 Google V8 # 08:类型转换:V8是怎么实现 1 + “2” 的?

图解 Google V8 # 01:V8 是如何执行一段 JavaScript 代码的?

图解 Google V8 # 09:运行时环境:运行JavaScript代码的基石

图解 Google V8 # 20 :垃圾回收:V8的两个垃圾回收器是如何工作的?

图解 Google V8 # 05:函数表达式的底层工作机制

图解 Google V8学习笔记合集 23 篇(完结)