<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>逆向工程 on CloudLake</title><link>https://cloudlake.eu.org/tags/%E9%80%86%E5%90%91%E5%B7%A5%E7%A8%8B/</link><description>Recent content in 逆向工程 on CloudLake</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><lastBuildDate>Sat, 24 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://cloudlake.eu.org/tags/%E9%80%86%E5%90%91%E5%B7%A5%E7%A8%8B/index.xml" rel="self" type="application/rss+xml"/><item><title>在浏览器里养一台计算器：TI-Nspire 在线模拟器从 0 到「长期使用」</title><link>https://cloudlake.eu.org/p/nspire-web-emulator/</link><pubDate>Sat, 24 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/nspire-web-emulator/</guid><description>&lt;img src="https://cloudlake.eu.org/p/nspire-web-emulator/cover.jpg" alt="Featured image of post 在浏览器里养一台计算器：TI-Nspire 在线模拟器从 0 到「长期使用」" /&gt;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;一个 GPL 的 ARM 计算器模拟器、一个从 2016 年捞出来的 Palm 游戏、一台 1995 年的 Windows 95 机器，最后都被塞进了同一个网页里。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;现在打开 &lt;a class="link" href="https://cloudlake.eu.org/nspire/" target="_blank" rel="noopener"&#10; &gt;cloudlake.eu.org/nspire&lt;/a&gt;，你会看到四张卡片：经典灰度 Touchpad、彩色 CX II CAS、一个彩蛋 Windows 95，以及长期使用的登录入口。下面这篇文章把这个平台从架构到踩坑完整讲一遍。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="入口页：四张卡片" class="gallery-image" data-flex-basis="305px" data-flex-grow="127" height="1100" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-entry.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-entry_hu_623494c3318391c8.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-entry.png 1400w" width="1400"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="一最后做出来的东西"&gt;&lt;a href="#%e4%b8%80%e6%9c%80%e5%90%8e%e5%81%9a%e5%87%ba%e6%9d%a5%e7%9a%84%e4%b8%9c%e8%a5%bf" class="header-anchor"&gt;&lt;/a&gt;一、最后做出来的东西&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;三种「机器」共用一个页面&lt;/strong&gt;：经典 Nspire Touchpad（灰度 320×240）、CX II CAS（彩色 16 位色）、以及通过 WiNspire 跑的 Windows 95&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;完整键盘 + 触摸板 + 文件传输&lt;/strong&gt;：70 个键的屏幕键盘、触摸板滑动/点击、拖拽/整文件夹上传、目录浏览与下载&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;游戏一键启动&lt;/strong&gt;：点一下就从服务器把 nPal（仙剑奇侠传）传进计算器并自动运行&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;长期使用模式&lt;/strong&gt;：用户名登录，状态存服务器，每 2 分钟自动保存，下次登录直接接着用&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;img alt="CX II 在线模拟器整页：左边屏幕、右边键盘、下面是文件传输和快速启动游戏" class="gallery-image" data-flex-basis="258px" data-flex-grow="107" height="1301" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-cx2.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-cx2_hu_6547d1e51d011008.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-cx2.png 1400w" width="1400"&gt;&lt;/p&gt;&#10;&lt;p&gt;经典灰度机的页面是同一个文件，只是换了镜像：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="经典 Touchpad 页面" class="gallery-image" data-flex-basis="252px" data-flex-grow="105" height="1331" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-classic.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-classic_hu_872dbeeb73d70df4.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-classic.png 1400w" width="1400"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="二地基把-firebird-编译成-webassembly"&gt;&lt;a href="#%e4%ba%8c%e5%9c%b0%e5%9f%ba%e6%8a%8a-firebird-%e7%bc%96%e8%af%91%e6%88%90-webassembly" class="header-anchor"&gt;&lt;/a&gt;二、地基：把 Firebird 编译成 WebAssembly&#10;&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://github.com/nspire-emus/firebird" target="_blank" rel="noopener"&#10; &gt;Firebird&lt;/a&gt; 是一个开源（GPL-3.0）的 TI-Nspire 模拟器，桌面版基于 Qt。它模拟的是 ARM926 级别的 SoC、LCD 控制器、键盘矩阵、NAND flash 和 USB 控制器。&lt;/p&gt;&#10;&lt;p&gt;网页版做的事情是：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;用 emscripten 把 Firebird core（约 33 个源文件）编译成 &lt;code&gt;firebird.js&lt;/code&gt;（约 88 KB）+ &lt;code&gt;firebird.wasm&lt;/code&gt;（约 230 KB）&lt;/li&gt;&#10;&lt;li&gt;页面把 &lt;strong&gt;flash 镜像&lt;/strong&gt;（整块 NAND 转储，经典机 33 MB，CX II 132 MB）写进 Emscripten 的内存文件系统&lt;/li&gt;&#10;&lt;li&gt;可选加载 &lt;strong&gt;snapshot&lt;/strong&gt;（gzip 压缩的 RAM + CPU 状态），实现「秒开」&lt;/li&gt;&#10;&lt;li&gt;用 &lt;code&gt;paintLCD()&lt;/code&gt; 把 320×240 的帧缓冲画到 &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; 上&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;几个设计决定后来被证明是对的：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;一个 &lt;code&gt;emu.html&lt;/code&gt; 支持所有机型&lt;/strong&gt;：机型差异全部收敛到 URL 参数 &lt;code&gt;?flash=...&amp;amp;boot=...&amp;amp;snap=...&lt;/code&gt;，页面代码是同一份。经典机、CX II、Windows 95、长期使用模式都复用这套加载逻辑。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;镜像放服务器、状态放浏览器/账号&lt;/strong&gt;：静态镜像走 Caddy 的 precompressed（brotli/gzip），经典 flash 压缩后约 12 MB，CX II 约 17 MB，浏览器缓存 30 天。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;快照优先&lt;/strong&gt;：冷启动 60 秒 vs 快照恢复几秒，体验差距巨大，所以后来能存快照的场景都尽量存。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="三让模拟器能用键盘触摸板文件传输"&gt;&lt;a href="#%e4%b8%89%e8%ae%a9%e6%a8%a1%e6%8b%9f%e5%99%a8%e8%83%bd%e7%94%a8%e9%94%ae%e7%9b%98%e8%a7%a6%e6%91%b8%e6%9d%bf%e6%96%87%e4%bb%b6%e4%bc%a0%e8%be%93" class="header-anchor"&gt;&lt;/a&gt;三、让模拟器「能用」：键盘、触摸板、文件传输&#10;&lt;/h2&gt;&lt;p&gt;只有画面没有输入，模拟器就是个屏保。这部分是平台真正的骨架。&lt;/p&gt;&#10;&lt;h3 id="31-键盘70-键矩阵"&gt;&lt;a href="#31-%e9%94%ae%e7%9b%9870-%e9%94%ae%e7%9f%a9%e9%98%b5" class="header-anchor"&gt;&lt;/a&gt;3.1 键盘：70 键矩阵&#10;&lt;/h3&gt;&lt;p&gt;Firebird 的键盘是 8×11 矩阵，网页端只暴露一个 &lt;code&gt;setKey(row, col, state)&lt;/code&gt;。页面上用 DOM 画了 70 个键，按下时转成矩阵坐标注入。&lt;/p&gt;&#10;&lt;p&gt;唯一的小坑：OS 的键盘扫描有间隔，太快松开会漏键，所以前端给每次按键加了 &lt;strong&gt;最少 80 ms 的保持时间&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;h3 id="32-触摸板从只认方向键到真正能推鼠标"&gt;&lt;a href="#32-%e8%a7%a6%e6%91%b8%e6%9d%bf%e4%bb%8e%e5%8f%aa%e8%ae%a4%e6%96%b9%e5%90%91%e9%94%ae%e5%88%b0%e7%9c%9f%e6%ad%a3%e8%83%bd%e6%8e%a8%e9%bc%a0%e6%a0%87" class="header-anchor"&gt;&lt;/a&gt;3.2 触摸板：从「只认方向键」到真正能推鼠标&#10;&lt;/h3&gt;&lt;p&gt;这是整个项目最值得写的一段。TI-Nspire 的触摸板在 OS 里既当 &lt;strong&gt;5 向键&lt;/strong&gt;用（菜单导航），又当 &lt;strong&gt;鼠标&lt;/strong&gt;用（图形、几何、Windows 95）。&lt;/p&gt;&#10;&lt;p&gt;原来的实现只会模拟「按触摸板边缘 → 方向键」，菜单能走，但 &lt;strong&gt;GUI 指针纹丝不动&lt;/strong&gt;。原因是：OS 的指针用的是触摸板驱动算出来的&lt;strong&gt;相对位移&lt;/strong&gt;（&lt;code&gt;touchpad_rel_x/y&lt;/code&gt;），而页面从来没把连续滑动坐标送进去。&lt;/p&gt;&#10;&lt;p&gt;修复思路：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;拖动时按 ~38 Hz 连续调用 &lt;code&gt;setTouchpadNorm(x, y, contact=1, down=0)&lt;/code&gt;，让 Firebird 的 &lt;code&gt;touchpad_set_state()&lt;/code&gt; 计算相对位移&lt;/li&gt;&#10;&lt;li&gt;轻点 = 按下脉冲（中心 = 左键，边缘 = 方向键；&lt;code&gt;Ctrl&lt;/code&gt; + 轻点 = 右键）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;混合回退&lt;/strong&gt;：如果滑动了 220 ms、画面完全没变化（TI-OS 菜单只认方向事件），自动退化成方向键手势，这样拖动导航不丢&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;修复后的效果（拖动前 / 后，Win95 的鼠标从画面中心移动到右下）：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="触摸板指针修复前后对比" class="gallery-image" data-flex-basis="593px" data-flex-grow="247" height="530" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-touchpad.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-touchpad_hu_68942d18c4994de5.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-touchpad.png 1310w" width="1310"&gt;&lt;/p&gt;&#10;&lt;h3 id="33-文件传输把虚拟-usb-线变成拖拽上传"&gt;&lt;a href="#33-%e6%96%87%e4%bb%b6%e4%bc%a0%e8%be%93%e6%8a%8a%e8%99%9a%e6%8b%9f-usb-%e7%ba%bf%e5%8f%98%e6%88%90%e6%8b%96%e6%8b%bd%e4%b8%8a%e4%bc%a0" class="header-anchor"&gt;&lt;/a&gt;3.3 文件传输：把虚拟 USB 线变成拖拽上传&#10;&lt;/h3&gt;&lt;p&gt;Firebird 的 USB Link 层允许宿主机和模拟器里的 OS 互传文件。我们在 WASM 里加了这些导出：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;emuPutFile(local, remote) MEMFS → 计算器&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;emuGetFile(remote, dest) 计算器 → MEMFS&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;emuNewDir / emuDirList / emuDelete / emuMove&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;emuUsblinkProgress / Busy / Done / Abort&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;前端在此之上做了：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;上传队列&lt;/strong&gt;：串行传输、逐文件进度条、失败自动重试（OS 侧偶发返回负数错误码时，&lt;code&gt;emuUsblinkAbort()&lt;/code&gt; + 重连再试）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;整文件夹上传&lt;/strong&gt;：&lt;code&gt;&amp;lt;input webkitdirectory&amp;gt;&lt;/code&gt; + 拖拽目录时用 &lt;code&gt;webkitGetAsEntry()&lt;/code&gt; 递归，保留相对路径，按层级 &lt;code&gt;emuNewDir&lt;/code&gt; 自动建目录&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;只收 &lt;code&gt;.tns&lt;/code&gt; 的 OS 限制&lt;/strong&gt;：实测 OS 文档服务只接受 &lt;code&gt;.tns&lt;/code&gt;，其它扩展名（&lt;code&gt;.txt/.dat/.bin/.zip/.py/...&lt;/code&gt;）一律被拒；文件夹里的非 &lt;code&gt;.tns&lt;/code&gt; 会在界面上标「跳过」，并提示先改名&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;img alt="文件传输面板：文件夹上传、队列、只收 .tns 的说明" class="gallery-image" data-flex-basis="583px" data-flex-grow="243" height="497" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-folders.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-folders_hu_aa2fe1ce3e9363eb.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-folders.png 1208w" width="1208"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="四踩过的最大的坑页面弹输入窗口然后冻结"&gt;&lt;a href="#%e5%9b%9b%e8%b8%a9%e8%bf%87%e7%9a%84%e6%9c%80%e5%a4%a7%e7%9a%84%e5%9d%91%e9%a1%b5%e9%9d%a2%e5%bc%b9%e8%be%93%e5%85%a5%e7%aa%97%e5%8f%a3%e7%84%b6%e5%90%8e%e5%86%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;四、踩过的最大的坑：页面「弹输入窗口」然后冻结&#10;&lt;/h2&gt;&lt;p&gt;有一段时间用户反馈：&lt;strong&gt;装 Ndless 的时候浏览器弹出一个输入窗口，然后整个页面卡死&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;追下去发现链路非常长：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Ndless 安装器在模拟器里触发了非法指令（&lt;code&gt;Prefetch abort&lt;/code&gt; / &lt;code&gt;Cannot set T bit&lt;/code&gt;）&lt;/li&gt;&#10;&lt;li&gt;Firebird 的异常处理会进入内置调试器&lt;/li&gt;&#10;&lt;li&gt;WASM 版的调试器输入函数还在读 &lt;code&gt;stdin&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Emscripten 的 stdin 兜底实现是 &lt;strong&gt;&lt;code&gt;window.prompt(&amp;quot;Input: &amp;quot;)&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;window.prompt&lt;/code&gt; 会&lt;strong&gt;阻塞页面主线程&lt;/strong&gt; → 整个模拟器看起来死了&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;修复是在 &lt;code&gt;firebird/emscripten/main.cpp&lt;/code&gt; 里给 &lt;code&gt;gui_debugger_request_input()&lt;/code&gt; 换成直接返回一条 &lt;code&gt;c&lt;/code&gt;（continue），让异常路径走 &lt;code&gt;EVENT_RESET&lt;/code&gt; 重启 OS，而不是等用户输入。&lt;/p&gt;&#10;&lt;p&gt;顺带修掉的还有一个自己引入的回归：&lt;strong&gt;点击画面曾经等于点触摸板中心&lt;/strong&gt;，会误开文档/对话框。现在画面单击只负责取键盘焦点，点击请用触摸板区域。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="五npal-移植把-2016-年的-palm-游戏跑进浏览器"&gt;&lt;a href="#%e4%ba%94npal-%e7%a7%bb%e6%a4%8d%e6%8a%8a-2016-%e5%b9%b4%e7%9a%84-palm-%e6%b8%b8%e6%88%8f%e8%b7%91%e8%bf%9b%e6%b5%8f%e8%a7%88%e5%99%a8" class="header-anchor"&gt;&lt;/a&gt;五、nPal 移植：把 2016 年的 Palm 游戏跑进浏览器&#10;&lt;/h2&gt;&lt;p&gt;nPal 是网友 ntzyz 把 &lt;strong&gt;PalmOS 时代的《仙剑奇侠传》&lt;/strong&gt; 移植到 TI-Nspire 的项目。我们拿到的是 2016 年的 nPal 1.2 发布包，里面是一个 1.25 MB 的 &lt;code&gt;sdlpal.tns&lt;/code&gt;。&lt;/p&gt;&#10;&lt;h3 id="51-旧包为什么会在-cx-ii-上弹兼容性模式"&gt;&lt;a href="#51-%e6%97%a7%e5%8c%85%e4%b8%ba%e4%bb%80%e4%b9%88%e4%bc%9a%e5%9c%a8-cx-ii-%e4%b8%8a%e5%bc%b9%e5%85%bc%e5%ae%b9%e6%80%a7%e6%a8%a1%e5%bc%8f" class="header-anchor"&gt;&lt;/a&gt;5.1 旧包为什么会在 CX II 上弹「兼容性模式」&#10;&lt;/h3&gt;&lt;p&gt;旧的 &lt;code&gt;sdlpal.tns&lt;/code&gt; 是 &lt;strong&gt;PRG 容器&lt;/strong&gt;（文件头 &lt;code&gt;50 52 47 00&lt;/code&gt;），里面根本没有 &lt;code&gt;Zehn&lt;/code&gt; 签名。Ndless r2022 会用旧加载路径跑它，于是：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;读不到 Zehn 标志位表&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;supports_hww&lt;/code&gt; 保持 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Ndless 弹出「Activating compatibility mode. This application hasn&amp;rsquo;t been updated to work with your hardware.」&lt;/li&gt;&#10;&lt;li&gt;走 320×320 的 LCD 兼容层，CX II 的原生 240×320 旋转屏适配失效&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="52-重新编译"&gt;&lt;a href="#52-%e9%87%8d%e6%96%b0%e7%bc%96%e8%af%91" class="header-anchor"&gt;&lt;/a&gt;5.2 重新编译&#10;&lt;/h3&gt;&lt;p&gt;用 &lt;strong&gt;Ndless r2022 SDK&lt;/strong&gt; 重新编译 nPal 的 SDL 部分：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;SDK 自带的 nSDL 1.1.1 已经改用新的 &lt;code&gt;lcd_blit&lt;/code&gt; 屏幕 API&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;genzehn&lt;/code&gt; 打包时扫描 ELF 符号，发现 &lt;code&gt;_genzehn_new_lcd_api&lt;/code&gt; 标记，自动写入 &lt;code&gt;USES_LCD_BLIT = true&lt;/code&gt; 和 &lt;code&gt;RUNS_ON_HWW = true&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;产物 &lt;code&gt;sdlpal.tns&lt;/code&gt; 419,248 B（纯 Zehn，无 PRG 外壳），&lt;code&gt;genzehn --info&lt;/code&gt; 能直接验证：&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Application: sdlpal&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This executable uses the new lcd_blit API.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This executable supports the HW-W 240x320 LCD.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;另外两条工具链（官方 GCC 14.2 和 apt GCC 13）构建出的版本，在模拟器里&lt;strong&gt;逐帧逐像素完全一致&lt;/strong&gt;，最后分发用官方工具链版。&lt;/p&gt;&#10;&lt;h3 id="53-资源文件与启动"&gt;&lt;a href="#53-%e8%b5%84%e6%ba%90%e6%96%87%e4%bb%b6%e4%b8%8e%e5%90%af%e5%8a%a8" class="header-anchor"&gt;&lt;/a&gt;5.3 资源文件与启动&#10;&lt;/h3&gt;&lt;p&gt;nPal 的资源是 &lt;code&gt;resources.bin.tns&lt;/code&gt;（23.9 MB）和 &lt;code&gt;desc.dat.tns&lt;/code&gt;（16 KB）——它们本质上就是数据文件，只是 &lt;strong&gt;故意用 &lt;code&gt;.tns&lt;/code&gt; 扩展名&lt;/strong&gt;，这样才能通过 OS 的文档服务传进计算器。全部放进计算器的 &lt;code&gt;/npal&lt;/code&gt;，运行 &lt;code&gt;sdlpal.tns&lt;/code&gt; 即可。&lt;/p&gt;&#10;&lt;p&gt;在模拟器里看到的结果：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="nPal 启动后的仙剑标题画面" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-npal.png" width="640"&gt;&lt;/p&gt;&#10;&lt;h3 id="54-一键游戏启动"&gt;&lt;a href="#54-%e4%b8%80%e9%94%ae%e6%b8%b8%e6%88%8f%e5%90%af%e5%8a%a8" class="header-anchor"&gt;&lt;/a&gt;5.4 一键游戏启动&#10;&lt;/h3&gt;&lt;p&gt;手动上传三个文件太麻烦，于是做了「快速启动游戏」：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;服务器放 &lt;code&gt;games.json&lt;/code&gt; 清单 + 每个游戏的文件（同时生成 &lt;code&gt;.br/.gz&lt;/code&gt; 供 Caddy 预压缩）&lt;/li&gt;&#10;&lt;li&gt;页面选择 nPal → 依次下载 3 个文件 → 自动创建 &lt;code&gt;/npal&lt;/code&gt; → 走 USB 上传 → 主程序最后上传，保证文件浏览器选中它 → 自动发回车运行&lt;/li&gt;&#10;&lt;li&gt;旁边还有「安装 Ndless」按钮：自动上传安装器+资源，再按固定节奏发 Enter 走完安装流程&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;实测（线上）：&lt;strong&gt;安装 Ndless 66 秒 + 启动游戏 59 秒&lt;/strong&gt;，然后就是标题画面。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="文件面板里的快速启动游戏行" class="gallery-image" data-flex-basis="458px" data-flex-grow="190" height="633" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-gamepanel.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-gamepanel_hu_a319d50a3e91baf0.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-gamepanel.png 1208w" width="1208"&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="Ndless 安装器在模拟器里的界面" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-ndless.png" width="640"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="六彩蛋在计算器里跑-windows-95"&gt;&lt;a href="#%e5%85%ad%e5%bd%a9%e8%9b%8b%e5%9c%a8%e8%ae%a1%e7%ae%97%e5%99%a8%e9%87%8c%e8%b7%91-windows-95" class="header-anchor"&gt;&lt;/a&gt;六、彩蛋：在计算器里跑 Windows 95&#10;&lt;/h2&gt;&lt;p&gt;WiNspire 是 &lt;strong&gt;tiny386（x86 PC 模拟器）的 CX II 移植&lt;/strong&gt;，以 Ndless 程序运行，Native 模式给 guest 16 MB RAM / 256 KB 显存。于是流程变成：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;浏览器 → Firebird(WASM，模拟 CX II) → TI-OS + Ndless → winspire.tns(tiny386) → Windows 95&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;ul&gt;&#10;&lt;li&gt;磁盘镜像是从 PCjs 的 Windows 95 RTM 演示机用它的 “Save HD” 导出的 68 MB raw image&lt;/li&gt;&#10;&lt;li&gt;把镜像改名成 &lt;code&gt;disk.img.tns&lt;/code&gt;，和 &lt;code&gt;winspire.tns&lt;/code&gt;、bios、vgabios、ini 一起上传到 &lt;code&gt;/winspire&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;冷启动大约 4–5 分钟：SeaBIOS → Windows 95 开机画面 → 桌面&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;img alt="WiNspire 里的 SeaBIOS 在浏览器中启动" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-seabios.png" width="640"&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="Windows 95 启动画面" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-win95-boot.png" width="640"&gt;&lt;/p&gt;&#10;&lt;p&gt;为了把「开机 4 分钟」变成「秒进桌面」，又做了预烘焙：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;在模拟器里跑到 Win95 桌面&lt;/li&gt;&#10;&lt;li&gt;调用 &lt;code&gt;saveSnapshot()&lt;/code&gt; 生成快照&lt;/li&gt;&#10;&lt;li&gt;关键：Firebird 的 NAND 修改默认只在内存里，必须调用新加的 &lt;code&gt;saveFlash()&lt;/code&gt;（内部就是 &lt;code&gt;flash_save_changes()&lt;/code&gt;）把修改块写回 &lt;code&gt;flash.img&lt;/code&gt;，否则导出的镜像还是原样&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;最终页面加载 &lt;code&gt;flash&lt;/code&gt;（brotli 后 17.2 MB）+ &lt;code&gt;snapshot&lt;/code&gt;（33 MB，基本不可压缩），点开始几秒后直接是 Windows 95 桌面：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="Windows 95 桌面（浏览器内）" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-win95.png" width="640"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="七长期使用用户名登录--状态持久化"&gt;&lt;a href="#%e4%b8%83%e9%95%bf%e6%9c%9f%e4%bd%bf%e7%94%a8%e7%94%a8%e6%88%b7%e5%90%8d%e7%99%bb%e5%bd%95--%e7%8a%b6%e6%80%81%e6%8c%81%e4%b9%85%e5%8c%96" class="header-anchor"&gt;&lt;/a&gt;七、长期使用：用户名登录 + 状态持久化&#10;&lt;/h2&gt;&lt;p&gt;短期玩一玩，刷新页面就重来；但总有人想「养」一台自己的计算器。于是加了长期使用模式：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;登录页输入用户名（无密码，HMAC 签名 cookie 记身份）&lt;/li&gt;&#10;&lt;li&gt;一个用户名同一时间只能开一个窗口（心跳续锁，180 秒无心跳自动过期）&lt;/li&gt;&#10;&lt;li&gt;新用户从干净的 CX II 系统开始（默认镜像）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;保存 = flash 镜像 + 内存快照&lt;/strong&gt;：&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;saveFlash()&lt;/code&gt; 把 NAND 修改写回 MEMFS 的 &lt;code&gt;flash.img&lt;/code&gt;（138 MB）&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;saveSnapshot()&lt;/code&gt; 生成 &lt;code&gt;snapshot.bin&lt;/code&gt;（gzip 流）&lt;/li&gt;&#10;&lt;li&gt;浏览器里用 &lt;code&gt;CompressionStream('gzip')&lt;/code&gt; 把 flash 压到约 19 MB，和快照（约 10–33 MB）一起 POST 到服务器&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;恢复 = 直接加载 flash + snapshot&lt;/strong&gt;，从快照恢复现场&lt;/li&gt;&#10;&lt;li&gt;每 2 分钟自动保存一次；「保存」按钮手动存；「退出」先保存再注销&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;后端是一个 &lt;strong&gt;Python 标准库写的单文件服务&lt;/strong&gt;（&lt;code&gt;nspire-lt&lt;/code&gt;，systemd 托管），Caddy 把 &lt;code&gt;/nspire/api/&lt;/code&gt; 反代到它，数据落在 &lt;code&gt;/var/lib/nspire-lt/users/&amp;lt;user&amp;gt;/&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;实测完整流程（登录 → 启动 → 打开图形应用 → 保存 → 退出 → 再登录）：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="err"&gt;·&lt;/span&gt; &lt;span class="err"&gt;已保存&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;restore&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="mi"&gt;9236&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="n"&gt;vs&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="err"&gt;恢复后画面与保存时逐像素一致&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;&lt;img alt="长期使用登录页" class="gallery-image" data-flex-basis="305px" data-flex-grow="127" height="1100" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-lt.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-lt_hu_27e2ee6570edcb8b.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-lt.png 1400w" width="1400"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="八数据与性能"&gt;&lt;a href="#%e5%85%ab%e6%95%b0%e6%8d%ae%e4%b8%8e%e6%80%a7%e8%83%bd" class="header-anchor"&gt;&lt;/a&gt;八、数据与性能&#10;&lt;/h2&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;机型&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;flash 原始&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;首屏（br 压缩）&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;启动&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;备注&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;经典 Touchpad&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;33 MB&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;~12 MB&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;快照恢复极快&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;灰度 320×240&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;CX II CAS&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;132 MB&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;17.2 MB&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;冷启动 ~60 s&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;彩色 16 位色&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Windows 95&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;132 MB + 68 MB 磁盘&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;~50 MB（含快照）&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;冷启动 4–5 分钟，快照秒开&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;双重模拟，慢&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;长期使用&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;每用户 flash + 快照&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;登录时下载&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;快照恢复 ~9 s&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;服务器存档&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;一些数字：模拟 USB 传输约 &lt;strong&gt;550 KB/s&lt;/strong&gt;（23 MB 的 nPal 资源约 40–60 秒）；一次长期存档上行约 &lt;strong&gt;20–50 MB&lt;/strong&gt;；长期模式每人磁盘占用约 &lt;strong&gt;30–50 MB&lt;/strong&gt;（gzip 后）。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="九还欠着的"&gt;&lt;a href="#%e4%b9%9d%e8%bf%98%e6%ac%a0%e7%9d%80%e7%9a%84" class="header-anchor"&gt;&lt;/a&gt;九、还欠着的&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Windows 95 冷启动版&lt;/strong&gt;：预烘焙时遇到 NAND 写回/flash 保存的边界问题，目前只保留快照直启入口&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Ndless 冷启动激活状态&lt;/strong&gt;：干净镜像冷启动后 Ndless 不是激活状态，需要在游戏面板点一次「安装 Ndless」（约 1 分钟）；长期使用模式里装好保存后，快照会保留激活状态&lt;/li&gt;&#10;&lt;li&gt;移动端触控（目前触摸板/鼠标事件主要按桌面调优）&lt;/li&gt;&#10;&lt;li&gt;音频：Firebird 的 PC 喇叭/声音还没接&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="结语"&gt;&lt;a href="#%e7%bb%93%e8%af%ad" class="header-anchor"&gt;&lt;/a&gt;结语&#10;&lt;/h2&gt;&lt;p&gt;这个项目最花时间的部分，其实不是「把模拟器编成 WASM」——那是几小时的活。真正难的是&lt;strong&gt;层与层之间的接缝&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;p&gt;浏览器输入 → ARM 模拟器 → TI-OS 驱动 → Ndless → 游戏/Windows，每一层都有自己的假设。触摸板的「方向 vs 指针」、文件系统的「只收 .tns」、调试器的「读 stdin」、NAND 的「写缓存」，每一个都足以让整个平台看起来「坏了」。&lt;/p&gt;&#10;&lt;p&gt;把这些接缝一个个对上之后，才有现在这个能在浏览器里养计算器、打仙剑、开 Windows 95、还能下次登录接着用的东西。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;p&gt;入口：&lt;a class="link" href="https://cloudlake.eu.org/nspire/" target="_blank" rel="noopener"&#10; &gt;https://cloudlake.eu.org/nspire/&lt;/a&gt;&lt;br&gt;&#10;长期使用：&lt;a class="link" href="https://cloudlake.eu.org/nspire/lt/index.html" target="_blank" rel="noopener"&#10; &gt;https://cloudlake.eu.org/nspire/lt/index.html&lt;/a&gt;&lt;br&gt;&#10;技术栈：Firebird（GPL-3.0）+ emscripten/WASM + Hugo + Caddy + 一个 Python 小服务&lt;/p&gt;&#10;</description></item><item><title>拆开 TI-Nspire 的 .tns 看里面</title><link>https://cloudlake.eu.org/p/zehnscope/</link><pubDate>Tue, 13 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/zehnscope/</guid><description>&lt;img src="https://cloudlake.eu.org/p/zehnscope/cover.jpg" alt="Featured image of post 拆开 TI-Nspire 的 .tns 看里面" /&gt;&lt;p&gt;这台机器上有一个真实的 TI-Nspire 开发环境：OS 3.9.0.463 classic Touchpad、Ndless、从源码构建的 ARM 工具链。&lt;/p&gt;&#10;&lt;p&gt;目录里躺着 &lt;strong&gt;1055 个 &lt;code&gt;.tns&lt;/code&gt; 文件&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;.tns&lt;/code&gt; 不是一个格式。它是一层 &lt;code&gt;PRG\0&lt;/code&gt; 外壳，里面是 &lt;strong&gt;Zehn 容器&lt;/strong&gt;——Ndless 自己的可执行容器格式。而这个格式&lt;strong&gt;没有公开文档&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;这个项目把它读懂了，并且用「&lt;strong&gt;能不能逐字节重建&lt;/strong&gt;」来证明。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="自己解出来的格式"&gt;&lt;a href="#%e8%87%aa%e5%b7%b1%e8%a7%a3%e5%87%ba%e6%9d%a5%e7%9a%84%e6%a0%bc%e5%bc%8f" class="header-anchor"&gt;&lt;/a&gt;自己解出来的格式&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 2&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 3&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 4&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 5&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 6&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 7&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 8&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 9&#10;&lt;/span&gt;&lt;span class="lnt"&gt;10&#10;&lt;/span&gt;&lt;span class="lnt"&gt;11&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.tns 文件&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 装载器桩 0 ~ 0x1EC 字节（常见；也有 0/480/604/616/632 字节的变体）&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── Zehn 容器&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 头 32 字节 8 个小端 u32：&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ &amp;#34;Zehn&amp;#34; / version=1 / file_size / reloc_count /&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ flag_count / extra_size / alloc_size / entry_offset&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 重定位表 reloc_count × 4 字节&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ 低 8 位 = 类型，高 24 位 = 偏移&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 标志表 flag_count × 4 字节（同样的 8/24 位切分）&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 额外数据 extra_size 字节（名字/作者/声明等字符串住在这里）&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 代码 剩下的；若首个重定位是 FILE_COMPRESSED 则为 zlib 流&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="两个坑我都先踩了"&gt;&lt;a href="#%e4%b8%a4%e4%b8%aa%e5%9d%91%e6%88%91%e9%83%bd%e5%85%88%e8%b8%a9%e4%ba%86" class="header-anchor"&gt;&lt;/a&gt;两个坑，我都先踩了&#10;&lt;/h2&gt;&lt;h3 id="坑一zehn这个字符串会骗你"&gt;&lt;a href="#%e5%9d%91%e4%b8%80zehn%e8%bf%99%e4%b8%aa%e5%ad%97%e7%ac%a6%e4%b8%b2%e4%bc%9a%e9%aa%97%e4%bd%a0" class="header-anchor"&gt;&lt;/a&gt;坑一：「Zehn」这个字符串会骗你&#10;&lt;/h3&gt;&lt;p&gt;最直接的思路是——搜文件里 &lt;code&gt;Zehn&lt;/code&gt; 这四个字节，从那里开始解。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;但装载器桩里也出现了这个字符串。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;只搜这 4 个字节会找到诱饵。真正的头必须同时满足三个条件：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;version == 1&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;各表长度放得下（不越界）&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;file_size&lt;/code&gt; 确实落在文件范围内&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;strong&gt;光靠一个魔数是不够的。&lt;/strong&gt; 得用那些字段之间的一致性来互相验证。&lt;/p&gt;&#10;&lt;h3 id="坑二能力不等于要求"&gt;&lt;a href="#%e5%9d%91%e4%ba%8c%e8%83%bd%e5%8a%9b%e4%b8%8d%e7%ad%89%e4%ba%8e%e8%a6%81%e6%b1%82" class="header-anchor"&gt;&lt;/a&gt;坑二：「能力」不等于「要求」&#10;&lt;/h3&gt;&lt;p&gt;genzehn 的 &lt;code&gt;--color-support&lt;/code&gt; &lt;strong&gt;默认是 true&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;它的含义是「&lt;strong&gt;支持&lt;/strong&gt; CX/CM 彩屏」，不是「&lt;strong&gt;需要&lt;/strong&gt;彩屏」。&lt;/p&gt;&#10;&lt;p&gt;我一开始理解反了，于是&lt;strong&gt;每一个正常程序都被判成「需要彩屏，所以在这台黑白机上跑不了」&lt;/strong&gt;——一个完全反向的结论，而且看起来非常合理。&lt;/p&gt;&#10;&lt;p&gt;正确判据必须看值为 &lt;strong&gt;0&lt;/strong&gt;：0 才表示&amp;quot;不支持这类机器&amp;quot;。&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这个坑值得单独说：&lt;strong&gt;当一个字段名叫 &lt;code&gt;supports&lt;/code&gt;，人们会下意识把它读成 &lt;code&gt;requires&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="验证逐字节重建"&gt;&lt;a href="#%e9%aa%8c%e8%af%81%e9%80%90%e5%ad%97%e8%8a%82%e9%87%8d%e5%bb%ba" class="header-anchor"&gt;&lt;/a&gt;验证：逐字节重建&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;rebuild()&lt;/code&gt; 从解析出来的字段把容器&lt;strong&gt;重新拼回去&lt;/strong&gt;，然后和原文件比字节。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ python -m zehnscope --rebuild &amp;#34;E:\QA\nspire-dev&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;11/11 containers rebuilt byte for byte&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ python tests/test_zehn.py&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ok all 100 containers rebuild identically&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;这比&amp;quot;解析没崩溃&amp;quot;强得多。&lt;/p&gt;&#10;&lt;p&gt;因为偏移、计数、位域切分、字节序、字符串在额外数据里的定位——&lt;strong&gt;任何一处理解错，字节就对不上&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;它一次同时检验所有假设。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;h3 id="语料是-1055-个真实文件不是精选样本"&gt;&lt;a href="#%e8%af%ad%e6%96%99%e6%98%af-1055-%e4%b8%aa%e7%9c%9f%e5%ae%9e%e6%96%87%e4%bb%b6%e4%b8%8d%e6%98%af%e7%b2%be%e9%80%89%e6%a0%b7%e6%9c%ac" class="header-anchor"&gt;&lt;/a&gt;语料是 1055 个真实文件，不是精选样本&#10;&lt;/h3&gt;&lt;p&gt;其中 100 个是真正的 Ndless 程序，955 个只是沿用了 &lt;code&gt;.tns&lt;/code&gt; 扩展名的文档/资源（&lt;code&gt;boot1.img.tns&lt;/code&gt;、Hitori 关卡、Liero 素材……）。&lt;/p&gt;&#10;&lt;p&gt;一个只见过&amp;quot;干净样本&amp;quot;的解析器，证明力弱得多。&lt;strong&gt;跨十年、多个工具链、8 种不同桩的 100 个真实容器&lt;/strong&gt;，才是像样的考验。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="手写一个-deflate-解码器"&gt;&lt;a href="#%e6%89%8b%e5%86%99%e4%b8%80%e4%b8%aa-deflate-%e8%a7%a3%e7%a0%81%e5%99%a8" class="header-anchor"&gt;&lt;/a&gt;手写一个 DEFLATE 解码器&#10;&lt;/h2&gt;&lt;p&gt;Zehn 的压缩用 zlib。&lt;/p&gt;&#10;&lt;p&gt;但语料里&lt;strong&gt;没有一个压缩容器&lt;/strong&gt;——光靠真实文件测不到这条路。&lt;/p&gt;&#10;&lt;p&gt;所以自己按格式&lt;strong&gt;造一个&lt;/strong&gt;再读回来。&lt;/p&gt;&#10;&lt;p&gt;解码器是手写的 RFC 1951 + 1950：stored / fixed / dynamic 三种块、LZ77 回引用、Adler-32 校验。&lt;/p&gt;&#10;&lt;p&gt;然后对照系统 zlib，在 &lt;strong&gt;40 组&lt;/strong&gt;样本上比对输出（10 种载荷 × 4 个压缩级别）。&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;&lt;strong&gt;一个实现自己和自己一致，说明不了什么。两个独立实现一致，才算证据。&lt;/strong&gt;&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="一个被窄样本骗过的断言"&gt;&lt;a href="#%e4%b8%80%e4%b8%aa%e8%a2%ab%e7%aa%84%e6%a0%b7%e6%9c%ac%e9%aa%97%e8%bf%87%e7%9a%84%e6%96%ad%e8%a8%80" class="header-anchor"&gt;&lt;/a&gt;一个被窄样本骗过的断言&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;tests/test_zehn.py&lt;/code&gt; 打印出来的实测结果：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;桩大小&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;容器数&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0x1EC (492)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;53&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;17&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;632&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;11&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;480&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;616&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;6&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;604&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;&lt;strong&gt;17 个容器完全没有桩&lt;/strong&gt;——容器就在文件偏移 0。&lt;/p&gt;&#10;&lt;p&gt;我最初写了一条「所有程序共用一个桩」的断言，在 14 个 &lt;code&gt;samples-tns&lt;/code&gt; 上&lt;strong&gt;全部通过&lt;/strong&gt;，在全语料上&lt;strong&gt;直接失败&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;这正是把断言写在窄样本上会骗人的例子：&lt;strong&gt;样本越小，错误越自信。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="兼容性判定一个被推翻的直觉"&gt;&lt;a href="#%e5%85%bc%e5%ae%b9%e6%80%a7%e5%88%a4%e5%ae%9a%e4%b8%80%e4%b8%aa%e8%a2%ab%e6%8e%a8%e7%bf%bb%e7%9a%84%e7%9b%b4%e8%a7%89" class="header-anchor"&gt;&lt;/a&gt;兼容性判定：一个被推翻的直觉&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;compat.py&lt;/code&gt; 拿程序&lt;strong&gt;自己声明的能力&lt;/strong&gt;和&lt;strong&gt;具体机器&lt;/strong&gt;对判。&lt;/p&gt;&#10;&lt;p&gt;这台机器的约束很紧：classic 非 CX、320×240 黑白、Touchpad、Ndless r2016、64MB。&lt;/p&gt;&#10;&lt;p&gt;原本的直觉解释是：「SDK 是 r2022、设备是 r2016，所以自写的程序被拒载重启」。&lt;/p&gt;&#10;&lt;p&gt;但实测结论是：&lt;strong&gt;这 100 个程序里，没有一个声明 Ndless 版本窗口&lt;/strong&gt;（&lt;code&gt;NDLESS_VERSION_MIN&lt;/code&gt; / &lt;code&gt;REVISION_MIN&lt;/code&gt; 全缺）。&lt;/p&gt;&#10;&lt;p&gt;也就是说 &lt;strong&gt;版本不匹配不会导致装载被拒&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;那条&amp;quot;合理&amp;quot;的解释，直接被数据否掉了。真正的原因得往别处找——&lt;code&gt;ut_read_os_version_index()&lt;/code&gt; 的地址哈希、&lt;code&gt;sc_addrs_ptr&lt;/code&gt; 查表、或者 classic 上看门狗没关。&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这是这个项目最值钱的一条：&lt;strong&gt;一个解释听起来越合理，越要拿数据去撞它。&lt;/strong&gt;&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="还能往哪走"&gt;&lt;a href="#%e8%bf%98%e8%83%bd%e5%be%80%e5%93%aa%e8%b5%b0" class="header-anchor"&gt;&lt;/a&gt;还能往哪走&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;容器写入器&lt;/strong&gt;：现在只会重建&lt;strong&gt;已有&lt;/strong&gt;容器。接上 ELF 解析就能把任意 ARM ELF 打包成合法 &lt;code&gt;.tns&lt;/code&gt;——包括 genzehn 那部分（扫 &lt;code&gt;.rel.text&lt;/code&gt;、发 &lt;code&gt;ADD_BASE&lt;/code&gt;/&lt;code&gt;ADD_BASE_GOT&lt;/code&gt;、处理 GOT 回滚）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;OS 镜像&lt;/strong&gt;：&lt;code&gt;boot1.img.tns&lt;/code&gt; / &lt;code&gt;boot2.img.tns&lt;/code&gt; / &lt;code&gt;diags.img.tns&lt;/code&gt; 不是 Zehn 容器，是 TI 的 OS/引导镜像，格式完全不同&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;syscall 面&lt;/strong&gt;：把容器里的重定位和 &lt;code&gt;syscall-list.h&lt;/code&gt; 的 ID 表关联，就能静态列出每个程序用到的 syscall，再和 r2016 的表做差集——&lt;strong&gt;那才是&amp;quot;为什么重启&amp;quot;的正确问法&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;p style="text-align:center;margin:2.5em 0"&gt;&#13;&#10;&lt;a href="https://cloudlake.eu.org/lab/zehn/" style="display:inline-block;padding:12px 26px;border-radius:8px;background:var(--accent-color);color:var(--accent-color-text);text-decoration:none;font-weight:600"&gt;看 100 个程序的容器报告 →&lt;/a&gt;&#13;&#10;&lt;/p&gt;&#13;&#10;&lt;hr&gt;&#10;&lt;h2 id="技术细节"&gt;&lt;a href="#%e6%8a%80%e6%9c%af%e7%bb%86%e8%8a%82" class="header-anchor"&gt;&lt;/a&gt;技术细节&#10;&lt;/h2&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;文件&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;作用&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;zehnscope/core.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;容器解析 + &lt;strong&gt;逐字节重建&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;zehnscope/inflate.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;手写 DEFLATE/zlib 解码器&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;zehnscope/__main__.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;CLI：inspect / &lt;code&gt;--rebuild&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;compat.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;能力 vs 机型判定（含&amp;quot;能力≠要求&amp;quot;的说明）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;tests/test_zehn.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;18 项断言&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;逆向工程真正的方法论，不是&amp;quot;猜对格式&amp;quot;。&lt;/p&gt;&#10;&lt;p&gt;是&lt;strong&gt;找到一种只有完全理解它才能通过的检验&lt;/strong&gt;，然后不断撞上去。&lt;/p&gt;&#10;&lt;p&gt;逐字节重建就是这样一种检验。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item></channel></rss>