AARCH64 上的 __int128 为何在 SF32LB52 上“罢工”?
你有没有遇到过这种情况:代码写得顺风顺水,语法检查全绿,GCC 编译也通过了——结果一链接,蹦出一行红字:
undefined reference to `__multi3'
一脸懵?别急,这事儿我经历过。而且不是一次两次。
最近在调试一款国产安全芯片 SF32LB52 的时候,我就踩进了这个坑。项目里用了 unsigned __int128 来处理一个高精度乘法运算,本以为是标准操作,毕竟 AARCH64 架构 + GCC 工具链,按理说应该稳如老狗。可现实狠狠打了脸——编译没问题,链接直接报错, __multi3 找不到。
这就奇怪了:AARCH64 不是支持 __int128 吗?GCC 文档也写了从 4.1 版本开始就支持了啊?怎么到了这块板子上就不灵了?
后来才明白: 架构支持 ≠ 工具链完整 。就像给你一辆车的底盘和方向盘,却不给发动机——看起来能开,其实动不了。
那个被“阉割”的 libgcc
先说结论:
📌
__int128在 SF32LB52 上失效的根本原因,并非 CPU 不支持,也不是 GCC 不认语法,而是 libgcc.a 中缺失关键的 ti-mode 运行时函数 。
什么意思?我们来拆解一下。
__int128 其实是个“假原生”
很多人以为 __int128 是靠硬件指令实现的,比如有个 mul128 指令。但真相是—— ARMv8-A 根本没有原生 128 位整数运算指令 。
那它是怎么工作的?答案是: 软件模拟 ,靠的是 GCC 自带的一套“数学外挂包”,也就是 libgcc 。
当你写下这段代码:
unsigned __int128 result = (unsigned __int128)a * b;
GCC 看完之后心里嘀咕:“好家伙,我这 CPU 最多只能算 64×64=128,没法直接返回一个 128 位值。”于是它不做内联优化,转头调用了一个叫 __multi3 的函数。
没错,这个 __multi3 就是专门用来做 128 位乘法的运行时辅助函数,定义在 libgcc 里。类似的还有:
-
__addti3→ 加法 -
__subti3→ 减法 -
__ashlti3→ 左移 -
__udivmodti4→ 无符号除法与取模
这些函数统称为 TI-mode(tetra-integer mode) 支持,对应的是 128 位整数(16 字节)的操作。
所以你看, __int128 表面上是你在写 C 语言扩展,实际上背后全是函数调用。一旦 libgcc 里没把这些函数编进去……那就只能干瞪眼。
实测验证:nm 一下见真章
为了确认这一点,我直接对 SF32LB52 SDK 提供的 libgcc.a 做了个符号扫描:
$ nm -C libgcc.a | grep __ti
输出几乎是空的。
再拿标准 ARM GNU Toolchain 的 libgcc 对比:
$ nm -C /opt/arm-gnu-toolchain/lib/gcc/aarch64-none-linux-gnu/13/libgcc.a | grep __multi3
__multi3.o:
0000000000000000 T __multi3
看到了吧?人家有 __multi3.o 目标文件,而你的 SDK 里的 libgcc 压根就没把这个模块打包进去。
为啥会这样?很简单: 裁剪过头了 。
为什么厂商要把 libgcc 给“瘦身”?
SF32LB52 是什么定位?工业控制、电力系统、国密算法加持的安全 MCU。这类芯片往往有几个特点:
- Flash 资源紧张(可能只有几 MB)
- 启动时间要求苛刻
- 安全性优先,尽量减少攻击面
所以厂商在构建工具链时,往往会做深度裁剪。他们的逻辑很清晰:
“客户主要跑的是加密协议、实时控制、通信栈——谁会在嵌入式设备上搞 128 位大数乘法?这种冷门功能不要也罢。”
于是,像 __addti3 、 __multi3 这种“非主流”函数就被当成了“冗余代码”给删了。
听起来合理吧?但问题在于: 他们没告诉你这件事 。
SDK 文档里不会写“不支持 __int128 ”,官网规格书也不会提“libgcc 被精简”。一切都要等到你真正用到那一刻,才会收到那个冰冷的链接错误。
这就好比买车,说明书上写着“支持自动驾驶”,结果交车才发现激光雷达是塑料模型……
怎么办?三条路,总有一条走得通
既然知道了病根,接下来就是治病。
你当然可以换平台、升级工具链,但在实际项目中,很多时候你根本没得选。所以我们得务实一点,看看有哪些可行的 workaround。
✅ 路径一:自己动手,丰衣足食 —— 手动实现 128 位运算
最稳妥的方式,就是彻底绕开 __int128 ,用纯 C 实现你需要的功能。
比如最常见的需求:两个 uint64_t 相乘,得到 128 位结果。
我们可以用经典的“分治法”来计算,把每个 64 位数拆成高低 32 位,然后像小学竖式乘法一样展开:
void umul128(uint64_t a, uint64_t b, uint64_t *high, uint64_t *low) {
uint64_t a_lo = (uint32_t)a;
uint64_t a_hi = a >> 32;
uint64_t b_lo = (uint32_t)b;
uint64_t b_hi = b >> 32;
uint64_t t = a_lo * b_lo;
uint64_t w1 = t & 0xFFFFFFFFULL;
uint64_t k = t >> 32;
t = a_hi * b_lo + k;
k = t >> 32;
uint64_t w2 = t & 0xFFFFFFFFULL;
uint64_t w3 = k;
t = a_lo * b_hi + w2;
k = t >> 32;
w2 = t & 0xFFFFFFFFULL;
*low = w1 | (w2 << 32);
*high = a_hi * b_hi + w3 + k;
}
这段代码虽然看着啰嗦,但它有三大优势:
- 零依赖 :不需要任何外部库;
- 确定性行为 :完全可控,适合安全认证场景;
- 可移植性强 :哪怕你在 8 位单片机上也能跑。
💡 小技巧:如果你只关心高位溢出(比如判断是否溢出),甚至可以更简化:
uint64_t mulhi(uint64_t a, uint64_t b) {
uint64_t a_lo = a & 0xFFFFFFFFUL;
uint64_t a_hi = a >> 32;
uint64_t b_lo = b & 0xFFFFFFFFUL;
uint64_t b_hi = b >> 32;
return a_hi * b_hi + ((a_hi * b_lo + a_lo * b_hi) >> 32);
}
适用于 CRC、哈希、随机数生成等只需要部分高位信息的场景。
✅ 路径二:借尸还魂 —— 注入完整的 libgcc 模块
如果你实在不想重写一堆数学逻辑,还有一个“野路子”: 从标准工具链中提取缺失的目标文件,手动链接进去 。
具体步骤如下:
第一步:找一台装有完整 AARCH64 工具链的机器
可以用 ARM 官方发布的 GNU Arm Embedded Toolchain ,或者 Linaro 的发行版。
假设路径为:
/opt/arm-gnu-toolchain/lib/gcc/aarch64-none-linux-gnu/13/libgcc.a
第二步:找出需要的 .o 文件
$ ar t libgcc.a | grep multi
__multi3.o
也可以批量查找所有 TI-mode 函数:
$ ar t libgcc.a | grep -E "(add|sub|mul|div|mod).*ti"
__addti3.o
__subti3.o
__multi3.o
__divti3.o
__udivmodti4.o
__ashlti3.o
__lshrti3.o
第三步:提取并链接
$ ar x libgcc.a __multi3.o __addti3.o __ashlti3.o
$ aarch64-sf32-linux-gnu-gcc test.c __multi3.o __addti3.o -o test
✅ 成功!现在你的程序就能正常链接了。
⚠️ 但注意几个风险点:
- ABI 兼容性 :必须确保目标架构、字节序、调用约定一致。否则运行时崩溃都不是不可能。
- 体积膨胀 :每个
.o文件大概几 KB,积少成多会影响固件大小。 - 版权合规 :某些 SDK 使用的是修改过的 GPL 工具链,擅自引入外部 object 可能违反许可协议。
建议仅用于原型验证或内部测试,正式发布前务必评估法律和技术风险。
✅ 路径三:优雅降级 —— 条件编译 + 类型抽象
真正的高手,从来不依赖某个特定平台的能力。他们会写出 自适应 的代码。
怎么做?很简单:用宏检测 __int128 是否可用,自动切换实现。
#if defined(__SIZEOF_INT128__) && !defined(SF32LB52_DISABLE_INT128)
// 平台支持 __int128,直接使用
typedef unsigned __int128 uint128_t;
static inline void umul128(uint64_t a, uint64_t b, uint64_t *high, uint64_t *low) {
unsigned __int128 r = (unsigned __int128)a * b;
*high = r >> 64;
*low = r;
}
#else
// 回退到结构体模拟
typedef struct {
uint64_t high;
uint64_t low;
} uint128_t;
static inline void umul128(uint64_t a, uint64_t b, uint64_t *high, uint64_t *low) {
// 此处填入上面的手动实现
...
}
#endif
这样一来,你的代码就可以做到:
- 在服务器、通用 Linux AARCH64 上走 fast path,性能最优;
- 在 SF32LB52 或其他受限平台上自动 fallback,保证可编译。
更进一步,你还可以加个编译时断言,提醒开发者注意:
#if !defined(__SIZEOF_INT128__)
#warning "__int128 is not supported on this platform"
#endif
或者在 CMake 中加入检测机制:
include(CheckCSourceCompiles)
set(CMAKE_REQUIRED_QUIET ON)
check_c_source_compiles("
#if defined(__SIZEOF_INT128__)
int main() { return 0; }
#else
bad
#endif
" HAS_INT128)
if(HAS_INT128)
add_compile_definitions(HAS_UINT128)
endif()
这才是工程化的正确姿势: 让工具替人干活,而不是让人去猜工具能不能用 。
更深层的思考:我们到底该不该用语言扩展?
讲到这里,你可能会问:既然 __int128 如此脆弱,那我们是不是干脆别用了?
我的看法是: 要用,但不能盲目用 。
什么时候可以用?
- 快速原型开发 ✅
- 算法验证阶段 ✅
- 服务器端高性能计算 ✅
- 已知工具链完整的环境 ✅
这时候 __int128 真的是神器。几行代码搞定的事,何必写几十行分段乘法?
什么时候要小心?
- 跨平台嵌入式开发 ❗️
- 使用定制化 SDK 的国产芯片 ❗️
- 需要通过功能安全认证(如 ISO 26262、IEC 61508)❗️
- 对固件体积敏感的应用 ❗️
在这种环境下,过度依赖编译器扩展等于把命运交给别人。
我记得有一次,在一个电力终端项目中,团队成员用了 __int128 做时间戳合并,结果在现场调试时发现设备启动失败。查了三天才发现是链接时报了 __addti3 未定义……最后还是回退到了结构体方案。
那次教训让我明白: 在资源受限的嵌入式世界里,最简单的才是最可靠的 。
如何提前避坑?实战建议清单
为了避免下次再被“链接错误”偷袭,我总结了一套实用的检查流程,推荐你在新平台开发初期就执行:
🔍 1. 检查 __SIZEOF_INT128__ 是否定义
echo | gcc -dM -E - | grep __SIZEOF_INT128__
如果有输出:
#define __SIZEOF_INT128__ 16
说明编译器层面支持。
但这还不够!
🔍 2. 检查 libgcc 是否包含 TI-mode 函数
nm -C path/to/libgcc.a | grep __multi3
如果找不到,那就意味着即使语法支持,也无法链接成功。
🔍 3. 写个最小测试用例验证
// test.c
unsigned __int128 test_func(uint64_t a, uint64_t b) {
return a * b;
}
int main() {
volatile uint64_t x = 123, y = 456;
volatile unsigned __int128 z = test_func(x, y);
return (int)(z & 1);
}
然后尝试编译链接:
${CROSS_COMPILE}gcc test.c -o test
如果失败,立刻警觉。
🔍 4. 查阅 SDK 发行说明(如果有)
有些厂商会在 release notes 中提到“libgcc 裁剪列表”或“不支持的语言特性”。虽然不多见,但值得一看。
🔍 5. 主动联系技术支持
别害羞!直接问:“你们的工具链是否完整支持 __int128 ?libgcc 是否包含 __multi3 和 __addti3 ?”
有时候,一句回复能省你三天 debug 时间。
写在最后:工程师的“怀疑精神”
回到最初的问题:AARCH64 支持 __int128 吗?
技术上讲, 是的,GCC 支持 。
但实际上呢? 不一定能用 。
这就是嵌入式开发的真实写照:文档写的是一回事,实际跑起来又是另一回事。
特别是面对越来越多的国产化替代芯片——它们带来了自主可控的优势,但也伴随着生态不完善、文档缺失、工具链差异等问题。
作为开发者,我们不能停留在“理论上可行”的层面。我们必须养成一种习惯:
🧠 永远不要假设某个特性一定存在。
编译能过 ≠ 链接能成
链接能成 ≠ 运行正确
本地能跑 ≠ 客户现场稳定
每一次引入新特性,都应该带着“怀疑精神”去做验证。不是不信任工具链,而是对自己负责。
毕竟,最终签字交付产品的那个人,是你。
🛠 所以下次当你想敲下 unsigned __int128 的时候,不妨先停下来问自己一句:
“我的 libgcc 里,真的住着
__multi3这个房客吗?”
如果不确定,那就先打个 nm 确认一下。
宁可前期多花五分钟,也不要后期熬夜三天找链接错误 😅
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
2514

被折叠的 条评论
为什么被折叠?



