⌕ ESC
↑↓ 选择↵ 打开esc 关闭⌘K 唤起
《编码:隐匿在计算机软硬件背后的语言(Charles Petzold · Code)》书封
拆书稿

编码:隐匿在计算机软硬件背后的语言(Charles Petzold · Code) 编码:隐匿在计算机软硬件背后的语言

Charles Petzold · 2000 拆解 28 章 全文约 8.4 万字 预计 3 小时 31 分
书架 · 商业与科技 书摘第一性原理抽象层编码计算机原理冯诺依曼

计算机不是被发明出来的,是被一层一层"搭"出来的——从两个小孩用手电筒隔窗打莫尔斯码开始,Petzold 只用 19 世纪就已存在的零件(电池、导线、灯泡、开关、电报继电器),一步不跳地搭出逻辑门 → 加法器 → 触发器 → 存储器 → 一台能跑循环的真计算机,再往上接出操作系统、高级语言和图形界面。

本书电梯陈述 · 编辑部拟

作者:Charles Petzold(Windows 编程界开创者之一,1994 年七位 Windows Pioneer 奖得主中唯一的作家)|英文原版 2000 年|中译本约 44.2 万字|实测研读:基于中译本全文 25 章 + 三篇序逐章精读,无跳读、无据小标题概括的章节|本拆书稿实测约 10.7 万字(净正文约 8.7 万字),研读约 250 分钟

0. 30 秒 TL;DR TL;DR
  1. 一全书的引擎是一句话:两个不同的事物,只要经过适当的组合,就可以表示所有类型的信息。手电筒的亮/灭、点/划、凸/平、通/断、0/1 —— 全书 25 章都是这句话的展开。
  2. 二叙事链一句话说完:手电筒 → 莫尔斯码 → 布莱叶盲文 → 电路 → 接地省线 → 电报继电器(由电流而非人手控制的开关) → 二进制 → 布尔代数 → 香农 1938:串联=AND,并联=OR → 逻辑门 → 加法器 → 用补数做减法 → 反馈→振荡器→触发器→锁存器→计数器 → 字节与十六进制 → RAM 阵列(地址=邮箱号) → 第 17 章:条件跳转诞生,机器变成计算机 → 芯片史 → 8080/6800 → ASCII → 总线与外设 → 操作系统 → 浮点数 → 高级语言 → 图形界面与互联网。每一环都只用上一环已经造好的东西,一个零件都没有从天而降。
  3. 三全书的分水岭在第 17 章:能否控制重复操作(循环),是计算机(computer)和计算器(calculator)的区别。而实现循环只需要两个新零件:一个零锁存器(加法器结果全 0 时置 1)和一条条件跳转指令。没有那个"机器自己能读的成败信号",就只有跑不完的无限跳转。
  4. 四TANSTAAFL(天下没有免费的午餐):第 17 章为了让机器通用(指令自带地址、代码与数据共用一个 RAM),取指令要花 3 个时钟周期、整机速度掉到原来的 1/4。通用性永远是拿吞吐换来的,这条工程定律在全书被点名。
  5. 五"编码"的定义比想象的宽:编码 = 一种在机器和人之间传递信息的方式,编码就是交流。莫尔斯码、盲文、条形码、胶卷上的 DX 编码、ASCII、机器码、汇编、HTML —— 都是同一件事的不同实例。而好编码有两个共性:按频率分配长度(常用字符给短码)和刻意的冗余(用来抗噪与检错)。
  6. 六换档码 / 逃逸码是全书最古老的陷阱:Baudot 电传码用"数字转义"切换后续字节的含义,结果同一串数据第二次输入打印机时全变成乱码——因为状态是隐式的、且跨消息保留。ASCII 用 7 位无状态编码把这个问题彻底解决掉。
  7. 七"被自己的感知模式禁锢"是本书反复敲打的错误:视力健全的霍伊给盲人做凸起的印刷字母(因为"A 就该摸起来像 A"),失明的布莱叶做出 2×3 点阵(64 个码字全部用满)。莫尔斯坚持要在纸上输出硬拷贝,操作员却直接改用耳朵听"滴-嗒"。新的感知通道要用它自己的形式,不能照搬旧介质。
  8. 八香农 1938 那个 84 年的空白:布尔代数 1854 年就有了,电报机 1844 年就演示成功了,白炽灯 1879 年就有了 —— 但把"AND/OR"和"串联/并联"接上,要等到 1938 年香农的硕士论文。连巴贝奇都没做到(他与布尔同时代且了解他的工作,却用了一辈子齿轮和杠杆)。真正的机会常常不是新元件,而是两个成熟领域之间没人连上的那根线。
  9. 九"晶体管让计算机更简单了吗?答案是否定的"(第 18 章):晶体管只是让同样的空间塞下更多逻辑门,互连问题一样难甚至更难。集成电路真正解决的是"把常用组合预先连好"。这是关于"底层能力变强时上层脚手架会怎样"的最干净的历史答案。
  10. 十抽象层是全书真正的主角:继电器 → 门 → 加法器 → 触发器 → 存储器 → 处理器 → 机器码 → 汇编 → 高级语言 → API → GUI。每上一层,能力变强、可见度变差、速度变慢。Petzold 的立场是"我们关注的是让一切有效地运转而不是以最简的形式运转"——先跑通,再简化。
  11. 十一第一性原理是有成本的:用继电器搭一个 64KB 的 RAM 阵列需要约 500 万个继电器(每存 1 比特要 9 个),Petzold 自己承认"不论在 100 年前还是在今天都是令人感到荒唐的事"。书的价值是让你知道那一层长什么样,不是让你真去搭它。
  12. 十二首尾呼应:全书最后一句回到第 1 章的手电筒——光纤里的光子,比起为了和一街之隔的好友深夜交流而绞尽脑汁想出的闪光灯,速度无与伦比。500 页只讲了一件事:人想说话,于是造出了所有这些东西。
1. 全书结构脉络 STRUCTURE

Petzold 在作者序里给出了全书的组织原则:计算机拥有与生俱来的层次化体系结构,底层是晶体管,顶层是显示器上呈现的信息;本书就是自底向上分析这个结构的每一层。 更狠的一个约束是:前 17 章里出现的所有设备,理论上都可以用已经存在了一个世纪的简单电子器件来构造。 这不是复古趣味,而是方法论——追溯得越久远,技术的脉络就越清晰。

全书可以切成五段,它们不是并列的主题,而是一条不可打乱的搭建链:

  • 第一段(1–3 章)· 编码是什么:先在没有任何电学知识的前提下,把"编码"这个概念立起来。手电筒 → 莫尔斯码 → 组合学(码字数 = 2^位数)→ 布莱叶盲文(64 个码字如何被用满,以及换档码 / 逃逸码)。这一段建立的是全书的词汇表:二进制、码字、频率设计、冗余、转义。
  • 第二段(4–9 章)· 从电到比特:手电筒的剖析(电路是回路、欧姆定律、开关只有两态)→ 绕过拐角(公用连接省 25% 的线、用地球当导体再省一根,以及长导线的电阻这个致命瓶颈)→ 电报机与继电器(全书最关键的零件)→ 十进制的任意性 → 八进制/四进制/二进制 → 比特作为信息的最小单位(黄丝带、提灯、DX 编码、UPC)。这一段把"物理世界"和"信息世界"焊在一起。
  • 第三段(10–17 章)· 从比特搭出计算机(全书的心脏):布尔代数 → 香农的等价关系 → 逻辑门 → 加法器 → 补数减法 → 反馈/振荡器/触发器/锁存器/计数器 → 字节与十六进制 → RAM 阵列与控制面板 → 第 17 章:自动操作。这八章是一条严丝合缝的链子,中间抽掉任何一节,后面全部塌掉。读这本书如果只读八章,就读这八章。
  • 第四段(18–22 章)· 真实世界的实现:从算盘到芯片(一部压缩的计算史 + 摩尔定律 + TTL 数据手册的自曝——前面所有"我发明的"组件其实都是从 1973 年的《TTL 数据手册》里借来的)→ 8080 与 6800 → ASCII 与字符编码 → 总线与外设(存储器板、视频显示、键盘、磁盘)→ 操作系统(CP/M → MS-DOS → UNIX)。这一段把第 17 章那台想象中的机器,对回到真实的硅片和产品。
  • 第五段(23–25 章)· 往上再爬三层:浮点数(IEEE 754 与它的精度谎言)→ 高级语言(汇编器/编译器/ALGOL 谱系)→ 图形化革命(从万·布什的麦克斯储存器到 GUI、多媒体、互联网、Java)。这一段是从"机器"到"人"的最后三级台阶。

贯穿全书的四条主线:

  1. "两态即万物"——从第 1 章的两种闪烁到第 25 章的光纤,全书只有一个物理前提。
  2. 抽象层的纪律——每一层只用下一层已建成的东西;每上一层就换一套词汇(继电器→门→加法器→ALU→指令→语句→API)。Petzold 甚至会在新一层出现时明确宣布:"本书从这里开始,继电器将会极少出现了。"
  3. 编码的两个永恒问题:长度与状态——按频率分配长度(莫尔斯、UPC、压缩),以及状态该显式还是隐式(换档码的灾难 → ASCII 的无状态 → Unicode 的代价)。
  4. 每一次进步都要标价——用地球省线要付出"低压不行"、通用指令格式要付出"速度 1/4"、浮点要付出"262144.00 和 262144.01 是同一个数"、Unicode 要付出"文件大小翻倍"、高级语言要付出"用不上处理器的移位指令"。Petzold 从不白送好处。

条目可点,跳到对应章
2. 逐章深拆
01推荐序(丁秋林)· 大方无隅,大象无形02译者序(薛佟佟)· 为什么这本书值得被译03作者序 · 这本书到底讲什么04第 1 章 · 至亲密友(手电筒与莫尔斯码)05第 2 章 · 编码与组合(发送易,接收难)06第 3 章 · 布莱叶盲文与二进制码(不要被自己的感知模式禁锢)07第 4 章 · 手电筒的剖析(电学的最小必要集)08第 5 章 · 绕过拐角的通信(省线,然后撞墙)09第 6 章 · 电报机与继电器(全书最关键的零件)10第 7 章 · 我们的十个数字(十进制的任意性)11第 8 章 · 十的替代品(卡通人物、龙虾、海豚)12第 9 章 · 二进制数(1 比特能装下多少东西)13第 10 章 · 逻辑与开关(布尔代数撞上电路)14第 11 章 · 门(从继电器到可组合的积木)15第 12 章 · 二进制加法器(144 个继电器)16第 13 章 · 如何实现减法(把减法变成加法)17第 14 章 · 反馈与触发器(电路第一次拥有记忆)18第 15 章 · 字节与十六进制(为什么是 8,为什么是 16)19第 16 章 · 存储器组织(地址就是邮箱号)20第 17 章 · 自动操作(全书的顶点:机器变成计算机)21第 18 章 · 从算盘到芯片(一部压缩的计算史,以及作者的自曝)22第 19 章 · 两种典型的微处理器(8080 与 6800)23第 20 章 · ASCII 码和字符转换(编码的政治学)24第 21 章 · 总线(把一台机器变成一个系统)25第 22 章 · 操作系统(从水泡到 API)26第 23 章 · 定点数和浮点数(精度是一种代价)27第 24 章 · 高级语言与低级语言(离开机器码)28第 25 章 · 图形化革命(回到"信息"这个主角)
2. 逐章深拆 CHAPTERS
01

推荐序(丁秋林)· 大方无隅,大象无形

  • 核心主张:科学不等同于晦涩,最复杂的理论也可用最简单的方式描述。全书用电线、灯泡、触发器这些常见零件"拼拼接接",最后令人惊异地建造了一台电子计算机。
  • 给读者的定调:这本书的读法是跟着作者一起搭,而不是听他讲。序作者引老子"为学日益,为道日损"作结——读到最后,你手上的零件反而应该越来越少。
02

译者序(薛佟佟)· 为什么这本书值得被译

  • 最好的观察:Petzold 的叙述"奇特却又如此自然"——他先从历史角度审视技术发展脉络,再自底向上讲体系结构;书里旁征博引,从当红影评人到百年之前的布莱叶盲文,从滑尺到冯·诺依曼,从《思维之际》到图形化革命,看似信手拈来,然则处处别出心裁。
  • 值得留意的细节:译者说翻译过程中不断"有动手一试的冲动"。这是全书最好的读者反馈——一本原理书能让人想动手,说明它的每一步都真的可执行。
03

作者序 · 这本书到底讲什么

  • 最好的段落(全书的宣言):Petzold 说他反复问自己"这本书是讲什么的",最后承认:"这本书其实是讲述计算机如何工作的。"但他立刻补一句:它不是讲"计算机—如何—工作"——书里没有一张又一张画着箭头的磁盘插图,也没有那种装载着一节又一节"0,1 数据"的"火车"图片(这句话他在序末又重复了一遍)。理由极硬:明喻与暗喻是文学描述中精妙的辅助手段,但它们常常掩盖了科学技术的真正光芒。
  • 他用来举例的那个问题:常有人问"存储器(storage)与内存(memory)有何区别",标准答案是"内存像你的办公桌,存储器像你的文件柜"。Petzold 明确说他对这个答案不满意——因为它把计算机体系结构和办公室结构等同起来,而两者的真实区别是逻辑层面的:它体现的是体系结构的需求与存储介质客观性能之间的矛盾(找不到一种同时具备速度快、容量大、非易失的介质),而这个区分正是冯·诺依曼体系结构不足的产物。

    ⚑ 全书最重要的方法论伏笔:他不是禁止比喻,而是要求先把机制讲透,比喻才允许出现。第 21 章讲完磁盘与 RAM 的真实差别之后,他自己就用了"办公桌 vs 文件柜"这个比喻,而且说"这个比喻恰到好处"。同一个比喻,讲之前用是遮蔽,讲之后用是压缩。

  • 他给这本书的两个自嘲式坐标:bit 的定义出现在第 70 页,byte 的定义出现在第 186 页,晶体管直到第 146 页才被介绍,而且只是顺便提及。以及:"Code 是这样一本书,它永远不会被命名为诸如 The Faster New Faster Thing 或 Business @ the Speed of a Digital Nervous System。"

04

第 1 章 · 至亲密友(手电筒与莫尔斯码)

  • 最好的故事(全书的开场,也是全书的收尾):你 10 岁,最好的朋友住在街对面,两人卧室的窗户正好相对。父母一喊"关灯",交流就断了。于是你们用手电筒。
  • 三次失败的迭代(这段是全书方法论的缩影):
    1. 用光在空中画字母——立刻失败,"这些旋转和倾斜的光线都太不准确了",大脑无法把复杂笔画组合出来。
    2. 闪 N 次代表第 N 个字母(A 闪 1 次,Z 闪 26 次)——可行但代价惊人:发一句 "How are you?" 要闪 131 次,而且还没给问号编码。
    3. 莫尔斯电码——引入两种闪烁(短闪=点、长闪=划),同一句话只要 32 下,且已经包含了问号的编码。
  • 核心主张:编码 = 一种在机器和人之间传递信息的方式。换句话说,编码就是交流。 口头话语、书面文本、手语、盲文、速记,全都是编码;一种编码有用,是因为它能用在其他编码无法取代的地方。
  • 莫尔斯码不是随意的:简短的编码分配给使用频率高的字母(E、T),冷僻字母(Q、Z)分配长编码。这是"按频率分配长度"这个原理在全书第一次出现(后面在 UPC、ASCII、数据压缩里会一再回来)。
  • 它的两个真实缺陷:不区分大小写;实际速度每分钟 5–6 个单词,而说话约 100 个单词/分钟。
  • 收束:问题的关键就在于数字 2。两种闪烁,两种声音。两个不同的事物,只要经过适当的组合,就可以表示所有类型的信息。
05

第 2 章 · 编码与组合(发送易,接收难)

  • 最好的问题:莫尔斯码发送比接收简单得多——因为你手上只有"字母 → 点划"的表,没有"点划 → 字母"的反查表,而这些点划序列看上去毫无规律,反查表根本没法按字母顺序排。
  • 解法(全书第一次"换一个组织维度"):忘掉字母顺序,按点划的数目来分组。
    • 1 位 → 2 个码字(E、T)
    • 2 位 → 4 个(I、A、N、M)
    • 3 位 → 8 个
    • 4 位 → 16 个
    • 合计 2+4+8+16 = 30 个码字,比拉丁字母的 26 个多出 4 个——多出来的 4 个正好是重音字母。
  • 规律与公式:每张表的码字数是前一张的两倍(因为新码字 = 旧码字 + 一个点 或 + 一个划)。于是:码字的数目 = 2^(编码的位数)。5 位 → 32(莫尔斯码的数字确实用 5 位);6 位 → 64,累计 126 个,对莫尔斯码来说太多,会留下大量"未定义"码字——而收到未定义码字,就说明发送方出错了(检错的雏形)。
  • 树形图解码:从左向右沿点/划分支走,"点-划-点"三步走到 R。Petzold 点明:构建这样一张表对于定义莫尔斯码规范本身就是必要的——它保证不会给两个字母定义相同码字,也保证不浪费编码长度。
  • 收束:莫尔斯码也被称作二进制码,因为它的组成元素只有两个。这一章做的分析属于组合学(组合分析)。
06

第 3 章 · 布莱叶盲文与二进制码(不要被自己的感知模式禁锢)

  • 最好的故事(全书最锋利的一刀,且只有一段):巴黎皇家盲人学校的创始人瓦伦丁·霍伊发明过一种在纸上印凸起印刷字母的系统,难用且书极少。Petzold 的判词极狠:"霍伊先生视力健全,因此他被自身的感知模式所禁锢。对他来说,一个字母 A 就是一个 A,而且字母 A 在记录时也必须看起来(或者感觉起来)像一个 A。" 他甚至补一句:如果让霍伊用手电筒交流,估计他也会像我们最开始那样,试图在空中比划出字母——而我们已经知道那法子根本不灵。
  • 真正的突破来自两处外部:法国军官 Charles Barbier 1819 年为了让士兵在黑暗中无声传信,发明了凸起点划的"夜间书写"(écriture nocturne);缺陷是它按读音编码,太啰嗦,能传短消息但编不了整本书。刘易斯·布莱叶(1809 生于库普雷,3 岁在父亲的马具工作间被锥子刺伤一眼、感染致双目失明,10 岁进巴黎皇家盲人学校,12 岁接触巴比尔系统,15 岁完成自己的系统,1852 年 43 岁死于肺结核)把它改成 2×3 = 6 个点的字符单元。
  • 核心推演(把第 2 章的公式直接套上):6 个点,每点两态 → 2⁶ = 64 个码字。全章其余部分就是回答一个问题:这 64 个码字是怎么被用满的?
    • a–j:只用上面 4 个点(1、2、4、5)
    • k–t:在 a–j 基础上加第 3 点
    • u–z:加第 3、6 点
    • a–j + 第 6 点 → 字符串缩写 + 补上法语里没有的 w
    • a–j "降位"到 2、3、5、6 点 → 标点符号(逗号、分号、冒号、句号;左右括号共用一个码字,但开闭引号用不同码字)
    • 剩下的 → 缩写、音调符号、小数点/强调符
    • 累计 51 → 到最后 7 个码字用完,一个都不浪费。
  • 两个会贯穿全书的概念,在这里第一次出现:
    • 换档码 / 优先码(shift / precedence code):数字标识符出现后,后续码字全部解释为数字,直到遇到字母标识符为止——它改变的是一段作用域的含义。
    • 逃逸码(escape code):大写标识符只影响紧随其后的那一个字母。"逃逸码让你'逃离'对编码串单调的、一成不变的解析,而转入一种新的解析方式。"
  • 为什么这一章不能跳:换档码的跨消息隐式状态会在第 20 章制造一场真实的灾难,而 ASCII 正是为了消灭它才选择了 7 位。
07

第 4 章 · 手电筒的剖析(电学的最小必要集)

  • 核心主张与纪律:电路就是一个回路——从电池到导线到灯泡到开关再回到电池,任何中断都会让灯灭。开关的作用就是控制这个中断。
  • Petzold 在这里立了一条规矩:"水和水管"这个比喻经常被用来解释电流,但这种模型就如同所有其他的模拟一样最终也无法自圆其说。理由是:在茫茫宇宙中,电不同于任何其他的事物,我们必须用它的术语来解释它。(注意:他后面还是用了水管比喻,但只在把电压、电流、电阻三个量都定义清楚之后才用——又一次"先讲机制,后给比喻"。)
  • 最小必要集:原子/质子/电子;electron 与 electricity 都源于古希腊语 ηλεκτρον,意思其实是"琥珀"(古希腊人用琥珀摩擦羊毛产生静电);导体(铜、银、金——最外层只有一个电子,且它们在元素周期表同一列不是巧合)与绝缘体;电池串联 3V、并联 1.5V 但寿命翻倍;欧姆定律 I = E / R;功率 P = E × I(手电筒 3V ÷ 4Ω = 0.75A = 2.25W)。
  • 一个语言学彩蛋:"闭合"和"断开"用来描述开关时,含义与描述房门时恰好相反——一扇闭合的门阻止一切通过,一个闭合的开关却使电流导通。
  • 收束(本章存在的唯一理由):开关只能闭合或断开,电流只能有或无,灯泡只能亮或不亮。就像莫尔斯和布莱叶发明的二进制码一样,没有介于二者之间的状态。
08

第 5 章 · 绕过拐角的通信(省线,然后撞墙)

  • 最好的场景:你 12 岁,最好的朋友搬走了,新的密友是隔壁邻居——但两家卧室的窗户朝向同一个方向,手电筒完全没用。于是你在自己房间放电池和开关,从窗户拉两根线绕过围墙,接到朋友房间的灯泡上。
  • 三次省线(一条完整的优化链):
    1. 双向通信 = 两套独立电路 = 4 根线。
    2. 公用连接(common):把两个电池的负极连在一起,两个回路仍相互独立 —— 4 根变 3 根,省 25%。Petzold 逐一验证了四种开关组合下的电流路径(包括两个开关同时闭合时公用段无电流)。
    3. 用地球当第三根导线:泥土不是好导体,但地球的截面积实在太大。做法是砸一根至少 8 英尺长、1/2 英寸粗的铜柱电极入地(提供约 150 平方英寸的接触面),或直接接在从室外地下接来的铜水管上。3 根变 2 根。
  • 一个被换掉的心智模型:与其把地球当成"一根线"(否则你会困惑"电子怎么知道要去哪"——其实它们不知道),不如把它当成电子的来源与储藏库:地球之于电子,就恰如海洋之于水滴。 大地也因此是零电势点(就像已经放在地面上的砖,没有空间再下落了)。
  • 代价必须付:这招对 1.5V 干电池 + 小灯泡不成立——对低电压电池而言,地球的电阻实在太高了。要用地球,就必须上高压 + 大电阻灯泡。(此后全书用大写 V 代替"负极接地的电池",并给了一个好用的意象:把 V 想象成电子吸尘器,把地面想象成电子的海洋。)
  • 本章真正的价值:制造出全书的第一个死局。铜不完美,线越长电阻越大,电流越小,灯越暗。20 号电话线每 1000 英尺约 10Ω;铺一英里,电阻超过 100Ω,电流掉到 0.03A 以下,灯根本亮不起来。加粗导线很贵,升压加大灯泡有限度。这正是 150 年前铺设跨美洲电报线路时的真实问题:系统跨度的设计极限是 200 英里,而纽约到加利福尼亚有数千英里。
  • 收束:这个难题的解决方案,"尽管它只不过是个很简陋的装置,但是正是基于这个装置,整个计算机系统才被构建出来"。
09

第 6 章 · 电报机与继电器(全书最关键的零件)

  • 人物:萨缪尔·莫尔斯 1791 年生于马萨诸塞(那一年莫扎特完成《魔笛》后不久去世),耶鲁毕业,在伦敦学艺术,是一位成功的画家(《拉法耶特将军》至今挂在纽约市政大厅),1836 年以独立候选人身份竞选纽约市长得票 5.7%,还是早期摄影爱好者——他跟达盖尔本人学过银板照相术,并在 1840 年把这项技术教给了 17 岁的马修·布雷迪(后者拍下了美国内战与林肯)。
  • 电磁铁:铁棒上绕几百圈细导线,通电即成磁铁,断电即失去磁性。这就是电报机的基础。1844 年 5 月 24 日华盛顿—巴尔的摩线路发出第一条消息:"What hath God wrought!"
  • 一个被 Petzold 特意点名的错误:莫尔斯最初坚持电报机必须在纸上写出东西("输出一份硬拷贝"),于是用电磁铁控制一支钢笔在滚动的纸带上画点划。Petzold 的批注是:"注意,莫尔斯现在一头扎进了一个需要纸张和阅读的模型,这很像瓦伦丁·霍伊的观点——盲人用的书应该印有凸起的字母。" 而现实是:电报操作员们很快发现,直接听钢笔弹起落下的声音就能翻译电码,于是钢笔被废弃,换成了纯发声器(快"滴-嗒"=点,慢"滴——嗒"=划)。"我们人类是个懒惰的物种" ——这句自嘲后面是一个严肃的教训:发明者的介质偏好,往往是产品里最先该被扔掉的部分。
  • 最好的故事(全书最漂亮的一个"发明的瞬间"):为了解决第 5 章的距离死局,只能设中继站。想象你被电报公司雇用,扔在纽约与加利福尼亚之间一个只有一桌一椅的无名小屋里:东窗进来一根线接发声器,你的电键接电池、线从西窗出去。你的工作是听着滴答声、同时用手指把消息转发出去。熟练之后的某一天,你瞄了瞄发声器上上蹿下跳的横杠,又看了看自己在电键上翻飞的手指,来回瞅了几次,恍然发现——两者的节奏完全一致。 于是你到外面找了根小木棍,用木棍和细绳把发声器和电键绑在了一起。设备开始自己工作,下午剩余的时间你可以去钓鱼了。
  • 这就是继电器:传入的弱电流驱动电磁铁,电磁铁拉下金属簧片,金属簧片本身是另一个开关的一部分,接通电池和输出线路——弱电流被"放大"成强电流。
  • 本章为什么是全书的枢纽:继电器是一个由电流控制、而不是由人手控制的开关。Petzold 直接把话挑明:"继电器是一个意义非凡的设备……使用它,你甚至可以装配好一台近乎完整的计算机!" 然后他半开玩笑地说:这项发明如此出色,以至于电报博物馆也为它留有一席之地——让我们悄悄拿起一台,藏在外套里,飞快地从警卫身边溜过去。不过在使用它之前,我们还要学会如何计数。
10

第 7 章 · 我们的十个数字(十进制的任意性)

  • 核心主张:数字是我们平常能接触到的最抽象的编码。而以 10 为基数完全是随意的——因为人有 10 根手指(英语 digit 同时是"手指/脚趾"和"数字",five 和 fist 同词根,都不是巧合)。
  • 罗马数字的死因:I V X L C D M 加减尚可(五个 I 是一个 V,两个 V 是一个 X),所以在欧洲记账里用了很久;但乘法和除法极其复杂。古希腊数字系统同病——所以古希腊人以几何学著称,却不以代数著称。
  • 阿拉伯(印度-阿拉伯)数字的三点不同(由花拉子米约公元 825 年的著作带入欧洲,algorithm 即由他的名字衍生;拉丁译本约 1120 年):
    1. 位置相关——数字的位置和数字本身一样重要(甚至更重要)。
    2. 没有表示 10 的专门符号。
    3. 有 0。"小小的一个零无疑是数字和数学史上最重要的发明之一"——它支持位置计数法(区分 25、205、250),并极大简化了乘除。
  • 拆解方式:4825 = 4×10³ + 8×10² + 2×10¹ + 5×10⁰;小数点右边则对应 10 的负整数次幂(42,705.684)。任何长度的十进制加法都能拆成一系列"两个一位数相加"——这就是为什么你小时候被逼着背加法表和乘法表。
  • 本章真正的落点(承上启下):位置计数系统的好处不在于它有多好用,而在于对非十进制的系统它同样成立。为了论证这一点,他抛出一个荒诞而好用的引子:大多数卡通人物每只手只有 4 根手指,所以它们需要一个八进制系统。
11

第 8 章 · 十的替代品(卡通人物、龙虾、海豚)

  • 最好的教学装置:用三种"生物"把非十进制系统一路推到底——
    • 卡通人物(每只手 4 指,共 8 指)→ 八进制:没有 8 和 9 的符号,7 之后直接是 10(读作"一零")。
    • 龙虾(前爪末端是螯,共 4 只)→ 四进制。
    • 海豚(两个鳍)→ 二进制:只有 0 和 1,1 之后是 10。
  • 大量对照(Petzold 用它们建立"同一个量、不同写法"的直觉):白雪公主的小矮人 7₁₀ = 7₈;卡通人物的手指 8₁₀ = 10₈;贝多芬的交响曲 9₁₀ = 11₈;一副纸牌 52₁₀ = 64₈;棋盘格数 64₁₀ = 100₈;美式足球场 100₁₀ = 144₈;孟菲斯市面积 256₁₀ = 400₈。
  • 关键观察(本章存在的理由):八进制里那些"好整数"100₈、200₈、400₈,在十进制里分别是 64、128、256 —— 全是 2 的整数次幂。因为任何 2 的整数次幂乘以 2 的整数次幂仍是 2 的整数次幂。"最右列的好整数暗示我们,十进制以外的数字系统可能对使用二进制码有所帮助。"
  • 二进制的两个反直觉点:① 二进制数不是数值增长得快,而是长度增长得快(一千二百万写成二进制是 24 位);② 为了可读,通常四位一组用连字符或空格分开(1011-0111-0001-1011-0000-0000)——这个"四位一组"的习惯,正是第 15 章十六进制存在的伏笔。
  • 一条会在第 14 章变成硬件的规律:观察前 16 个二进制数的每一竖列——最右列在 0 和 1 之间交替;右数第二列每两个交替;下一列每四个;再下一列每八个。换个说法:每当某一位由 1 变为 0,紧挨着的高位就会翻转一次。(第 14 章的分频器/计数器就是把这条规律直接做成电路。)
  • 收束(把三段合上):电线(有电流=1)、开关(闭合=1)、灯泡(亮=1)、电报继电器(闭合=1)——都可以表示二进制数字。以及一个词源:约 1948 年,美国数学家 John Wilder Tukey 意识到二进制数会越来越重要,想给 binary digit 起个短词,考虑过 bigit 和 binit,最终选了 bit。
12

第 9 章 · 二进制数(1 比特能装下多少东西)

  • 两个开场故事:① Tony Orlando 1973 年的歌——在老橡树上系一条黄丝带就是"是",不系就是"不",他没有唱"如果你想考虑一下就系半条"。② 朗福罗诗里的保罗·里威尔:一盏提灯=英军从陆路来,两盏=从海路来,不挂灯=没有入侵。
  • 保罗·里威尔那一段的真正洞见:三种可能其实一盏灯不够、两盏灯有余(2 比特能表达 4 种)。Petzold 指出这不是浪费,而是冗余(redundancy)——用来抵消噪声。噪声在这里是黑夜的微光和距离;诗里那句"徘徊、凝视,直到看清所有的灯"正说明里威尔并没有在分辨哪盏灯先亮。
  • 核心定义:信息是指多个可能性中的一种。 所有可以被转换成"对两种或多种可能性的选择"的信息,都可以用比特表示。(他也诚实地划了边界:人类有很多形式的交流不能被表示成非此即彼的选择——"这就是人类为什么没有与计算机之间创建起浪漫的关系的原因。")
  • 一串递进的例子:影评人西斯科和艾伯特的两个大拇指 = 2 比特(4 种);加上你自己 = 3 比特(8 种);李纳德·马丁的 1–4 星带半星(且不给一星、改给"BOMB")= 7 种 → 3 比特,其中 111 未定义——如果出现就说明出错了,因为人不会给出这样的评分;《娱乐周刊》A+ 到 F 共 13 级 → 4 比特,剩 3 个编码没用到。
  • 两个"比特就在你身边"的实证案例(本章最出彩的部分):
    • 35 毫米胶卷上的 DX 编码:卷轴上那些银黑交错的方格就是 12 个比特(银=1、黑=0,方格 1 和 7 恒为银)。ASA 感光度共 24 个等级 → 需要 5 比特(2⁴=16 不够,2⁵=32 够)。相机内部有 6 个金属触点对准方格 1–6,银色方格是暗盒的金属(导体),黑色方格涂有颜料(绝缘体)——电流从方格 1 流入,能否流经 2–6 决定了读数。廉价相机只读方格 2、3。
    • UPC 条形码:整个 UPC 就是一串 95 位二进制数。结构是:左护线 101(让扫描仪确定单个比特的条宽,否则所有包装上的 UPC 都得是指定尺寸)→ 6 组 7 位 → 中间护线 01010(固定模式,是内置的检错码,找不到它就拒绝解码) → 6 组 7 位 → 右护线 101。左侧编码以 0 开头 1 结尾且是奇校验;右侧编码是左侧的补码,以 1 开头 0 结尾且是偶校验——因此扫描仪只要看第一个数是不是偶校验,就知道自己是不是在反向扫描。Campbell 10¾ 盎司罐装鸡汁面汤的 UPC 解出来是 0 51000 01251 7:首位是数字系统符(0=常规、2=需称重的肉/农产品、5=票券),中间 5 位是制造商编码(51000=Campbell),再 5 位是商品编码,末位是模校验字符,算法是 3×(奇位之和) + (偶位之和),再用最近的 10 的整倍数减去它(本例 3×4+11=23,30−23=7)。UPC 里不包含价格——价格在商店的计算机里。
  • 冗余的账:0–9 本来 4 比特就够,UPC 每个数字用了 7 比特;加上两侧空白,整个 UPC 用 113 个比特编码 11 个有效的十进制数,平均每个数字超过 10 比特。Petzold 直说这是必要的:"这种商品编码如果能够被顾客用笔轻易地改动,那么这种产品编码措施也就没有任何意义了。"
  • 收尾的两次转译:他把莫尔斯码重写成比特串(点=1,划=11,间隔=0,字母间以两个 0 结尾)——"看起来就像 UPC 的一个截面";把布莱叶盲文的 "code" 写成 100100 101010 100110 100010。收束句:比特从根本来说是数字;用比特表示其他信息时我们要做的,就是计算有多少种可能性。 而最后一句把门推向下一章:比特在逻辑学中也很重要——真和假同样可以表示为 1 和 0。
13

第 10 章 · 逻辑与开关(布尔代数撞上电路)

  • 起点:亚里士多德的三段论(《工具论》,公元前 4 世纪)。Petzold 特意举了一个结论不明显的例子(19 世纪数学家查尔斯·道奇森,即刘易斯·卡罗尔所出):"所有哲人都是符合逻辑的;一个没有逻辑的人总是非常顽固的。"→ 结论是"一些顽固的人不是哲人",而"一些"这个词的出现完全出乎意料。
  • 人物:乔治·布尔(1815 年生,父亲是鞋匠、母亲做过女佣,按当时英国的等级制度他本该一辈子做鞋匠;靠父亲对科学数学文学的兴趣与自身的探究精神,年少时学了拉丁语、希腊语和数学,1849 年成为爱尔兰皇后书院数学系首席教授;1864 年因冒雨赶去上课感染肺炎去世,年仅 49 岁)。著作:《逻辑的数学分析》(1847)、《思维规律的研究》(1854,通称《思维规律》)。书名本身就是雄心:如果找到用数学描述逻辑的方法,也就找到了用数学描述大脑如何工作的方法——"当然,现在这种观点显得很是天真(然而在那个时代这确实是非常先进的思想)"。
  • 布尔代数的核心跳跃:布尔的天才在于把代数从"数"的概念中抽离出来。操作数不是数字,而是类(集合)。用猫做例子:M=公猫、F=母猫、T=褐、B=黑、W=白、O=其他色、N=已绝育、U=未绝育。
    • + = 并集(B+W = 所有黑猫和白猫)
    • × = 交集(F×T = 所有褐色母猫)
    • 1 = 全集(M+F=1;T+B+W+O=1;N+U=1)
    • 0 = 空集(F×M=0)
    • 1−M = 除公猫外的所有猫 = F
    • 矛盾律:F + (1−F) = 1,F × (1−F) = 0
  • 两条把它和传统代数彻底分开的式子:F × F = F(母猫和母猫的交集还是母猫;布尔本人认为 X² = X 就是区分他的代数与传统代数的那条语句)和 F + F = F。另外 1 + F = 1(所有猫与母猫的并集是所有猫)在传统代数里毫无意义。
  • 用它解三段论:"所有人都难逃一死" → P×M = P;"苏格拉底是人" → S×P = S。代入并用结合律:S×(P×M) = (S×P)×M = S×M = S —— 苏格拉底必有一死。(Petzold 还顺手指出:若 S×M=0 则苏格拉底不死;若 S×M=M 则只有苏格拉底会死、其他一切不死。)
  • 最好的案例(本章与下一章共用,也是全书最有辨识度的道具):你走进宠物商店说:"我想要一只公猫,已绝育的,白色或褐色都可以;或者一只母猫,也要是已绝育的,除了白色任何颜色都可以;或者,只要是黑猫就可以。" 店员翻译成:
    (M × N × (W + T)) + (F × N × (1 − W)) + B
    
    然后把 + 读作 OR、× 读作 AND、1− 读作 NOT,把字母赋值为 0/1(1=符合、0=不符合),逐只猫代入化简:
    • 未绝育的褐色公猫 → 结果 0(不要)
    • 已绝育的白色母猫 → 结果 0(不要)
    • 已绝育的灰色母猫 → 结果 1(带回家,而且还是只漂亮的小猫)
  • 然后是全书最关键的一次连接:当晚小猫在你腿上睡着时,你突发奇想——能不能用开关和灯泡来判断一只猫符不符合标准?
    • 两个开关串联:只有两个都闭合灯才亮 → 关键词是"与" → 这与 AND 表完全一样。
    • 两个开关并联:任一个闭合灯就亮 → 关键词是"或" → 这与 OR 表完全一样。
    • 于是按表达式接出 8 个开关(每个 × 对应一处串联、每个 + 对应一处并联),三只猫依次验证,第三只点亮了灯泡。
  • 本章最重的一段是历史,不是技术:乔治·布尔从没接过这样的电路——他对此毫无兴趣,而且他死后 15 年人类才发明白炽灯。但是——莫尔斯早在 1844 年就论证了电报机可行,比《思维规律》早 10 年,把发声器换成灯泡本应非常简单。然而:

    在 19 世纪,没有人将布尔代数中的 AND 和 OR 同线路中的开关串联及并联关联到一起。没有这样的数学家,没有这样的电学家,没有这样的电报员,没有这样的人。 甚至连查尔斯·巴贝奇也没有——他与布尔同时代、了解布尔的工作,一生都在设计差分机和分析机,"本来有些东西可以帮到巴贝奇",那就是根据一台电报器来创建计算机,而非使用齿轮和杠杆。

  • 这条空白有多长:布尔代数(1854)与开关电路(1844)之间,隔了 84 年才被接上。(谁接的,见下一章。)
14

第 11 章 · 门(从继电器到可组合的积木)

  • 接上的那个人:克洛德·艾尔伍德·香农,1938 年 MIT 硕士论文《继电器和开关电路的符号分析》。1938 年之前人们当然知道串联要都闭合、并联闭合其一即可,但没有人像香农那样清晰严谨地阐述:电子工程师可以运用布尔代数的全部工具去设计开关电路;而且如果你简化了描述网络的布尔表达式,你也就简化了相应的网络。(十年后他又发表《通信的数学原理》,第一篇使用 bit 这个词的文章。)
  • 化简的实效:对猫的表达式用结合律与分配律把重复的 N 提出来 →
    (N × ((M × (W + T)) + (F × (1 − W)))) + B
    
    括号更丑,但少了一个变量,就少了一个开关。更彻底地看:理论上只需要 4 个开关——1 位性别、1 位是否绝育、2 位颜色(4 种颜色 = 2 比特)。于是做出一个控制面板:4 个开关 + 1 个灯泡。输入设备(input device)与输出设备(output device)这两个术语在这里正式登场。
  • 继电器优于开关的唯一理由:继电器可以被其他继电器所控制,而不必由人工控制。 因此这些简单的逻辑门组合起来可以实现更复杂的功能。
  • 四个基本门 + 两个辅助件(全部由继电器搭出,并给出电气工程师的标准符号):
    • 与门 AND:两个继电器串联(上一个的输出给下一个供电)
    • 或门 OR:两个继电器并联(两个输出接在一起)
    • 或非门 NOR:串联但取上触点输出
    • 与非门 NAND:并联但取上触点输出
    • 反向器(inverter):单继电器取上触点。注意:它不是逻辑门——逻辑门通常有两个或更多输入。
    • 缓冲器(buffer):输入=输出,"很明显,缓冲器'没有什么作用'"——但它能放大微弱信号(这正是当年电报机用继电器的理由),也能延迟一个信号(因为继电器需要几分之一秒才被触发)。
  • 第一个复合件:2-4 译码器(4 个与门 + 2 个反向器)。输入 2 个二进制位,输出 4 个信号,任何时刻只能有一个是 1。用它把两个颜色开关翻译成 W/B/T/O 四路信号。同理可造 3-8、4-16 译码器。
  • 一条被明确写出的连接规则:一个门的输出可以作为一个或多个其他门的输入,但两个或多个门的输出不可以相互连接。(这条禁令会在第 21 章被"三态输出"以一种精妙的方式绕过。)
  • 摩根定律:带两个反相输入的与门 ≡ 或非门;带两个反相输入的或门 ≡ 与非门。人物注脚也很动人:德摩根比布尔大 9 岁,《形式逻辑》与布尔的《逻辑的数学分析》据说同一天出版;布尔研究逻辑的灵感正源于一场公开争论——摩根被指剽窃(历史最后证实他是清白的);摩根很早发现了布尔的洞察力,无私地鼓励他、帮助他开展研究,而今天除了这条定律,他几乎已被人们所遗忘。
  • Petzold 在这里为全书定了调:摩根定律是简化电路的重要手段,"从历史的角度来说,这正是香农的论文带给电气工程师们的真正意义",但在本书中简化电路不是重点——我们关注的是让一切有效地运转而不是以最简的形式运转。 然后一句话推门:我们下面要做的就是一台加法器。
  • 成本预告:每个与门/或门/与非门 = 2 个继电器,每个反向器 = 1 个。当你抱怨"这一堆继电器太多了"时,Petzold 的回答是:"是的,确实如此……但是我在这里要说的就是你必须习惯它。"
15

第 12 章 · 二进制加法器(144 个继电器)

  • 开篇的大断言:加法是最基本的运算,"当你真正面对它时,就会发现,原来加法计算就是计算机要做的唯一工作。" 有了加法器,就可以用它实现减法、乘法和除法,计算按揭付款,引导火箭飞到火星、下棋,以及填写话费账单。
  • 拆解(一步极漂亮的分解):把二进制加法表拆成两张表——
    • 进位位(carry bit)表:只有 1+1 才进位 → 这与与门的输出结果一模一样。
    • 加法位(sum bit)表:或门几乎对,除了右下角(1+1);与非门几乎对,除了左上角(0+0)。把两者接到相同输入,再把两个输出送进一个与门 —— 正好得到想要的结果。这个组合有专门的名字:异或门 XOR(要么只有 A 为 1,要么只有 B 为 1;两个都为 1 则输出 0)。
  • 组装链(每一层都被封装成一个符号):
    • 异或门(sum)+ 与门(carry)= 半加器(Half Adder)
    • 半加器的缺陷:它不接受来自前一列的进位,而除最右列外每一列都要三个数相加。
    • 两个半加器 + 一个或门 = 全加器(Full Adder)。(Petzold 特意解释了为什么这里用或门而不是第三个半加器:两个半加器的进位输出不可能同时为 1,所以或门就够。)
    • 8 个全加器串起来 = 8 位二进制加法器。第 1 列的进位输入接地(=0),每列的进位输出接下一列的进位输入。控制面板:两排各 8 个开关 + 9 个灯泡(两个 8 位数相加可能得到 9 位)。
  • 成本账(全书最著名的一个数字):每个与/或/与非门 2 个继电器 → 异或门 6 个 → 半加器 8 个 → 全加器 18 个 → 8 位加法器 144 个继电器。("如果将全部电路展示出来,你一定会崩溃的。没有人能看懂以各种方式连接起来的 144 个继电器所表达的意义。"——这正是抽象层存在的理由:不是为了优雅,是为了人脑装得下。)
  • 两个诚实的补充:① 这种"每一列等前一列进位"的做法叫行波进位(ripple carry),总速度 = 位数 × 单个全加器速度;更快的加法器用前置进位电路。② 计算机已经不再使用继电器了——1930 年代的第一批数字计算机用过,后来用真空管,今天用晶体管,"晶体管要比继电器计算速度更快、体积更小,而且噪声更弱、耗能也更低,而且更便宜";8 位加法器仍需 144 个晶体管,但是电路却是极小的。
  • 可扩展性:把两个 8 位加法器级联(右边的进位输出接左边的进位输入)就得到 16 位加法器。
16

第 13 章 · 如何实现减法(把减法变成加法)

  • 问题的本质:加法有进位,减法有借位——"一种与加法存在本质区别的麻烦机制"。硬做借位的逻辑门很难,所以 Petzold 干脆不做。
  • 技巧(十进制版,先建立直觉):算 253 − 176。
    1. 先算 999 − 176 = 823(对 9 的补数)。从一串 9 里减任何数都不需要借位——这是全部的关键。
    2. 823 + 253 = 1076
    3. +1,再 −1000 → 77
    • 代数解释:253 − 176 + 1000 − 1000 = 253 + (999 − 176) + 1 − 1000。用两个减法和两个加法替代一个减法,代价是彻底消灭了借位。
  • 减数大于被减数时(176 − 253):999−253=746,746+176=922,此时不能再走 +1−1000 那条路(会借位);改成 999 − 922 = 77,并意识到结果为负 → −77。
  • 二进制版(更简单):从一串 1 里减数 = 每一位取反 → 对 1 的补数(也叫反码/相反数)。"这里你可能会想起第 11 章中的反向器,它的作用就是将 0 变为 1,将 1 变为 0。" 于是:取反 → 相加 → 加 1 → 减 100000000。
  • 硬件改造(本章的落地):把 8 个异或门并起来,其中一个输入统一接"取反"信号 → 求补器(One's Complement):取反信号为 0 时输出=输入,为 1 时输出全部反置。然后把同一个 SUB 信号同时接到三处:求补器(要不要取反)、加法器的进位输入 CI(做减法时置 1,正好完成"+1")、以及和加法器的进位输出 CO 一起送进一个异或门去点亮"上溢/下溢"指示灯。

    ⚑ 值得停一秒的设计之美:一个控制位(加/减开关)同时驱动三处不同的行为,而这三处行为在数学上是同一件事的三个部分。这就是"用一个信号编码一个决策"的原型——你在写 agent 的模式开关时,最容易犯的错就是把它拆成三个互相可能不一致的标志位。

  • 负数的表示(2 的补数):先用一个支票账户的十进制例子建立直觉——假设余额只可能在 −500 到 499 之间(共 1000 个数),那么三位数字就够了,不需要负号:500 表示 −500,999 表示 −1,000 表示 0,499 表示 499。这形成一个循环排序(最小的负数看起来像最大正数的延续;999 加 1 得 1000,而三位数下就是 000)。这就是 10 的补数(= 9 的补数 + 1)。于是"减一个数就等于加一个负数"这句话变成了字面真理——所有的步骤都用加法来进行(开一张 78 美元的支票:143 + 922 = 65)。
  • 二进制的 2 的补数:8 位下范围变成 −128 ~ +127,最高位就是符号位(1=负)。求法:每位取反再加 1(125 = 01111101 → 取反 10000010 → +1 → 10000011 = −125)。上溢/下溢的判据非常简洁:两个操作数符号相同,而结果符号与它们不同,这个加法就是无效的。
  • 收束(一句会在第 15、19、23 章反复回响的话):二进制数可以是有符号的,也可以是无符号的,而数字本身是无法显示这一点的。10110110 可能是 −74,也可能是 182,你必须先问它是有符号数还是无符号数。

    "这就是二进制数的麻烦之处,它们只是一些 0 和 1,本身并没有任何含义。" ⚑ 这句话是全书的元命题:比特没有语义,语义全在约定里。第 15 章的 99h(可以是 153、−103,也可以是 BCD 的 99)、第 17 章的 FFh(既是 HLT 指令又是 −1)、第 20 章的 ASCII("12"是两个字符不是数字 12)、第 23 章的浮点格式,都是这一句的注脚。

17

第 14 章 · 反馈与触发器(电路第一次拥有记忆)

  • 第一次反馈:振荡器。把继电器的输出接回它自己的输入 → 电磁铁吸下簧片 → 电路断开 → 磁性消失 → 簧片弹回 → 电路接通……簧片开始上下跳动。加上刺耳的声音就是蜂鸣器,加上小锤和锣就是电铃。用第 11 章的符号画,就是一个反向器的输出接到它自己的输入——看似矛盾(输出与输入相反,而输出同时又是输入),但继电器翻转需要一点点时间,所以它就永远地翻下去。
    • 这是全书的一个分水岭:在此之前所有电路的状态改变都依赖人为干预(拨开关);振荡器却在不需要人干涉的情况下完全自发地工作。 "为了使不同组件同步工作,所有计算机都配备着某种振荡器。"
    • 周期、频率、赫兹(纪念亨利希·鲁道夫·赫兹,用法起源于 1920 年代的德国)。振荡器又常被称为时钟(clock)。
  • 第二次反馈:R-S 触发器。两个或非门交叉相接(左边的输出是右边的输入,右边的输出是左边的输入)。行为极其反常:
    • 闭合上面的开关 → 灯亮;再断开它,灯依然亮着。
    • 闭合下面的开关 → 灯灭;再断开它,灯一直不亮。
    • "同样是在两个开关都断开的状态下,灯泡有时亮着,有时却不亮。" 在此之前所有电路的输出都只依赖于当下的输入,这次不是了。
  • 它的意义:电路"记住"了最近一次是哪个开关先闭合。比喻是跷跷板(两个稳定态,不会长期停在中间,一看就知道哪边最后被压下)。发明于 1918 年(英国无线电物理学家 Eccles 与 F.W. Jordan)。"想象一下,如果你没有了记忆力,该如何去数数?不记得刚刚数过的数,当然也就无法确定下一个数是什么!"
  • 真值表(functional / logic / truth table) 在这里作为工具被引入:S=1 置位、R=1 复位、S=R=0 保持不变、S=R=1 是非法的(会让 Q 和 Q̄ 同时为 0,与它们互反的假设矛盾)。
  • 第三步:D 型锁存器(1 位存储器)。目标变了——不再是"记住哪个键被按过",而是"记住某个特定时间点上,一个信号是 0 还是 1"。做法:在 R-S 前面加两个与门,引入第二个输入叫"保持位";再把数据端信号本身当置位、把它的反相当复位,于是三个输入减为两个:数据端 D + 保持位。保持位为 0 时数据端怎么变都不影响输出;保持位置 1 再回 0 时,最后的数据被"锁"住。
    • 改名即升级:书里把"保持位"改称 Clk(时钟)——它并不真的是时钟,"但是现在时钟仅仅用来指示什么时候保存数据"。这就是电平触发的 D 型触发器 / D 型锁存器 / 1 位存储器。
  • 第四步:8 位锁存器 → 累加器。把 8 个 1 位锁存器的时钟端连在一起就是 8 位锁存器(D0–D7 进、Q0–Q7 出)。把它接到第 12 章加法器的输出、再把它的输出接回加法器的一个输入 → 可以把一长串数连续累加。为此还需要两个小件:
    • 2-1 选择器(用一个"选择"信号决定加法器的 B 输入取自开关还是取自锁存器输出);
    • 清零(Clear)信号(强制 Q 输出为 0,与其他输入无关)。
    • 于是控制面板变成:一排 8 个开关 + 一个"清零"开关 + 一个"相加"开关(它现在控制锁存器的时钟输入)+ 8 个灯泡。这就是累加器(accumulator)的雏形,第 17 章会正式给它命名。
  • 第五步:边沿触发。电平触发的问题是:时钟为 1 期间,数据端的任何变化都会立刻反映到输出。边沿触发的 D 型触发器(两级 R-S 串联,第一级的时钟取反)只在时钟由 0 跳变到 1 的那一瞬间保存数据。功能表里用 ↑ 表示正跳变,符号里用一个小三角表示边沿触发。
  • 第六步:分频器 → 计数器(本章的高潮)。把振荡器接到边沿触发 D 触发器的时钟端,再把 Q̄ 输出接回它自己的 D 输入——"反馈紧接着反馈!"结果是:每当时钟由 0 变 1,Q 就翻转一次 → 输出频率正好是输入的一半 → 分频器(frequency divider)。
    • 把三个分频器串起来,画出顶部四个信号的时序图,然后——把这张图顺时针旋转 90°,逐行读那四位数字:0、1、2、3……15。
    • 这正是第 8 章那条规律的硬件实现:每一列在 0 和 1 之间交替的频率,是它右边一列的一半。
    • 8 个串起来 = 8 位行波计数器(每个触发器的输出是下一个的时钟输入,因此变化要一级一级传递,有延迟;更先进的是并行/同步计数器,所有输出同时改变)。
    • 顺手解决了本章开头挂着的问题:怎么测振荡器有多快?把 8 位计数器接上 8 个灯泡,全灭时启动秒表,全亮时停表——那就是振荡器循环 256 次的时间。若为 10 秒,则频率 25.6 Hz。
  • 收尾件:带预置(Pre)和清零(Clr)的边沿 D 触发器(Pre=1 强制 Q=1,Clr=1 强制 Q=0,两者不能同时为 1)。这两个端口就是第 17 章 Jump 指令的实现基础。
  • Petzold 的休息提示:"现在,我们已经懂得如何使用继电器来做加法、减法和计数了,这是一件很有成就感的事情,因为我们使用的硬件是 100 多年前就存在的东西。"
18

第 15 章 · 字节与十六进制(为什么是 8,为什么是 16)

  • 本章解决的第一个问题:数据路径(Data Path)为什么是 8 位宽? Petzold 的诚实回答是——"追根究底,原始加法机偏偏是 8 位其实没有什么特别的原因",只是每次用 8 位一切都很方便。但他随即给出 8 位真正站得住的三条理由:
    • BCD:一个字节正好装两位十进制数(IBM 采用字节的一个重要原因)。
    • 文本:全世界大部分书面语言(除中文、日文、朝鲜文的象形文字体系外)的基本字符数都少于 256,所以字节是理想的存文本单位;装不下的用两个字节(65536 个)也是很好的方案。
    • 灰度:肉眼能区分的灰度约 256 种。
    • 词源:byte 约 1956 年由 IBM 提出,最早拼作 bite,为避免与 bit 混淆才把 i 换成 y;1960 年代中期随 System/360 定型为"8 比特"。半个字节 = 半字节(nibble / nybble)。
  • 本章解决的第二个问题:为什么用十六进制而不是八进制? 这是一段很漂亮的排除法:
    • 十进制转换要笔算,麻烦。
    • 八进制转换极简(3 位一组查表),10110110 → 266₈。但它有一个致命缺陷:一个字节 8 位不能被 3 整除——最右和中间的八进制数各对应 3 位,最左那个只对应 2 位。后果是:一个 16 位数直接转八进制得到的结果,和把它拆成两个字节分别转八进制得到的结果对不上。
    • 所以需要一个能等分单个字节的系统:4 位一组 → 十六进制。一个字节能且只能由一对十六进制数表示。
  • 教学上的一个小设计(值得偷师):他先不给 A–F,而是给 10–15 六个图像化符号——10 加仑的牛仔帽、一个橄榄球(11 人一队)、一打面包圈(12)、一只黑猫(13)、一轮满月(后弦月 14 天之后)、一把匕首(凯撒在三月的月中第 15 日被刺)。理由是"每个符号都很容易识记,而且其背后都隐含着实际数字意义"。建立直觉之后,才换成正式的 A–F。
  • 同一个字节的五种写法(他把全书前 8 章串了一遍):10110110₂ = 2312₄ = 266₈ = 182₁₀ = B6h。(书里统一用小写 h 后缀。)
  • 转换的实操:十六进制→十进制用位权模板(9A48Ch = 631,948);十进制→十六进制用连除(182 ÷ 16 = 商 11(B) 余 6 → B6h;31,148 → 79ACh;51,966 → CAFE——"我想应该没人愿意点一杯叫做 56,495 的东西,然后把它当作咖啡喝下去吧")。也提醒了用计算器算除法时看不到余数的坑。
  • 回接第 13 章:在十六进制里,最高位是 8、9、A、B、C、D、E 或 F 的两位带符号数都是负数(因为对应二进制最高位为 1)。99h 可以是无符号的 153,也可以是有符号的 −103。
  • 留下的钩子:"奇妙的是,99h 这个十六进制字节从某种意义上来讲,也代表着十进制的 99!" 这与前面学的一切相抵触,答案要等到第 23 章(BCD)。但在此之前,我们必须先讨论存储器。
19

第 16 章 · 存储器组织(地址就是邮箱号)

  • 改名即升级(又一次):把第 14 章的 D 型电平锁存器拿来,只改名字不改结构——Q 输出改叫数据输出(Data Out),时钟输入改叫写操作端(Write)。理由说得极好:"就像信息可以被记录在纸上一样,写操作端的信号同样使得数据输入信号被写入(也可以称之为被存储)到电路中。" 这就是 1 位锁存器。
  • 第一种组合方式(平凡的):8 个并起来,共用一个写信号 = 8 位锁存器(存一个 8 位数)。
  • 第二种组合方式(本章的全部内容):假设你只想存 8 个单独的比特(不是一个 8 位数),而且——关键的约束——你只有一个灯泡。
    • 读侧:要从 8 个锁存器里选一个显示 → 需要 3 个开关(2³ = 8)→ 8-1 选择器(3 个反向器 + 8 个四输入与门 + 1 个八输入或门)。选择输入 101 就把 D5 的值送到输出。
    • 写侧:8 个数据输入可以并在一起,但 8 个写信号不能并在一起(否则一次会写全部)→ 需要把唯一的写信号路由到指定的那一个 → 3-8 译码器(结构与选择器对称)。
    • 两者共用同一组选择信号,这组信号叫做地址(Address)。 Petzold 的比喻是邮箱号:3 位地址决定 8 个锁存器中的哪一个被引用——写的时候决定哪个被触发保存,读的时候决定哪个被送出来。
  • 命名的三重解释(每个词都有理由,不是术语堆砌):
    • 叫存储器,因为它能保存信息;
    • 叫读/写存储器,因为既能写新数据也能检查每个锁存器里存了什么;
    • 叫随机访问存储器(RAM),因为读写很自由——只需改变地址就能取任意一个;相反,一些其他类型的存储器必须顺序读取(想读地址 101 得先把 100 读出来)。
  • 规模的公式与堆叠:RAM 阵列的存储容量 = 2^(地址输入端的个数)。8×1 → 共享地址得 8×2 → 用 2-1 选择器和 1-2 译码器加"第 4 根地址线"得 16×1 → ……一路堆到 1024×8 = 1 KB(10 根地址线)。
  • KB 的那个"1024 而不是 1000":根因是公制基于 10 的幂而计算机基于 2 的幂,两者没有交集(不存在整数 a、b 使 10ᵃ = 2ᵇ);但 2¹⁰ ≈ 10³,于是借用了 kilo。KB → MB → GB → TB(约 10¹²) → PB(2⁵⁰)→ EB(2⁶⁰)。他还提醒:谈存储器时用字节,谈线路上流动的数据时才用千比特/兆比特(56K 调制解调器是 56 千比特每秒,不是千字节)。
  • 本章的成品:64K × 8 的 RAM 阵列。为什么选 64 KB?因为 65,536 = 2¹⁶,地址恰好是两个字节(0000h–FFFFh)。配一个控制面板:16 个地址开关 + 8 个数据开关 + 1 个写开关 + 8 个灯泡,外加右下角一个 控制端(takeover)开关——它决定 RAM 由控制面板管还是由外部电路管,实现方式是 25 个 2-1 选择器(16 地址 + 8 数据 + 1 写)。

    ⚑ 这个控制面板是全书最被低估的一件东西:它是"人可以随时接管、并逐字节看见与改写机器全部内部状态"的最早形态。第 17 章的自动机器、第 19 章的 Altair 8800 前面板、第 22 章的引导过程,全都建立在它之上。

  • 两个必须直面的成本:
    • 数量:存 1 个比特要 9 个继电器(1 个锁存器的门 + 路由),64K×8 需要至少 500 万个继电器。Petzold 直接承认:"我也相信你不想这么做。"
    • 易失性:断电 → 所有电磁铁失磁 → "随着'梆'的一声,金属片将弹回原位" → 全部继电器回到未触发状态 → RAM 里的数据"将如风中残烛般消失在黑暗中"。因此 RAM 也叫易失性(volatile)存储器:为了保证数据不丢,它需要恒定的电流。

      ⚑ 请记住这一段:第 21 章的磁介质存储、第 22 章的文件系统与引导程序,全部是为了对抗这声"梆"。

20

第 17 章 · 自动操作(全书的顶点:机器变成计算机)

这一章是整本书的心脏。它用九次迭代,把一台"要人一直坐着按开关的加法器"变成一台真正的计算机。每一次迭代都是被上一次的具体缺陷逼出来的,没有一步是凭空跳的。 下面按这条链子拆。

迭代 0(起点):第 14 章那台带累加器的加法器——按清零、拨开关输入第一个数、按"相加"存进锁存器、再拨第二个数、再按"相加"……缺陷显而易见:要加 100 个数,你得端坐在那儿把 100 个数一个个拨进去;等你终于做完,发现其中两个输错了,就只能重来一遍。

迭代 1 · 让 RAM 喂数据、让计数器走地址:把 100 个数先放进第 16 章的 64KB RAM(可以随时改),用 RAM 的输出替代那排开关;再用第 14 章的 16 位计数器驱动 RAM 的地址。振荡器的时钟每跳变一次,两件事同时发生:锁存器保存加法器的结果,计数器加 1 指向下一个地址。

  • 前提假设被点名:振荡器必须足够慢,慢到每个周期里继电器们来得及依次触发、让加法器输出有效结果。
  • 缺陷:停不下来(计数器数到 FFFFh 会像汽车里程表一样滚回 0000h,把所有数再累加一遍)、只能做 8 位加法、结果只在灯泡上。

迭代 2 · 结果写回 RAM:把累加器的输出接到 RAM 的数据输入,用 RAM 控制面板的灯泡看结果。需要一个控制 RAM 写信号的办法。

迭代 3 · 操作码诞生(本章第一个大跳):新需求是"分别算 3 个数的和、2 个数的和、3 个数的和"。这需要机器自己知道每一步该干什么,于是定义四种操作:

  • Load:把存储器某地址的字节送进累加器
  • Add:把某地址的字节加到累加器
  • Store:把累加器的内容存回某地址
  • Halt:停机(好让你去看结果)

实现方式简单粗暴:再放一个 RAM 阵列专门存"代码",与存"数据"的 RAM 同步、顺序寻址。代码 RAM 每个地址存一个数字代码,指示机器对数据 RAM 同一地址该做什么。这些数字代码就是指令码(instruction code)/ 操作码(operation code, opcode)。 控制信号(计数器的时钟与清零、锁存器的时钟与清零、RAM 的写、选择器的选择)由操作码经各种逻辑门组合产生——例如操作码是 Load 时,2-1 选择器的选择输入必须是 0。

迭代 4 · 减法(几乎免费):给操作码表加 Subtract。Add(20h) 与 Subtract(21h) 只差最低有效位,称为 C0——这一位同时干两件事:让数据在进加法器前经过反向器取反,以及把加法器的进位输入置 1。一位信号,一次改造,减法到手。(示例:56h + 2Ah − 38h = 48h。)

迭代 5 · 进位锁存器 → 多字节运算:16 位加法要分低字节、高字节两步,中间的进位必须传递。保存 1 位用什么?1 位锁存器。 于是有了进位锁存器(Carry latch),以及两条新指令 Add with Carry 和 Subtract and Borrow。规则被讲得很细:普通 Add 的进位输入是 0、进位输出照存(即使用不到);Add with Carry 才把锁存器的输出当进位输入。"只要进行多字节数加法运算,不管实际是否需要,都应该使用 Add with Carry 指令。" 有了它,8/16/24/32/40 位甚至更多位的加减都成立(32 位加法 = 1 条 Add + 3 条 Add with Carry)。

迭代 6 · 三字节指令(本章的转折点,也是全书对"通用性"最重的一次定价):

  • 暴露的死结:代码存储器和数据存储器是同步、顺序、都从 0000h 开始的,代码里第 n 条指令只能操作数据里第 n 个地址。后果是——一旦执行了 Store,那个结果就再也不能被 Load 回来;每次要用它都得重算。而且多字节数的字节被迫散落在 0000h、0003h、0006h…… 这种不连续的地址上。
  • 根本性修改:除 Halt 外,每条指令占 3 个字节——第 1 个字节是操作码,后面 2 个字节是一个 16 位存储器地址。 于是 Load 可以从任意地址取、Store 可以往任意地址存,操作数和结果可以放在 64KB 里的任何地方(例如 4000h–4005h)。
  • 实现:代码 RAM 的输出接到三个 8 位锁存器——第一个存操作码,第二、三个存地址的高低字节;后两个的输出就是数据 RAM 的 16 位地址。
  • 两个新术语:取指令(instruction fetch)——从存储器中取出指令的过程;执行(execute)——机器响应指令码做一系列操作。(Petzold 特意澄清:这不表明机器是一种有生命的东西,它不能自行分析机器代码并决定该做什么;每一种机器码只是以其唯一的方式触发多种控制信号。)
  • 代价(全书被点名的工程定律):每条指令要 3 个时钟周期取指、一个完整指令周期需要 4 个时钟周期 → 同样的振荡器下,运算速度只有本章第一台加法器的 1/4。Petzold 给它一个名字:

    TANSTAAFL —— "天下没有免费的午餐"。"通常上帝总是很公平的,你改进了机器的某个方面,则其他方面就会受到损失,有得就有失。"

迭代 7 · 合并两个 RAM(冯·诺依曼结构成形):既然指令自己带地址了,代码和数据就没有必要再放两个独立的 RAM。加一个 2-1 选择器决定 RAM 的地址来自 16 位计数器(取指令时)还是来自锁存的 16 位地址(读写操作数时)。于是操作码和操作数存进同一个 RAM——指令从 0000h 起排,数据从 0010h 起排。

迭代 8 · Jump(被"Halt 挡路"逼出来的):

  • 现在你想在原程序后面追加几条指令。000Ch 处那条 Halt 挡着路。问题在于:不管你用什么替换它,000Ch 处的字节都会被当成指令码;更糟的是从那里开始每隔 3 个字节(000Fh、0012h、0015h……)的字节也会被当成指令码。万一其中某个字节恰好是 11h(Store 的操作码),而它后面两个字节恰好指向 0023h,机器就会把累加器的内容写进一个存着重要数据的地址。 即使这些都不发生,取指的节奏也会错位(001Eh 之后是 0021h,而不是你想要的 0020h)。
  • 解法:Jump(也叫分支 branch / Goto),操作码 30h,后跟 16 位目标地址。实现方式是用 16 位计数器里每个 D 型边沿触发器的预置(Pre)和清零(Clr)端——第 14 章特意留下的那两个端口在这里被用上了:置位信号为 1 时,若 A 位为 1 则 Pre=1、Clr=0;若 A 位为 0 则 Pre=0、Clr=1 → Q 被设置成与 A 相同的值。锁存到的 16 位地址既送 2-1 选择器(作 RAM 地址),也送计数器的置位输入。

迭代 9 · 条件跳转(全书的临界点):

  • 逼出它的问题是乘法:怎么算 A7h × 1Ch?答案是把 A7h 累加 28 次。手写 6 条指令重复 27 遍?还是放 Halt 然后按 28 次复位键?"当然,这两种方式都不是很理想。"
  • 用无条件 Jump 跳回顶部?它会永远跑下去,停不了。
  • 解法:再加一个 1 位锁存器——零锁存器(Zero latch)。只有当 8 位加法器的输出全部为 0 时,它锁存的值才是 1(用一个或非门实现:所有输入全 0 时输出才为 1)。这个位叫零标志位(Zero flag),并且只有 Add / Subtract / Add with Carry / Subtract with Borrow 这四条指令会更新它(Store 不影响它)。
  • 于是有了 Jump If Not Zero (JNZ) 等四条条件跳转。乘法程序变成:把计数值 1Ch 载入累加器 → 加上 FFh(即 −1) → 存回 → 若零标志位为 0 则跳回顶部。第 28 次循环时 1 + FFh = 0,零标志位置 1,不再跳转,落到 Halt 上。工作完成。
  • 然后是全书最重的一句判词:

    "条件跳转指令将它与我们以往设计的加法器区别开来,能否控制重复操作或者循环(looping),是计算机(computer)和计算器(calculator)的区别。" 有了它,乘法之外的除法、开平方根、对数、三角函数全都可以用软件做出来——机器所需要的仅仅是能做加减法的硬件,以及利用条件跳转执行代码的方法。"正如程序员经常挂在嘴边的一句话:'我可以用软件完成其他工作'。"

术语总收(这一章结尾把全书前 16 章的成果一次性命名):

  • 数字计算机 = 处理器 + 存储器 + 至少一个输入设备 + 一个输出设备(这里的输入输出就是 RAM 控制面板上的开关和灯泡)。
  • 处理器 = 累加器 + ALU(算术逻辑单元,本机中就是 8 位反相器 + 8 位加法器) + 程序计数器 PC(那个 16 位计数器) + 各种控制逻辑。(他还提醒:不要把处理器叫"大脑","因为我们所设计的处理器称不上是大脑"。)
  • 硬件 vs 软件:"之所以把'硬'改成了'软',是因为相对于硬件而言,指令和数据更容易修改。"
  • 机器码 / 机器语言;助记符(LDA A,[1003h] / ADD A,[001Eh] / STA [1003h],A / JNZ 0000h)——注意方括号表示"这个地址里的值",而跳转地址不加方括号;标号(NUM1、NUM2、RESULT、NEG1);注释(分号后);汇编语言——"它是全数字的机器语言和指令的文字描述的一种结合体",而且每一条汇编语句都对应着机器语言中的某些特定字节。此时还需要手工汇编(在纸上把每条语句翻成机器码,再用开关敲进 RAM)。
  • bug:"对于学习计算机程序设计的人来说,应该尽早了解'错误'这个术语。" 例子极具体:你想输 10h(Load)却输成 11h(Store),结果不是"数据没被加载",而是那个地址的数据还被累加器的内容覆盖掉了。
  • 作者主动指出自己程序里的 bug(全书最值得学的一个动作):上面那个乘法程序有一个错误——如果你把它执行两次,第二次会得到 A7h × 256 的结果,并且加到第一次的结果上。 原因是第一次跑完后 1003h 处的计数值已经是 0,第二次 FFh 加 0 不等于 0,于是程序会一直跑到它变成 0 为止。
  • 历史校正(防止读者误以为历史就是这么走的):这些硬件部件 100 年前就发明出来了,但本章设计的这台计算机在当时并没有被造出来。1930 年代中期继电器计算机问世时,其中很多概念直到 1945 年左右世人才开始慢慢了解;当时人们仍在尝试用十进制而非二进制,程序也不是完全存在存储器里(有时存在纸带上);用 500 万个电报继电器构建 64 KB 的 RAM,不论 100 年前还是今天都是荒唐的。
  • 收尾的预告:继电器终将被真空管和晶体管取代;也许有人会造出处理器和存储器能力与此相当、却精致小巧到可以放在掌心的东西。
21

第 18 章 · 从算盘到芯片(一部压缩的计算史,以及作者的自曝)

这一章是全书的"回望章"——前 17 章造了一台不存在的机器,这一章告诉你历史上真实发生了什么,以及那台机器为什么是可信的。

  • 前史(人类一直在找外挂):小卵石与谷粒计数 → 欧洲的计数板、中东的算盘("尽管人们通常将算盘与亚洲文化尤其是中国文化联系到一起,但它似乎是在公元 1200 年左右由商人带到中国的")→ 纳皮尔(1550–1617)发明对数(把乘法变成加法:查表 → 相加 → 反查)→ 甘特与奥特雷德的滑尺(1976 年 K&E 公司把它制造的最后一把滑尺赠给史密森学会,宣告退场;杀死它的是手持计算器)→ 纳皮尔骨架 → 席卡德的机械计算器(约 1620)→ 帕斯卡与莱布尼茨的齿轮式计算器。
  • 一句被埋在这里的核心判词:"起初,进位仅被看做是加法运算中的'一碟小菜',但它却成了加法器的核心问题。" 评价老式计算器的关键就是它的进位处理成不成功——帕斯卡的进位机制甚至禁止做减法(只能加 9 的补数)。直到 19 世纪后半叶,真正可用的机械计算器才出现。

    ⚑ 可迁移:"看起来是配菜的那一步,往往才是真正的难点。"(对你的 PRD 工厂:碰撞协议里最容易被省略的"独立初稿互不可见"就是进位。)

  • 杰奎德织布机(约 1801):用打孔金属卡片控制织物图案,用了约 1 万张卡片织出一幅黑白线的自画像。它对计算史的影响不亚于对纺织业的影响。
  • 巴贝奇(1791–1871,与莫尔斯同代):为了消除数学表里的错误而设计差分机(本质是一个大型机械加法器,十进制轮子 + 10 的补数表示负数;获得过英国政府少量资助,1833 年放弃)→ 转向解析机(后半生一直在设计与修改,只做出几个小模型和部件)。解析机有存储部件(≈ 存储器)和运算部件(≈ ALU),乘法由重复加法实现、除法由重复减法实现,并且可以用改造过的杰奎德卡片编程。
  • 艾达·拜伦(拉弗雷斯女伯爵,1815–1852):她翻译一篇意大利数学家的文章时留下的评注是这一章最美的一句——"正如杰奎德提花织布机织出了花瓣和树叶,解析机编织出了代数的结构模型。" 而 Petzold 特意指出:巴贝奇大概是第一个意识到条件跳转在计算机中重要性的人,艾达写下了"操作循环(cycle)"的定义,以及"由一个或多个循环构成的重复组群——也就是循环中包含的一个循环或者多个循环"。
    • 冷峻的注脚:差分机 1853 年终于由舒尔茨父子造出来,但巴贝奇的工作被遗忘了好多年,直到 1930 年代人们探索计算机起源时才被重新提起;那时除了超前的自动化观念,他所做的工作对 20 世纪计算机工程来说几乎没有可以利用的。
  • 霍尔瑞斯与 1890 年人口普查(第一次"自动化救急"):美国宪法要求每 10 年一次人口普查,1880 年那次采集花了约 7 年,官员担心 1890 年会超过 10 年。霍尔瑞斯的穿孔卡片:24 列 × 12 个位置 = 288 个位置。
    • 一个关键的反直觉细节:这 288 个位置不是 288 个比特。性别用两个位置(男穿一个、女穿另一个);年龄用两个穿孔(一个指 5 年区间、一个指区间内第几年),光年龄就占 28 个打孔位置,而纯二进制只要 7 位就能编 0–127。理由有两条:① 让 1890 年的普查员做二进制转换"要求太高了";② 真正的二进制系统会产生几乎所有孔都被打穿的情况,卡片会极易碎裂。
    • 制表机(288 根弹簧针压下去,接触水银池导通就驱动电磁体计数)+ 分类机。结果:处理超过 6200 万张卡片,数据量是 1880 年的两倍,时间却只用了三分之一。
    • 后续:1896 年创办制表机公司 → 1911 年合并为 C-T-R → 1915 年托马斯·华生任总裁 → 1924 年更名 IBM。1928 年定型的 80 列 × 12 行 IBM 卡("do not spindle, fold, or mutilate")用了将近 50 年。
  • Petzold 在这里插了一段自省(很重要):本书因主题所限,更多关注数字性质的发明(电报、盲文、巴贝奇机器、霍尔瑞斯卡片),"而在与数字概念以及相关设备打交道时,你会发现整个世界皆为数字。但是,19 世纪的发现和发明确切地讲不是数字的。实际上,通过感官所认识的大自然中只有很少一部分是数字的,更多的时候表现为不可分割的整体。"
  • 继电式计算机(1930 年代中期起):
    • 康拉德·楚泽,1935 年还是工科学生时在柏林父母家里造出第一台,用二进制,早期版本用机械存储器,在老式 35 毫米电影胶片上穿孔来编程。
    • 乔治·史提必兹,1937 年把一对电话继电器带回家,在厨房桌子上接了一个 1 位加法器,妻子叫它 K 机器(K = kitchen);这个实验催生了 1939 年贝尔实验室的复数计算机。
    • 霍华德·艾肯 + 哈佛 + IBM → 1943 年的 ASCC,即 Harvard Mark I,第一台可以打印表格的数字计算机——它最终把巴贝奇的梦想付诸现实。Mark II 是最大的继电式计算机,用了 13,000 个继电器。
    • 1947 年那只飞蛾:人们从 Mark II 的一个继电器里发现了一只飞蛾,格蕾丝·莫瑞·赫柏把它用带子绑在计算机日志上,注明"第一个被发现的有生命的 bug"。
    • 和第 17 章那台机器的两个差别被明确指出:当时没人疯狂到用继电器造 524,288 位的存储器(钱、空间、能耗都不允许),极少的存储器只用来存中间结果,程序本身存在纸带等物理媒介上;而且早期继电式计算机并不都用二进制。
  • 真空管(弗莱明、德福雷斯特):能像继电器一样接成与门、或门、与非门。"究竟是由继电器还是由真空管组成这些逻辑门并不重要,重要的是这些逻辑门可以被装配组合成加法器、选择器、解码器、触发器,以及计数器。" —— 这句话是全书抽象层思想最直白的一次表述。
    • 代价:贵、耗电、发热,而且会烧坏;计算机里真空管数量巨大,"按统计学来分析,每隔几分钟就会烧坏一个"。
    • 好处:状态转变在百万分之一秒(μs)内完成,比继电器快 1000 倍(继电器最好也要 1ms)。
    • 一个极反直觉的史实:"十分有趣的是在计算机的早期发展中,计算速度并不是主要考虑的问题"——因为整机速度受限于从纸带或电影胶片读程序的速度,快 1000 倍并不重要。
  • 1940 年代的关键人物与机器:Colossus(1943,破 Enigma);图灵——1937 年首次提出可计算性并构想图灵机,后来提出图灵测试;ENIAC(埃克特与莫克利,18,000 个真空管,1945 年底完成,约 30 吨,"到 1977 年,人们可以在 Radio Shack 买到速度更快的计算机")。
  • 冯·诺依曼与 EDVAC(第 17 章那台机器的历史身份证):1946 年与 Burks、Goldstine 合写的论文提出几个比 ENIAC 先进的特点——内部使用二进制数、拥有尽可能大容量的存储器、存储器同时存放程序代码和数据、指令顺序存放并由程序计数器寻址但允许条件跳转。这就是存储程序概念与冯·诺依曼结构。Petzold 直接认领:"上一章中设计的计算机就是一个经典冯·诺依曼计算机。"
    • 随之而来的代价:冯·诺依曼瓶颈——为了执行指令要花大量时间先把指令从存储器取出来。"我们仔细回忆一下,第 17 章中最后设计的计算机需要花费 3/4 的时间用来取指令。"
    • 存储器的临时方案:水银延迟线路存储器(5 英尺水银管,每 1μs 发一个短脉冲,约 1ms 到达另一端并折回,一根管能存约 1024 位);1950 年代中期的磁芯存储器(被磁化的小金属环串在电线上,一环 1 位)——"所以常常会听见老一辈程序员们把存储器的访问过程叫做'访问磁芯'"。
  • 同时代的另两个思想家:香农 1948 年《通信的数学原理》把 bit 介绍给世界并开创信息论(研究数字信息在有噪声情况下的传输以及如何弥补损失);1949 年写了第一篇关于如何编程让计算机下棋的文章;1952 年做了一只继电器控制的机械鼠,能记住迷宫路径;还因骑单车变戏法在贝尔实验室出名。韦纳 18 岁拿到哈佛数学博士,1948 年的《控制论》用新词 cybernetics(源于希腊语"舵手")描述生物过程与机械原理的关系 → 后来的 cyber- 前缀 → cyberspace(源自吉布森 1984 年小说《神经漫游者》里的 cyberpunk)。
  • 商用化:1951 年 UNIVAC 完成并交付人口普查局;1952 年在 CBS 用来预测总统选举结果,沃尔特·克朗凯特称它为"electronic brain"(电脑);同年 IBM 发布 701。
  • 晶体管(1947-12-16):贝尔实验室的巴丁与布兰坦用一块锗平板和一条黄金薄片做出放大器,一周后演示给肖克利。而肖克利早在 1939-12-29 就在笔记本上写下:"今天我突然想到,使用半导体来制作放大器从原理上讲比使用真空管更为可能。" 三人 1956 年同获诺贝尔物理学奖。
    • 原理:半导体(锗、硅)最外层有 4 个电子(是最大数的一半);掺入杂质成 N 型/P 型;P 夹在两个 N 之间 = NPN 晶体管(集电极/基极/发射极)——在基极施加微小电压就可以控制非常大的电压从集电极到发射极。
    • 第一个真正的商业应用不是收音机,而是助听器——"为了纪念亚历山大·格雷厄姆·贝尔为聋人奉献毕生精力,AT&T 公司允许助听器制造商无偿使用晶体管技术"。德州仪器造了第一批便携晶体管收音机(1954 年圣诞节)。1960 年晶体管电视机诞生。(他也留了个人情味的注脚:一些高保真音响爱好者和电子吉他手至今更喜欢真空管放大器产生的音质。)
    • 1956 年肖克利离开贝尔实验室,回到出生地加州帕罗奥图创业——他的公司是第一个落户该地区的大公司,此后其他半导体和计算机公司立刻跟进,旧金山南部的这个地区现在被称为硅谷。
  • 本章最重要的一问一答(对你的元痛点的直接回答):

    "有一个疑问:晶体管肯定可以使计算机更加可靠、体积更小以及需要的电量更少,但晶体管可以使计算机的结构变得更简单么?答案是否定的。" 理由说得极清楚:晶体管允许在更小的空间里安装更多逻辑门,但是你还是要考虑这些组件之间的互连问题。把晶体管连成逻辑门,和把继电器、真空管连成逻辑门一样困难;某些方面还更困难,因为晶体管更小而且不容易被控制。如果你想用晶体管造第 17 章那台机器加 64KB RAM,设计工作的重要部分是构造某种可以放置所有部件的结构,而你的大部分体力劳动是在数百万只晶体管之中连接数百万根线。

    • 真正的解法是"把常用组合预先连好":一对晶体管连成门,门连成振荡器、加法器、选择器、解码器,再组成锁存器与 RAM 阵列。"如果把晶体管预先连接成常见的构件,再用其来组装计算机会更加容易。"
  • 集成电路:英国物理学家杜默 1952 年 5 月的演讲预言了"不采用连线而是由固体块组成的电子设备"(绝缘层、导体层、整流层、放大层)→ 1958 年 7 月 TI 的基尔比想到在一块硅片上做出多个元件(他并不知道杜默的预言)→ 1959 年 1 月仙童的诺依斯独立想到(他 1957 年与另外 7 人一起离开肖克利创办仙童)→ TI 先申请专利但诺依斯先获得专利,法律纠纷打了 10 年,今天两人被并称为集成电路(IC / 芯片)的共同发明者。DIP 封装(14/16/40 管脚,间距 1/10 英寸)。
    • 1964 年第一台用 IC 的民用商品又是助听器(极点公司);1971 年 TI 的便携计算器、脉冲星的电子手表。
    • 摩尔定律(1965 年戈登·摩尔在仙童时发现 1959 年后每年翻一倍,后修正为 18 个月翻一倍);SSI(<10 门)/ MSI(10–100)/ LSI(100–5000)/ VLSI(5000–5 万)/ SLSI / ULSI。
  • 作者的自曝(全书最诚实的一段):讲完 TTL 7400 系列和 1973 年德州仪器那本 1.25 英寸厚的《TTL 数据手册》之后(7400 = 四个双输入正与非门、7402 = 或非、7404 = 六反相器、7408 = 与、7432 = 或、7474 = 带预置清零的双 D 型正边沿触发器、7483 = 4 位全加器、74151 = 8-1 选择器、74154 = 4-16 译码器、74161 = 4 位同步计数器),他写道:

    "现在你应该知道我是怎么想出从第 11 章中就开始使用的这么多不同种类的组件的,这些都是从《TTL 数据手册》借鉴而来。" 他不是从零发明了那些积木,他是照着一本真实存在的元件目录反向讲了一遍。 这也解释了为什么前 17 章的每一个组件都恰好"刚刚好够用"。

  • 纳秒(本章最动人的一段):影响集成电路性能最重要的因素是传播时间(propagation time)——输入变化引起输出变化所需的时间,以纳秒计(7400 的与非门保证 < 22 ns)。

    "地球上的所有人对纳秒只有概念上的理解,除此之外别无所有。纳秒比人类感觉到的任何事情都要短暂得多,以至于永远无法理解它。任何解释只会将纳秒变得更加难以捉摸。"(他试了一个:拿着一本离脸 1 英尺的书,纳秒就是光从书页到眼睛的时间——"但这种解释可以使你更好地认识纳秒么?") "然而,纳秒使计算机成为可能。" 处理器迟钝地做着简单的事——取一个字节、加两个字节、存回去——迅速地完成这些操作是计算机能完成任何实际工作的唯一原因。 诺依斯的话被引在这里:"当更好地认识了纳秒后,从概念上来讲计算机操作是相当简单的。"

  • 微处理器的诞生:1971 年英特尔为日本 Busicom 的可编程计算器设计芯片时,工程师特德·霍夫做了一个决定性的选择:

    "我想让它成为一个具有通用功能的计算机,进而可以通过编程成为一个计算器,而不是使这个设备成为一个只有一些编程能力的计算器。" 这导致了 Intel 4004——第一块微处理器,2300 个晶体管("依照摩尔定律,18 年后微处理器包含的晶体管数将是这个数字的 4000 倍,或者说是 1000 万。这是一个相当精确的预测")。

  • 衡量微处理器的三个标准(以及它们的局限):位宽(4004 是 4 位)、最大时钟频率(108 KHz)、可寻址存储器(640 字节)。但 Petzold 立刻泼冷水:

    "上述三个数字指标并不能影响一台计算机的计算能力。" 4 位处理器同样能做 32 位加法,只是拆成 4 位一段。"如果一台处理器从硬件上无法做到另外一台可以做到的事情,那么它可以通过软件途径做到,最终它们可以完成相同的事情——这是 1937 年图灵在论文里面关于可计算性的一种定义。" "然而,速度是处理器之间的根本不同点,同时速度也是我们使用计算机的一大原因。" 而且速度最终决定用途:比人脑还慢的计算机是毫无用处的;如果画一帧图要一分钟,就永远播不了电影。

  • 1972 年 4 月 8008(8 位 / 200 KHz / 16 KB);1974 年 5 月,英特尔与摩托罗拉同时发布了改进版——"正是两款芯片改变了整个世界"。
22

第 19 章 · 两种典型的微处理器(8080 与 6800)

  • 1974 年:4 月 Intel 8080(8 位,6000 个晶体管,2 MHz,寻址 64 KB),8 月 Motorola 6800(4000 个晶体管,1 MHz,同样 64 KB;1977 年出 1.5 / 2 MHz 版)。同年还有 TI 的 TMS 1000 和国半的 PACE(首个 16 位),但回看历史,8080 和 6800 是最具重大历史意义的两块。
    • 一个价格上的讽刺被特意点出:英特尔为 8080 定的价格是 360 美元——这对 IBM 的 System/360 来说是极大的讽刺(后者是售价动辄几百万美元的大型机)。"今天,你用 1.95 美元就可以买到一块 8080 芯片。"
  • 方法论声明(很重要):讲微处理器时人们习惯画内部框图,"然而在第 17 章我们已经使用了数不清的框图来描述它",所以这一章把微处理器当作一个不需要详细研究其内部操作的黑盒,通过观察芯片的输入输出信号,特别是它的指令集来理解它。

    ⚑ 这是全书唯一一次主动"停止下钻":因为下一层已经在第 17 章手工建过了,所以这里可以放心地只看接口。知道自己什么时候可以不再往下看,是抽象层能力的另一半。

  • 管脚即接口:40 管脚;8080 需要三种电源电压(+5V / −5V / +12V,1976 年的 8085 就是为了简化这个);16 个地址输出(→ 2¹⁶ = 65,536 字节);8 个数据信号既可输入又可输出(读存储器时是输入,写时是输出);其余 10 个是控制信号(RESET、写信号等)。复位后从 0000h 取第一条指令。
  • 与第 17 章那台机器的对照(这一章的教学主轴):
    第 17 章的机器 Intel 8080
    指令长度 全部 3 字节(除 HLT) 1、2 或 3 字节
    指令周期 固定 4 个时钟 4–18 个时钟(2–9 μs @2MHz)
    指令条数 12 条 244 条
    寄存器 只有累加器 A + B、C、D、E、H、L
    • 但 Petzold 冷静地补一句:244 条"看起来似乎是很多,但从总体上说,其功能并不比第 17 章的计算机强大"——想用 8080 做乘除法,你仍然需要自己写一小段代码。
  • 寄存器为什么存在:H = High、L = Low,HL 常合成一个 16 位寄存器对用来寻址。"从理论上讲,这些寄存器不是必需的,在第 17 章也没有用到它们,但在实际应用中使用它们将带来很大的方便……因为程序访问内存的次数越少其执行速度就越快。"
  • MOV 与操作码的位模式(本章最漂亮的一处回接):MOV 一条指令就占了 63 个操作码。它的位模式是 01 ddd sss——ddd 是目标寄存器编码,sss 是源寄存器编码。然后 Petzold 说:

    "可以设想一下,在 8080 的内部可能是这样的:标记为 sss 的 3 位用于 8-1 数据选择器,标记为 ddd 的 3 位用来控制 3-8 译码器以此确定哪一个寄存器锁存了值。" 这正是第 16 章造 RAM 时用的那两个零件。 指令集不是魔法,它就是地址线。

    • 顺带一个有趣的冲突:MOV M,M 这个组合本该存在,但它被占用为 HLT(停止)指令。
  • 三种寻址方式(第 17 章只有第一种):直接寻址(LDA A,[aaaa],地址写在指令里)、间接/索引寻址(MOV B,M,地址在 HL 寄存器对里)、立即数寻址(MVI E,37h,数据直接跟在操作码后)。
  • 标志位与 PSW:第 17 章有 CF 和 ZF,8080 增加到五个——SF(符号)、ZF(零)、PF(奇偶)、CF(进位)、AF(辅助进位),全部装在一个专用 8 位寄存器 PSW(程序状态字)里。规则被明确:LDA、STA、MOV 从不影响标志位;ADD、SUB、ADC、SBB 会影响。PF(结果中 1 的个数是偶数则置 1)"有时会被用来进行简单的错误检查",但在 8080 程序中并不常用;AF 只服务于 DAA 一条指令。
  • 算术之外的能力:8080 比第 17 章的机器多了 AND / OR / XOR(都是按位运算)、CMP(做减法但不保存结果,只为设置标志位,用来比较大小)、CMA(累加器按位取反,等价于 XRI A,FFh)、INR/DCR(加 1/减 1,第 17 章要靠"加 FFh"实现)、四条循环移位 RLC/RRC/RAL/RAR(用于乘 2 / 除 2,只影响 CF)。
  • DAA —— 第 15 章那个钩子的答案:DAA(十进制调整累加器)可能是 8080 中最复杂的一条指令,8080 里专门设计了一个完整的小部件来执行它。 它服务于 BCD 码:每 4 位表示一位十进制数(0000–1001)。例子极干净:累加器放 BCD 的 27h、B 放 94h,ADD 得到 BBh(不是合法 BCD),执行 DAA 后累加器变成 21h 且进位标志置 1 —— 因为十进制 27 + 94 = 121。
  • 堆栈(本章最好的比喻):想象你在办公室工作,有人到你桌前分配任务,每项工作用一个文件夹;做第一件事之前要先做另一件相关的,于是你把第一个文件夹放下、在它上面打开第二个;又来了更高优先级的第三个……"事实上,这些堆叠的文件夹很有序地保存了你干活的顺序轨迹。" 最上面的永远是当前该做的,做完了往下走,处理完最底下那个(最早接到的任务)就可以回家了。
    • 这就是后进先出(LIFO);术语是压入(push)与弹出(pop);堆栈本身就是一段普通的 RAM,只是相对独立不另作他用,由一个 16 位的堆栈指针 SP 寻址。
    • 机制被讲到字节级:SP=8000h 时执行 PUSH BC → SP 减 1 变 7FFFh、B 存进去 → SP 再减 1 变 7FFEh、C 存进去;POP 反过来。堆栈上溢 / 下溢是真实的错误来源(PUSH 太多会覆盖掉存储器里的代码或数据)。
    • 一个漂亮的工程约定:把 SP 初始化为 0000h,第一次 PUSH 就减到 FFFFh——堆栈从存储器最高地址开始往下长,而程序代码从 0000h 往上排,两者保持最远距离。
  • CALL / RET 与子程序(第 17 章没有的能力):Call 像 Jump,但它把原来的地址保存起来——保存到堆栈。"这种策略使 Call 指令有效地记录了'从何处跳转'。" Return 从堆栈弹出两个字节装回 PC。"对于任何处理器来说,Call 和 Return 指令都非常重要。在它们的帮助下,程序员可以在程序中使用子程序(subroutine)"——一段频繁使用("频繁"意味着"不止一次")的完成特定功能的代码,是汇编语言的基本组成部分。
    • 书里给出完整的 8080 乘法子程序:乘数被乘数放 B、C,乘积放 HL;开头用 PUSH 保存要用到的寄存器(子程序的通行礼节);用 SUB 清零 H、L 而不是 MVI(省 2 个字节);先判乘数是否为 0,是则用 JZ 直接跳到结尾的 POP;否则用 DAD 把 BC 加到 HL、累加器减 1、JNZ 循环。
  • I/O 与中断:外围设备的接口与存储器类似。8080 在 65,536 个存储器地址之外,另外增加了 256 个专用的 I/O 端口(IN / OUT 指令);而 6800 没有独立 I/O 端口,所有外设地址都是存储器地址空间的一部分——这叫内存图像 I/O(memory-mapped I/O)。中断:外设把 INT 置 1 → 8080 从存储器取指令来响应 → 外设提供一条 RST 指令(像 CALL 一样把 PC 存进堆栈,然后跳到固定地址:RST 0 → 0000h、RST 1 → 0008h……RST 7 → 0038h)。必须先执行 EI 才会响应中断(复位后默认不响应),DI 关闭。
  • NOP:243 个操作码之外还有一条"什么也不做"的指令。"这样做有什么好处呢?填空,即保持处理器的运行状态而不做任何事情。"
  • 6800 的差异(对照组的价值):两个累加器 A、B(功能完全相同,不像 8080 把 B 当普通寄存器)、一个 16 位索引寄存器(类似 HL)、有溢出标志位但没有奇偶标志位、时钟信号更简单、没有独立 I/O 端口。操作码和助记符完全不同,两者不能执行对方的机器码,为一种芯片写的汇编程序也不能在另一种上跑。
  • 大端小端(一个能当茶余谈资的真差异):同样是"把 347Bh 地址的字节装入累加器",8080 假设低字节在前,6800 假设高字节在前。这就是 little-endian(Intel 方式) 与 big-endian(Motorola 方式),这个根本区别从未得到解决,直到今天。而 big-endian 这个术语出自《格列佛游记》——小人国与邻国关于吃鸡蛋前该敲碎哪一头的争论。Petzold 的态度很坦率:"因此,这种争论可能是没有意义的(另一方面,坦白地说,在本书第 17 章设计的计算机所采用的方式我个人并不喜欢)。" 但差异确实制造了真实的兼容性问题。
  • 后来发生了什么:8080 被用在 Altair 8800 上(1975 年 1 月《大众电子》封面),而 Petzold 在这里放了全书最会心的一句回接——

    "当你看到 Altair 8800 时,前面板上的灯泡和开关会让你感到似曾相识。这个界面和第 16 章介绍 64 KB RAM 阵列时的初始'控制面板'的界面是类似的。" 然后是谱系:8085 → Z-80(Zilog,由参与过 4004 的英特尔前雇员菲戈金创立,完全兼容 8080 并增加了许多有用指令,1977 年用在 TRS-80 上);1977 年苹果的 Apple II 用的既不是 8080 也不是 6800,而是基于 MOS 技术、更便宜的 6502(6800 的改进加强版);1978 年 6 月 8086(16 位,寻址 1 MB,操作码与 8080 不兼容但包含乘法和除法指令)→ 一年后 8088(内部同 8086,外部按字节访问,因此能用为 8080 设计的流行 8 位外围芯片)→ IBM PC 5150,1981 年秋。x86 一路到 186/286(1982)、386(1985,32 位)、486(1989)、奔腾(1993 起),指令集不断扩展但始终支持 8086 以来所有早期处理器的操作码。苹果这边:1984 年 Macintosh 用摩托罗拉 68000(6800 的下一代),68K 系列是"已发布的处理器中最受欢迎的一类";1994 年起改用 PowerPC(摩托罗拉+IBM+苹果联合开发的 RISC 架构:指令等长(32 位)、只有加载和保存能访问存储器、尽量简化指令操作、设置大量寄存器以减少访存)。PowerPC 指令集完全不同,靠仿真(emulate)跑 68K 程序——逐一检查 68K 操作码并执行对应操作,比原生慢但能工作。

  • 摩尔定律带来的晶体管都去哪了:适应不断增加的数据宽度(4→8→16→32 位)、支持新指令(浮点、图形重复计算)、以及两项提速技术——流水线(pipelining)(执行一条指令的同时读取下一条,尽管 Jump 指令会在一定程度上打断这种流程)和 Cache(处理器内部访问极快的 RAM 阵列,存放最近要执行的指令;因为程序经常执行小的指令循环,用 Cache 可以避免反复加载)。
23

第 20 章 · ASCII 码和字符转换(编码的政治学)

  • 本章解决的问题:计算机只能存比特,而人类积累的大部分信息都是文本。要给每个字母、数字、标点一个唯一编码 = 字符编码集(Coded Character Set),其中每个编码 = 字符编码(Character Code)。第一个问题是:需要多少比特?
  • 先做减法(本章最好的思维动作):Petzold 花了大段篇幅把问题变小——

    "我极力想阐述一个重要的观点,那就是文本与其印刷在纸上时采用的二维排版格式是两码事。充分发挥想象力,将文本看成是一维的由字母、数字和标点符号组成的数据流吧。" 论据是:一个你曾在杂志上读过的故事,几年后在另一本书里重逢,你回忆起的是故事本身而不是排版,没有人会因为行距不同而把它们当成两个故事;字体变了,"字体改变了文本的表现形式,但故事本身的内容并没有因此而改变"。再进一步:不要斜体、粗体、下划线、颜色、上下标、音标——只要赤条条的拉丁字母,它们组成了英语中 99% 的文本。

  • 为什么前面学的编码都不行:莫尔斯码是变长的(常用字符短、冷僻字符长),"这样的编码非常适合电报系统,但并不适用于计算机",而且不分大小写;布莱叶盲文是定宽的(6 位),非常适合计算机,但它靠转义码区分大小写、靠移位码表示数字。
  • Baudot 与转义码的灾难(本章最有价值的一段):法国电报服务公司职员 Emile Baudot 1874 年发明的可打印电报机用 5 位编码(1877 年被采纳,经 Donald Murray 修改,1931 年成为 ITA-2 标准,在美国常被称为 Baudot、但更准确地说叫 Murray 编码)。5 位 = 32 个编码:00h 保留,26 个字母,5 个格式控制(空格 04h、掉头 02h、换行 08h——两个动作分开:掉头是让滑架回到最左,换行是移到下一行)。数字和标点靠 1Bh"数字转义"和 1Fh"字符转义"来切换。
    • 灾难现场:一行 "I SPENT $25 TODAY." 编码后正确打印;但把同一串数据流再输入一次,打印出来变成 8'03,5 $25 TODAY. —— 因为打印机接收到第二行时,它记得的最后一个转义码是"数字转义",于是把开头几个编码当成数字解释了。

      ⚑ 这是全书对"隐式状态"最狠的一次示范:故障不在数据里,不在解析器里,而在两次输入之间被保留下来的那个模式。Petzold 的结论是:"这种问题产生的根源就是采用了转义码,这的确很让人头痛。"

  • 7 位的算术(干净的排除法):大小写字母 52 + 数字 10 = 62,加上标点超过 64 → 至少 6 位;但总数应该远不到 128 → 不超过 8 位。"所以,答案就是 7。在采用 7 位编码时,不需要转义字符,而且可以区分字母的大小写。"
  • 为什么必须是标准而不是自创:如果你完全自造一台计算机、自写全部程序、且永不与别人连接,你当然可以自定编码。"但是这种自己制造计算机,并且独立使用的情况实在是太少了,所以所有人都遵循并使用统一化的编码,计算机的存在才有意义。" 另外,字母表的编码按顺序排能让排序和分类变得简单易行。
  • ASCII(1967 年正式公布):7 位,00h–7Fh。教学顺序也很讲究——他从第二组 32 个编码(标点与数字)讲起,因为前 32 个控制字符最难理解。20h 是空格。95 个可显示字符 = 图形文字(graphic characters),另外 33 个是控制字符(control characters)。
  • 两个特别值得记的性质:
    • I am 12 years old. 里的 "12" 被编码成 31h 32h(字符 1 和 2 的 ASCII),不是 01h 02h、不是 BCD 的 12h、也不是 0Ch——"因为这些编码在 ASCII 码中表示其他的意思"。(第 13 章那句"比特本身没有含义"的又一次实证。)
    • 一个大写字母与其对应小写字母的 ASCII 值恰好相差 20h。于是"把字符串转成大写"这件事可以简化成一条指令:ANI A,DFh(DFh = 11011111,把左起第 3 位置 0,其余位保留)。这是"编码设计好,代码就短"的最直接证据。
  • 控制字符的实用面:Tab(09h,跳到 8 的倍数位置,使字符保持按列对齐);换页符 12h;回退符 08h 可以打印复合字符——65 08 60 = e、回退、重音 → è;掉头(CR,0Dh)与换行(LF,0Ah)是控制符里最重要的两个。
  • 对照组:IBM 的 EBCDIC(System/360 内部使用的 8 位编码,由 6 位 BCDIC 扩展而来,根源在 IBM 打孔卡)。结构是:低半字节 = 数字穿孔的 BCD 码,高半字节 = 区域穿孔的编码(只穿第 12 行 → 1100,只穿第 11 行 → 1101,穿第 0 行 → 1110;数字 0–9 不需要区域穿孔,高半字节是 1111)。它的坑被点名:大写字母的编码里 R 与 S 之间有跳变——"有时在编写程序的时候,尤其是程序中用到 EBCDIC 编码时,这个容易被忽视的小细节往往会令人抓狂"。而且几十年几代技术演变下来,打孔卡与 EBCDIC 之间的逻辑性和一致性也逐渐消失了。
    • 顺带回答了一个自然的疑问:打孔卡每列 12 个孔,本可以用其中 7 个表示 ASCII——"但是,这种方案有一个非技术方面的缺陷,那就是太多的穿孔将使得卡片变得很脆弱,容易折断。"
  • "一个字符 = 一个字节"的红利(本章最有用的一段常识):ASCII 本质是 7 位,但仍以 8 位形式存储,"今天来看,8 位的字节存储已经作为了一项标准"。这个等价关系让"估算文本占多大空间"变成数字符:
    • 8.5×11 英寸打印纸、双倍行距、1 英寸页边距 ≈ 每页 1750 字节;单倍行距约 3.5 KB
    • 《纽约客》每页 3 栏 × 60 行 × 40 字符 ≈ 7200 字节
    • 《纽约时报》每页 6 栏(假设纯文字)≈ 32 KB
    • 精装书每页约 500 词、每词约 6 字母(含词间空格)≈ 3000 字符;按 333 页算 ≈ 1 MB
    • 实测:《了不起的盖茨比》≈ 300 KB|《麦田守望者》≈ 400 KB|《哈克贝里·弗恩历险记》≈ 540 KB|《愤怒的葡萄》≈ 1 MB|《白鲸》≈ 1.3 MB|《汤姆·琼斯》≈ 2.25 MB|《乱世佳人》≈ 2.5 MB|《末日逼近》≈ 2.7 MB|《战争与和平》≈ 3.9 MB|《追忆似水年华》≈ 7.7 MB
    • 美国国会图书馆约 2000 万本 ≈ 20 万亿字符 ≈ 20 TB(且不含大量照片和录音)。

      ⚑ 这一串数字本身就是一种能力:把抽象的存储单位换算成"你读过的东西",你对成本的直觉才立得住。

  • ASCII 的病根在它的名字里:American Standard Code for Information Interchange —— "它是太美国化了!" 有美元符号却没有英镑符号;没有西欧的重音字母;没有希腊文、阿拉伯文、希伯来文、西里尔文;没有婆罗门系、天城体、孟加拉语、泰语、藏语;"简单的 7 位编码在面对数以万计的中国、日本、韩国的象形文字,以及朝鲜文音节时也显得力不从心。"
    • 补救一:标准里保留了 10 个可被各国重新定义的编码位(40h、5Bh–5Eh、60h、7Bh–7Eh),英镑符号可替换 #、通用货币符号可替换 $——但"如果在文本文件中使用了这些重定义的符号,相关人员都必须知道这些变化"。
    • 补救二:扩展 ASCII(8 位,00h–7Fh 保持 ASCII,80h–FFh 另加 96 个字符,如 Latin-1:A0h = 不中断空格(显示为空格但不允许在此断行,例如 "WW II")、ADh = 软连字符(单词被迫分行时才显示的连字符))。
    • 补救三:双字节字符集 DBCS,如 Shift-JIS 用 81h–9Fh 作为双字节编码的首字节,可再编约 6000 个字符;亚洲还有另外三个类似的流行方案。它的缺陷不只是兼容性——"一些字符,特别是通用的 ASCII 码字符,是用单个字节编码表示的,相比而言,成千上万的象形文字则是双字节编码,这在无形之中增加了使用这种字符集的难度。"
  • Unicode(1988 年起由几大公司合作):16 位,0000h–FFFFh,65,536 个字符;前 128 个与 ASCII 一致,00A0h–00FFh 与 Latin-1 一致,全世界很多标准被一同收录。但它唯一的问题很实在:

    "对于 Unicode 来讲,它唯一的问题,就是它改变了字符与存储空间之间'单字符,单字节'的等价对应关系。" ASCII 存的《愤怒的葡萄》约 1 MB,Unicode 存约 2 MB。为了使编码系统兼容,Unicode 在存储空间上付出了相应的代价。 Petzold 也很清醒:ASCII 和无数有小缺陷的扩展 ASCII 已经在计算机领域根深蒂固,想一下子取代它们并不是轻而易举的。

24

第 21 章 · 总线(把一台机器变成一个系统)

  • 总线的定义与四类信号:总线 = 提供给计算机上每块电路板的数字信号的集合,分四类:地址信号(微处理器产生,寻址 RAM 或其他设备)、数据输出信号(微处理器产生,写入 RAM 或设备)、数据输入信号(其他部件提供、微处理器读取)、控制信号(双向,与具体微处理器相关)。另外总线还为各电路板供电。(他特意提醒术语的相对性:来自微处理器的"数据输出",就是 RAM 和其他设备的"数据输入"。)
  • 总线简史与"开放"的价值(本章最有商业含量的一段):S-100(1975 年 MITS Altair 首用,100 个连接器故得名,5.3×10 英寸板,母板上约 12 个扩展插槽)→ ISA(IBM 为初代 PC 设计,62 针,20 地址 + 8 复用数据 + 6 中断 + 3 DMA)→ AT 加 36 针插槽 → MCA(1987,部分成为 IBM 专利、其他公司要付授权费,"也许就正是由于这个原因,MCA 才没能成为一种工业标准")→ EISA(1988 年 9 家公司联合推出,并不包括 IBM,32 位,取代了 MCA)→ PCI(Intel)。
    • DMA(直接存储器访问):通常所有读写内存都由微处理器完成,采用 DMA 后其他设备可以不通过微处理器而获得总线的控制权,直接对内存读写——这比其他方法快得多。
    • 1981 年秋,IBM 公布了 PC 的技术参考资料,里面包含整台计算机的全部电路图。 结果:"这创造出了整个 PC 的克隆体",从初代 PC 发展而来的计算机占据约 90% 的桌面市场份额(尽管 IBM 本身只占很小一部分);而苹果的 Macintosh 起初采用封闭体系结构,在当时的桌面市场只占不到 10%。

      ⚑ 请给这条结论标一个时间戳:本书写于 2000 年。 这个判断在当时完全正确,但它是全书最需要被今天的读者打折扣的一条(详见「批判性审视」)。

  • 一块存储器板是怎么造出来的(第 16 章的工业版):以 1970 年代中期流行的 2102 芯片为例(1024 × 1 位,MOS 工艺,读取时间 350–1000 ns 因型号而异,有 R/W 信号和片选信号 CS)。
    • 8 片并起来才能存一个字节 → 这就是一个 1024×8 = 1 KB 的阵列,为存完整字节而连在一起的一组芯片叫存储体(bank)。
    • 一块板放 32 片 = 4 个存储体 = 4 KB。16 位地址的分工极清晰:A0–A9 直接接 RAM 芯片;A10、A11 选择 4 个存储体中的哪一个;A12–A15 决定这块板响应哪个 4 KB 区域(64 KB 被划成 16 个区)。
    • 让地址范围可配置:用 DIP 开关(插在标准 IC 插槽里的一排微型开关)+ 异或门比较器——把 A12–A15 分别与开关设定值送进 4 个异或门,再进一个或非门;四位全部相同时,或非门输出 1(Equal)。把 Equal 和一个 2-4 译码器联合,就为四个存储体各产生一个选中信号。
  • 三态输出(本章最精妙的一个概念):第 11 章立过一条禁令——两个门的输出不能连在一起(一个输出 1、一个输出 0,"恐怕谁也无法回答"结果是什么)。但总线上所有板都必须共用同一组数据线。解法是 2102 的数据输出是三态(tri-state)的:除了逻辑 0 和逻辑 1,还有第三种状态——"一种'真空'态,就像芯片的引脚上什么也没连一样",由片选信号控制。

    "几乎所有连接在总线上的器件都使用由总线传递而来的数据输入信号。但不管何时,连接在总线上的电路板中只有一个能确定总线数据输入信号的类型,其他电路板处于三种状态中的无效状态。" ⚑ 这是"共享通道 + 互斥写权"的硬件原型——多写者冲突不是靠协商解决的,是靠让所有非当前写者物理上断开解决的。

  • SRAM vs DRAM:SRAM(如 2102)每位约 4 个晶体管,只要供电内容就保持;DRAM 每位只要 1 个晶体管,但需要定期访问其内容——"更新(refresh)周期",每秒必须进行几百次。Petzold 的比喻很传神:"这种做法就好像为不让某人入睡而每隔一段时间就用手肘轻推他一样。" 尽管有争议,DRAM 容量增长快(基本每三年翻两番,符合摩尔定律),最终成为标准 → SIMM / DIMM 内存条。
  • 视频显示(把带宽算成像素):CRT 的扫描行、水平回归、垂直回归;隔行扫描与场/帧(美国 60 场/秒、水平扫描频率 15,750 Hz → 262.5 行/场 → 525 行/帧);带宽限制了信号从黑到白再回黑的变化速率(美国广播电视约 4.2 MHz)。
    • 算术:2 × 4,200,000 ÷ 15,750 ≈ 533 像素/行,但约 1/3 被隐藏(图像远端与水平回归)→ 可用约 320;垂直方向 525 里同样有损失,且计算机不用隔行 → 合理值 200。
    • 结论:早期普通电视机上的视频适配器分辨率是 320×200 = 64,000 个像素。
  • "视频 RAM 需要多大?"——一个漂亮的分档回答:"这个问题并不太好回答!我们只能说,结果可能处于 1 KB~192 KB 之间。"
    • 只显示文本:25 行 × 40 字符 = 1000 个字符 × 7 位 ≈ 1 KB。此时还需要一个字符生成器(character generator)——通常是 ROM(只读存储器,生产时已填入数据,没有数据输入信号),把 7 位 ASCII 翻译成 8×8 的像素图。聪明的接口设计:不要做 7 个地址 + 64 个数据输出(芯片会太大),而是 10 个地址信号(7 位 ASCII + 3 位行号)+ 8 个数据输出(该行的 8 个像素)。另外还要两个 8 位寄存器存光标的行和列。
    • 1 位/像素的黑白图形:64,000 位 = 8000 字节。
    • 1 字节/像素的 256 级灰度:64,000 字节——"这与一直在讨论的某个 8 位微处理器的整个地址空间非常接近"。
    • 3 字节/像素的真彩色(红绿蓝各一字节):192,000 字节。
    • 颜色数 = 2^(每个像素赋予的比特数)。
  • 两个"历史决定现在"的尺寸:
    • IBM PC 的显示器是 25 行 × 80 字符,因为 80 与 IBM 打孔卡上的字符数一样(早期连大型机的 CRT 就是用来显示打孔卡内容的,"偶尔,你会听到有人称只显示字符的视频显示器为卡片")。
    • 640×480 从 1987 年的 IBM PS/2 与 Macintosh II 起成为最低标准,而它之所以是 4:3 —— "是因为它和托马斯·爱迪生有关":大约 1889 年爱迪生与工程师迪克生在研究活动电影摄影机时决定让画面宽比高多出 1/3,这个 1.33:1(4:3)的屏幕长宽比被电影沿用了 60 多年,电视也照搬。而 640×480 正好是 4:3,这意味着 100 像素的水平线和 100 像素的垂直线物理长度相同——即正方形像素(square pixel),这对计算机图形学是非常重要的特性。
  • 键盘:每个按键就是一个简单的开关。关键的设计判断:键盘硬件提供的不是 ASCII 而是扫描码(scan code)。 理由极实在——按键 A 对应 41h 还是 61h 取决于用户有没有按 Shift;而且现代键盘上很多键根本没有 ASCII 与之对应。"当按下键盘上的某个按键时,一小段计算机程序就会计算出这个按键对应的 ASCII 码(如果有的话)。"
    • 硬件实现(16 键版):4 位计数器在 16 个编码间快速重复循环(循环速度必须足够快,以保证在按下并松开一个按键之前循环已经结束),输出同时送给 2-4 译码器和 4-1 选择器;某个键被按下且计数器恰好扫到它时,选择器输出 1,此刻计数器的值就是该键的扫描码。扩展规律:64 键 → 6 位扫描码(3-8 译码器 + 8-1 选择器组成 8×8 阵列);65–128 键 → 7 位。
    • 再加一个中断信号,让微处理器不必轮询。
  • 长期存储(对抗第 16 章那声"梆"):打孔卡与纸带的两个问题是介质不可重用和效率很低。磁介质的谱系:1878 年美国工程师奥柏林·史密斯描述了工作原理 → 1898 年丹麦发明家波尔森造出第一块可用的(他后来发明了录音电话机)→ 1928 年澳大利亚发明家佛勒玛用"生产香烟金属带"的技术把铁粒子覆在长纸带上(纸带后来被醋酸盐纤维素取代)→ 卷轴式磁带 → 1950 年雷明顿兰德的第一个商用数字磁带系统(0.5 英寸卷轴只有几兆字节)。磁带的死穴是只能顺序访问,"频繁地快进和倒带会花费很多时间"。
    • 磁盘:1956 年 IBM 的 RAMAC(Random Access Method of Accounting and Control)——50 个直径 2 英尺的金属盘片,存储量 5 MB。软盘(8 英寸 → 5.25 → 3.5)与硬盘;磁道(tracks)→ 扇区(sectors)→ 通常每扇区 512 字节。初代 IBM PC 的软盘:单面、40 磁道、每磁道 8 扇区、每扇区 512 字节 = 163,840 字节 = 160 KB;后来的双面 80 磁道 18 扇区 = 1,474,560 字节 = 1440 KB。1983 年 IBM PC/XT 首用硬盘,容量仅 10 MB;16 年后的 1999 年,20 GB 硬盘售价不到 400 美元。
    • 接口标准:SCSI / ESDI / IDE,都利用 DMA 让数据在 RAM 与硬盘之间直接传送,以块为单位(扇区字节数的倍数,通常 512)。
  • 术语的收口(终于回答了作者序里那个问题):"最近几年推出了一条分类规则……memory(内存)仅仅表示半导体随机访问存储器;storage(存储器)用来指任何的存储设备,通常包括软盘、硬盘和磁带。" 而真正的区别是易失与否:RAM 掉电就没,磁介质除非故意删除或覆盖否则一直保留。还有一条硬差别:微处理器不能直接从磁盘读取数据,必须先执行一小段程序把数据从磁盘调入内存。

    ⚑ 此时——也只有在此时——Petzold 才允许自己用那个比喻:随机访问存储器就像办公桌桌面,上面的东西可以直接使用;磁介质存储器就像文件柜,要用得站起来走过去找出来带回桌面;桌面太挤还得先把暂时不用的放回柜子。他的评价是:"这个比喻恰到好处。" 对照作者序里他明确表示不满意的那个同样的比喻——差别不在比喻,在你有没有先把机制讲透。

25

第 22 章 · 操作系统(从水泡到 API)

  • 开机的弗兰肯斯坦时刻(本章的引子):所有硬件各就各位,你伸手给它上电——阴极射线管加热后,屏幕上出现一串排列整齐而又完全随机的 ASCII 字符。因为半导体存储器上电时处于随机且不可预测的状态,而微处理器会把这些随机字节当作机器代码执行。"不用担心会因此发生什么糟糕的事情——计算机不会因此而坏掉——但是,计算机也无法完成任何有意义的工作。"

    "这里我们漏掉的就是软件。"

  • 鸡生蛋问题与第一次解法:微处理器复位后从固定地址(8080 是 0000h)开始执行。那第一条指令是怎么进去的? 用第 16 章那个控制面板(现在再加一个接到微处理器复位输入的复位开关):打开复位 → 打开控制端 → 用 16 个开关指定地址、用 8 个开关设定字节、把写开关开再关 → 全部敲完后关掉控制和复位开关,微处理器开始执行。

    "不言而喻,这是一个耗时耗力的过程……看看你的手指,或许已经磨出了水泡,你的大脑也感觉一片混乱,而这一切都是工作的代价。"

  • 第一个坑:显示不是想象的那样。你算出结果 4Bh,不能直接把它写进视频显示器的内存——那样屏幕上会显示字母 K(因为 K 的 ASCII 正是 4Bh)。要显示 "4B" 这两个字符,必须写入 34h 和 42h。于是需要一个把半字节转成 ASCII 的子程序(书里给了 NibbleToAscii),再调用两次把一个字节转成两个 ASCII 字符。想显示十进制还得再做一轮除法。

    ⚑ 这一小段是"表示层与值层永远不是一回事"的最短证明。

  • 改进链(每一步都由上一步的痛点逼出来):
    1. 键盘处理程序(keyboard handler) —— 挂在中断上(按键 → 中断控制芯片给出 RST 1 → 微处理器把 PC 压栈并跳到 0008h)。
    2. 初始化代码(initialization code) —— 复位后先跑:设置堆栈指针到内存有效区域、把视频内存每个字节设成 20h(空格)以免屏幕全是随机字符、把光标定位到第一行第一列、用 EI 开中断、最后 HLT。此后计算机停在 HLT 上,只能被复位信号或键盘中断唤醒。
    3. 回显与编辑 —— 按键 → 查扫描码 + Shift 状态 → 得 ASCII → 写进光标位置的视频内存(这叫回显 echoing)→ 光标右移。退格键(08h)的实现就是把 20h 写回那个位置并让光标退一格。
    4. 命令处理程序(command processor) —— 按下 Enter(0Dh)时,把屏幕上这一行文本解释为一条命令。三条命令就够用了:
      • W(Write):W 1020 35 4F ... 从 1020h 开始把这些字节写进内存(要把 ASCII 转回字节,正是上面那个转换的逆过程)
      • D(Display):D 1030 显示从 1030h 开始的 11 个字节(11 是算出来的:40 字符一行,扣掉命令和地址就只剩这么多)
      • R(Run):R 1000 —— 把 1000h 装进 HL,然后执行 PCHL(把 HL 装进程序计数器),程序就从那里跑起来

        "键盘处理程序及命令处理程序简化了很多工作,可以说是计算机发展的一个里程碑。一旦使用了它,就无须理会那个令人难以忍受的控制面板了。" 而且更重要的是:计算机变得可交互(interactive)了——你输入什么,它立刻响应并显示出来。

    5. ROM —— 断电还是全丢,于是把这些代码烧进只读存储器。PROM 只能编程一次;EPROM 正面开一个玻璃窗口,用紫外线照射就能擦除重写。(RAM 板上那个 DIP 开关此时派上用场:把 RAM 让到更高地址,把 0000h 留给 ROM。)
    6. 磁盘的 S/L 命令 —— 把内存块按扇区存到磁盘、再读回来。但立刻暴露四个新问题:① 你得用纸笔手工记录什么东西存在哪个面、哪个道、哪个扇区;② 不能把某个地址的代码加载到另一个地址(因为 Jump 和 Call 里写的还是原地址);③ 程序可能大于一个扇区;④ 磁盘上空闲扇区是零碎的,可能找不到一块足够大的连续空间。

      "出于这个原因,文件系统(file system)应运而生。"

  • CP/M(Gary Kildall,1970 年代中期专为 8080 开发,后创立 DRI):这一节是全书讲得最细的一个真实系统。
    • 介质与布局:单面 8 英寸盘,77 磁道 × 26 扇区 × 128 字节 = 256,256 字节。CP/M 自身占最开始的两个磁道,剩下 75 个磁道存文件。
    • 分配块:扇区按 8 个一组分成分配块(1024 字节),全盘共 243 块(编号 0–242)。
    • 目录区占第 0、1 两块(2048 字节),每个文件的目录项固定 32 字节 → 一张盘最多只能放 64 个文件(2048 ÷ 32)。
    • 目录项的 32 个字节都用在哪(这是"元数据"这个概念的最小可运行样本):第 1 字节是共享属性(通常为 0);第 1–8 字节是文件名(最多 8 字符);第 9–11 字节是文件类型(最多 3 字符) —— 这就是 8.3 命名法(MYLETTER.TXT、CALC.COM);第 12 字节是扩展序号;第 15 字节记录最后一个分配块用到了几个 128 字节扇区;第 16–31 字节是磁盘存储表,列出这个文件占用了哪些分配块(例如前 4 项是 14h、15h、07h、23h 就表示占 4 块 = 4 KB,而且这四块不必连续)。
    • 一个天然的容量上限与它的解法:磁盘存储表 16 字节 → 单个目录项最多描述 16 KB → 更大的文件用扩展(extents),即第二个、第三个目录项,靠第 12 字节的序号串起来。
    • 文本文件 vs 二进制文件:书里用三个 16 位数(5A48h、78BFh、F510h)做对照——二进制只要 6 个字节(且 Intel 格式低字节在前、Motorola 格式高字节在前);写成 ASCII 文本要多得多,但可以直接显示、直接阅读,而且十进制形式比十六进制更符合人的习惯。
    • 引导(booting):ROM 里只需一小段引导程序(bootstrap loader)——"操作系统的其余部分可以通过这段代码的自举操作被高效地引导"。开机时磁盘最开始那个 128 字节扇区被加载并运行,它再把 CP/M 的其余部分拉进内存。
    • 加载后的内存布局:高地址区放 CP/M 的三个组成部分——CCP(控制台命令处理程序)+ BDOS(基本磁盘操作系统)+ BIOS(基本输入/输出系统),共约 6 KB;其余约 58 KB 是 TPA(临时程序区),一开始是空的。(键盘 + 显示器合称"控制台 console"。)
    • 命令:DIR(列目录,支持 ? 和 * 通配)、ERA(删除,"一旦删除文件,此文件的目录项及其所占用的磁盘空间都将被释放")、REN(改名)、TYPE(显示文本文件内容)、SAVE。
    • 最关键的一条约定:"如果你输入一个不能被 CP/M 识别的命令,CP/M 就会默认为输入的是保存在磁盘上的一个程序名。" 于是 CCP 去磁盘找同名的 .COM 文件,找到就加载到固定的 0100h 并跳过去执行。(因此 CP/M 程序不能被随意放到内存任意位置,必须装在 0100h。)
    • 实用程序 vs 应用程序:PIP(外设交换程序,复制文件)、ED(文本编辑器)这类小而单一的叫实用(utility)程序;字处理、电子表格那类叫应用(application)程序——它们的存储类型都是 COM。
  • 操作系统的第三大功能:API(本章的理论高潮):运行在 CP/M 下的程序要写屏、读键盘、读写磁盘文件,"然而在通常情况下程序本身是很难直接做到的"。解法是 CP/M 提供一整套子程序:把功能号放进寄存器 C(例如"从键盘读一个字符"、"在光标处显示一个字符"、"新建文件"——文件名地址放 DE),然后执行 CALL 5。
    • CALL 5 是怎么工作的:CP/M 在内存地址 0005h 处放了一条 JMP 指令,跳到 BDOS 里的某个位置。 BDOS 维护文件系统,需要碰硬件时就去调 BIOS 的子程序;BIOS 是 CP/M 中唯一需要了解具体硬件的部分。 CCP 自己的全部功能也是靠调 BDOS 实现的。
    • 这就是应用程序接口(API),而它最重要的性质是与设备无关(device-independent):

      "程序使用 CP/M 提供的功能便可完成对键盘、视频显示器和磁盘驱动器操作,简言之,API 屏蔽了硬件之间的差异。" 于是同一个 CP/M 程序可以在硬件差别很大的不同机器上跑(前提是处理器能执行 8080 指令)。"如果没有标准的 API,程序必须根据不同型号的计算机进行相应的修改后,才能访问硬件。"

  • MS-DOS 的来路与差别:西雅图计算机产品公司的提姆·帕特森写的 QDOS(Quick and Dirty Operating System)在很多方面借鉴了 CP/M → 更名 86-DOS → 被微软买断版权 → 1981 年随初代 IBM PC 成为 MS-DOS(IBM 版叫 PC-DOS)。16 位的 CP/M-86 也能用于 IBM PC,但 MS-DOS 影响力更大,很快成为标准。
    • 文件系统换成了 FAT(文件分配表,微软 1977 年就开始用):磁盘分成簇(cluster,512 字节到 16 KB 不等);目录项只记录文件的起始簇,每一簇的下一簇位置由 FAT 记录。目录项仍是 32 字节、仍是 8.3(但术语改叫"文件扩展名"而不是"文件类型"),并且多了最后修改的日期、时间和文件大小。
    • 结构差异:IBM PC 的 ROM 里已经有一套完整的 BIOS,所以 MS-DOS 里不再需要 BIOS;命令处理器是 COMMAND.COM;程序分 COM(不超过 64 KB) 和 EXE(更大)。API 从 CALL 5 换成利用 8086 的软件中断功能:INT 21h —— "它与子程序调用很类似,只不过程序不必知道它所调用的子程序的确切地址"。
  • "反传统性"与它的历史后果(很值得记的一段):理论上应用程序应该只通过操作系统访问硬件。但实际上,在 1970 年代末和 80 年代初,许多应用程序往往都绕过操作系统直接写视频显示存储器,因为那样快得多;而对某些任务(比如显示图形),当时的操作系统甚至"心有余而力不足"。

    "也许正是因为 MS-DOS 系统卓越的'反传统性',使得大多数程序员都很热衷于它,它可以让程序员编写的程序最大限度地达到硬件的最快速度。" 代价是:流行软件因此依赖 IBM PC 这个硬件平台;其他厂商为了竞争不得不沿袭这些硬件特点,否则流行软件跑不起来;于是"与 IBM PC 百分之百兼容"成了软件盒子上的显著字样。 ⚑ 一条冷酷的产业规律:当"绕过抽象层"能换来足以决定胜负的性能时,抽象层就会被击穿,而被击穿处的具体实现细节会反过来固化成事实标准。

  • MS-DOS 2.0(1983 年 3 月)与层次文件系统:硬盘容量变大 → 文件变多 → 查找和组织变难 → 引入层次文件系统("只是在原有文件系统上做了一些小的改动"):有些文件本身就是目录。根目录(root directory)与子目录(subdirectories,有时称文件夹 folder)。而这以及 MS-DOS 2.0 的其他很多功能,都是从 UNIX 借鉴而来的。
  • UNIX(1970 年代初贝尔实验室,汤普森与里奇):
    • 名字的来历:源自早期的 Multics(贝尔实验室 + MIT + GE 的合作项目),因进度缓慢、代价昂贵,1969 年贝尔实验室退出,但汤普森和里奇继续做,为了区别于 Multics 取名 UNIX。
    • 最关键的性质:可移植(portable)。"以前大部分操作系统是针对特定的计算机硬件平台开发的,但 UNIX 打破了常规,它不针对具体的计算机硬件平台,也就意味着它在各种机器上都可以运行。"
    • 它扩散的真正原因是法律:贝尔实验室隶属 AT&T,为了限制 AT&T 在电话业务上的垄断地位,法院裁定禁止 AT&T 销售 UNIX,AT&T 被迫将它授权给别人。于是从 1973 年初起大量大学、公司和政府机构获得授权使用和研究 UNIX。(1983 年 AT&T 获准重返计算机业务并发布了自己的 UNIX 版本。)
    • "UNIX 思想"(一条不成文的约定):人们都使用文本文件作为公用文件形式;许多实用程序读文本文件、做一点处理、再写出另一个文本文件,因此可以用链的形式来组织这些实用程序。
    • 多用户与多任务:1960–70 年代计算机庞大昂贵,只服务一个人不现实 → 时分复用(time sharing),多个终端通过快速切换让每个用户感觉机器只为自己工作。支持同时运行多个程序 = 多任务(multitasking)操作系统,"显然,与 CP/M 和 MS-DOS 这样的单任务系统相比,这种操作系统要复杂得多":文件可能被多人同时访问、需要内存管理(memory management)、内存不够时需要虚拟内存(virtual memory)——在磁盘上划出空间存临时文件,把暂时不用的内存块换出去,需要时再调回来。
    • FSF / GNU / Linux:斯塔门创建自由软件基金会与 GNU 项目("GNU 与 UNIX,既要划清界限又相辅相成"),宗旨是创建一个与 UNIX 兼容但不受私有权限制的操作系统和开发环境;其中最著名的是 Linux——内核与 UNIX 完全兼容,大部分由芬兰的托瓦兹完成,由于开源,近年来已成为非常流行的操作系统。
  • 本章的出口:从 1980 年代中期开始,操作系统走向更大、更复杂——苹果的 Macintosh 系统与微软的 Windows,融合了图形和高级可视化视频显示技术,使应用程序变得更易于使用。(→ 第 25 章)
26

第 23 章 · 定点数和浮点数(精度是一种代价)

  • 本章的前提:数字计算机只能处理离散数据——"二进制中的每一位非 0 即 1,两者之间没有任何数",而我们习惯把数字看成连续的(任给两个有理数都能取平均值找到中间的一个)。32 位能表示的自然数范围是 0–4,294,967,295;但想存下 4.5,就得换新方法。
    • 数的谱系被完整交代了一遍:整数 → 有理数(两个整数之比,如 3/4 = 0.75,本质是 75/100;1/3 与 1/7 是无限循环小数但仍是有理数)→ 无理数(√2,小数部分无穷且无规律)→ 超越数(不是任何整系数代数方程的解,π 和 e 是典型;所有超越数都是无理数,但反之不成立)→ 实数 → 虚数(负数的平方根,"虚数确实存在于现实世界,它在解决电子学的某些高级问题中有重要应用")→ 复数。
  • 方案一:定点格式(fixed-point)。用 BCD 存十进制位,两个 BCD 数字压进一个字节(压缩 BCD),再单独留一个半字节或整字节做符号位。例子:处理 ±100 万美元以内的钱款,每笔用 5 个字节,−4,325,120.25 存成 00010100 00110010 01010001 00100000 00100101(十六进制看更直观,最左半字节的 1 就是负号)。
    • "定点"的真正含义与它的隐患:小数点总在某个特定位置,"值得注意的是,有关小数点位置的计数信息并没有与整个数字一起存储。所以,使用定点小数的程序必须知道小数点的位置。" 你可以设计任意小数位数的定点数、甚至在一个程序里混用多种,但所有做算术的代码都必须知道每一种的小数点在哪。
    • 它的破产场景:地球到太阳约 490,000,000,000 英尺,氢原子半径约 0.00000000026 英尺 —— 用定点格式要 12 个字节才能同时容纳。
  • 方案二:浮点格式(floating-point),基于科学计数法:有效数(significand,也叫首数/尾数)× 10^指数。规范化式要求有效数 ≥1 且 <10。指数的正负只表明数的大小,不表明数本身的正负。
    • 二进制小数:小数点右边对应 2 的负整数次幂,101.1101 = 5.8125。二进制科学计数法的规范化式要求有效数 ≥1 且 <10₂(即十进制的 2),于是——

      "这个规则暗示了这样一个有趣的现象:在规范化二进制浮点数中,小数点的左边通常只有一个 1,除此之外没有其他数字。" 既然它永远是 1,就没必要存它。 这就是 IEEE 标准里"隐含位"的由来。

  • IEEE 754-1985(ANSI 认可,"它只有 18 页"——相对于其他标准来说非常简短,但奠定了以简便方式编码二进制浮点数的基石):
    • 单精度 4 字节 = 1 位符号 + 8 位指数(偏移量 127)+ 23 位有效数小数部分(但因为隐含的那个 1,仍称 24 位精度)
    • 双精度 8 字节,指数偏移量 1023,有效数 53 位
    • 特殊值的编码规则(这套设计很值得学):e=0 且 f=0 → 0(通常 32 位全 0;符号位为 1 则是"负 0",可用来表示小到无法表示、但仍小于 0 的数);e=0 且 f≠0 → 合法但非规范化(此时有效数小数点左边是 0);e=255 且 f=0 → 无穷大/无穷小(看符号位);e=255 且 f≠0 → NaN(not a number),用来表示未知的数或非法操作的结果。
  • 精度的真相(本章最重要、也最实用的一段):因为 2¹⁰ ≈ 10³,单精度的 24 位二进制精度 ≈ 十进制 7 位。这句话的具体含义被 Petzold 摊开来讲:
    • 16,777,216 和 16,777,217,以及处于这两个数之间的所有数(例如 16,777,216.5),会被表示成同一个数。
    • "如果你为银行编写程序,用单精度浮点数来存放以美元、美分为单位的数字时,就会发现 262144.00 美元和 262144.01 美元在计算机中存储为同一个数。" —— "这也是为什么人们在处理以美元、美分表示的钱款数目时更愿意使用定点数的一个原因。"
    • "当使用浮点数时,你会发现它还存在着一些让人崩溃的小问题。你的程序进行了一系列计算,应该得到的结果为 3.50 的,但由于使用浮点数,你得到的可能是 3.499999999999。这种问题在浮点数运算中经常发生,而且没有一套完整的解决方案。"
    • 双精度 53 位 ≈ 十进制 16 位,好很多,但仍不能避免:140,737,488,355,328.00 和 …328.01 在内存里是同一个数(42E0000000000000h)。

      ⚑ 全书唯一一处明说"没有一套完整的解决方案"的地方。抽象层是有漏洞的,而这个漏洞是由物理(有限位数)而非疏忽造成的。

  • 浮点运算怎么做(都能拆回整数运算):加法的关键是先用两个数的指数差决定把哪个有效数右移多少位来对齐,再把有效数当整数相加(两个指数相差太大时,小的那个对结果甚至没有影响——比如太阳距离加氢原子半径);乘法是有效数当整数相乘、指数相加,然后调整一到两次以规范化;平方根、指数、对数、三角函数全都可以由这四种基本运算实现——例如 sin 用泰勒级数展开,把弧度限制在 0~π/2(由此可推导出所有正弦值),大约展开 12 项就能达到双精度浮点数要求的 53 位精度。

    "当然,使用计算机的目的就是帮助人们更加方便地解决问题,而编写程序来进行浮点数运算这一繁杂工作似乎和这个目的背道而驰。但这正是软件的优势所在:一旦某个人为特定的计算机编写了浮点数运算的程序,那么其他的人都可以使用它。" ——因此在计算机发展的早期,为新机器做的第一项工作就是为它编写浮点数运算程序。

  • 硬件浮点的历史(一段关于"可选配件"的教训):
    • 1954 年 IBM 704 是第一台把浮点运算硬件作为可选配件的商用计算机(36 位 = 27 位有效数 + 8 位指数 + 1 位符号)。
    • 1980 年 Intel 8087 数字协同处理器(也叫数学协处理器 / FPU 浮点运算单元),40 管脚,不能独立工作,必须与 8086/8088 配合;CPU 读到 ESC 指令时协处理器接管并执行下一条机器指令(68 条之一)。它内部执行存在 ROM 里的指令序列,这些内部指令称做微代码(microcode);虽然是循环执行、不能立刻出结果,但速度至少是软件方法的 10 倍。
    • 初代 IBM PC 主板上那个 40 管脚的插槽是空的 —— 用户要自己买 8087 插上去。而且装了也不是所有程序都变快(字处理几乎用不到浮点,电子表格则提升很大)。
    • 由此产生的开发者困境:"程序员必须使用协处理器机器码指令来编写特殊的代码,因为数学协处理器不是标准硬件……而这些工作让程序员烦不胜烦。尽管他们不愿意,他们仍不得不编写自己的浮点数运算子程序(因为大多数人并没有安装数学协处理器),因此这就多了一个额外的工作。" 最后的局面是:装了协处理器要写代码支持它,没装就要写代码仿真它。
    • 谱系:287(配 286)→ 387(配 386)→ 486DX(1989)把 FPU 内建进 CPU,不再可选 → 但 1991 年低端的 486SX 又把它拿掉,另卖 487SX 协处理器 → 1993 年的奔腾起,CPU 内置 FPU 再次成为标准,"也许这是永远的标准"。摩托罗拉:68881/68882 → 1990 年的 68040 首次内建;PowerPC 同样内置。

      ⚑ 这是一部关于"能力该不该做成可选项"的完整案例:可选 → 生态分裂 → 开发者被迫双份工作 → 最终被吸收进标准。任何"高级能力作为付费/可选插件"的产品决策,都会把成本转嫁给上层开发者,而历史的终局通常是内置。

27

第 24 章 · 高级语言与低级语言(离开机器码)

  • 开篇的比喻(全书最好的一个):

    "使用机器码编写程序就如同用牙签吃东西:伸出手臂使出较大的力气刺向食物,但每次都只获取到小小的一块,这个过程是辛苦且漫长的。" 每个机器码字节所完成的工作,都是"你能想象到的最微小且最简单的工作"。

  • 第一级台阶:汇编器(assembler)。在 CP/M 上用 ED.COM 写一个 PROGRAM1.ASM 文本文件,用 ASM PROGRAM1.ASM 汇编,得到 PROGRAM1.COM。示例程序只有 7 行,但每一行都被解释:
    • ORG 0100h —— 不对应任何 8080 指令,只是告诉汇编器下面的语句从 0100h 开始(CP/M 装载程序的固定地址)
    • LXI D,Text —— 把标号 Text 的地址装入 DE
    • MVI C,9 / CALL 5 —— 调用 CP/M 的 9 号功能:显示从 DE 所指地址开始的字符串,直到遇到 $ 为止("可以看到,在程序的结尾处使用了美元符号作为文本的结束标志,这种方式看起来很奇怪,但 CP/M 就是采取的这种方式")
    • RET 交还控制权给 CP/M;DB 定义数据字节;END 表示汇编文件结束
    • 编译出来正好 16 个字节:3(LXI)+ 2(MVI)+ 3(CALL)+ 1(RET)+ 7(Hello!$ 的 ASCII)
  • 汇编器为什么简单:"构成汇编语言的助记符和机器码之间是一一对应的。" 汇编器只需持有一张助记符与参数的表,逐行分解、匹配、替换。但它替你做了一件真正有价值的事:自动算出 Text 的地址是 0109h——

    "一般来说,程序员在使用汇编器时有很多方便之处,其中一点就是不需要关心汇编程序各部分在内存中的存放地址。"

  • 两个漂亮的自举概念:第一个编写汇编器的人需要手工对程序汇编;此后要写新汇编器,可以用汇编语言写、用旧汇编器汇编,"一旦新的汇编器通过了汇编,则它也就可以对自身进行汇编"。而为新面世的微处理器写汇编器时,可以在已有的机器上写并汇编——这叫交叉汇编(cross-assembler)。
  • 汇编语言剩下的两个根本问题:① 非常乏味——"因为这是在微处理器芯片级的编程,因此不得不考虑每一个微小的细节";② 不可移植——为 8080 写的程序不能在 6800 上跑,必须重写(虽然没第一次那么难,因为组织和算法问题已经解决了,但仍有很多工作)。
  • 第二级台阶:高级语言与编译器。设想用经典数学表达式描述复杂运算(而不是用一堆与处理器绑定的机器码指令),再写程序把它转成机器码。"现在你所创建的东西已经触及所谓的高级程序设计语言。" 相对地,汇编语言叫低级程序设计语言,因为它与硬件的关系相当紧密。
    • 他还给了一个"高级"的极限想象:如果你是公司总裁,坐在计算机前输入(甚至口头发布)"计算出本年度的收益和损耗,生成年度报表,最后打印出 2000 份送至每个股东","你所使用的才真正是一种非常高级的语言!但在实际工作中,程序设计语言还达不到这种理想化的水平。"
    • 语言是被设计出来的:人类语言经历千百年的互相影响与偶然演变,但高级程序设计语言是经过深思熟虑的设计的。设计的一大挑战是"如何让语言更具吸引力"——因为语言定义了人们向计算机传送指令的方式,只有更易用的方式才能让人们对语言产生兴趣。据 1993 年的一项估算,1950 到 1993 年间大约有 1000 多种高级程序设计语言被发明出来并被应用。
    • 编译器(compiler)比汇编器难得多:汇编语言与机器码一一对应,而高级语言不具备这种对应关系,编译器通常必须把一条语句转换成多个机器码指令。"编译器的编写非常复杂,许多书都是用全部的篇幅来讲解如何设计和构造编译器。"
  • 用 ALGOL 当原型(选择理由说得很讲究):ALGOL 是"过去 40 年中许多曾经流行一时的通用高级语言的直接鼻祖","该语言可看做是一粒种子,它的成长最终形成了高级语言这棵大树",直到今天人们仍在使用"类 ALGOL 程序设计语言"的概念。(顺带一个冷知识:ALGOL 也是仙女座第二亮的恒星的名字。)版本:ALGOL 58 → ALGOL 60(本章所用) → ALGOL 68。
    • 两类错误的对照(教学价值极高):把 end 拼成 ende → 编译器立刻报语法错误(syntax error),"ALGOL 编译器对拼写的检查非常严格,它在这一点上比传统的语文教师更甚";但把要显示的文本里的 first 拼成 fist → 编译器完全查不出来,程序照跑照显示——这叫运行时错误(run-time error)。
    • 语言要素被逐个引入:begin/end;print("ALGOL 语言的正式规范中并不包括 print 语句",是编译器提供的内部函数 built-in function);变量与声明(real / integer,变量名以字母开头、可含数字、不能含空格);赋值用 :=("在大多数计算机语言中,赋值语句通常只包括等号");read;为什么用 * 而不是 × —— 因为 × 没有被包括在 ASCII 和 EBCDIC 字符集中(ALGOL 还用了非 ASCII 的 ↑ 做乘方、÷ 做整数除法);循环 for ... do + begin/end 语句块 + step;条件 if / else(用来避免给 sqrt 传负数);数组 array a[1:100] 与下标;布尔型(Boolean,"该名称是为了纪念第 10 章提到的乔治·布尔")。
    • 压轴例子:爱拉托逊斯筛法求 2–10,000 的素数(爱拉托逊斯,约公元前 276–196,传说是亚历山大图书馆的管理员,因准确计算出地球周长而永载史册)。程序结构:布尔数组全部初始化为 true → 外层 for 只跑到 100(10000 的平方根) → 若 a[i] 为 true 就把它所有的倍数标成 false → 最后遍历输出。
  • "程序设计是科学还是艺术"(本章的哲学插曲):

    "如果让 100 个人来编写输出素数的程序,你可能会得到 100 个不同的解决方法。就算所有的程序员都使用'爱拉托逊斯筛法',其最后所写的程序也不一定与本文所写程序完全相同。如果说程序设计是一门科学,那么就不应该出现如此多的解法,而不正确的方法将会非常明显。" 他引费曼:"从某种程度上看计算机科学像是一种工程,它的工作范畴是利用一些事物去实现其他事物";也提到克努斯的《计算机编程艺术》。他自己的结论很克制:偶尔会有极富创造性的火花或洞若观火的觉察力,这是"艺术";但更多的时候,程序设计是设计和建造,就像修建一座大桥。

  • 语言谱系(每一种都带着它的社会动机):
    • FORTRAN(FORmula + TRANslation,IBM 1950 年代中期为 704 开发)——目前仍在使用的最古老的高级语言;广泛支持浮点甚至复数运算;几十年来是科学和工程应用的首选。
    • COBOL(COmmon Business Oriented Language,1959 年由美国工业界与国防部组成的委员会发起,受霍珀早期编译器影响)——第一个成功用于商务系统的语言,广泛支持记录(record)与报表(report)。两个值得记的细节:① 它的设计里渗透着"让管理人员至少能看懂代码、能检验程序是否完成预定工作"的思想——"(实际上这种情况非常少见)";② 早期 COBOL 程序处理 IBM 打孔卡上的 80 列记录,为了尽量减少孔洞占用的卡片空间,年份通常设计成 2 位而不是 4 位 ——"随着时间的推移,这个设计的缺陷逐渐显露出来,最终导致在 2000 年出现了著名的'千年虫问题'。"

      ⚑ 全书最贵的一个"局部最优":一个为了省几列打孔卡而做的、当时完全合理的编码决策,代价在四十年后结算。

    • PL/I(IBM 1960 年代中期,配合 System/360;想融合 ALGOL 的块结构 + FORTRAN 的数学函数 + COBOL 的记录报表,但远没有达到 FORTRAN 和 COBOL 那样的使用程度)。
    • BASIC(Beginner's All-purpose Symbolic Instruction Code,1964 年达特茅斯的克莫尼与克鲁兹,为分时系统、为非数学专业的学生设计——"他们不应该为打孔卡片和复杂的程序语法花费太多精力");行号;不要求程序员指定变量类型;很多后续版本是解释器(interpreter)而不是编译器——"解释器比编译器的原理简单一些,因此更容易编写,但其运行程序的速度要比后者要慢";1975 年比尔·盖茨和保罗·艾伦为 Altair 8800 编写了 BASIC 解释器,同一年他们创建了微软。
    • Pascal(沃思,1960 年代末;继承 ALGOL 的结构 + COBOL 的记录处理)——Turbo Pascal 1983 年由宝兰公司发布,售价 49.95 美元,由丹麦大学生安德斯·海尔斯伯格开发,"它提供了完整的集成化开发环境……程序的文本编辑器和编译器集成在一起,大大加快了程序开发速度。集成化开发环境以前主要用于大型计算机,Turbo Pascal 实现了在小型计算机上的突破。"
    • Ada(为美国国防部开发,以奥古斯塔·艾达·拜伦命名)。
    • C(里奇,1969 年设计、1973 年完成;名字来源很朴素——它是早期语言 B 的后继者,B 是 BCPL 的精简版,BCPL 来自 CPL)。1973 年 UNIX 用 C 重写成功,"从此以后 UNIX 操作系统和 C 语言就变得密不可分了"。风格极简:用 {} 取代 begin/end;i = i + 5 可简写为 i += 5;只加 1 时写 i++(在 16 位或 32 位微处理器中,i++ 仅需一条机器码指令)。最关键的一点:很多高级语言不支持移位和按位布尔运算,"C 语言打破了这种局限,它广泛地支持这类运算",加上对指针(本质是数字化描述的内存地址)的支持,"因此 C 也被称为高级汇编语言"。
    • 非冯·诺依曼体系的两个例外:LISP(麦卡锡,1950 年代末,主要应用于人工智能)与 APL(艾佛森,同期,使用一个特殊的符号集,可以一次性对整个数组里的数字完成操作)。

      "但是,所有的类 ALGOL 语言——即大多数常用程序设计语言——其设计模式都是基于冯·诺依曼计算机体系的。设计一种非冯·诺依曼体系的程序设计语言并非易事,而让人们接受并使用这种语言则更加困难。"

  • 高级语言的代价(Petzold 从不白送好处):
    • "一个优秀的汇编程序员所编写的程序比编译器所产生的代码更加有效率"——生成的可执行程序更大更慢("但从近年的发展来看,这种差别已变得不再明显")。
    • "虽然高级语言提高了处理器的易用性,但并没有让其变得更强大……因为高级语言必须转化成机器码,所以它只会降低微处理器的能力。"
    • 最锋利的一句:"事实上,如果某种高级语言具有真正意义的可移植性,那么它将不能使用某些处理器的特有功能。" 典型证据就是移位指令——几乎没有哪种高级语言包含它,要移位就得用乘 2 / 除 2 来仿真。("这并不是什么坏事:事实上,许多现代编译器都是利用处理器的移位指令来实现乘以或除以 2 的幂的。" —— 抽象层把能力藏起来,又在下面偷偷用回来。)
  • 汇编为什么退场:处理器引入流水线等新硬件后"使得汇编语言变得更加复杂且不易处理";与此同时编译器越来越成熟;大容量存储器也推了一把——"程序员不再局限于编写运行在小内存和小磁盘上的程序"。
  • 一个人物注脚:公认第一个真正可以工作的编译器是 A-0,1952 年由雷明顿兰德公司的格瑞斯·穆雷·霍珀为 UNIVAC 开发完成。她 1944 年就在艾肯手下研究 Mark I(就是后来贴飞蛾的那位),八十多岁时仍在计算机界工作。
28

第 25 章 · 图形化革命(回到"信息"这个主角)

  • 用户界面这条主线:计算机变小、变快、变便宜 → "当计算机体积越小、处理速度越快,软件就能发挥更大的作用,而机器就可以承担越来越多的工作" → 最该改进的关键部位就是用户界面(人机交互的轴心)。然后是一句极冷静的判断:

    "人与计算机是两种完全不同形式的'客观存在',只可惜在人机交互的这个过程中,与其让计算机去适应人类的特性,远不如劝服人们进行调整以适应计算机的特性来得容易。"

  • 界面演进的四段:批处理(1950–70 年代,程序和数据分布在打孔卡上,一次性录入,最后把结果打印在纸上) → 电传打字机(达特茅斯分时系统,用户输入一行、计算机输出一行或多行,"随着机器的运行,相应的事务也随着打印纸的旋转逐步推进")→ CRT 但被当成"玻璃屏幕电传打字机"(一行一行显示、填满就整体上翻;CP/M 和大部分 MS-DOS 工具软件都是这样,而 UNIX"还一直保留着这种传统工艺")→ ANSI 转义序列(1979 年 ANSI 发布"ASCII 码使用的附加控制"标准,初衷是"适应二维字符-图像设备输入/输出控制中迫在眉睫的相关需求":Esc [ 2 J 清屏并把光标移到左上角,Esc [ 5 ; 2 9 H 把光标移到第 5 行第 29 列——"这种定义在电传打字机上是不可能出现的")→ 哑终端(dumb terminal):"相对于电传打字机速度要更快,从某种程度来讲也更灵活,但从速度的提高程度上来讲,并不足以引领用户界面的革新"。

    "真正的革新出现在 20 世纪 70 年代小型计算机中——它配备了'视频显示存储器',并作为微处理器地址空间的组成部分。"

  • 分水岭事件:VisiCalc(1979,布莱克林与弗兰克斯顿,Apple II)。它把纸质的行列数据表搬到屏幕上,让用户可以在表中随处游走、输入数据和公式、修改后重新计算。而 Petzold 点出的关键,是它为什么必须发生在小型计算机上:

    "令我们惊讶与无奈的是,VisiCalc 这款应用程序无法在大型机上运行。因为像 VisiCalc 这类程序需要以较快的速度不断更新屏幕,所以,它们直接将数据写入 Apple II 视频显示器所配备的 RAM 中……大型时分计算机以及哑终端之间的接口速度过慢,以至于电子报表程序无法使用。" ⚑ 这是全书关于"延迟决定产品形态"的最强论据:不是大型机不够强,是那条链路上的延迟让某一类交互根本不可能存在。

    • 由此也解释了 IBM 硬件标准的作用:IBM 创建了一套硬件标准、其他制造商照着生产,"这样软件制造商就可以绕过操作系统直接操控硬件,统一化的硬件标准确保了程序的正确运行"——"如果所有同构的 PC 都拥有异构的视频显示器硬件接口,这种做法无异于将软件厂商推到了火坑里"。
  • 为什么从字符到图形这么慢:早期 PC 应用大多只输出字符,因为快——显示一个字符只要把它的 ASCII 写进内存;而图形显示器要写 8 个甚至更多字节,才能画出这个字符的外观。
  • 图形的史前史:SAGE 与光笔。冯·诺依曼早在 1945 年就预见了一种类似示波器的显示器;1950 年代早期 MIT 的林肯实验室(IBM 资助)为美国空军做防空系统 SAGE(半自动地面防空系统),要构建一个显示图形的屏幕来帮助操作员分析海量数据。
    • SAGE 用的是矢量(vector)显示器而非光栅——电子枪可以被电驱动定位到任何一点,直接画直线和曲线;因为当时光栅技术需要的存储器空间太大。
    • 光笔(light pen)的原理是全章最反直觉的一段:

      "理解它的关键在于光笔并不发射光——它所做的是检测光。" 电子枪移动控制电路有两个功能:光笔一旦感知到电子枪射出的光,系统立即做出反应;并在反应过程中确定光笔当时指向的屏幕位置。

    • 1963 年伊凡·苏泽兰为 SAGE 演示了 "画板"(sketchpad):把图像信息存在存储器里、显示在屏幕上,并可以用光笔在显示器上画和改,计算机全程跟踪光笔轨迹。
  • 恩格尔巴特与鼠标:他读过 1945 年布什的《思维之际》,五年后开始致力于研究计算机界面显示的新方法并为之奉献毕生精力。1960 年代中期在斯坦福研究所,他重新设计了输入设备:五股键盘(未普及)和一种配备轮子和按钮的设备——鼠标(mouse)。
  • 施乐 PARC 与 Alto(GUI 的诞生地):施乐 1970 年创建帕洛阿尔托研究中心,任务之一是加快公司迈入计算机产业的步伐;此时光栅显示器已经变得经济实用。PARC 最著名的预言家是阿伦·凯——14 岁那年他在罗伯特·海因莱因的一篇故事里读到了万·布什提出的微缩胶片图书馆,深受启发,不久构想了名为 Dynabook 的便携式计算机。
    • Alto(1972–73):落地式系统单元、16 位处理器(当时 16 位单芯片微处理器还未面世,所以它的处理器由将近 200 个集成电路组成)、两个 3 MB 磁盘驱动器、128 KB 内存(可扩到 512 KB)、三按钮鼠标。
    • 最动人的一个设计决定:"屏幕的大小和形状就像一张纸——8 英寸宽,10 英寸高。" 光栅成像,606 × 808 = 489,648 个像素,每像素 1 位(黑或白),专用视频存储器 64 KB,占用处理器的地址空间。
    • 软件由此可以直接写视频内存来绘图、显示不同字体和大小的文本;用户可以移动鼠标定位指针、与屏幕对象交互。"视频显示器与电传打字机在很多方面不尽相同,电传打字机顺序响应用户输入并按行将程序输出,而视频显示器的屏幕可以看做二维空间上的高密度的信息阵列,它还可以作为直接的用户输入源。"
    • 1970 年代晚期,Alto 上出现了窗口(多个程序同时显示)、图形对象(按钮、菜单,以及被称做图标的小图片)成为用户接口的一员、鼠标可在多窗口间选择并触发功能。
    • Petzold 在这里给软件下了一个定义:"软件的内涵就在于此,它的意义远不止仅有的用户接口,还包括与用户的亲密耦合。软件使得计算机所涵盖的应用领域变得更广,而不仅仅局限于简单的数字变换。" 而软件被设计出来的最终目的,引用恩格尔巴特 1963 年那篇著名论文的标题——《为了扩展人类的智慧》。
    • 结局:"施乐公司并没有将 Alto 推向市场(价格定位 3 万美元以上绰绰有余)。"
  • 从 Alto 到 Macintosh:1979 年乔布斯带苹果代表团访问 PARC,"在那里的所见所闻给他们留下了深刻的印象";花了三年多才推出 1983 年 1 月的 Apple Lisa(当时并不被看好);一年后的 Macintosh 大获成功——Motorola 68000、64 KB ROM、128 KB RAM、3.5 英寸 400 KB 软驱、键盘、鼠标、512×342 = 175,104 像素、每像素 1 位、约 22 KB 视频显示存储器。"最原始的 Macintosh 机硬件方面很精巧,但是可更新能力很差",真正让它与众不同的是 1984 年的系统软件(即今天的 Mac OS)。
  • 图形操作系统为什么必须变大(API 的爆炸):基于文本的单用户系统(CP/M、MS-DOS)体积很小但不支持扩展的 API;而图形化操作系统"包含了上百个 API 函数"。理由很硬:

    显示文本只要几个简单 API;显示图像"从理论上来讲,一个 API 函数完全可以胜任这项任务,函数的功能就是设置某个水平和垂直坐标下的像素的颜色。但在实际应用中,这种方法效率较低以至于严重影响到了图像显示的速度。" 所以操作系统必须提供一整套图形编程系统:画线(实线/虚线/点线)、画矩形、画椭圆(含不同填充模式)、画文本(不同字体、大小、加粗、下画线),并负责把这些图形对象转成显示器上的点阵集合。

    • 红利一:WYSIWYG。因为程序在显示器和打印机上画图使用的是一套完全相同的 API,所以屏幕上的文档和打印出来的纸质文档看上去非常相似。(词源注脚:"所见即所得"这句话出自喜剧演员弗雷普·威尔森扮演的杰拉尔丁角色,后来成了计算机领域的一个经典口号。)
    • 红利二:GUI 也是给程序员的。"不同的应用程序使用着大致相同的工作原理",操作系统承担支持 API 的重任,应用程序用这些 API 实现按钮和菜单。"GUI 不仅是一种看上去简洁友好的用户环境,对于程序员而言,它还是一种重要的开发环境。程序员在开发新一代用户界面的时候可以不用从底层开始重新编写。"
  • PC 阵营的两个额外难题(对比苹果软硬件自研的轻松):① 要同时兼容许许多多不同厂商的硬件(Macintosh 系统软件只需支持一种磁盘驱动器、一种视频显示器和两种打印机);② 必须直接兼容已有的 MS-DOS 应用程序,"就好像 MS-DOS 应用程序是为新的操作系统专门设计的"(而 Macintosh 就不必兼容 Apple II 的软件,因为微处理器型号不同)。
    • 竞赛结果:1985 年 DRI 的 GEM、VisiCorp 的 VisiOn、微软的 Windows 1.0("作为一匹黑马,当时它也被很多人认为将会成为'视窗争夺战'的胜利者")→ 直到 1990 年 3 月 Windows 3.0 发布,Windows 才真正受到瞩目。星星之火从那时开始燎原。 到本书出版的 2000 年,约 90% 的小型计算机使用 Windows。
    • 有意思的是硬件要求:"从原理上来分析,除了图形显示器,图形操作系统与文本操作系统相比,对硬件支持的要求并没有太多不同。从理论上来讲甚至硬盘驱动器都可以算是多余的:比如最初的 Macintosh 没有配备,Windows 1.0 也不需要。虽然大家都认为使用鼠标操作会更加方便,但其实 Windows 1.0 可以不需要鼠标。"(而 2000 年左右的主流图形操作系统已需要 200 MB 硬盘和 32 MB 以上内存。)
  • 面向对象(本章的语言学转折):PARC 大概从 1972 年开始研发 Smalltalk,嵌入了面向对象程序设计思想。Petzold 把它的意义放在冯·诺依曼的对立面来讲,这段极精彩:

    "从传统意义上来讲,高级程序设计语言会自然而然地区分出代码和数据……这种区分毫无疑问来自于冯·诺依曼计算机的体系结构。在这样一种体系结构中,只有两种元素,一种是机器码,一种是机器码所操作的数据。" "在面向对象的程序设计中,和冯·诺依曼计算机的体系结构所不同的是,对象(object)实际上是代码和数据的组合。在对象内部,与其相关联的代码决定了数据存在的意义,要理解数据的存储方式首先需要理解代码。" 对象之间靠发送/接收消息(message)通信。

    • 为什么它特别适合 GUI:"因为编程人员处理屏幕上的对象(如窗口和按钮等)的过程就是用户感知屏幕元素的过程。" 按钮的尺寸、位置、显示的文本或图标是与对象关联的数据;用户按下按钮时,系统向按钮对象发送一个消息,对象调用与自身关联的代码来响应。
    • C++(斯特劳斯特卢普,贝尔实验室;名字来源是 C 语言里的自增操作 ++)——最开始它是一种转换程序,把程序转换成 C 程序("但是转换出的 C 程序即难看又难以理解"),转换后再正常编译。
    • 一句非常克制的评价:"其实,面向对象语言能做到的,传统语言也能做到。但是编程终究是人类发明的一种解决问题的活动,面向对象语言使得编程人员多了一种可选的解决方案,这种解决方案具备更加优越的组织结构。"
    • IDE 与可视化编程(Visual Programming):按钮和其他组件可以通过鼠标拖曳"排版",从而达到在窗口交互设计的目的。
  • 文件格式的回旋(呼应第 22 章的"UNIX 思想"):文本文件在基于文本的年代是应用程序之间交流的理想媒介,"它的最大优点是可检索性"。但一旦要表达字体、大小、斜体黑体下画线,文本文件就不够用了,"很多字处理软件其实都会使用一种自己独有的二进制格式来存储文档"。折中方案是——用一个适当的转换字符把格式信息编码进去,而不影响可读性:微软的 RTF(富文本格式) 用 {、} 和 \ 封装格式信息;PostScript(Adobe 创始人之一约翰·沃诺克设计)把这个概念发挥到极致,是一种通用的图形编程语言。
  • 两类图形(这条分类被前后用了三次):
    • 矢量图形:用直线、曲线、填充区域配合算法生成;这是 CAD 的领域,在工程和建筑设计中至关重要;存成图元文件(metafile)——由生成矢量图形的一系列绘制命令的集合组成,通常已被编码为二进制形式。
    • 它的天花板被讲得很清楚:"如果你想设计桥梁,使用矢量图形来实现将很简单,但如果要显示桥梁的实际结构,矢量图形就显得无能为力了。"
    • 光栅图形 / 位图(bitmap):把图像编码成矩阵阵列,一个单位对应一个像素;有分辨率(宽 × 高)和色深(color depth / 颜色分辨率);"尽管位图从表现形式上看是二维的,但其本身的存储形式却是一串连续的字节"(通常从最顶端一行开始逐行往下)。
    • 颜色的算术:3 位/像素(红绿蓝各一位)"可能只适合简单的类似卡通画的图像";2 字节/像素(每原色 5 位 + 1 位保留)= 32,768 色 = 高彩色(thousands of colors);3 字节/像素 = 16,777,216 色 = 全彩色(millions of colors),640×480 下需要 921,600 字节(将近 1 MB)。颜色数 = 2^(每像素比特数);显存有限时提高分辨率就得降色深(1 MB 显存下,640×480 可以 3 字节/像素,800×600 就只能 2 字节)。
    • 位图从哪来:绘制程序"手工绘制"、算法生成、或通过 CCD(电荷耦合器,一种在光照下会起电的半导体器件,每个像素需要一个 CCD 单元来采样)捕捉——扫描仪(一行 CCD 扫过表面)、视频摄像机 + 帧采集器(把仿真信号转换成像素值阵列)、数码相机("近几年,数码相机的价格已逐渐降低到了家庭用户可以承担的水平")。格式:Macintosh 的 Paint / PICT(PICT 同时支持位图和矢量),Windows 的 BMP。
  • 数据压缩(一节小而完整的教学):
    • RLE(游程长度编码):一行天空可能连续 72 个蓝色像素,用"蓝色重复 72 次"来表示就小得多。办公室传真机用的就是它——因为传真只有黑白、没有灰度和彩色,通常会有很长串的白色区域。
    • LZW(GIF 所用,1987 年 CompuServe 开发,名字来自 Lempel、Ziv、Welch 三位创建者):比 RLE 更强大,因为它考虑的是像素值的模式(patterns),而 RLE 针对的是具有相同值的像素串。
    • 无损(lossless)压缩是可逆的,但"可逆压缩方法并不适用于所有类型的文件……在某些情况下,采用这些方法'压缩'后的文件比初始文件还要大!"
    • 有损(lossy)压缩不可逆,不应用于电子表格或文字处理文档("少一个数字或者字母都会'失之毫厘,谬以千里'"),但对图像可以接受。它的思想起源被特意点出:心理视觉——"所探究的是人的视觉,并根据心理因素确定人们所看到的景象中哪些比较重要而哪些不重要"。 JPEG(联合图像专家组)涵盖了几种压缩技术,有的无损、有的有损。
    • 两个方向的不对称(很重要):图元文件 → 位图很简单("视频显示存储器与位图在概念上保持一致",能画到显存就能画到位图);位图 → 图元文件却不容易,复杂时甚至无法转换 —— 为此发明了 OCR(光学字符识别),"OCR 软件会对比特流进行模式识别,然后确定其代表的字符。由于算法的复杂性,OCR 软件并不能保证百分之百准确。"

      ⚑ 这条不对称是全书的一个隐藏主题:从结构化到栅格化是投影(容易且有损),从栅格化回结构化是推断(困难且不保真)。 它同时是 OCR、语音识别、以及今天一切"从非结构化数据里恢复结构"的任务的形状。

  • 声音(把第 9 章的"信息 = 多个可能性中的一种"用到连续量上):
    • CD(1983,飞利浦 + 索尼):直径 12 cm,可存 74 分钟数字化声音——"而这个 74 分钟的时长因为贝多芬的第九交响曲刚好可以存储在一张 CD 上"。
    • PCM(脉冲编码调制):"名字听起来有点故弄玄虚,但从概念上讲 PCM 其实是一种很简单的过程。" 振动是声音之源 → 麦克风的可变电阻把声音转成连续变化的电流(仿真 analog 声波) → ADC(仿真数字转换器)把电压转成二进制数(12 位可区分 4096 个电压级别)→ 数值以小孔形式刻在光盘上 → 播放时用DAC(数字仿真转换器)转回电流("DAC 还可以用在彩色图形板上,将像素值转换成仿真信号并传输至显示器")。
    • 采样率(sampling rate):1928 年贝尔实验室的哈里·奈奎斯特证明——采样频率应至少为被采样信号最大频率的两倍。 人耳可听 20–20,000 Hz,所以 CD 用 44,100 Hz(比两倍还大一些)。
    • 位深与动态范围:贝尔(bel)这个名称来源于 Alexander Graham Bell 的最后一个单词的三个字母;1 分贝代表人类能感知到的最小强度变化;16 位能表示 96 分贝的动态范围——下限是刚好能听到的阈值,上限是人能承受的极限阈值。
    • 算总账:每秒 44,100 个样本 × 每样本 2 字节 × 立体声翻倍 = 每秒 176,400 字节 = 每分钟 10,584,000 字节;74 分钟立体声 CD = 783,216,000 字节。"这是个庞大的数字,正因如此,20 世纪 80 年代前数字记录声音的方法并没有普及。"
    • 数字化最大的红利被讲得很生活化:仿真声音每复制一次就失真一点,数字声音可以无失真转录。"细心观察的人会发现,过去在电话通话中,信号传输线路越长则声音越糟。这种情况已经一去不复返了,因为现在大部分电话系统都已经数字化了,即使你打跨国电话,听筒中的声音也像在对街对话一样清晰。"
    • 家用降规格:Mac 和 Windows 提供 22,050 / 11,025 / 8,000 Hz 采样和 8 位样本,降到每秒 8000 字节、每分钟约 480,000 字节。
    • 语音合成 vs 语音识别(一个清晰的难度分级):合成有两条路——预录句子短语单词再拼接("常见于电话公司的信息系统中,在这种环境下只需要有限的单词、数字及其组合"),或把 ASCII 转成波形数据(需要词典或复杂算法确定发音,用音素 phonemes 组词,"如果一个句子后紧跟着的是问号,则最后一个单词的发音频率必须相应提高")。而语音识别"是一个极其复杂的过程"——"其实许多人在理解口语化的方言时也有很多困难",个人计算机上通常需要用样本训练软件才能较准确地转录某个人的话语。

      "这中间涉及的问题已经大大超出了 ASCII 码文本转换的范畴,从本质上来讲是一个利用编程技术使得计算机'理解'人类语言的过程。这类问题正是人工智能所研究的领域。"

    • MIDI(乐器数字接口,1980 年代早期由电子音乐合成器制造协会推出):合成器以演奏声音的方式响应长度通常为 1、2 或 3 个字节的短消息序列(指明乐器、音符或休止符);MIDI 文件 = 消息集合 + 时间信息,"通常一个 MIDI 文件'麻雀虽小,五脏俱全'"。而 Petzold 给了一个非常好的类比:

      "就文件的相对大小而言,如果把一个波形文件比作位图文件,则 MIDI 文件就好像矢量图元文件。" 缺点也同源:合成器是异构的,同一个 MIDI 文件可能在一个合成器上完美演奏,在另一个上不堪入耳。

  • 数字化电影:24 帧/秒(电影)、30 帧/秒(北美电视)、25 帧/秒(世界大部分地区)。算账:640×480、24 位真彩色 → 每帧 921,600 字节 → 30 帧/秒 = 每秒 27,648,000 字节 → 每分钟 1,658,880,000 → 两小时约 199,065,600,000 字节 ≈ 200 GB。"正因如此,我们的计算机上播放的电影经常很短、分辨率很小,而且清晰度很差。"
    • MPEG 的原理被一句话点明:"动态图像压缩技术基于的是一种客观事实,即每一帧继承了前一帧的大部分信息,也就是说存在冗余信息。" MPEG-2 用于 HDTV 与 DVD(物理大小与 CD 相同但两面可记录、每面两层;视频可压缩为原始大小的五十分之一,两小时电影约 4 GB,只占一面一层;双面四层可达 16 GB,是 CD 的 25 倍)。
  • 回到麦克斯储存器(本章的合环):CD-ROM 和 DVD-ROM 比缩微胶片更适合担此重任,"由于电子媒体易于检索,它们比物理媒体更具有优越性。可惜同时访问多个 CD 或 DVD 驱动器并不现实。"

    "我们的存储设备有一点与万·布什所提出的概念有所不同,那就是所有信息并不一定要触手可及,真正使它们达到信息共享的做法是计算机互连,这样做还可以更有效地利用存储空间。"

  • 联网:对计算机进行公开性远程操作的第一人是乔治·史帝比兹(就是那位在厨房桌上用两个继电器做加法器的人),演示地点在达特茅斯,时间是 1940 年。
    • 调制解调器:电话线传的是声音不是比特 → 用单一频率振幅的连续声波(载波 carrier)本身传不了信息,对它进行调制(modulate)使其反映两种不同状态就能表示 0 和 1 → modem = 调制 + 解调。早期用频移键控(FSK):300 bps 时 0 调到 1070 Hz、1 调到 1270 Hz;每个字节被夹在一个起始位和一个停止位中间,所以实际占 10 位 → 300 bps 下每秒只传 30 个字节。("现在许多采用先进技术的新一代调制解调器速度超过了它的 100 倍。")它以串口(serial)方式工作——比特一个接一个传;而打印机常用并口,整个字节由 8 根线并行传输。
    • BBS → 大型信息服务(如 CompuServe),"在大多数环境下,通信中采用的一般都是 ASCII 码"。
    • Internet 的本质差别:"Internet 与这些早期的发明有本质上的差别,因为它是一种非中心化的系统。Internet 从本质上来讲是一组协议的集合。" 最重要的是 TCP/IP——"不再是简单地通过线路传输 ASCII 码字符,而由基于 TCP/IP 协议的传输系统把大的数据块分割成小的包(packets),之后在传输线路上独立发送,最后在传输线路的另一端将数据重新组装起来。"
    • WWW:HTTP + HTML。而 Petzold 把它接回了开头:"其中超文本(hypertext)这个单词用来描述相关链接信息,非常类似于万·布什预言的麦克斯储存器。" HTML 与 RTF 很相似(都是含有格式信息的 ASCII 文本),可以内嵌 GIF / PNG / JFIF;"大部分万维网浏览器都支持浏览 HTML 文件,这是文本格式的优势所在。易检索性也是 HTML 被定义成文本文件的另一个优点。" 并且明确澄清:"虽然名字可能有些让人迷惑,但 HTML 并不是真正的程序设计语言。"
    • 服务器端(CGI 脚本)vs 客户端(JavaScript)。
    • 然后是全书最后一个漂亮的推理链——"为什么 Web 站点不直接给你一个可执行程序?"

      要回答这个问题,首先要明确:我们的计算机是什么样的? 如果是 Macintosh,这个可执行程序需要包含 PowerPC 机器码并引用 Mac OS 的 API;如果是 PC 兼容机,需要 Intel 奔腾机器码并使用 Windows API。"但计算机及图形操作系统的种类繁杂……进一步来说,我们也不想毫无目的地下载可执行文件,它们很可能来自于非信任站点而且会带有恶意行为。" 答案是 Java(Sun 开发,成熟的面向对象语言,与 C++ 有些类似,请勿与 JavaScript 混淆):它需要经过编译,但编译的结果不是机器码,而是 Java 字节码——与机器码在结构上很相似,但在一台虚拟的计算机即 Java 虚拟机(JVM)上被解释执行。 因此 Java 程序不受限于机器与图形操作系统的类型,具有平台无关性。 ⚑ 这一段是全书抽象层思想的最后一次、也是最高一次运用:当下面那一层(硬件 + OS)无法统一时,就在它上面再造一层虚拟的、统一的机器。 第 17 章造了一台真机器,第 25 章造了一台假机器——而"假的那台"反而更通用。

  • 全书的最后一段(首尾呼应):讲完在导线上用电流传信号之后,他指出还有更有效的方法——光纤,由玻璃或聚合体制造的光导纤维,通过从不同角度对光进行反射达到传输效果,速率可达千兆赫兹——即每秒十亿个比特。然后是全书的收尾句:

    "展望未来,在以后的家庭和办公室中,光子似乎要替代电子承担海量信息传输的重任。比起莫尔斯码中的一'点'一'划',比起为了与一街之隔的好友深夜交流而绞尽脑汁想出的闪光灯,光子的速度无与伦比。" 500 多页、25 章、从两个孩子的手电筒开始,到光纤里的光子结束——全书只讲了一件事:人想说话,于是造出了所有这些东西。


3. 关键概念词典 GLOSSARY 逐条速查

第一组 · 编码与信息

  • 编码(code):一种在机器和人之间传递信息的方式;换句话说,编码就是交流。全书把口语、文字、手语、盲文、莫尔斯码、条形码、ASCII、机器码、HTML 都当作同一类事物的实例。
  • 二进制码(binary code):只由两种元素组成的编码。码字的数目 = 2^(编码的位数)——这是全书唯一一条从头用到尾的公式(莫尔斯码的分组表、盲文的 64 个码字、比特位数、RAM 容量、颜色数,全是它)。
  • 比特(bit):binary digit 的缩写,约 1948 年由 John Wilder Tukey 造词(他考虑过 bigit、binit)。1 比特是可能存在的最小信息量;信息 = 多个可能性中的一种。
  • 冗余(redundancy)与噪声(noise):噪声是影响通信效果的一切事物;冗余是为抵消噪声而刻意付出的额外比特。保罗·里威尔用两盏灯传三种可能、UPC 用 113 比特编 11 个数字、奇偶校验、模校验字符、中间护线,都是冗余的实例。
  • 频率设计:常用的编码给短码、冷僻的给长码(莫尔斯码的 E/T 与 Q/Z)。这是压缩的祖先。
  • 换档码 / 优先码(shift / precedence code):改变其后一整段编码的解释方式,直到遇到另一个换档码(盲文的数字标识符、Baudot 的数字转义)。它引入的是跨消息的隐式状态,是全书最古老的 bug 源。
  • 逃逸码(escape code):只改变紧随其后的那一个编码的解释(盲文的大写标识符、ASCII 的 Esc 序列)。作用域比换档码短,因此更安全。
  • ASCII:1967 年公布的 7 位字符编码。为什么是 7:52 个大小写 + 10 个数字 + 标点 > 64(所以 ≥7 位),但总数远不到 128(所以 <8 位)。大小写只差 20h,所以转大写只要 ANI A,DFh。它的病根在名字里:太美国化了。
  • Unicode:16 位统一字符编码(前 128 个与 ASCII 一致)。它唯一的问题是打破了"单字符 = 单字节"的等价关系——同一本书的存储要翻倍。

第二组 · 从电到逻辑

  • 继电器(relay):由电流而非人手控制的开关。全书最关键的零件——因为它的输出可以驱动另一个继电器的输入,所以电路可以被组合、可以被"编程"。
  • 布尔代数:操作数是类(集合)而非数;+ 是并集、× 是交集、1 是全集、0 是空集。区别于传统代数的标志式是 X² = X。
  • 香农等价(1938):两个开关串联 = AND,并联 = OR;而且简化布尔表达式就等于简化电路。这条等价把 1854 年的布尔代数和 1844 年的电报机接上,中间隔了 84 年。
  • 逻辑门(logic gate):与、或、与非、或非(各 2 个继电器)+ 反向器与缓冲器(各 1 个)。反向器不是门(门通常有两个以上输入)。异或门 XOR 由或门 + 与非门 + 与门组合而成,是加法位的关键。
  • 摩根定律:两个反相输入的与门 ≡ 或非门;两个反相输入的或门 ≡ 与非门。
  • 译码器与选择器:n-2ⁿ 译码器把 n 位地址翻译成"恰有一路为 1"的输出(写侧路由);2ⁿ-1 选择器从 2ⁿ 个输入里挑一个送出(读侧选路)。这两个零件在第 16 章造出 RAM,又在第 19 章解释了 MOV 指令的位模式。

第三组 · 算术与表示

  • 半加器 / 全加器:半加器 = 异或门(和)+ 与门(进位);全加器 = 两个半加器 + 一个或门(多了进位输入)。8 位加法器 = 8 个全加器 = 144 个继电器。
  • 行波进位(ripple carry):每一列等前一列的进位,总速度 = 位数 × 单个全加器速度。更快的方案是前置进位。
  • 补数(complement):把减法变成加法的技巧。对 9 的补数 / 对 1 的补数(二进制里就是每位取反,即一排反向器);10 的补数 / 2 的补数 = 补数 + 1,用来表示负数。8 位 2 的补数的范围是 −128 ~ +127,最高位是符号位。
  • 上溢 / 下溢:判据是"两个操作数符号相同,而结果符号与它们不同"。
  • BCD(二进制编码的十进制):每 4 位表示 0–9,一个字节装两位十进制数(压缩 BCD)。8080 专门用 DAA 指令和一个内置小部件来支持它。
  • 定点格式(fixed-point):小数点固定在某个位置,但小数点的位置本身并不与数字一起存储——程序必须自己知道。适合钱款,不适合跨量级的数。
  • 浮点格式(floating-point)/ IEEE 754:符号位 + 偏移指数 + 有效数。规范化二进制的小数点左边永远只有一个 1,所以那一位不存。单精度 24 位精度 ≈ 十进制 7 位;双精度 53 位 ≈ 十进制 16 位。特殊值靠指数全 0 或全 1 的编码空间来表达:0 / 非规范化数 / 无穷 / NaN。
  • 精度陷阱:单精度下 262144.00 与 262144.01 是同一个数;"应该得到 3.50 却得到 3.499999999999" 这类问题没有一套完整的解决方案。

第四组 · 记忆与自动

  • 反馈(feedback):输出接回输入。它造出了振荡器(自发工作,即时钟 clock)和触发器(有记忆)。
  • R-S 触发器 / D 型锁存器 / 边沿触发:R-S 记住"最后是哪个输入为 1";D 型锁存器记住"某个时刻数据端是 0 还是 1"(1 位存储器);电平触发在时钟为 1 的整段时间里都跟随数据,边沿触发只在时钟 0→1 的瞬间采样。
  • 分频器 / 计数器:Q̄ 接回 D 就得到分频器(输出频率减半);串起来就是二进制计数器——它是第 8 章"每一列的翻转频率是右边一列一半"这条规律的硬件化。
  • 累加器(accumulator):用来累加多个数的锁存器;在处理器里它是那个被特殊对待的寄存器(算术结果总落在它这里)。
  • 地址(address):译码器与选择器共用的那组选择信号。RAM 阵列的存储容量 = 2^(地址输入端的个数)。比喻是邮箱号。
  • RAM / 易失性(volatile):可随机访问的读写存储器;断电即全部丢失,因此需要恒定电流。(对照:顺序访问的磁带;非易失的磁盘与 ROM。)
  • 控制面板(takeover panel):能让人接管机器、并逐字节读写与查看其全部内部状态的装置。第 16 章的原型 → 第 22 章的 W/D/R 命令 → Altair 8800 的前面板。全书最被低估的一件东西。
  • 操作码(opcode)/ 指令 / 取指令 / 执行:数字代码指示电路执行某种操作;取指令是把它从存储器读进来,执行是它触发一组控制信号。"这并不表明机器是一种有生命的东西。"
  • 零标志位 + 条件跳转:零锁存器在加法器输出全 0 时置 1;条件跳转据此决定跳不跳。"能否控制循环,是计算机和计算器的区别。"
  • 冯·诺依曼结构:二进制 + 大容量存储器 + 代码和数据存在同一个存储器 + 程序计数器顺序寻址但允许条件跳转。代价是冯·诺依曼瓶颈——第 17 章那台机器 3/4 的时间花在取指令上。
  • TANSTAAFL:天下没有免费的午餐。"你改进了机器的某个方面,则其他方面就会受到损失。"(第 17 章为通用性把速度降到 1/4。)

第五组 · 系统与抽象

  • 总线(bus):提供给每块电路板的数字信号集合(地址 / 数据输出 / 数据输入 / 控制 + 供电)。开放体系结构 vs 封闭体系结构决定了第三方生态能否长出来。
  • 三态输出(tri-state):0、1 之外的第三种状态——"一种'真空'态,就像芯片的引脚上什么也没连一样"。它让多块板共用同一组数据线而不打架:任一时刻只有一块在驱动,其余物理断开。
  • 扫描码(scan code):键盘硬件给出的是扫描码而非 ASCII,因为同一个键的 ASCII 取决于 Shift,而且很多键根本没有 ASCII。从扫描码到 ASCII 是软件的活。
  • 文件系统 / 目录项 / 分配块 / FAT:把裸扇区变成"有名字、可以不连续存放、能被查找"的文件。CP/M 的目录项 32 字节、8.3 命名、16 字节磁盘存储表(单项上限 16 KB,更大的用扩展 extents);MS-DOS 改用簇 + FAT 链,并加上了日期、时间和文件大小。
  • 引导(booting / bootstrap loader):ROM 里一小段代码把磁盘首扇区拉进内存,那个扇区再把操作系统其余部分拉进来。"自举"。
  • API(应用程序接口):操作系统提供的、与设备无关(device-independent)的子程序集合。"API 屏蔽了硬件之间的差异。" CP/M 用 CALL 5,MS-DOS 用软件中断 INT 21h。
  • 多任务 / 时分复用 / 虚拟内存:多人多程序共享一台机器所必需的三件套;虚拟内存是"把暂时不用的内存块换到磁盘上"。
  • 汇编器 vs 编译器:汇编语言与机器码一一对应,所以汇编器只是查表替换(外加替你算地址);高级语言不具备这种对应,编译器通常要把一条语句转换成多个机器码指令。
  • 交叉汇编 / 自举:在 A 机上为 B 机汇编;新汇编器写好后可以对自身进行汇编。
  • 语法错误 vs 运行时错误:ende 拼错编译器立刻抓到;fist 拼错编译器永远抓不到。
  • 可移植性的代价:"如果某种高级语言具有真正意义的可移植性,那么它将不能使用某些处理器的特有功能。"
  • 面向对象 / 对象 / 消息:对象是代码和数据的组合——这一点恰恰是对冯·诺依曼"代码与数据两分"的偏离;对象之间靠消息通信。
  • 矢量图形 vs 位图:矢量存的是绘制命令的集合(图元文件),位图存的是像素阵列。图元文件 → 位图容易,位图 → 图元文件极难(这就是 OCR 的处境)。 同样的关系也出现在音频里:MIDI 文件之于波形文件,正如矢量图元文件之于位图文件。
  • 无损 vs 有损压缩:RLE(相同值的串)、LZW(像素值的模式)是无损可逆的,但对某些文件"压缩"后反而更大;JPEG/MPEG 的有损压缩思想起源于心理视觉——按人眼是否在乎来决定丢什么。
  • 虚拟机(JVM):当下面那层无法统一时,就在它上面再造一层虚拟的、统一的机器。编译到字节码 + 由虚拟机解释 = 平台无关性。

4. 批判性审视 COUNTERPOINTS 另一种声音 · 编辑部整理,不代表原书立场

哪些经久不衰(且对你直接可用)

  • 抽象层的搭建纪律是全书最耐用的资产,而且它与技术细节无关。"每一层只用下一层已建成的东西""每上一层就换一套词汇""知道自己什么时候可以停止下钻"(第 19 章把 8080 当黑盒)——这三条对搭 agent 工厂、写 skill 库、设计知识库主题骨架一比一成立。
  • "元件变强不会自动消灭上层脚手架,因为真正的成本是互连"(第 18 章)。这是全书最反直觉、也最抗时间的一条判断。它解释了为什么晶体管没让计算机结构变简单,也解释了为什么更强的模型不会自动让 agent 编排消失——集成电路真正做的事是"把常用组合预先连好",而不是"取消连接"。
  • 条件跳转 = 计算机与计算器的分界。把它翻译成今天的话:一个能被机器自己读取的成败信号,是"自动化"与"自我终止的循环"之间的全部差别。
  • TANSTAAFL:每加一层通用性都要付出吞吐。这条在 2000 年是关于指令周期的,今天是关于 token、延迟和多轮编排的,形式变了,账没变。
  • "被自己的感知模式禁锢"(霍伊 / 莫尔斯)与"隐式状态会在断点续跑时炸掉"(Baudot 转义码),是两类会一直重复发生的设计错误,跟技术代际无关。
  • 开放 vs 封闭的生态效应依然成立(尽管结论要打折,见下):公布接口 → 第三方生态 → 事实标准,这个机制本身没有过期。

哪些已被时代修正 / 需要打折扣

  • 全书的技术终点停在 2000 年:处理器讲到奔腾、显示讲到 1600×1200、内存讲到 128 MB、硬盘讲到 20 GB、DVD 是"未来的软件存储媒介"、Java 是跨平台的答案。这些具体数字只有量纲价值,没有现实价值。
  • 摩尔定律的叙述已经被历史修正。书里说"一些人相信直到 2015 年摩尔定律仍然有效",但它完全没有预见到 2000 年代中期单核频率撞墙、转向多核 这件事。更要命的是:全书从头到尾没有并行计算,没有多核,没有 GPU。 而今天最重要的计算形态恰恰是大规模并行——这是本书体系里最大的一块空白。
  • "开放 90% vs 封闭 10%"这条结论被后来的历史部分证伪。2000 年时它是对的;但苹果此后靠软硬件一体的封闭体系做出了 iPhone 与 App Store 这个人类历史上最成功的计算平台之一。正确的读法不是"开放胜出",而是"开放与封闭各自的胜出条件不同"——开放赢在需要大量异构第三方硬件的时代,封闭赢在体验一致性与端到端性能是决定性变量的时代。(书里其实埋了这个变量:PC 图形操作系统开发者面对五花八门的硬件,而 Macintosh 只需支持一种驱动器、一种显示器、两种打印机。)
  • Unicode "16 位 65,536 个字符够全世界用"的判断已被推翻。今天 Unicode 的码位远超 10 万,需要 UTF-8 / UTF-16 与代理对——而这恰恰又是书里那句"编码的每一次统一都要在存储上付代价"的进一步验证:Petzold 的原理对了,他给的数字过时了。
  • 本书是彻底的"冯·诺依曼中心主义"。作者自己诚实地承认了这一点(第 24 章:"所有的类 ALGOL 语言,其设计模式都是基于冯·诺依曼计算机体系"),并且把 LISP、APL、面向对象都作为偏离来讲。但今天最重要的计算范式——神经网络——恰恰模糊了"代码与数据"的分界:权重既是数据又是程序,行为不是被写出来的而是被拟合出来的。 这在本书的框架里完全没有位置。读这本书能让你彻底理解你的 agent 跑在什么之上,但理解不了你的 agent 本身。
  • 它对"性能优化"的态度是刻意天真的。Petzold 明说"我们关注的是让一切有效地运转而不是以最简的形式运转"。这对教学是正确的取舍,但如果有人拿本书的方法去做工程判断,会严重低估现代系统里缓存、流水线、内存层次带来的复杂度(他把 Cache 和流水线放在第 19 章末尾用两句话带过)。
  • 一处外部补充(非原书内容,明确标注):Petzold 在 2022 年出版了 Code 的第二版,据我所知补充了更现代的内容。本报告完全基于用户提供的 2012 年中译本(对应 2000 年英文原版),上述"时代局限"的判断只适用于这一版。

与其他书的对话(都在你的书库里)

  • 与《人月神话》(Brooks):Brooks 区分本质复杂度与附属复杂度,并断言没有银弹。Petzold 这本书写的几乎全是一部"附属复杂度的消除史"——从手工汇编到汇编器、从裸扇区到文件系统、从直接写显存到图形 API。两本书合读的价值在于:Petzold 告诉你抽象层是怎么造出来的,Brooks 告诉你造完之后剩下的那部分为什么还是那么难。
  • 与《失控》(凯文·凯利):两本书都从最底层往上讲复杂系统的涌现,但方向相反——Petzold 是"自底向上地设计",KK 是"自底向上地演化"。放在一起看能让你对"哪些系统该被设计、哪些该被培育"有更清楚的判断(你的 agent 工厂两者都有:契约该被设计,选题该被培育)。
  • 与《AI 未来已来》(李开复):李开复的核心方法论是"先画地图(本体),再接模型"。Petzold 的整本书就是一张最彻底的地图——它论证的是"当你真的拥有从底到顶的完整地图时,每一层的取舍都会变得可判断"。两者是同一条主张的两个尺度。
  • 与《高产出管理》(格鲁夫):格鲁夫的"黑箱开窗 + 领先指标"和 Petzold 的"控制面板 + 零标志位"是同一个念头的两种语言——在系统能自己跑之前,先造出能看见它内部的窗。

6. 章节速览 AT A GLANCE
  • 推荐序:科学不等同于晦涩;用电线、灯泡、触发器"拼拼接接"造出一台计算机。
  • 译者序:Petzold 从历史切入、自底向上,旁征博引却处处别出心裁;译者读到想动手。
  • 作者序:这本书讲计算机如何工作,但不用比喻代替机制;层次化体系结构就是本书的编写结构;前 17 章的设备都能用一个世纪前的器件造出来。
  • 第 1 章:手电筒交流的三次迭代(画字母 → 闪 N 次 → 莫尔斯码);编码就是交流;关键在于数字 2。
  • 第 2 章:接收比发送难 → 按点划数目分组 → 码字数 = 2^位数 → 树形解码图。
  • 第 3 章:霍伊被自己的感知模式禁锢;布莱叶用 2×3 六点把 64 个码字用满不浪费;换档码与逃逸码登场。
  • 第 4 章:电路是回路;欧姆定律与功率;开关只有两态,和莫尔斯、布莱叶一样没有中间状态。
  • 第 5 章:公用连接省 25%、用地球当导体再省一根;但长导线的电阻造出了全书第一个死局(跨美洲电报的真实困境)。
  • 第 6 章:莫尔斯坚持纸上硬拷贝而操作员改用耳朵;中继站员用木棍把发声器和电键绑起来 → 继电器:由电流而非人手控制的开关。
  • 第 7 章:数字是最抽象的编码;十进制纯属手指数;阿拉伯数字的三点不同(位置相关、没有 10 的符号、有 0)。
  • 第 8 章:卡通人物八进制、龙虾四进制、海豚二进制;八进制的好整数全是 2 的幂;电线/开关/灯泡/继电器都能表示比特;bit 的词源。
  • 第 9 章:黄丝带与保罗·里威尔的提灯(冗余抗噪);信息 = 多个可能性中的一种;胶卷 DX 编码与 UPC 条形码的完整解码。
  • 第 10 章:布尔把代数从"数"里抽离出来(X²=X);宠物店选猫表达式;串联=AND,并联=OR;而 19 世纪没有任何人把两者接上,连巴贝奇也没有。
  • 第 11 章:香农 1938 补上了那 84 年的空白;四个基本门 + 反向器 + 缓冲器 + 2-4 译码器;摩根定律;"让一切有效地运转而不是以最简的形式运转"。
  • 第 12 章:加法就是计算机要做的唯一工作;进位表=与门,加法位=异或门;半加器→全加器→8 位加法器 = 144 个继电器。
  • 第 13 章:用补数把减法变成加法(对 1 的补数就是一排反向器);求补器 + 一个 SUB 信号驱动三处;2 的补数与符号位;"比特本身并没有任何含义"。
  • 第 14 章:反馈 → 振荡器(时钟)→ R-S 触发器(电路第一次有记忆)→ D 型锁存器 → 8 位锁存器/累加器 → 边沿触发 → 分频器 → 计数器。
  • 第 15 章:字节为什么是 8(BCD/文本/灰度);十六进制赢在能等分一个字节;同一个数的五种进制写法;埋下 99h 的 BCD 钩子。
  • 第 16 章:锁存器改名成"写操作端";8-1 选择器 + 3-8 译码器 = 地址;容量 = 2^地址线数;64K×8 RAM 与控制面板;500 万个继电器与断电时的那声"梆"。
  • 第 17 章(全书顶点):九次迭代把加法器变成计算机——RAM 喂数据 → 结果写回 → 操作码 → 减法 → 进位锁存器 → 三字节指令(代价:速度 1/4,TANSTAAFL) → 合并 RAM → Jump → 零标志位 + 条件跳转 = 计算机与计算器的分界;术语总收(ALU、PC、汇编、bug),并主动指出自己程序里的 bug。
  • 第 18 章:算盘到集成电路的压缩史;进位才是加法器的核心问题;巴贝奇与艾达;霍尔瑞斯与 IBM 的由来;K 机器与那只飞蛾;冯·诺依曼结构与瓶颈;"晶体管让结构更简单吗?答案是否定的";摩尔定律;《TTL 数据手册》的自曝;纳秒;霍夫的"通用计算机而非计算器"。
  • 第 19 章:8080 与 6800 的对照;把微处理器当黑盒看;寄存器为何存在;MOV 的位模式就是选择器 + 译码器;三种寻址方式;DAA 解开 99h 之谜;堆栈(叠文件夹)与 CALL/RET/子程序;大端小端与《格列佛游记》;Altair 前面板 = 第 16 章的控制面板。
  • 第 20 章:文本不是排版;Baudot 转义码的乱码灾难;7 位是怎么算出来的;ASCII 与大小写差 20h;EBCDIC 与打孔卡;用书的大小建立字节直觉;"它太美国化了";扩展 ASCII、DBCS、Unicode 与它的双倍代价。
  • 第 21 章:总线的四类信号;开放体系结构造就 90% 份额;三态输出让多板共用数据线;SRAM/DRAM 与"更新";带宽算出 320×200;显存需求 1 KB–192 KB;80 列来自打孔卡、4:3 来自爱迪生;扫描码;磁介质与"memory vs storage"的分类规则。
  • 第 22 章:开机是一屏随机字符——缺的是软件;用控制面板敲到手起水泡;4Bh 会显示成 K;键盘处理程序 → 命令处理程序(W/D/R)→ ROM → 计算机变得可交互;CP/M 的目录项 32 字节与 8.3;引导与自举;API 屏蔽硬件差异;MS-DOS 与 FAT;"反传统性"让抽象层被击穿并固化成标准;UNIX 的可移植与"用文本文件当公用格式"。
  • 第 23 章:数的谱系;定点格式与它的破产场景;科学计数法与"小数点左边永远只有一个 1";IEEE 754 的四种特殊值;262144.00 = 262144.01 的精度真相;浮点运算都能拆回整数;8087 那个空插槽与"可选能力"的历史终局。
  • 第 24 章:"用机器码编程就像用牙签吃东西";汇编器只是查表但替你算地址;自举与交叉汇编;汇编的两个死穴(乏味、不可移植);语法错误 vs 运行时错误;ALGOL 全要素 + 爱拉托逊斯筛法;科学还是艺术;FORTRAN/COBOL(千年虫的由来)/BASIC/Pascal/C 谱系;"真正可移植就用不上处理器的特有功能"。
  • 第 25 章:万·布什的《思维之际》与麦克斯储存器(主角是信息);界面演进四段;VisiCalc 跑不了在大型机上;SAGE 与光笔(它检测光而不是发光);恩格尔巴特的鼠标;PARC 的 Alto(屏幕像一张纸)→ Lisa → Macintosh;图形 API 的爆炸与 WYSIWYG;面向对象是对冯·诺依曼两分法的偏离;矢量 vs 位图、RLE/LZW/JPEG、OCR 的困难;PCM/奈奎斯特/CD 的 74 分钟;MPEG 与 200 GB 的电影;TCP/IP 的非中心化;为什么不直接给你可执行文件 → Java 虚拟机;结尾回到那支手电筒。

7. 金句集(提炼式,非逐字引用) QUOTES

说明:为尊重版权,本节以压缩转述为主,只保留极短的原话片段,并标注章次。

  1. 关键在于数字 2——两个不同的事物,适当组合就能表示一切信息。(第 1 章)
  2. 编码就是交流;一种编码有用,是因为它能用在别的编码到不了的地方。(第 1 章)
  3. 码字的数目 = 2 的编码位数次幂——全书唯一一条从头用到尾的公式。(第 2 章)
  4. 霍伊"被自身的感知模式所禁锢":对他来说 A 就必须摸起来像个 A。(第 3 章)
  5. 逃逸码让你"逃离"对编码串一成不变的解析,转入另一种解析。(第 3 章)
  6. 在茫茫宇宙中,电不同于任何其他事物——必须用它自己的术语来解释它。(第 4 章)
  7. 地球之于电子,就恰如海洋之于水滴。(第 5 章)
  8. 继电器意义非凡:它是一个由电流、而非人手控制的开关——用它甚至能装出一台近乎完整的计算机。(第 6 章)
  9. 位置计数法真正的价值,不在于它好用,而在于它对非十进制系统同样成立。(第 7 章)
  10. 信息是指多个可能性中的一种。(第 9 章)
  11. 19 世纪没有任何人把 AND/OR 与串联/并联接上——没有数学家,没有电学家,没有电报员,连巴贝奇也没有。(第 10 章)
  12. 我们关注的是让一切有效地运转,而不是以最简的形式运转。(第 11 章)
  13. 当你真正面对它时会发现:加法就是计算机要做的唯一工作。(第 12 章)
  14. 二进制数的麻烦在于——它们只是一些 0 和 1,本身并没有任何含义。(第 13 章)
  15. 如果没有了记忆力,你该如何去数数?(第 14 章)
  16. 断电之后,RAM 里的数据将"如风中残烛般消失在黑暗中"。(第 16 章)
  17. TANSTAAFL——天下没有免费的午餐:改进了一个方面,另一个方面必然受损。(第 17 章)
  18. 能否控制循环,是计算机(computer)与计算器(calculator)的区别。(第 17 章)
  19. 起初进位只被看作加法里的"一碟小菜",但它才是加法器的核心问题。(第 18 章)
  20. 艾达·拜伦:"解析机编织出了代数的结构模型",正如提花织布机织出花瓣和树叶。(第 18 章)
  21. 晶体管让计算机结构更简单了吗?答案是否定的——难的从来是互连。(第 18 章)
  22. 纳秒使计算机成为可能,尽管地球上没有人真正理解纳秒。(第 18 章)
  23. 霍夫的选择:做一台能被编程成计算器的通用计算机,而不是一台稍能编程的计算器。(第 18 章)
  24. 处理器之间真正的根本区别是速度——而速度最终决定用途。(第 18 章)
  25. Altair 8800 的前面板,和第 16 章那个 RAM 控制面板是类似的。(第 19 章)
  26. 把文本看成一维的数据流:字体变了,故事没变。(第 20 章)
  27. ASCII 的病根写在它的名字里——它太美国化了。(第 20 章)
  28. Unicode 唯一的问题,是它打破了"单字符 = 单字节"的等价关系。(第 20 章)
  29. 三态的第三种状态是一种"真空"态——就像引脚上什么也没连一样。(第 21 章)
  30. 640×480 之所以是 4:3,是因为它和托马斯·爱迪生有关。(第 21 章)
  31. 开机屏幕上全是随机字符——我们漏掉的就是软件。(第 22 章)
  32. API 屏蔽了硬件之间的差异——这是操作系统的第三大功能。(第 22 章)
  33. 浮点数会让你"应该得到 3.50 却得到 3.499999999999",而这没有一套完整的解决方案。(第 23 章)
  34. 单精度下,262144.00 美元和 262144.01 美元是同一个数。(第 23 章)
  35. 用机器码写程序就像用牙签吃东西:使很大的力,每次只夹到一小块。(第 24 章)
  36. 如果一种高级语言真正可移植,它就用不上某些处理器的特有功能。(第 24 章)
  37. 100 个人写素数程序会得到 100 种解法——如果它是科学,就不该如此。(第 24 章)
  38. 万·布什 1945 年那篇文章的不同之处在于:这个故事的主角是信息。(第 25 章)
  39. 与其让计算机适应人,不如劝服人去适应计算机——这更容易,这也正是问题所在。(第 25 章)
  40. 光笔的关键在于:它并不发射光,它检测光。(第 25 章)
  41. Alto 的屏幕大小和形状就像一张纸。(第 25 章)
  42. 对象是代码和数据的组合——这正是对冯·诺依曼两分法的偏离。(第 25 章)
  43. 比起那支为了跟一街之隔的好友深夜说话而想出的手电筒,光子的速度无与伦比。(第 25 章,全书末句)

📒 附 · 你 2020–21 年的旧笔记(已融入本报告)

来源:30_sources/notion/epic-tracker/04_书架/编码-隐匿在计算机软硬件背后的语言.md(原存于 Notion「Epic Tracker」知识 OS 的书架里,含一个内嵌数据库《编码扩展阅读》)。

  • 一份完整的 25 章目录抄录(说明你当年至少把全书结构过了一遍)。
  • 一条标题为「推荐序笔记(摩斯密码)」的批注,配了两张图(摩斯密码字母对照表 / 摩斯密码解读查询表),内容是:
    • 莫尔斯码按字母常用频率设计,以最大程度减少发送和接收时间;
    • 把莫尔斯码按点、划的类型分类,得到一张按频率查看的对照表;
    • 你自己的两条推论:"对新人的引导——即使是第一次见到这张表的人也能马上意识到并能够正确使用";"对交互设计的启发——从最简单到最复杂的层层递进和引导,是这张表这么成功的原因"。
  1. 你当年读到的是全书第 1–2 章,而你从里面提炼出的,恰好是全书 25 章的组织法本身。 Petzold 用整本书做的事,就是"从最简单到最复杂的层层递进"。你在第 2 章就把方法论抓住了,只是当时把它归到了"交互设计"这个抽屉里。
  2. 你的两条推论今天有了新的用武之地。 "第一次见到的人能不能马上正确使用"——这条标准今天该被用在你的 skill 文档、_shared/entities、agent 契约、PRD 模板上:一个新 agent(或新同事)第一次读到它,能不能不问任何问题就正确使用? 这比"写得全不全"是更硬的验收标准。
  3. 它也暴露了一个事实:这条笔记停在了推荐序/前两章,没有往下走。而本报告已经把剩下的 23 章补齐了——六年前你抓住了这本书的"读法",今天可以补上它的"内容"。 特别是第 16、17、22 章那条"控制面板 → 命令处理程序 → 操作系统"的线,是六年前的你不需要、而今天搭 agent 工厂的你最需要的部分。

这本书的邻居 NEIGHBORS 同书架 · 商业与科技