


先写测试,修改低层级调试器代码而不破坏其他平台
在让AI修改一个支持多种操作系统和后端的编译型语言调试器时使用。AI会先写会失败的测试,把操作系统专用代码放进单独的文件,修改共享接口时检查所有后端,并让提交信息以子系统名开头。
要使用这个AI规则
## 修改调试器前,先读负责该行为的那一层
面向编译型语言的调试器是一个复杂的多层系统。要在其中高效地工作,需要理解调试内部机制、二进制调试信息格式、操作系统特定的进程控制,以及语言运行时的内部机制。
要把每一次改动都当作触及一个系统来对待——在这个系统中,跨平台、跨后端的正确性远比单纯的速度更重要。在内存访问、寄存器处理或后端行为上的一个细微失误,往往会表现为只在某一个平台上出现、且难以复现的故障。
在编辑之前,先阅读周围的代码层,弄清楚你要修改的行为归哪一层抽象所有。对共享接口的改动会向外波及所有实现该接口的后端,因此在动手之前先评估影响范围。
## 改代码前先写会失败的测试,绝不跳过测试
所有代码改动都必须遵循测试驱动开发。这不是可选项。
遵循“红-绿-重构-验证”循环:
1. **红(RED)** —— 先编写一个会失败的测试;它必须因为正确的原因而失败。
2. **绿(GREEN)** —— 编写让测试通过所需的最少代码。
3. **重构(REFACTOR)** —— 在保持测试全部通过的前提下清理代码。
4. **验证(VERIFY)** —— 提交前运行完整的测试套件。
强制性规则:
1. 绝不在没有失败测试的情况下先写实现代码。
2. 绝不在没有测试的情况下提交代码。
3. 绝不为了让 CI 通过而跳过测试或将其标记为跳过。
4. 绝不禁用现有测试——要么修复测试,要么修复代码。
5. 每个测试只验证一个具体行为。
6. 测试名称要描述所测试的场景。
当先写测试感觉很困难时,这种困难本身就是一个信号,而不是可以偷懒的借口。它通常意味着某个函数承担了过多职责、依赖关系耦合过紧,或者需求本身不清晰。不要跳过 TDD——去修复它所暴露出的设计问题。
## 决定各类测试放在哪里,并让每次结果一致
测试应放在哪里:
- **单元测试**:与其所覆盖的代码放在同一位置。
- **集成测试**:放在专门的端到端测试目录中。
- **平台特定测试**:用构建标签/条件编译进行门控。
- **后端特定测试**:对每个后端分别进行测试。
- **测试夹具(Fixtures)**:以源码形式保留,在测试运行时编译,而不是使用预先构建好的二进制文件;使用共享的夹具定位辅助工具,而不是手写路径查找逻辑。
质量标准:
- 测试行为,而不是测试实现细节。
- 优先做到一个测试对应一个断言,这样一旦失败就能精确定位到一个原因。
- 命名要清晰,采用下划线分隔的模式,依次表达被测对象、场景和预期结果。
- 复用已有的测试工具,而不是重新造轮子。
- 测试必须是确定性的——不使用随机值,不存在竞态条件。
- 对于输出格式类的测试,应断言完整的预期输出,而不是断言某个子字符串。完整输出匹配能够捕捉到子字符串检查会悄悄漏掉的回归问题——多余的空白、字段顺序变化、格式改动等。
## 在各层之间分开只读接口和修改状态的接口
该系统被组织为职责分离清晰的若干层:最上层是 CLI,其下是服务/协议层,再往下是高层操作,再往下是进程/目标抽象层,最底层是可插拔的后端实现(直接的操作系统级进程控制、远程协议客户端、离线转储分析、内核级追踪)。
在抽象边界处采用两级接口设计:
- 一个只读的公共接口,供调用方用来观察状态。
- 一个独立的内部接口,承载所有会修改状态的操作。
- 一个围绕内部接口的有状态包装对象,外加一个用于同时管理多个目标的分组抽象。
各个后端在内部接口之下是可插拔的。当你修改这个接口时,改动通常会影响到所有的实现——要把每一个后端都考虑进去,而不仅仅是眼前正在处理的那一个。
## 把各操作系统和各体系结构专用的代码放进单独文件
使用基于文件名的条件编译来隔离操作系统特定和体系结构特定的代码——按操作系统划分的源文件、按体系结构划分的源文件,由构建系统在编译期选择,而不是在运行时通过分支判断来选择。
- 绝不把平台特定的代码放进通用文件中。
- 把平台特定的代码限定在真正需要它的后端中(直接操作系统控制的后端和远程协议后端),而不是放进共享的核心逻辑里。
- 在需要支持多种体系结构时,把寄存器、ABI 和调用约定相关的细节放在按体系结构划分的文件中。
做得好的话,共享核心部分能保持可移植性,而添加一个新平台就只是增加文件的问题,不需要重写通用代码。
## 避免读取进程内存和设置断点时的常见错误
1. **不要直接修改共享状态**——要通过内部接口提供的方法来操作,这些方法负责加锁和保持状态一致性。
2. **后端特定的操作各不相同**——同一个概念上的动作(例如插入一个断点),在不同后端中的实现方式并不相同;切勿假定存在单一的实现机制。
3. **区分相似的抽象概念**——操作系统线程与语言层面的并发单元并不是同一回事;要清楚一个操作作用的对象是哪一个。大多数操作作用于语言层面的单元,而不是原始线程。
4. **内存安全**——在读取目标进程内存时,始终要处理错误。目标进程可能已经终止,内存可能已被解除映射,地址也可能无效。
5. **不要新增冗余的遍历**——如果已有某个流程已经在遍历所有单元(编译单元、符号等),就把你的逐单元检查加到那个流程里,而不是单独再扫描一遍。
6. **将插桩点放在函数序言之后**——把断点/探针设置在函数序言(prologue)之后,而不是设置在原始入口点,因为如果在入口处插桩,运行时的栈检查序言可能会被重复触发。
## 提交信息以子系统名开头,并写明修改原因
使用基于子系统的提交格式:主题行先写明受影响的子系统,接一个冒号和简短的改动摘要,然后空一行,接着是一段说明改动原因的正文,最后以一行引用所修复问题的内容作结。
- 在主题行前加上受影响子系统的前缀,这样历史记录就能按区域快速浏览。
- 主题行要简洁,并控制长度上限(约 70 个字符)。
- 用正文说明改动的原因,并进行换行排版(约 80 个字符一行)。
- 引用触发该改动的相关问题,让每一次改动都能追溯到其起因。
结构化的提交信息能让变更日志保持可浏览性,也便于日后重建任何一次改动背后的原因。
