判据、全绿、缺口、漂移与自查 —— 线上补课(下)
系列gdev-masterNVIDIA/nouveau 用户态 GPGPU 运行时从 C/C 到 Rust 的移植工程 ·第三部换了一个问题前两部回答「怎么搬」这一部回答「凭什么说对了」。一句话核心最值钱的一格每写一条检查同时写「它凭什么可信」一条从未失败过的检查和没有检查在证据强度上完全相同把「全绿」这两个字的含义钉死被自己的判据逮住四次全在证伪测试全绿之后比译错更贵的是从来没进过清单差 208 MiB 的真差异所有检查全是绿的代码漂移会响文档漂移永远不红最毒的形态只有一个版本而它是错的查看「跑偏了吗」回答只能靠查事实复核的时候不许改要复核的那条命令一、判据13先让它失败一次最直接的现场两个后端「必须恰好选一个」的编译期断言写的是——assert!(DRIVER_NOUVEAU||DRIVER_NVRM||DRIVER_PSCNV);// 而 DRIVER_NOUVEAU 的定义就是「另外两个都没开」!NVRM !PSCNV代入之后这个析取式恒为真它声称要防「两个后端同时开着」而那恰恰是它唯一防不住的情况。发现它不靠读代码靠一条真命令故意把两个后端都打开去构建——照编不误、退出码 0。改成和式三个开关计数 1后同一格立刻编译期报错。由此固定成新判据的三道关关内容抓过的活例① 先红新判据先确认它会红且红在该红的那条一条永远红的检查会盖住别人的绿14 篇的GLIBC_前缀就是恒红② 注入 回绿把被测代码改坏一点必须变红改回去必须变绿只证红不证复原 半个自检③ 非退化比对的两边必须真是两个东西.so.1软链没建好加载器悄悄回落原版 C 库——那门比的是「C 对 C」输出当然全绿对 Rust 侧零信息第三关还带出了「基准的来历」问题golden 必须证明是当前源码现编出来的「先传.so、后传清单」这个顺序本身就断言了新鲜度C 基线的指纹默认判失败、不自动放行而指纹若只存在本机「本机产物 ↔ 本机指纹」就是循环论证。同一次把 9 处判据整体审计了一遍7 处修、2 处登记。硬件后端那条路还有个结构性问题开发机没有设备节点不换掉内核一千多行函数体一行都走不到。做法是LD_PRELOAD一个假内核在libc 边界拦open/ioctl/mmap/close同一份假内核同时服务 C 侧和 Rust 侧两个 mock 会各自漂移比四条通道stdout / stderr / 退出码 / 系统调用轨迹。附带一条硬要求到内核必须走 libc不许裸syscall——裸调用绕过拦截后失效是静默的轨迹通道还断言「非空」实测每案 ≥3 行防它悄悄退化成空比对。这条守卫后来自己改强过一次强到把真用例也拦了——于是给那个用例换通道守卫和用例各归各位。这一篇留下的判据纪律后来每个任务都在跑缺产物判失败、不许跳过跳过和通过在日志里长得一样红在指定入口红在别的格不算数判据与实现不同源不能拿实现自己的输出当期望值绿表必须带任务名——不带名的绿等于没判定那张 48/48 的绿表就是按这条攒出来的。二、全绿14一个需要被证明有信息量的信号先写清楚门不能证什么① 覆盖不到的路径没进用例的代码绿与它无关② 无卡路径上两侧一起失败在同一个错误码——diff 照样全绿③ 系统调用轨迹这类重通道一旦降级成空输出「比对通过」什么都没说④ 抽样抽中的绿不等于没抽到的绿。这道门自己就撞过 ①上机脚本在开发机上从未真跑过——第一次跑就撞出两个配置错缺xf86drm.h要-idirafter补路径drm 要链接宿主的库目录还发现 libdrm 会引进 glibc 2.17/2.33/2.38 的符号、而目标机是 2.16 ⇒ 产物只能出库、不能建可执行可执行会被目标机的动态加载器拒载_init/_fini模板符号是恒红噪声——排除之外还要在绿行里写清「排除了什么」。从此文档里的验证状态一律带标记【已实测 日期】/【未验】——「备好」和「跑过」必须长得不一样。「两侧同败也全绿」这个死角用差分实验堵上把设备藏起来LD_PRELOAD垫片让打开/dev/dri/*、/dev/gdev*返回「不存在」——不用 root、不动内核同一个用例的输出必须变。于是「全绿」有了正式定义✅ 全绿 逐字节一致且至少一个用例的行为真的依赖设备。细节全是要命的open/open64/openat/openat64四个入口必须全拦漏一个就藏不住差分对文案零假设不许假设错误消息长什么样只断言「变了」一个数字不许有两个含义配正控负控双向证伪。这一篇最值钱的是四个错全在判据自己身上而且全发生在它的证伪测试已经全绿之后错形状为什么危险比较版本号一边带GLIBC_前缀恒红恒绿坏在看不见恒红坏在「红得理直气壮」——它判的是好产物人照着报错越改越远两种失败真因共用一句报错误导处置正好相反的两件事缺件 vs 没变化让人走错方向未加引号的 heredoc 裸反引号静默执行正文里的反引号在本机被执行了——同样机制可以悄悄吞内容总结横幅打「all 个局面」数没被数过把选择器参数当统计结果打印——而总结横幅是唯一会被抄进记录的那一行最后一条的推广版值得单独抄走「跑一遍」抓不到的东西里有一种是「跑完之后打的那句话」。判定对了不等于总结对了总结和判定必须走同一条计算路径。出门前的最后准备也按同一标准过了一遍本机彩排三个局面正常/缺库/环境错逐条判输出与退出码五段剧本每段写清「这段绿凭什么绿」三条不可协商前提环境别人配不碰/C 源树一字节不改/判据不许为绿而改当天不许的四件事不覆盖 C 基线产物/不碰内核模块/不写设备节点/不用 sudo。这套剧本后来原样兑现——见 19 上机。三、缺口15门是绿的差异是真的三种形状的缺口都不在清单上、都不产生输出、都不响藏在编译期开关后面。driver这个开关其实同时切三件事(a) 编进哪些源文件、(b) 生成头里的GDEV_DRIVER_*宏、© 链接哪个库——我们只落了 (a)©、漏了 (b)。后果共享内存基址smem_base差了 218,103,808 字节约 208 MiBC0x0e000000vs Rust0x01000000。为什么所有检查全绿那个值只被写进内核描述结构体开发机没有 GPU两侧的启动都必然失败在同一个错误码——四条通道全一致。更狠的是那里有一条单测期望值钉的正是错误分支22 条单测全绿在这个问题上不是证据是同谋。修法三件开关收成单一事实源三个后端 21 个同名#[no_mangle]导出在同一.so里必然重名「共存」不是风格是做不到单测期望值改成硬字面量不许从被测代码现算顺手打掉那条恒真断言。整块没排进计划。判据从「参考移植转过没有」改成「是不是 gdev 用户态、实现在不在本仓」两块东西自己掉出来一个169 行的 dump 工具把库编进自己、静态链 rlib 独立成包——它不是测试是能单独跑的产物两个出口方向相反参数错走 stdout 返 1加载失败走 stderr返 0——C 自身 bug忠实复刻不许修用例里有「截断 ELF 前 200 字节」这种恶意输入和一个1,487 行.c 1,589 行.h、72 个函数的第三后端仓顶nvrm/与lib/user/nvrm/是软链、同一批文件——这条后来在 16 篇惹出第七种漂移形态见下节。第三后端永远等不到真机验证目标机不走它编译面 0 诊断、契约 21/21——所以判据必须先于实现就位先写完实现再想怎么验注意力就全花在「怎么让它绿」上判据先行的三个 commit97257e8/527d1ba/37c09d5。符号有了行为没人验。21 个导出符号 ABI 面扎实445 行结构体逐字节对拍、集合差为 0但函数体一行都没有行为覆盖。收口时把「没有判据」白纸黑字登记为缺口——「没有判据」和「验过了」是两件事前者必须写在纸上不许默认成后者。补欠账时还推翻了自己登记的根因台账写着的根因指向一段根本没被编译的预处理分支三条可复现证据——缺口登记的根因本身也是一个待验的命题。这一批里还有一条故意让它崩的用例C 的 unmap 不把映射指针置空第二次映射返回悬垂地址真实 use-after-unmap。两侧必须崩在同一个地方退出码 139它同时是守卫——谁哪天「顺手修干净」那行退出码从 139 变 0形状变化立刻被尺子看见这条守卫实测过注入-见红-撤销-回绿。四、漂移16代码漂移会响文档漂移永远不红根子一句话同一件事写在两个地方它们就会分叉不是「可能会」是只差时间。副本的危险不在会错在错了也不吭声——上游加了三条规则副本安静地少三条。文档不会构建失败所以漂移只能靠翻。五批漂移形态一批比一批难发现批形态例14 处数没钉到命令「59 个薄转发」实为 60「约 11.5k 行」复现不出 ⇒ 换成可复现的 17,518「38 万行」实为 394,440「258 个 commit」实为 260这数每次提交都变引用时现算5 类同一文档自己和自己对不上同一份 spec 前半「914 个必须仍全绿」、后半三处「915 passed」、总账早已写明实测 9226 条计数口径错三处「26 个 ioctl 包装」实为 23——26 是它的表行数3 个drm* 23 个pscnv_*8 类更正写在下游旧结论留在上游两个版本同时可读读者拿走哪个取决于先读到哪行10 条★ 只有一个版本而它是错的没有对撞面——唯一能发现的方式是去读被移植的 C 源码本身批五里最重的一条单独讲。被移植的 C 里有个状态机守卫if(prototypeStatePS_ReturnParams||PS_Params){/* push 参数 */}enum{PS_NoState,PS_ReturnParams,PS_Params,PS_End};第二项是裸枚举常量值为 2非零整式等价于(x 1) || 2——恒真。而它的 spec 摘要把这行概括成「prototypeState恒等于PS_Params」恒真的意思是「无论如何都进」摘要读成了「只有一个状态可达」——把恒真的意思读反了。实现照摘要做丢掉了PS_NoState这个状态到验收阶段才捞回来修正提交ed2d4c0。由此得到全系列被引最多的一句「C 是 oracle」这条铁律在这里的作用不是防改 C是防把 C 的摘要当成 C。摘要是有损压缩而且损失是无声的——它不会自己标注「我这里丢了一个状态」。前几批至少还能靠「两个数对不上」发现这一种只能回到源码本身去读那一行。批四则给出了能照做的规矩更正必须与被更正的结论相邻——在原结论那一行就地标 ⛔ 或划掉旧值紧挨着写新值不许只在文末写「更正见 §X」那等于留下两个版本同时可读。这条纪律写下的当晚就对 28 处漂移执行完毕实修 27、撤回 1、改正位置 1另带 4 条顺带bb14662。最值钱的反而是执行中的两次「台账与实况不符」——都是台账自己犯的一次记了没复现过的位置、一次记错了文件。所以执行要求是每条先自己跑一遍复现命令对不上就停下来报告不许照台账硬改。台账也是文档也会漂。写这篇的过程中又连续逮到两种新形态第六种——结论站得住但挂在一句跑不出来的复现命令上「这个宏族只命中 1 处」命令原样跑出来 5 处另外几处是别的后端的同族宏 生成宏的脚本自己一条假的可复现证据比没有证据更伤人家一验数字不对连对的那句结论也一起不信了。第七种——后写的文档用一次空的检测改错了先写的正确记录一份盘点表把某目录登记成「死副本」证据是「13 个文件与另一处逐字节相同已 diff」可那 13 个文件本来就是软链diff 说一样是必然的——早先的登记明明写着「符号链接壳」。判断「是不是两份东西」得看 inode看内容永远看不出来「两处数字恰好相等」不是交叉验证——它得先证明两条证据链互不相干。配套一条复核别人的数时顺手给命令加了个参数、当场判「复现不了」——错的是复核方复核的时候不许改你要复核的那条命令一个开关就是口径的一部分。诚实边界照例写清那 28 处「已改」不是「已验」子代理执行 抽验几条复现命令其余二十来条只核了改动形态这几批漂移全部在文档面、无一在判据面所以不阻塞上机——但「登记了就算处理过」是假账诚实的说法是「已知、已定位、已给出处置方案」。五、自查17回答「跑偏了吗」只能靠查事实有人提了三条难听的质疑结构没讲清/规划乱/记录粗糙加三个追问走了参考博客废弃的路为什么代码量这么大unsafe 口径有什么影响。查法不是辩解是逐条去量——结果每条都查出了真的问题「讲不清要翻译什么」——查实全仓确实没有单一出处。全树量出的总账要译 98,556 行63,979.c/.cpp 34,577.h、不译 616,845 行合计 715,401-type f口径 705,830——差值是 58 条软链的 9,571 行两种口径都对引用要带口径——要译的只占 11.8%。这张映射表一行一个目录、可被命令重放。顺手逮到三件漏网编译器目录下还有第二份 ocelot15.7 万行同名不同物、仓顶nvrm/是软链不是死副本、common/gdev_nvidia_nv50.c584 行是不在编译列表的死码。另有一次自我更正总账第一版漏算了 10,956 行 flex/bison 生成物。「规划乱」——查实不是「有好几套顺序」是顺序文档的标题与它自己两周前的裁决正好相反写着「硬件最后」实际 P4 先于 P3 剩余执行从 9-07 错到 9-18。收成唯一权威的 8 行表其余文档一律引用。那张表当时唯一没答的是 M4上机——这个空格后来由第四部补上了19 上机。「记录粗糙」——查实根因是一条机制工作流模板里标着「由脚本生成」的示范段落其实是手写散文——因为模板说它是生成的陈旧了也没人敢改。三个追问的答案也各有硬货走了参考博客废弃的路吗没走。把参考博客 12 篇原文8,263 行逐条核对成一张对照表方法与结构 7 条全 ✅、删除/重设计决定 4 条1 条 N/A全 ✅、验证方法 9 条 8✅1⏳那条「C 测试不改一行源码」正是 M4 判据现已兑现、本仓对参考博客的偏离 5 条逐条给处置、参考博客自己的毛病 4 条明确不照抄。被废弃的是「方案 C逐文件替换」——把.o塞回 C 的 CMake 再 bindgen 绑一层原文自己给的死因翻 150 行的device.c单 bindgen 生成的bindings.rs就有 1,200 行8 倍「产出仅是『Rust 语法的 C』」。而「前三层直译、第四层重设计」是参考博客明文规定的方法——本项目恰好没走废弃那条独立 workspace没有.o回填。为什么代码量这么大口径问题。参考博客「22,500 → 3,400」是预估、从未落地实交约 10,300且它自己的总数三处打架9,080 / 10,300 / 11,054。本仓 P3 层实测Rust 55,842 行 vs C 纯.c/.cpp48,762含头文件 76,093——同一堆代码三种口径全量1.15×反而变大、扣测试0.85×、再扣注释空行0.41×三个都「对」。所以对外不许只报一个数报数必须同时给口径加命令这条纪律在 20 性能三问 又原样兑现每个数带 median、min–max、口径三件套。至于为什么确实缩得不多参考博客的 ocelot 被裁到 8.2k 行它的硬件不走 PTXmaster 是完整版 48.8k——两者根本不是同一个东西直译层本来就缩不动测试内联在行数里。unsafe 口径有什么影响影响的是「Rust 换掉 C 到底换来了什么」这句话有没有证据——散文式的「实质等价」不可机械核对等于没有证据。于是立门三条判据unsafe 只能出现在白名单结构里unsafe extern不计量但超基线 WARNpub unsafe fn必须带# Safety文档节。立门过程自己就栽了两跤都值得抄走①原稿写「// SAFETY注释 0 条」是量错的——本仓用的是/// # Safety文档节实测209处按「pub unsafe fn有没有# Safety节」量是 104/117 89%、缺 13。结论口径确实缺不变理由整段换掉——前一个说法会误导下一个人。②第一版判据只数unsafe {块——而生产代码里还有unsafe impl4 个、unsafe extern579 个38 个文件、unsafe fn声明 228 个unsafe impl不是 FFI是安全性断言恰恰最该被人看一眼可它对旧口径整个不存在。计量单位改成「块 impl」并为此多补一条注入0 块 1 impl ⇒ 必须红。——这两跤是同一个形状判据的覆盖范围没量全就宣布了覆盖率纪律是凡要宣布「X 都在边界内」先量 X 有几种形态、各多少。这一篇的一句话没有显卡的时候「比对」是唯一的证据来源于是「比对本身可不可信」就成了工程的主线——判据要会失败、全绿要证明有信息量、缺口要登记、账本要能重放、质疑要拿命令回答。