资讯动态

Apollo-11 AGC仿真复原:从指令集到制导任务的可观测执行内核

发布时间:2026/10/16 4:12:15 来源:尧图企业网站定制
1. 项目缘起与整体设计思路第一次看到“Apollo-11”这个标题很多人脑子里蹦出来的可能是那段著名的“个人一小步、人类一大步”。但在我这里它指的是一套我反复打磨过的历史指令与制导计算机AGC仿真与代码复原项目。说白了就是把当年那台算力还不如现在一块电子表、却把人送上月球的机载计算机用现代的方式重新跑起来、读明白、改得动。这个项目能做什么它能让你在本地把原始汇编指令一条条执行、观察寄存器变化、复现关键飞行阶段的制导逻辑它解决的是“历史代码看不懂、跑不起来、改不动”的问题适合对嵌入式、汇编、实时系统、航天软件工程感兴趣的从业者和硬核爱好者。我之所以选这个方向是因为现代软件工程里太多“抽象层”把底层遮得严严实实很多人写了五年代码都没见过寄存器长什么样。而AGC这套东西没有操作系统、没有内存管理单元、没有浮点单元一切靠裸机汇编和巧妙的数学技巧硬扛。把它啃下来你对“计算机到底怎么工作”的理解会上一个台阶。整个项目的设计思路可以概括成三句话先复原指令集再复现执行环境最后对齐历史任务阶段。这三步缺一不可顺序也不能乱因为指令集是地基执行环境是骨架任务阶段是血肉。在方案选型上我做过几轮取舍。第一轮我试过直接用现成的开源仿真器结果发现它们大多只关心“能不能跑”对内部状态的可见性做得很差调试时像隔着一层毛玻璃。第二轮我想自己从零写一个解释器但很快意识到指令集的边界情况太多纯手写容易漏。最终我采用的是**“参考公开的指令集文档 自建可观测执行内核 历史任务脚本驱动”**的组合方案。这个方案的优势在于执行内核完全可控每一条指令执行前后的寄存器、累加器、程序计数器都能打印出来同时任务脚本可以按历史时间线逐步推进方便对照公开的飞行阶段记录做验证。要避免的问题也很明确——不要一上来就追求“全指令集覆盖”那样会陷入细节泥潭正确的做法是先覆盖核心的算术、跳转、I/O指令把主流程跑通再逐步补齐边角指令。这里有个关键判断我想单独说为什么不用高级语言模拟而坚持用接近硬件的解释方式因为AGC的很多“坑”恰恰藏在硬件行为里。比如它的累加器是16位但实际有效位更少溢出处理有特定规则比如它的指令周期和内存访问时序强相关。如果你用Python的整数直接模拟这些细节全被抹平了跑出来的结果“看起来对”但一旦对照真实遥测数据就会发现偏差。所以我的执行内核里所有寄存器都严格按位宽和溢出规则实现宁可慢一点也要保证行为一致。实测下来这种“较真”在后期对齐任务阶段时省了无数排查时间。2. 核心细节解析与实操要点2.1 指令集复原从文档到可执行代码指令集复原是整个项目最枯燥但也最不能偷懒的环节。AGC的指令字长是16位但并不是所有位都用来表示操作码有些位是给寄存器地址、有些是给扩展操作码用的。我踩过的第一个坑就是想当然地按“操作码操作数”的经典格式去解析结果发现它的编码方式更接近“位域拼接”不同指令的操作码长度还不一样。解决办法是老老实实把指令集手册里的编码表逐条抄下来做成一张查找表用位掩码去匹配。具体操作上我建议你这样做先建立一个指令描述结构包含助记符、操作码掩码、操作码值、操作数位数、执行函数指针。然后用一个循环去匹配当前指令字。这里有个细节技巧——把高频指令放在查找表前面因为AGC程序里跳转和算术指令占比极高提前匹配能省不少时间。虽然现代机器不在乎这点开销但养成这个习惯对理解“为什么当年要这么设计”有帮助。注意指令集文档里有些指令在不同模式下行为不同比如某些算术指令在“溢出模式”下会触发中断。复原时一定要把这些模式标志位也实现进去否则跑复杂任务时会莫名其妙卡死。2.2 执行内核让每条指令都“看得见”执行内核是我花时间最多的地方也是这个项目最有价值的部分。核心要求就一个任何时刻都能回答“现在寄存器里是什么、下一条要执行什么、上一条干了什么”。为了实现这个我在内核里维护了一个完整的状态快照结构包括累加器、辅助寄存器、程序计数器、溢出标志、中断使能等。每执行一条指令先把快照存进环形缓冲区再执行执行完再存一次。这样调试时可以把最近几百条指令的状态变化全部回放出来。这里有个实操要点环形缓冲区的大小要可配置默认我设的是4096条。太小了排查长流程不够用太大了内存占用高且回放慢。4096这个值是我反复试出来的平衡点覆盖大多数子程序的完整执行轨迹绰绰有余。另外快照结构里我特意加了一个“指令来源地址”字段这样当程序跑飞时能立刻定位到是哪条跳转指令把控制流带偏的。还有一个容易被忽略的点是中断处理。AGC的中断不是现代意义上的“异常”而是硬件级别的信号跳转。我在内核里用了一个待处理中断队列每执行完一条指令就检查队列有中断就插入执行。这个设计让整个执行流程保持单线程、可预测避免了多线程带来的状态竞争问题。实测下来这种“伪并发”方式完全够用而且调试友好得多。2.3 任务脚本驱动把历史飞行阶段变成可执行序列光有指令集和执行内核还不够你得有东西去“驱动”它。我的做法是把公开的飞行阶段拆解成一系列任务脚本每个脚本是一串带时间戳的输入事件和预期状态检查点。比如“起飞后第30秒检查高度寄存器是否在预期范围内”。这样做的好处是整个仿真过程变成可重复、可验证的而不是“跑一遍看看”。写任务脚本时我建议你从最简单的阶段开始比如地面测试阶段先验证基本的输入输出和算术逻辑再逐步推进到制导、导航这些复杂阶段。每个脚本都要有明确的“通过/失败”判定条件不能靠肉眼观察。我早期偷懒没写判定条件结果后面阶段一多根本分不清是仿真错了还是我观察错了。后来补上自动判定效率直接翻倍。提示任务脚本里的时间戳不要用真实时间而要用“指令周期数”。因为仿真速度可快可慢用真实时间会导致结果不可复现。指令周期数是确定性的跑一万遍结果都一样。3. 实操过程与核心环节实现3.1 环境搭建与依赖准备先说环境。我用的是一台普通的开发机操作系统不限因为整个项目是跨平台的。核心依赖只有两个一个支持位操作的编程语言运行时一个用于可视化状态变化的轻量级界面库。语言我选的是C原因是位操作直接、性能可控、和底层打交道的直觉最接近。如果你更习惯Rust或Go也完全没问题只要能把位域和指针玩明白就行。搭建步骤我列一下你可以直接抄作业创建工作目录初始化版本管理。这一步别省后面改指令集实现时你会感谢自己。建立三个核心模块目录isa指令集、core执行内核、scripts任务脚本。在isa里先写指令描述结构体和查找表初始化函数。在core里写状态快照结构和执行循环。在scripts里写第一个最小任务脚本比如“加载一个数、加一、存回去”。这里有个参数选择我想展开说状态快照的存储方式。我试过两种一种是每执行一条指令就完整拷贝一份状态结构另一种是只记录变化字段。前者简单但内存占用大后者省内存但回放逻辑复杂。最终我选了前者因为4096条快照的内存占用在现代机器上完全可以接受而简单带来的调试便利性远超那点内存。算一下一个状态结构大概几十字节4096条也就几百KB忽略不计。3.2 指令执行循环的完整实现执行循环是整个项目的心脏我把它拆成四个阶段取指、译码、执行、写回。取指阶段从程序计数器指向的内存地址读出指令字译码阶段用查找表匹配指令并解析操作数执行阶段调用对应的执行函数写回阶段更新寄存器和标志位。这四个阶段之间我加了可选的钩子函数方便在任意阶段插入日志或断点。关键代码逻辑我用伪代码说明一下while (running) { snapshot_before capture_state(); instruction memory[pc]; decoded decode(instruction); if (decoded INVALID) { handle_invalid_instruction(); continue; } execute(decoded); snapshot_after capture_state(); ring_buffer_push(snapshot_before, snapshot_after); check_interrupts(); check_breakpoints(); }这段逻辑看起来简单但有几个细节决定成败。第一capture_state必须是无副作用的不能改变任何寄存器或标志位否则会污染执行结果。第二decode失败时不能直接崩溃要有明确的错误处理路径因为历史程序里可能存在未文档化的指令或数据被误当作指令执行的情况。第三check_interrupts要在写回之后、下一条取指之前执行这个时序不能乱否则中断响应会延迟一个周期导致时间相关的逻辑对不上。实测下来这个循环在普通机器上每秒能执行几百万条指令跑完一个完整任务阶段也就几秒钟。如果你觉得慢可以把日志钩子关掉性能还能再翻几倍。3.3 第一个可运行任务从“加一”到“制导”我建议你的第一个任务脚本就做最傻的事把一个数加载到累加器加一存回内存然后检查结果。这个脚本虽然简单但能验证取指、译码、执行、写回、内存访问全链路是否通畅。我当初就是靠这个脚本发现了内存地址映射的一个错误——我把指令内存和数据内存的地址空间搞混了导致存回去的数覆盖了后面的指令程序直接跑飞。跑通“加一”之后下一步是实现一个简单的循环比如从1累加到100。这个阶段你会遇到跳转指令和条件判断是验证控制流的关键。再往后就可以尝试复现历史任务里的制导计算了。制导计算涉及大量的三角函数和向量运算AGC用了一种叫“CORDIC”的迭代算法来算三角函数没有查表也没有浮点。我在实现这部分时专门写了一个CORDIC的独立测试脚本先验证算法本身正确再嵌入到任务流程里。这里分享一个实操心得每实现一个新指令或新算法都要写一个独立的单元测试脚本不要直接塞进大任务里跑。因为大任务一旦出错你很难判断是哪个环节的问题。独立测试脚本跑通了再集成出问题的概率会低很多。我早期图省事跳过了这步结果在一个大任务里排查了整整两天最后发现只是一个标志位没清零。4. 常见问题与排查技巧实录4.1 程序跑飞控制流异常排查程序跑飞是这类项目最常见的现象表现就是程序计数器跳到一个不该去的地方然后执行一堆无意义的指令。排查思路我总结成三步先看最后一条正常执行的指令再看跳转目标地址最后检查跳转条件。大多数跑飞都是因为跳转条件判断错了而条件判断错往往是因为标志位没正确更新。我遇到过一个典型案例一个条件跳转指令在累加器为零时应该跳转但实际没跳。查了半天发现前一条算术指令执行后零标志位确实置位了但我在写回阶段不小心把它清零了。原因是我在写回逻辑里加了一个“默认清零”的操作本意是防止标志位残留结果把该保留的也清了。解决办法是只清零那些明确需要复位的标志位其他标志位保持原样。这个坑很隐蔽因为大部分时候程序逻辑是对的只有特定条件组合才会暴露。注意如果你发现程序在某个特定输入下才跑飞大概率是标志位或边界条件问题。这时候把状态快照打开逐条对比正常和异常输入下的执行轨迹差异点就是问题所在。4.2 结果偏差数值计算精度问题AGC的数值计算有很多“近似”和“截断”如果你用现代浮点数去模拟结果会对不上。我早期就犯过这个错用双精度浮点算三角函数结果和预期值差了十万八千里。后来才明白AGC用的是定点数加迭代算法精度和舍入方式都和浮点完全不同。解决办法是完全按定点数规则实现包括乘法后的移位、加法的溢出处理、除法的迭代次数。这里有个参数需要计算定点数的小数位数。AGC里不同变量的小数位数不一样有的用单精度比如14位小数有的用双精度比如28位小数。我在实现时给每个变量都标注了精度运算时按最高精度对齐。这个工作量不小但一旦对齐结果就非常稳定。排查精度问题时我建议你从最简单的运算开始验证比如两个定点数相加再逐步到乘法、除法、三角函数。每步都和公开的预期值对比偏差超过阈值就停下来查。不要等到整个任务跑完才发现结果不对那时候排查范围太大了。4.3 常见问题速查表问题现象可能原因排查方法解决技巧程序计数器跳飞跳转条件错误或标志位异常查看最后正常指令和跳转目标检查标志位写回逻辑避免误清零数值结果偏差大浮点模拟代替定点运算对比单步运算结果按定点规则重实现标注精度执行速度突然变慢日志钩子或断点未关闭检查钩子开关状态生产运行时关闭所有调试钩子中断未响应中断队列检查时机错误查看中断检查在循环中的位置确保在写回后、取指前检查内存数据被覆盖指令和数据地址空间混淆打印内存访问地址范围严格分离指令内存和数据内存任务脚本不可复现使用真实时间而非周期数检查脚本时间戳类型统一改用指令周期数4.4 独家避坑技巧除了上面这些我再分享几个只有实际动手才会遇到的坑。第一个是指令字的字节序问题。AGC的指令字是16位但存储时高字节和低字节的顺序在不同文档里描述不一致。我一开始按一种顺序实现结果所有指令都译码错了。后来用一条已知指令的二进制表示去反推才确定正确的字节序。这个坑的教训是不要信文档的描述要用已知数据去验证。第二个是内存映射的边界。AGC的可用内存很小而且不同区域有不同用途有的只读、有的可写、有的映射到I/O。我早期没做边界检查结果程序写到了一个只读区域仿真器直接崩溃。后来加了访问权限检查非法访问时打印警告并跳过而不是崩溃。这样即使程序有问题仿真器也能继续跑方便定位。第三个是任务脚本的检查点设计。检查点不能太密也不能太疏。太密了脚本臃肿太疏了出问题定位不到。我的经验是在每个逻辑阶段结束时设一个检查点比如“制导计算完成”“姿态调整完成”。检查点里对比关键寄存器和内存值偏差超过容差就报错。容差怎么定我一般设成预期值的千分之一这个精度对大多数阶段够用也不会因为正常的舍入误差误报。5. 项目延展与个人体会这个项目跑通之后我做了几个延展觉得挺有意思。一个是把执行轨迹可视化用简单的折线图展示关键寄存器随时间的变化一眼就能看出程序在哪个阶段“抖”了一下。另一个是做指令集覆盖率统计看看哪些指令在历史任务里从没被用到哪些是高频指令。统计结果挺反直觉的——有些文档里重点介绍的指令实际任务里一次都没出现而一些看起来不起眼的跳转指令占了执行总量的三成以上。还有一个延展方向是对比不同版本的指令集实现。AGC的指令集在项目过程中有过修订有些指令的行为变了。我把不同版本的实现做成可切换的跑同一个任务脚本观察结果差异。这个对比让我对“硬件迭代如何影响软件行为”有了非常具体的感受。我个人在实际操作中的体会是这类复原项目最大的价值不在于“跑通”而在于逼你去理解每一个设计决策背后的约束。为什么用定点不用浮点因为当年没有浮点硬件而且定点在特定范围内精度更可控。为什么中断处理那么简单因为实时性要求高复杂的中断嵌套会引入不可预测的延迟。这些约束在现代开发里往往被抽象掉了但理解它们能让你在面临类似约束时做出更好的判断。最后再分享一个小技巧如果你也想做类似的项目从最小的可运行单元开始每一步都写测试每一步都做版本管理。不要想着一次性把整个系统搭好再跑那样大概率会卡在某个细节上出不来。我见过太多人兴致勃勃地开始然后在指令译码阶段就放弃了。慢慢来反而比较快。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑