当前位置: 首页 > news >正文

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

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"
opencvndkclang
4.9.025.2.951965314.0.7
4.8.018.1.50630457.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 版本对应关系如下:

ndkndkclang
r28b28.1.1335670919.0.0
r27c27.2.1247901818.0.3
r26d26.3.1157926417.0.2
r25c25.2.951965314.0.7
r25b25.1.893739314.0.6
r2424.0.821588814.0.1
r23c23.2.856831312.0.9
r22b22.1.717167011.0.5
r21e21.4.70755299.0.9
r21b21.1.63524629.0.8
r20b20.1.59489448.0.7
r19c19.2.53456008.0.2
r18b18.1.50630457.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 deploymentAArch64 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, spadrp    x19, aiadd     x19, x19, :lo12:aimov     w0, #1mov     x1, x19bl      __aarch64_ldadd4_acqmov     w0, #5mov     x1, x19bl      __aarch64_swp4_acq_relldr     x19, [sp, #16]ldp     x29, x30, [sp], #32retai:.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 ndktarget_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
#endifint 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

相关文章:

  • 网络靶场基础知识
  • rest_framework学习之认证 权限
  • 重定向及基础实验
  • Python变量作用域
  • [学习]RTKLib详解:ephemeris.c与rinex.c
  • 如何修复WordPress数据库
  • Vscode (Windows端)免密登录linux集群服务器
  • Linux中的防火墙
  • 【Linux学习笔记】基础IO之理解文件
  • 学成在线之缓存
  • 【金仓数据库征文】金仓数据库 KES:MySQL 迁移实用指南
  • 服务器数据恢复—Linux操作系统服务器意外断电导致部分文件丢失的数据恢复
  • 《运维那些事儿》专栏总目录(持续更新)
  • 如何解决 Linux 系统文件描述符耗尽的问题
  • vue2 结合后端预览pdf 跨域的话就得需要后端来返回 然后前端呈现
  • vue中scss使用js的变量
  • uniapp上架苹果APP Store踩雷和部分流程注意事项(非完整流程)
  • uniapp|实现多终端聊天对话组件、表情选择、消息发送
  • CSS3 过渡与动画
  • XML简单介绍
  • 人民日报整版调查:中小学春秋假,如何放得好推得开?
  • 公示!17个新职业、42个新工种亮相
  • 44岁街舞运动推广者、浙江省街舞运动协会常务理事钟永玮离世
  • 绿城约13.93亿元竞得西安浐灞国际港港务片区地块,区内土地楼面单价首次冲破万元
  • 美联储主席:不打算先发制人地降息,将继续观望
  • 世界哮喘日|专家:哮喘无法根治,“临床治愈”已成治疗新目标