跳转至

一、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

四、运行时检测

代码稳定性_260821_112205.png

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)

  1. APK 应用:AndroidManifest.xml 设置 android:gwpAsanMode="always"
  2. 直接 adb shell 运行独立可执行文件:启动时环境变量 GWP_ASAN_ENABLE=always ./your_bin

使用场景:

  1. 排查第三方闭源预编译 .so 的偶现堆内存破坏
  2. 整机 / 真机大样本测试,线上样本抓偶现野崩溃
  3. 不会崩溃、不会安装失败、不会影响业务逻辑;仅仅该配置被系统直接忽略,GWP‑ASan 完全不生效,应用正常跑。
  4. 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版本冲突问题。对豆包分析出的几种方案进行评价:

  1. 统一基线:稳定性好,但周期长,而且要处理好老的量产项目为了修复bug而升级导致的兼容性问题。
  2. 进程隔离:一个套件一般不会有多个进程。进程使用的系统资源毕竟比较多。
  3. 包装层只做 C 风格 plain‑C 接口

关于C接口设计和使用,需要注意:

  1. C++ 异常、STL 对象、C++ 内存堆、RTTI 全部禁止跨过 so 边界。(避免ABI差异)
  2. 接口函数必须被extern "C"包裹,保证使用 C ABI。
  3. malloc和free的操作这必须同源。
  4. 同一个进程内,不能同时加载两个不同 NDK 版本编译的 libomp.so。同一个进程最多只有一个应用使用libomp.a,此时禁止使用libomp.so,否则环境变量冲突,进程启动时就蹦。禁止dlclose()卸载带 libomp 依赖的 so。
  5. 信号处理:桥接 so 禁止修改进程全局 sigaction/signal 处理器,信号处理属于进程全局资源处理好接口可行见,详见「接口可见性」这篇文章。进程内部所有 so 共享一张全局符号表,inker 会在已经加载的 so 列表从上往下找同名符号,找到第一个匹配就直接绑定,不管这个符号来自哪个 so、哪个 NDK 版本
  6. JNI 边界:桥接 C 接口不导出 JNI 类型;JNIEnv 禁止跨线程传递;全局引用需要主动释放。
  7. 回调约束:回调内部产生的 C++ 异常必须全部在桥接层捕获,不向外透传。
  8. 同一进程内,同时存在两套不同 NDK 版本的 asan runtime.

注:上述内容可以作为接口设计者和使用者用大模型扫描的依据。

malloc和free的操作这必须同源的原因,

  1. Android 系统有两套堆来源: a. 系统 libc (bionic) 的 malloc/free b. libc++_shared.so /gnustl_shared 内部自带的堆(老 gnustl 尤为明显)
  2. 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 dumpLinux 原生机制:当进程发生异常(段错误 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 专门开发的一套异常处理系统,用来统一接管整机各类异常:

  1. 覆盖范围非常广:
  2. Native 进程崩溃(对应普通 Android 的 tombstone 场景)
  3. Kernel 内核 panic、hardware watchdog 硬件看门狗重启
  4. Modem 基带崩溃、系统卡死、NE、KE、HWRESET 等各类死机重启
  5. 工作逻辑:
  6. 内核 / 驱动 / 用户态发生异常,触发 AEE 模块;
  7. AEE 可以收集:tombstone、内核栈、lk 日志、modem 日志、部分 core 信息、系统 log;
  8. 输出的文件叫KE/NE/HWRESET等异常 db 文件,存放在/data/aee_exp
  9. NE (Native Exception):用户态 native 程序崩溃,等价于标准 Android 生成 tombstone 的场景;
  10. 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:

  1. host 端交叉 gdb 和设备端 gdbserver,必须取自同一个 NDK 包,不要混用不同 NDK 的 gdb/gdbserver;
  2. Android 系统版本本身不强制绑定 gdbserver 版本;gdbserver 是你 adb push 进去的,不是系统自带;
  3. 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 问题现象

  1. 异常场景:运行环境无 libc++_shared.so 时,直接触发 dlopen 失败,报错 cannot locate symbol "__emutls_get_address"
  2. 隐藏风险场景:运行环境存在低版本 NDK 20 的 libc++_shared.so 时,链接器会静默匹配该旧库符号,dlopen 成功,但因 ABI 版本不匹配,产生随机崩溃、功能异常等难以排查的隐性问题。

7.1.3 核心根因

  1. 编译侧:NDK 静态链接 libc++_static 时,emutls(模拟线程本地存储)相关符号无法被静态链接器完全解析,残留为未定义符号,出现符号泄露;
  2. 链接机制:静态编译 C++ 标准库不会将 libc++_shared.so 写入 DT_NEEDED 依赖,导致库声明依赖与实际符号依赖不匹配;
  3. 系统机制:Android 动态链接器采用全局符号检索规则,不校验符号来源合法性,极易触发版本错乱、符号寄生问题。

7.1.4 快速排查方法

  1. 查看库声明依赖(DT_NEEDED):确认无 libc++_shared 依赖
llvm-readelf -d libtest.so | grep NEEDED
  1. 检测泄露的未定义符号(核心校验):定位残留的 emutls 未定义符号
llvm-objdump -T libtest.so | grep UND
  1. 事前静态检测(核心,替代事后复现):编译后自动筛查非法未定义符号,提前发现符号泄露,无需运行复现
# 筛查静态编译 SO 中,不该存在的 C++/emutls 未定义符号,提前捕获符号泄露
llvm-objdump -T libtest.so | grep UND | grep -E "emutls|cxxabi"

检测规则说明:纯静态编译(-static-libstdc++)的 SO,不允许残留 emutls、cxxabi 相关未定义符号,筛查出结果即判定为符号泄露,可在编译阶段直接拦截问题,避免上线后隐性崩溃。

编译全局添加参数,关闭模拟 TLS,消除 __emutls_get_address 依赖,适配纯静态 C++ 库编译:

target_compile_options(xxx PRIVATE -fno-emutls)

方案二:补齐动态依赖,规范链接关系

无法关闭 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(内置静态库)

现象:

  1. 两者 NDK 版本一致、且业务代码未执行 libtest2 的 omp 逻辑:进程正常不崩溃;
  2. 两者 NDK 版本不一致:即使代码不常驻,只要触发静态 omp 代码路径,极大概率崩溃;
  3. 双静态场景(两个 SO 都链接 libomp.a):100% 高危,必然运行时冲突崩溃;
  4. 使用 if 逻辑完全绕开静态 omp 调用:无代码执行路径,不会触发初始化与运行时,可临时规避崩溃(属于临时规避,非根治)。

7.2.2 核心原理

libomp 属于有全局状态、线程池、TLS、全局初始化/析构的运行时库,不支持多实例共存。

  1. 动态链接 libomp.so:进程全局单实例运行时,所有符号全局唯一
  2. 版本不一致时:静态库内置 omp 逻辑与动态库 ABI 不兼容,一旦走入静态 omp 代码,出现内存错乱、线程池双重初始化、double free、死锁、随机崩溃;
  3. 双静态链接:两个 SO 两份独立 omp 运行时,全局状态互相踩踏,必定崩溃。

7.2.3 if 分支规避是否有效?

结论:短期有效、可规避崩溃,但不推荐作为正式方案。

原理:只要完全无执行路径进入静态 omp 代码,omp 的全局初始化、线程创建、TLS 读写都不会触发,不存在多实例冲突,因此不会崩溃。

风险:业务迭代、分支逻辑变更、隐式调用、第三方代码间接调用,随时可能打破规避逻辑,触发隐性崩溃。

7.2.4 最终工程结论(核心规范)

进程内绝对不允许存在多套 libomp 运行时依赖,包括:

  • 动态 omp + 不同版本静态 omp(高危隐性崩溃)
  • 动态 omp + 同版本静态 omp(侥幸稳定,不规范)
  • 多 SO 同时静态链接 omp(绝对禁止,必崩)

7.2.5 前置检测方案(编译阶段提前发现)

  1. 检查所有 SO 是否内置静态 omp 符号
llvm-objdump -T xxx.so | grep -i omp
  1. 检查是否依赖动态 omp
llvm-readelf -d xxx.so | grep NEEDED | grep omp
  1. CI 拦截规则:整进程所有 SO 只允许一种 omp 依赖形态:统一动态 or 单 SO 静态,禁止混编、禁止多静态

7.2.6 标准修复方案

最优规范:全局统一动态 libomp.so

所有业务 SO 统一依赖外部动态 omp,进程全局单实例,彻底杜绝多实例、版本 ABI 冲突。

兜底规范:仅允许单个 SO 静态链接 omp.a,其余 SO 禁止任何 omp 依赖

7.2.7 问题总结

  1. 你的现象描述完全合理:混编不同版本动态/静态 omp 存在隐性崩溃风险,if 绕开可临时规避;
  2. 核心风险不在于链接成功与否,而在于多 omp 运行时实例 + ABI 版本不兼容;
  3. 工程铁规:一个进程只能存在一套 libomp 运行时,严禁多形态、多版本混编依赖。

7.3 NDK 老版本编译指针截断/位数适配异常问题

NDK 老版本编译指针截断/位数适配异常问题报告

7.3.1 问题概述

使用低版本 NDK(NDK20 及以下)编译代码时,存在原生指针位数兼容缺陷,编译出的动态库在高版本 Android 系统、64 位设备上出现指针截断、指针高位丢失、非法指针访问问题。该问题属于 NDK 老旧编译器隐性 BUG,无编译报错、无链接报错,仅运行时随机崩溃、内存错乱、数据读写异常,排查难度极高。

7.3.2 核心问题现象

  1. 仅老版本 NDK 触发:NDK20 及以下编译产物存在问题,NDK21+ 高版本 NDK 编译同套代码完全正常;
  2. 64 位设备隐性异常:64 位进程运行时,64 位指针被截断为 32 位,指针高位数据丢失,导致指向非法内存地址;
  3. 崩溃特征:随机闪退、内存越界、野指针访问、结构体数据错乱,崩溃堆栈不固定、无规律复现;
  4. 无编译告警:编译、链接阶段无任何报错告警,属于编译器底层适配缺陷,非业务代码问题。

7.3.3 问题根因

  1. 老版本 Clang 编译器缺陷:NDK20 及以下内置 Clang 对 Android 64 位 ABI、长指针适配存在 BUG,部分场景下不会严格保留 64 位指针完整位数,会隐性截断高位 4 字节;
  2. 指针类型隐式转换不严谨:老编译器对 uint64_t/void* 与 32 位整型的隐式转换校验缺失,自动降级截断指针高位;
  3. 系统适配滞后:老旧 NDK 编译规则未适配高版本 Android 系统的 64 位内存寻址机制,高系统运行老旧编译产物时,指针寻址错位;
  4. 关键特征:代码逻辑完全无误,纯编译工具链版本导致的运行时内存异常。

7.3.4 精准排查方案(事前+事后)

  1. 快速定位特征排查(事后复盘)

满足以下 3 点即可判定为 NDK 老版本指针截断问题:

  • 崩溃仅出现在 NDK20 及以下版本编译的产物;
  • 64 位设备必现/随机异常,32 位设备运行正常;
  • 业务代码无指针强转错误,升级高版本 NDK 后问题直接消失。

  • 静态检测(编译阶段提前排查)

扫描代码中高危指针隐式转换(老 NDK 重点踩坑点):

排查所有 64 位指针赋值给 32 位整型、指针与 int 强转、指针低位截断代码:

// 高危代码(老 NDK 必触发指针异常)
int addr = (int)ptr;          // 64 位指针强转 32 位,老编译器直接截断高位
uint32_t val = (uint32_t)buf; // 高位丢失,内存地址错乱
  1. 编译产物校验

对比高低版本 NDK 编译产物:

  • NDK20 产物:存在指针位数压缩、地址截断指令;
  • NDK21+ 产物:完整保留 64 位指针寻址指令,无截断逻辑。

  • 运行时日志排查

观察崩溃日志:若异常内存地址均为 32 位低位地址(0x0000FFFFxxxx 格式),64 位高位清零,即可确诊为指针截断问题。

7.3.5 解决方案

  1. 根治方案(推荐)

统一升级 NDK21 及以上稳定版本,彻底修复编译器 64 位指针截断 BUG,从工具链层面规避问题。

  1. 临时兼容方案(无法升级 NDK 时)

  2. 全员规范指针类型:禁止 void*int/uint32_t 强转,统一使用 uint64_t/int64_t 承接指针地址;

  3. 编译全局开启严格类型校验:杜绝隐式指针截断转换;
  4. 64 位编译模式下,强制关闭编译器低位优化,保留完整指针位数。

  5. CI 拦截规范

禁止使用 NDK20 及以下老旧版本编译线上产物,统一管控编译工具链版本,规避工具链底层 BUG。

7.3.6 问题总结与工程规范

  1. 该问题为 NDK 老旧工具链固有 BUG,与业务代码无关,属于典型的编译阶段隐性风险,极难排查;
  2. 核心风险:64 位指针高位截断,导致内存寻址非法,触发随机内存崩溃;
  3. 工程铁规:线上 Release 产物禁止使用 NDK20 及以下老旧版本,统一使用新版稳定 NDK,规避编译器底层适配缺陷。

八、其他

项目活的越久,功能变动不大的情况下,就会越稳定,就是靠时间填出来的。如何看待很多“屎山”代码却异常稳定? - 知乎

设计:好的架构设计,对API的使用场景有深刻理解,输入边界控制, 稳定性,难的不是技术 - 技术琐话的文章 - 知乎

重构和设计实施方案前,一定要把代码改动量说明清楚,一开始领导们都认为改动量很小,你做完后改动量很大(即使你一开始预估也是那么大),他们担心稳定性问题不敢上,只能白忙活。在获取团队的信任前,自己不要在量产稳定的代码上做大的修改,能交给其他人就交给其他人。