把 Google Breakpad 搬上鸿蒙 PC:崩析台(Minidump 分析台)移植实战
把 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 个文件,可以切成三块:
| 组件 | 位置 | 产物 | 系统依赖 |
|---|---|---|---|
| 客户端 client | src/client/linux/ | libbreakpad_client.a | 信号、clone、ptrace、/proc、直接 syscall |
| 解析端 processor | src/processor/ | libbreakpad.a | 只读文件,纯 C++,几乎无系统依赖 |
| 工具 tools | src/tools/linux/dump_syms 等 | dump_syms、minidump_stackwalk | ELF/DWARF、zlib;上传工具依赖网络 |
所以第一步不是"编译",而是定义产品形态:鸿蒙 PC 上要的是一个能双击打开的东西,不是一个 .a。可选形态有三:
- 命令行工具集:把
minidump_stackwalk、dump_syms交叉编译成 ELF,打成 HNP 给开发者用; - 崩溃采集 SDK:把
libbreakpad_client.a编出来,给别的鸿蒙原生工程集成; - 桌面分析器:把解析端 + 符号工具做成 GUI 应用,用户拿
.dmp和.so来看崩溃栈。
第 1 条在鸿蒙 HAP 里几乎是死路。此前多个移植工程已经踩实三条硬约束:
- HAP 应用没有
execv,HNP 里的二进制带有hnp_fileSELinux 标签,应用拉不起来; 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 | 需 shell | HAP 无法 execv,不能作为应用主功能 |
| 客户端 SDK | libbreakpad_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 符号库:会话内归档的符号

列表里是 test_app.pdb · 5A9832E5287241C1838ED98914E9B7FF1 和 kernel32.pdb · BCE8785C57B44245A669896B6A19B9542,即 MODULE 行里的 <debug_file> · <debug_id>。空态会提示用户导入 .sym 或选择带调试信息的 .so。这里也暴露了一个 UI 状态刷新问题,见第 9 章。
图 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 found | OHOS 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 file | products[].signingConfig 为空,SignHap 被跳过 |
报告有帧但全是 ?? | 符号目录层级 / .pdb → .sym 命名不对,Supplier 静默找不到 |
属性链里 .width(this.xxx) / 调私有方法不生效 | 改字面量或三元字面量;逻辑预计算后读字段 |
还有三个诚实的限制:
- 指标卡的刷新时序。截图里能看到:报告已经出来了,顶部"符号模块 / 线程数 / 报告字符"仍是首屏的
0 / — / 0。原因是@Builder metricCard(label, value, accent)是按值传参的自定义构建函数,ArkUI 对这类参数不会随@State变化自动重建内部 UI。修法有三种:改成按引用传参($$)、把卡片内联进build()直接读@State、或者用一个@Observed的状态对象承载统计值。这不影响解析功能,但属于"看起来像 bug"的体验问题,已列入下一轮。 - 大转储会卡 UI。
setTimeout(..., 0)只是让状态先渲染一帧,analyzeMinidump本身是同步 NAPI 调用。几十 KB 的内置样例无感,但 1 MB 以上、几十线程的 dump 会阻塞 UI 线程。下一步可以套用已有工程的 worker 模式:worker 里 import NAPI,UI 只传路径、收字符串。 - 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 真机运行。
更多推荐




所有评论(0)