☰
从手写汇编到形式化验证:NASA 软件工程 50 年变迁,阿波罗 11 号的遗产还剩多少
2026/10/20 1:58:12 网站建设 项目流程

从手写汇编到形式化验证:NASA 软件工程 50 年变迁,阿波罗 11 号的遗产还剩多少

【免费下载链接】Apollo-11Original Apollo 11 Guidance Computer (AGC) source code for the command and lunar modules.项目地址: https://gitcode.com/GitHub_Trending/ap/Apollo-11

2019 年 7 月 20 日,人类登月 50 周年纪念日前后,一份来自 1969 年的源代码仓库被推上 GitHub Trending 头条,连续数日霸榜,随后又被蜂拥而至的中文灌水帖淹没——这个插曲本身就是一段颇具戏剧性的开源生态注脚。这份代码,正是阿波罗 11 号制导计算机(Apollo Guidance Computer,AGC)的原始汇编固件:指令舱侧的 Comanche055 与登月舱侧的 Luminary099。

半个多世纪过去,NASA 的飞行软件早已从 15 位字长的手工汇编,走到了 C++ 组件框架与形式化验证的时代。但当我们逐行翻阅这份 AGC 源码,会发现今天航天软件栈的许多"现代"做法,其实都能在 1969 年的注释里找到原型。本文以仓库源码为证据,回答一个问题:阿波罗 11 号的软件遗产,究竟还剩多少?

一、一台 1.024 MHz 的"机器",两套手工汇编固件

先看这份代码的物质基础。AGC(Block II)采用 15 位字长、二进制补码、单精度加双精度运算,时钟仅 1.024 MHz,核心内存约 36K 字的固定存储(电磁线只读存储器)加 2K 字可擦写存储——合计约 64KB,其中绝大多数被这套飞行软件占满。指令舱程序 Comanche 055(代号 Colossus 2A,1969 年 4 月 1 日装配)与登月舱程序 Luminary 099(代号 Luminary 1A,1969 年 7 月 14 日装配)加起来约 14.5 万行,是后来流传最广的一个规模数字。

这批源码最反直觉的特征是构建方式。以 Luminary099/MAIN.agc 为例,整个程序就是一个由$指令把几十个文件"拼"起来的单体:

$ASSEMBLY_AND_OPERATION_INFORMATION.agc # pp. 1-27 $TAGS_FOR_RELATIVE_SETLOC.agc # pp. 28-37 $ERASABLE_ASSIGNMENTS.agc # pp. 90-152 ... $INTERPRETER.agc # pp. 1002-1094 $EXECUTIVE.agc # pp. 1103-1116 $WAITLIST.agc # pp. 1117-1132

Luminary099/README.md 直言不讳:"The original development team had no linker. The builds were monolithic just like this."当时的"复用"是把你的穿孔卡片垛插到别人的卡片垛里。没有链接器、没有动态内存管理、没有调试器,所有符号、地址、银行切换全靠人在注释里约定。这不是年代局限,而是一种被资源逼出来的工程纪律。

作为对比,现代航天软件栈已经完全换血。NASA 主流的 cFS(core Flight System)是模块化 C 框架,飞行软件被拆成可独立编译、单元测试的组件;JPL 的 F Prime(fprime)则干脆用 C++ 和一门专门的 FPP 配置语言来声明组件、端口与拓扑,构建、测试、部署都有工具链支撑。从"一个巨型汇编文件"到"组件化 + 自动生成的拓扑代码",工具链的复杂度上升了不止一个数量级。

二、在 64KB 里搭出实时操作系统:EXECUTIVE、WAITLIST 与解释器

如果说单体构建是"土办法",那 AGC 的调度内核则是货真价实的早期 RTOS。三个模块构成了它的运行时骨架:EXECUTIVE(作业优先级调度)、WAITLIST(定时任务)、T3/T4/T5/T6中断引导(见 Comanche055/INTERRUPT_LEAD_INS.agc)。

Comanche055/EXECUTIVE.agc 提供了完整的作业生命周期原语:NOVAC/FINDVAC入队、CHANG1/CHANG2挂起、PRIOCHNG动态改优先级、JOBSLEEP/JOBWAKE睡眠唤醒、ENDOFJOB出队。典型的入队代码长这样:

NOVAC INHINT AD FAKEPRET # LOC(MPAC +6) - LOC(QPRET) TS NEWPRIO # PRIORITY OF NEW JOB + NOVAC C(FIXLOC) EXTEND INDEX Q # Q WILL BE UNDISTURBED THROUGHOUT. DCA 0 # 2CADR OF JOB ENTERED. DXCH NEWLOC

而在 Luminary099/EXECUTIVE.agc 的FINDVAC2中,调度器用CCS依次轮询 5 个 VAC(向量累加器)工作区、再在 7 个 CORE SET(每个 11 个寄存器的任务上下文)中寻找空位,找不到就带着告警码跳入BAILOUT1:

CCS VAC1USE TCF VACFOUND CCS VAC2USE ... TC BAILOUT1 OCT 1201 # NO VAC AREAS.

注意那个OCT 1201——它就是阿波罗 11 号着陆时让宇航员手心冒汗的告警码前身。这套"有限资源 + 明确溢出路径"的调度器,今天读来依然是一份出色的嵌入式实时系统教材。

定时侧由 Comanche055/WAITLIST.agc 负责:调用方把"多少厘秒后开始执行"的延时压入一个以TIME3为基准的差值链表(LST1/LST2),到期由 T3 中断触发任务。注释里写得很清楚:

# 1) 1 <= C(A) <= 16250D (1 CENTISECOND TO 162.5 SEC) # 2) 9 TASKS MAXIMUM # 3) TASKS CALLED UNDER INTERRUPT INHIBITED

最多 9 个挂起任务、最小间隔 10 毫秒——在 1.024 MHz 上这就是现实。今天的 fprime 用 Rate Group 和调度表(scheduling table)实现同样的周期执行语义,思路一脉相承:把"何时运行、多久一次"从业务代码中剥离出来,交给调度框架。

更超前的是一层名为INTERPRETER的"虚拟机"。出于内存与数值精度考虑,AGC 团队在汇编之上实现了一个解释器(见 Comanche055/INTERPRETER.agc),提供DLOAD、VLOAD、DAD、DSU、DPAGREE、双精度开方等"指令",运算数据放在多用途累加器 MPAC 及其后继寄存器中:

DLOAD EXTEND INDEX ADDRWD DCA 0 # LOAD DP C(C(ADDRWD)) INTO MPAC,MPAX +1 SLOAD2 DXCH MPAC CAF ZERO # ZERO MPAC +2

写导航、制导方程的工程师面对的是这套"解释性指令",而不是原始机器码——这大概是航天领域最早的"领域专用语言"实践之一,也是后来高抽象语言路线的最初雏形。

三、表驱动、状态机与程序化容错:1969 年就写好的原则

现代嵌入式软件常提的"以数据驱动逻辑、用查表替代分支",在 AGC 里不是风格建议,而是内存预算下的必然选择。两个例子最有说服力。

第一个是 Comanche055/JET_SELECTION_LOGIC.agc。RCS 喷气选择逻辑把"旋转 + 平移 + 喷气四元组(AC/BD quad)故障"的三维组合全部编码进一张 15 行的查表PYTABLE,用INDEX+ 掩码取位:

# ROT TRANS QUAD BIAS PYTABLE OCT 0 # 0 0 0 OCT 5125 # + 0 0 OCT 5252 # - 0 0 OCT 0231 # 0 + 3 OCT 2421 # + + 3 ...

TABPCOM用命令索引查表、MASK PJETS取出喷气组合、MP BIT7提取喷气数量——整个决策路径上没有一颗动态分配的内存,没有一次递归。这套表驱动方法在今天的飞行软件里仍是主流:控制律参数、配置、调度表全部外置为表格/配置文件,代码本身保持"薄"。

第二个是登月舱的点火例程 Luminary099/BURN_BABY_BURN--MASTER_IGNITION_ROUTINE.agc。它通过擦除寄存器WHICH(E4)索引P12TABLE、P40TABLE、P41TABLE、P42TABLE、P63TABLE、ABRTABLE等表,一套通用点火逻辑服务 P12、P40、P42、P61、P63 五个程序:

P63TABLE VN 0662 # (0) TCF ULLGNOT # (1) TCF COMFAIL3 # (2) TCF V99RECYC # (3) TCF TASKOVER # (4) TCF P63SPOT # (5) DEC 2240 # (6) EBANK= WHICH 2CADR SERVEXIT # (7)

这也是 P63 着陆制动阶段真正点火的地方(Luminary099/THE_LUNAR_LANDING.agc 先做 IMU 状态检查 R02、初始化WHICH指向P63ADRES,再由BURNBABY接管)。文件里还保留着一段可爱的史料:作者 Don Eyles 与 Peter Adler 解释,BURN, BABY, BURN的名字来自 1965 年洛杉矶骚乱期间电台 DJ Magnificent Montague 的经典口号——硬核的航天工程代码,也能有摇滚气质。

容错方面,Comanche055/ALARM_AND_ABORT.agc 定义了完整的告警/中止体系:非中止性告警ALARM、可显示优先级告警PRIOLARM、致命跳转POODOO(源码注释戏称为 ENEMA/WHIMPER 系列)。Luminary099/ASSEMBLY_AND_OPERATION_INFORMATION.agc 第 24 页附近列出了数十条告警码及分级规则:

# 01201 * EXECUTIVE OVERFLOW - NO VAC AREAS EXEC # 01202 * EXECUTIVE OVERFLOW - NO CORE SETS EXEC # 01203 * WAITLIST OVERFLOW - TOO MANY TASKS WAITLIST

注释下方还专门说明:*表示导致软件重启的 abort 码,**表示更严重、直接回到 R00 的 abort 码,P表示优先级告警。1969 年 7 月 20 日,当登月舱下降至约 1800 米高度时,AGC 正是在EXECUTIVE溢出(1201/1202 系列)下继续工作,靠的是优先级调度、软件重启路径与地面指挥快速判读的合力。这套"提前定义故障码、区分可恢复/致命、软件自行降级重启"的思想,就是今天航天器 FDIR(故障检测、隔离与恢复)与健康管理系统的直接祖先。同一套目录下,Luminary099/KALMAN_FILTER.agc 的RATELOOP把喷气力矩积分成角速率并汇入下链遥测寄存器DOWNTORK——导航滤波在 64KB 机器上已经有了早期落地。

四、从"人工校对扫描件"到形式化验证:50 年的工具链演进

今天这份仓库能逐行对齐 1969 年的真实飞行代码,本身就是一次"逆向工程考古"。仓库的 CONTRIBUTING.md 规定了近乎苛刻的转录规范:Tab 缩进、宽度 8、无行尾空格;注释必须与扫描件逐字一致,连原作者的拼写错误都要保留(比如扫描件印的是SPAECRAFT,转录就绝不能"好心"改成SPACECRAFT)。Luminary099/README.md 还记录了汇编器谱系:原始 YUL 早已不可考,GAP 也已消失,如今的 re-assembly 依赖 Virtual AGC 项目重写的yaYUL汇编器。也就是说,"1969 年的二进制"在今天可复现可模拟,靠的是一套现代工具链(Virtual AGC + yaYUL + 仿真器)对旧世界遗产的忠实翻译。

而这恰好折射出 NASA 软件工程 50 年的主线:验证手段从"人海校对"走向"机器证明"。

1960 年代,AGC 的可靠性来自三层人工手段:代码走查、海量地面仿真、以及逐字对照纸带打印件。那是一个没有 CI、没有静态分析、没有形式化工具的年代,连汇编器本身都是"一次性"的。今天的对应物则是:

  • cFS / F':组件化 + 自动生成的接口代码,把"人肉对接"变成"编译器对接";
  • C++ 与编码标准:NASA 与 JPL 的飞行软件大量使用 C++(fprime 即为其代表),辅以 MISRA C++ 等静态约束,用类型系统和规范把一类错误挡在编译期;
  • 形式化验证:SPARK/Ada、Coq、模型检验等技术进入航天关键软件领域。SPARK 允许工程师给出前置/后置条件与循环不变式,由证明工具验证——把"逐字校对注释"换成了"证明程序性质"。

fprime 与 AGC 的思想继承关系尤其值得玩味。fprime 的核心构件——组件(component)、端口(port)、Rate Group 周期调度、命令/遥测表、自动生成的 FPP 拓扑——几乎可以在 AGC 源码里找到一一对应的"老前辈":组件对应MAIN.agc里的 LOG SECTION 划分;端口对应INTER-BANK_COMMUNICATION.agc的BANKCALL/SWCALL/POSTJUMP/SUPERSW跨银行调用协议;Rate Group 对应WAITLIST;命令/遥测表对应 DSKY 的 VERB/NOUN 与下链遥测程序(Comanche055/DOWN-TELEMETRY_PROGRAM.agc)。差别在于:fprime 用工具生成样板代码、用 C++ 对象封装内存,而 AGC 只能靠注释和约定。架构思想是继承的,实现机制是进化的。

五、哪些 AGC 原则至今仍在 NASA 服役

把源码读完一遍,可以提炼出四条至今仍在 NASA 工程文化中"服役"的原则:

1. 确定性优先于炫技。AGC 的调度器是严格的优先级抢占 + 有限工作区模型,每个作业的运行窗口、上下文大小都在汇编期定死。今天的 RTOS(VxWorks、RTEMS)与 fprime 的 Rate Group 依然强调可预测的时序与确定的调度语义——为的是同一个目标:让验证者能回答"最坏情况下它会不会超时"。

2. 表驱动与配置化。从PYTABLE到BURN_BABY_BURN的各点火表,AGC 把"逻辑"与"数据"彻底分离,换一次点火时序只需改表、不动代码。现代飞行软件的参数配置、调度表、遥测表延续了同一哲学。

3. 显式声明故障路径。1202 不是 bug,它是一条被设计出来的、带等级的故障出口。现代航天器把这种思想制度化成了 FDIR 架构:每个组件都必须回答"失效时往哪走、告警码是什么、降级到哪一档"。

4. 注释即规格,文档即验收。AGC 源码的注释密度在今天的工业代码里几乎不可想象:每个子程序开头有程序名、修订号、功能描述、调用序列、告警码对照表。Margaret Hamilton 以 Colossus Programming Leader 的身份在 CONTRACT_AND_APPROVALS.agc 的审批页上签名——她是"software engineering"这一概念的早期推动者之一。这份"软件要有工程文档、要有审查链"的执念,演化成了今天的 DO-178C 适航认证体系和 NASA 的软件工程规范(NPR 7150.2)。

至于"开源遗产",则是一个意味深长的闭环:1969 年的代码以公有领域身份进入 README.md 所述档案,2016 年被转录上传,2019 年在登月 50 周年之际冲上趋势榜、又因中文灌水风波出圈。今天,Virtual AGC 模拟器让任何人都能"重跑"这套固件,yaYUL 重新汇编验证了它的可构建性。阿波罗 11 号留给软件行业的,不只是一段登月的代码,更是一整套关于"有限资源下如何把正确性当成第一公民"的方法论——这套方法论没有过时,它只是换了一门语言。

结语:从手工汇编、解释器、优先级调度,到 cFS、fprime、SPARK 形式化验证,NASA 软件工程的载体变了,但三样东西没变:确定性、可验证性、以及对"软件也是硬工程"的敬畏。AGC 的遗产还剩多少?答案是:它的架构思想活在每一个现代航天软件的调度表里,它的文档纪律活在每一份 DO-178C 合规报告里,而它面对死线时的从容,依然值得今天每一个在 deadline 前加班的嵌入式工程师反复回味。

【免费下载链接】Apollo-11Original Apollo 11 Guidance Computer (AGC) source code for the command and lunar modules.项目地址: https://gitcode.com/GitHub_Trending/ap/Apollo-11

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询