<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>X86 on CloudLake</title><link>https://cloudlake.eu.org/tags/x86/</link><description>Recent content in X86 on CloudLake</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><lastBuildDate>Thu, 15 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://cloudlake.eu.org/tags/x86/index.xml" rel="self" type="application/rss+xml"/><item><title>没有 nasm 也没有 qemu，我从零写了汇编器和模拟器</title><link>https://cloudlake.eu.org/p/kiln/</link><pubDate>Thu, 15 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/kiln/</guid><description>&lt;img src="https://cloudlake.eu.org/p/kiln/cover.jpg" alt="Featured image of post 没有 nasm 也没有 qemu，我从零写了汇编器和模拟器" /&gt;&lt;p&gt;我想在这台机器上跑一段能在裸机上启动的代码。&lt;/p&gt;&#10;&lt;p&gt;需要两样东西：汇编器（把汇编变成字节）和模拟器（把字节执行起来）。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;nasm&lt;/code&gt; 装不上，&lt;code&gt;qemu&lt;/code&gt; 装不上——&lt;code&gt;sudo&lt;/code&gt; 要密码、Docker 没启动、直接下载超时。于是这个项目把两样都自己实现了。&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;&#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;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;kiln/cpu.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8086 寄存器组、段寄存器、ModRM 全部寻址方式、8/16 位运算与标志位、中断、VGA 文本显存、键盘控制器&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;&lt;code&gt;kiln/asm.py&lt;/code&gt;&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;&lt;code&gt;kiln/decode.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;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="为什么反汇编器要独立写"&gt;&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e5%8f%8d%e6%b1%87%e7%bc%96%e5%99%a8%e8%a6%81%e7%8b%ac%e7%ab%8b%e5%86%99" class="header-anchor"&gt;&lt;/a&gt;为什么反汇编器要独立写&#10;&lt;/h2&gt;&lt;p&gt;汇编器决定&amp;quot;一条指令变成哪些字节&amp;quot;，反汇编器决定&amp;quot;这些字节是哪条指令&amp;quot;。&lt;/p&gt;&#10;&lt;p&gt;如果反汇编器照着汇编器的逻辑写，那它只能证明&amp;quot;我按我的想法解释我自己的输出&amp;quot;——这是自证。&lt;/p&gt;&#10;&lt;p&gt;所以两者&lt;strong&gt;互不参照&lt;/strong&gt;。然后测试断言它们在 &lt;code&gt;boot.asm&lt;/code&gt; 的&lt;strong&gt;每一条语句&lt;/strong&gt;上都一致。&lt;/p&gt;&#10;&lt;p&gt;这样，&amp;ldquo;两边以完全相同的方式出错&amp;quot;才会漏过去——比&amp;quot;汇编器没崩溃&amp;quot;强得多。&lt;/p&gt;&#10;&lt;p&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;strong&gt;手算的期望值&lt;/strong&gt;对最终寄存器和内存下断言。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;tests/test_core.py&lt;/code&gt; 里的每个期望值都是手写推导的，不是从模拟器输出里抄的。否则又成了自证。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="测试抓到的-12-个真实缺陷"&gt;&lt;a href="#%e6%b5%8b%e8%af%95%e6%8a%93%e5%88%b0%e7%9a%84-12-%e4%b8%aa%e7%9c%9f%e5%ae%9e%e7%bc%ba%e9%99%b7" class="header-anchor"&gt;&lt;/a&gt;测试抓到的 12 个真实缺陷&#10;&lt;/h2&gt;&lt;p&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;&#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;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;解码器对 &lt;code&gt;0x83 /r&lt;/code&gt; 的立即数宽度判错&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;add ax,1&lt;/code&gt; 编码 3 字节、解码成 4 字节，之后全部错位&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;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;_cond&lt;/code&gt; 里局部变量名与标志位常量同名&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;CF&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;3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;_apply_alu&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;4&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;ALU 的 form 分支反了&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;add ax,[mem]&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;5&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;ModRM 构造把 src/dst 写反&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;add ax,bx&lt;/code&gt; 编成 &lt;code&gt;03 D8&lt;/code&gt;（实为 &lt;code&gt;add bx,ax&lt;/code&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;6&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;解码器方向同样反了&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;汇编器改对后，清单仍显示相反结果，&lt;strong&gt;误导出 3 次错误&amp;quot;修复&amp;rdquo;&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;7&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;_shift&lt;/code&gt; 对 mod=3 的寄存器操作数走了内存写入&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;shl ax,8&lt;/code&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;8&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;0F&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;9&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;C1 /4 ib&lt;/code&gt; 立即数取在 ModRM &lt;strong&gt;之前&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;把 ModRM 字节当移位位数&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;10&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;段寄存器 push/pop 未实现，位判断用错&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;1E&lt;/code&gt;（&lt;code&gt;push ds&lt;/code&gt;）被当成 4 字节 NOP，指令流错位&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;11&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;两遍汇编 + 前向引用猜测 → 标签地址漂移&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;call blit&lt;/code&gt; 跳到标签&lt;strong&gt;后 3 字节&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;12&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;test reg,reg&lt;/code&gt; 用位测试而非 &lt;code&gt;84/85&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;test al,al&lt;/code&gt; 把寄存器当立即数&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;第 5 和第 6 条最值得说：&lt;strong&gt;它们是对称的&lt;/strong&gt;。汇编器把方向搞反，解码器也把方向搞反，于是&amp;quot;汇编→解码&amp;quot;的往返测试&lt;strong&gt;完全通过&lt;/strong&gt;——两个错误互相抵消了。只有拿手算字节去对，才看得见。&lt;/p&gt;&#10;&lt;p&gt;第 11 条促成了根本性重新设计：改成&lt;strong&gt;迭代到收敛&lt;/strong&gt;，并且&lt;strong&gt;加宽单调&lt;/strong&gt;（一旦加宽就不回缩）。否则跳转长度和标签地址会互相影响，永远不收敛。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="引导扇区在裸机上画-mandelbrot"&gt;&lt;a href="#%e5%bc%95%e5%af%bc%e6%89%87%e5%8c%ba%e5%9c%a8%e8%a3%b8%e6%9c%ba%e4%b8%8a%e7%94%bb-mandelbrot" class="header-anchor"&gt;&lt;/a&gt;引导扇区：在裸机上画 Mandelbrot&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;examples/boot.asm&lt;/code&gt; 是一个 512 字节的引导扇区，末尾带 &lt;code&gt;55 AA&lt;/code&gt; 签名。BIOS 把它载入 &lt;code&gt;0000:7C00&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;流程：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;建立 &lt;code&gt;DS/SS&lt;/code&gt; 与栈&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;int 10h&lt;/code&gt; 切到 80×25 彩色文本&lt;/li&gt;&#10;&lt;li&gt;打印横幅&lt;/li&gt;&#10;&lt;li&gt;用 &lt;strong&gt;8.8 定点整数&lt;/strong&gt;渲染 Mandelbrot 到 &lt;code&gt;0800:0000&lt;/code&gt; 的缓冲区（40×24 字符）&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;rep movsw&lt;/code&gt; 整块搬到 &lt;code&gt;B800:0000&lt;/code&gt; 显存&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;hlt&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;缓冲区放在扇区&lt;strong&gt;之外&lt;/strong&gt;有原因：800 个字符塞不进 512 字节。而且放在已知地址后，验证器可以直接从 RAM 里读渲染结果，和&lt;strong&gt;独立的浮点实现&lt;/strong&gt;逐格比对。&lt;/p&gt;&#10;&lt;h3 id="定点乘法两个看着都对的错版本"&gt;&lt;a href="#%e5%ae%9a%e7%82%b9%e4%b9%98%e6%b3%95%e4%b8%a4%e4%b8%aa%e7%9c%8b%e7%9d%80%e9%83%bd%e5%af%b9%e7%9a%84%e9%94%99%e7%89%88%e6%9c%ac" class="header-anchor"&gt;&lt;/a&gt;定点乘法，两个看着都对的错版本&#10;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;imul&lt;/code&gt; 把有符号 32 位积放在 &lt;code&gt;DX:AX&lt;/code&gt;。8.8 定点结果就是该积的 bit 8..23，由两半拼成：&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-asm" data-lang="asm"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;imul&lt;/span&gt; &lt;span class="no"&gt;bx&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;mov&lt;/span&gt; &lt;span class="no"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;dx&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;shl&lt;/span&gt; &lt;span class="no"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="c1"&gt;; bit 16..23 -&amp;gt; bit 8..15&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;shr&lt;/span&gt; &lt;span class="no"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="c1"&gt;; bit 8..15 -&amp;gt; bit 0..7&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;or&lt;/span&gt; &lt;span class="no"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;cx&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;strong&gt;都表现为&amp;quot;逻辑 bug&amp;quot;而不是&amp;quot;算术 bug&amp;quot;&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;只做 &lt;code&gt;shl ax,8&lt;/code&gt;：保留 bit 16..31，&lt;strong&gt;丢掉低字的小数部分&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;对低字做 &lt;code&gt;shr cx,8 / shl ax,8 / or&lt;/code&gt;：&lt;strong&gt;丢掉整数部分&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;这类错误的可怕之处在于：结果总是&amp;quot;差不多但不是&amp;quot;，而&amp;quot;差不多&amp;quot;在图形上表现为轻微的错位，很容易被误认为&amp;quot;渲染问题&amp;quot;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="现在的状态如实说"&gt;&lt;a href="#%e7%8e%b0%e5%9c%a8%e7%9a%84%e7%8a%b6%e6%80%81%e5%a6%82%e5%ae%9e%e8%af%b4" class="header-anchor"&gt;&lt;/a&gt;现在的状态（如实说）&#10;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;已经跑通的：&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;python tests/test_core.py&lt;/code&gt; → &lt;strong&gt;62/62&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;汇编器对 &lt;code&gt;boot.asm&lt;/code&gt; 收敛输出&lt;strong&gt;正好 512 字节&lt;/strong&gt;，签名 &lt;code&gt;55 AA&lt;/code&gt; 正确&lt;/li&gt;&#10;&lt;li&gt;反汇编器与汇编器在冒烟测试的每一条指令上长度一致，逐条核对助记符&lt;/li&gt;&#10;&lt;li&gt;模拟器通过全部条件码、栈/call/ret、段 wraparound、除零故障、BIOS 显存、键盘阻塞/轮询断言&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;strong&gt;尚未完成的：&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;引导扇区的 &lt;strong&gt;Mandelbrot 渲染循环在末尾仍有一处控制流缺陷&lt;/strong&gt;，会跑到扇区外执行。&lt;/p&gt;&#10;&lt;p&gt;定点乘法本身已经被单独验证正确（&lt;code&gt;imul 0xFE00×0xFE00 → 0x0400&lt;/code&gt;），渲染循环也能正确遍历 960 个像素、正确推进缓冲区指针。&lt;strong&gt;出问题的是渲染之后的返回路径&lt;/strong&gt;，&lt;code&gt;tests/test_boot.py&lt;/code&gt; 的 14 项断言目前 10 项通过。&lt;/p&gt;&#10;&lt;p&gt;这一条我没有修完就停手了。&lt;/p&gt;&#10;&lt;p&gt;它属于&amp;quot;能自己走完的收尾&amp;quot;，但它前面连续暴露了 12 个更深的机制性缺陷。我判断继续在同一处反复试错的收益，已经低于把已有成果讲清楚。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="已知的模拟器边界"&gt;&lt;a href="#%e5%b7%b2%e7%9f%a5%e7%9a%84%e6%a8%a1%e6%8b%9f%e5%99%a8%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;已知的模拟器边界&#10;&lt;/h2&gt;&lt;p&gt;这些会&lt;strong&gt;明确报错&lt;/strong&gt;，而不是猜着执行：&lt;/p&gt;&#10;&lt;p&gt;32 位操作数/地址、保护模式、FPU、多数字符串指令、&lt;code&gt;0F&lt;/code&gt; 扩展表、本项目未用到的 BIOS 服务。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="如果继续"&gt;&lt;a href="#%e5%a6%82%e6%9e%9c%e7%bb%a7%e7%bb%ad" class="header-anchor"&gt;&lt;/a&gt;如果继续&#10;&lt;/h2&gt;&lt;ol&gt;&#10;&lt;li&gt;先用一个 &lt;code&gt;trace&lt;/code&gt; 工具把渲染后的返回路径逐条打出来——这是定位最后那个缺陷最短的路。&lt;/li&gt;&#10;&lt;li&gt;给模拟器加&lt;strong&gt;&amp;ldquo;指令边界断言&amp;quot;开关&lt;/strong&gt;：任何一次 &lt;code&gt;step()&lt;/code&gt; 落在清单记录之外就立即报错。本次 12 个缺陷里有 5 个都是靠等价的观察才找到的，做成常驻检查会省很多时间。&lt;/li&gt;&#10;&lt;li&gt;加&lt;strong&gt;收敛性检查&lt;/strong&gt;：断言任意源码都在 N 遍内收敛，并把实际遍数记进清单。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这个项目最后教会我的不是 x86。&lt;/p&gt;&#10;&lt;p&gt;是&lt;strong&gt;&amp;ldquo;能跑&amp;quot;和&amp;quot;对&amp;quot;之间隔着多远&lt;/strong&gt;——中间那 12 个缺陷，每一个都穿着&amp;quot;能跑&amp;quot;的外衣。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item></channel></rss>