花括号:Unix和C语言的演变

Hacker News Top 新闻

摘要

详细探讨了早期Unix系统在Teletype Model 33上如何输入花括号,涵盖了ASCII 1963、三字符组、双字符组以及终端驱动程序转换。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/05/24 18:41

# 花括号:UNIX与C语言的演变 来源:https://thalia.dev/blog/unix-braces/ ## Thalia Archibald 的博客 (https://thalia.dev/) 2026年5月19日 在UNIX上,人们是如何使用Teletype Model 33输入花括号 `\{` 和 `\}` 的?这些字符对C语言尤为重要,但该终端上却没有。 我刚刚被问到一个类似的问题1 (https://thalia.dev/blog/unix-braces/#fn:question),作为回应,本文从这一视角出发,回顾UNIX与C语言的共同演变,并以各个时代的“hello, world”为例。 本作品完全由我本人创作(未使用AI),代码示例均为我自行构建。所有推论的来源均已注明。 ## ASCII 1963 Teletype Model 33 以不能输入小写字母而闻名。这款电传打字机是根据第一版ASCII标准(ASA X3.4-1963)设计的,当时该标准尚未决定添加小写字母。标准委员会中的一些人认为,使用有限的编码空间容纳更多控制字符会更好。该标准很快演变为现代形式,但Model 33是ASCII的第一个商业应用,且广受欢迎,因此其问题得以延续。除了缺少小写字母,ASCII 1963 和 Model 33 还缺少 `\{` `\}` 花括号、`|` 竖线、`\`` 反引号和 `~` 波浪号;它们用 `↑` 上箭头代替 `^` 脱字符,用 `←` 左箭头代替 `_` 下划线。 ## 三字符组与双字符组 花括号是C语法的重要组成部分,用于代码块。例如: ```c int main(int argc, char *argv[]) { printf("hello, world!\n"); } ``` 为了支持没有这些字符的字符集,C89 发明了三字符组,因此 `{` 可以写成 `??<`,`}` 写成 `??>`: ```c int main(int argc, char *argv[]) ??< printf("hello, world!\n"); ??> ``` 反斜杠 `\\` 的三字符组 `??/` 可用于行尾,产生行连接,这在通用字符名称内部会导致词法未定义行为。我在编写静态分析时遇到了这种情况,但后来在C++26中修复了2 (https://thalia.dev/blog/unix-braces/#fn:lexub)。 然后,C95 引入了外观更友好的双字符组,因此 `{` 可以写成 `<%`,`}` 写成 `%>`: ```c int main(int argc, char *argv[]) <% printf("hello, world!\n"); %> ``` 但三字符组是在Teletype Model 33被淘汰之后才引入的。那么,在70年代初期,人们是如何编写C代码的呢? ## 终端驱动程序 从1973年11月的UNIX V4开始,电传打字机驱动程序会在 `\(` 和 `{` 之间、`\)` 和 `}` 之间进行转换: ```c main(argc, argv) char *argv[]; \( printf("hello, world!\n"); \) ``` 这一支持是在1973年8月的nsys内核3 (https://thalia.dev/blog/unix-braces/#fn:nsyscanon) 与1973年11月的V4手册4 (https://thalia.dev/blog/unix-braces/#fn:v4dc) 之间添加的。 Utah_v4内核(1974年6月)5 (https://thalia.dev/blog/unix-braces/#fn:v4canon) 和 Dennis_v5内核(1974年11月)6 (https://thalia.dev/blog/unix-braces/#fn:v5canon) 都支持这一功能,但nsys(V4的预发布版本,尚未重新加入管道)却没有。 V2和V3内核是用汇编编写的,未能保存下来,但nsys内核与V3手册7 (https://thalia.dev/blog/unix-braces/#fn:v3dc) 和 V1内核8 (https://thalia.dev/blog/unix-braces/#fn:v1canon) 相匹配。 UNIX通过公共字节流接口暴露设备,这种字符转换对用户空间程序是透明的。程序使用ASCII `{` `}` 的字节,内核在向Teletype Model 33写入时将其转换为 `\(` `\)`,或在读取时反向转换。 这种转义源于删除终端发送字符的需求,因为电传打字机无法擦除已经打印在纸上的文本。他们采用从Multics继承的方案:按行处理输入,将 `#` “擦除” 解释为删除前一个字符,将 `@` “终止” 解释为清空当前行。这两个字符都可以通过反斜杠转义以获得字面字符。例如,以下Utah_v4会话使用Teletype Model 33编写该程序,并使用 @ 和 # 修正了几个错误: ``` % ed hello.c ? a main(argc, argv) char *argv[]; \( printf("hallo, welt@ printf("hello #, world!\n"); \) . w 63 q % cc hello.c % a.out hello, world! ``` 如果你使用其他终端登录,你会看到: ``` % cat hello.c main(argc, argv) char *argv[]; { printf("hello, world!\n"); } ``` 那么在UNIX V4之前呢?从1972年6月C语言诞生之初9 (https://thalia.dev/blog/unix-braces/#fn:cstart),它就只使用花括号。你只需要使用一个能产生花括号的终端即可。 ## 早期C语言的结构体 有趣的是,1972年12月首次在C语言中添加结构体时10 (https://thalia.dev/blog/unix-braces/#fn:prestructparen),使用的是圆括号而不是花括号!在1973年8月nsys前后的一段时间里,你甚至可以用圆括号或花括号来编写结构体。最晚到1974年6月11 (https://thalia.dev/blog/unix-braces/#fn:v4paren),它才完全切换为仅使用现代语法。以下nsys中的定义同时使用了两种风格12 (https://thalia.dev/blog/unix-braces/#fn:nsysparen): ```c struct user { int u_rsav[2]; /* 必须是第一个 */ /* ... */ struct ( int u_ino; char u_name[DIRSIZ]; ) u_dent; /* ... */ } u; /* u = 140000 */ ``` 但这只是语言的一个特性;代码块仍然需要花括号。 ## B语言 在C语言之前是B语言,一种由Ken Thompson为UNIX编写的解释型语言。B语言没有类型——每个值都是一个机器字——这对于UNIX起步的PDP-7(具有18位字长)来说非常完美。这种早期B语言的后代一直用于Honeywell 6070,远在UNIX的B语言被C语言取代之后。该机器具有36位字长,因此一个字可以容纳四个字符。1973年针对H6070的B语言教程13 (https://thalia.dev/blog/unix-braces/#fn:bintro) 包含了有史以来第一个“hello, world”程序,同样使用了花括号: ```c main( ) { extrn a, b, c; putchar(a); putchar(b); putchar(c); putchar('!*n'); } a 'hell'; b 'o, w'; c 'orld'; ``` ## 从B到NB 但这种“万物皆字”的策略在PDP-11上失败了,UNIX很快就过渡到了该机器。PDP-11具有16位字长和8位寻址能力。由于地址可能不对齐,PDP-11上的B语言需要一个权宜之计:链接器未对齐的全局变量会在运行时被修补14 (https://thalia.dev/blog/unix-braces/#fn:bsquoze)。因此,大约在1972年5月9 (https://thalia.dev/blog/unix-braces/#fn:cstart),Dennis Ritchie向语言中添加了 `char` 和 `[]` 指针类型,并将其命名为“new B”。注意只使用了 `[]`,而不是后来的 `*`: ```c main(argc, argv) char argv[][]; { printf("hello, world!\n"); } ``` ## 从NB到C 随后,他将其转变为生成机器代码的编译器(而不是B语言低效的线程代码),并将其重命名为C。语法保持不变: ```c main(argc, argv) char argv[][]; { printf("hello, world!\n"); } ``` 然而,1972年6月的第一个C编译器已经去掉了B语言中花括号的 `$(` `$)` 转义,并且从未恢复15 (https://thalia.dev/blog/unix-braces/#fn:cbrace)。但它仍保留了B语言的许多语义。函数、数组甚至标签都通过可写指针间接引用16 (https://thalia.dev/blog/unix-braces/#fn:chist),导致了一些怪癖,例如可重新赋值的标签17 (https://thalia.dev/blog/unix-braces/#fn:clvalue)18 (https://thalia.dev/blog/unix-braces/#fn:cgdlvalue): ```c goto init; init: ouptr = oubuf; init = init1; init1: ``` 这种间接引用在添加结构体时被移除,从而产生了指针和数组之间的区别。指针可重新赋值,而数组则不可。因此,最晚在1973年8月19 (https://thalia.dev/blog/unix-braces/#fn:nsysptr),引入了 `*`: ```c main(argc, argv) char *argv[]; { printf("hello, world!\n"); } ``` 有了编译器和结构体,C语言变得足够快和表达力强,以至于可以用C语言重写内核,最终发布了UNIX V4。 ## PDP-11 B 从C语言回溯到B语言,我们终于可以再次使用Teletype Model 33了!PDP-11上的B语言支持花括号,此外还有以下转义20 (https://thalia.dev/blog/unix-braces/#fn:bref): - `*0`: NUL - `*e`: 文件结束符 - `*\(`: `{` - `*\)`: `}` - `*t`: 制表符 - `**`: `*` - `*'`: `'` - `*"`: `"` - `*n`: 换行 UNIX于1971年2月移植到PDP-1121 (https://thalia.dev/blog/unix-braces/#fn:v0date)。在此期间到1972年1月的B语言参考手册20 (https://thalia.dev/blog/unix-braces/#fn:bref) 之间,发明了花括号 `{` `}` 的使用,使其与前辈区分开来。在1971年中的手册草案22 (https://thalia.dev/blog/unix-braces/#fn:bv0) 中,他们显然在使用Teletype Model 37,一款支持花括号的新式电传打字机23 (https://thalia.dev/blog/unix-braces/#fn:termsv0)。如果PDP-11 B并非从一开始就支持 `{` `}`,那么它们很快就被添加了。 运行时库类似于后来的C语言: ```c main() $( printf("hello, world!*n"); $) ``` 不幸的是,这个时代没有B语言源代码保存下来。然而,1972年6月的已编译PDP-11 B运行时库存活了下来24 (https://thalia.dev/blog/unix-braces/#fn:blib) 并被反汇编,一个能生成此输出的编译器已经被重建14 (https://thalia.dev/blog/unix-braces/#fn:bsquoze)。 ## PDP-7 B 而在PDP-11之前,PDP-7 B也支持使用 `$(` 和 `$)` 代替(或额外支持)花括号。但它使用双字符打印来适应18位字长: ```c main() $( write('he'); write('ll'); write('o,'); write(' w'); write('or'); write('ld'); write(041012); $) ``` UNIX的PDP-7时代只保存了两个B语言程序,均显示了这种语法25 (https://thalia.dev/blog/unix-braces/#fn:bprograms): ```c main $( auto ch; extrn read, write; goto loop; while (ch != 04) $( if (ch > 0100 & ch < 0133) ch = ch + 040; if (ch==015) goto loop; if (ch==014) goto loop; if (ch==011) $( ch = 040040; write(040040); write(040040); $) write(ch); loop: ch = read()&0177; $) $) ``` 这种语法直接借用于其前身BCPL。 ## 从BCPL到B B语言是Ken Thompson对BCPL的简化版本,正如他经常做的那样,将其精简到核心。名称本身也是BCPL或Bon(他Multics时期创建的一种无关语言)的缩写16 (https://thalia.dev/blog/unix-braces/#fn:chist)。以下示例采用1967年BCPL手册的风格,反映了B语言从BCPL分支出来时该语言的状态26 (https://thalia.dev/blog/unix-braces/#fn:bcpl67): ```c let Start() be $( Writech(MONITOR,'h'); Writech(MONITOR,'e'); Writech(MONITOR,'l') Writech(MONITOR,'l'); Writech(MONITOR,'o'); Writech(MONITOR,',') Writech(MONITOR,' '); Writech(MONITOR,'w'); Writech(MONITOR,'o') Writech(MONITOR,'r'); Writech(MONITOR,'l'); Writech(MONITOR,'d') Writech(MONITOR,'!'); Writech(MONITOR,'*n') $) ``` 尽管该手册混合使用了大小写字母和丰富符号,但规范风格是大写27 (https://thalia.dev/blog/unix-braces/#fn:bcpldmr)。1967年的手册未指定入口点,因此我从1979年BCPL书中的 `START` 改编而来28 (https://thalia.dev/blog/unix-braces/#fn:bcpl79)。BCPL后来才添加了 `{` 和 `}` 用于代码块,这是模仿C语言的结果27 (https://thalia.dev/blog/unix-braces/#fn:bcpldmr)。 ## Teletype Model 37 甚至在UNIX V4扩展终端驱动程序为Teletype Model 33替换 `\(` 和 `\)` 之前,UNIX程序员就已经停止使用 `$(` 和 `$)` 编写B代码。他们从Model 33升级到了其后续型号——Teletype Model 37。 Model 37速度提高了50%,并支持完整的现代ASCII字符集。他们不再受限于ASCII 1963子集。这是有史以来最先进的机电式电传打字机,即完全依靠机械运行,没有数字逻辑,但很快就被视频终端所淘汰。它有许多转义序列:黑色和红色、半前进和半后退换行(可用于上下标)、反向换行、水平和垂直制表设置,以及半双工和全双工29 (https://thalia.dev/blog/unix-braces/#fn:37notes)30 (https://thalia.dev/blog/unix-braces/#fn:37type)。 去年,Brian Kernighan在UNIX小组中讲述了一个关于这些功能之一的幽默用法:Robert Morris Sr. 给 Joe Ossanna 发了一封邮件,其中包含一百个反向换行,导致长折叠纸从Model 37的后方被吸出并掉在地上31 (https://thalia.dev/blog/unix-braces/#fn:37vcf)。 ## UNIX上的终端 UNIX很早就添加了对Teletype Model 37的支持,并迅速成为首选。PDP-7 UNIX仅支持Model 3332 (https://thalia.dev/blog/unix-braces/#fn:termspdp7)。但在UNIX V1手册定稿之前,一份1971年中的手册草案就暗示许多UNIX用户已经在使用Teletype Model 3723 (https://thalia.dev/blog/unix-braces/#fn:termsv0)。V1内核支持Model 3733 (https://thalia.dev/blog/unix-braces/#fn:termsv1)。从最晚V2到V5的 `login` 会循环切换速度和不同终端的登录信息,支持TermiNet 300和Teletype Model 3734 (https://thalia.dev/blog/unix-braces/#fn:termsv245)。在V6中,随着 `getty` 用C语言重写,它支持了更多终端,并在V7中进一步扩展,但仍支持Model 3735 (https://thalia.dev/blog/unix-braces/#fn:termsv67)。汇编内核的任何版本(即使V1)在其源代码中都没有使用花括号,因为开发工作已经转向了Model 37。 ## 现代影响 Teletype Model 33的字符集限制对现代计算产生了持久影响。UNIX几乎只使用小写字母。这至今仍是Ken的写作风格36 (https://thalia.dev/blog/unix-braces/#fn:kenlower)。PDP-7 UNIX源代码中没有一个下划线(在Model 33上它会是 `←`)。后续版本也很少使用。libc的核心仍使用平大小写命名风格。C语言中的标识符非常短,以适应7或8字节的限制。许多这样的函数至今仍在libc中。不过,这是汇编器的问题——这个话题值得另写一篇文章,等我完成自己的汇编器后再写。 1963年的设计决策至今仍在影响我们,63年后! --- *我收集电传打字机,正在寻找一台Teletype Model 3737 (https://thalia.dev/blog/unix-braces/#fn:mytty37)。如果你有相关线索,请与我联系!另外,我希望最终也能拥有一台PDP-11。* ## 附录:hello, world 所有“hello, world”片段汇总: ```c // BCPL, 约1967年 let Start() be $( Writech(MONITOR,'h'); Writech(MONITOR,'e'); Writech(MONITOR,'l') Writech(MONITOR,'l'); Writech(MONITOR,'o'); Writech(MONITOR,',') Writech(MONITOR,' '); Writech(MONITOR,'w'); Writech(MONITOR,'o') Writech(MONITOR,'r'); Writech(MONITOR,'l'); Writech(MONITOR,'d') Writech(MONITOR,'!'); Writech(MONITOR,'*n') $) /* PDP-7 B, 1969年 */ main() $( write('he'); write('ll'); write('o,'); write(' w'); write('or'); write('ld'); write(041012); $) /* PDP-11 B, 1971年 */ main() $( printf("hello, world!*n"); $) /* NB, 1972年5月 */ main(argc, argv) char argv[][]; { printf("hello, world!\n"); } /* 早期C, 1972年6月 */ main(argc, argv) char argv[][]; { printf("hello, world!\n"); } /* C, 最晚1973年8月 */ main(argc, argv) char *argv[]; { printf("hello, world!\n"); } /* C与UNIX V4, 1973年11月 */ main(argc, argv) char *argv[]; \( printf("hello, world!\n"); \) /* C89, 1989年 */ int main(int argc, char *argv[]) ??< printf("hello, world!\n"); ??> /* C95, 1995年 */ int main(int argc, char *argv[]) <% printf("hello, world!\n"); %> // C99, 2000年 int main(int argc, char *argv[]) <% printf("hello, world!\n"); %> ``` ## 参考文献

相似文章

TTY 解密 (2008)

Hacker News Top

详细解释Linux和UNIX中的TTY子系统,涵盖从电传打字机到现代模拟终端的历史,以及线路规程的作用。

Strace-ui、Bonsai_term 与 TUI 复兴

Hacker News Top

Jane Street 的工程师介绍了 strace-ui,这是一个用于 strace 的交互式终端 UI,通过过滤、PID 追踪和手册页集成简化了系统调用调试,并强调了由其 Bonsai 框架推动的 TUI 复兴。

一些有趣的软件趣闻

Hillel Wayne — Computer Things

一系列有趣且鲜为人知的软件趣闻,包括在康威生命游戏中实现的俄罗斯方块、Vim 的图灵完备性以及反斜杠字符的历史。

用C语言搞怪,第&((int*)-8)[3]部分

Lobsters Hottest

一篇幽默的教育性文章,涵盖C语言基础知识,如前向声明、运算符优先级、无条件跳转和基本算术运算,并附带有意搞怪的代码示例。