一、CI/CD¶
| 中文名称 | 英文全称 | 简称 | 核心描述 | 车机NDK项目典型行为 |
|---|---|---|---|---|
| 持续集成 | Continuous Integration | CI | 代码提交/PR阶段自动构建、编译、静态扫描、单元测试;提前发现缺陷,做代码门禁 | PR触发:NDK编译、clang‑tidy、cppcheck、SonarQube、gtest单元测试;失败阻止合并 |
| 持续交付 | Continuous Delivery | CD | CI执行完成后,自动生成就绪的发布包;包已就绪,但人工手动触发发布,不自动上线 | 合入主分支后,自动编译固件/APK、归档so、符号文件、报告;交付测试团队,人工决定刷车机 |
| 持续部署 | Continuous Deployment | CD | 在持续交付基础上,完全自动化部署到目标环境,无人工卡点 | 车机项目几乎不用;常见于互联网后端服务,构建完成直接上线 |
AI coding 导致代码量大幅增加,如何做好代码质量管控? - 知乎
二、静态代码扫描¶
SonarQube:配置质量门禁,新增代码不允许突破阈值;存量旧代码不强制一次性改,但不允许继续恶化 Cppchek: 使用方法-知乎,轻量级代码复杂度和质量检测,需要配置脚本使用才能满足项目要求 clang-tidy: 嵌入式使用-github, NDK clang 原生
git钩子: clang-format(团队内要统一版本),代码复杂度检测前置 译器内置静态检查:-Wall -Wextra -Wpedantic -Wunused,把警告当错误 -Werror,CI 门禁拦截。
开源组件漏洞扫描
TODO 单元测试及测试覆盖率
三、大模型代码检测¶
当前主要使用claude code的pr-review-toolkit进行代码检查,
安装命令:/plugin install pr-review-toolkit@claude-plugins-official
说明详见pr-review-toolkit:模块化 PR 审查工具箱 · Claude 插件官方指南
规则补充:
- 只看新增代码,不看 cherry-pick
- 类名与函数名命名合理性
- 使用项目上的宏,特别是打印函数
- 过度注释检查
3.1 优先级定义和去重¶
收到所有 Agent 结果后,对问题进行去重,然后按以下规则映射到本技能的 P0/P1/P2 体系:
| Agent 原始严重度 | 映射规则 |
|---|---|
| code-reviewer: Critical (90-100) | → P0 |
| code-reviewer: Important (80-89) | → P1 |
| silent-failure-hunter: CRITICAL | → P0 |
| silent-failure-hunter: HIGH | → P1 |
| silent-failure-hunter: MEDIUM | → P2 |
| pr-test-analyzer: 9-10 分 | → P1(测试缺口不直接导致崩溃,但必须关注) |
| pr-test-analyzer: 5-8 分 | → P2 |
| type-design-analyzer: 评分 ≤ 4 的维度 | → P1 |
| type-design-analyzer: 评分 5-6 的维度 | → P2 |
| code-simplifier: 建议项 | → P2 |
| 级别 | 条件 | 数量策略 |
|---|---|---|
| P0 | 崩溃风险(确认)、内存泄漏(确认无释放路径)、严重性能问题、安全漏洞、功能缺失 | 不限 |
| P1 | 架构问题、性能优化、逻辑缺陷、代码质量(确认影响可维护性) | P0>10 时减少 50% |
| P2 | 代码复用机会、命名改进、细微优化、复杂度简化建议 | P0>10 时减少 70% |
| 类型 | 检查项 | 不提条件 | 优先级 |
|---|---|---|---|
| NPE | 调用方已校验? | 是 → 不提 | 确认 → P0 |
| 越界 | 上文已检查长度? | 是 → 不提 | 确认 → P0 |
| 资源泄漏 | finally 已关闭? | 是 → 不提 | 确认 → P0 |
| 性能 | 数据量? | <= 5 → 不提 | 确认卡死 → P0 |
| 线程 | 耗时? | < 1ms → 不提 | 确认卡顿 → P0 |
| 提交信息 | 符合 [TYPE\|MODULE\|LABEL]SUBJECT? |
是 → 不提 | 不符合 → P1 |
| 分支命名 | 符合 feature/MODULE/name 等规范? |
是 → 不提 | 不符合 → P1 |
| Bug修复 | 第二行包含 BugId:ID-desc? |
非纯数字ID → 不提 | 缺失 → P1 |
四、运行时检测¶
4.1 ASan¶
Address Sanitizer | Android NDK | Android 开发者 - 安卓文档
4.1.1 版本记录¶
- NDK 官方
asan文档页面(developer.android.com/ndk/guides/asan)是从 NDK r17 / API‑27 才正式发布,也就是wrap.sh机制正式落地的版本,r16b 比这个正式文档要早,属于实验性 ASan。 - NDK ≥ r27d,Android arm64 完整支持 LSan,detect_leaks 选项生效,可以输出 native 内存泄漏堆栈。r19‑r26 NDK 的
libclang_rt.asan‑aarch64‑android.so虽然编译进了 LSan 代码,但Android 平台 LSan 大量 bug、漏报、进程异常退出、detect_leaks行为错乱,属于半成品,不建议车载项目使用。
4.1.2 编译链接时¶
APP_STL := c++_shared # Or system, or none.
APP_CFLAGS := -fsanitize=address -fno-omit-frame-pointer
APP_LDFLAGS := -fsanitize=address```
-fomit‑frame‑pointer只是把 x29 帧指针寄存器回收做通用寄存器,破坏「帧指针链式回溯」;不等于彻底失去栈回溯能力。release版本默认这个行为。
Android aarch64 共享库(gnu_shared,libc++_shared)会生成 .eh_frame DWARF CFI unwind 表,ASan 可以走 CFI 慢回溯器(_Unwind_Backtrace libunwind),不靠 x29 帧指针也能还原调用栈。
CMake设置: set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
- -fno-omit-frame-pointer 是 GCC 编译器的一个选项,用于禁用帧指针优化,加上该参数,可以提升 AddressSanitizer 等内存检测工具的准确性,在初步了解阶段,本篇实例代码先不加此参数
--asan-redzone=16: 堆红区大小,默认值,可调整为 8/32 以平衡检测能力和内存开销。栈红区大小编译器内置(不可直接配置),通常为16。--asan-stack-trace-depth=1: 栈跟踪深度,减少错误报告的详细程度,降低性能影响-fsanitize-recover=address: 错误恢复模式编译选项,其核心作用是在检测到内存错误后不立即终止程序,而是允许程序继续运行,从而捕获更多潜在错误。
| 机制 | 保护目标 | 检测方式 | 调试支持 | 性能开销 | 适用场景 |
|---|---|---|---|---|---|
-fstack-protector-strong |
栈内存 | Canary 值检查 | 依赖帧指针(可选) | 中(约 1%-5%) | 默认启用,平衡安全性与性能 |
-fsanitize=address |
堆内存 | 运行时插桩检测 | 依赖帧指针(可选) | 高(约 2x) | 开发/测试阶段,不推荐生产环境 |
-fno-omit-frame-pointer |
调试可观测性 | 保留帧指针 | 提升调用栈准确性 | 低(通常 <1%) | 需调试或性能分析时推荐启用 |
4.1.3 运行时¶
运行前,先把libclang_rt.asan-aarch64-android.so推送到车机,设置LD_LIBRARY_PATH。
环境变量示例:
export ASAN_OPTIONS="halt_on_error=0 abort_on_error=0 log_to_report_runtime=1 symbolize=1"
export LSAN_OPTIONS="report_objects=1"
| 选项 | 取值 | 说明与原因 |
|---|---|---|
| halt_on_error | 0 | 遇到内存错误进程继续运行,一次运行收集全部ASAN问题;若为1(默认),第一个内存错误就停止进程,后续测试用例无法执行 |
| abort_on_error | 0 | 出错不直接abort退出,便于测试框架捕获进程退出码,做自动化结果判定 |
| log_to_report_runtime | 1 | 将asan检测报告输出到运行时日志,方便自动化抓取解析 |
| symbolize | 1 | 开启堆栈符号化;设备缺少llvm‑symbolizer工具时符号化会失败,但不影响内存错误检测能力 |
| detect_leaks | - | NDK r16b(clang‑5.0)Android arm64未移植LSan;配置该选项会输出detect_leaks is not supported on this platform并直接进程退出;NDK ≥r27d才完整支持LSan内存泄漏检测 |
| report_objects(LSAN) | 1 | LSan选项,输出泄漏对象详细信息,仅r27d+生效;r16b下LSan本身不可用,该选项无效 |
自动化测试场景使用halt_on_error=0,abort_on_error=0可以批量收集问题,但注意:部分严重堆损坏场景即使配置为0,asan仍会强制终止进程。
4.1.4 部分So插ASan、部分不插桩¶
| 场景 | 现象 | 判定 |
|---|---|---|
| free在未插桩so,UAF访问在插桩so | 报UAF,分配栈有?? ??:0 |
真实bug,堆栈残缺 |
| malloc/free/UAF全部在未插桩so | 直接SIGSEGV,无asan日志 | 漏报,不是错报 |
| 未插桩so踩坏堆元数据,插桩so触发报错 | 报UAF/corrupted‑heap,报错栈非根因 | 报错事件真实,位置错位 |
| 插桩so分配,未插桩so free | 报invalid‑free | 业务代码bug |
| 进程存在两份asan runtime | 随机报UAF/double‑free,现象诡异 | 配置错误,结果完全不可信 |
实践提示:自动化测试遇到UAF,不能只看报错栈;如果存在大量未插桩so,要警惕根因在其它未插桩模块的堆踩踏。
当套件中部分用asan编译部分没有的情况下,在避免有两份asan runtime的前提下,对于「未插桩 so 踩坏堆元数据,插桩 so 触发报错」问题,可以使用正向分析+报问题的代码块屏蔽的方式,如果正向未分析出异常且屏蔽后未出现,可能是踩坏的数据为影响正常插桩,可以尝试让助理压测。
A 库 ASan 插桩,A 调用未插桩 B 库,为什么能报出 B 库产生的 Use‑After‑Free: TODO
4.1.5 检测能力¶
4.1.6 注意事项¶
- 在使用 libc++_static 时,ASan 目前与 C++ 异常处理不兼容。使用 libc++_shared 或不使用异常处理的应用要么不受影响,要么有可用的变通方法。
- ASan 的 CPU 开销大约为 2 倍,代码大小开销在 50% 到 2 倍之间,且内存开销较大(取决于您的分配模式,大约为 2 倍)。
4.2 HwAsan¶
HWAddress Sanitizer | Android NDK | Android Developers - 安卓文档 TODO 暂未用过
4.2.1 完整对比表¶
| 对比项 | ASan | HWASan |
|---|---|---|
| 全称 | AddressSanitizer | Hardware‑Assisted AddressSanitizer |
| 支持平台 | aarch64、x86‑64;32位arm也支持 | 仅AArch64 64位ARM;不支持x86、32位arm |
| NDK最低版本 | r16b | r21;r16b完全不可用 |
| Android版本 | 老版本也支持 | Android10(API29)+ |
| CPU开销 | ~2x | ~2x |
| 代码体积膨胀 | 50%‑100% | 40‑50% |
| 内存开销 | ~200%(翻倍,内存压力大) | 10‑35%,远小于ASan |
| 检测能力 | 堆UAF、double‑free、heap‑overflow、stack‑use‑after‑scope ❌无法检测 stack‑use‑after‑return(函数返回后访问栈局部变量) | ASan全部能力 + ✅stack‑use‑after‑return(函数返回后栈变量使用) |
| UAF实现机制 | quarantine隔离队列;队列满后内存归还系统,会漏报UAF | 无隔离队列;free改写tag;存在1/256 tag碰撞漏报概率 |
| 堆越界检测 | 依赖redzone红区;溢出超过redzone大小会漏报 | 不需要redzone;任意大小越界都可以检测(tag不匹配即报错) |
| 误报 | 无误报 | 无误报;仅小概率tag碰撞漏报,不误报 |
| 运行时行为 | 一旦加载asan runtime,全进程malloc/free被拦截 | 一旦加载hwasan runtime,全进程malloc/free被拦截 |
| 部分so插桩场景 | 未插桩so内部内存访问漏报;分配释放可被runtime捕获 | 和ASan行为完全一致:未插桩so内部load/store不会检测;分配释放会被hwasan runtime捕获 |
| 官方状态 | Android arm64不再积极维护,遗留bug不修复 | Android官方首选内存错误检测工具 |
4.3 GWP‑ASan¶
缺点和优点的很明显。优点是不需要重新编译代码,可以检测闭源 so 堆错误,几乎无性能开销 (<5%)。缺点是概率采样,只能检测堆相关文件。
GWP‑ASan 最低要求 Android 11 (API‑30)。
- APK 应用:
AndroidManifest.xml设置android:gwpAsanMode="always" - 直接 adb shell 运行独立可执行文件:启动时环境变量
GWP_ASAN_ENABLE=always ./your_bin
使用场景:
- 排查第三方闭源预编译
.so的偶现堆内存破坏 - 整机 / 真机大样本测试,线上样本抓偶现野崩溃
- 不会崩溃、不会安装失败、不会影响业务逻辑;仅仅该配置被系统直接忽略,GWP‑ASan 完全不生效,应用正常跑。
- asan开启后,会覆盖GWP-ASan
✅可检测(堆相关)
- Use‑After‑Free
- Double‑Free / Invalid‑Free
- Heap‑Buffer‑Overflow / Underflow(堆越界读写)Huawei Dev...
❌不能检测
- 栈 UAF、栈越界(栈相关全部不处理)
- 内存泄漏(LSAN/HWASan 才做)
- 未初始化内存读取(Valgrind 强项)
- 没有采样命中的堆错误,直接漏报
4.4 valgrind¶
Valgrind 不需要源码、不需要编译插桩:整个进程指令集做动态翻译,进程内所有 so(无论第三方闭源 strip 后的 so)全部会被检测。
相对于HwAsan和Asan, 它能检测到未初始化内存。
4.5 watchdog¶
看门狗设计在避免pipeline卡死,UI卡死等场景可以认为是基础设计。不过多追述,很多硬件也自带看门狗功能。
五、升级¶
5.1 NDK维护¶
对于多团队维护一个SDK时,如果避免NDK版本冲突问题。对豆包分析出的几种方案进行评价:
- 统一基线:稳定性好,但周期长,而且要处理好老的量产项目为了修复bug而升级导致的兼容性问题。
- 进程隔离:一个套件一般不会有多个进程。进程使用的系统资源毕竟比较多。
- 包装层只做 C 风格 plain‑C 接口。
关于C接口设计和使用,需要注意:
- C++ 异常、STL 对象、C++ 内存堆、RTTI 全部禁止跨过 so 边界。(避免ABI差异)
- 接口函数必须被
extern "C"包裹,保证使用 C ABI。 - malloc和free的操作这必须同源。
- 同一个进程内,不能同时加载两个不同 NDK 版本编译的 libomp.so。同一个进程最多只有一个应用使用libomp.a,此时禁止使用libomp.so,否则环境变量冲突,进程启动时就蹦。禁止
dlclose()卸载带 libomp 依赖的 so。 - 信号处理:桥接 so 禁止修改进程全局 sigaction/signal 处理器,信号处理属于进程全局资源处理好接口可行见,详见「接口可见性」这篇文章。进程内部所有 so 共享一张全局符号表,inker 会在已经加载的 so 列表从上往下找同名符号,找到第一个匹配就直接绑定,不管这个符号来自哪个 so、哪个 NDK 版本
- JNI 边界:桥接 C 接口不导出 JNI 类型;JNIEnv 禁止跨线程传递;全局引用需要主动释放。
- 回调约束:回调内部产生的 C++ 异常必须全部在桥接层捕获,不向外透传。
- 同一进程内,同时存在两套不同 NDK 版本的 asan runtime.
注:上述内容可以作为接口设计者和使用者用大模型扫描的依据。
malloc和free的操作这必须同源的原因,
- Android 系统有两套堆来源:
a. 系统 libc (bionic) 的 malloc/free
b.
libc++_shared.so/gnustl_shared 内部自带的堆(老 gnustl 尤为明显) - ASan 会完全接管整个进程的 malloc/free/new/delete,全部替换为 asan runtime 的内存分配器。
5.2 开源协议¶
和稳定性无关,但如果不符合开源协议,要大改也会引入产品的稳定性问题。
个人通过关注协议来规避,企业通过开源组件 / 第三方组件漏洞扫描工具 来获取第三方组件漏洞,从而知道升级修复。
许可证义务仅在对外分发产品(装车/交付客户) 时触发;仅公司内部使用,无论什么协议均无需对外公开源码。
| 许可证类型 | 修改开源组件源码后对外分发 | 上层调用业务代码是否需要公开 | 关键义务&注意事项 | 典型组件 |
|---|---|---|---|---|
| MIT / BSD‑3‑Clause / Apache‑2.0(宽松协议) | 不需要公开你的修改代码 | ❌ 不需要公开 | 1.保留原始版权声明、LICENSE文件 2.Apache‑2.0需标注代码修改点、保留NOTICE文件 3.可闭源商用 | JSON‑cpp、curl、protobuf |
| BSD‑3‑Clause(Bionic libc) | 修改Bionic源码对外分发,仅需要公开Bionic本身修改源码;上层业务代码无需公开 | ❌ 不需要公开 | 1.无Copyleft传染;无论静态链接、动态链接Bionic,上层业务代码完全不用开源 2.二进制分发必须保留原始版权声明、LICENSE/NOTICE 3.可闭源商用;NDK编译的App/可执行文件链接系统Bionic不受协议传染 | Android Bionic libc/libm/libdl |
| LGPLv2.1 / LGPLv3(库弱Copyleft) | 仅当修改LGPL库本身源码时,需要公开该库的修改后源码;上层业务代码不用公开 | ❌ 不需要公开 | 1.动态链接独立.so、未修改库源码:仅标注许可证、提供库原始源码;固件签名锁死so存在Tivoization合规风险 2.修改库源码:对外提供修改后的LGPL库源码/补丁 3.静态链接:除库源码,还需要提供业务.o目标文件、链接脚本,供第三方重链接;闭源产品尽量规避静态链接 |
ffmpeg、glibc、Qt开源版 |
| GPLv2 / GPLv3(强Copyleft) | 分发衍生作品,整体衍生作品全部源码必须以GPL协议公开 | ✅ 会被传染,需要公开 | 1.静态链接风险极高;动态链接在商业闭源产品仍存在法律争议 2.建议采用独立进程+IPC/Binder通信做架构隔离,规避衍生作品风险 | Linux内核、GCC |
| AGPLv3 | 对外分发或对外提供网络服务,衍生作品全部源码必须公开 | ✅ 会被传染,需要公开 | 闭源产品尽量规避;网络接口调用即触发开源义务 | 旧版MongoDB |
六、崩溃解析¶
6.1 core dump¶
core dump是Linux 原生机制:当进程发生异常(段错误 SIGSEGV、非法指令 SIGILL、abort ()、SIGBUS 等),内核把该进程的完整内存镜像、寄存器、栈、库映射等信息写入磁盘,生成core文件,就是崩溃转储文件。文件体积巨大(和进程内存大小相当),手机端磁盘空间有限,Android 默认不直接使用原生 core dump。
linux下配置:
#开启核心转储,即不限制存储文件大小
ulimit -c unlimited
# 指定核心转储路径,程序崩溃时自动存储到这个路径下,wsl下无效
echo "/tmp/%e.core" | sudo tee /proc/sys/kernel/core_pattern
6.2 Android Tombstone¶
Tombstone(墓碑)是 Android 框架层实现的崩溃日志,用来记录 Native (C/C++) 进程崩溃(JNI、native 可执行程序)。
- 触发源:
debuggerd(Android 调试守护进程,Android 11 + 改为debuggerd64) - 当 native 进程收到崩溃信号,内核把进程挂起,通知
debuggerd;debuggerd 读取进程寄存器、调用栈、内存片段、线程信息,不输出完整 core 文件,只输出精简文本日志,保存到/data/tombstones/tombstone_0x。 - 文本格式,体积小,手机默认开启;只针对 Native 崩溃;Java 异常不会产生 tombstone。Java 崩溃输出在
main log。
6.3 MTK AEE¶
AEE(Advanced Exception Environment)是联发科 MTK 平台专属异常捕获框架,不是 AOSP 原生 Android 组件,只存在 MTK 芯片手机。
MTK 专门开发的一套异常处理系统,用来统一接管整机各类异常:
- 覆盖范围非常广:
- Native 进程崩溃(对应普通 Android 的 tombstone 场景)
- Kernel 内核 panic、hardware watchdog 硬件看门狗重启
- Modem 基带崩溃、系统卡死、NE、KE、HWRESET 等各类死机重启
- 工作逻辑:
- 内核 / 驱动 / 用户态发生异常,触发 AEE 模块;
- AEE 可以收集:tombstone、内核栈、lk 日志、modem 日志、部分 core 信息、系统 log;
- 输出的文件叫
KE/NE/HWRESET等异常 db 文件,存放在/data/aee_exp; - NE (Native Exception):用户态 native 程序崩溃,等价于标准 Android 生成 tombstone 的场景;
- KE (Kernel Exception):内核 panic,标准 Android 没有统一的保存机制。
重点:原生 AOSP 没有 AEE,只有 MTK 平台才有。高通平台有等价的
KDump。
6.4 gdb &&lldb¶
应用场景:死锁;core dump现场恢复;内核、驱动、裸机、硬件相关异常
示例:coredump的那些事:04.多线程程序的调试 - ToBrightmoon - 博客园
gdb 有两种调度模式:all‑stop(全部停止,默认) / non‑stop(非停止模式)。Android NDK gdb 默认是 all‑stop。
使用adb时,cmake记得调为debug,或者说编译器优化设置为-O0,否则指令优化,会导致watch和variable list显示异常。
NDK下使用gdb:
- host 端交叉 gdb 和设备端 gdbserver,必须取自同一个 NDK 包,不要混用不同 NDK 的 gdb/gdbserver;
- Android 系统版本本身不强制绑定 gdbserver 版本;gdbserver 是你 adb push 进去的,不是系统自带;
- NDK r24 及以后彻底移除 gdb,只能用LLDB。
vscode下可以很好的兼容NDK gdb的操作,只要让大模型配置好launch.json和task.json即可。
gdb 有attach模式,还有spawn模式,我们一般使用的是后者。
6.4.1 NDK gdb 命令行demo¶
如果要命令行形式,可以参考一下脚本:
#!/bin/bash
# spawn模式:gdbserver拉起 demoExe,非attach模式
# NDK r23b aarch64‑linux‑android‑gdb + gdbserver64
set -o pipefail
NDK_R23=/path/to/android-ndk-r23b
PORT=5039
DEST=/data/local/tmp/demo_env
LOG=/data/local/tmp/gdb_demo.log
##############################################################################
# 工具函数:资源清理收尾
##############################################################################
cleanup() {
echo ""
echo "===== doing cleanup ====="
# 杀掉demoExe残留进程
DEMO_PID_LIST=$(adb shell pidof demoExe 2>/dev/null)
if [ -n "${DEMO_PID_LIST}" ]; then
echo "kill leftover demoExe pid: ${DEMO_PID_LIST}"
adb shell kill ${DEMO_PID_LIST} 2>/dev/null
fi
# 杀掉残留gdbserver64
GS_PID_LIST=$(adb shell pidof gdbserver64 2>/dev/null)
if [ -n "${GS_PID_LIST}" ]; then
echo "kill leftover gdbserver64 pid: ${GS_PID_LIST}"
adb shell kill ${GS_PID_LIST} 2>/dev/null
fi
# 清除adb端口转发
adb forward --remove tcp:${PORT} 2>/dev/null
# 打印设备端日志
echo "===== device gdb log ====="
adb shell cat "${LOG}" 2>/dev/null
echo "=========================="
}
# trap捕获退出信号,无论正常退出/ctrl+c都会执行cleanup
trap cleanup EXIT SIGINT SIGTERM
##############################################################################
# 前置清理:防止上次残留进程占用端口
##############################################################################
echo "===== pre‑cleanup old resource ====="
cleanup
##############################################################################
# 1.推送gdbserver64到设备
##############################################################################
echo "push gdbserver64 ..."
adb push "${NDK_R23}/prebuilt/android-arm64/gdbserver/gdbserver64" /data/local/tmp/
adb shell chmod 755 /data/local/tmp/gdbserver64
##############################################################################
# 2.设备后台启动gdbserver spawn demoExe
# 使用nohup保证adb shell退出后gdbserver不被回收;原生&后台
##############################################################################
echo "start gdbserver on device, spawn demoExe ..."
inner_cmd="( cd ${DEST} && LD_LIBRARY_PATH=${DEST}/Lib nohup /data/local/tmp/gdbserver64 :${PORT} ./demoExe --arg1=val1 --arg2=val2 >${LOG} 2>&1 </dev/null ) &"
adb shell "${inner_cmd}"
##############################################################################
# 3.设置adb端口转发
##############################################################################
adb forward tcp:${PORT} tcp:${PORT}
##############################################################################
# 4.轮询等待demoExe进程创建,最大3s,10次*0.3s
##############################################################################
DEMO_PID=""
for i in {1..10}; do
RAW_OUT=$(adb shell pidof demoExe 2>/dev/null)
# 取第一个pid,忽略多进程场景多余pid
DEMO_PID=$(echo "${RAW_OUT}" | awk '{print $1}')
if [ -n "${DEMO_PID}" ]; then
echo "demoExe created, pid=${DEMO_PID}"
break
fi
sleep 0.3
done
if [ -z "${DEMO_PID}" ]; then
echo "ERROR: demoExe not started!"
adb shell cat "${LOG}"
exit 1
fi
##############################################################################
# 5.启动主机交叉gdb【交互式模式,人工操作gdb】
# 人工操作:target remote :5039; set solib‑search‑path ...; continue ...
# 用户手动quit退出gdb之后,脚本继续往下执行trap的cleanup
##############################################################################
echo ""
echo "=================================================="
echo "gdb starting, please manual operate gdb:"
echo " target remote :${PORT}"
echo " set solib-search-path ./obj/local/arm64-v8a"
echo " info inferior"
echo " info threads"
echo " continue"
echo "after your debug, input quit to exit gdb"
echo "=================================================="
echo ""
"${NDK_R23}/prebuilt/linux-x86_64/bin/aarch64-linux-android-gdb" ./demoExe
# gdb执行quit退出后,脚本走到末尾,触发trap → cleanup自动执行
echo ""
echo "gdb exited, will run cleanup automatically."
6.5 trace¶
NDK trace功能:
| 函数 | 需要 NDK | 需要 compileSdk | r16b + compileSdk=27 | r25 + compileSdk=33 |
|---|---|---|---|---|
ATrace_beginSection |
r11+ | ≥23 | ✅ | ✅ |
ATrace_endSection |
r11+ | ≥23 | ✅ | ✅ |
ATrace_isEnabled |
r11+ | ≥23 | ✅ | ✅ |
ATrace_setCounter |
r20+ | ≥29 | ❌ 头文件里没有 | ✅ |
ATrace_beginAsyncSection |
r20+ | ≥29 | ❌ 头文件里没有 | ✅ |
ATrace_endAsyncSection |
r20+ | ≥29 | ❌ 头文件里没有 | ✅ |
手机厂商安卓hal层C++代码的trace也类似。
解析使用处理: perfetto
6.6 方法论¶
从日志拆解出用户API的调用逻辑,尝试分析是否有资源无锁导致的竞争问题。
其他团队依赖: 没扫asan,又信息不足,要求提供扫asan的结果,测试代码覆盖率要求达80%以上。
客户回复下,虽然经常容易信息不足,但需要有实施动作,例如加日志等。
为了快速熟悉项目问题,提供大模型分析的索引,从git中对问题进行分类,并整理skill。
- C1 并发竞态:共享成员跨线程读写未保护 : 对象被业务线程与回调线程同时读写、同一变量在不同函数里用了不同的锁,等于没锁; 静态成员 / 全局变量无锁
- C2 生命周期与销毁时序:UAF、悬挂指针、析构顺序:引擎在实例已析构后仍回调,回调里访问
this、析构顺序错误、环形依赖、工作线程未detach(使用RAII规避)、浅拷贝深拷贝问题 - C3 空指针与未初始化(普通问题一般能被静态扫描出来,出现一般是失败后的连锁反应)
- C4 入参与协议校验缺失 :API开发过程中,未考虑全客户调用非法参数,或并发调用,很难一步优化到位,但可以通过接口乱调测试发现。
- C5 越界与整型溢出: 溢出类崩溃几乎都出现在长时稳定性测试中(跑够时长后数值才溢出),短测发现不了。
- C6 测试自身崩溃。
如果最终还是定位不到原因,也要保持好心态,这类问题解起来有时候也要缘分 😄
七、不可控但静态能扫出的问题¶
遇到过的问题,纯大模型总结生成报告,仅做纪录,不做口语化精简化编辑 。 NDK 修订历史记录 | Android NDK | Android Developers - 安卓文档
7.1 Android NDK 动态库未定义符号泄露问题¶
Android NDK 动态库未定义符号泄露问题
7.1.1 问题概述¶
基于 NDK 27d 静态编译的 libtest.so,存在未定义符号泄露问题。该库 DT_NEEDED 依赖列表中未声明 libc++_shared.so,但内部残留 __emutls_get_address 等未定义符号。Android 动态链接器不会限制仅解析 DT_NEEDED 内库的符号,会全局检索进程内已加载动态库匹配符号,引发隐蔽的兼容崩溃问题。
7.1.2 问题现象¶
- 异常场景:运行环境无
libc++_shared.so时,直接触发dlopen失败,报错cannot locate symbol "__emutls_get_address"; - 隐藏风险场景:运行环境存在低版本 NDK 20 的
libc++_shared.so时,链接器会静默匹配该旧库符号,dlopen成功,但因 ABI 版本不匹配,产生随机崩溃、功能异常等难以排查的隐性问题。
7.1.3 核心根因¶
- 编译侧:NDK 静态链接
libc++_static时,emutls(模拟线程本地存储)相关符号无法被静态链接器完全解析,残留为未定义符号,出现符号泄露; - 链接机制:静态编译 C++ 标准库不会将
libc++_shared.so写入 DT_NEEDED 依赖,导致库声明依赖与实际符号依赖不匹配; - 系统机制:Android 动态链接器采用全局符号检索规则,不校验符号来源合法性,极易触发版本错乱、符号寄生问题。
7.1.4 快速排查方法¶
- 查看库声明依赖(DT_NEEDED):确认无
libc++_shared依赖
- 检测泄露的未定义符号(核心校验):定位残留的 emutls 未定义符号
- 事前静态检测(核心,替代事后复现):编译后自动筛查非法未定义符号,提前发现符号泄露,无需运行复现
# 筛查静态编译 SO 中,不该存在的 C++/emutls 未定义符号,提前捕获符号泄露
llvm-objdump -T libtest.so | grep UND | grep -E "emutls|cxxabi"
检测规则说明:纯静态编译(-static-libstdc++)的 SO,不允许残留 emutls、cxxabi 相关未定义符号,筛查出结果即判定为符号泄露,可在编译阶段直接拦截问题,避免上线后隐性崩溃。
编译全局添加参数,关闭模拟 TLS,消除 __emutls_get_address 依赖,适配纯静态 C++ 库编译:
方案二:补齐动态依赖,规范链接关系
无法关闭 emutls 时,改为动态依赖 libc++_shared.so,让依赖写入 DT_NEEDED,确保符号解析可控、版本一致。
方案三:修正编译链接顺序
将 -static-libstdc++、静态库链接参数置于链接命令末尾,保证链接器完整解析所有静态符号。
7.1.5 问题总结¶
本次问题本质是静态编译残留未定义符号 + 动态链接全局检索机制共同导致的隐性兼容问题。核心风险为符号版本静默不匹配,极易引发线上随机崩溃。通过禁用 emutls 可彻底规避该类符号泄露,是 NDK 高版本静态编译 C++ 库的最优合规方案。
7.2 多SO混编静态/动态libomp依赖冲突问题¶
多 SO 混编静态/动态 libomp 依赖冲突问题
7.2.1 问题现象(精准校准)¶
同一 APP 进程存在两套 libomp 依赖场景:
- libtest1.so:动态链接 libomp.so(外部动态库)
- libtest2.so:静态链接 libomp.a(内置静态库)
现象:
- 两者 NDK 版本一致、且业务代码未执行 libtest2 的 omp 逻辑:进程正常不崩溃;
- 两者 NDK 版本不一致:即使代码不常驻,只要触发静态 omp 代码路径,极大概率崩溃;
- 双静态场景(两个 SO 都链接 libomp.a):100% 高危,必然运行时冲突崩溃;
- 使用 if 逻辑完全绕开静态 omp 调用:无代码执行路径,不会触发初始化与运行时,可临时规避崩溃(属于临时规避,非根治)。
7.2.2 核心原理¶
libomp 属于有全局状态、线程池、TLS、全局初始化/析构的运行时库,不支持多实例共存。
- 动态链接 libomp.so:进程全局单实例运行时,所有符号全局唯一
- 版本不一致时:静态库内置 omp 逻辑与动态库 ABI 不兼容,一旦走入静态 omp 代码,出现内存错乱、线程池双重初始化、double free、死锁、随机崩溃;
- 双静态链接:两个 SO 两份独立 omp 运行时,全局状态互相踩踏,必定崩溃。
7.2.3 if 分支规避是否有效?¶
结论:短期有效、可规避崩溃,但不推荐作为正式方案。
原理:只要完全无执行路径进入静态 omp 代码,omp 的全局初始化、线程创建、TLS 读写都不会触发,不存在多实例冲突,因此不会崩溃。
风险:业务迭代、分支逻辑变更、隐式调用、第三方代码间接调用,随时可能打破规避逻辑,触发隐性崩溃。
7.2.4 最终工程结论(核心规范)¶
进程内绝对不允许存在多套 libomp 运行时依赖,包括:
- 动态 omp + 不同版本静态 omp(高危隐性崩溃)
- 动态 omp + 同版本静态 omp(侥幸稳定,不规范)
- 多 SO 同时静态链接 omp(绝对禁止,必崩)
7.2.5 前置检测方案(编译阶段提前发现)¶
- 检查所有 SO 是否内置静态 omp 符号
- 检查是否依赖动态 omp
- CI 拦截规则:整进程所有 SO 只允许一种 omp 依赖形态:统一动态 or 单 SO 静态,禁止混编、禁止多静态
7.2.6 标准修复方案¶
最优规范:全局统一动态 libomp.so
所有业务 SO 统一依赖外部动态 omp,进程全局单实例,彻底杜绝多实例、版本 ABI 冲突。
兜底规范:仅允许单个 SO 静态链接 omp.a,其余 SO 禁止任何 omp 依赖
7.2.7 问题总结¶
- 你的现象描述完全合理:混编不同版本动态/静态 omp 存在隐性崩溃风险,if 绕开可临时规避;
- 核心风险不在于链接成功与否,而在于多 omp 运行时实例 + ABI 版本不兼容;
- 工程铁规:一个进程只能存在一套 libomp 运行时,严禁多形态、多版本混编依赖。
7.3 NDK 老版本编译指针截断/位数适配异常问题¶
NDK 老版本编译指针截断/位数适配异常问题报告
7.3.1 问题概述¶
使用低版本 NDK(NDK20 及以下)编译代码时,存在原生指针位数兼容缺陷,编译出的动态库在高版本 Android 系统、64 位设备上出现指针截断、指针高位丢失、非法指针访问问题。该问题属于 NDK 老旧编译器隐性 BUG,无编译报错、无链接报错,仅运行时随机崩溃、内存错乱、数据读写异常,排查难度极高。
7.3.2 核心问题现象¶
- 仅老版本 NDK 触发:NDK20 及以下编译产物存在问题,NDK21+ 高版本 NDK 编译同套代码完全正常;
- 64 位设备隐性异常:64 位进程运行时,64 位指针被截断为 32 位,指针高位数据丢失,导致指向非法内存地址;
- 崩溃特征:随机闪退、内存越界、野指针访问、结构体数据错乱,崩溃堆栈不固定、无规律复现;
- 无编译告警:编译、链接阶段无任何报错告警,属于编译器底层适配缺陷,非业务代码问题。
7.3.3 问题根因¶
- 老版本 Clang 编译器缺陷:NDK20 及以下内置 Clang 对 Android 64 位 ABI、长指针适配存在 BUG,部分场景下不会严格保留 64 位指针完整位数,会隐性截断高位 4 字节;
- 指针类型隐式转换不严谨:老编译器对
uint64_t/void*与 32 位整型的隐式转换校验缺失,自动降级截断指针高位; - 系统适配滞后:老旧 NDK 编译规则未适配高版本 Android 系统的 64 位内存寻址机制,高系统运行老旧编译产物时,指针寻址错位;
- 关键特征:代码逻辑完全无误,纯编译工具链版本导致的运行时内存异常。
7.3.4 精准排查方案(事前+事后)¶
- 快速定位特征排查(事后复盘)
满足以下 3 点即可判定为 NDK 老版本指针截断问题:
- 崩溃仅出现在 NDK20 及以下版本编译的产物;
- 64 位设备必现/随机异常,32 位设备运行正常;
-
业务代码无指针强转错误,升级高版本 NDK 后问题直接消失。
-
静态检测(编译阶段提前排查)
扫描代码中高危指针隐式转换(老 NDK 重点踩坑点):
排查所有 64 位指针赋值给 32 位整型、指针与 int 强转、指针低位截断代码:
// 高危代码(老 NDK 必触发指针异常)
int addr = (int)ptr; // 64 位指针强转 32 位,老编译器直接截断高位
uint32_t val = (uint32_t)buf; // 高位丢失,内存地址错乱
- 编译产物校验
对比高低版本 NDK 编译产物:
- NDK20 产物:存在指针位数压缩、地址截断指令;
-
NDK21+ 产物:完整保留 64 位指针寻址指令,无截断逻辑。
-
运行时日志排查
观察崩溃日志:若异常内存地址均为 32 位低位地址(0x0000FFFFxxxx 格式),64 位高位清零,即可确诊为指针截断问题。
7.3.5 解决方案¶
- 根治方案(推荐)
统一升级 NDK21 及以上稳定版本,彻底修复编译器 64 位指针截断 BUG,从工具链层面规避问题。
-
临时兼容方案(无法升级 NDK 时)
-
全员规范指针类型:禁止
void*↔int/uint32_t强转,统一使用uint64_t/int64_t承接指针地址; - 编译全局开启严格类型校验:杜绝隐式指针截断转换;
-
64 位编译模式下,强制关闭编译器低位优化,保留完整指针位数。
-
CI 拦截规范
禁止使用 NDK20 及以下老旧版本编译线上产物,统一管控编译工具链版本,规避工具链底层 BUG。
7.3.6 问题总结与工程规范¶
- 该问题为 NDK 老旧工具链固有 BUG,与业务代码无关,属于典型的编译阶段隐性风险,极难排查;
- 核心风险:64 位指针高位截断,导致内存寻址非法,触发随机内存崩溃;
- 工程铁规:线上 Release 产物禁止使用 NDK20 及以下老旧版本,统一使用新版稳定 NDK,规避编译器底层适配缺陷。
八、其他¶
项目活的越久,功能变动不大的情况下,就会越稳定,就是靠时间填出来的。如何看待很多“屎山”代码却异常稳定? - 知乎
设计:好的架构设计,对API的使用场景有深刻理解,输入边界控制, 稳定性,难的不是技术 - 技术琐话的文章 - 知乎
重构和设计实施方案前,一定要把代码改动量说明清楚,一开始领导们都认为改动量很小,你做完后改动量很大(即使你一开始预估也是那么大),他们担心稳定性问题不敢上,只能白忙活。在获取团队的信任前,自己不要在量产稳定的代码上做大的修改,能交给其他人就交给其他人。
