3. 环境搭建:NDK环境配置、交叉编译工具链、addr2line与objdump工具安装

好,咱们正式开始动手。这一章我带你把吃饭的家伙事儿全装上。

做Native层的问题定位,说白了就是跟二进制文件打交道。你手里没工具,就像医生没有听诊器,光靠猜可不行。我个人习惯先把环境搭得干干净净,后面排查问题才能一气呵成。

3.1 NDK环境配置

NDK是什么?它就是Android上写C/C++代码的“编译器全家桶”。没有它,你的.so文件就编不出来。

第一步:下载NDK

我建议你直接用Android Studio自带的SDK Manager下载。路径很简单:

Tools -> SDK Manager -> SDK Tools -> 勾选 NDK (Side by side) -> Apply

嗯,这里要注意。新版Studio默认下载的是最新版NDK。但如果你维护的是老项目,可能会遇到兼容性问题。我曾经在一个项目里,因为NDK版本太高,导致编译出来的so在Android 5.0上直接崩溃。后来老老实实切回r21版本才搞定。

第二步:配置环境变量

下载完别急着用。你得让系统知道NDK在哪。我个人习惯在 ~/.bashrc~/.zshrc 里加上:

export ANDROID_NDK_HOME=$HOME/Library/Android/sdk/ndk/25.2.9519653
export PATH=$PATH:$ANDROID_NDK_HOME

配置完记得 source ~/.bashrc 让它生效。然后你敲一下 ndk-build --version,如果能打印出版本号,说明环境通了。

小技巧: 如果你同时维护多个项目,可以用软链接来管理NDK版本。比如 ln -s ndk-21 ndk-current,切换时只改链接指向就行。

3.2 交叉编译工具链

交叉编译,说白了就是在你的电脑上编译出能在手机上跑的程序。手机是ARM架构,电脑是x86架构,这中间需要一个“翻译官”。

NDK从r19版本开始,推荐使用独立的工具链。你不需要手动去下载什么gcc-arm-linux-androideabi,NDK里全给你准备好了。

获取工具链的方式:

$ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py \
    --arch arm64 \
    --api 29 \
    --install-dir ./my-toolchain

这条命令会在当前目录生成一个 my-toolchain 文件夹。里面包含了编译器、链接器、头文件等等。

你想想看,有了这个工具链,你就可以在命令行里直接编译C代码了:

./my-toolchain/bin/aarch64-linux-android29-clang main.c -o main

生成的 main 文件,直接push到手机上就能跑。我在早期做嵌入式开发时,经常用这个方法来验证一些底层算法,比每次都跑Android Studio快多了。

避坑指南: 我曾经遇到过一个问题——用工具链编译出来的so,在真机上加载时报“cannot locate symbol”。查了半天,发现是API level选错了。目标手机是Android 10,我选了API 29没问题,但链接的依赖库是API 28编译的。记住:工具链的API level一定要 <= 目标设备的API level。

3.3 addr2line与objdump工具安装

这两个工具,是破解Native Crash的“左膀右臂”。

addr2line:把地址变成代码行号

当你的app发生Native Crash时,logcat里会打印一串地址,比如:

signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
backtrace:
  #00 pc 0001a3b4  /data/app/lib/arm64/libnative-lib.so

这个 0001a3b4 是什么?是内存地址。你直接看它,啥也看不出来。但用addr2line一转换,就能定位到具体的源文件和行号。

安装方式:

addr2line就在NDK工具链里。你不需要额外安装。找到它:

$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-addr2line

注意,不同操作系统路径不同。Linux上是 linux-x86_64,Windows上是 windows-x86_64

使用示例:

llvm-addr2line -f -e libnative-lib.so 0001a3b4

输出结果:

_Z7crashMePv
/home/user/project/jni/crash.cpp:15

你看,一下子就定位到了 crash.cpp 的第15行。函数名是 crashMe。嗯,这里要注意,你的so文件必须带符号表(编译时不要strip),否则addr2line也救不了你。

objdump:反汇编利器

有时候光看行号还不够。比如你怀疑是编译器优化导致的问题,或者想看看某段代码生成的汇编指令。这时候objdump就派上用场了。

常用命令:

# 查看so文件的段信息
llvm-objdump -h libnative-lib.so

# 反汇编指定函数
llvm-objdump -d libnative-lib.so --disassemble-symbols=_Z7crashMePv

# 查看动态符号表
llvm-objdump -T libnative-lib.so

我个人最常用的是 -d 参数。有一次排查一个死锁问题,代码逻辑怎么看都没毛病。反汇编一看,发现某个全局变量被编译器优化到了寄存器里,导致多线程访问时数据不一致。这种问题,光看源码是看不出来的。

核心要点:
  • addr2line用于将地址转换为文件名和行号
  • objdump用于反汇编和查看二进制文件结构
  • 两个工具都集成在NDK的llvm工具链中
  • 一定要保留符号表(编译时加 -g 选项)

3.4 验证环境是否可用

工具都装好了,咱们来做个快速验证。写一个会崩溃的C程序:

// test_crash.c
#include <stdio.h>

void crash_me() {
    int *p = NULL;
    *p = 42;  // 故意解引用空指针
}

int main() {
    crash_me();
    return 0;
}

用交叉编译工具链编译:

aarch64-linux-android29-clang -g test_crash.c -o test_crash

然后push到手机上运行:

adb push test_crash /data/local/tmp/
adb shell /data/local/tmp/test_crash

你会看到类似这样的输出:

Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
backtrace:
  #00 pc 00000734  /data/local/tmp/test_crash

记下这个地址 00000734,然后用addr2line:

llvm-addr2line -f -e test_crash 00000734

如果输出是 crash_metest_crash.c:4,恭喜你,环境搭建成功了!

我的习惯: 我会把addr2line和objdump的路径加到alias里。比如 alias a2l='$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-addr2line'。这样每次敲命令能省不少时间。

好了,环境搭完了。下一章咱们就开始真正分析Crash日志。你把这些工具准备好,后面会省很多力气。