5. 计算机指令:让我们试试用纸带编程

早期计算机程序是通过「打孔卡」来实现的,人们在特定位置上打洞或者不打洞,来表示“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 列代表一条指令来打一个穿孔纸带,那么这条命令大概就长这样:

如果你想一起学习这门课,可以扫下面的二维码购买:


你可能感兴趣的:(5. 计算机指令:让我们试试用纸带编程)