android-ndk开发(9): undefined reference to `__aarch64_ldadd4_acq_rel` 报错分析

发布于:2025-05-09 ⋅ 阅读:(12) ⋅ 点赞:(0)

1. 概要

基础库 libbase.a 基于 android ndk r18b 编译, 被算法库 libfoo.so 和算法库 libbar.a 依赖, 算法库则分别被 libapp1.so 和 libapp2.so 依赖。

libapp1.so 的开发者向 libfoo.so 的开发者反馈了链接报错:

error: undefined symbol: __aarch64_ldadd4_acq_rel

libapp2.so 的开发者向 libbar.a 的开发者反馈了链接报错:

undefined reference to `__aarch64_ldadd4_acq_rel'

libapp2.so 的开发者通过升级 NDK 版本解决了问题。 由于没有 libbar.a 和 libapp2.so 对应的 ndk 详细版本和最小复现代码,

在搜索引擎上以 “undefined reference to `__aarch64_ldadd4_acq_rel’” 为关键字找到相似案例并分析, 确定了升级 NDK 能消除上述链接报错的原因。

2. opencv issue 24856

2.1 报错描述

https://github.com/opencv/opencv/issues/24856 (参考链接[1])

用户 kevinzezel 在 android 平台, 使用 OpenCV 4.9.0 + ncnn, 遇到链接报错, 而使用 OpenCV 4.5.5~4.8.0 时没有报错。 报错信息是:

undefined reference to `__aarch64_ldadd8_acq_rel’

opencv 维护人员 asmorkalov 提议让用户开启 -mno-outline-atomics 编译选项, 用户 kevinzezel 反馈说 ndk 21.4.7075529 不支持这一选项.

catboost 维护者 andrey-khropov 说, 很可能是 clang 13.0.0-rc1 版本引入的问题, 对应到 commit: https://github.com/llvm/llvm-project/commit/c5e7e649d537067dec7111f3de1430d0fc8a4d11 (参考链接[2])。 解决方法: 要么使用低版本 clang (< 13.0.0), 要么用高版本 clang 但是传入 -mno-outline-atomics 选项。

用户 bvnp43 说, 升级 ndk 到 26.2.11394342 后问题解决。

用户 chenyan-master 说, 编译 V8 时遇到类似问题 (ndk 22.0.7026061), 升级 ndk 版本解决了。

2.2 分析

ncnn 版本没有被提及, 大概率是没有变化。 OpenCV 从 4.8.0 到 4.9.0 引发了链接报错, 猜测是因为 OpenCV 官方编译 android sdk 时更换了 ndk 版本, 而 ndk 版本和 clang 版本有一定的对应关系。

https://stackoverflow.com/questions/53385892/find-the-ndk-version-used-for-building-opencv-android-native-libraries (参考链接[3]) 给出了从 OpenCV 静态库反推编译他它的 ndk 版本、 ndk 里的 clang 版本的方法:

strings libopencv_core.a | ag "Android NDK"
strings libopencv_core.a | ag "C++ Compiler"
opencv ndk clang
4.9.0 25.2.9519653 14.0.7
4.8.0 18.1.5063045 7.0.2

ndk 和 clang 的版本对应关系, 则通过 clang++ --version 确定, e.g.

D:/soft/android-ndk/r21e/toolchains/llvm/prebuilt/windows-x86_64/bin/clang++ --version

ndk 的数字形式的 x.y.z 版本, 和字母形式的版本如 r21e, 则通过 <ndk-目录>/source.properties 确定, 例如 ndk-r21e:

Pkg.Desc = Android NDK
Pkg.Revision = 21.4.7075529

整理后的 ndk 和 clang 版本对应关系如下:

ndk ndk clang
r28b 28.1.13356709 19.0.0
r27c 27.2.12479018 18.0.3
r26d 26.3.11579264 17.0.2
r25c 25.2.9519653 14.0.7
r25b 25.1.8937393 14.0.6
r24 24.0.8215888 14.0.1
r23c 23.2.8568313 12.0.9
r22b 22.1.7171670 11.0.5
r21e 21.4.7075529 9.0.9
r21b 21.1.6352462 9.0.8
r20b 20.1.5948944 8.0.7
r19c 19.2.5345600 8.0.2
r18b 18.1.5063045 7.0.2

2.3 结论

根据前一节的两个表格知道:

opencv 4.8.0 是用 ndk-r18b 编译, 对应的编译器是 clang 7.0.2

opencv 4.9.0 是基于 ndk-r25c 编译, 对应的编译器是 clang 14.0.7

ndk 没有使用过 clang 13.0.0 版本, 是从 clang 12.0.9 (ndk-r23c) 直接升级到 clang 14.0.1 (ndk-r24)

编译选项 -moutline-atomics-mno-outline-atomics 是在 clang 13.0.0 引入的, ndk-r21e 使用的是 clang 9.0.9, 因此会提示“不支持 -mno-outline-atomics 选项”.

3. libgcc.a 的类似报错: __aarch64_swp4_acq_rel

3.1 问答描述

https://stackoverflow.com/questions/75045297/libgcc-linker-error-hidden-symbol-aarch64-swp1-acq-rel-in-libgcc-a-is-referen (参考链接[4]) 给出了 gcc aarch64 下的链接报错, __aarch64_swp4_acq_rel 符号找不到, 这和 opencv issue 24856 报错很像, 符号名字结尾略有差别。

用户 Bill Cong 的回答给出了很好的最小复现: 定义和使用 std::atomic<int> 变量, 使用 -O1 优化等级, 在不同编译器下查看对应的反汇编,

#include <atomic>

std::atomic<int> ai(3);

int main() {
    return ai.exchange(5);
}
  • ARM64 gcc 9.3, 不会生成 __aarch64_swp4_acq_rel 汇编指令

  • ARM64 gcc 10.2, 会生成 __aarch64_swp4_acq_rel 汇编指令

  • ARM64 gcc 10.2, 传入 -mno-outline-atomics 选项, 不会生成 __aarch64_swp4_acq_rel 汇编指令

https://godbolt.org/z/ssK3GGaoE (参考链接[5])

在这里插入图片描述

3.2 分析

-m-outline-atomics 编译选项是哪个版本引入 GCC 的? 含义是什么?

https://stackoverflow.com/questions/65239845/how-to-enable-mno-outline-atomics-aarch64-flag (参考链接[6]) 提到, gcc 9.4 开始提供 -m-outline-atomics 编译选项。

https://gcc.gnu.org/gcc-9/changes.html (参考链接[7]) 则证实了这一点
在这里插入图片描述

The option -moutline-atomics has been added to aid deployment of the Large System Extensions (LSE) on GNU/Linux systems built with a baseline architecture targeting Armv8-A. When the option is specified code is emitted to detect the presence of LSE instructions at run time and use them for standard atomic operations. For more information please refer to the documentation.

有人在编译 Android FFmpeg 5.0 后的运行阶段遇到报错 (参考链接 [8])

dlopen failed: cannot locate symbol “__aarch64_ldadd8_acq_rel”

参考链接[9] 则给出了关于 LSE 的进一步解释:

Out-of-line Atomics for LSE deployment

AArch64 Large System Extensions (LSE) were introduced in Armv8.1-A. These provide more efficient atomic instructions for large multi-core systems.

LLVM 12 adds support for a new flag ‘-moutline-atomics', which detects at runtime whether the processor supports LSE. It then uses these new atomic instructions if possible, falling back to Armv8.0-A LL/SC loops on processors without LSE support. This option behaviour mirrors similar support available within GNU family of projects.  We are working towards making this option enabled by default in the upcoming LLVM13 release.

即:

Armv8.1-A 引入了 LSE(大系统扩展), 用于在多核系统上提供更高效的原子指令。 LLVM12 添加了 -moutline-atomics 编译选项, 在运行时检查处理器是否支持 LSE, 如果存在则使用, 不存在则回退到 Arm8.0-A 的 LL/SC。 这个选项在 GNU 家族的工程中也提供了(就是 GCC)。 在 LLVM13 中 -moutline-atomics 选项则被默认开启。

clang 12.0.1 开始,添加了 -moutline-atomics-mno-outline-atomics 选项(参考链接 [10]):
在这里插入图片描述

而根据参考链接 [11], LLVM 的开发者们讨论提到, gcc 9.3.1 开始提供了 LSE 的支持, 并在 gcc 10.1 默认开启:

Outline atomics were added with gcc 9.3.1 and turned on by default in gcc 10.1

3.3 结论

-moutline-atomics 编译选项表示开启 LSE (Large System Extension), 意思是在运行时提供更高效率的原子操作, 在 Arm8.1-a 上起作用, 在 Arm8.0 上回退到原本的原子操作处理上。 -mno-outline-atomics 则关闭这一编译选项。

-moutline-atomics 是在 gcc 9.3.1 版本开始提供, 在 clang 12.0.1 版本开始提供, 但都是默认不开启状态。 在 gcc 10.1 版本和 clang 13.0.0-rc1 版本中默认开启了 -moutline-atomics 编译选项。

4. __aarch64_ldadd4_acq_rel__aarch64_swp4_acq_rel 指令, 对应的 C/C++ 代码是什么?

4.1 C++11

基于参考链接[5], 很容易构造如下代码, 并在 armv8-a clang 13.0.0 生成 __aarch64_ldadd4_acq_rel 指令 (仍然使用 -O1):

https://godbolt.org/z/T7vzesfxd (参考链接[12])

#include <atomic>
std::atomic<int> ai(3);

int main() {
    ai.fetch_add(1, std::memory_order_acquire);
    return ai.exchange(5);
}

汇编如下, 生成了 __aarch64_ldadd4_acq__aarch64_swp4_acq_rel 指令.

main:
        stp     x29, x30, [sp, #-32]!
        str     x19, [sp, #16]
        mov     x29, sp
        adrp    x19, ai
        add     x19, x19, :lo12:ai
        mov     w0, #1
        mov     x1, x19
        bl      __aarch64_ldadd4_acq
        mov     w0, #5
        mov     x1, x19
        bl      __aarch64_swp4_acq_rel
        ldr     x19, [sp, #16]
        ldp     x29, x30, [sp], #32
        ret

ai:
        .word   3

4.2 C++03: ncnn XADD 宏的实现

ncnn 库使用 C++03 编译, 在大部分平台使用到了 __atomic_fetch_add, __sync_fetch_add_add 等类似的 builtin 函数:

https://github.com/Tencent/ncnn/blob/20250503/src/allocator.h#L105-L151 (参考链接[13])

#if NCNN_THREADS
// exchange-add operation for atomic operations on reference counters
#if defined __riscv && !defined __riscv_atomic
// riscv target without A extension
static NCNN_FORCEINLINE int NCNN_XADD(int* addr, int delta)
{
    int tmp = *addr;
    *addr += delta;
    return tmp;
}
#elif defined __INTEL_COMPILER && !(defined WIN32 || defined _WIN32)
// atomic increment on the linux version of the Intel(tm) compiler
#define NCNN_XADD(addr, delta) (int)_InterlockedExchangeAdd(const_cast<void*>(reinterpret_cast<volatile void*>(addr)), delta)
#elif defined __GNUC__
#if defined __clang__ && __clang_major__ >= 3 && !defined __ANDROID__ && !defined __EMSCRIPTEN__ && !defined(__CUDACC__)
#ifdef __ATOMIC_ACQ_REL
#define NCNN_XADD(addr, delta) __c11_atomic_fetch_add((_Atomic(int)*)(addr), delta, __ATOMIC_ACQ_REL)
#else
#define NCNN_XADD(addr, delta) __atomic_fetch_add((_Atomic(int)*)(addr), delta, 4)
#endif
#else
#if defined __ATOMIC_ACQ_REL && !defined __clang__
// version for gcc >= 4.7
#define NCNN_XADD(addr, delta) (int)__atomic_fetch_add((unsigned*)(addr), (unsigned)(delta), __ATOMIC_ACQ_REL)
#else
#define NCNN_XADD(addr, delta) (int)__sync_fetch_and_add((unsigned*)(addr), (unsigned)(delta))
#endif
#endif
#elif defined _MSC_VER && !defined RC_INVOKED
#define NCNN_XADD(addr, delta) (int)_InterlockedExchangeAdd((long volatile*)addr, delta)
#else

但查看 ncnn 库文件的反汇编, 例如 ncnn-20250503-android-shared.zip, 并没有找到 __atomic_fetch_add 指令:

aarch64-linux-android-objdump -d ncnn-20250503-android-shared/arm64-v8a/lib/libncnn.so | ag '__aarch64_ldadd4_acq'

原因是 ncnn 的 CMakeLists.txt 里主动开启了 -mno-outline-atomics:

https://github.com/Tencent/ncnn/blob/master/src/CMakeLists.txt#L648-L652

    if(ANDROID_NDK_MAJOR AND (ANDROID_NDK_MAJOR GREATER_EQUAL 23))
        # llvm 12 in ndk-23 enables out-of-line atomics by default
        # disable this feature for fixing linking atomic builtins issue with old ndk
        target_compile_options(ncnn PRIVATE -mno-outline-atomics)
    endif()

(此处存疑, 可能是 ncnn 注释 typo, 个人理解是 llvm 13 和 ndk-r25 默认开启 out-of-line atomics; nihui: 使用 __aarch64_ldadd4_acq_rel后,树莓派上会 crash,链接器会有undefined行为, 因此 ncnn 未开启 )

4.3 自行构造

构造了简单直白的一份 MY_XADD 宏的实现: https://godbolt.org/z/x567r5Gfr (参考链接[14])

#if _MSC_VER
#   include <windows.h>
#   define MY_XADD(addr, delta) ::InterlockedExchangeAdd((volatile long*)(addr), delta)
#elif __linux__
#   define MY_XADD __sync_fetch_and_add
#endif

int main() {
    int refcount = 1;
    return MY_XADD(&refcount, -1);
}

使用 -O1 优化等级, 在各个编译器下结果如下:

在这里插入图片描述

  • armv8-a clang 12.0.0: 生成 “平凡的” ldaxr, stlxr 指令
  • armv8-a clang 13.0.0: 生成 __aarch64_ldadd4_acq_rel 指令
  • armv8-a clang 13.0.0, 生成 -mno-outline-atomics 编译选项: 生成 “平凡的” ldaxr, stlxr 指令
  • arm64 msvc v19.43 VS17.13: 生成 “平凡的” ldaxr, stlxr 指令
  • x86-64 gcc 15.1: 生成 lock xadd 指令

ldaxr 指令的解释:

Load-Acquire Exclusive Register derives an address from a base register value, loads a 32-bit word or 64-bit doubleword from memory, and writes it to a register. The memory access is atomic. The PE marks the physical address being accessed as an exclusive access. This exclusive access mark is checked by Store Exclusive instructions. See Synchronization and semaphores. The instruction also has memory ordering semantics as described in Load-Acquire, Store-Release. For information about memory accesses, see Load/Store addressing modes.

Load-Acquire Exclusive Register 指令根据基址寄存器的值计算地址,从内存中读取一个 32 位字(word)或 64 位双字(doubleword),并写入目标寄存器。此内存访问是原子的。处理单元(PE)会将正在访问的物理地址标记为“独占访问”,该标记稍后由 Store Exclusive 指令检查。详见“同步与信号量”一节。
此外,该指令具有 Load-Acquire/Store-Release 描述的内存排序语义。有关内存访问的更多信息,请参阅“装载/存储寻址模式”。

stlxr 指令的解释:

Store-Release Exclusive Register stores a 32-bit word or a 64-bit doubleword to memory if the PE has exclusive access to the memory address, from two registers, and returns a status value of 0 if the store was successful, or of 1 if no store was performed. See Synchronization and semaphores. The memory access is atomic. The instruction also has memory ordering semantics as described in Load-Acquire, Store-Release. For information about memory accesses, see Load/Store addressing modes.

Store-Release Exclusive Register(存储-释放独占寄存器)指令在处理元(PE)对某内存地址拥有独占访问权限时,可将两个寄存器中的数据写入内存:写入 32 位字或 64 位双字。如果写入成功,指令返回状态值 0;若未执行写入,则返回状态值 1。参见“同步与信号量”。

该内存访问是原子的。此指令还具备与“Load-Acquire / Store-Release”描述一致的内存排序语义。关于内存访问的详细说明,请参阅“加载/存储寻址模式”。

4.4 分析 libbase.a 库

前一节构造的最小复现代码,MY_XADD 宏的定义和使用, 对应到文章开头 libbase.a 基础库中的 BASE_XADD 宏实现:

base/atomic.hpp

#if _MSC_VER
#   include <windows.h>
#   define BASE_XADD(addr, delta) ::InterlockedExchangeAdd((volatile long*)(addr), delta)
#elif __linux__
#   define BASE_XADD __sync_fetch_and_add
#endif

base/atomic.hpp 被不同的编译器处理:

  • ndk-r18: clang 7.0.2, 编译出 libbase.a

  • ndk-r25c 或更高版本: clang >= 14.0.7, 编译出 libfoo.so 和 libbar.a

猜测 libapp1.solibapp2.so 不会接触到 base/atomic.hpp, 并且这两个 app so 是用 ndk-r24 或更低版本编译。 这样一来, libfoo.so 和 libbar.a 的编译过程产生了非预期的 __aarch64_ldadd4_acq_rel 汇编指令, 下游的集成人员遇到链接报错, 集成人员换到 clang >= 13.0.0 的 ndk 版本, 也就是 ndk >= r25, 就避免了报错。

另一种改法: 是 libfoo.solibbar.a 的构建过程, 做类似于 ncnn 的配置, 判断 ndk >= r24 并添加 -mno-outline-atomics 编译选项。

5. 总结

gcc 9.3.1 和 clang 12.0.1 添加了 -moutline-atomics 编译选项, 用来开启 LSE (Large System Extension), 用于在 Armv8.1-a 上改善多核情况下的原子操作的性能。

这个选项在 gcc 9 和 clang 12 是默认不开启的, 相当于是默认开启了 -mno-outline-atomics

从 gcc 10 和 clang 13 开始默认开启 -moutline-atomics 编译选项。

ndk 版本和 clang 版本有对应的关系, clang >= 13 对应到 ndk >= r24。

当使用 clang >= 13 (ndk >= r24) 编译了带有 __sync_fetch_and_add (C/C++ Builtin) 或 C++11 的 std::atomic<T> 的代码, 例如 opencv/ncnn 中的 XADD 的实现, 会生成 __aarch64_ldadd4_acq_rel 等 LSE 相关的汇编指令, 这样的二进制被 clang < 13 (ndk < r24) 的编译器做链接, 会遇到 __aarch64_ldadd4_acq_rel 符号找不到的问题。

当遇到上述报错, 可以对齐编译器版本到 clang >= 13 (ndk >= r24), 或手动指定 -mno-outline-atomics 选项。

如果是 clang 12 或 gcc 9 编译器, 但传入了 -moutline-atomics 选项, 也会发生类似的链接报错, 处理方式同前。

当然, 考虑到 ndk-r24 没有 ndk-24b, ndk-r24c 等版本, 建议用 ndk >= r25c 的版本。

References

  • [1] https://github.com/opencv/opencv/issues/24856
  • [2] https://github.com/llvm/llvm-project/commit/c5e7e649d537067dec7111f3de1430d0fc8a4d11
  • [3] https://stackoverflow.com/questions/53385892/find-the-ndk-version-used-for-building-opencv-android-native-libraries
  • [4] https://stackoverflow.com/questions/75045297/libgcc-linker-error-hidden-symbol-aarch64-swp1-acq-rel-in-libgcc-a-is-referen
  • [5] https://godbolt.org/z/ssK3GGaoE
  • [6] https://stackoverflow.com/questions/65239845/how-to-enable-mno-outline-atomics-aarch64-flag
  • [7] https://gcc.gnu.org/gcc-9/changes.html
  • [8] https://juejin.cn/post/7149468268674154510
  • [9] https://community.arm.com/arm-community-blogs/b/tools-software-ides-blog/posts/llvm12-for-arm
  • [10] https://releases.llvm.org/12.0.1/tools/clang/docs/ReleaseNotes.html
  • [11] https://reviews.llvm.org/D93585
  • [12] https://godbolt.org/z/T7vzesfxd
  • [13] https://github.com/Tencent/ncnn/blob/20250503/src/allocator.h#L105-L151
  • [14] https://godbolt.org/z/x567r5Gfr

网站公告

今日签到

点亮在社区的每一天
去签到