Appearance
指令编码实例:读懂一条机器码
概念
指令编码(instruction encoding)指把一条汇编指令翻译成一串二进制位的动作;反过来把二进制位还原成汇编叫做解码(decoding)。
CPU 只认机器码。人类写汇编,编译器(或手算)负责编码——本章就是让你能手算这件事。
为什么必须会手算:408 里"给一条指令求机器码"或"给机器码反汇编"是常规题型;更重要的原因是——只有手算过一遍,才会真正记住"分支偏移量是以指令为单位"、"立即数是符号扩展"这类细节。这些细节靠背书是背不住的。
原理
一、编码要解决的三件事
| 步骤 | 内容 | 本篇对应考点 |
|---|---|---|
| ① 定格式 | 这条指令属于哪种格式(字段怎样划分) | R / I / J 三选一 |
| ② 填字段 | 每个字段该放什么值(寄存器号、立即数、偏移量) | 寄存器编号表、符号扩展、单位换算 |
| ③ 落内存 | 二进制怎么排进存储单元(字节序、对齐) | 大端小端、按字节编址 |
二、指令格式:定长与变长
| 类型 | 代表 | 取指难度 | 编码特点 |
|---|---|---|---|
| 定长 | RISC(MIPS32 恒 4 字节) | 低(地址永远 +4) | 字段位置固定,译码可并行 |
| 变长 | CISC(x86 1~15 字节) | 高(要先判断长度) | 编码紧凑,但需要复杂的长度译码器 |
定长格式的三种划分(MIPS32,见《语言 · 汇编与 C》/lang/01-isa.md):
R 型(寄存器-寄存器运算):
31 26 25 21 20 16 15 11 10 6 5 0
┌──────────┬───────┬───────┬───────┬───────┬────────┐
│ op=0 │ rs │ rt │ rd │ shamt │ funct │
│ 6 位 │ 5 位 │ 5 位 │ 5 位 │ 5 位 │ 6 位 │
└──────────┴───────┴───────┴───────┴───────┴────────┘
op 全 0, 真正的操作由 funct 决定 -> 提供 64 种运算
I 型(含立即数 / 访存 / 条件分支):
31 26 25 21 20 16 15 0
┌──────────┬───────┬───────┬────────────────────────┐
│ op │ rs │ rt │ immediate 16 位 │
│ 6 位 │ 5 位 │ 5 位 │ 立即数 / 偏移量 / 基址偏移 │
└──────────┴───────┴───────┴────────────────────────┘
J 型(无条件跳转):
31 26 25 0
┌──────────┬─────────────────────────────────────────┐
│ op │ target 26 位 │
└──────────┴─────────────────────────────────────────┘为什么 op 只用 6 位却能表达几百条指令:R 型的 64 种运算全部共用 op = 0,靠 6 位 funct 再细分。这是一种"两级操作码",与第 20 篇的扩展操作码同源——用二级字段扩充编码空间。
三、寄存器编号(编码时必须查)
| 编号 | 0 | 1 | 2~3 | 4~7 | 8~15 | 16~23 | 24~25 | 26~27 | 28 | 29 | 30 | 31 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 名字 | $zero | $at | $v0-$v1 | $a0-$a3 | $t0-$t7 | $s0-$s7 | $t8-$t9 | $k0-$k1 | $gp | $sp | $fp | $ra |
$t0=8、$t1=9、$t2=10、$sp=29、$fp=30、$ra=31 这几个必须张口就来。5 位字段
四、立即数:符号扩展还是零扩展
| 指令 | 扩展方式 | 例 |
|---|---|---|
算术类(addi、addiu) | 符号扩展(复制最高位填满高位) | addi $t0,$zero,-1 → imm 0xFFFF → 扩展成 0xFFFFFFFF |
逻辑类(andi、ori、xori) | 零扩展(高位全填 0) | ori $t0,$zero,0xFFFF → 0x0000FFFF |
访存类(lw/sw 的偏移) | 符号扩展(偏移可正可负) | sw $t1,-8($fp) → imm 0xFFF8 → 偏移 |
口诀:算术带符号、逻辑不带(这与《语言 · 汇编与 C》第 1 篇里的结论一致)。
负数怎么编:用补码。
这是手算编码时最容易错的一步:0x8008,而是 0xFFF8。
五、分支偏移量:单位是"指令"不是"字节"
I 型的 16 位立即数字段在条件分支里表示偏移量,但它数的不是字节,而是指令条数:
| 关键点 | 说明 |
|---|---|
| 基准是 | PC 已指向下一条指令(第 20 篇的相对寻址) |
| imm 单位是"条" | 因为指令恒 4 字节,省下 2 位编码空间 |
| 范围 | |
| imm 是有符号的 | 支持向前跳(循环) |
考试里两种问法都要会:
- 给"目标地址与当前地址",求 imm:
- 给"imm",求跳多远:跳 imm 条指令,即
字节
六、跳转 target:26 位怎么变 28 位
J 型的 target 只有 26 位,但地址是 32 位。还原规则:
| 步骤 | 说明 |
|---|---|
| ① 低位补 2 个 0 | 因为指令必然 4 字节对齐,低 2 位永远是 0,不必存 |
| ② 高位借 4 位 | 直接用 |
| ③ 反向(编码时) |
七、变长指令的编码(对照)
扩展操作码在内存里长什么样?用第 20 篇的 16 位方案举例:
| 指令 | 二进制 | 十六进制 | 占内存 |
|---|---|---|---|
| 三地址(OP=0x1,A=2,3,4) | 0001 0010 0011 0100 | 0x1234 | 2 B |
| 二地址(OP=0xF2) | 1111 0010 A1 A2 | 0xF2.. | 2 B |
| 零地址(OP=0xFFF4) | 1111 1111 1111 0100 | 0xFFF4 | 2 B |
变长的代价:CPU 取指时先把前 4 位取来,判断该指令的总长度,才知道要读几个字节、下一条从哪里开始。这就是为什么 x86 的取指单元特别复杂,也是 RISC 坚持定长的根本原因。
示例
例 1:编码 add $t0, $t1, $t2
完整计算过程:
第一步,判断格式。add 是寄存器间运算、无立即数 → R 型。
第二步,查各字段值:
| 字段 | 值 | 说明 |
|---|---|---|
op | 0 | 所有 R 型共用 |
rs | 9 | $t1 |
rt | 10 | $t2 |
rd | 8 | $t0(结果写这里) |
shamt | 0 | 非移位指令 |
funct | 0x20 | add 的功能码(表 1) |
⚠️ 注意 rs/rt/rd 的对应:MIPS 汇编写 add rd, rs, rt,即第一个操作数是 rd。所以 add $t0,$t1,$t2 里
第三步,按位拼装(每位对应一个位置):
op rs rt rd shamt funct
000000 01001 01010 01000 00000 100000第四步,转十六进制。按 4 位一组切:
校验一下:0x012A4020 的高 6 位是 0(R 型 ✓),funct 低 6 位是 100000 add ✓)。
例 2:编码 lw $t0, 4($sp)
完整计算过程:
第一步,判断格式。lw 带立即数(偏移 4)→ I 型。
第二步,查字段:
| 字段 | 值 | 说明 |
|---|---|---|
op | 0x23 | lw 的操作码(不是 0,与 R 型不同) |
rs | 29 | $sp(基址寄存器) |
rt | 8 | $t0(装入的目标) |
imm | 4 | 偏移量,符号扩展后为 |
⚠️ 访存指令的 rt 是"数据寄存器":lw $t0, 4($sp) 里 $t0 是 rt(存放读到的数据),$sp 是 rs(提供基址)。这与 R 型的三个寄存器位置完全不同。
第三步,拼装:
op rs rt imm
100011 11101 01000 0000000000000100第四步,转十六进制:
自检:高 6 位 100011 = 0x23 ✓。
例 3:编码 sw $t1, -8($fp)(负数偏移)
完整计算过程:
第一步,格式:I 型(sw 带偏移)。
第二步,字段:
| 字段 | 值 | 说明 |
|---|---|---|
op | 0x2B | sw |
rs | 30 | $fp |
rt | 9 | $t1(要写出的数据) |
imm | 符号扩展 |
第三步,把
第四步,拼装并转十六进制:
op rs rt imm
101011 11110 01001 1111111111111000对照要点:例 2 的 imm 是正的(0x0004),本例是负的(0xFFF8)。两个例子的高 6 位 op 也不同(0x23 vs 0x2B)——差一位就是"读"和"写"的区别,这是最好用的记忆抓手。
例 4:编码 beq $t0, $t1, 5(分支偏移以指令为单位)
完整计算过程:
第一步,格式:I 型(条件分支)。
第二步,字段:
| 字段 | 值 | 说明 |
|---|---|---|
op | 0x04 | beq |
rs | 8 | $t0(第一个比较数) |
rt | 9 | $t1(第二个比较数) |
imm | 5 | 偏移 5 条指令 |
第三步,拼装:
op rs rt imm
000100 01000 01001 0000000000000101第四步(考点!)——这个 5 到底是多远:
即:从本条指令起,向后跳过 6 条指令的位置(含本条算 0)。若本条在地址 0x00400000:
反面算(给地址求 imm):若本条在 0x00400000、目标是 0x00400018:
一句话总结:imm 数的是"指令条数",不是字节数。给字节要先除以 4。
例 5:编码 j 0x00400000(26 位 target 还原)
完整计算过程:
第一步,格式:J 型。
第二步,op = 0x02(j,也写作 jump)。
第三步,算 target(注意是取地址的第 2~27 位):
为什么除以 4:地址低 2 位恒为 0,不必存;右移 2 位就是"丢掉那两个 0",等价于除以 4。
第四步,拼装:
op target(26 位)
000010 00000100000000000000000000反向验证(解码):
注意:解码时高 4 位来自
例 6:反汇编——给机器码还原汇编
解码
0x012A4020与0x8FA80004。
完整计算过程:
(1)0x012A4020
第一步,转二进制并按 R 型切分:
0000 0001 0010 1010 0100 0000 0010 0000
op rs rt rd shamt funct
000000 01001 01010 01000 00000 100000第二步,读数:
| 字段 | 值 | 含义 |
|---|---|---|
op | 0 | R 型 |
rs | 01001 = 9 | $t1 |
rt | 01010 = 10 | $t2 |
rd | 01000 = 8 | $t0 |
shamt | 0 | 非移位 |
funct | 100000 = 0x20 | add |
第三步,按 op rd, rs, rt 组装:
与例 1 完全对应 ✓。
(2)0x8FA80004
第一步,取高 6 位判格式:0x8FA80004 >> 26 = 0x23 ≠ 0 → I 型。
第二步,切分:
1000 1111 1010 1000 0000 0000 0000 0100
op=0x23 rs=11101(29) rt=01000(8) imm=0x0004第三步,读数:
| 字段 | 值 | 含义 |
|---|---|---|
op | 0x23 | lw |
rs | 29 | $sp(基址) |
rt | 8 | $t0(目标) |
imm | 0x0004 = 4 | 偏移 |
第四步,组装:
与例 2 完全对应 ✓。
例 7:Python——编码与解码器(附完整验证)
REG = {'$zero':0,'$at':1,'$v0':2,'$v1':3,'$a0':4,'$a1':5,'$a2':6,'$a3':7,
'$t0':8,'$t1':9,'$t2':10,'$t3':11,'$t4':12,'$t5':13,'$t6':14,'$t7':15,
'$s0':16,'$s1':17,'$s2':18,'$s3':19,'$s4':20,'$s5':21,'$s6':22,'$s7':23,
'$t8':24,'$t9':25,'$k0':26,'$k1':27,'$gp':28,'$sp':29,'$fp':30,'$ra':31}
NAME = {v: k for k, v in REG.items()}
FUNCT = {0x20:'add',0x22:'sub',0x24:'and',0x25:'or',0x26:'xor',0x2a:'slt',0x00:'sll',0x08:'jr'}
OPC = {0x23:'lw',0x2b:'sw',0x08:'addi',0x04:'beq',0x05:'bne',0x0d:'ori'}
def enc_R(rs, rt, rd, sh, fn): return (rs<<21)|(rt<<16)|(rd<<11)|(sh<<6)|fn
def enc_I(op, rs, rt, imm): return (op<<26)|(rs<<21)|(rt<<16)|(imm & 0xFFFF)
def enc_J(tgt): return (0x02<<26)|((tgt>>2) & 0x03FFFFFF)
def decode(w):
op = w >> 26
if op == 0: # ---- R 型
rs,rt,rd = (w>>21)&31, (w>>16)&31, (w>>11)&31
sh, fn = (w>>6)&31, w&63
if fn == 0x08: # jr: 只有 rs 一个寄存器
return 'jr %s' % NAME[rs]
if fn in (0x00, 0x02, 0x03): # 移位类: rd, rt, shamt
return '%s %s, %s, %d' % (FUNCT[fn], NAME[rd], NAME[rt], sh)
return '%s %s, %s, %s' % (FUNCT[fn], NAME[rd], NAME[rs], NAME[rt])
if op == 0x02: # ---- J 型
return 'j 0x%08X' % ((w & 0x03FFFFFF) << 2)
rs,rt,imm = (w>>21)&31, (w>>16)&31, w & 0xFFFF # ---- I 型
sgn = imm - 0x10000 if imm & 0x8000 else imm # 符号扩展
if op in (0x04, 0x05): # 分支: imm 单位是"条"
return '%s %s, %s, %d (-> PC+4%+d 字节)' % (OPC[op], NAME[rs], NAME[rt], sgn, sgn*4)
if op in (0x23, 0x2b): # 访存: imm 是基址偏移
return '%s %s, %d(%s)' % (OPC[op], NAME[rt], sgn, NAME[rs])
return '%s %s, %s, %d' % (OPC[op], NAME[rt], NAME[rs], sgn)
cases = [
('add $t0,$t1,$t2', enc_R(REG['$t1'], REG['$t2'], REG['$t0'], 0, 0x20)),
('sub $t2,$t0,$t1', enc_R(REG['$t0'], REG['$t1'], REG['$t2'], 0, 0x22)),
('sll $t0,$t0,2', enc_R(0, REG['$t0'], REG['$t0'], 2, 0x00)),
('jr $ra', enc_R(REG['$ra'], 0, 0, 0, 0x08)),
('lw $t0,4($sp)', enc_I(0x23, REG['$sp'], REG['$t0'], 4)),
('sw $t1,-8($fp)', enc_I(0x2b, REG['$fp'], REG['$t1'], -8)),
('addi $t0,$zero,10',enc_I(0x08, REG['$zero'], REG['$t0'], 10)),
('beq $t0,$t1,5', enc_I(0x04, REG['$t0'], REG['$t1'], 5)),
('j 0x00400000', enc_J(0x00400000)),
]
print('%-20s %-12s %s' % ('汇编', '机器码', '反汇编回读'))
for asm, w in cases:
print('%-20s 0x%08X %s' % (asm, w, decode(w)))
print('\n分支偏移的两种口径 (beq ... ,5, 本条在 0x00400000):')
print(' imm = 5 (条) -> 目标 = PC+4+5*4 = 0x%08X' % (0x00400000 + 4 + 5*4))
print(' 反算: imm = (目标 - (PC+4)) / 4 = (%d - %d)/4 = %d'
% (0x00400018, 0x00400004, (0x00400018 - 0x00400004)//4))
print('\n-j 的 26 位 target 还原: target=0x%X -> (PC+4)高4位:target:00 = 0x%08X'
% ((0x08100000 & 0x03FFFFFF), ((0x08100000 & 0x03FFFFFF) << 2)))
print('负数立即数: -8 -> 16 位补码 0x%04X' % (-8 & 0xFFFF))
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照:九条指令的机器码与例 1~例 5 手算逐字符一致(add = 0x012A4020、lw $t0,4($sp) = 0x8FA80004、sw $t1,-8($fp) = 0xAFC9FFF8、beq = 0x11090005、j 0x00400000 = 0x08100000);反汇编回读与原始汇编一致;分支口径输出 0x00400018(= PC + 24);target 还原回 0x00400000;0xFFF8。
考点
考点
1. 必背结论
- 格式判定看
op:op = 0→ R 型;op ∈ {2, 3}→ J 型;其余 → I 型。 - R 型三段对应:
add rd, rs, rt—— 第一个操作数是 rd。三位写反是最常见错误。 - I 型的"rt 是谁"随指令而变:
lw $t0,4($sp)里 rt = 目标寄存器、rs = 基址;beq $t0,$t1,L里 rs、rt 是两个比较数。 - 负数用 16 位补码:
(不是0x8008)。 - 扩展方式:算术类符号扩展、逻辑类零扩展、访存偏移符号扩展。
- 分支偏移单位是"指令条数":
,范围目 标 。 - J 型 target 是"地址右移 2 位":
位 → 还原时低位补00、高位取 的高 4 位,只能在本 区域内跳。 - MIPS32 恒 4 字节(定长),所以取指地址永远
。
2. 高频陷阱
add的 rd/rs/rt 位置:汇编里"第一个寄存器是 rd",而不是 rs。手算时先把rd的位置空出来再填。- 分支偏移忘记"以指令为单位":给的是字节必须先除 4;给的是"跳 5 条"就不要乘 4 再乘 4。
- 分支的基准是
而不是 PC:与第 20 篇相对寻址同一个坑。 - J 型不能只凭 target 断地址:必须结合当前
的高 4 位。 - 负偏移编成"最高位置 1":
的 16 位补码是0xFFF8,不是0x8008。 sll/srl的 rs 字段是 0(移位量在shamt里,不是rs)。这是 R 型中唯一"用 shamt"的家族。andi/ori是零扩展,addi是符号扩展。同一段位型0xFFFF在两者下差 。- 访存指令
op不是 0:lw是0x23、sw是0x2B。它们属 I 型,不靠 funct。 - 字节序只在"字节访问 + 观察内存"时暴露:
lw/sw移动的是整字,与字节序无关;lb/sb有关。
3. 手算编码的固定五步(照做不会错)
① 定格式: 看有无立即数/是否跳转 -> R(无imm) / I(有imm) / J(无条件跳)
② 查 op 与 funct: op 决定大类, R 型再看 funct
③ 填寄存器号: 查编号表 (t0=8, t1=9, sp=29, fp=30, ra=31 ...)
④ 处理立即数: 负数取 16 位补码; 分支的 imm 单位是"条"不是"字节"
⑤ 按位拼 -> 每 4 位一组转十六进制; 用"高 6 位是否为 0"自检 R/I4. 与相邻章节的接口
- 第 20 篇:本篇是"指令格式与寻址方式"的具体落地——I 型的 imm 就是第 20 篇的"形式地址 A",
lw就是"基址+偏移"寻址。 - 第 21 篇:MIPS32 恒 4 字节、三格式、只有 Load/Store 访存,是 RISC 原则的活标本。
- 第 30 篇:数据通路要执行本篇的编码——取指周期取回的就是这 32 位,
op送控制器、rs/rt送寄存器堆地址端。 - 《语言 · 汇编与 C》:/lang/01-isa.md 给出完整 ISA 条款与寄存器约定,/lang/02-addressing.md 给出寻址方式的落地形式。408 主线讲通用理论与手算方法,lang 篇讲 MIPS32 的具体约定,两处互补不重复。
小结
- 编码就是把"汇编语义"塞进固定位置的位域:先定格式(R/I/J),再填字段,最后拼位。
op = 0是 R 型,真正的操作在funct(6 位给 64 种运算)——两级操作码与第 20 篇的扩展操作码同源。- R 型的
add rd, rs, rt:汇编第一个寄存器是 rd。这句话能挡掉一半的手算错误。 - I 型三个寄存器含义随指令变:
lw/sw是"基址 rs + 数据 rt";beq/bne是"两个比较数"。 - 立即数:负数取补码;算术符号扩展、逻辑零扩展。
- 分支偏移量以"指令条数"为单位,基准是
,范围 。 - 跳转 target 26 位,还原时补
00并借 的高 4 位,限本 区域。 - 定长 4 字节是 MIPS32 一切编码简化的根源——也是 RISC 与 CISC 在编码上的分水岭。
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。