早期计算机程序是通过「打孔卡」来实现的,人们在特定位置上打洞或者不打洞,来表示“0”或者“1”。
那为什么我们每天用高级语言的程序,最终是怎么变成一串串“0”和“1”的?这一串串“0”和“1”是怎么在CPU中处理的?今天来说说「机器码」和「计算机指令」。
在软硬件接口中,CPU帮我们做了什么事?
CPU,又名「中央处理器」,是计算机的电脑。
从硬件的角度,是一个超大规模集成电路,通过电路实现加法和乘法等各种处理逻辑。
从软件的角度,CPU是一个执行各种计算机指令的逻辑机器。这里的「计算机指令」好比一门CPU能听得懂的语言,我们也称之为“机器语言”。
不同的CPU能听懂的语言也不同。比如,「个人电脑的Intel的CPU」和「苹果手机ARM的CPU」就是不同的CPU,这两者听懂的语言也不同。类似这样两种CPU各自支持的语言,就是两组不同的计算机指令集。
一个计算机程序,不可能只有一条指令,而是有成千上万条指令。但是CPU不能一直放着这些指令,所以计算机程序一般是存储在存储器里的。这种程序指令存储在存储器里的计算机,我们就叫做存储程序计算机。
从编译到汇编,代码怎么变成机器码?
// test.c
int main()
{
int a = 1;
int b = 2;
a = a + b;
}
这段代码,要在Linux操作系统上跑起来,我们首先需要把整个程序翻译成汇编语言的程序。这个过程,我们称之为「编译成汇编代码」。
针对汇编代码,我们可以再用「汇编器」翻译成「机器码」。这些机器码是由“0”和“1”组成的机器语言表示。这一条条机器码,也就是「计算机指令」。这一串串16进制数字,就是CPU能够识别的计算机指令。
在Linux操作系统上,运行gcc和objdump这两条指令。
$ gcc -g -c test.c
$ objdump -d -M intel -S test.o
test.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 :
int main()
{
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
int a = 1;
4: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1
int b = 2;
b: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
a = a + b;
12: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
15: 01 45 fc add DWORD PTR [rbp-0x4],eax
}
18: 5d pop rbp
19: c3 ret
我们可以看到,左侧一对数字,那就是「机器码」。右侧的push,mov,add这些是「汇编代码」。你可能会问,我们实际在用GCC(GUC编译器套餐)编译器的时候,可以直接把「代码」编译成「机器码」呀,为啥还要先编译成「汇编代码」?原因是你看这些机器码看不懂,你看「汇编代码」还能猜出一些含义。
因为「汇编代码」就是「给程序员看的机器码」。也正是这样,「汇编代码」和「机器码」是一一对应的。
从高级语言到汇编代码,再到机器码,就是日常开发程序,最终变成CPU可以执行的计算机指令的过程。
解析指令和机器码
接着我们看看一行行「汇编代码」和「机器码」都是啥意思。一般来说,常见的指令分为五大类。
第一类是算术类指令。加减乘除,在CPU层面,都会变成一个个算术类指令。
第二类是数据传输类指令。给「变量赋值」,在内存里面「读写数据」,用的是数据传输类指令。
第三类是逻辑类指令。逻辑上的与或非。
第四类是条件分支类指令。日常写的「if」「else」。
第五类是无条件跳转类指令。写一些大一点的程序,需要调用函数,在「调用函数」的时候就是无条件跳转类指令。
接着我们看看,「汇编代码」如何变成「机器码」的。
上述说过,不同的CPU对应不同的「计算机指令集」,也对应着不同的汇编语言和不同的机器码。选用一个简单的MIPS指令集,来看看「机器码」是如何生成的。
MIPS是一个32位的整数,高6位叫做「操作码」(Opcode),也就是表示这条指令具体「是一条什么样的指令」。剩下的26位有三种格式,分别是R,I和J。
R指令一般用作「算术」和「逻辑」类指令,里面读取和写入数据的寄存器地址。如果是逻辑位移操作,后面还有位移操作的位移量,而最后的「功能码」,则是在前面的「操作码」不够的时候,扩展「操作码」表示对应的具体指令的。
I指令则通常是用在数据传输、条件分支,以及在运算的时候使用的并非变量还是常数的时候。这个时候,没有了位移量和操作码,也没有了第三个寄存器,而是把这三部分直接合并成了一个地址值或者一个常数。
J指令就是一个跳转指令,高 6 位之外的 26 位都是一个跳转后的地址。
add $t0,$s2,$s1
我以一个简单的加法算术指令 add t0,s1, $s2, 为例,给你解释。为了方便,我们下面都用十进制来表示对应的代码。
对应的 MIPS 指令里 opcode 是 0,rs 代表第一个寄存器 s1 的地址是 17,rt 代表第二个寄存器 s2 的地址是 18,rd 代表目标的临时寄存器 t0 的地址,是 8。因为不是位移操作,所以位移量是 0。把这些数字拼在一起,就变成了一个 MIPS 的加法指令。为了读起来方便,我们一般把对应的二进制数,用 16 进制表示出来。在这里,也就是 0X02324020。这个数字也就是这条指令对应的机器码。
回到开头我们说的打孔带。如果我们用打孔代表 1,没有打孔代表 0,用 4 行 8 列代表一条指令来打一个穿孔纸带,那么这条命令大概就长这样:
如果你想一起学习这门课,可以扫下面的二维码购买: