3.4 手工编码与解码一条指令实战


3.4 手工编码与解码一条指令实战

本节摘要:这是编码车间的总装台。本节不带汇编器,纯用格式表把四条 RISC-V 指令与两条 x86 指令从零装配成机器字节,再反向解码验收,最后故意制造一个方向错位并当场修复。读完你应当获得全册的核心手艺——位级对账,这也是进入第 5 章解码车间前的入场券。

演练场地与工具

需要的装备只有三样:一张 RISC-V 基本格式速查表(下面就有)、一支笔、一个免费的反汇编工具或在线反汇编站点用于验收。全程约需要一段完整的空闲时间,请把它当成一次真正的装配作业而不是阅读——纸面上的位串必须亲手誊写与相加,眼过千遍不如手过一遍。

先把速查表钉在案头。寄存器编号按 ABI 名对应:zero=0,ra=1,sp=2,a0=10,a1=11,a2=12,t0=5,s0=8。RISC-V 各格式的字段从高位到低位排列如下:

R 型: funct7 rs2 rs1 f3 rd opcode I 型: imm[11:0] rs1 f3 rd opcode S 型: imm[11:5] rs2 rs1 f3 imm[4:0] opcode B 型: imm[12] imm[10:5] rs2 rs1 f3 imm[4:1] imm[11] opcode 常用操作码:R 型 0110011 I 运算 0010011 访存 load 0000011 store 0100011 分支 1100011 jal 1101111

第一批:RISC-V 四连装

指令一:add a0, a1, a2(R 型)。字段清点:funct7=0000000,rs2=a2=12=01100,rs1=a1=11=01011,f3=000,rd=a0=10=01010,opcode=0110011。按位拼接:

0000000 01100 01011 000 01010 0110011 → 0000 0000 1100 0101 1000 0101 0011 0011 → 0x00C58533

指令二:addi sp, sp, -16(I 型)。立即数按补码:-16 的 12 位表示是 111111110000。字段:imm=111111110000,rs1=sp=2=00010,f3=000,rd=sp=2=00010,opcode=0010011:

111111110000 00010 000 00010 0010011 → 1111 1111 0000 0001 0000 0001 0001 0011 → 0xFF010113

这是所有函数序言里最高频的一条指令——栈指针下压 16 字节。你会在无数反汇编输出的开头见到 ff010113,今天之后你不仅认识它,还能拆穿它。

指令三:sw a0, 4(sp)(S 型)。立即数 4 要拆两段:高段 imm[11:5]=0000000,低段 imm[4:0]=00100。字段:imm[11:5]=0000000,rs2=a0=01010,rs1=sp=00010,f3=010,imm[4:0]=00100,opcode=0100011:

0000000 01010 00010 010 00100 0100011 → 0000 0000 1010 0001 0010 0010 0010 0011 → 0x00A12223

注意 S 型的陷阱:立即数不是连续一段,而是首尾两段,中间夹着 rs2、rs1、f3。第一次手工装配十有八九在这里出错。

指令四:beq a0, a1, +16(B 型)。立即数 16 = 二进制 1 0000 0000 0000 后五位是 10000:imm[12]=0,imm[10:5]=000000,imm[4:1]=1000,imm[11]=0。字段拼接:

0 000000 01011 01010 000 1000 0 1100011 → 0000 0000 1011 0101 0000 1000 0110 0011 → 0x00B50863

B 型比 S 型更碎:立即数被切成三段,且不含最低位(分支目标必须按 2 字节对齐,最低位恒零干脆不编码)。装配完别急着看下一题,先把四个十六进制值抄到验收环节去。

第二批:x86 两连装

指令五:mov eax, 0x1234。查表:寄存器立即数装载占 B8 到 BF 一带,eax 对应 B8,后随 32 位小端立即数:

B8 34 12 00 00

指令六:add eax, ebx。操作码 01 是"寄存器加到寄存器/内存";ModRM 需要自填:mod=11(寄存器对寄存器),reg=ebx=011(作为源),rm=eax=000(作为目标)——11 011 000 = 0xD8:

01 D8

两字节搞定,这就是 2.1 节说的 CISC 密度优势。同时请记住 reg 与 rm 的方向语义:操作码换成 03(方向反转)时,ebx 与 eax 的角色就互换。方向位是 x86 手工编码的第一大坑。

验收与解码回程

把六个值送进反汇编工具:0x00C58533 应还原为 add a0,a1,a20xFF010113 还原为 addi sp,sp,-160x00A12223 还原为 sw a0,4(sp)0x00B50863 还原为 beq a0,a1,16B8 34 12 00 0001 D8 分别还原为两条 x86 指令。任何一条对不上,回到拼接步骤逐段核对——九成错误出在立即数拆段。

再来一道纯解码题检验回程能力。拿到陌生字节 67 80 00 00,按小端拼成 0x00008067:低 7 位 1100111 是 jalr;f3=000;rs1=00001(ra);rd=00000(zero);立即数为 0。语义:跳到 ra 里的地址,把返回地址写进 zero(等于丢弃)。这就是 ret 的真身——你在 1.1 节见过的老朋友,现在你能徒手拆它了。

事故演练:一次方向错位

最后故意制造一次真实事故。目标是把 ebx 写进 eax 指向的内存(即 mov [eax], ebx),正确做法是操作码 89 加 ModRM:mod=00(无位移)、reg=ebx=011(源)、rm=eax=000(目标内存),编码 89 18。假设你顺手写成了 8B 18——操作码 8B 是"读进寄存器"方向,同一份 ModRM 18 立刻被解释成"把 eax 指向的内存读进 ebx"。两个字节之一写反,语义完全颠倒:本想存数,实际在覆盖 ebx。更阴险的是这类错误不会报错:它是合法指令,反汇编器欣然通过,只有跑起来数据不对才会暴露。教训:x86 的 89 与 8B 像双向阀门,装配时先默念数据流向,再落操作码。

本节要点回顾

  • 手工装配五步:拆语义、选格式、清点字段、按位拼接、十六进制定稿;解码回程是同一张表的逆向使用。
  • 立即数拆段是错误重灾区:S 型切两段、B 型切三段且不含零位——拼接时先画格子再填位。
  • 高频指令值得背下字节形态:函数序言 0xFF010113、返回 0x00008067,见过即能秒解。
  • x86 的方向位(89 与 8B)是第一大坑:错位不报错、语义全颠倒,落码前先默念数据流向。
  • 位级对账手艺的验收标准:给你任意合法字节,能不查工具拆出字段;给你任意汇编,能独立装出字节。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U