作者:Charles Petzold(Windows 编程界开创者之一,1994 年七位 Windows Pioneer 奖得主中唯一的作家)|英文原版 2000 年|中译本约 44.2 万字|实测研读:基于中译本全文 25 章 + 三篇序逐章精读,无跳读、无据小标题概括的章节|本拆书稿实测约 10.7 万字(净正文约 8.7 万字),研读约 250 分钟
Petzold 在作者序里给出了全书的组织原则:计算机拥有与生俱来的层次化体系结构,底层是晶体管,顶层是显示器上呈现的信息;本书就是自底向上分析这个结构的每一层。 更狠的一个约束是:前 17 章里出现的所有设备,理论上都可以用已经存在了一个世纪的简单电子器件来构造。 这不是复古趣味,而是方法论——追溯得越久远,技术的脉络就越清晰。
全书可以切成五段,它们不是并列的主题,而是一条不可打乱的搭建链:
贯穿全书的四条主线:
⚑ 全书最重要的方法论伏笔:他不是禁止比喻,而是要求先把机制讲透,比喻才允许出现。第 21 章讲完磁盘与 RAM 的真实差别之后,他自己就用了"办公桌 vs 文件柜"这个比喻,而且说"这个比喻恰到好处"。同一个比喻,讲之前用是遮蔽,讲之后用是压缩。
electron 与 electricity 都源于古希腊语 ηλεκτρον,意思其实是"琥珀"(古希腊人用琥珀摩擦羊毛产生静电);导体(铜、银、金——最外层只有一个电子,且它们在元素周期表同一列不是巧合)与绝缘体;电池串联 3V、并联 1.5V 但寿命翻倍;欧姆定律 I = E / R;功率 P = E × I(手电筒 3V ÷ 4Ω = 0.75A = 2.25W)。digit 同时是"手指/脚趾"和"数字",five 和 fist 同词根,都不是巧合)。algorithm 即由他的名字衍生;拉丁译本约 1120 年):binary digit 起个短词,考虑过 bigit 和 binit,最终选了 bit。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 里不包含价格——价格在商店的计算机里。100100 101010 100110 100010。收束句:比特从根本来说是数字;用比特表示其他信息时我们要做的,就是计算有多少种可能性。 而最后一句把门推向下一章:比特在逻辑学中也很重要——真和假同样可以表示为 1 和 0。+ = 并集(B+W = 所有黑猫和白猫)× = 交集(F×T = 所有褐色母猫)1 = 全集(M+F=1;T+B+W+O=1;N+U=1)0 = 空集(F×M=0)1−M = 除公猫外的所有猫 = F(M × N × (W + T)) + (F × N × (1 − W)) + B
然后把 + 读作 OR、× 读作 AND、1− 读作 NOT,把字母赋值为 0/1(1=符合、0=不符合),逐只猫代入化简:× 对应一处串联、每个 + 对应一处并联),三只猫依次验证,第三只点亮了灯泡。在 19 世纪,没有人将布尔代数中的 AND 和 OR 同线路中的开关串联及并联关联到一起。没有这样的数学家,没有这样的电学家,没有这样的电报员,没有这样的人。 甚至连查尔斯·巴贝奇也没有——他与布尔同时代、了解布尔的工作,一生都在设计差分机和分析机,"本来有些东西可以帮到巴贝奇",那就是根据一台电报器来创建计算机,而非使用齿轮和杠杆。
bit 这个词的文章。)(N × ((M × (W + T)) + (F × (1 − W)))) + B
括号更丑,但少了一个变量,就少了一个开关。更彻底地看:理论上只需要 4 个开关——1 位性别、1 位是否绝育、2 位颜色(4 种颜色 = 2 比特)。于是做出一个控制面板:4 个开关 + 1 个灯泡。输入设备(input device)与输出设备(output device)这两个术语在这里正式登场。⚑ 值得停一秒的设计之美:一个控制位(加/减开关)同时驱动三处不同的行为,而这三处行为在数学上是同一件事的三个部分。这就是"用一个信号编码一个决策"的原型——你在写 agent 的模式开关时,最容易犯的错就是把它拆成三个互相可能不一致的标志位。
"这就是二进制数的麻烦之处,它们只是一些 0 和 1,本身并没有任何含义。" ⚑ 这句话是全书的元命题:比特没有语义,语义全在约定里。第 15 章的 99h(可以是 153、−103,也可以是 BCD 的 99)、第 17 章的 FFh(既是 HLT 指令又是 −1)、第 20 章的 ASCII("12"是两个字符不是数字 12)、第 23 章的浮点格式,都是这一句的注脚。
byte 约 1956 年由 IBM 提出,最早拼作 bite,为避免与 bit 混淆才把 i 换成 y;1960 年代中期随 System/360 定型为"8 比特"。半个字节 = 半字节(nibble / nybble)。10110110₂ = 2312₄ = 266₈ = 182₁₀ = B6h。(书里统一用小写 h 后缀。)99h 可以是无符号的 153,也可以是有符号的 −103。⚑ 这个控制面板是全书最被低估的一件东西:它是"人可以随时接管、并逐字节看见与改写机器全部内部状态"的最早形态。第 17 章的自动机器、第 19 章的 Altair 8800 前面板、第 22 章的引导过程,全都建立在它之上。
⚑ 请记住这一段:第 21 章的磁介质存储、第 22 章的文件系统与引导程序,全部是为了对抗这声"梆"。
这一章是整本书的心脏。它用九次迭代,把一台"要人一直坐着按开关的加法器"变成一台真正的计算机。每一次迭代都是被上一次的具体缺陷逼出来的,没有一步是凭空跳的。 下面按这条链子拆。
迭代 0(起点):第 14 章那台带累加器的加法器——按清零、拨开关输入第一个数、按"相加"存进锁存器、再拨第二个数、再按"相加"……缺陷显而易见:要加 100 个数,你得端坐在那儿把 100 个数一个个拨进去;等你终于做完,发现其中两个输错了,就只能重来一遍。
迭代 1 · 让 RAM 喂数据、让计数器走地址:把 100 个数先放进第 16 章的 64KB RAM(可以随时改),用 RAM 的输出替代那排开关;再用第 14 章的 16 位计数器驱动 RAM 的地址。振荡器的时钟每跳变一次,两件事同时发生:锁存器保存加法器的结果,计数器加 1 指向下一个地址。
迭代 2 · 结果写回 RAM:把累加器的输出接到 RAM 的数据输入,用 RAM 控制面板的灯泡看结果。需要一个控制 RAM 写信号的办法。
迭代 3 · 操作码诞生(本章第一个大跳):新需求是"分别算 3 个数的和、2 个数的和、3 个数的和"。这需要机器自己知道每一步该干什么,于是定义四种操作:
实现方式简单粗暴:再放一个 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 · 三字节指令(本章的转折点,也是全书对"通用性"最重的一次定价):
TANSTAAFL —— "天下没有免费的午餐"。"通常上帝总是很公平的,你改进了机器的某个方面,则其他方面就会受到损失,有得就有失。"
迭代 7 · 合并两个 RAM(冯·诺依曼结构成形):既然指令自己带地址了,代码和数据就没有必要再放两个独立的 RAM。加一个 2-1 选择器决定 RAM 的地址来自 16 位计数器(取指令时)还是来自锁存的 16 位地址(读写操作数时)。于是操作码和操作数存进同一个 RAM——指令从 0000h 起排,数据从 0010h 起排。
迭代 8 · Jump(被"Halt 挡路"逼出来的):
迭代 9 · 条件跳转(全书的临界点):
"条件跳转指令将它与我们以往设计的加法器区别开来,能否控制重复操作或者循环(looping),是计算机(computer)和计算器(calculator)的区别。" 有了它,乘法之外的除法、开平方根、对数、三角函数全都可以用软件做出来——机器所需要的仅仅是能做加减法的硬件,以及利用条件跳转执行代码的方法。"正如程序员经常挂在嘴边的一句话:'我可以用软件完成其他工作'。"
术语总收(这一章结尾把全书前 16 章的成果一次性命名):
这一章是全书的"回望章"——前 17 章造了一台不存在的机器,这一章告诉你历史上真实发生了什么,以及那台机器为什么是可信的。
⚑ 可迁移:"看起来是配菜的那一步,往往才是真正的难点。"(对你的 PRD 工厂:碰撞协议里最容易被省略的"独立初稿互不可见"就是进位。)
bit 介绍给世界并开创信息论(研究数字信息在有噪声情况下的传输以及如何弥补损失);1949 年写了第一篇关于如何编程让计算机下棋的文章;1952 年做了一只继电器控制的机械鼠,能记住迷宫路径;还因骑单车变戏法在贝尔实验室出名。韦纳 18 岁拿到哈佛数学博士,1948 年的《控制论》用新词 cybernetics(源于希腊语"舵手")描述生物过程与机械原理的关系 → 后来的 cyber- 前缀 → cyberspace(源自吉布森 1984 年小说《神经漫游者》里的 cyberpunk)。"有一个疑问:晶体管肯定可以使计算机更加可靠、体积更小以及需要的电量更少,但晶体管可以使计算机的结构变得更简单么?答案是否定的。" 理由说得极清楚:晶体管允许在更小的空间里安装更多逻辑门,但是你还是要考虑这些组件之间的互连问题。把晶体管连成逻辑门,和把继电器、真空管连成逻辑门一样困难;某些方面还更困难,因为晶体管更小而且不容易被控制。如果你想用晶体管造第 17 章那台机器加 64KB RAM,设计工作的重要部分是构造某种可以放置所有部件的结构,而你的大部分体力劳动是在数百万只晶体管之中连接数百万根线。
"现在你应该知道我是怎么想出从第 11 章中就开始使用的这么多不同种类的组件的,这些都是从《TTL 数据手册》借鉴而来。" 他不是从零发明了那些积木,他是照着一本真实存在的元件目录反向讲了一遍。 这也解释了为什么前 17 章的每一个组件都恰好"刚刚好够用"。
"地球上的所有人对纳秒只有概念上的理解,除此之外别无所有。纳秒比人类感觉到的任何事情都要短暂得多,以至于永远无法理解它。任何解释只会将纳秒变得更加难以捉摸。"(他试了一个:拿着一本离脸 1 英尺的书,纳秒就是光从书页到眼睛的时间——"但这种解释可以使你更好地认识纳秒么?") "然而,纳秒使计算机成为可能。" 处理器迟钝地做着简单的事——取一个字节、加两个字节、存回去——迅速地完成这些操作是计算机能完成任何实际工作的唯一原因。 诺依斯的话被引在这里:"当更好地认识了纳秒后,从概念上来讲计算机操作是相当简单的。"
"我想让它成为一个具有通用功能的计算机,进而可以通过编程成为一个计算器,而不是使这个设备成为一个只有一些编程能力的计算器。" 这导致了 Intel 4004——第一块微处理器,2300 个晶体管("依照摩尔定律,18 年后微处理器包含的晶体管数将是这个数字的 4000 倍,或者说是 1000 万。这是一个相当精确的预测")。
"上述三个数字指标并不能影响一台计算机的计算能力。" 4 位处理器同样能做 32 位加法,只是拆成 4 位一段。"如果一台处理器从硬件上无法做到另外一台可以做到的事情,那么它可以通过软件途径做到,最终它们可以完成相同的事情——这是 1937 年图灵在论文里面关于可计算性的一种定义。" "然而,速度是处理器之间的根本不同点,同时速度也是我们使用计算机的一大原因。" 而且速度最终决定用途:比人脑还慢的计算机是毫无用处的;如果画一帧图要一分钟,就永远播不了电影。
⚑ 这是全书唯一一次主动"停止下钻":因为下一层已经在第 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 |
01 ddd sss——ddd 是目标寄存器编码,sss 是源寄存器编码。然后 Petzold 说:"可以设想一下,在 8080 的内部可能是这样的:标记为 sss 的 3 位用于 8-1 数据选择器,标记为 ddd 的 3 位用来控制 3-8 译码器以此确定哪一个寄存器锁存了值。" 这正是第 16 章造 RAM 时用的那两个零件。 指令集不是魔法,它就是地址线。
MOV M,M 这个组合本该存在,但它被占用为 HLT(停止)指令。XRI A,FFh)、INR/DCR(加 1/减 1,第 17 章要靠"加 FFh"实现)、四条循环移位 RLC/RRC/RAL/RAR(用于乘 2 / 除 2,只影响 CF)。big-endian 这个术语出自《格列佛游记》——小人国与邻国关于吃鸡蛋前该敲碎哪一头的争论。Petzold 的态度很坦率:"因此,这种争论可能是没有意义的(另一方面,坦白地说,在本书第 17 章设计的计算机所采用的方式我个人并不喜欢)。" 但差异确实制造了真实的兼容性问题。"当你看到 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 操作码并执行对应操作,比原生慢但能工作。
"我极力想阐述一个重要的观点,那就是文本与其印刷在纸上时采用的二维排版格式是两码事。充分发挥想象力,将文本看成是一维的由字母、数字和标点符号组成的数据流吧。" 论据是:一个你曾在杂志上读过的故事,几年后在另一本书里重逢,你回忆起的是故事本身而不是排版,没有人会因为行距不同而把它们当成两个故事;字体变了,"字体改变了文本的表现形式,但故事本身的内容并没有因此而改变"。再进一步:不要斜体、粗体、下划线、颜色、上下标、音标——只要赤条条的拉丁字母,它们组成了英语中 99% 的文本。
8'03,5 $25 TODAY. —— 因为打印机接收到第二行时,它记得的最后一个转义码是"数字转义",于是把开头几个编码当成数字解释了。⚑ 这是全书对"隐式状态"最狠的一次示范:故障不在数据里,不在解析器里,而在两次输入之间被保留下来的那个模式。Petzold 的结论是:"这种问题产生的根源就是采用了转义码,这的确很让人头痛。"
I am 12 years old. 里的 "12" 被编码成 31h 32h(字符 1 和 2 的 ASCII),不是 01h 02h、不是 BCD 的 12h、也不是 0Ch——"因为这些编码在 ASCII 码中表示其他的意思"。(第 13 章那句"比特本身没有含义"的又一次实证。)ANI A,DFh(DFh = 11011111,把左起第 3 位置 0,其余位保留)。这是"编码设计好,代码就短"的最直接证据。65 08 60 = e、回退、重音 → è;掉头(CR,0Dh)与换行(LF,0Ah)是控制符里最重要的两个。⚑ 这一串数字本身就是一种能力:把抽象的存储单位换算成"你读过的东西",你对成本的直觉才立得住。
#、通用货币符号可替换 $——但"如果在文本文件中使用了这些重定义的符号,相关人员都必须知道这些变化"。"对于 Unicode 来讲,它唯一的问题,就是它改变了字符与存储空间之间'单字符,单字节'的等价对应关系。" ASCII 存的《愤怒的葡萄》约 1 MB,Unicode 存约 2 MB。为了使编码系统兼容,Unicode 在存储空间上付出了相应的代价。 Petzold 也很清醒:ASCII 和无数有小缺陷的扩展 ASCII 已经在计算机领域根深蒂固,想一下子取代它们并不是轻而易举的。
⚑ 请给这条结论标一个时间戳:本书写于 2000 年。 这个判断在当时完全正确,但它是全书最需要被今天的读者打折扣的一条(详见「批判性审视」)。
"几乎所有连接在总线上的器件都使用由总线传递而来的数据输入信号。但不管何时,连接在总线上的电路板中只有一个能确定总线数据输入信号的类型,其他电路板处于三种状态中的无效状态。" ⚑ 这是"共享通道 + 互斥写权"的硬件原型——多写者冲突不是靠协商解决的,是靠让所有非当前写者物理上断开解决的。
⚑ 此时——也只有在此时——Petzold 才允许自己用那个比喻:随机访问存储器就像办公桌桌面,上面的东西可以直接使用;磁介质存储器就像文件柜,要用得站起来走过去找出来带回桌面;桌面太挤还得先把暂时不用的放回柜子。他的评价是:"这个比喻恰到好处。" 对照作者序里他明确表示不满意的那个同样的比喻——差别不在比喻,在你有没有先把机制讲透。
"这里我们漏掉的就是软件。"
"不言而喻,这是一个耗时耗力的过程……看看你的手指,或许已经磨出了水泡,你的大脑也感觉一片混乱,而这一切都是工作的代价。"
NibbleToAscii),再调用两次把一个字节转成两个 ASCII 字符。想显示十进制还得再做一轮除法。⚑ 这一小段是"表示层与值层永远不是一回事"的最短证明。
W 1020 35 4F ... 从 1020h 开始把这些字节写进内存(要把 ASCII 转回字节,正是上面那个转换的逆过程)D 1030 显示从 1030h 开始的 11 个字节(11 是算出来的:40 字符一行,扣掉命令和地址就只剩这么多)R 1000 —— 把 1000h 装进 HL,然后执行 PCHL(把 HL 装进程序计数器),程序就从那里跑起来"键盘处理程序及命令处理程序简化了很多工作,可以说是计算机发展的一个里程碑。一旦使用了它,就无须理会那个令人难以忍受的控制面板了。" 而且更重要的是:计算机变得可交互(interactive)了——你输入什么,它立刻响应并显示出来。
"出于这个原因,文件系统(file system)应运而生。"
MYLETTER.TXT、CALC.COM);第 12 字节是扩展序号;第 15 字节记录最后一个分配块用到了几个 128 字节扇区;第 16–31 字节是磁盘存储表,列出这个文件占用了哪些分配块(例如前 4 项是 14h、15h、07h、23h 就表示占 4 块 = 4 KB,而且这四块不必连续)。? 和 * 通配)、ERA(删除,"一旦删除文件,此文件的目录项及其所占用的磁盘空间都将被释放")、REN(改名)、TYPE(显示文本文件内容)、SAVE。CALL 5。CALL 5 是怎么工作的:CP/M 在内存地址 0005h 处放了一条 JMP 指令,跳到 BDOS 里的某个位置。 BDOS 维护文件系统,需要碰硬件时就去调 BIOS 的子程序;BIOS 是 CP/M 中唯一需要了解具体硬件的部分。 CCP 自己的全部功能也是靠调 BDOS 实现的。"程序使用 CP/M 提供的功能便可完成对键盘、视频显示器和磁盘驱动器操作,简言之,API 屏蔽了硬件之间的差异。" 于是同一个 CP/M 程序可以在硬件差别很大的不同机器上跑(前提是处理器能执行 8080 指令)。"如果没有标准的 API,程序必须根据不同型号的计算机进行相应的修改后,才能访问硬件。"
CALL 5 换成利用 8086 的软件中断功能:INT 21h —— "它与子程序调用很类似,只不过程序不必知道它所调用的子程序的确切地址"。"也许正是因为 MS-DOS 系统卓越的'反传统性',使得大多数程序员都很热衷于它,它可以让程序员编写的程序最大限度地达到硬件的最快速度。" 代价是:流行软件因此依赖 IBM PC 这个硬件平台;其他厂商为了竞争不得不沿袭这些硬件特点,否则流行软件跑不起来;于是"与 IBM PC 百分之百兼容"成了软件盒子上的显著字样。 ⚑ 一条冷酷的产业规律:当"绕过抽象层"能换来足以决定胜负的性能时,抽象层就会被击穿,而被击穿处的具体实现细节会反过来固化成事实标准。
00010100 00110010 01010001 00100000 00100101(十六进制看更直观,最左半字节的 1 就是负号)。101.1101 = 5.8125。二进制科学计数法的规范化式要求有效数 ≥1 且 <10₂(即十进制的 2),于是——"这个规则暗示了这样一个有趣的现象:在规范化二进制浮点数中,小数点的左边通常只有一个 1,除此之外没有其他数字。" 既然它永远是 1,就没必要存它。 这就是 IEEE 标准里"隐含位"的由来。
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),用来表示未知的数或非法操作的结果。⚑ 全书唯一一处明说"没有一套完整的解决方案"的地方。抽象层是有漏洞的,而这个漏洞是由物理(有限位数)而非疏忽造成的。
"当然,使用计算机的目的就是帮助人们更加方便地解决问题,而编写程序来进行浮点数运算这一繁杂工作似乎和这个目的背道而驰。但这正是软件的优势所在:一旦某个人为特定的计算机编写了浮点数运算的程序,那么其他的人都可以使用它。" ——因此在计算机发展的早期,为新机器做的第一项工作就是为它编写浮点数运算程序。
⚑ 这是一部关于"能力该不该做成可选项"的完整案例:可选 → 生态分裂 → 开发者被迫双份工作 → 最终被吸收进标准。任何"高级能力作为付费/可选插件"的产品决策,都会把成本转嫁给上层开发者,而历史的终局通常是内置。
"使用机器码编写程序就如同用牙签吃东西:伸出手臂使出较大的力气刺向食物,但每次都只获取到小小的一块,这个过程是辛苦且漫长的。" 每个机器码字节所完成的工作,都是"你能想象到的最微小且最简单的工作"。
PROGRAM1.ASM 文本文件,用 ASM PROGRAM1.ASM 汇编,得到 PROGRAM1.COM。示例程序只有 7 行,但每一行都被解释:$ 为止("可以看到,在程序的结尾处使用了美元符号作为文本的结束标志,这种方式看起来很奇怪,但 CP/M 就是采取的这种方式")Hello!$ 的 ASCII)Text 的地址是 0109h——"一般来说,程序员在使用汇编器时有很多方便之处,其中一点就是不需要关心汇编程序各部分在内存中的存放地址。"
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 章提到的乔治·布尔")。"如果让 100 个人来编写输出素数的程序,你可能会得到 100 个不同的解决方法。就算所有的程序员都使用'爱拉托逊斯筛法',其最后所写的程序也不一定与本文所写程序完全相同。如果说程序设计是一门科学,那么就不应该出现如此多的解法,而不正确的方法将会非常明显。" 他引费曼:"从某种程度上看计算机科学像是一种工程,它的工作范畴是利用一些事物去实现其他事物";也提到克努斯的《计算机编程艺术》。他自己的结论很克制:偶尔会有极富创造性的火花或洞若观火的觉察力,这是"艺术";但更多的时候,程序设计是设计和建造,就像修建一座大桥。
⚑ 全书最贵的一个"局部最优":一个为了省几列打孔卡而做的、当时完全合理的编码决策,代价在四十年后结算。
{} 取代 begin/end;i = i + 5 可简写为 i += 5;只加 1 时写 i++(在 16 位或 32 位微处理器中,i++ 仅需一条机器码指令)。最关键的一点:很多高级语言不支持移位和按位布尔运算,"C 语言打破了这种局限,它广泛地支持这类运算",加上对指针(本质是数字化描述的内存地址)的支持,"因此 C 也被称为高级汇编语言"。"但是,所有的类 ALGOL 语言——即大多数常用程序设计语言——其设计模式都是基于冯·诺依曼计算机体系的。设计一种非冯·诺依曼体系的程序设计语言并非易事,而让人们接受并使用这种语言则更加困难。"
"人与计算机是两种完全不同形式的'客观存在',只可惜在人机交互的这个过程中,与其让计算机去适应人类的特性,远不如劝服人们进行调整以适应计算机的特性来得容易。"
Esc [ 2 J 清屏并把光标移到左上角,Esc [ 5 ; 2 9 H 把光标移到第 5 行第 29 列——"这种定义在电传打字机上是不可能出现的")→ 哑终端(dumb terminal):"相对于电传打字机速度要更快,从某种程度来讲也更灵活,但从速度的提高程度上来讲,并不足以引领用户界面的革新"。"真正的革新出现在 20 世纪 70 年代小型计算机中——它配备了'视频显示存储器',并作为微处理器地址空间的组成部分。"
"令我们惊讶与无奈的是,VisiCalc 这款应用程序无法在大型机上运行。因为像 VisiCalc 这类程序需要以较快的速度不断更新屏幕,所以,它们直接将数据写入 Apple II 视频显示器所配备的 RAM 中……大型时分计算机以及哑终端之间的接口速度过慢,以至于电子报表程序无法使用。" ⚑ 这是全书关于"延迟决定产品形态"的最强论据:不是大型机不够强,是那条链路上的延迟让某一类交互根本不可能存在。
"理解它的关键在于光笔并不发射光——它所做的是检测光。" 电子枪移动控制电路有两个功能:光笔一旦感知到电子枪射出的光,系统立即做出反应;并在反应过程中确定光笔当时指向的屏幕位置。
显示文本只要几个简单 API;显示图像"从理论上来讲,一个 API 函数完全可以胜任这项任务,函数的功能就是设置某个水平和垂直坐标下的像素的颜色。但在实际应用中,这种方法效率较低以至于严重影响到了图像显示的速度。" 所以操作系统必须提供一整套图形编程系统:画线(实线/虚线/点线)、画矩形、画椭圆(含不同填充模式)、画文本(不同字体、大小、加粗、下画线),并负责把这些图形对象转成显示器上的点阵集合。
"从传统意义上来讲,高级程序设计语言会自然而然地区分出代码和数据……这种区分毫无疑问来自于冯·诺依曼计算机的体系结构。在这样一种体系结构中,只有两种元素,一种是机器码,一种是机器码所操作的数据。" "在面向对象的程序设计中,和冯·诺依曼计算机的体系结构所不同的是,对象(object)实际上是代码和数据的组合。在对象内部,与其相关联的代码决定了数据存在的意义,要理解数据的存储方式首先需要理解代码。" 对象之间靠发送/接收消息(message)通信。
++)——最开始它是一种转换程序,把程序转换成 C 程序("但是转换出的 C 程序即难看又难以理解"),转换后再正常编译。{、} 和 \ 封装格式信息;PostScript(Adobe 创始人之一约翰·沃诺克设计)把这个概念发挥到极致,是一种通用的图形编程语言。⚑ 这条不对称是全书的一个隐藏主题:从结构化到栅格化是投影(容易且有损),从栅格化回结构化是推断(困难且不保真)。 它同时是 OCR、语音识别、以及今天一切"从非结构化数据里恢复结构"的任务的形状。
"这中间涉及的问题已经大大超出了 ASCII 码文本转换的范畴,从本质上来讲是一个利用编程技术使得计算机'理解'人类语言的过程。这类问题正是人工智能所研究的领域。"
"就文件的相对大小而言,如果把一个波形文件比作位图文件,则 MIDI 文件就好像矢量图元文件。" 缺点也同源:合成器是异构的,同一个 MIDI 文件可能在一个合成器上完美演奏,在另一个上不堪入耳。
"我们的存储设备有一点与万·布什所提出的概念有所不同,那就是所有信息并不一定要触手可及,真正使它们达到信息共享的做法是计算机互连,这样做还可以更有效地利用存储空间。"
modem = 调制 + 解调。早期用频移键控(FSK):300 bps 时 0 调到 1070 Hz、1 调到 1270 Hz;每个字节被夹在一个起始位和一个停止位中间,所以实际占 10 位 → 300 bps 下每秒只传 30 个字节。("现在许多采用先进技术的新一代调制解调器速度超过了它的 100 倍。")它以串口(serial)方式工作——比特一个接一个传;而打印机常用并口,整个字节由 8 根线并行传输。要回答这个问题,首先要明确:我们的计算机是什么样的? 如果是 Macintosh,这个可执行程序需要包含 PowerPC 机器码并引用 Mac OS 的 API;如果是 PC 兼容机,需要 Intel 奔腾机器码并使用 Windows API。"但计算机及图形操作系统的种类繁杂……进一步来说,我们也不想毫无目的地下载可执行文件,它们很可能来自于非信任站点而且会带有恶意行为。" 答案是 Java(Sun 开发,成熟的面向对象语言,与 C++ 有些类似,请勿与 JavaScript 混淆):它需要经过编译,但编译的结果不是机器码,而是 Java 字节码——与机器码在结构上很相似,但在一台虚拟的计算机即 Java 虚拟机(JVM)上被解释执行。 因此 Java 程序不受限于机器与图形操作系统的类型,具有平台无关性。 ⚑ 这一段是全书抽象层思想的最后一次、也是最高一次运用:当下面那一层(硬件 + OS)无法统一时,就在它上面再造一层虚拟的、统一的机器。 第 17 章造了一台真机器,第 25 章造了一台假机器——而"假的那台"反而更通用。
"展望未来,在以后的家庭和办公室中,光子似乎要替代电子承担海量信息传输的重任。比起莫尔斯码中的一'点'一'划',比起为了与一街之隔的好友深夜交流而绞尽脑汁想出的闪光灯,光子的速度无与伦比。" 500 多页、25 章、从两个孩子的手电筒开始,到光纤里的光子结束——全书只讲了一件事:人想说话,于是造出了所有这些东西。
第一组 · 编码与信息
binary digit 的缩写,约 1948 年由 John Wilder Tukey 造词(他考虑过 bigit、binit)。1 比特是可能存在的最小信息量;信息 = 多个可能性中的一种。ANI A,DFh。它的病根在名字里:太美国化了。第二组 · 从电到逻辑
+ 是并集、× 是交集、1 是全集、0 是空集。区别于传统代数的标志式是 X² = X。第三组 · 算术与表示
第四组 · 记忆与自动
第五组 · 系统与抽象
CALL 5,MS-DOS 用软件中断 INT 21h。ende 拼错编译器立刻抓到;fist 拼错编译器永远抓不到。哪些经久不衰(且对你直接可用)
哪些已被时代修正 / 需要打折扣
与其他书的对话(都在你的书库里)
bit 的词源。说明:为尊重版权,本节以压缩转述为主,只保留极短的原话片段,并标注章次。
来源:30_sources/notion/epic-tracker/04_书架/编码-隐匿在计算机软硬件背后的语言.md(原存于 Notion「Epic Tracker」知识 OS 的书架里,含一个内嵌数据库《编码扩展阅读》)。
_shared/entities、agent 契约、PRD 模板上:一个新 agent(或新同事)第一次读到它,能不能不问任何问题就正确使用? 这比"写得全不全"是更硬的验收标准。