进阶 #assembly

机器码与指令编码

机器码是CPU直接理解和执行的二进制指令,每条指令由操作码和操作数编码组成

从硬件到指令

你已经了解了 ALU 如何执行加法、逻辑门 如何进行与或非运算——但 CPU 怎么知道自己”当前该做什么”?是加法还是减法?数据从哪来、结果存哪去?

答案藏在指令里。

每条指令就是一行二进制的”命令”,告诉 CPU 三件事:

  1. 做什么(操作:加?减?还是存数据?)
  2. 对谁做(数据在哪:寄存器?内存地址?还是指令本身?)
  3. 结果放哪(目标地址)

机器码 = 二进制编码的指令

机器码(Machine Code) 就是 CPU 能直接”看懂”的指令,表现为一串 0 和 1。CPU 的 控制单元 逐条读取机器码,解码并执行。

每条机器码可以分为两个部分:

┌─────────────────┬────────────────┐
│    操作码        │    操作数       │
│  (Opcode)       │  (Operand)     │
│  做什么          │  对谁做/结果放哪 │
└─────────────────┴────────────────┘
  • 操作码(Opcode):指定要执行的操作(如加法、减法、加载数据)
  • 操作数(Operand):指定参与操作的数据或数据的位置

类比:点餐

想象你在食堂点餐:

  • 操作码 = “我要一份”(动作)
  • 操作数 = “番茄炒蛋”(对象)

机器码就是:[我要一份][番茄炒蛋] 这串编码。食堂阿姨(CPU)看到这个编码就知道该做什么。

指令编码示例

来看一个简化的 8 位指令格式——前半段是操作码,后半段是操作数:

二进制编码含义
0000 0001ADD R1 — 把寄存器 R1 的值加到累加器
0001 0010SUB R2 — 从累加器减去寄存器 R2 的值
0010 0011MOV R3 — 把累加器的值存入寄存器 R3
1111 0000HLT — 停止执行

从 C 语言到机器码:完整链路

理解机器码最好的方式——看一段 C 代码怎么一路变成二进制:

// C 源码
int a = 3, b = 5;
int c = a + b;
编译器逐级翻译:

C:          int c = a + b;
              ↓ 编译
RISC-V 汇编:  ADDI  R1, R0, 3     ; a=3
              ADDI  R2, R0, 5     ; b=5
              ADD   R3, R1, R2    ; c=a+b
              ↓ 汇编器
机器码(十六进制):  0x00500093  0x00108133
              ↓ 显示为二进制
机器码(二进制):
    00000000010100000000000010010011
              ↓ CPU 控制单元解码
控制信号:ALU 做加法、寄存器 R0→ALU、立即数 3→ALU
         结果写回寄存器 R1

这个链路就是你写的每一行代码的”宿命”——最终变成二进制,被 CPU 逐条读取和执行。

类比:食堂点餐的编码

  1. 控制单元分析高 4 位 0000 → “哦,这是加法指令”
  2. 再看低 4 位 0001 → “操作数是寄存器 R1”
  3. ALU 执行加法:累加器 ← 累加器 + R1

💡 实际 CPU 的指令编码比这复杂得多(x86 指令长度从 1 到 15 字节不等),但核心思想完全相同——操作码 + 操作数。

从机器码到汇编语言

直接写 0 和 1 简直反人类。试试看下面这段:

10110000 01100001

你完全看不出它在干嘛。但如果写成:

MOV AL, 0x61

就清楚了:把十六进制数 61(即十进制 97,ASCII 中字母 ‘a’ 的编码)存入寄存器 AL。

汇编语言(Assembly Language) 就是用助记符代替二进制码的人可读编程语言。它和机器码是一一对应的——每条汇编指令对应一条机器码指令。

汇编器(Assembler) 负责把汇编语言翻译成机器码。就像翻译官把中文(汇编)翻译成英语(机器码)。

指令格式的变化

不同 CPU 的指令编码格式不同,主要分为两类:

定长指令

所有指令长度相同(如 RISC-V 的 32 位定长):

ADD  R1, R2, R3    →  0000000_00010_00011_000_00001_0110011
  • 优点:解码简单,CPU 可以高效流水线处理
  • 缺点:每条指令占用空间大,代码体积大

变长指令

指令长度可变(如 x86 的 1~15 字节):

NOP                →  90(1 字节)
MOV AX, BX         →  89 D8(2 字节)
  • 优点:代码紧凑,节省内存
  • 缺点:解码电路复杂

类比:字典 vs 对话

  • 定长指令 = 字典里每个词条固定 20 个字——查起来容易但书本很厚
  • 变长指令 = 日常对话中”嗯""好”可以很短,“我昨天在超市遇到了一个老朋友”很长——省纸但理解起来更费劲

指令编码的全过程

graph LR
    A[汇编源码<br>MOV R1, #42] --> B[汇编器<br>Assembler]
    B --> C[机器码<br>二进制指令]
    C --> D[内存中存储]
    D --> E[CPU 读取]
    E --> F[控制单元解码]
    F --> G[ALU 或其它单元执行]

小结

机器码是 CPU 唯一能直接理解的”母语”。每条机器码由操作码(做什么)和操作数(对谁做)组成。

因为人类记不住 0 和 1,我们发明了汇编语言——用助记符替代二进制码,再由汇编器翻译回机器码。不同 CPU 有不同的指令编码方式(定长 vs 变长),这引出了一个更根本的问题:指令集架构——CPU 到底”懂”哪些指令?这就是接下来要学习的内容:指令集架构概述