2018年初,Spectre和Meltdown两个处理器漏洞让安全圈忙了大半年。同一年,ACM Queue上一篇题为《C Is Not a Low-level Language》的文章,借这两个漏洞把矛头指向了程序员对C语言的一个老误解:C并不像大家以为的那样贴近硬件。
文章的判断很直接:C的串行内存模型写于PDP-11时代,跟今天处理器依赖推测执行、多级缓存的实际运行方式已经脱节。程序员能用指针精确控制内存,不代表能准确预测这段代码在芯片里到底怎么跑——这既是一笔性能账,也是一笔安全账。
C不是PDP-11的快速版,只是看起来像
在PDP-11上,C的抽象几乎和硬件一一对应:程序顺序执行,内存是扁平地址空间,一条表达式对应一两条机器指令。这也是C当年被叫作"低级语言"的真正原因。
现代高端Intel核心完全不是这么回事。文章提到的一个背景数字是,这类核心里最多同时有约180条指令在处理中,而C的抽象机仍然假设一条指令执行完才轮到下一条。C代码里平均每7条指令出现一次分支,处理器要填满这条流水线,得连续猜中未来25个分支的走向——这两个数字来自文章讨论的特定处理器和代码集,不代表所有芯片和所有程序都是这个比例。
猜错的后果不只是浪费一次运算。被丢弃的推测执行结果会在缓存里留下可观测的痕迹,Spectre正是靠这条路径读取本不该读到的内存内容。
C语言的语法没变,但它早就不是自己声称的那门语言了
性能不是C语言给的,是编译器和芯片一起补出来的
原文提到一个容易被忽略的数字:Clang编译器加上相关LLVM代码约200万行,光是让C代码跑得快的分析和优化部分,就占了将近20万行。C的高性能从来不是语言本身自带的,而是海量编译器工程堆出来的结果。
不同路线的对比能看得更清楚:
| 路线 | 拿性能靠什么 | 程序员要付出什么 |
|---|---|---|
| PDP-11时代的C | 抽象直接对应机器指令 | 几乎不需要额外心智负担 |
| 现代C(x86/ARM) | 编译器优化 + 处理器推测执行补齐串行语义 | 得懂编译器版本和目标CPU的具体行为 |
| GPU | 显式线程级并行 | 得重写并行逻辑,但性能相对可预测 |
| Fortran | 语言层面别名信息比C更充分,编译器少猜一步 | 语法不如C灵活 |
C标准留下的模糊地带,也在把风险转嫁给编译器。结构体填充、未定义行为、指针转成整数再转回指针是否保留"来源信息"(provenance),标准都没给死答案。GCC和Clang对同一段代码的处理方式可能不同,这类分歧已经对应过真实的安全漏洞案例,不是纯理论问题。
这笔账砸在谁头上,接下来看什么
对写C/C++的系统和性能开发者,这篇文章的实际提醒是:别再拿"贴近硬件"当性能直觉的挡箭牌。restrict、内存对齐、分支预测友好的写法,效果取决于具体编译器版本和目标CPU,换一个版本可能就失效。团队如果在做新的高性能系统,Rust这类对指针别名有更明确规则的语言值得放进候选名单——不是因为它更"低级",而是因为它把一部分本来要编译器去猜的信息,交回给了程序员显式声明。
对编译器作者和处理器安全工程师,这篇文章指出的错位没有过期。Spectre之后的缓解手段——retpoline、LFENCE插入、微码更新——本质都是在给这层错位打补丁,而不是消除它。补丁通常带来性能损耗,这个取舍会随着处理器越来越复杂继续存在,具体某次微码或编译器更新对目标负载的实测影响,比套用一个通用结论更值得跟。
这篇文章发表于2018年,"180条在途指令""200万行代码"都是当时的估算背景,不是通用换算比例。但它指出的结构性问题——C的抽象和硬件实现之间存在错位——到今天没有过时。接下来值得看的,是C标准委员会怎么正式定义指针provenance,以及Rust一类语言在系统编程里的份额会不会因此继续往上走——目前这两件事都还没有定论。
