尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
PA2指令系统实现:取指译码执行与框架代码阅读指南
简介一份面向计算机系统结构实验的PDF文档专门讲解指令系统的实现过程适合正在完成PA类指令实验、需要理清取指—译码—执行主线的本科生或自学者。文档从指令系统基础切入逐步展开框架代码解析覆盖fetchInstruction、decode、寄存器读写、译码模板以及ADD/SUB、MOV/NOP、栈操作、标志位更新等指令细节并延伸到打印变量与打印栈帧的实现思路。资源为单个PDF文件大小1.46MB已有1044人学习内容围绕实验中的关键难点组织包括寄存器组织、函数调用、栈帧变化与指令模板的对应关系能够帮助读者快速定位框架函数、理解操作数读取方式减少陌生代码的阅读成本。整份资料结构清晰、重点突出适合作为实验前的预习笔记与实验中的排错参考也可用于快速上手同类指令系统的框架代码。1. PA2 指令系统先把取指、译码、执行三个动作打通PA2 指令系统是整条实验链里第一个需要你亲手把框架代码读懂、把 opcode 表格填满的环节。我一开始以为只写几个执行函数就行结果卡在宏定义和三四个文件的引用关系上三天没动。这份资源真正有用的地方是它把取指、译码、执行的调用链拆开把 ADD、SUB、PUSH、CALL 每类指令的模板选择和 flags 更新时间点讲清楚还带上了打印变量和打印栈帧的实现思路。做完之后 opcode 表不再是黑匣子0x00 到 0xff 每一项都能对应到print_asm的汇编输出上。适合正在 PA2 卡壳、想补框架阅读方法、或者要对照符号表和栈帧打印做扩展的人。下面按我当时实际动手的顺序展开。2. 框架代码怎么读先找 location.h 的宏再理 exec_wrapper 的分发PA2(上) 的大部分工作量不在“写指令”而在“读懂框架”。这个实验的框架代码不是从头给你一个干净的 CPU而是把一个指令系统的骨架摆好留了一堆宏和函数让你填所以第一件事不是写代码是把宏展开关系摸清楚。2.1 取指、译码、执行的调用链先画出来常见的做法是每次 CPU 执行一条指令时都会走cpu_exec→exec_wrapper→ 查表 → 译码 → 执行这条路。我建议拿到框架先别读细节直接在注释里把这条链写出来static inline void exec_wrapper(void) { uint8_t opcode instr_fetch(cpu.eip, 1); // 取指读 1 字节并让 eip 顺序前进 seq_eip cpu.eip; // 备份顺序下一条地址跳转时会用到 OpcodeEntry *entry table[opcode]; // 以 opcode 为下标查指令表 entry-decode(); // 译码把操作数填进 id_dest / id_src entry-execute(); // 执行EHelper 宏展开成具体操作 print_asm(entry-name, id_dest, id_src); // 打印汇编方便后续单步对拍 }逻辑说明instr_fetch读一个字节后cpu.eip已经不是当前指令地址了而是指向下一条顺序指令。seq_eip就是把这个地址备份下来。普通指令执行完cpu.eip就停留在seq_eip框架自然进入下一条跳转类指令必须直接改cpu.eip否则会继续往下顺序执行。参数说明opcode是uint8_t所以table最多 256 项instr_fetch的第二个参数是读取长度本次实验的指令都是一个字节的 opcode所以传 1 就够。entry-name是给print_asm用的字符串不是执行逻辑的一部分但它在验证阶段特别有用。2.2 location.h 里的三类宏make_EHelper、make_DHelper、make_instr_helper框架代码里最关键的文件基本都叫location.h或者instruction.h里面封装了三类宏。我建议按“执行函数、译码模板、表项绑定”三组来记忆宏作用我一般怎么用make_EHelper(name)定义一个指令执行函数在里面写 RTL 操作如rtl_add、rtl_pushmake_DHelper(template)定义一种译码模板按操作数形式选模板如寄存器加立即数make_instr_helper(template, ehelper)把模板和执行函数绑成一个表项在指令表中填一行opcode 查表后自动配对比如我要实现一条“寄存器 1 字节立即数”的算术指令常见的做法是在decode.c里已经有类似rB_I1这样的模板名字的意思是“一个寄存器和 1 字节立即数”。我只需要在location.h里把这条指令绑定到rB_I1上译码的细节就不用自己写了。模板一旦确定print_asm打印出来的汇编格式也随之确定这就是为什么有些指令不需要单独写print_asm。有一点要注意不同版本的框架里模板命名不完全一样不要死记模板名。我一般先去decode.c里看make_DHelper的宏列表确认每个模板读多少字节、是否做符号扩展再回来填location.h。2.3 seq_eip 与 cpu.eip一个管顺序一个管跳转这个点我在前面代码里已经埋了伏笔但值得单独拿出来说因为它是 PA2 所有跳转指令出 bug 的根源。cpu.eip是真正的指令指针硬件靠它决定下一个取指位置seq_eip是“当前指令按顺序执行完后的下一条地址”。普通指令执行完框架不会额外去动cpu.eip它就停在seq_eip但 JMP、JE、JNE、CALL、RET 这些指令必须自己把cpu.eip改成目标地址或者栈上弹出的地址。框架里那个“顺序推进”的动作不会帮你做跳转你如果在跳转指令里用了seq_eip来赋值等于告诉 CPU “不要跳”。这个坑后面避坑章我会再展开。3. 从 ADD 到条件跳转指令模板选型与 flags 更新细节指令实现不是一条条孤立写的而是先看它属于哪一类操作数格式再决定用哪个模板。实现清单可以先列成一张表然后逐个补执行逻辑。3.1 先看指令清单哪些共用模板哪些要单独写指令典型模板注意点ADD / SUBrB_I1 / rB_I2要更新 OF/CF/SF/ZF/PF写回目标CMP同 SUB 的模板只更新 flags不写回目标MOVrB / rB_I1不更新 flagsPUSH / POP寄存器或内存操作数先改 esp 再读写栈顶JMP / JE / JNE1 字节相对偏移改 cpu.eip偏移做有符号扩展CALL / RET2 字节立即数 / 无操作数返回地址压栈存 seq_eipSETB / SETNE1 字节寄存器或内存条件成立写 1否则写 0从这张表能看出来模板选型决定了译码的字节数。同一个 opcode 段里一条指令可能按 ModRM 的 reg 位分成八个分支比如算术组里 ADD、OR、ADC、SBB、AND、SUB、XOR、CMP 就是共享同一批模板、由 reg 区分具体运算。所以实现时先确认这个区段有几个分支再决定是复用一个 EHelper 还是拆开写。3.2 flags 更新公式OF/CF/SF/ZF/PF 一次算全算术指令最容易漏的就是 flags。我一开始只更新了 ZF 和 SF导致条件跳转完全不按预期走。后来把 flags 更新统一抽成一个函数所有加减类指令都调它static inline void update_flags(uint32_t dest, uint32_t src, uint32_t result, bool is_sub) { uint32_t sign (dest ^ src ^ result); cpu.eflags.CF is_sub ? (dest src) : (result dest); cpu.eflags.OF ((~(dest ^ src) (dest ^ result)) 31) 1; cpu.eflags.ZF (result 0); cpu.eflags.SF result 31; uint8_t low result 0xff; int ones __builtin_popcount(low); cpu.eflags.PF (ones % 2 0); }逻辑说明所有 flags 的输入是“操作前的目标值 dest、操作数 src、操作后的结果 result”。CF 在减法里是借位标志判断条件是 dest 是否小于 src在加法里是进位标志判断条件是 result 是否小于 dest。OF 符号溢出公式在所有补码加减法里都通用只对最高位这一位有效所以结果是右移 31 位再与 1。参数说明is_sub决定 CF 的判据如果不传它CMP 和 SUB 会复用同一套逻辑但减法语义会错。ZF、SF、PF 是纯结果相关跟加减无关。PF 统计的是低 8 位里 1 的个数个数为偶数时置 1注意不是整个 32 位数里 1 的个数。3.3 CMP 与 SUB 的差异算完了但不写回CMP 的操作和 SUB 完全相同唯一的差别是结果不进目标寄存器。所以代码结构上可以把 SUB 的 EHelper 拆成“算 result”和“写回”两步CMP 只做前一步make_EHelper(sub) { uint32_t dest id_dest-val; uint32_t src id_src-val; uint32_t result dest - src; update_flags(dest, src, result, true); operand_write(id_dest, result); // SUB 写回目标 } make_EHelper(cmp) { uint32_t dest id_dest-val; uint32_t src id_src-val; uint32_t result dest - src; update_flags(dest, src, result, true); // 没有 operand_write }这里的 id_dest 和 id_src 是译码阶段填好的操作数结构id_dest-val已经是寄存器或内存里取出的实际值而operand_write(id_dest, result)会根据操作数类型决定写寄存器还是写内存。CMP 不调用写回这就是它对 flags 有影响但不动数据的原因。3.4 条件跳转和 SETccZF/CF 如何决定下一步JE 和 JNE 只看 ZFSETB 只看 CF。跳转类指令的核心是“偏移量有符号扩展”和“写 cpu.eip 而不是 seq_eip”make_EHelper(je) { if (cpu.eflags.ZF 1) { cpu.eip (int8_t)id_src-val; // 偏移量必须按有符号扩展 } // ZF 为 0 时不改 cpu.eip自然顺序执行 } make_EHelper(setne) { uint32_t val (cpu.eflags.ZF 0) ? 1 : 0; operand_write(id_dest, val); }SETcc 这一类指令和跳转原理几乎一样只是把条件结果写到 1 字节目标里。实验里如果没用到部分条件把对应的 CF/ZF/OF 置零即可不需要额外实现整套逻辑。需要特别注意的是(int8_t)id_src-val这个强制转换如果偏移量读出来是 0xFE直接当无符号数 254 去加跳到天边去了转成 int8_t 之后就变成 -2往回到正确位置。4. 从 PUSH 到打印栈帧栈上数据链与符号表匹配栈相关的指令是 PA2(上) 第二个坎。PUSH/POP 本身不难难的是 CALL/RET 的返回地址链以及打印栈帧时要靠返回地址反查函数名。这条链一旦理顺打印变量和栈帧就只是符号表遍历的问题。4.1 PUSH/POP入栈先减栈顶出栈先读再增x86 的栈向低地址增长所以入栈的第一步一定是esp - 4然后才把值写入esp指向的内存。顺序反了会把数据写到旧栈顶上面覆盖掉返回地址。常见实现如下make_EHelper(push) { cpu.esp - 4; vaddr_write(cpu.esp, 4, id_dest-val); } make_EHelper(pop) { uint32_t val vaddr_read(cpu.esp, 4); cpu.esp 4; operand_write(id_dest, val); }vaddr_write的三个参数分别是地址、长度、数据。PUSH 的id_dest-val来自译码阶段如果是PUSH eax它就是 eax 的值。POP 则是从栈顶读 4 字节再写回目标寄存器。注意operand_write在这里会自动按目标宽度写比如pop al只写低 8 位。4.2 CALL/RET返回地址压的是 seq_eip不是 cpu.eipCALL 要先把下一条指令地址压栈然后跳转。这里最容易写错的就是把cpu.eip压进去。因为instr_fetch之后cpu.eip已经指向顺序下一条了理论上它和seq_eip相等但 CALL 还需要读 2 字节立即数读完后cpu.eip已经越过立即数区域如果这时候把cpu.eip压栈RET 返回时会落在立即数中间。make_EHelper(call) { uint32_t ret_addr seq_eip; // 指向 CALL 下一条真正的指令 cpu.esp - 4; vaddr_write(cpu.esp, 4, ret_addr); cpu.eip id_dest-val; // 跳到目标地址 } make_EHelper(ret) { uint32_t ret_addr vaddr_read(cpu.esp, 4); cpu.esp 4; cpu.eip ret_addr; }这就是为什么我在第 2 章反复强调seq_eip。CALL 里压栈的返回地址是译码完成时按顺序算出的下一条指令地址不是某个寄存器当前值。RET 反向操作从栈顶弹出后就地修改cpu.eip这一改主循环下一次取指就直接到调用者的下一条指令了。4.3 打印变量遍历 ELF 符号表拿 st_name 去字符串表匹配实验要求支持打印变量本质是在 ELF 的符号表里找名字匹配的符号然后按符号地址读内存。ELF 里st_name是一个偏移量真正名字在字符串表里偏移量加在字符串表基地址上才是可读字符串。常见做法如下void print_var(char *var_name) { int sym_num elf_get_num_symbols(); bool found false; for (int i 0; i sym_num; i) { Elf32_Sym *sym elf_get_symbol(i); char *name (char *)(symtab_base sym-st_name); if (strcmp(name, var_name) 0 ELF32_ST_TYPE(sym-st_info) STT_OBJECT) { uint32_t addr sym-st_value; uint32_t val vaddr_read(addr, sym-st_size); printf(%s %u\n, var_name, val); found true; break; } } if (!found) printf(No such variable\n); }逻辑说明符号类型STT_OBJECT过滤掉函数符号避免把函数名当变量打印。st_size是变量字节数vaddr_read(addr, st_size)统一读入实验里通常是 4 字节的 int值直接打印即可。参数说明不同框架封装了不同接口有的叫elf_get_symbol有的直接暴露symtab和strtab两个数组核心字段都是一样的。注意形参不会出现在全局符号表里因为形参没有分配静态空间它只活在栈上靠 ebp 偏移访问这正是思考题里“消失的符号”的答案。4.4 打印栈帧用返回地址反查函数名沿 ebp 链回退打印栈帧backtrace的关键是理解调用者栈帧布局ebp指向当前帧底部[ebp]存调用者的 ebp[ebp4]存返回地址。所以回退是循环读取而不是递归void print_backtrace(void) { uint32_t ebp_val cpu.ebp; for (int depth 0; ebp_val ! 0 depth 32; depth) { uint32_t ret_addr vaddr_read(ebp_val 4, 4); char *name find_func(ret_addr); printf( #%d 0x%08x in %s\n, depth, ret_addr, name); ebp_val vaddr_read(ebp_val, 4); // 跳到调用者的 ebp } }find_func的实现思路是遍历符号表找满足addr st_value addr st_value st_size的STT_FUNC符号返回它的名字。左闭右开很关键避免返回地址恰好落在相邻函数开头时归属错。这个循环我加了深度上限 32防止某个测试程序栈帧链被破坏后无限回退刷屏。5. 避坑排查五个我在 PA2(上) 翻过的车5.1 寄存器访问宽度不一致结果算对但寄存器没变现象ADD 指令执行后用info reg查看 eax值还是旧的但单步调试里 result 明明算对了。原因我直接给cpu.eax赋值而模板在译码阶段是按id_dest-width读操作数的。如果你的目标操作数是 1 字节模板读的是al映射的位置你写 32 位寄存器等于跨宽度写低 8 位被框架后续按位域读回时覆盖了。解决统一走operand_write(id_dest, result)它会根据id_dest-width自动选reg_b、reg_w、reg_l宏写对应宽度。实在要手动写寄存器也必须先看模板里操作数的宽度再选对应的访问宏。5.2 跳转偏移量没做有符号扩展往回跳直接跳到高位地址现象JMP 往前跳的时候eip 一下变成 0xFFFF 开头的地址程序直接跑到未定义区域触发 invalid opcode。原因译码读出来的偏移是uint8_t0xFE 被当成 254 处理而不是 -2。JMP 的偏移在 x86 里是相对当前指令的带符号数负偏移表示往回跳。解决cpu.eip (int8_t)id_src-val;先把 8 位无符号转成有符号 char再做加法时自动符号扩展成 32 位。这个强制转换是我踩过最值的坑也是思考题大端机器读立即数的前置概念。5.3 跳转指令里用了 seq_eip导致条件跳转永远顺序执行现象JE 条件满足时没有跳转程序照常往下跑单步看寄存器ZF 确实是 1eip 就是不动。原因我在 EHelper 里写的是cpu.eip seq_eip;等于把跳转目标覆盖成顺序下一条地址。seq_eip这个变量是框架在每次执行前备份的顺序地址不是真正的“目标地址”。解决所有跳转类指令统一改cpu.eip不要碰seq_eip。JMP、JE、JNE、CALL、RET 一律是cpu.eip 目标值或者cpu.eip 偏移框架主循环自然会从那继续取指。5.4 flags 更新拿错了操作数条件判断反着来现象CMP 后 JE 跳转结果和汇编语义完全相反SUB 后 ZF 总是 0哪怕两个寄存器相等。原因我在算 ZF 时用了执行后的 dest 值而 dest 已经被改掉了比如dest - src之后才去调 update_flags传进去的 dest 是结果而不是操作前原值。CF 的加法判断result dest也依赖“操作前 dest”一旦 dest 被覆盖整个 flags 全错。解决执行函数开头先把id_dest-val存到局部变量算完结果后再调update_flags(原dest, src, result, is_sub)。这个顺序不能省我后来把所有算术指令都改成“先取原值 → 算结果 → 更新 flags → 写回”四步模板再没出过这类问题。5.5 大端机器读立即数两字节顺序反了现象把vaddr_read(addr, 2)改成逐字节读立即数后0x1234 被读成 0x3412程序行为全部错乱。原因小端机器上低字节在低地址我逐字节循环读到第一个字节后直接imm | b把它放到了高位第二个字节又放到了低位等于按大端重组了。解决要么直接用框架封装好的vaddr_read(addr, 2)它内部已经按小端拼好要么自己拼时写成imm | (uint32_t)b (8 * i)i 表示第几个字节。如果你要模拟大端机器反过来从高位字节开始循环即可这也是思考题里“遍历读入再反过来重组”的实操版本。6. 验证方法单步对拍、断点卡变量、跑回归测试6.1 单步对拍让 NEMU 的 eip 和 objdump 输出逐行对上每次实现完一组指令我一定是先进 NEMU 单步跑一个短程序然后对照反汇编检查 eip。流程是先用交叉编译器编一个测试程序比如一个简单的int main() { return 0; }然后查看它的汇编riscv32-linux-gnu-objdump -d /path/to/test /tmp/test.asm # 版本可能不同按你的工具链来 ./nemu --batch # 或者 make debug具体入口看 Makefile接着在 NEMU 里用si单步每执行一步就用info r看 eip 落在哪个地址再和/tmp/test.asm里该地址的指令对照。如果 NEMU 的print_asm输出和 objdump 不一致基本就是译码模板选错或返回字节数不对。这一步能快速暴露 5.3 这类 seq_eip 问题因为 eip 序列一对上跳转是否生效立刻可见。6.2 打印变量和栈帧的回归验证打印变量是很容易做回归的先用p打印一个局部变量再反汇编看它的栈偏移手动算一遍值对不对。打印栈帧就更有意思我习惯写一个三层嵌套函数在里面手动设断点然后bt看栈帧深度是不是三层、每层函数名是否和源码一致。从那以后我每次接触新框架都强制先画调用链图再动手写指令哪怕只是在注释里把 exec_wrapper、table、decode 的关系列一遍。这个习惯帮我少走了很多弯路也希望你在这份资源上能少踩几个我踩过的坑。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

金融级分布式数据库选型与落地:从市场报告到生产压测的实战指南

金融级分布式数据库选型与落地:从市场报告到生产压测的实战指南

简介:这份报告由沙利文联合头豹研究院发布,聚焦2024年中国金融级分布式数据库市场,面向金融行业从业者、数据库供应商、政策制定者、研究机构及投资者。内容涵盖行业发展背景、安全可靠测评名单分析、厂商技术动态与生态动态、市场容量与份额…

📅 2026/10/11 17:41:52
C#实现Excel实时导入SQL Server:NPOI+SqlBulkCopy完整方案

C#实现Excel实时导入SQL Server:NPOI+SqlBulkCopy完整方案

有段时间我接到一个需求:业务部门每天把Excel报价单丢进一个共享目录,希望系统能把新数据自动读进SQL Server,全程不靠人点按钮。听到这个需求,我的第一反应是“读个Excel写个库而已”,真动手才发现,容易的…

📅 2026/10/11 17:41:52
Coze微信机器人私聊与群聊双模落地实战

Coze微信机器人私聊与群聊双模落地实战

简介:这是一份面向AI开发者与技术爱好者的实战型教程,聚焦于利用Coze平台快速构建可落地的微信私聊及群聊智能机器人,解决个人或小团队在日常咨询、知识问答、社群运营等场景中的自动化响应需求。资源为单文件PDF手册,共1个1.66MB…

📅 2026/10/11 17:41:52
MORE NEWS

更多资讯

📰

免费本地AI绘图:MeiGen AI Design MCP接入ComfyUI完整教程,GPU零成本出图

【免费下载链接】MeiGen-AI-Design-MCP Supports GPT Image 2, Seedance & ComfyUI, with a 1,400 prompt library, carefully crafted hooks and a multi-task orchestration system 项目地址: https://gitcode.com/gh_mirrors/me/MeiGen-AI-Design-MCP 点击查…

📰

YCBlogs 算法笔记:从上往下打印二叉树——队列实现层序遍历(BFS)原理与实战

教程技术博客文档 【免费下载链接】YCBlogs 技术博客笔记大汇总,包括Java基础,线程,并发,数据结构;Android技术博客等等;常用设计模式;常见的算法;网络协议知识点;部分fl…

📰

性能测试指标体系详解:从RT、TPS到系统资源与稳定性

1. 先立个框架:性能指标不是一个点,而是一条链路1.1 别急着看数值,先回答“这些指标给谁看”我做了这么多年性能测试,发现最容易翻车的不是不会压测,而是拿到结果不知道该怎么解读。你给领导汇报,说TPS有12…

📰

PO模式+数据驱动:打造低维护成本的UI自动化测试框架

写自动化测试的朋友应该都经历过这些:用例写起来一时爽,维护起来火葬场。前端页面刚改了个按钮的class,测试脚本跟着红一片,定位不到元素、用例全挂。这种情况多了以后,团队里难免出现一种声音——自动化到底值不值得搞…

📰

用LangChain和Playwright构建大模型驱动的测试智能体

把大模型接进自动化测试这件事,我之前一直持保留态度。直到用LangChain把Playwright包成一个能自己看页面、自己点按钮、自己写断言的测试智能体之后,我才意识到传统的UI自动化写法确实到了该升级的时候。这篇文章就把我搭建这个测试智能体的完整思路、核…

📰

西门子Teamcenter仿真体系建设:构建自动驾驶研发数字主线

简介:本资源系统阐述西门子在自动驾驶与智能网联汽车领域的仿真开发方法论与平台化体系建设路径,面向汽车电子工程师、ADAS/自动驾驶算法开发者、仿真流程架构师及PLM系统实施人员,解决多学科协同难、仿真数据分散、V模型全流程工具链割裂、C…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬