返回

JNI崩溃栈解析失败?原因与解决指南

Linux

JNI 项目崩溃栈回溯解析失败问题

在JNI(Java Native Interface)开发中,利用 backtrace 函数生成崩溃堆栈跟踪是常见的做法。但是,当尝试使用 addr2line 工具解析这些跟踪信息时,可能会遇到解析失败,无法将内存地址映射到具体的代码行。 本文探讨该问题的原因,并提供几种解决办法。

问题原因分析

addr2line 工具依赖于调试信息才能将地址映射到源代码。 如果出现以下一种或者几种情况,就可能导致 addr2line 无法解析崩溃堆栈:

  1. 编译时未包含调试信息 :默认情况下,发布版的库或可执行文件不会包含调试信息,这些信息对定位代码的内存地址和函数非常有帮助。编译器在构建时可以通过使用类似 -g 的参数保留这些调试信息,确保 addr2line 能够读取并使用它们。
  2. 库或可执行文件被剥离 :为减小文件体积, 部署环境可能使用 strip 工具删除了二进制文件中的调试信息,同样会导致addr2line 解析失败。
  3. 加载地址与编译时地址不匹配 : backtrace 生成的是程序运行时实际的内存地址,如果动态库加载时没有使用指定的加载地址,运行时地址会与 addr2line 预期的地址不符,即使包含调试信息也无法正确解析。动态库一般默认使用相对地址。但是地址重定位也是一种方式,通常情况,为了便于开发过程调试问题,动态库加载时要按照期望地址加载。
  4. 代码优化 : 编译器会执行多种代码优化。当开启了代码优化(如 -O2, -O3)时, 内联等操作可能会改变实际的代码结构,使 addr2line 很难找到对应的源码位置。在性能优化的同时,会牺牲一些代码调试信息。

解决方案

针对上述问题,可以采取以下几种方法进行解决。

1. 编译时包含调试信息

编译动态库或者可执行程序时,请务必加上 -g 参数,以生成必要的调试信息。
cmake 项目中可以配置编译类型来指定编译参数:

示例:

# CMakeLists.txt
set(CMAKE_BUILD_TYPE Debug)

设置 CMAKE_BUILD_TYPEDebug 时, 会在 CMAKE_CXX_FLAGSCMAKE_C_FLAGS 上自动添加 -g 标记。

确保编译过程中正确设置了编译参数。之后,重新编译项目,并在崩溃时生成新的堆栈跟踪,重新尝试使用 addr2line 命令。

2. 使用未剥离的二进制文件

确保用于 addr2line 解析的二进制文件,和生成崩溃堆栈跟踪时使用的动态库或可执行文件是同一个。 如果二进制文件被 strip 命令删除了调试信息, 请找到对应的未剥离的版本用于符号解析。 比如编译出来的 libvectorcorejni_x86_avx512.so 是开发阶段没有strip的lib,而在部署环境上,对lib文件做了 strip 处理,导致 addr2line 解析失败,需要使用本地未strip的 libvectorcorejni_x86_avx512.so 用于解析。
此方案可以有效定位运行崩溃的本地函数源码位置,但是不能找到具体代码行数。

3. 指定正确的库加载地址

当动态库有明确加载地址,运行时按照预定的加载地址进行加载,addr2line 才能解析对应的符号。

  1. 编译时指定链接起始地址
    -Wl,-Ttext,0x7fc724700000 -Wl,-soname,libvectorcorejni_x86_avx512.so
    
在链接过程中添加上述参数, `0x7fc724700000` 可以为指定虚拟地址, 此值可被设定为一个较为靠后的地址,用于避开和内存其他区域的冲突,  `-soname` 指定链接名字。
  1. 运行程序时加载地址需要与指定地址一致 :
    需要配置加载环境,避免动态链接库被加载到错误的地址,一种方法可以使用环境变量 LD_PRELOAD, 在动态库加载前使用 mmap 将so 文件映射到指定位置,或者在android等移动端平台上做相应的库加载指定。
    比如执行下列指令, 使用 preload lib 强行让 libvectorcorejni_x86_avx512.so 加载到 0x7fc724700000
```
LD_PRELOAD=./libvectorcorejni_x86_avx512.so  ./program
```

重新生成崩溃堆栈信息,并且保证动态库或者可执行程序的加载地址与 -Ttext 参数地址一致。 此时再次尝试 addr2line 就可以解析正确的代码行号。
使用固定加载地址需要注意:由于 ASLR 机制,需要手动指定地址,避免加载的动态库占用系统已使用内存地址导致崩溃。

4. 降低代码优化级别

如果上述方法还不能解决问题,可以尝试降低编译时的优化级别,或者暂时关闭代码优化, 这有助于确保代码的实际布局与调试信息保持一致。 例如,可以将编译优化标志由 -O2-O3 修改为 -O1 甚至 -O0

# CMakeLists.txt
set(CMAKE_CXX_FLAGS "-O0 -g")
set(CMAKE_C_FLAGS "-O0 -g")

优化等级为 O0 关闭代码优化,并增加 g 开启 debug。 虽然这种方式会降低性能, 但是在开发调试过程中十分有用。完成调试之后重新开启代码优化选项。

其他安全建议

  • 保护调试信息 : 包含调试信息的库文件,应该保存在安全的开发环境中,不应泄露到生产环境。 可以只把解析符号的工具放到生产环境进行线上符号解析。
  • 使用独立的崩溃报告工具 :对于更复杂的崩溃分析,可以考虑使用专业的崩溃报告工具,它们通常会提供更加丰富的信息和更好的符号解析功能, 可以更加快速,更加全面解析崩溃问题。比如可以使用 Breakpad, Sentry等崩溃报告系统。

总结
解决 JNI 项目崩溃堆栈回溯解析失败的关键在于确保编译时包含了调试信息,并且加载的地址与预期地址匹配,必要时,可以适当降低编译器的代码优化级别, 以便获取更准确的调试信息。结合合适的崩溃分析工具, 可以更好的解决崩溃问题。