CPU 能处理的只有指令,我们写的程序归根结底就是指令,高级语言只有翻译成机器码,即计算机能够识别的指令,才能够被执行。

不同的 CPU 拥有不同的指令集,一般 PC 使用 Intel 的 CPU,iPhone 使用 ARM 的 CPU。所以如果在电脑上写个程序,装到手机上,一般无法运行,因为语言不通,而装到另一台电脑上,通常能正常运行,就是因为语言相通。

代码怎样变成机器码

来看段 C 代码,究竟如何变成机器码的。

1
2
3
4
5
6
7
// test.c
int main()
{
int a = 1;
int b = 2;
a = a + b;
}

要在 Linux 上跑起来,首先需要编译成汇编语言,然后通过汇编器翻译成机器码。
通过 gcc 和 objdump 两个命令,来将对应的汇编和机器代码打印。

1
2
gcc -g -c test.c
objdump -D -S test.o

可以看到,左侧有一堆数字,这些就是一条条机器码;右边有一系列的 push、mov、add、pop 等,这些就是对应的汇编代码。一行 C 语言代码,有时候只对应一条机器码和汇编代码,有时候则是对应两条机器码和汇编代码。汇编代码和机器码之间是一一对应的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
test.o:	file format Mach-O 64-bit x86-64

Disassembly of section __TEXT,__text:
_main:
; {
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: 31 c0 xorl %eax, %eax
; int a = 1;
6: c7 45 fc 01 00 00 00 movl $1, -4(%rbp)
; int b = 2;
d: c7 45 f8 02 00 00 00 movl $2, -8(%rbp)
; a = a + b;
14: 8b 4d fc movl -4(%rbp), %ecx
17: 03 4d f8 addl -8(%rbp), %ecx
1a: 89 4d fc movl %ecx, -4(%rbp)
; }
1d: 5d popq %rbp
1e: c3 retq

这里可以想象一下,高级语言转换成机器码,为什么要多出中间一步转换,即先转换成汇编代码,如果直接转换有哪些不妥?

高级语言->汇编代码->机器码

CPU 如何解析机器码

当 CPU 拿到一条指令(一条机器码)的时候,就会去对应的指令集中看,这条指令是什么操作。

这里介绍一下最简单的 MIPS 指令集。

MIPS 指令集

MIPS 的指令是一个 32 位的整数,高 6 位叫操作码(Opcode),也就是代表这条指令具体是一条什么样的指令,剩下的 26 位有三种格式,分别是 R、I 和 J。

R 指令是一般用来做算术和逻辑操作,里面有读取和写入数据的寄存器的地址。如果是逻辑位移操作,后面还有位移操作的位移量,而最后的功能码,则是在前面的操作码不够的时候,扩展操作码表示对应的具体指令的。

I 指令,则通常是用在数据传输、条件分支,以及在运算的时候使用的并非变量还是常数的时候。这个时候,没有了位移量和操作码,也没有了第三个寄存器,而是把这三部分直接合并成了一个地址值或者一个常数。

J 指令就是一个跳转指令,高 6 位之外的 26 位都是一个跳转后的地址。

我们常用的 Intel CPU ,大概 2000 左右个指令集,正对应了上图中五类操作。

  • 算术类指令
  • 数据传输类指令
  • 逻辑类指令
  • 条件分支类指令
  • 无条件跳转指令

CPU 如何执行指令

还拿 Intel CPU 来说,里面差不多有几百亿个晶体管,通过电路不断切换运转起来。逻辑上,可以认为 CPU 其实就是由一堆寄存器组成的。而寄存器就是 CPU 内部,由多个触发器(Flip-Flop)或者锁存器(Latches)组成的简单电路。

触发器和锁存器,其实就是两种不同原理的数字电路组成的逻辑门。

N 个触发器或者锁存器,就可以组成一个 N 位(Bit)的寄存器,能够保存 N 位的数据。

一个 CPU 里面会有很多种不同功能的寄存器。这里介绍三种比较特殊的。

一个是 PC 寄存器,我们也叫指令地址寄存器。用来存放下一条需要执行的计算机指令的内存地址。

第二个是指令寄存器,用来存放当前正在执行的指令。

第三个是条件码寄存器,用里面的一个一个标记位(Flag),存放 CPU 进行算术或者逻辑计算的结果。

实际上,一个程序执行的时候,CPU 会根据 PC 寄存器里的地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。可以看到,一个程序的一条条指令,在内存里面是连续保存的,也会一条条顺序加载。

而有些特殊指令,比如 J 类指令,也就是跳转指令,会修改 PC 寄存器里面的地址值。这样,下一条要执行的指令就不是从内存里面顺序加载的了。事实上,这些跳转指令的存在,也是我们可以在写程序的时候,使用 if…else 条件语句和 while/for 循环语句的原因。