MIPS汇编与机器码互转:从指令编码到Python实现 简介这份资源面向学习计算机组成原理、嵌入式开发及MIPS指令集的学生与开发者聚焦MIPS汇编语言与机器码之间的双向转换。包内提供可运行的汇编/反汇编工具及其Java源码帮助读者把助记符指令翻译成32位机器码或将二进制机器码还原为可读汇编从而理解寄存器、内存访问与控制流指令的底层实现。资源共5个文件包含1个jar可执行程序、1个java源代码、2张png界面或示例截图以及1份txt使用说明压缩包约81KB体积轻巧便于随取随用。目前已有351人学习下载。借助源码与说明读者可自行运行工具、对照示例验证汇编与机器码的对应关系并在此基础上调试和优化MIPS代码适合作为课程实验与自学指令集的辅助材料。1. MIPS 汇编与机器码互转从 Asm.zip 这个标题能拆出什么如果你手头拿到一个叫Asm.zip的包里面大概率是一套围绕 MIPS 指令集的汇编/反汇编工具链——它要解决的核心问题就一句话MIPS 汇编指令和 32 位机器码之间怎么互相翻译。往上走是汇编器把add $t0,$t1,$t2变成0x012A4020往下走是反汇编器把0x012A4020还原成可读指令中间还夹着机器码的字节序、指令编码格式、伪指令展开这些容易翻车的地方。这篇文章面向三类人做嵌入式逆向需要读 MIPS 固件的、写裸机或 RTOS 需要手算指令编码的、以及想自己实现一个最小 MIPS 汇编器/反汇编器的开发者。下面从指令格式讲起一路落到能跑的命令和参数最后给出验证方法和几个我踩过的坑。2. MIPS 指令编码三条格式和机器码的对应关系2.1 R/I/J 三种格式决定了机器码长什么样MIPS32 指令固定 4 字节但内部字段划分分三种格式。理解这三条格式是后面写汇编器和反汇编器的地基。格式31-2625-2120-1615-1110-65-0R 型opcode(0)rsrtrdshamtfunctI 型opcodersrtimmediate(16位)J 型opcodeaddress(26位)R 型指令 opcode 恒为 0真正决定操作的是最低 6 位 funct。比如add的 funct 是0x20sub是0x22jr是0x08。I 型用于带立即数或偏移的指令比如addi、lw、sw、beq。J 型只有j和jal26 位地址是字地址实际跳转时要左移 2 位再和 PC 高 4 位拼接。拿add $t0,$t1,$t2举例rs$t19rt$t210rd$t08shamt0funct0x20。拼起来就是000000 01001 01010 01000 00000 100000 0000 0001 0010 1010 0100 0000 0010 0000 0x012A4020这个手算过程就是汇编器最核心的一步。反汇编则是反过来先看 opcode 判断格式再按位段切出字段。2.2 寄存器编号和伪指令机器码里看不到的东西寄存器编号是固定的$zero0$at1$v0-$v12-3$a0-$a34-7$t0-$t78-15$s0-$s716-23$t8-$t924-25$k0-$k126-27$gp28$sp29$fp30$ra31。写汇编器时这张表要硬编码进去。伪指令是另一个关键点。move $t0,$t1在机器码层面根本不存在汇编器会把它展开成addu $t0,$zero,$t1。li $t0,0x12345678会展开成lui $t0,0x1234加ori $t0,$t0,0x5678。la、b、beqz、nop本质是sll $zero,$zero,0机器码0x00000000都是伪指令。反汇编器如果不做伪指令识别输出会是一堆addu $t0,$zero,$t1而不是move可读性差很多但语义完全正确。提示做反汇编时先输出真实指令再在展示层做伪指令合并不要把伪指令逻辑混进解码核心否则调试时很难定位。2.3 字节序同一个机器码为什么读出来是反的MIPS 支持大端和小端两种字节序。0x012A4020在大端下内存里是01 2A 40 20在小端下是20 40 2A 01。很多 MIPS 固件尤其是路由器、老式嵌入式设备用大端而一些 MIPS 开发板跑小端 Linux。反汇编时如果字节序搞反你会看到一堆莫名其妙的指令这是最常见的翻车点之一。判断方法看固件头部或 ELF header 里的EI_DATA字段或者直接两种都试一遍哪种能解出合理的指令序列就用哪种。写工具时字节序一定要做成可配置参数不要写死。3. 用 Python 写一个最小 MIPS 汇编器和反汇编器3.1 汇编器从指令文本到 32 位机器码下面是一个能处理 R 型和部分 I 型指令的最小汇编器核心。它不做伪指令展开只处理真实指令方便你先跑通编码逻辑。# mips_asm.py - 最小 MIPS 汇编器核心 # 只处理 R 型和 I 型真实指令不含伪指令 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 } # R 型指令: (funct, 是否需要 shamt) R_TYPE { add:0x20,addu:0x21,sub:0x22,subu:0x23, and:0x24,or:0x25,xor:0x26,nor:0x27, slt:0x2a,sltu:0x2b,sll:0x00,srl:0x02,sra:0x03, jr:0x08,jalr:0x09 } # I 型指令: opcode I_TYPE { addi:0x08,addiu:0x09,andi:0x0c,ori:0x0d,xori:0x0e, lw:0x23,sw:0x2b,lb:0x20,lbu:0x24,lh:0x21,lhu:0x25, sb:0x28,sh:0x29,beq:0x04,bne:0x05,slti:0x0a,sltiu:0x0b } def parse_reg(s): s s.strip() if s not in REG: raise ValueError(f未知寄存器: {s}) return REG[s] def asm_r(op, rd, rs, rt, shamt0): 编码 R 型指令 funct R_TYPE[op] code (0 26) | (rs 21) | (rt 16) | (rd 11) | (shamt 6) | funct return code 0xFFFFFFFF def asm_i(op, rt, rs, imm): 编码 I 型指令imm 为 16 位有符号 opcode I_TYPE[op] imm imm 0xFFFF # 截断到 16 位 code (opcode 26) | (rs 21) | (rt 16) | imm return code 0xFFFFFFFF def assemble_one(line): 解析一行汇编返回 32 位机器码 line line.split(#)[0].strip() # 去注释 if not line: return None parts line.replace(,, ).split() op parts[0].lower() if op in R_TYPE: if op in (sll, srl, sra): # 移位指令: op rd, rt, shamt rd parse_reg(parts[1]) rt parse_reg(parts[2]) shamt int(parts[3], 0) 0x1F return asm_r(op, rd, 0, rt, shamt) elif op in (jr, jalr): rs parse_reg(parts[1]) return asm_r(op, 0, rs, 0, 0) else: rd parse_reg(parts[1]) rs parse_reg(parts[2]) rt parse_reg(parts[3]) return asm_r(op, rd, rs, rt) elif op in I_TYPE: if op in (lw, sw, lb, lbu, lh, lhu, sb, sh): # 访存指令: op rt, offset(rs) rt parse_reg(parts[1]) mem parts[2] offset_str, reg_str mem.split(() imm int(offset_str, 0) rs parse_reg(reg_str.rstrip())) return asm_i(op, rt, rs, imm) else: # 其他 I 型: op rt, rs, imm rt parse_reg(parts[1]) rs parse_reg(parts[2]) imm int(parts[3], 0) return asm_i(op, rt, rs, imm) else: raise ValueError(f不支持的指令: {op}) if __name__ __main__: tests [ add $t0, $t1, $t2, addi $t0, $t1, 100, lw $t0, 4($sp), sll $t0, $t1, 2, jr $ra, ] for t in tests: code assemble_one(t) print(f{t:30s} - 0x{code:08X})这段代码的逻辑说明asm_r按 R 型格式把各字段移位拼接asm_i按 I 型格式拼接。assemble_one负责解析文本区分移位指令、跳转指令、访存指令和普通 I 型指令的语法差异。参数方面shamt只取低 5 位imm截断到 16 位这是硬件字段宽度决定的超出会静默截断实际工具里应该报错而不是截断。跑出来应该是add $t0, $t1, $t2 - 0x012A4020 addi $t0, $t1, 100 - 0x21280064 lw $t0, 4($sp) - 0x8FA80004 sll $t0, $t1, 2 - 0x00094080 jr $ra - 0x03E000083.2 反汇编器从机器码还原指令反汇编是汇编的逆过程先看 opcode 判断格式再切字段。# mips_disasm.py - 最小 MIPS 反汇编器核心 REG_NAME {v: k for k, v in REG.items()} R_FUNCT {v: k for k, v in R_TYPE.items()} I_OPCODE {v: k for k, v in I_TYPE.items()} def disasm_one(code): 把 32 位机器码还原成汇编文本 code 0xFFFFFFFF opcode (code 26) 0x3F rs (code 21) 0x1F rt (code 16) 0x1F rd (code 11) 0x1F shamt (code 6) 0x1F funct code 0x3F imm code 0xFFFF if opcode 0: # R 型 if funct not in R_FUNCT: return f.word 0x{code:08X} # 未知 R 型 funct0x{funct:02X} op R_FUNCT[funct] if op in (sll, srl, sra): return f{op} {REG_NAME[rd]}, {REG_NAME[rt]}, {shamt} elif op in (jr, jalr): return f{op} {REG_NAME[rs]} else: return f{op} {REG_NAME[rd]}, {REG_NAME[rs]}, {REG_NAME[rt]} elif opcode in I_OPCODE: op I_OPCODE[opcode] # 有符号立即数 simm imm if imm 0x8000 else imm - 0x10000 if op in (lw, sw, lb, lbu, lh, lhu, sb, sh): return f{op} {REG_NAME[rt]}, {simm}({REG_NAME[rs]}) else: return f{op} {REG_NAME[rt]}, {REG_NAME[rs]}, {simm} elif opcode in (0x02, 0x03): # J 型 op j if opcode 0x02 else jal addr (code 0x03FFFFFF) 2 return f{op} 0x{addr:08X} else: return f.word 0x{code:08X} # 未知 opcode0x{opcode:02X} if __name__ __main__: codes [0x012A4020, 0x21280064, 0x8FA80004, 0x00094080, 0x03E00008] for c in codes: print(f0x{c:08X} - {disasm_one(c)})逻辑说明disasm_one先提取 opcode为 0 走 R 型分支按 funct 查表否则查 I 型表再处理 J 型。simm那行做 16 位到有符号数的转换因为addi的立即数是有符号的而andi、ori是无符号的——严格来说这里应该按指令区分简化版统一按有符号处理实际工具要分开。参数上shamt和funct都是 6 位以下字段掩码不能写错。3.3 字节序处理读二进制文件时的关键一步从文件读机器码时字节序决定怎么拼成 32 位整数。import struct def read_words(data, endianbig): 从 bytes 按指定字节序读出 32 位字列表 fmt I if endian big else I words [] for i in range(0, len(data) - 3, 4): w struct.unpack(fmt, data[i:i4])[0] words.append(w) return words # 用法示例 # with open(firmware.bin, rb) as f: # data f.read() # for addr, w in enumerate(read_words(data, big)): # print(f{addr*4:08X}: {w:08X} {disasm_one(w)})struct.unpack的格式串里是大端是小端。这个参数必须暴露给用户不能写死。实际固件里还可能混有数据段反汇编出来是乱码很正常需要配合地址范围过滤。4. 避坑与排查MIPS 汇编/反汇编最常见的 5 个翻车点4.1 现象反汇编结果全是sll $zero,$zero,0原因字节序搞反了。0x00000000在大端小端下都是 0但其他指令会变成完全不同的值而 0 恰好解码成nop。如果你看到大段nop夹杂乱码先怀疑字节序。解决把read_words的endian参数换一个再试。或者看文件头ELF 的e_ident[5]是 1 表示小端2 表示大端。4.2 现象li $t0,0x12345678汇编报错或结果不对原因li是伪指令真实指令里没有。最小汇编器不处理伪指令就会直接失败。解决在汇编器里加一层伪指令展开把li拆成luiori。注意如果立即数高 16 位为 0只需要ori如果低 16 位为 0只需要lui。这个优化不做也不影响正确性但生成的机器码会多一条。4.3 现象beq跳转地址算出来差了好几条指令原因MIPS 分支指令的偏移是相对当前 PC4 的指令条数不是字节偏移也不是相对当前指令。很多人按字节算结果差 4 倍。解决编码时imm (target - (pc 4)) 2解码时target pc 4 (simm 2)。这个4是流水线延迟槽的历史遗留必须带上。4.4 现象lw的 offset 是负数时解析出错原因lw $t0, -4($sp)里-4要编码成 16 位补码0xFFFC解析时如果按无符号处理会变成 65532。解决编码时imm 0xFFFF解码时判断最高位做符号扩展。上面反汇编器里的simm那行就是干这个的。4.5 现象J 型指令跳转地址不对原因J 型只存 26 位实际地址是(pc4)的高 4 位拼接(addr 2)。如果目标地址跨越了 256MB 边界直接拼接会出错。解决编码时检查目标地址的高 4 位是否和pc4一致不一致就报错。解码时target ((pc4) 0xF0000000) | (addr 2)。这个边界情况在小型程序里不常见但写通用工具必须处理。5. 验证方法与一个实用技巧用已知指令做回归测试写完汇编器和反汇编器怎么确认它是对的我的习惯是建一组已知答案的测试用例覆盖三种格式和边界值每次改代码跑一遍。汇编文本期望机器码覆盖点add $t0,$t1,$t20x012A4020R 型基本sub $s0,$s1,$s20x02328022R 型 funct 不同addi $t0,$t1,-10x2128FFFFI 型负数立即数lw $t0,0($sp)0x8FA80000访存 offset0beq $t0,$t1,00x11090000分支 offset0j 0x004000000x08100000J 型地址编码nop0x00000000全零指令回归测试的写法很简单把上表存成列表逐条跑assemble_one比对再跑disasm_one看能否还原。双向都通过基本编码逻辑就没问题。一个实用技巧如果你手头有真实的 MIPS 工具链比如mips-linux-gnu-as和objdump可以用它生成标准答案来校准自己的实现。流程是写一个.s文件用工具链汇编成.o再用objdump -d反汇编把输出和自己的工具对比。差异处就是你的 bug。这个交叉验证比手算靠谱得多尤其是伪指令展开和重定位相关的部分。我自己在这个方向上最大的教训是不要一上来就追求支持全部指令。先把 R 型、I 型、J 型各挑两三条跑通把字节序、符号扩展、PC 相对偏移这三个最容易错的地方验证死再往上堆指令表。我第一版就是贪多结果beq的偏移算错调了一下午才发现是漏了4。先把骨架跑通再填肉这个顺序能省很多后悔药。希望帮到你。本文还有配套的精品资源点击获取