把 Google Breakpad 搬上鸿蒙 PC:崩析台(Minidump 分析台)移植实战

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_Breakpad

本文记录把一个"底层库"的开源 C++ 项目 —— Google Breakpad —— 做成鸿蒙 PC 上可安装、可运行、可交互的桌面应用的完整过程。从源码侦察、OHOS NDK 兼容性体检、架构选型,到 CMake 交叉编译、NAPI 进程内化、ArkTS 工作台 UI、签名与真机效果,所有命令、报错与补丁都来自实际工程,不是概念演示。

工程:harmony-pc/ohos_Breakpad 产物:entry-default-signed.hap(约 17.6 MB) 目标设备:HarmonyOS PC / 2in1,API 21+,aarch64


在这里插入图片描述

0. 成果速览

最终交付的是一个叫「崩析台」的深色工作台应用:左侧导航(工作台 / 符号库 / 关于),顶部工具栏,四张指标卡,中间是"数据源 + STACK TRACE"分栏,底部状态栏。它能:

  • 解析 Breakpad / Crashpad 格式的 .dmp,输出崩溃原因、崩溃地址、每个线程的符号化调用栈、缺符号模块清单;
  • 从带调试信息的 ELF(.so)里提取 Breakpad .sym 符号文件;
  • 按 Breakpad 官方目录规范归档符号,导入外部 .sym;
  • 把报告导出成 txt;
  • 内置一个上游测试转储 + 两个符号文件,完全离线即可自测解析内核。

三张真机截图分别对应工作台、符号库和关于页,后文第 8 章逐图说明。先放主界面:

在这里插入图片描述

一句话架构:把 Breakpad 的 processor 与 dump_syms 静态链进一个 NAPI .so,ArkTS 用文件选择器拿到路径后在进程内直接调用;不启子进程、不 exec、不 ptrace、不申请任何敏感权限。


1. 立项:先想清楚"移植的到底是什么"

Breakpad 的 README 第一句就写得很清楚:

Breakpad is a set of client and server components which implement a crash-reporting system.

它不是带窗口的软件,而是一套崩溃报告系统的库与工具集。源码浅克隆下来 929 个文件,可以切成三块:

组件位置产物系统依赖
客户端 clientsrc/client/linux/libbreakpad_client.a信号、clone、ptrace、/proc、直接 syscall
解析端 processorsrc/processor/libbreakpad.a只读文件,纯 C++,几乎无系统依赖
工具 toolssrc/tools/linux/dump_syms 等dump_syms、minidump_stackwalkELF/DWARF、zlib;上传工具依赖网络

所以第一步不是"编译",而是定义产品形态:鸿蒙 PC 上要的是一个能双击打开的东西,不是一个 .a。可选形态有三:

  1. 命令行工具集:把 minidump_stackwalk、dump_syms 交叉编译成 ELF,打成 HNP 给开发者用;
  2. 崩溃采集 SDK:把 libbreakpad_client.a 编出来,给别的鸿蒙原生工程集成;
  3. 桌面分析器:把解析端 + 符号工具做成 GUI 应用,用户拿 .dmp 和 .so 来看崩溃栈。

第 1 条在鸿蒙 HAP 里几乎是死路。此前多个移植工程已经踩实三条硬约束:

  • HAP 应用没有 execv,HNP 里的二进制带有 hnp_file SELinux 标签,应用拉不起来;
  • filesDir 以 noexec 挂载,把 ELF 拷进沙箱再执行同样不行;
  • 普通应用也拿不到 PTY(posix_openpt 直接 EACCES),终端模拟器那条路要另做静态链。

第 2 条是真实需求,但风险集中在 Linux 客户端的信号 / ptrace 链路(第 10 章展开),不适合作为第一版。于是 P0 选第 3 条:离线解析端 GUI 化。理由是它复用了 Breakpad 最有价值的 C++ 代码(minidump 解析、栈回溯、DWARF 符号化),却完全不需要碰那些在鸿蒙沙箱里最不确定的系统调用。

这条思路和此前把 Apache「进程内化」进 NAPI .so 的 XAMPP 移植、把 PuTTY 编成静态库再改 main 符号的 MTPuTTY 移植是同一套:既然不能起进程,就把程序变成函数。


2. 源码侦察:把构建系统和平台分支摸清楚

2.1 构建系统是 autotools,而且有隐藏依赖

上游用 configure.ac + Makefile.am,并且要求 C++20:

# configure.ac
AX_CXX_COMPILE_STDCXX(20, , mandatory)
AC_CHECK_FUNCS([arc4random getcontext getrandom memfd_create])
AM_CONDITIONAL([HAVE_GETCONTEXT], [test "x$ac_cv_func_getcontext" = xyes])

# 只有这些 host 才编 Linux 客户端
case $host in
  *-*-linux* | *-android* ) LINUX_HOST=true ;;
esac
case $host in
  *-android*) ANDROID_HOST=true ;;
esac

-Werror 也默认打开:

WARN_CXXFLAGS = ... -Wmissing-braces -Wnon-virtual-dtor -Woverloaded-virtual -Werror

结论很直接:Windows 宿主机上让 configure 去探测 OHOS 交叉工具链,性价比极低。后面我们自己写 CMakeLists,只按 Makefile.am 的源码清单建库。

Makefile.am 里三组清单是裁剪依据:

# 解析端:334~465 行
src_libbreakpad_a_SOURCES = \
    src/processor/basic_code_modules.cc \
    src/processor/basic_source_line_resolver.cc \
    ... \
    src/processor/stackwalker_arm64.cc \
    src/processor/tokenize.cc

# 客户端:497~537 行
src_client_linux_libbreakpad_client_a_SOURCES = \
    src/client/linux/handler/exception_handler.cc \
    src/client/linux/minidump_writer/linux_ptrace_dumper.cc \
    ... \
    src/common/linux/elfutils.cc

# dump_syms:673~704 行
src_tools_linux_dump_syms_dump_syms_SOURCES = \
    src/common/dwarf_cfi_to_module.cc \
    src/common/dwarf/bytereader.cc \
    src/common/linux/dump_symbols.cc \
    ... \
    src/tools/linux/dump_syms/dump_syms.cc
src_tools_linux_dump_syms_dump_syms_LDADD = -lz

还有个容易漏的坑:DEPS 里两个 git submodule:

deps = {
  "src/src/testing": "https://github.com/google/googletest.git@6910c9d...",
  "src/src/third_party/lss":
    "https://chromium.googlesource.com/linux-syscall-support/@29164a8...",
}

src/third_party/lss/linux_syscall_support.h 是客户端的信号安全 syscall 封装,浅克隆 GitHub 之后这个目录是空的;本机 chromium.googlesource.com 直接连不上,最后用 GitHub 镜像 d-ronin/breakpad-lss 补齐(里面确认有完整的 __aarch64__ syscall 实现)。gtest 只给单元测试用,P0 不需要。

2.2 Linux 客户端到底依赖什么

看懂这段,就知道为什么"先别碰采集端":

// src/client/linux/handler/exception_handler.cc
const int kExceptionSignals[] = { SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS, SIGTRAP };
// sigaction(SA_ONSTACK | SA_SIGINFO) + sigaltstack

const pid_t child = sys_clone(
    ThreadEntry, stack, CLONE_FS | CLONE_UNTRACED, &thread_arg, nullptr,
    nullptr, nullptr);
...
sys_prctl(PR_SET_PTRACER, child, 0, 0, 0);   // Yama LSM 的 Linux 专有 prctl
SendContinueSignalToChild();
const int r = HANDLE_EINTR(sys_waitpid(child, &status, __WALL));

克隆出的子进程再用 PTRACE_ATTACH / PTRACE_GETREGSET 挂住崩溃进程的各线程,读 /proc/<pid>/{maps,status,auxv},最后写 minidump。整个信号上下文里只走 lss 的直接 syscall,不碰 libc(async-signal-safe 约束)。

在鸿蒙上,PR_SET_PTRACER 是否生效、应用 UID 能否 PTRACE_ATTACH 自己的线程、sigaction 会不会被系统 faultloggerd / hiAppEvent 的崩溃处理链覆盖,全是要真机验证的未知数。而系统本身已经会产 CppCrash 日志 —— 这决定了 P0 的边界。

好消息是 aarch64 支持是现成的:__aarch64__ 分支、fpsimd_context 浮点上下文、src/common/linux/breakpad_getcontext.S 的 arm64 汇编实现、解析端的 stackwalker_arm64.cc 都在。


3. 鸿蒙 PC 兼容性体检(本机 DevEco SDK 实测)

用 SDK 自带的 clang 打印目标宏:

$clang = 'C:\Program Files\Huawei\DevEco Studio\sdk\default\openharmony\native\llvm\bin\clang.exe'
'' | & $clang -target aarch64-linux-ohos -dM -E -x c++ - |
  Select-String '^#define (__OHOS__|__linux__|__aarch64__)'

输出:

#define __OHOS__ 1
#define __aarch64__ 1
#define __linux 1
#define __linux__ 1
#define __unix__ 1

__linux__ 与 __aarch64__ 都存在、__ANDROID__ 不存在,意味着源码里的 Linux/aarch64 分支会自动走对;需要手工补的只有 autotools 的 --host 判定(我们干脆不用 autotools)。

再查 sysroot 头文件:

$inc = '...\native\sysroot\usr\include'
'sys/ptrace.h','sys/ucontext.h','sys/prctl.h','link.h','elf.h','zlib.h','paths.h' |
  ForEach-Object { '{0} : {1}' -f $_, (Test-Path (Join-Path $inc $_)) }

结果全在;sys/ptrace.h 有 PTRACE_GETREGSET (0x4204),架构专属头在 include/aarch64-linux-ohos/asm/ 下(含 struct user_pt_regs、struct fpsimd_context、FPSIMD_MAGIC),clang 会自动加这个 -isystem 路径。libz.so 也在 usr/lib/aarch64-linux-ohos,dump_syms 的 -lz 不缺。

musl 的关键差异:OHOS 是 musl libc,getcontext() 没有实现(llvm-nm -D libc.so 里看不到),而 ExceptionHandler::SimulateSignalDelivery() 会调它。上游早有兜底:

// src/common/linux/breakpad_getcontext.h
#ifndef HAVE_GETCONTEXT
int breakpad_getcontext(ucontext_t* ucp);
#define getcontext(x) breakpad_getcontext(x)
#endif

所以交叉编译时 HAVE_GETCONTEXT=0,把 breakpad_getcontext.S(有完整 aarch64 实现)编进去即可。这一点决定了 P1 编客户端时的配置,P0 不编客户端则不受影响。

还有一条:OHOS 没有 <stab.h>。这是 stabs_reader.cc 需要的老式符号调试头,上游在 src/common/android/include/stab.h 备了一份。我们的做法是只放这一个头到 cpp/compat/,作为额外 -I,不让 Android 目录里的 elf.h / link.h 覆盖系统头(第 5 章有对应 CMake)。

体检结论:解析端几乎可以原样交叉编译;采集端能编,但能不能跑取决于真机权限。


4. 架构设计

在这里插入图片描述

几条设计取舍:

  • 不复用 CLI 的 main。dump_syms 的 main 会 freopen(_PATH_DEVNULL, "w", stderr)、写 std::cout,在 HAP 里既难捕获又会污染全局状态。直接调 WriteSymbolFile() 写 std::ofstream 更干净。
  • 不捕获 stdout 生成报告。minidump_stackwalk 的 PrintProcessState() 是往 stdout 打印的,musl 下 open_memstream 也不好依赖。改为自己遍历 ProcessState,用 std::ostringstream 拼字符串返回给 ArkTS。
  • 不编 disassembler_objdump.cc 的运行时路径。它为了链接符号必须留着(后面会讲),但两条 objdump 开关都在初始化时关掉,因为 HAP 里没有 objdump 也不能 exec。
  • 路径全部走 Picker 授权 + filesDir。/tmp 在 HAP 里不可靠,dump / sym / 报告都落在 /data/storage/el2/base/files 或用户通过保存对话框指定的 uri。

对比一下三条可选路线:

路线交付物系统权限风险
HNP 命令行ELF + hnp.json需 shellHAP 无法 execv,不能作为应用主功能
客户端 SDKlibbreakpad_client.a信号 + ptrace与系统 DFX 冲突,需真机探针
NAPI 进程内解析(本作)HAP零权限低,主要功能闭环可离线验证

5. 原生层落地:CMake 与 NAPI

5.1 源码裁剪

工程把上游 src/ 整体放到 entry/src/main/cpp/breakpad/src(含 LICENSE),但 CMake 只挑需要的 .cc:

cmake_minimum_required(VERSION 3.5.0)
project(breakpad_bridge C CXX)

set(BP ${CMAKE_CURRENT_SOURCE_DIR}/breakpad/src)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_C_STANDARD 11)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
add_compile_definitions(_GNU_SOURCE _DEFAULT_SOURCE)
add_compile_options(-Wno-deprecated -Wno-unused-variable -Wno-unused-function
                    -Wno-sign-compare -Wno-unused-result)

# processor/*.cc:排除 CLI main、单测、selftest
file(GLOB BP_PROCESSOR_CC "${BP}/processor/*.cc")
list(FILTER BP_PROCESSOR_CC EXCLUDE REGEX ".*(_unittest|_test|selftest)\\.cc$")
list(FILTER BP_PROCESSOR_CC EXCLUDE REGEX
     ".*/(minidump_dump|minidump_stackwalk|microdump_stackwalk)\\.cc$")

# x86 exploitability 需要的第三方反汇编
file(GLOB BP_LIBDISASM_C "${BP}/third_party/libdisasm/*.c")

# dump_syms 的 DWARF/ELF 读取链(不含 tools/.../dump_syms.cc 的 main)
set(BP_DUMPSYMS_CC
    ${BP}/common/dwarf_cfi_to_module.cc
    ${BP}/common/dwarf_cu_to_module.cc
    ${BP}/common/dwarf_line_to_module.cc
    ${BP}/common/dwarf_range_list_handler.cc
    ${BP}/common/language.cc
    ${BP}/common/module.cc
    ${BP}/common/path_helper.cc
    ${BP}/common/stabs_reader.cc
    ${BP}/common/stabs_to_module.cc
    ${BP}/common/dwarf/bytereader.cc
    ${BP}/common/dwarf/dwarf2diehandler.cc
    ${BP}/common/dwarf/dwarf2reader.cc
    ${BP}/common/dwarf/elf_reader.cc
    ${BP}/common/linux/crc32.cc
    ${BP}/common/linux/dump_symbols.cc
    ${BP}/common/linux/elf_symbols_to_module.cc
    ${BP}/common/linux/elfutils.cc
    ${BP}/common/linux/file_id.cc
    ${BP}/common/linux/linux_libc_support.cc
    ${BP}/common/linux/memory_mapped_file.cc
    ${BP}/common/linux/safe_readlink.cc
    ${BP}/common/convert_UTF.cc
    ${BP}/common/md5.cc
    ${BP}/common/string_conversion.cc
    ${BP}/common/linux/scoped_pipe.cc
    ${BP}/common/linux/scoped_tmpfile.cc
)

add_library(breakpad_core STATIC
            ${BP_PROCESSOR_CC} ${BP_DUMPSYMS_CC} ${BP_LIBDISASM_C})
# OHOS musl 没有 <stab.h>;只把这一个兼容头加入 include 路径
target_include_directories(breakpad_core PUBLIC
    ${BP} ${CMAKE_CURRENT_SOURCE_DIR}/compat)
target_link_libraries(breakpad_core PUBLIC z)

add_library(breakpad_bridge SHARED breakpad_bridge.cpp)
target_include_directories(breakpad_bridge PRIVATE ${BP})
target_link_libraries(breakpad_bridge PRIVATE
    breakpad_core
    libace_napi.z.so
    libhilog_ndk.z.so
    z)

HAP 侧的 entry/build-profile.json5 只要把 CMake 挂上去,并统一 C++ 标准:

"externalNativeOptions": {
  "path": "./src/main/cpp/CMakeLists.txt",
  "arguments": "",
  "cppFlags": "-std=c++20"
}

5.2 编译期实际踩到的四个坑

① stab.h 缺失。

src/common/stabs_reader.cc:41:10: fatal error: 'stab.h' file not found

从 common/android/include/stab.h 复制一份到 cpp/compat/,只加这个目录到 -I,避免 Android 版 elf.h / link.h 覆盖 OHOS 系统头。

② SymbolData 不在 google_breakpad 命名空间。

error: no member named 'SYMBOLS_AND_FILES' in namespace 'google_breakpad'

common/symbol_data.h 里枚举是全局的,只有 DumpOptions 在命名空间内。写成 ::SYMBOLS_AND_FILES | ::CFI。

③ DisassemblerObjdump 链接符号缺失。

ld.lld: error: undefined symbol:
  google_breakpad::DisassemblerObjdump::DisassemblerObjdump(...)
>>> referenced by minidump_processor.cc
>>> referenced by exploitability_linux.cc

一开始我把 disassembler_objdump.cc 排除了,结果 minidump_processor.cc 和 exploitability_linux.cc 都引用它。解决办法是编进来、运行时关掉:

MinidumpProcessor processor(supplier.get(), &resolver);
// HAP 里没有 objdump,也不能 exec;两条反汇编路径都关闭
processor.set_enable_objdump(false);
processor.set_enable_objdump_for_exploitability(false);

④ 上游默认 -Werror,老代码在 clang 15 下会撞 deprecated 警告。 CMake 里统一加 -Wno-deprecated 等开关,不改上游源码。

独立验证(不依赖 hvigor,编译报错能快速迭代):

$sdk = 'C:\Program Files\Huawei\DevEco Studio\sdk\default\openharmony\native'
& "$sdk\build-tools\cmake\bin\cmake.exe" -S <cpp目录> -B <build目录> -G Ninja `
  "-DCMAKE_MAKE_PROGRAM=$sdk\build-tools\cmake\bin\ninja.exe" `
  "-DCMAKE_TOOLCHAIN_FILE=$sdk\build\cmake\ohos.toolchain.cmake" `
  -DOHOS_ARCH=arm64-v8a -DCMAKE_BUILD_TYPE=Release
& "$sdk\build-tools\cmake\bin\cmake.exe" --build <build目录>

产物校验:

Class:   ELF64
Machine: AArch64
NEEDED:  libace_napi.z.so / libhilog_ndk.z.so / libz.so / libc++_shared.so / libc.so

libc++_shared.so 不用手工拷 —— hvigor 的 ProcessLibs 会把它收进 HAP 的 libs/arm64-v8a/。

5.3 NAPI 接口:把库变成三个函数

types/libbreakpad_bridge/index.d.ts 是 ArkTS 侧的契约:

interface BreakpadBridgeNative {
  engineVersion(): string;
  /** symbolDirs: <root>/<debug_file>/<debug_id>/<name>.sym 的根目录列表 */
  analyzeMinidump(dmpPath: string, symbolDirs: Array<string>): string;
  /** 成功返回 .sym 首行 MODULE ...,ArkTS 据此归档 */
  dumpSymbols(binaryPath: string, outSymPath: string): string;
}
declare const breakpadBridge: BreakpadBridgeNative;
export default breakpadBridge;

解析函数的核心(省略头部 include 与 helper):

std::string BuildReport(const std::string& dmp_path,
                        const std::vector<std::string>& symbol_dirs) {
  using google_breakpad::Minidump;
  using google_breakpad::MinidumpProcessor;
  using google_breakpad::BasicSourceLineResolver;
  using google_breakpad::SimpleSymbolSupplier;
  using google_breakpad::ProcessState;

  MinidumpThreadList::set_max_threads(UINT32_MAX);
  MinidumpMemoryList::set_max_regions(UINT32_MAX);

  Minidump dump(dmp_path);
  if (!dump.Read()) return "无法读取 minidump: " + dmp_path;

  std::unique_ptr<SimpleSymbolSupplier> supplier;
  if (!symbol_dirs.empty())
    supplier.reset(new SimpleSymbolSupplier(symbol_dirs));

  BasicSourceLineResolver resolver;
  MinidumpProcessor processor(supplier.get(), &resolver);
  processor.set_enable_objdump(false);
  processor.set_enable_objdump_for_exploitability(false);

  ProcessState state;
  auto result = processor.Process(&dump, &state);
  std::ostringstream os;
  if (result != google_breakpad::PROCESS_OK) {
    os << "MinidumpProcessor::Process 失败,错误码 " << (int)result;
    return os.str();
  }

  const auto* sys = state.system_info();
  os << "操作系统  : " << sys->os << " " << sys->os_version << "\n"
     << "CPU       : " << sys->cpu << " " << sys->cpu_info << "\n"
     << "崩溃原因  : " << state.crash_reason() << "\n"
     << "崩溃地址  : 0x" << std::hex << state.crash_address() << std::dec << "\n"
     << "崩溃线程  : " << state.requesting_thread() << "\n"
     << "线程总数  : " << state.threads()->size() << "\n\n";

  const auto* threads = state.threads();
  for (size_t t = 0; t < threads->size(); ++t) {
    const auto* stack = (*threads)[t];
    os << "---- 线程 " << t << (t == (size_t)state.requesting_thread() ? " (崩溃线程)" : "")
       << " tid=" << stack->tid() << " ----\n";
    const auto* frames = stack->frames();
    for (size_t f = 0; f < frames->size(); ++f) {
      const auto* frame = (*frames)[f];
      std::string mod = frame->module ? frame->module->code_file() : "(未知模块)";
      std::string fn = frame->function_name.empty() ? "??" : frame->function_name;
      os << "  " << f << "  " << mod << "!" << fn;
      if (!frame->source_file_name.empty())
        os << " [" << frame->source_file_name << " : " << frame->source_line << "]";
      os << "   pc=0x" << std::hex << frame->instruction << std::dec
         << "\n      " << frame->trust_description() << "\n";
    }
    os << "\n";
  }
  const auto* missing = state.modules_without_symbols();
  os << "缺少符号的模块: " << (missing ? missing->size() : 0) << "\n";
  for (const auto* m : *missing)
    os << "  " << m->debug_file() << "  id=" << m->debug_identifier() << "\n";
  return os.str();
}

dumpSymbols 直接调用库函数,绕开 CLI 的 main:

napi_value DumpSymbols(napi_env env, napi_callback_info info) {
  // ... 取 binary / out_path 两个字符串参数
  std::ofstream out(out_path, std::ios::binary);
  // SymbolData 与枚举值在全局命名空间
  ::SymbolData data = ::SYMBOLS_AND_FILES | ::CFI;
  google_breakpad::DumpOptions options(data, true, false, false);
  bool ok = google_breakpad::WriteSymbolFile(
      binary, google_breakpad::BaseName(binary), "Linux", "",
      std::vector<std::string>(), options, out);
  out.close();
  if (!ok) { ThrowError(env, "WriteSymbolFile failed: " + binary); return nullptr; }

  std::ifstream in(out_path);
  std::string first_line;
  std::getline(in, first_line);   // "MODULE Linux arm64 <id> <name>"
  return MakeString(env, first_line);
}

注册走标准 NAPI 模板,模块名与 oh-package.json5 里的本地依赖对应:

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
  napi_property_descriptor desc[] = {
      {"engineVersion", nullptr, EngineVersion, nullptr, nullptr, nullptr, napi_default, nullptr},
      {"analyzeMinidump", nullptr, AnalyzeMinidump, nullptr, nullptr, nullptr, napi_default, nullptr},
      {"dumpSymbols", nullptr, DumpSymbols, nullptr, nullptr, nullptr, napi_default, nullptr},
  };
  napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
  return exports;
}
EXTERN_C_END

static napi_module breakpadModule = {
    .nm_version = 1, .nm_flags = 0, .nm_filename = nullptr,
    .nm_register_func = Init, .nm_modname = "breakpad_bridge",
    .nm_priv = nullptr, .reserved = {0},
};
extern "C" __attribute__((constructor)) void RegisterBreakpadBridgeModule(void) {
  napi_module_register(&breakpadModule);
}

6. ArkTS 应用层:从"能解析"到"像个应用"

6.1 本地 NAPI 依赖与类型声明

entry/oh-package.json5
  "dependencies": {
    "libbreakpad_bridge.so": "file:./src/main/cpp/types/libbreakpad_bridge"
  }

ArkTS 里直接 import breakpadBridge from 'libbreakpad_bridge.so',就能调用上文的三个函数。编译期会有一条 Currently module for 'libbreakpad_bridge.so' is not verified 的 WARN —— 只要 .d.ts 与导出函数一致,运行时没问题。

6.2 文件能力:Picker + fileIo,零权限

鸿蒙的文件访问走用户授权选择器,不申请 READ/WRITE_MEDIA 之类的敏感权限。选择、读取、写入都用同一套 fileIo:

import { picker } from '@kit.CoreFileKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { util } from '@kit.ArkTS';

private async pickDmp(): Promise<void> {
  const dp = new picker.DocumentViewPicker(this.context!);
  const opt = new picker.DocumentSelectOptions();
  opt.fileSuffixFilters = ['.dmp', '.mdmp'];
  opt.maxSelectNumber = 1;
  const uris: Array<string> = await dp.select(opt);
  if (uris.length > 0) {
    this.dmpPath = uris[0];
    this.dumpLabel = this.displayNameOf(uris[0]);
  }
}

private readFirstLine(path: string): string {
  const f = fs.openSync(path, fs.OpenMode.READ_ONLY);
  try {
    const buf: ArrayBuffer = new ArrayBuffer(512);
    const n: number = fs.readSync(f.fd, buf, { offset: 0 });
    const text: string = new util.TextDecoder().decode(new Uint8Array(buf, 0, n));
    return text.split('\n')[0];       // MODULE 行
  } finally { fs.closeSync(f); }
}

private copyBytes(src: string, dst: string): void {
  const srcFile = fs.openSync(src, fs.OpenMode.READ_ONLY);
  let buf: ArrayBuffer = new ArrayBuffer(0);
  try {
    const st = fs.statSync(srcFile.fd);
    buf = new ArrayBuffer(st.size);
    fs.readSync(srcFile.fd, buf, { offset: 0 });
  } finally { fs.closeSync(srcFile); }
  const dstFile = fs.openSync(dst,
    fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC);
  try {
    fs.writeSync(dstFile.fd, buf);
    fs.fsyncSync(dstFile.fd);
  } finally { fs.closeSync(dstFile); }
}

导出报告用 DocumentViewPicker.save():

const opt = new picker.DocumentSaveOptions();
opt.newFileNames = [`breakpad_report_${Date.now()}.txt`];
opt.fileSuffixChoices = ['文本文件|txt'];
const uris: Array<string> = await new picker.DocumentViewPicker(this.context!).save(opt);

注意 fileIo.openSync() 返回的是 File 对象,readSync/writeSync/fsyncSync 要传 .fd;util.TextDecoder.decode() 要喂 Uint8Array,不能直接喂 ArrayBuffer。这几个是 ArkTS 严格模式下最常见的低级错误。

6.3 符号目录:必须严格对齐 SimpleSymbolSupplier

Breakpad 的 SimpleSymbolSupplier::GetSymbolFilePath() 规则很死:

// src/processor/simple_symbol_supplier.cc
path.append(PathnameStripper::File(module->debug_file()));  // 目录 1
path.append(module->debug_identifier());                    // 目录 2
if (debug_file_extension == ".pdb")
  path.append(debug_file_name.substr(0, debug_file_name.size() - 4));
else
  path.append(debug_file_name);
path.append(".sym");

即 <root>/<debug_file>/<debug_id>/<name>.sym,其中 xxx.pdb 会被换成 xxx.sym。ArkTS 侧从 dumpSymbols 返回的 MODULE 首行解析出 id 与 name,再归档:

/** MODULE <os> <cpu> <debug-id> <debug-file> */
private moduleInfo(line: string): Array<string> {
  const parts: Array<string> = line.trim().split(' ')
    .filter((s: string): boolean => s.length > 0);
  if (parts.length < 5 || parts[0] !== 'MODULE')
    throw new Error(`符号文件缺少 MODULE 行: ${line}`);
  return [parts[4], parts[3]];          // [name, id]
}

private archiveSym(srcPath: string, moduleLine: string): void {
  const info: Array<string> = this.moduleInfo(moduleLine);
  const name: string = info[0], id: string = info[1];
  const dir: string = `${this.userSymRoot}/${name}/${id}`;
  const symName: string = name.endsWith('.pdb')
    ? `${name.substring(0, name.length - 4)}.sym` : `${name}.sym`;
  this.ensureDir(dir);                 // 逐级 mkdir,避免依赖 recursion 参数
  this.copyBytes(srcPath, `${dir}/${symName}`);
  this.symRootLabel = `${name} (${id.substring(0, 12)}…)`;
  this.addSymbolEntry(name, id);
}

这一步如果拼错目录,表现是"分析报告能出来,但所有帧都是 ?? 和裸地址",非常难查 —— 因为 SimpleSymbolSupplier 找不到符号时只打 INFO 日志,不会报错。

6.4 内置样例:把测试数据打进 HAP

resources/rawfile/testdata/ 里放了上游的 minidump2.dmp(11 KB)与两个 .sym(约 1.5 MB)。启动时展开到 filesDir/testdata,按上面的目录规范摆好:

private async prepareSample(): Promise<void> {
  const root = `${this.filesDir}/testdata`;
  const symBase = `${root}/symbols`;
  this.ensureDir(`${symBase}/test_app.pdb/5A9832E5287241C1838ED98914E9B7FF1`);
  this.ensureDir(`${symBase}/kernel32.pdb/BCE8785C57B44245A669896B6A19B9542`);
  await this.writeRaw('testdata/sample.dmp', `${root}/sample.dmp`);
  await this.writeRaw('testdata/sym_test_app.sym',
    `${symBase}/test_app.pdb/5A9832E5287241C1838ED98914E9B7FF1/test_app.sym`);
  await this.writeRaw('testdata/sym_kernel32.sym',
    `${symBase}/kernel32.pdb/BCE8785C57B44245A669896B6A19B9542/kernel32.sym`);
  this.symRoot = symBase;
  this.dmpPath = `${root}/sample.dmp`;
}

private async writeRaw(rawName: string, dst: string): Promise<void> {
  const bytes: Uint8Array =
    await this.context!.resourceManager.getRawFileContent(rawName);
  const f = fs.openSync(dst,
    fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC);
  try { fs.writeSync(f.fd, bytes.buffer as ArrayBuffer); }
  finally { fs.closeSync(f); }
}

这样"运行内置样例"按钮不需要网络、不需要外部文件、不需要真机崩溃,就能证明解析内核 + 符号化链路是通的。

6.5 触发解析与状态联动

private doAnalyze(): void {
  if (this.dmpPath.length === 0 || this.busy) return;
  this.busy = true;
  this.statusText = '正在解析 minidump…';
  this.report = '';
  // 让状态先渲染一帧,再执行同步的 C++ 解析
  setTimeout((): void => {
    try {
      const dirs: Array<string> = [this.userSymRoot, this.symRoot];
      const text: string = breakpadBridge.analyzeMinidump(this.dmpPath, dirs);
      this.report = text;
      this.statChars = text.length.toString();
      const marker = '线程总数  : ';
      const at = text.indexOf(marker);
      this.statThreads = at >= 0
        ? text.substring(at + marker.length).split('\n')[0].trim() : '—';
      this.statusText = '解析完成';
    } catch (e) {
      this.report = `解析失败: ${(e as Error).message}`;
      this.statusText = '解析失败';
    }
    this.busy = false;
  }, 0);
}

传两个符号根目录是有意的:userSymRoot 存用户导入的符号,symRoot 在运行内置样例时会切到样例目录,两者同时生效。

6.6 UI:深色工作台

主题用一组常量收口,避免到处写魔法色值:

const C_BG = '#0B1220';
const C_SIDEBAR = '#0F192B';
const C_PANEL = '#141F33';
const C_BORDER = '#233252';
const C_TEXT = '#E8EFFB';
const C_SUB = '#8B9DBB';
const C_MUTED = '#5E7191';
const C_ACCENT = '#5B8CFF';
const C_OK = '#3FB950';

布局是 Row(侧栏 220 + 主区 layoutWeight(1)),主区再按「标题 → 工具栏 → 指标卡 → 内容区 → 状态栏」竖向分层。指标卡抽成 @Builder:

@Builder
metricCard(label: string, value: string, accent: string) {
  Column() {
    Text(label).fontSize(11).fontColor(C_MUTED).width('100%')
    Text(value).fontSize(19).fontWeight(FontWeight.Bold)
      .fontColor(accent).width('100%').maxLines(1)
      .textOverflow({ overflow: TextOverflow.Ellipsis }).margin({ top: 8 })
  }
  .layoutWeight(1).padding(14)
  .backgroundColor(C_PANEL).borderRadius(12)
  .border({ width: 1, color: C_BORDER })
}

工具按钮不用系统 Button,而是 Row + Text 自定义,配色与圆角完全可控:

Row() { Text('运行内置样例').fontSize(13).fontColor('#FFFFFF') }
  .height(36).padding({ left: 16, right: 16 }).borderRadius(9)
  .backgroundColor(this.busy ? '#1D355F' : C_ACCENT)
  .margin({ right: 9 })
  .onClick((): void => { void this.runSample(); })

报告查看器用等宽字体 + Scroll + layoutWeight(1) 吃满剩余高度:

Row() {
  Text('STACK TRACE').fontSize(11).fontColor(C_ACCENT)
  Blank()
  Text('清空').fontSize(11).fontColor(C_MUTED)
    .onClick((): void => { this.clearReport(); })
  Text('导出报告').fontSize(11).fontColor(C_SUB)
    .onClick((): void => { void this.exportReport(); })
}.width('100%').margin({ bottom: 12 })

Scroll() {
  Text(this.report.length > 0 ? this.report : '暂无报告 …')
    .fontSize(12).lineHeight(20).fontColor(C_TEXT)
    .fontFamily('monospace').width('100%')
}.width('100%').layoutWeight(1)
 .backgroundColor('#0A1222').borderRadius(9)
 .border({ width: 1, color: C_BORDER }).padding(14)

有三个 ArkUI 细节值得记:属性链里不要调用私有方法(会被静默忽略);.width()/.height() 读普通 @State 数值变量在运行时不刷新,要写字面量或三元字面量;字号 .fontSize() 读 @State 正常。所以配色、宽度都用常量或三元表达式。


7. 构建、签名与两个真实报错

7.1 一键构建脚本

@echo off
REM Build ohos_Breakpad HAP with the DevEco-bundled hvigor/Clang toolchain.
setlocal
set "DEVECO_SDK_HOME=C:\Program Files\Huawei\DevEco Studio\sdk"
set "JAVA_HOME=C:\Program Files\Huawei\DevEco Studio\jbr"
set "PATH=C:\Program Files\Huawei\DevEco Studio\jbr\bin;%PATH%"
set "HVIGORW=C:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat"
cd /d "%~dp0ohos_hap"
REM 归档旧产物,避免二次构建时 safe-delete 批量删除超时
set "STAMP=%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%_%TIME:~0,2%%TIME:~3,2%%TIME:~6,2%_%RANDOM%"
set "STAMP=%STAMP: =0%"
if exist "entry\.cxx" move "entry\.cxx" "entry\.cxx_prev_%STAMP%" >nul
if exist "entry\build" move "entry\build" "entry\build_prev_%STAMP%" >nul
call "%HVIGORW%" --mode module -p module=entry@default -p product=default assembleHap --no-daemon
endlocal

DEVECO_SDK_HOME 和把 DevEco 自带 jbr\bin 前置到 PATH 是必须的:PackageHap / 签名工具要 java,否则会报 spawn java ENOENT(00308018 家族错误)。

7.2 坑:二次构建 00308018

第一次从空目录构建一路成功;第二次增量构建时:

> hvigor ERROR: Failed :entry:default@BuildNativeWithCmake...
Error Code: 00308018 Unknown Error
[safe-delete] 操作失败: spawnSync ...\genie-trash\win32-x64.exe ETIMEDOUT

这是 IDE 的批量删除保护在清理已有 .cxx(119 个文件、约 109 MB)和 build/ 产物时超时,不是代码问题。试过只归档 .cxx,下一次又会在 CompileArkTS 阶段超时。最终方案就是脚本里那两行 move:用改名代替删除,每次从干净目录构建。代价是每次多花约 2~3 分钟重编 native,收益是稳定可复现。旧 entry/*_prev_* 目录可以定期手工清理。

7.3 坑:9568320 no signature file

自动签名后仍然装不上,DevEco 日志是:

$ hdc file send ...\entry-default-unsigned.hap ...
Install Failed: error: failed to install bundle.
code:9568320
error: no signature file.

注意它发的是 unsigned 包。查 build-profile.json5 发现 DevEco 自动签名只把材料写进了 app.signingConfigs,并没有把 product 挂上去:

"signingConfigs": [ { "name": "default", "type": "HarmonyOS",
                      "material": { "certpath": "...", "profile": "...",
                                    "storeFile": "...", "keyPassword": "...",
                                    "storePassword": "..." } } ],
"products": [ {
  "name": "default",
  "signingConfig": ""      // ← 根因
} ]

改成 "signingConfig": "default" 后,SignHap 才会真正执行:

> hvigor Finished :entry:default@SignHap... after 6 s 106 ms
> hvigor BUILD SUCCESSFUL
entry-default-signed.hap   17,612,738 bytes
entry-default-unsigned.hap 17,414,768 bytes

顺带确认 .p7b 里绑定的是 org.breakpad.ohos(调试 profile 与 bundleName 强绑定,不能拿别的工程的材料混用)。用 [IO.File]::ReadAllText(path, [Text.Encoding]::ASCII) -match 'org\.breakpad\.ohos' 可以直接验。

HAP 内容:

libs/arm64-v8a/libbreakpad_bridge.so   3,949,224
libs/arm64-v8a/libc++_shared.so        1,262,504
resources/rawfile/testdata/sample.dmp     11,317
resources/rawfile/testdata/sym_test_app.sym 840,285
resources/rawfile/testdata/sym_kernel32.sym 622,319
ets/modules.abc                           41,576

安全提醒:build-profile.json5 里带本机调试证书路径和加密密码,推到公开仓库前必须清空 signingConfigs 并把 signingConfig 改回 ""。


8. 运行效果(真机截图)

在真机上完整的操作链路是:点「运行内置样例」展开 rawfile 里的 dmp 与 sym 并立刻解析,或者先「从 .so 提取」拿到 MODULE 行、按 <debug_file>/<debug_id> 归档符号,再「选择 .dmp」载入自己的转储,点「分析」,最后「导出」成 txt。符号库页可以随时确认本次会话归档了哪些符号。整个流程没有一步需要网络,也没有一步申请权限 —— 文件进出全部由系统文件选择器的用户授权完成。下面三张图分别对应工作台、符号库和关于页。

图 1 工作台:内置样例解析结果

在这里插入图片描述

点击「运行内置样例」后,右侧 STACK TRACE 直接给出 upstream minidump2.dmp 的解析结果A。

函数名、源文件名、行号都出来了,和上游 src/processor/testdata/minidump2.stackwalk.out 的期望输出一致 —— 这说明 SimpleSymbolSupplier 找到了我们归档的两个 .sym,BasicSourceLineResolver 也确实做了符号化,而不是只打印裸地址。左侧「数据源」卡片同时显示 minidump 路径、符号根目录和当前生效的符号集。

图 2 符号库:会话内归档的符号

图 2 崩析台符号库视图

列表里是 test_app.pdb · 5A9832E5287241C1838ED98914E9B7FF1 和 kernel32.pdb · BCE8785C57B44245A669896B6A19B9542,即 MODULE 行里的 <debug_file> · <debug_id>。空态会提示用户导入 .sym 或选择带调试信息的 .so。这里也暴露了一个 UI 状态刷新问题,见第 9 章。

图 3 关于:把能力边界写进应用里

图 3 崩析台关于视图

关于页写清了内核来源(Google Breakpad processor / C++20 / OHOS aarch64)、dump_syms 的调用方式、NAPI 进程内运行形态、BSD-3-Clause 许可,以及两条明确的限制:P0 不含采集端、内置样例是 HarmonyOS 7.0。把"不做什么"写进产品界面,比写在 README 里更诚实。


9. 踩坑清单与已知限制

现象根因 / 处理
fatal error: 'stab.h' file not foundOHOS musl 无此头,单独补 compat/stab.h
no member named 'SYMBOLS_AND_FILES' in namespace枚举定义在全局命名空间
undefined symbol: DisassemblerObjdump::...编入该 .cc,运行时关掉两个 objdump 开关
增量构建 00308018 + safe-delete ETIMEDOUT构建前把 .cxx / build 改名归档
安装 9568320 no signature fileproducts[].signingConfig 为空,SignHap 被跳过
报告有帧但全是 ??符号目录层级 / .pdb → .sym 命名不对,Supplier 静默找不到
属性链里 .width(this.xxx) / 调私有方法不生效改字面量或三元字面量;逻辑预计算后读字段

还有三个诚实的限制:

  1. 指标卡的刷新时序。截图里能看到:报告已经出来了,顶部"符号模块 / 线程数 / 报告字符"仍是首屏的 0 / — / 0。原因是 @Builder metricCard(label, value, accent) 是按值传参的自定义构建函数,ArkUI 对这类参数不会随 @State 变化自动重建内部 UI。修法有三种:改成按引用传参($$)、把卡片内联进 build() 直接读 @State、或者用一个 @Observed 的状态对象承载统计值。这不影响解析功能,但属于"看起来像 bug"的体验问题,已列入下一轮。
  2. 大转储会卡 UI。setTimeout(..., 0) 只是让状态先渲染一帧,analyzeMinidump 本身是同步 NAPI 调用。几十 KB 的内置样例无感,但 1 MB 以上、几十线程的 dump 会阻塞 UI 线程。下一步可以套用已有工程的 worker 模式:worker 里 import NAPI,UI 只传路径、收字符串。
  3. P0 不是崩溃采集工具。内置样例是上游的 Windows x86 minidump,用来证明"解析内核在鸿蒙上跑通了";应用本身不注册信号处理器、不产生鸿蒙崩溃转储。真要采集,见下一章。

9.1 安全与隐私边界

这个应用没有在 module.json5 里申请任何 requestPermissions:文件能力全部来自 DocumentViewPicker,用户每次选择/保存都是一次显式授权;原生层拿到的路径只来自 ArkTS 传入的参数,C++ 侧没有自行拼接系统目录,也没有在后台扫描文件。symupload / minidump_upload 这两个网络上传工具根本没有进入 CMake 源码清单,HAP 里既没有 HTTP 客户端代码,也没有 INTERNET 权限,因此不存在"崩溃数据被偷偷传走"的链路。

但 minidump 本身是敏感数据:它包含寄存器、栈内存、部分堆内存快照、环境变量、加载模块路径与 build-id,.sym 文件又带有源码文件名与函数名。本作的处理是:解析只在本地内存里进行,报告只显示在界面上,导出必须由用户点「导出报告」并在保存对话框里指定目标路径,没有自动落盘、没有日志回传、没有剪贴板写入。这一点对"拿生产崩溃包来分析"的场景尤其重要 —— 工具的设计应该让用户始终知道数据去了哪里。

还有一处需要如实说明:为了让完整转储不被截断,BuildReport 把 MinidumpThreadList::set_max_threads() 和 MinidumpMemoryList::set_max_regions() 都设成了 UINT32_MAX。面对内部样例这没问题,但解析不可信来源的二进制时,等于放弃了一道资源上限保护。生产化版本应该在 ArkTS 侧先检查文件大小、在原生层限制线程/内存区数量并加超时,避免畸形 dmp 把应用内存吃满。这类"解析器的输入预算"问题,和此前 ZIP 解压预算、符号表膨胀防护是同一类工程纪律。

另一个容易被忽略的点是签名材料:build-profile.json5 里的调试证书路径与加密密码属于本机私有数据,一旦把工程推到公开仓库就会泄漏。发布源码前清空 signingConfigs、signingConfig 置空,再用各人自己的 DevEco 自动签名重新生成,是必须写进 README 的一条。

9.2 与鸿蒙系统 DFX 的分工

鸿蒙自身有一套完整的故障处理体系:JS Crash、CppCrash、AppFreeze 由 FaultLogger / hiAppEvent 捕获,开发者可以订阅 APP_CRASH 事件,或者在 DevEco 的 FaultLog 窗口里看归档日志;普通 Release 应用默认读不到完整的崩溃上下文,这是系统刻意的沙箱约束。也就是说,"抓到崩溃"这件事系统已经做了,"把一份 minidump 读成带源码行号的调用栈"才是本作补上的缺口。

三者可以这样分工:开发阶段先用 CppCrash 日志确认信号类型、崩溃线程和大致模块;手里有 Breakpad minidump 与对应符号文件时,用崩析台做符号化栈回溯和缺符号模块排查;如果只有系统日志里的裸地址,则需要 llvm-symbolizer / addr2line 配合带调试信息的 ELF。至于系统自带的 minidump 能否直接喂给 Breakpad,必须先取一份真机样本验证格式:如果它是 Breakpad 的 MDMP 结构就能直接导入,如果是 ELF core 或私有结构,就得额外写一层转换或 reader。这个结论不能靠猜,也不该在文章里假装已经验证过。

正因为采集侧既有系统方案、又有权限与信号链的不确定性,P0 选择彻底不碰它,把界面上的"能力边界"写清楚(图 3)。一个工具明确说明自己不做什么,比宣称"全平台崩溃监控"更有用。

10. 复现命令汇总

# 1) 取源码(GitHub 镜像 + lss 子模块镜像)
git clone --depth 1 https://github.com/google/breakpad.git _breakpad_ref
git clone --depth 1 https://github.com/d-ronin/breakpad-lss.git _breakpad_lss_ref
cp _breakpad_lss_ref/linux_syscall_support.h \
   ohos_Breakpad/ohos_hap/entry/src/main/cpp/breakpad/src/third_party/lss/

# 2) 本地构建(Windows + DevEco)
ohos_Breakpad\run-build.cmd

# 3) 产物
ohos_Breakpad\ohos_hap\entry\build\default\outputs\default\entry-default-signed.hap

工程结构(关键文件):

ohos_Breakpad/
├── run-build.cmd
├── screenshots/                       # 本文三张真机截图
└── ohos_hap/
    ├── build-profile.json5             # signingConfig: "default"、cppFlags -std=c++20
    ├── AppScope/app.json5              # bundleName org.breakpad.ohos
    └── entry/src/main/
        ├── module.json5
        ├── ets/pages/Index.ets         # 工作台 UI + Picker + fileIo
        ├── resources/rawfile/testdata/ # 内置 dmp + 2 sym
        └── cpp/
            ├── CMakeLists.txt
            ├── breakpad_bridge.cpp
            ├── compat/stab.h
            ├── breakpad/               # 上游源码 + LICENSE (BSD-3-Clause)
            └── types/libbreakpad_bridge/index.d.ts

11. 常见问题(QA)

以下问题来自实际构建、真机运行和评审反馈。涉及"最新版本"的说明以本节为准:正文 6.4 与第 8 章截图摄于早期版本,内核结论未变。

Q1:为什么内置样例是 Linux 转储,不再用 Windows 的 minidump2.dmp?

A:minidump2.dmp 是 Windows XP 的转储,符号化验证很顺,但放在鸿蒙 PC 上跑,报告里写着 Windows NT 5.1 既突兀,也容易被误会。最新包换成上游的 linux_null_read_av.dmp(Linux 内核、SIGSEGV、崩溃地址 0x0、1 线程、8 个模块)外加四个官方 .sym:null_read_av、libc-2.13.so、ld-2.13.so、libgcc_s.so.1。这四个模块的 CV record debug id 与符号目录逐字节核对过;另外四个模块故意没有符号,用来同时展示"命中"和"缺符号"两种状态。

Q2:报告里的"转储来源系统"为什么是 Linux、CPU 为什么是 amd64,而不是 HarmonyOS / arm64?

A:这两个字段读的是 minidump 的 SystemInfoStream,描述产生转储的那台机器,不是运行分析器的机器。当前样例来自上游 x86_64 Linux 测试集,所以如实显示。分析器本机的信息单独放在报告顶部的「运行环境(本机)」区块,由 @kit.BasicServicesKit 的 deviceInfo 读取 productModel / osFullName / sdkApiVersion / abiList,不硬编码。上游测试集里没有任何鸿蒙或 arm64 的 minidump,想看 arm64 鸿蒙转储只能导入真机 dmp。

Q3:为什么不用 Crashpad?

A:Crashpad 是"客户端 + handler 进程"模型:崩溃时 spawn 一个 handler,通过 socket 传 minidump。鸿蒙侧 noexec、无 execv,子进程这条路直接封死。Breakpad 的 processor 是纯离线库,进程内调用即可,正好匹配"零权限、零子进程"的约束。

Q4:为什么把 Breakpad 编成静态库 + NAPI,而不是起子进程跑 minidump_stackwalk?

A:原因同上:鸿蒙应用不能 execv,/data/local/tmp 那套端侧调试思路在正式应用里不可用。编成 libbreakpad_bridge.so 后,解析、符号查找、报告拼装都在同一进程内完成,ArkTS 只负责 picker、fileIo 和展示,中间没有进程边界,也没有 shell。

Q5:Picker 选中的 file://docs/... 为什么不能直接交给 native?

A:DocumentViewPicker 返回的是系统授权的 uri,不是普通文件路径,native 侧 open() 不认识。做法是在 ArkTS 里用 fs.openSync(uri) 打开,按字节读到 ArrayBuffer,再写到 filesDir/work/ 下的真实路径,最后把路径传给 native。copyBytes() 和 writeRaw() 承担的就是这个转换。

Q6:符号文件为什么必须摆成 <root>/<name>/<id>/<name>.sym?

A:这是 SimpleSymbolSupplier 的查找规则:<root>/<debug_file>/<debug_id>/<file>.sym,.pdb 模块会把后缀换成 .sym。debug_id 必须与 minidump 模块 CV record 算出来的标识完全一致(GUID + age),差一位就找不到;找不到时 supplier 只打 INFO 日志、不报错,表现是"报告出来了,但帧全是 ?? 和裸地址"。

Q7:怎么快速判断符号到底有没有命中?

A:看三处:报告里模块行是 OK 还是 MISSING;栈帧是 function [file : line + 0x…] 还是 ??;末尾"缺符号模块"清单是否为空。当前 Linux 样例故意一半有符号、一半没有,这三处能同时看到两种状态,比只看"成功"更有说服力。

Q8:dump_syms 从 .so 提的符号,为什么在真机上可能对不上?

A:debug_id 由 ELF 的 build id 决定。如果设备上的 .so 经过 strip、重打包,或者与提符号用的不是同一条构建流水线,build id 变了,符号必然匹配不上。必须用与设备上二进制完全同一份的 .so 提符号,并在报告里核对 debug_id。

Q9:编译时为什么要加一串 -Wno-…?

A:Breakpad 是 C++20 时代的代码,但保留了大量老写法,OHOS 的 clang 会把 deprecated、unused parameter 之类升级成 error,另外 CMake 给 clang 传的 --gcc-toolchain 会触发 -Wunused-command-line-argument。处理方式是 -Wno-error=… 逐项关闭,而不是全局 -w;上游源码一行没改(除了引入 stab.h 兼容头)。

Q10:二次构建报 00308018 是怎么回事?

A:hvigor 的 native 缓存和残留的 CMakeCache.txt 撞在一起,第二次构建找不到 ninja 目标。run-build.cmd 的处理是每次清掉 native 构建目录、重新跑 CMake 配置,再 assembleHap;代价是全量编译约 2 分 35 秒,好处是构建结果可复现。

Q11:签名报 9568320 / 9568289 怎么办?

A:9568320 no signature file 一般是 signingConfigs 里 profile 是空串,或证书路径没填;在 DevEco 的 Project Structure → Signing Configs 里勾自动签名,或手工填 .p7b/.cer/.p12。9568289 是设备没进调试证书的 UDID 列表,把设备加进 profile 重签,或直接用自动签名。装包按本机 hdc 流程执行即可。

Q12:为什么一个权限都不申请?

A:文件访问全部走 DocumentViewPicker(用户主动选择,系统授权),复制目标在应用沙箱 filesDir,不需要媒体/存储权限;解析在进程内完成,不需要网络;module.json5 里没有任何 requestPermissions。这是选择"离线分析端"定位的直接收益。

Q13:能不能直接分析鸿蒙系统自己产生的崩溃?

A:当前 P0 不能。系统 FaultLog 需要系统级权限或调试应用,格式也未必是 minidump;P0 的输入明确是 .dmp 文件。P1 的路线是复用 Breakpad client 侧的 minidump writer(信号处理里写文件,不执行新代码),或接 HiAppEvent 的崩溃事件做补充信息。能力边界写在"关于"页里,避免用户误以为它是系统级崩溃采集器。

Q14:代码能商用吗?

A:Breakpad 是 BSD-3-Clause,保留版权声明即可商用,工程的 breakpad/LICENSE 原样保留;third_party/lss 也是 BSD。libc++_shared.so 由 hvigor 的 ProcessLibs 自动收进 HAP,分发时不需要额外处理。上架前按应用市场要求补隐私声明——本应用不采集、不上传任何数据,声明很好写。


小结

这次移植最关键的判断不是"怎么把 Breakpad 编译到 aarch64",而是先把一个库重新定义成一个产品。一旦确定做离线解析端,鸿蒙的三条硬约束(noexec / 无 execv / ptrace 受限)就从"致命障碍"变成了"与我无关";代价是放弃采集端,收益是零权限、零子进程、可离线自测的完整闭环。

剩下的问题都是工程细节:用 CMake 绕开 autotools 的交叉探测、按 Makefile.am 裁剪源码、补 musl 缺的头文件与符号、把 CLI 的 main 换成库函数调用、把符号目录严格对齐 SimpleSymbolSupplier、最后让 ArkTS 负责所有文件交互与呈现。整个过程里,真正花时间的是四个编译错误、一个构建缓存怪癖和一个签名配置空串 —— 这也是鸿蒙原生移植的常态:难点往往不在 fancy 的架构,而在工具链的最后一公里。

上游:google/breakpad(BSD-3-Clause)。本文所有截图来自 HarmonyOS PC 真机运行。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐