计算机指令-机器码
计算机指令
CPU是一个可以执行各种指令的逻辑机器,而计算机指令就是指特定CPU认识的指令集。
每种CPU的指令集不同
我们平时用的PC是Intel CPU,他的指令集与苹果最新的M1芯片指令集不同,两边编译后的程序自然也不能直接运行。
编译->汇编->机器码
高级语言 ----编译---> 汇编语言 ----汇编器----> 机器码(计算机指令,16进制)
一行汇编代码对应一行机器码,也就是一行计算机指令,一般CPU有2000多种指令,但是大致可以分为五种:
MIPS指令集
MIPS 技术公司在 80 年代中期设计出来的 CPU 指令集,现在开源了,以这个指令集,我们尝试理解一下这个CPU指令集。
指令位数设计
MIPS 的指令是一个 32 位的整数,高 6 位叫操作码(Opcode),也就是代表这条指令具体是一条什么样的指令。
剩下的 26 位有三种格式,分别是 R、I 和 J。
R 指令是一般用来做算术和逻辑操作,里面有读取和写入数据的寄存器的地址。如果是逻辑位移操作,后面还有位移操作的位移量,而最后的功能码,则是在前面的操作码不够的时候,扩展操作码表示对应的具体指令的。
I 指令,则通常是用在数据传输、条件分支,以及在运算的时候使用的并非变量还是常数的时候。这个时候,没有了位移量和操作码,也没有了第三个寄存器,而是把这三部分直接合并成了一个地址值或者一个常数。
J 指令就是一个跳转指令,高 6 位之外的 26 位都是一个跳转后的地址。
可以看出每种指令的每一位都有专门的用途。
人肉指令翻译
add $t0,$s2,$s1
这是一个加法语句,将s2+s1相加,并将值赋予t0,我们尝试将其翻译成MIPS指令集的指令。
- 首先它是加法,应该是R指令,那我们参考R指令的位数设计
- operCode使用add,rs可以放入s2的地址,rt可以放入s1的地址,rd可以放入t0的地址,CPU就会将s2+s1的结果放入t0变量中。
3.我们将每个位数的二进制位合并,最终转换为16进制就如下图
上图的内容按16进制转换成打孔纸,就如下所示:
总结:
这一节,我们学习了指令集的一些常识,并且基于MIPS指令集,将一个加法指令转换成了打孔纸,我们理解了软件是如何转换成CPU指令的。
而平时使用的高级语言,也是一步步最终转换成16进制的指令,由CPU执行的。
指令跳转
高级语言编程是通过编译器、汇编器变成指令送给CPU执行的,这节说明了CPU是如何执行这些指令的。
CPU结构
在了解指令执行之前,先得了解CPU内部结构,如下这些寄存器

- PC寄存器(程序计数器) 用来存储下一条指令的地址。
- 指令寄存器,用来存储当前执行的指令。
- 条件码寄存器,用来存储条件和逻辑运算的结果。
- 其他各种寄存器
看懂指令
如下是C写的一段小代码,转换成汇编之后,主要略熟悉指令含义,就可以读懂汇编代码。
// test.c
#include <time.h>
#include <stdlib.h>
int main()
{
srand(time(NULL));
int r = rand() % 2;
int a = 10;
if (r == 0)
{
a = 1;
} else {
a = 2;
}
}
汇编代码:
if (r == 0)
3b: 83 7d fc 00 cmp DWORD PTR [rbp-0x4],0x0
3f: 75 09 jne 4a <main+0x4a>
{
a = 1;
41: c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x1
48: eb 07 jmp 51 <main+0x51>
}
else
{
a = 2;
4a: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
51: b8 00 00 00 00 mov eax,0x0
}
for (int i = 0; i <= 2; i++)
b: c7 45 f8 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0 //将0赋值给i
12: eb 0a jmp 1e //无条件跳转到1e
{
a += i;
14: 8b 45 f8 mov eax,DWORD PTR [rbp-0x4] //将i赋值到eax累加器中
17: 01 45 fc add DWORD PTR [rbp-0x8],eax //将eax累加器的值加到a中
1a: 83 45 f8 01 add DWORD PTR [rbp-0x4],0x1 //将1加到i上
1e: 83 7d f8 02 cmp DWORD PTR [rbp-0x4],0x2 //比较i和2
22: 7e f0 jle 14 //若小于等于则跳转到14行
24: b8 00 00 00 00 mov eax,0x0 //将0赋值给累加器
}
- cmp指令(compare) 比较指令,比较后面两位的值,第一位是内存地址,第二位是16进制的0,并把结果存到条件码寄存器里。
- jne指令(jump if not equal) 判断跳转指令,它会读取上一个指令的条件码寄存器结果值进行判断,若为1,顺序执行,若为0,跳转到4a行代码执行。
- mov指令(move) 移动指令,将后一位的值,移动到前一位处,第一位是变量a的地址值,第二位是16进制的1,其实就相当于给a赋值了。
- jmp指令(jump)无条件跳转指令,可以指定当前程序跳到某一行。
- jle指令(或jng)(jump if less or equal,or not greater)若小于等于则跳转,它会读取上一个指令的条件码寄存器结果值进行判断。
- add指令,将两个地址的值相加
- EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
- EBX 是"基地址"(base)寄存器, 在内存寻址时存放基地址。
- ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
- EDX 则总是被用来放整数除法产生的余数。
函数栈
这节讲的是函数栈的原理,栈已经很熟悉了,先入后出的数据结构,刚好符合方法调用逻辑。这里要说清楚几个概念:
- 函数栈内部组成
- 函数栈指令层面的算法
- 栈溢出概念
- 函数内联概念
函数栈内部组成
如下图所示,函数栈分为栈底和栈顶(实际是上底下顶),栈内每个函数一个栈帧(Stack Frame),栈帧内部保存了函数的局部变量、参数、返回地址等内容。

函数栈指令层面算法
int static add(int a, int b)
{
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 89 7d fc mov DWORD PTR [rbp-0x4],edi
7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi
return a+b;
a: 8b 55 fc mov edx,DWORD PTR [rbp-0x4]
d: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
10: 01 d0 add eax,edx
}
12: 5d pop rbp
13: c3 ret
0000000000000014 <main>:
int main()
{
14: 55 push rbp
15: 48 89 e5 mov rbp,rsp
18: 48 83 ec 10 sub rsp,0x10
int x = 5;
1c: c7 45 fc 05 00 00 00 mov DWORD PTR [rbp-0x4],0x5
int y = 10;
23: c7 45 f8 0a 00 00 00 mov DWORD PTR [rbp-0x8],0xa
int u = add(x, y);
2a: 8b 55 f8 mov edx,DWORD PTR [rbp-0x8]
2d: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
30: 89 d6 mov esi,edx
32: 89 c7 mov edi,eax
34: e8 c7 ff ff ff call 0 <add>
39: 89 45 f4 mov DWORD PTR [rbp-0xc],eax
3c: b8 00 00 00 00 mov eax,0x0
}
41: c9 leave
42: c3 ret
- 栈内并不会存储指令,指令执行依旧按照指令寄存器和PC寄存器顺序执行。
- rbp 当前栈帧的栈底位置,即帧指针。
- rsp 永远是栈顶位置,即栈指针。
- call 调用函数,会做两件事:①将PC寄存器的下一个地址压栈(如上图所示)②会跳转到指定方法的起始位置
- push rbp 将之前的rbp地址压栈(如上图所示)
- mov rbp,rsp 将现在栈指针地址赋值给rbp,也就是创建了新的栈帧地址。
- 然后开始执行指令,其中的参数和变量会不断压栈缓存到当前栈帧中。
- pop rbp 将当前栈帧弹出,恢复到main函数的帧指针地址。
- ret 弹出返回地址,跳出当前过程,继续执行调用者的代码。此时会将栈顶的返回地址弹出到PC,然后程序将按照弹出的返回地址继续执行。
栈溢出概念
栈溢出就是指栈内的空间不足,导致异常,例如函数调用链路过长,函数局部变量过多;单个java的栈一般可以存入1000多个栈帧,不使用递归很难会导致栈溢出。
函数内联
函数内联是编译器优化的一种策略,如果A调用B,将B的代码直接嵌入到A中,就可以删减掉对栈操作的指令,提高性能;
但是很明显,函数代码无法复用后,会导致编译后的汇编代码量急速膨胀。
内联后的函数,没有调用其他函数,只会被调用的函数,我们一般称之为叶子函数(或叶子过程)。
浙公网安备 33010602011771号