凌晨三点,报警响了。上线六小时的版本在打嗝:每隔几分钟,进程在退出流程里崩溃一次。回滚,天亮,复盘。
事故的起因是一个新人第一周提交的 PR,改动小得不好意思叫改动。他在 get_timeout_ms 里发现一段”明显多余”的代码:
int get_timeout_ms(void) {
if (g_cfg == NULL) { // 评审记录:"g_cfg 启动时就初始化了,不可能为 NULL"
return DEFAULT_TIMEOUT; // 新人删掉这两行,评审无人反对,合并,上线
}
return g_cfg->timeout_ms;
}
评审三个人看了一遍,都觉得他说得对:g_cfg 在 main 第三行就初始化了,全程没人置空,这个检查在任何一条正常路径上都不成立。删掉它是清理,不是冒险。
三班倒的崩溃说明:存在一条不正常的路径。关停流程里,热更新线程先释放 g_cfg,而退出排干的残留请求在这之后才会查询超时——窗口只有几十毫秒,白天流量小的时候撞不上,凌晨批量任务集中退出,每次都中。
回滚之后,有人对着这行被删掉的代码跑了 git blame(本文演示仓库实测,构造方法见附录 D):
57d40255 (zhangsan 2016-04-19 03:20:00 +0800 13) if (g_cfg == NULL) {
57d40255 (zhangsan 2016-04-19 03:20:00 +0800 14) return DEFAULT_TIMEOUT;
57d40255 (zhangsan 2016-04-19 03:20:00 +0800 15) }
2016 年 4 月 19 日,凌晨三点二十。提交说明写着:fix: 关停竞态期间取默认超时(INC-2016-0417)。
这不是一行多余的代码,这是一座纪念碑。它纪念的是八年前另一个凌晨三点,另一个工程师对着另一个 core dump 熬过的那一夜。碑立在那里,只是没有碑文——评审记录、代码规范、静态检查那天晚上全都正常工作,唯一失效的环节是:没有人记得。
切斯特顿(G. K. Chesterton)在 1929 年写过一条栅栏定律:看到路上立着一道栅栏而不知道它的用途,我绝不会让你拆掉它。等你搞清楚它为什么立在那里,再来告诉我它没用了。这条定律在软件行业的引用率极高,遵守率极低——因为”搞清楚它为什么立在那里”这一步,没有任何工具自动替你完成。它是一门手艺。
这篇文章讲的就是这门手艺:代码考古学。文中的 git 输出全部在 Windows 11 + git 2.49.0 + PowerShell 实测;演示仓库由脚本按历史日期回填生成(GIT_AUTHOR_DATE),十三个提交模拟 2009 到 2023 年的沉积过程,构造脚本见附录 D,所有命令你可以原样复现(commit 哈希会因构造时间不同而不同)。演示代码同样过了编译验证:gateway.c、session.cpp、net_utils.cpp 在 Visual Studio 2026(cl 14.51,/W4 /utf-8 /std:c++17)下编译通过,其中 net_utils.cpp 另经 WSL Ubuntu 24.04 + gcc 13.3(-Wall -Wextra)验证——前两个文件依赖 Win32 API,Linux 侧天然不适用,双平台验证只对可移植层有意义。
一、前提拆除:代码的含义不在代码里
读遗留代码的第一课,是拆掉一个你从未意识到自己在坚持的前提:代码的含义在代码里。
这个前提在新代码上大体成立。你上周写的模块,意图还在你脑子里,约束还在评审记录里,变量名还有体温。读它,确实是读逻辑。
但遗留代码是另一回事。它的作者离职了;当年促成这个设计的约束(”那个客户还在用 IE6″”服务器只有 2G 内存””计费服务一压就垮”)无人记得;那次让所有人长记性的事故,连事故单都随工单系统的两次迁移一起蒸发了。你面对的不再是逻辑,而是一份作者永远缺席的历史文档。
一旦接受这个定位,整套方法论都要跟着换。因为代码和历史的关系是:
每一行代码,都是一连串意图、约束、事故和妥协的终态快照。写作是有损压缩,阅读是解压——而大部分”为什么”在压缩的那一刻就被丢弃了。
或者更狠一点:代码只记录判决,不记录庭审。
这个处境,调试系列的老读者其实见过。《每根栈都是清白的:程序”卡死”的鉴别诊断学》(暂未发布,上线后补链接)里有一种最刁钻的静止叫丢失唤醒:唤醒事件发生在过去,没有登记任何账本,你抓一百次 dump,一百次只能看到”等的姿势”。遗留代码就是源码世界的丢失唤醒:决策事件发生在过去,没有登记,你读多少遍代码,”为什么”那一维的信息量都是零。
所以阅读遗留系统不是”阅读理解”,是取证。你要像考古学家一样,用残存下来的冗余信息——地层、器物、铭文、外部档案——去重建那个没有被记录的信号。接下来的四节,就是考古学的四门手艺在代码库上的投影。
二、地层学:代码不是写的,是沉积的
考古学的第一原理是叠压律:在没有扰动的情况下,上层的遗物比下层的年轻。考古学家挖的从来不是土,是关系——1973 年 Edward Harris 发明的哈里斯矩阵(Harris matrix),把地层之间的叠压与打破关系画成一张有向图,单个地层本身并不含年代,年代存在于地层与地层的关系里。
这个洞察原样适用于代码:单文件无历史,历史在文件与提交的关系里。而一个真实代码库的地层,常常在同一个函数里握手。这是演示仓库里 session.cpp 的现状(实测文件):
DWORD WINAPI SessionThread(LPVOID lpParam) { // 地层 A,2009:匈牙利命名 + Win32 风
char szHost[256]; // 地层 A:固定栈缓冲区
DWORD dwLen;
auto timeout = std::chrono::seconds(30); // 地层 B,2016:C++11 改造波及到这里
dwLen = GetPrivateProfileStringA("server", "host", "", szHost, 256, szIniPath); // 地层 A:ANSI API
if (dwLen == 0) {
strcpy(szHost, "127.0.0.1");
}
std::string_view sv{szHost}; // 地层 C,2023:后人的补丁
printf("connecting %.*s\n", (int)sv.size(), sv.data());
return 0;
}
三个年代,十四行。判读地层的第一件工具是碳测年:git blame 给每一行标出最后一次触碰它的提交。但 blame 有一个致命的扰动源,必须先说清楚。
扰动层:碳测年会重置
直接对演示仓库的 gateway.c 跑 blame(实测):
$ git blame gateway.c
f75b4237 (liwei 2013-02-11 ...) // gateway.c - 网关转发与计费上报
...
0be7adbb (bot 2021-11-02 ...) int get_timeout_ms(void) {
0be7adbb (bot 2021-11-02 ...) if (g_cfg == NULL) {
0be7adbb (bot 2021-11-02 ...) return DEFAULT_TIMEOUT;
...
0be7adbb (bot 2021-11-02 ...) Sleep(50);
0be7adbb (bot 2021-11-02 ...) return do_send(host, port, payload, retries);
几乎所有函数体行都显示 2021 年 11 月,作者 bot。这是 2021 年那次”全仓统一 clang-format 格式化”留下的扰动层:一次大规模重排,把几十年沉积的年代信息一夜抹平。考古学里这叫扰动——后世施工把地层打乱了。代码库里的扰动源不止格式化:批量改版权头、目录大迁移、行尾符转换,个个都是。
对策是把扰动层从测年里剔除。git 2.23 起支持 .git-blame-ignore-revs:把格式化提交的哈希写进这个文件(GitHub 的 blame 视图原生认它),再测(实测):
$ git blame --ignore-revs-file=.git-blame-ignore-revs gateway.c
f75b4237 (liwei 2013-02-11 ...) int send_to_billing(...) {
f75b4237 (liwei 2013-02-11 ...) int retries = 3;
c1795da7 (zhangsan 2014-11-21 ...) if (is_internal(host)) {
40b05358 (liwei 2016-07-14 ...) if (port == 8080) {
8ea81237 (zhangsan 2017-05-09 ...) if (strcmp(host, "bill-backup") == 0) {
5475c450 (zhangsan 2018-11-02 ...) Sleep(50);
年代归位:Sleep(50) 回到 2018 年,空指针检查回到 2016 年,每一段疤痕重新长回自己的地层。(建议顺手 git config blame.ignoreRevsFile .git-blame-ignore-revs,让它成为默认行为。)
洛阳铲:扰动层打不穿的钻探
更漂亮的性质在下面这个对比里。找 Sleep(50) 是什么时候进仓库的,用 pickaxe(实测):
$ git log -S "Sleep(50)" --oneline -- gateway.c
5475c45 fix: 下游计费服务限流,上报间隔 50ms(INC-2018-1102)
一击命中,而且结果里根本没有 2021 年那次格式化提交。原因在于 -S 的判定机制:它找的是”某个字符串出现次数发生变化”的提交。格式化重排了代码,但没有增删 Sleep(50) 这个字符串,所以在 pickaxe 眼里扰动层是透明的。碳测年会被扰动层重置,洛阳铲直接穿过去——断代工具要按污染机理选,这是地层学的第一条实用纪律。
地层学的最后一条洞察,也是最重要的:读懂了每一层的年代之后你会发现,bug 很少住在某一层内部,它住在层与层的接触面上。2009 年的地层假设”配置对象永生”,2016 年的补丁只在被烫过的地方打了补丁,2023 年的新代码又在这个假设上盖了新楼。这和卡死篇里”环不在任何一根栈里,在栈与栈的关系里”是同一个句式:遗址的病在关系里,不在单件器物里。
三、器物类型学:风格即年代
考古学家看一眼陶器的纹样和胎质,就能报出年代区间。程序员有同款能力,只是很少有人有意识地用:惯用法是年代的指纹。
| 器物(惯用法) | 年代层位 |
|---|---|
NULL、手写索引 for 循环 | C++11 之前 |
sprintf + 固定 char[] 缓冲区 | 安全意识普及之前 |
A 后缀 API + ANSI 字符串 | Unicode 迁移完成之前 |
匈牙利命名法(dwLen、szHost) | 老 Windows 学派鼎盛期 |
裸 new/delete + try/catch 手工释放 | RAII 还没传到这个团队 |
宏当常量、#define MAX_RETRY | constexpr 之前,或 C 项目 |
| 错误码 + 返回值检查 | 异常风格统一之前,或系统层传统 |
(完整版见附录 C。)类型学断代分三步:认器物,定年代,还原约束——第三步才是目的。一段”愚蠢”的手工内存管理,在没有智能指针可用的年代是理性选择;一个写死的 256 字节缓冲区,在主机名不超过 63 字节的年代绰绰有余。
这里必须立一块警示牌,否则整篇文章都会被读歪:用今天的工具箱审判当年的决策,是代码考古里的辉格史观。辉格史观是史学界的大忌——站在今天的结果上嘲笑古人的局限。在代码库里它的变体是:一边享受着 SSD、64G 内存和现代标准库,一边在评审里说”这写的什么垃圾”。器物类型学教你断代,不是教你傲慢。每一层丑代码都先问一句:当年换了我,手头这些工具,我写得更好吗?
四、铭文互证:单一证据不可信
地层给你年代,器物给你约束。但考古学家最想要的永远是文字材料——铭文。代码库里”自称记录了意图”的东西不少,问题是没有一种可以完全信任。干这行要先背一张证据等级表:
代码 > 测试 > commit message > 注释 > 文档 > 口头传说
逐个说透,每一条都是一门手艺。
代码排第一,因为它是唯一不可能说谎的证据——它是什么就是什么。但它只回答”是什么”,拒绝回答”为什么”。这就是全部问题的根源。
测试是活的证词:可执行,可证伪。但测试也会变成化石。一个测了八年的用例,证明的是”八年前的某人很在乎这个行为”,不证明”这个行为今天还重要”。读测试要先读它的年代,再决定它证词的效力。
commit message 是当代日记——写下它的人就在现场,时间上最接近庭审。好的 message 长这样(演示仓库实测,git show 5475c45):
commit 5475c450fa5821dd176c203fc9741cf87c21117d
Author: zhangsan <zhangsan@corp.example>
Date: Fri Nov 2 02:47:00 2018 +0800
fix: 下游计费服务限流,上报间隔 50ms(INC-2018-1102)
改了什么,为什么改,事故编号多少——三要素齐全,这是铭文里的上品。与之相对的是占行业大多数的”fix bug””update””改一下”:缺席的庭审,空白的判决书。
注释是后世注疏,不是原始铭文。这是最需要破除迷信的一条。注释描述的是”写下那一刻的代码”,代码变了它不变,腐烂速度比代码快得多。演示仓库里有一份铁证(实测,git blame config.h):
f75b4237 (liwei 2013-02-11 ...) // 最多重试 3 次
e4480484 (chenjun 2019-08-15 ...) #define MAX_RETRY 5
注释说 3,代码是 5。行级断代一目了然:这条注释比它描述的那行代码老了六岁半——2019 年的提交改了数字没改注释。注释不是不能读,是不能当原始证据读;它和代码冲突时,永远信代码,然后顺手把注释修掉(那是你给下一个考古学家的赠品)。
文档是理想国的地图,描述的是系统应该的样子。口头传说——老员工的记忆——排在最后,不是因为它没价值,恰恰相反,它常常是”为什么”的唯一来源;而是因为它会重构。记忆每次被提取都会被改写,老员工言之凿凿的”当年是为了性能”,可能只是他愿意记住的版本。口述史是合法证据,但必须交叉印证:用 commit、监控存档、外部系统的变更记录去对。印证不上的,标”存疑”,别标”采信”。
最后是一种可遇不可求的好运气:罗塞塔石碑。当你在仓库里发现新旧两套同构实现并存——老计费模块和正在灰度的新计费模块,C 版的解析器和 Rust 重写版——你手里就有了双重铭文。两套代码互为译文,哪边读通了都能解译另一边。考古学家靠一块三语石碑破译了埃及象形文字,你靠并行实现破译无人理解的遗留逻辑。遇到并行系统,先别嫌它乱,它是全仓库含金量最高的遗址。
五、发掘纪律:非破坏优先
考古和盗掘挖的是同一个坑,区别只有一条:记录。考古学家每揭一层都要拍照、编号、测绘,因为发掘本身就是破坏——地层一经翻动,原始关系永久消失。改遗留代码一模一样:你动的每一行,都在销毁它携带的历史信息。所以四门手艺里最后这门是纪律。
纪律一:先拍照,后动土。Michael Feathers 在《Working Effectively with Legacy Code》里给这件事起了正式名字:characterization test(特征化测试)。要点是不断言”正确”,只固定”现在”:
// 改造前先给现状拍照——钉住的是行为,不是道理
TEST(LegacyParser, Characterize_CurrentBehavior) {
EXPECT_EQ(parse_price("1,234"), 1234); // 现状如此,先钉住
EXPECT_EQ(parse_price(""), 0); // 哪怕它可能是 bug,也是今天的现实
}
照片的意义在于:动土之后任何行为漂移都会立刻报警。你拍的不是对错,是现状。
纪律二:一次只剥一层。地层要逐层揭,重构要逐层做。同时动三层的后果是出了问题无法归因——这和调试里”一次只改一个变量”是同一条戒律在两个战场的投影。
纪律三:写发掘日志。你花三天验证的假设、排除掉的死路、挖通的关系,写下来,进 commit message 或 wiki。开头那个凌晨三点的故事里,链条断裂的位置不是新人鲁莽,而是 2016 年那位工程师只留了半块碑——commit message 记了事故编号,但事故单今天已经打不开了。你要默认自己写下的每个字,都是下一个考古学家唯一的铭文来源。你今晚 save 的不只是代码,还有三年后某个凌晨三点的陌生人。
六、疤痕组织学:每一行丑代码都是一座纪念碑
四门手艺合在一起,就能干最实用的那件事:读变更模式。遗留代码的每一处”丑”,几乎都是一种化石。把演示仓库的 send_to_billing 按 blame 结果释读一遍(年代与作者均为实测):
int send_to_billing(const char* host, int port, const char* payload) {
int retries = 3; // 2013 liwei:原始河床
if (is_internal(host)) { // 2014 zhangsan:INC-2014-0331,内网重试放大雪崩
retries = 1;
}
if (port == 8080) { // 2016 liwei:老客户端写死 8080
port = 80;
}
if (strcmp(host, "bill-backup") == 0) { // 2017 zhangsan:计费迁移期双写
// TODO(zhangsan): 迁移期双写,下版本删除 <- 2017 年的"下版本"
enqueue(payload);
}
Sleep(50); // 2018 zhangsan:INC-2018-1102,下游限流
return do_send(host, port, payload, retries);
}
十行函数,五座纪念碑。这个函数不是”写”出来的,是一次事故接一次事故长出来的。推广开去,是一张化石对照表:
| 地表特征 | 地下埋藏 |
|---|---|
| 越来越长的 if 条件链 | 每个分支是一次事故或一个大客户 |
| “这里不可能为 NULL”的防御检查 | 它为 NULL 过,在生产上 |
| 注释掉的代码块 | 舍不得埋的尸体,或待平反的冤案 |
| feature flag / 配置开关 | 政治妥协的停战协定(”先做成可关的”) |
| 写着”兼容 XX”的代码 | 与外部系统的和平条约,缔约方可能已消亡 |
| 六年没删的 TODO | 一个被新优先级永远推迟的承诺 |
| 两千行的函数 | 沉积过快、常年无人清淤的主河床 |
| 几乎相同但略有差异的复制粘贴块 | 平行地层:同一逻辑在不同分支各自演化 |
这张表的读法只有一句口诀:每一行丑代码都可能是某次线上事故的纪念碑,只是没刻碑文。考古学家的工作,就是补刻碑文——把年代、事故、约束查清楚,写进注释或 commit。补完你会发现一件反直觉的事:读懂之后的遗留代码往往没那么丑了。丑的不是代码,是失忆。
七、断代工程与无主文物
现实里的遗址很少像演示仓库这样铭文齐全。这一节处理三种残酷工况。
无主文物:被抹掉历史的仓库。公司从 SVN 迁 git 那年,有人图省事,把十年历史 squash 成一个提交——initial commit,五万行。blame 全灭,每一行都显示同一天。这种文物考古学里叫”无出处”,处理规程是:放弃行级断代,退回器物类型学(按惯用法把代码分成若干年代区块),加外部档案(老文档、邮件、发版记录),再加口述史。断代精度从”行”降到”区块”,但够用。
热点钻探:先挖哪里。遗址太大不可能全挖,考古学家靠勘测选点,程序员靠数据。Adam Tornhill 在《Your Code as a Crime Scene》——书名本身就是这门手艺最好的广告——给出的指标是热点 = 改动频率 × 复杂度。改动频率一行命令就有(实测):
$ git shortlog -sn -- gateway.c
4 zhangsan
2 liwei
1 bot
$ git rev-list --count HEAD -- gateway.c
7
gateway.c 七年被改了七次、四个人经手,而 config.h 安静得多——改动最频繁又最复杂的文件,就是埋葬事故最密集的主河床,考古资源优先投给它。
行为钻探:什么时候变的。blame 回答”这行代码什么年代”,但很多时候你真正想问的是”行为在哪个层位改变”。MAX_RETRY 到底是哪天从 3 变成 5 的?对着十三层地层钻探,二分法(实测,完整过程):
$ git bisect start
$ git bisect bad HEAD
$ git bisect good f75b423
Bisecting: 4 revisions left to test after this (roughly 2 steps)
$ git bisect run sh -c "git grep -q 'MAX_RETRY 3' -- config.h"
running 'sh' '-c' 'git grep -q ...'
Bisecting: 2 revisions left to test after this (roughly 1 step)
...
e4480484c3d4aa92b3fc693272efb3133e3c6fd9 is the first bad commit
commit e4480484c3d4aa92b3fc693272efb3133e3c6fd9
Author: chenjun <chenjun@corp.example>
Date: Thu Aug 15 16:20:00 2019 +0800
fix: 弱网环境重试 3 次不够,改为 5 次
两步钻探,定位到 2019 年 8 月 15 日那一层。(PowerShell 用户注意一个实测坑:给 sh -c 传带空格的探测命令时要用外双引号、内单引号,否则参数会被拆开,bisect 会沉默地给你错误答案——本文第一次就是这么踩的。)bisect 的本质是对地层做二分钻探,任何”什么时候行为变了”的问题——性能哪天开始退化的、这个头文件哪天开始被污染的——都是它的猎场。
最后是一对必须区分的概念:老,不等于死。五年没动的地层有两种可能:稳定,或者已经没人敢碰的僵尸。区分方法不是看年代,是看调用方——用引用分析查谁还在用它。还有调用方的老代码是成熟;没有调用方却没人敢删的老代码是废墟。前者供起来,后者清走。
八、主案例:那个 50 毫秒的 Sleep
把全部手艺装成一次完整办案。案情:网关服务的平均延迟比设计值高 50ms,不多不少,整整齐齐 50ms。有人怀疑网络,有人怀疑 GC。你不猜,你挖。
第一步,分诊。延迟整齐到毫秒级,多半是人为常量,不是系统噪声。目标锁定:找到那个常量。先别读代码——全仓上万行,读是最后手段。
第二步,钻探。洛阳铲直指嫌疑犯(实测):
$ git log -S "Sleep(50)" --oneline -- gateway.c
5475c45 fix: 下游计费服务限流,上报间隔 50ms(INC-2018-1102)
一条命中。看铭文(前文第四节引过全文):2018 年 11 月 2 日凌晨 2 点 47 分,zhangsan,事故编号 INC-2018-1102。
第三步,互证。拿着事故编号去查工单系统——查无此单。工单系统五年里迁移过两次,2018 年的数据只归档了标题,正文没了。这是真实考古的常态:铭文残缺,庭审记录损毁。别指望爽文结局,转向口述史:找到当年在团队里的老员工,他记得”计费服务好像被上游打爆过一次,后来他们加了限流”。再交叉印证:翻下游计费团队的 changelog,2018 年 11 月确有限流改版记录。证据链闭合:当年上游没有限流,我方每个请求都直怼计费,一次流量尖峰把对方打崩,事后约定我方主动限速 50ms——石碑立在了请求路径上。
第四步,结案。判定这座碑今天是否还承重:计费服务 2020 年已重写,自带限流和熔断,缔约的技术前提已消亡。但注意处置方式——不是直接删,那是盗墓。正确的结案是:把 50ms 改成配置项(默认保留原值),在代码里补刻碑文(注释写明 INC-2018-1102 与 2020 年下游改版两次事件),灰度调零观察两周,确认无回潮再彻底移除。整个发掘过程写进最终的 commit message——发掘日志闭环,碑文的碑文也立好了。
回看这次办案:钻探花了五分钟,互证花了两天。工具快的部分都不值钱,值钱的永远是证据链。
九、考古学的边界:什么时候别挖
写完八节方法论,必须写一节反方法论,否则这篇文章会成为另一种迷信。
按风险分诊,不按好奇心。要改的代码才值得挖两天;只是路过看到的丑代码,拍张照(加一行”此处历史待考”的注释)就走。考古资源永远稀缺,投给主河床,不投给路边每一块怪石。
反浪漫化。有些怪代码下面没有故事,就是当年周五晚上赶工写烂了。类型学能告诉你年代,不能赋予意义。挖了三层什么都没挖到时,要有勇气承认:这里不是遗址,是垃圾堆。垃圾堆的处理方式是清运,不是供起来。
栅栏定律的另一半。切斯特顿的原话是”搞清楚它为什么立在那里,再来拆”,重点在搞清楚,不在永远不拆。三段证据(地层、铭文、口述史)都查无收获时,做风险定价:影响面小就上 feature flag 灰度去试,让实验替考古回答;影响面大才值得继续深挖。把”没人知道为什么”当成永久豁免牌,那是对栅栏定律最大的曲解——它从护栏变成了挡箭牌。
重写是理性选项,但重写前仍要考古。当发掘成本持续高于重建成本,重写是对的。可重写不等于豁免考古:旧系统的疤痕清单必须完整继承到新系统,否则你会把每一道疤重新受一遍——行业里大部分”重写灾难”,灾难的不是重写本身,是把阑尾和疫苗疤一起切掉之后,旧病复发。
十、AI 时代:洛阳铲与考古学家
这篇文章写成于 AI 编程的喧嚣年代,回避这个话题是不诚实的。我的判断分三层,尽量克制。
AI 擅长地表测绘和类型学。把一个陌生仓库丢给它,它能秒级画出模块结构、生成摘要、识别每个区块的惯用法年代——这些恰好是考古里最耗时但最不需要”为什么”的工序。用它干这个,划算。
AI 读不了历史,而且会假装能读。“为什么”不在任何文本里,AI 和你一样无从得知。但它不会说不知道——它会基于代码的”合理性”幻觉出一个听起来很像那么回事的解释。这比没有解释更危险,因为它写得像真的。考古的铁律在 AI 时代加倍适用:任何”为什么”的答案,都必须有铭文或外部档案背书,不管它来自人还是模型。
真正的变化在沉积速率。AI 让代码生产提速一个数量级,意味着未来的地层将以十倍速度堆积——而且新地层里天然没有铭文,因为生成过程本就没有庭审。commit message、ADR、特征化测试这些”可考古性”实践,正在从工程美德变成生存需求。一句话收束:AI 是洛阳铲,不是考古学家。它让你钻探得更快,释读仍然是你。
十一、收束:做一具好尸体,留一块好碑
这个系列拆过好几个前提:调试篇拆了”作者知道意图”,海森堡篇拆了”观察不改变系统”,卡死篇拆了”程序卡住就是程序有病”。这篇拆的是最后一个、也最根本的一个:”代码的含义在代码里。”崩溃在空间上撒谎,卡死在时间上撒谎,代码在历史上沉默。
对付沉默的办法,前文已经讲了很多:地层学、类型学、铭文互证、发掘纪律。但手艺终究是下游,上游在我们自己手里。每个工程师每天同时干两份工作:写代码,以及制造未来的地层。第二份工作没有 KPI,但它决定三年后那个凌晨三点的陌生人要挖多久。
所以收束成三条铭文纪律:
- commit message 记录 why 和事故编号,不记录 what。what 代码自己会说;why 你不说,就永远没人说了。
- 重大决策写 ADR(架构决策记录):决策、备选、否决理由。它是有意识的墓葬铭文,是给未来考古学家备好的罗塞塔石碑。
- 顺手补碑。读代码时挖通的每一段历史,写成注释或提交说明。你欠下一个考古学家的,正如上一个考古学家欠你的。
软件工程的大部分工作,其实是与不在场的人协作——与过去的作者,与未来的读者。代码考古学把这种跨时间的协作,从运气变成手艺。
你今天写的代码,就是某人明天要发掘的遗址。做一具好尸体,留一块好碑。
附录 A:发掘速查表
| 处境 | 用手艺 | 用工具 |
|---|---|---|
| 看不懂某段代码为何存在 | 地层学 + 铭文互证 | blame(配 ignore-revs)/ log -S / 口述史 |
| 不敢动某段代码 | 发掘纪律 | characterization test |
| 要修 bug / 改行为 | 疤痕组织学 + 行为钻探 | log -S / log -G / bisect |
| 评估重写还是维护 | 断代工程 + 热点分析 | shortlog / rev-list / 引用分析 |
| 历史被 squash 抹平 | 无主文物规程 | 类型学分区 + 外部档案 + 口述史 |
附录 B:git 断代命令对照
以下命令全部在 git 2.49.0 + PowerShell 实测(输出见正文):
git blame <file> # 碳测年:行级年代
git config blame.ignoreRevsFile .git-blame-ignore-revs # 永久排除扰动层
git blame --ignore-revs-file=.git-blame-ignore-revs <file> # 临时排除
git log -S "字符串" --oneline -- <file> # 洛阳铲:字符串何时引入/消失(按出现次数变化判定,免疫格式化)
git log -G "正则" --oneline -- <file> # 按 diff 内容找变更
git log --follow --oneline -- <file> # 追踪文件更名(不带 --follow 历史在更名处断裂)
git shortlog -sn -- <file> # 层的作者分布
git rev-list --count HEAD -- <file> # 层位总数(改动频率)
git show <hash> # 读铭文:某提交的完整证据
git bisect start / bad / good / run sh -c "..." # 地层二分钻探:定位行为变更层位
两个实测坑:其一,PowerShell 里给 sh -c 传参用”外双引号 + 内单引号”,否则探测命令被拆词,bisect 给出静默错误答案;其二,-S 后面跟的是字面字符串,含正则元字符(如括号)无需转义,这正是它比 -G 适合精确钻探的原因。
附录 C:地层指纹表(C 与 C++)
| 器物 | 年代层位 | 当年约束 |
|---|---|---|
NULL、0 当空指针 | C++11 前 | 没有 nullptr |
匈牙利命名(dw/sz/lpsz 前缀) | 老 Windows 学派 | 类型系统弱,靠命名补 |
A 后缀 API、ANSI 字符串 | Unicode 迁移前 | 中文环境乱码史 |
sprintf、固定 char[N] | 安全意识前 | 缓冲区溢出还没上头条 |
裸 new/delete 配手工释放 | RAII 前 | 智能指针未普及 |
GetPrivateProfileStringA + ini 文件 | 配置中心前 | 单机部署年代 |
| 错误码返回值 | 异常统一前 | 跨编译器 ABI 顾虑 |
| 宏定义常量 | constexpr 前 | 编译器优化能力弱 |
Sleep 当同步手段 | 条件变量普及前 | 并发原语匮乏 |
std::auto_ptr | C++98–C++11 过渡期 | 移动语义未诞生 |
用法:认器物 → 定年代 → 还原约束。记住辉格史观警示牌:指纹表用来理解,不用来嘲讽。
附录 D:演示仓库的构造方法
文中演示仓库由脚本生成:每次提交前设置 GIT_AUTHOR_DATE / GIT_COMMITTER_DATE(以及作者姓名邮箱),即可回填任意年代。核心模式如下(bash 版,PowerShell 完整脚本同思路):
GIT_AUTHOR_NAME=zhangsan GIT_AUTHOR_DATE="2018-11-02 02:47:00 +0800" \
GIT_COMMITTER_NAME=zhangsan GIT_COMMITTER_DATE="2018-11-02 02:47:00 +0800" \
git commit -m "fix: 下游计费服务限流,上报间隔 50ms(INC-2018-1102)"
仓库共 13 个提交:2009 会话模块 → 2010 网络工具 → 2013 网关与配置 → 2014/2016/2017/2018 五道疤痕 → 2019 说谎的注释 → 2021 全仓格式化(扰动层)→ 2022 文件更名 → 2023 string_view 补丁。照此构造后,正文与附录 B 的每条命令可原样复现;commit 哈希会随构造时间变化,属正常现象。(PowerShell 用户注意:脚本文件若含中文,须保存为带 BOM 的 UTF-8,否则 PowerShell 5.1 按 ANSI 解析会产生乱码并破坏语法——本文构建脚本实测踩过。)
编译验证命令(对应正文的实测声明):
cl /nologo /W4 /utf-8 /c gateway.c
cl /nologo /W4 /utf-8 /std:c++17 /EHsc /c session.cpp
cl /nologo /W4 /utf-8 /std:c++17 /EHsc /c net_utils.cpp
gcc -x c -Wall -Wextra -c net_utils.cpp # WSL 侧:仅可移植文件
两个开关不可省。其一,/std:c++17:MSVC 默认标准是 C++14,2023 年地层的 string_view 会直接报”不是 std 的成员”。其二,/utf-8:仓库文件是无 BOM 的 UTF-8(Linux 友好的存法),缺了它,MSVC 按本地代码页解码中文注释,实测中把 #include <windows.h> 所在行吞掉,Sleep 随之变成”未定义;假设外部返回 int”——一个编码问题伪装成了符号缺失。这两个坑本文都替你先踩过了。
相关文章
- 你越想看清它,它就越不存在:海森堡 bug 与调试的观察者效应 – 取证方法论的认识论源头
- AI 写的 bug 像正确的代码 – 系列认识论的起点:”意图不可知”在调试侧的投影
- 非侵入探针:ASan/TSan/UBSan 如何让 bug 在发作之前显形 – 用结构证据替代时序证据,与铭文互证同构
- 代码从资产变成负债 – 考古学是负债的清点方法
- 简单不是初级是最高级 – 少制造地层,是最好的铭文
