如果你只会一门“安全”的语言,你可能永远没见过程序真正出事的样子。Low Level频道的《Coding in C Until My Program Is Unsafe》就是这么一条视频:不讲完整个C语言教程,只做一件事——现场写C代码,然后看着它一步步从“能跑”滑向“危险”。这条视频在YouTube拿到了超过351万次播放(据2026年9月系统抓取的频道页面数据),成了近几年C语言与内存安全话题上最出圈的演示之一。它火的原因不猎奇:它把一个所有工程师都听过、但很少人亲眼看着发生的事实,演出来了——让程序变得不安全的,往往不是什么高深攻击,而是几行再“正常”不过的代码。本文带你完整拆解这条视频的论证结构、背后的语言设计与标准演进、真实世界的漏洞史,以及整个软件产业2022年以来关于“内存安全”的转向。你没看过视频也能懂,看过视频能懂更深。
| 项目 | 内容 |
|---|---|
| 英文标题 | Coding in C Until My Program Is Unsafe |
| 中文译名 | 用C写代码,写到程序不安全为止 |
| 频道 | Low Level |
| 发布日期 | 本次素材抓取未含该字段(以频道页面为准) |
| 播放量 | 3,513,465次(截至2026年9月中旬,据系统两次独立抓取) |
| 链接 | youtube.com/watch?v=oTEiQx88B2U |
| 时长与形式 | 单人口头讲解+现场写代码演示(据视频页面) |
这条视频的叙事骨架非常克制:不是攻击教学,而是把C语言最容易被忽视的那条滑坡,当着观众的面走一遍。按叙事顺序,它大致落在四个节点上(据视频公开内容归纳)。
第一站:程序“看起来”是对的。 视频从一个再普通不过的小程序起步——几行C代码,逻辑简单,编译通过,运行正确。这正是大多数人对C的日常印象:只要语法写对、逻辑想清,它就是最听话的语言。视频前几分钟刻意维持这种安全感,因为接下来要拆的就是它。
第二站:越界写入,邻居遭殃。 接下来,代码开始“越界”——向一个数组的合法范围之外写了数据。在Python、Java里,这一步会当场抛异常;而在C里,程序一声不吭地继续跑,只是内存里某处“隔壁”的数据已经被悄悄改掉了。C语言不做数组边界检查,这是它设计之初就定下的取舍:语言信任程序员,把边界判断的成本留给写代码的人。一旦这个判断缺席,写坏的数据就会落到同一片内存里别的变量头上——如果那个变量恰好是一个“是否管理员”“是否验证通过”之类的开关,程序的控制流就被改写了。视频演示的正是这一类手法:不是远程黑客的天顶星技术,而是本地一次朴素到近乎笨拙的写越界。
用一段经典的示意代码说明这个原理(这是教学用的最小示例,并非视频逐行复现):
#include <string.h>
#include <stdio.h>
int main(void) {
int access_granted = 0; /* 安全开关 */
char name[8];
gets(name); /* 危险:不检查长度 */
if (access_granted != 0) {
printf("Welcome, admin.\n");
}
return 0;
}
只要输入超过8个字符,多出来的字节就会落在 name 数组之外的内存上。在典型的栈布局里,紧挨着它的变量可能正是 access_granted——一个“0/1开关”就这样被用户的输入改成了非0,检查形同虚设。(注意这里说的是“典型的栈布局”,个中细节下文批判性审查部分再拆。)
第三站:未定义行为,编译器的“合法背叛”。 视频另一个关键节点指向C标准里最锋利的概念——未定义行为(Undefined Behavior, UB)。C标准对大量情形(越界访问、有符号整数溢出、解引用空指针……)不做任何承诺:标准不管,实现随意。现代编译器为了性能,默认假设UB永远不发生,并据此大幅优化代码。于是出现一个反直觉的现象:一个包含越界访问的程序,编译器可能直接把“出问题的检查”优化掉——不是运行时报错,而是那个 if 判断整个消失。这不是编译器坏了,恰恰是它按标准办事。大学教师John Regehr(犹他大学)多年来持续研究并科普这一机制,他的结论被业界反复引用:UB不是“运行时可能崩”,而是“编译器有权把你的程序改写成另一个程序”。
第四站:本地演示,全局投影。 视频最后把镜头从演示程序拉远到真实世界:这些手法在过去三十多年里,正是无数真实漏洞的原型。演示里那几行“笨拙”的越界代码,放大到产业尺度,就是下面这串历史。
这条视频之所以值得深读,是因为它提到的每一类手法,都有教科书级的出处。
1. C的设计哲学:《C程序设计语言》与“信任程序员”。 C诞生于贝尔实验室,Dennis Ritchie在20世纪70年代初实现,1978年Brian Kernighan与Ritchie合著的《The C Programming Language》(K&R)确立了它的经典形态。C哲学的核心可以概括为:不替程序员做多余的事——没有边界检查、没有垃圾回收、类型系统宽松。这带来了极致的性能和对硬件的直接控制,也让它成为操作系统、数据库、嵌入式领域的地基(Linux内核、SQLite、Python解释器本身都是C写成的)。但“信任程序员”的另一面是:语言把内存安全这道题,原封不动地留给了每一个写代码的人。1989年C被ANSI标准化(C89),此后C99、C11、C17持续演进——值得注意的是,C11曾引入一套带边界检查的标准库接口(Annex K),但几十年过去,主流平台几乎无人采用,这本身就是产业对“靠自觉补安全”投出的不信任票。
2. 未定义行为:C标准正文与优化器研究。 UB不是坊间传说,它写在C标准条文里:标准明确列出“本标准对此行为不施加任何要求”的情形。真正把它推成公共话题的,是编译器优化研究——前文提到的Regehr及其合作者的系列工作系统解释了“为什么开了优化的编译器会让带UB的代码行为诡异”:因为优化器把“程序员保证不发生UB”当作公理,一切看似奇怪的化简都由此而来。
3. 真实漏洞史:从Morris蠕虫到Heartbleed。 1988年的Morris蠕虫是互联网史上第一次大规模安全事件,其传播手段之一就是利用fingerd服务的缓冲区溢出——正是上面那段示意代码的同款原理。2014年的Heartbleed(CVE-2014-0160)则是另一种内存不安全:OpenSSL心跳实现中的越界读取,攻击者每次可以顺手带走64KB的相邻内存内容,密码、私钥、会话cookie尽在其中。一个越界写、一个越界读,相隔26年,原理同源——都是“语言不检查边界”。
4. 产业量化:70%这个数字从哪来。 “内存安全问题占漏洞大头”并非感觉,而是有公开统计:微软MSRC在2019年公开的数据显示,当年微软修复的CVE中约70%属于内存安全类;谷歌Chromium项目随后公布过同类口径——约70%的高危安全漏洞是内存安全问题。两个独立来源、两个独立代码库,给出同一个量级,这是全文学术性最强的交叉证据。
5. 产业转向:从“写好C”到“换语言”。 2022年11月,美国国家安全局(NSA)发布关于软件内存安全的指南,建议开发者优先选择内存安全语言,并点名C#、Go、Java、Ruby、Rust、Swift;2024年2月,白宫国家网络主任办公室(ONCD)发布报告《Back to the Building Blocks》,把内存安全上升为国家级软件供应链议题。产业侧的实证随之而来:谷歌在Android上推进Rust新代码后公开数据称,内存安全漏洞在Android总漏洞中的占比从2019年的约76%降到2022年的约35%。这些公开材料共同构成了视频话题的政策与数据背景。
6. 防御工线的谱系。 即便留在C里,工程师也不是赤手空拳:1998年Cowan等人提出的StackGuard给栈帧装上“金丝雀值”,越界写会先踩坏哨兵从而触发中止,此后栈保护成为主流编译器标配;地址空间布局随机化(ASLR)让攻击者难以猜地址;数据执行保护(DEP/NX)禁止栈上代码执行;模糊测试(fuzzing)则把“替你产生恶意输入”工业化——Google在2016年上线的OSS-Fuzz已为开源项目找出数以万计的真实缺陷。视频演示的脆弱性,正是这条防御工线半个世纪以来试图围堵的对象。
证据充分的部分。 内存安全漏洞占比(微软与谷歌双源互相印证)、UB的优化器机制(C标准条文+学术研究)、Morris蠕虫与Heartbleed的史料、NSA/ONCD的官方文件,这些都是可以独立查证的硬事实。视频选取的演示方向,与这些事实完全对得上号。
演示是简化的,别当成交叉科学。 第一点要打的折扣:示意代码里“越界写坏隔壁的开关变量”依赖典型的栈布局,但C标准并不保证变量在栈上的相对位置——实际编译器完全可能重排局部变量,甚至因为access_granted未被可观察地初始化而产生别的行为。换句话说,这类演示的教学价值在于建立“越界会污染未知内存”的直觉,而不是承诺“写坏的一定是那个变量”。真实的漏洞利用要复杂得多,也因此更脆弱。
“内存安全语言”不是免死金牌。 第二点要打的折扣:换成Rust、Go、Java,消灭的是内存这一类漏洞,逻辑漏洞、并发缺陷、权限设计错误一个都不会少。NSA指南自己也强调,内存安全语言不能替代其他安全实践。把“内存安全”听成“绝对安全”,是把一个精确的工程概念读成了营销词。
“全部重写”是伪命题。 第三点:产业转向不等于C会被抛弃。Linux内核、SQLite、几十年的嵌入式存量代码仍在C的世界里,推倒重写的成本高于渐进治理。SQLite本身就是最好的反证——一个用C写成的数据库,靠着近乎偏执的测试纪律(官方文档《How SQLite Is Tested》详细记载其覆盖率要求、故障注入与多种模糊测试)长期保持极低的缺陷密度。同一门语言,不同的工程纪律,可以是两个世界。
一个值得补充的注脚。 视频没有展开但必须一提的是责任归属问题:内存不安全究竟是“程序员菜”还是“语言设计欠债”?C语言之后,图灵奖得主Tony Hoare在2009年的演讲里把1965年发明的空引用称为“我的十亿美元错误”——语言设计者的一个省事决定,让之后每一代程序员持续付费。把全部锅甩给个体开发者,恰恰是这类事故反复发生的制度原因。
第一条:把“越界”当作默认假设,而不是意外。 写C(以及任何手动管理内存的语言)时,最值钱的习惯转变是:每写一个数组、一次拷贝、一个循环,先问三个问题——长度在哪、索引会不会到边界、字符串有没有终止符。工程落地上,把编译器警告当错误(-Wall -Wextra -Werror),让AddressSanitizer在测试里常开——这些工具的成本几乎为零,却能拦下视频里那一类事故的绝大多数。
第二条:用fuzzer的眼睛审自己的代码。 OSS-Fuzz的思想可以搬到日常:与其假设用户输入是善意的,不如主动让机器生成“恶意”输入去砸自己的接口。你不需要搭建Google规模的基建,一个简单的随机输入循环,往往就能在一小时内找到你代码里那个“从没想过有人会这么传参”的路径。模糊测试把“想象力”外包给了机器,而人的想象力恰恰是安全审查里最稀缺的资源。
第三条:把内存安全当架构约束,而非风格偏好。 2022年之后,"新项目要不要选内存安全语言”已经从品味之争变成了工程决策。做技术选型时,值得把它与性能、生态放在同一个权重下认真评估;而对必须维护的存量C代码,对应的策略是隔离(最小权限、进程沙箱)与渐进替换,而不是指望“大家小心一点”。
flowchart TD
A[C语言设计哲学<br/>信任程序员] --> B[不做边界检查]
B --> C[越界写入<br/>污染隔壁变量]
C --> D[控制流被改写<br/>检查形同虚设]
B --> E[未定义行为<br/>标准不做承诺]
E --> F[优化器假设UB不存在<br/>检查可能被整段删除]
D --> G[真实漏洞史<br/>Morris蠕虫与Heartbleed]
F --> G
G --> H[产业量化<br/>约七成CVE属内存安全]
H --> I[两条出路]
I --> J[新代码用内存安全语言]
I --> K[存量C加固<br/>工具链与测试纪律]
本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
