图解 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。
-
加载的指令:其作用是从内存中复制指定长度的内容到通用寄存器中,并覆盖寄存器中原来的内容。
上图使用了 movl 指令,指令后面跟着的第一个参数是要拷贝数据的内存的位置,第二个参数是要拷贝到 ecx 这个寄存器。 -
存储的指令:和加载类型的指令相反,其作用是将寄存器中的内容复制内存某个位置,并覆盖掉内存中的这个位置上原来的内容。
上图也是使用 movl 指令,movl 指令后面的%ecx
就是寄存器地址,-8(%rbp)
是内存中的地址,这条指令的作用是将寄存器中的值拷贝到内存中。 -
更新指令:其作用是复制两个寄存器中的内容到 ALU 中,也可以是一块寄存器和一块内存中的内容到 ALU 中,ALU 将两个字相加,并将结果存放在其中的一个寄存器中,并覆盖该寄存器中的内容。
上图里的 addl 指令,将寄存器 eax 和 ecx 中的值传给ALU
,ALU
对它们进行相加操纵,并将计算的结果写回 ecx。 -
跳转指令:从指令本身抽取出一个字,这个字是下一条要执行的指令的地址,并将该字复制到 PC 寄存器中,并覆盖掉 PC 寄存器中原来的值。
上图是通过 jmp 来实现的,jmp 后面跟着要跳转的内存中的指令地址。 -
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
总结
下面是来自王楚然
同学的总结:
- 二进制代码装载进内存,系统会将第一条指令的地址写入到 PC 寄存器中。
- 读取指令:根据pc寄存器中地址,读取到第一条指令,并将pc寄存器中内容更新成下一条指令地址。
- 分析指令:并识别出不同的类型的指令,以及各种获取操作数的方法。
- 执行指令:由于cpu访问内存花费时间较长,因此cpu内部提供了通用寄存器,用来保存关键变量,临时数据等。指令包括加载指令,存储指令,更新指令,跳转指令。如果涉及加减运算,会额外让ALU进行运算。
- 指令完成后,通过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的两个垃圾回收器是如何工作的?