[Experience sharing] 一次神秘的二进制闪退:LLM不止是调试搭档, 还要和ta斗智斗勇
Tofloor
poster avatar
Ziggy
deepin
an hour ago
Author

前言

如果你做过Linux软件适配,一定经历过那种"毫无头绪"的时刻:程序跑起来,窗口没弹,控制台没输出,进程就没了。没有 Segmentation fault,没有 abort,没有 core dump——就像一个三无病人,你说"你哪里不舒服"?它不理你。

我在如意玲珑社区做应用适配时,就遇到了这样一个病例。一个 Electron 应用——uTools,在经过签名注入 section 并重新打包 deb 后,运行即静默终止。没有任何报错,没有任何线索。

更令人困惑的是:

  1. 原始二进制可以正常启动
  2. 通过其他容器化方案打包时,只要 不剥离调试符号,也能正常运行
  3. 原始二进制 不写入 section 直接打包 deb,同样能正常运行

三个线索像三条断头路,彼此矛盾。于是我决定——让 LLM 来帮忙。这不是一次"AI替我干活"的故事,而是一次"人给方向、AI做验证、来回迭代逼近真相"的协作实验。


第1章 谜题浮现:一个"没报错"的闪退

故事从一个简单的问题开始。

我把 uTools 的 deb 包解压、做了一些签名注入操作、重新打包,运行。然后——什么都没发生。没有窗口,没有 stderr,没有任何日志。echo $? 返回 1。

对于有过 debug 经验的人来说,exit(1)SIGSEGV 可怕得多。SIGSEGV 告诉你"我挂了,这是挂的地方";exit(1) 告诉你"我不高兴,但我不告诉你为什么"。

当时的我已经有一些已知线索:

  • 签名注入后二进制增加了 .sig section(PKCS7 格式的 X.509 数字证书)
  • 原始二进制在签名前是能跑的
  • 但到底是签名注入本身导致的,还是签名注入过程中有其他副作用?

我打开了 opencode,开始了第一轮尝试——让 DeepSeek V4 Pro 帮我看看。


第2章 弯路也是路:AI的三次失焦

Round 1:DeepSeek——"让我看看你的 libffmpeg"

和 LLM 协作的第一步通常是:你先描述问题,它给出一个分析计划,然后跑一堆命令。DeepSeek 拿到了 /usr/bin/utools 这个路径——等等,/usr/bin/utools 是一个指向 /opt/apps/u.utools.utools/files/uTools/utools 的符号链接。DeepSeek 立刻兴奋了:

"libffmpeg.so => not found!"

然后它开始写长篇大论:RPATH 问题、库缺失、加载器失败……但我知道 libffmpeg$ORIGIN 目录里,动态加载不会有问题。

人类第一次纠正:那不是问题,直接用 GDB。

DeepSeek 调头了,但调得不彻底。它开始分析 strace 输出,发现了 exit_group(1) 调用——"用户态代码调用了 exit",结论是"程序自己决定退出的"。这个结论没错,但和"为什么退出"差了十万八千里。

Round 2:Qwen——"让我看看你的 asar"

换 Qwen 3.5-122b 上。这次 AI 学乖了?不,它换了一个方向沉浸——"ASAR 完整性验证"。

Qwen 花了大量时间去解压 resources/app.asar,研究 Electron 的内嵌模块,分析 app-update.yml。它甚至发现了 leveldown 依赖的文件在 app.asar.unpacked 里不完整——"可能是 asar 提取失败导致运行崩溃!"

方向其实不对——原始二进制在没有这个缺失文件的情况下能跑,为什么签名版本就跑不了?但 AI 没有问自己这个问题。它沉浸在自己的推理链里,一路走到底。

人类第二次纠正:二进制运行就退出,跟缺失的 leveldown 文件没关系。

Round 3:GLM——"一定是窗口管理器的问题"

GLM-5 上场后,怀疑点变成了 DISPLAY 环境变量和 window manager。"XDG_SESSION_TYPE=tty,DISPLAY=localhost:11.0,这是个远程/SSH环境,没有图形界面——大概率是缺少 GUI 环境导致的 exit(1)。"

但已知线索明确写着"原始二进制在同一环境可以启动"。人类第三次纠正:"同一台机器,同一个环境。"

这三个回合下来,我产生了一个很深的感受:LLM 的"自信感"是一种幻觉。每个模型都能写出一篇逻辑自洽的技术分析,但只要初始假设偏离一点点,后面的推理就是千里之遥。它不是故意的——它只是不知道自己在犯错。

那是不是 LLM 就没用?恰恰相反。三次失焦的价值在于:每次错误的假设都排除了一个方向。排除法,是 debug 最重要的策略之一。

插章:多模型轮战观察

371 轮对话穿梭了 4 个模型,我整理了一个小观察笔记:

模型 擅长 踩坑
DeepSeek V4 Pro 快速全局扫描、生成实验计划 容易过早下结论,忽略已知上下文
Qwen 3.5-122b ELF 结构分析、符号表比对 陷入单一路径,反复纠结一个假设
GLM-5 反汇编能力强、V8 Snapshot 分析 对打包构建流程理解不足

没有完美的模型,但有互补的模型。交叉验证,是和人协作的重要方法论。但真正的转折点——不是来自模型,而是来自实验室。


第3章 人类的关键一手:实验台的价值

在 AI 跑了 100 多轮对话、贴了几十个 readelf 输出之后,我做了一个事:在 linyaps 包格式实验平台上亲手打包了一次

不是 pull request,不是按规范流程走——而是故意搞破坏。

我构建了两个版本:

  • binary.withDebugSymbols:保留所有调试符号
  • binary.withoutDebugSymbols:剥离调试符号

结果出来了:

有调试符号的 → 正常运行
剥离调试符号的 → 静默闪退

那一刻,前面所有的 .sig 签名分析、V8 Snapshot 校验分析、asar 完整性分析——全都需要翻盘。

之前我们一直以为是"签名注入"的问题,但实验证明:剥离 .symtab.strtab 这两个 section 才是根因。

而这个实验,AI 是做不了的——它没有物理环境,没有应用容器,没有 deb 构建工具链。它只能在已有的二进制文件上做静态分析。真正一刀切进核心的,是那个坐在电脑前、按下 Enter 键的人。

我把实验结果给了 GLM-5,说:"方向变了,不是签名,是符号。重新分析。"


第4章 AI的爆发:从45度行到全局图

方向一旦正确,AI 的价值就爆发了。

我给定了两个目录——一个包含调试符号,一个剥离了调试符号——让 GLM-5 做差异分析。以下是我看到的:

秒级对比 16980 个符号。 人类手动比对?一天。AI?几秒钟。

对比结果显示:

符号数量完全一致:16980
差异不在符号内容上

那差在哪?GLM 开始逐行排查 section 结构:

  • .symtab:被剥离(符号表,从有到无)
  • .strtab:被剥离(字符串表,从有到无)
  • .gnu_debuglink:两个版本都存在,但 CRC 值不同

然后 GLM 做了一件让我刮目相看的事——它反汇编了 V8 的 VerifyChecksum 函数。不是简单的符号解引用,而是给出了反汇编代码的具体逻辑:

_ZN2v88internal8Snapshot14VerifyChecksumEPKNS_11StartupDataE  (0x3a25bd0)
  ├─ 读取 snapshot_blob.bin 头部
  ├─ 调用 Checksum (0x3a256a0) 计算实际校验和
  ├─ 与 GetExpectedChecksum (0x34e2de0) 的结果比较
  └─ 不匹配 → PrintF 输出 → 进程退出

它还发现了 Electron Fuses 的值:0x1237ad0 位置,二进制值为 1011000。这个值告诉我们:ASAR 完整性验证是关闭的——所以 not asar 的问题。

然后它挖到了 .gnu_debuglink 的变更细节:

withDebugSymbols 的 .gnu_debuglink CRC: CRC31 (0xXXXXXXXX)
withoutDebugSymbols 的 .gnu_debuglink CRC: CRC32 (0xYYYYYYYY)

两个 CRC 算法和值都不同。这是剥离符号时,objcopy --only-keep-debug 重写了 .gnu_debuglink 的结果。

这就是 AI 的高光时刻。它从一个已知的目录结构和几十个反汇编函数里,理出了一张完整的校验流程图——V8 Snapshot → Electron Fuses → .gnu_debuglink CRC → 未知完整性校验的层级关系。人类可以在宏观方向上做判断,但要逐个去查反汇编里的 45 行代码、16980 个符号、十几个 section 的 CRC,那会是一个漫长到不敢开始的过程。

AI 不是决策者,它是微观分析的加速器。

但这时候,我们还没找到终极答案。因为上面的分析虽然好,但都是"可能"。我们还没能确认到底是哪个校验触发了闪退。《三体》里有句话:"弱小和无知不是生存的障碍,傲慢才是。" 但在二进制分析里,"不够精确的假设"才是。我们需要一个实验来锁死结论。


第5章 拼图的最后一角:Section完整性保护

故事的最后一转来自人类的一个新问题:

"等一下,我用 deb 版本也试了一下。deb 的 withoutDebugSymbols 两个目录 都有 16980 个符号,但 deb 版同样闪退。这说不通啊——如果根因是符号,那 deb 版符号没丢,为什么也闪退?"

AI 迅速对比了 deb 版的两个目录,发现:

目录 符号数 额外差异
withDebugSymbols 16980 .sig
withoutDebugSymbols 16980 .sig(12KB PKCS7 签名)

"withoutDebugSymbols" 这个命名具有误导性——它实际上没剥离符号,只是多了一个签名 section。

那 deb 版闪退的原因就变成了:添加 .sig section 导致了什么?

人类提出了一个假设性追问:"是否uTools根本不允许二进制文件发生 section 层面的变化?"

AI 拉了一张表:

版本 原始 sections 修改后 sections 差异方向 结果
linyaps 38 36 -2(移除 .symtab, .strtab 闪退
deb 38 39 +1(添加 .sig 闪退

这不是巧合。

两个完全不同的打包方案、两个完全不同的修改方式(一个是删 section,一个是加 section),结果一模一样:静默闪退

共同点只有一个:section 数量/结构变化了

结论:uTools 的主二进制存在 ELF Section 头表完整性保护机制。无论增减,section 数量一旦偏离 38,程序即触发静默退出。

回顾前面的分析,这个校验点大概率不在 V8、不在 Electron、不在 ASAR——它位于更底层,可能是 ELF 加载阶段的自检代码,在 main 函数执行前就已经完成了完整性判断。

同时这也解释了为什么前几轮 AI 的分析总是差一点:因为我们一直在查 V8 Snapshot 校验、查 Electron Fuses、查 bytecode checksum——通通是"上层机制"。而真正的保护位于一个更隐蔽的层次:ELF Section 头表本身


第6章 技术深水区:我们排除了什么

这份排除了的清单,本身就是一套完整的二进制逆向分析 checkList:

排除项 原因
V8 Snapshot 校验和 snapshot_blob.binv8_context_snapshot.bin 在两个版本中大小和内容完全一致
Electron Fuses 1011000 明确显示 ASAR 完整性验证关闭,IsOnlyLoadAppFromAsar 也是关闭
Bytecode checksum resources.pak 在两个版本间完全一致
policy-integrity 二进制中虽然有相关符号,但未找到触发条件
.gnu_debuglink CRC 虽然 CRC 变了,但 V8 VerifyChecksum 校验的是 snapshot blob,不是二进制本身
BuildID 两个版本完全一致
.sig 签名本身 linyaps 版本没有签名一样闪退

如果你在分析一个 Electron 二进制闪退问题,这个单子可以帮你直接跳过一大半假设。


第7章 协作复盘:谁做了什么

回看整个 371 轮对话(大约 880 万 token)的调试过程,人和 AI 的角色非常清晰:

人类贡献了什么?

  1. 实验台构造:linyaps 和 deb 两套构建环境的搭建,产生可观测的实验数据
  2. 方向纠偏:三次纠正 AI 的错误方向(seccomp → GTK → 窗口管理器 → 符号)
  3. 假设提出:"是否不允许 section 数量变化?"——这最关键的一句,把整个故事推进到终点

AI 贡献了什么?

  1. 秒级全局扫描:系统调用跟踪、符号表比对、ELF section 结构分析
  2. 知识库实时检索:GDB 调试命令、V8 内部实现、Electron Fuses 格式、PKCS7 签名结构
  3. 反汇编与归纳:45 行反汇编 → 全局校验架构图
  4. 数据验证:52 条假设 → 交叉验证 → 排除清单
  5. 报告生成:结构化输出,方便复盘和归档

协作节奏

人类给方向 ──→ AI 做分析 ──→ AI 给结论 ──→ 人类提出质疑
    ↑                                            │
    └──────────────── 下一个假设 ──────────────┘

螺旋式逼近。每次循环排除一批可能性,最终收敛于真相。


第8章 与LLM协作的反思

1. 不要当"提示词工程师",要当"指挥官"

很多人觉得用 LLM debug 就是写好 prompt,然后躺平等结果。这不对。AI 需要的是你告诉它"什么值得看、什么可以忽略"。

比如第二次 Qwen 沉迷 asar 分析时,我如果不说"方向不对",它可能会继续分析 50 轮 asar 的结构。你是那个说"这里停一下,往那边看"的人。

2. AI 的"自信感"是一道门槛

DeepSeek 说"libffmpeg 缺失"的时候语气非常肯定。Qwen 说"asar 完整性验证导致闪退"的时候也非常肯定。GLM 说"窗口管理器问题"的时候同样肯定。

每个模型都能写出一篇逻辑自洽的分析,但正确的概率并不高。你必须去验证,而不是接受。

3. 交叉验证的价值

371 轮对话穿梭了 4 个模型。如果只用一个模型,很多错误方向不会被发现。多模型有一个隐藏的好处:当两个独立的模型给出相同的结论时,置信度会大幅提升。反之,当一个模型推翻另一个模型的结论时——那比单模型的任何输出都更有信息量。

4. AI + 人类的不可替代组合

AI 不可替代的

  • 微观分析速度(秒级扫描 16980 个符号)
  • 知识广度和检索深度(V8 反汇编、Electron Fuses 格式、PKCS7)
  • 结构化报告生成

人类不可替代的

  • 实验直觉和方向判断
  • 动手能力(构建环境、执行实验)
  • 对"异常现象"的敏感——那把最后一句"是不是不允许 section 变化"问出来

5. 一条实际的建议

如果你打算用 LLM 帮你分析类似的二进制问题,不要期待它从头到尾替你 debug。你应该这样做:

  1. 自己先跑一次基础环境验证(能不能复现?)
  2. 用自己的经验给出前三个假设方向(不要等着 AI 帮你找方向)
  3. 让 AI 去验证每一个假设(反汇编、符号表、文件对比)
  4. 对 AI 的输出保持怀疑,用实验证伪(不要听它的结论,要看它的原始数据)
  5. 交叉验证(换一个模型,或者让它换一个角度重新分析同样的数据)

尾声

这个故事最终找到了结论:uTools 存在 ELF Section 头表完整性保护,section 数量偏离 38 即触发静默退出。但比结论更重要的是一路上的协作方式——AI 加速了每一步微观分析,而人类提供了每一步的方向和实验验证。

这不是"AI 替我 debug"的故事,而是"AI 和我一起 debug"的故事。最好的 debug 不是 AI 替你思考,而是 AI 帮你加速思考。就像你在走廊的这头喊一声"那边有没有问题?",AI 瞬间跑过去看,跑回来告诉你"有,是这个"。但决定"要看哪边"的那个人,还是你。


本文为如意玲珑社区技术博客系列。事实验证来源:uTools-debug-end.md(371 轮 AI 对话,约 880 万 token 输入),utools-debug-1.md(首轮分析记录,约 100 万 token)。所有 GDB 命令、section 数量、fuse 值、符号数量均为真实实验数据。

Reply Favorite View the author
All Replies
avatar
骑🐖追帅哥bot
Moderator
7 minutes ago
#1

人类提供直觉和方向,AI 提供算力和微观扫描

Reply View the author
avatar
神末shenmo
deepin
Spark-App
Q&A Team
3 minutes ago
#2

最后还是失败了~shamed

Reply View the author