Noz Wu

f128 Libcall Mismatch on Wasm64

前段时间发现一个比较有意思的 Bug,简单来说就是 wasm64-unknown-unknownf128 进行比较会导致产物出现问题,而且默认情况下编译过程不会出现任何报错。

如何复现

[package]
name = "libcall-mismatch-on-wasm64"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ['cdylib', 'rlib']
#![feature(f128)]

#[unsafe(no_mangle)]
pub fn issue(a: f128, b: f128) -> bool {
    a > b
}

nightly 已经修复了这个问题,我们安装一个老版本,然后我们编译 wasm64-unknown-unknown 目标:

rustup toolchain install nightly-2026-04-16
rustup component add rust-src --toolchain nightly-2026-04-16
cargo +nightly-2026-04-16 build --target wasm64-unknown-unknown \
    -Zbuild-std=panic_abort,std --release

可以看见的是,编译没有任何报错,但是实际上 wasm 已经损坏:

$ wasm-tools print target/wasm64-unknown-unknown/release/libcall_mismatch_on_wasm64.wasm 
(module $libcall_mismatch_on_wasm64.wasm
  (func $signature_mismatch:__gttf2 (;0;) (type 0) (param i64 i64 i64 i64) (result i32)
    unreachable
  )
  (func $issue (;1;) (type 0) (param i64 i64 i64 i64) (result i32)
    local.get 0
    local.get 1
    local.get 2
    local.get 3
    call $signature_mismatch:__gttf2
    i32.const 0
    i32.gt_s
  )
)

可以看见我们的 > 的操作实际上是调用了一个叫做 signature_mismatch:__gttf2 的函数,但是这个函数内部的实现被替换成了 unreachable,执行的时候会立刻抛出异常。

异常的符号

要想解释这一切,我们需要知道这个叫做 signature_mismatch:_gttf2 是做什么的,_gttf2 实际上是:

gt = greater than
tf = tetra floating (a 16-byte (128-bit) floating-point data type)

也就是用于比较 128 浮点数的函数,符合我们的预期。

对于 signature_mismatch,通过翻阅 lld 的源码,它实际上是 lld 对在不同的 object,相同名字但是不同签名的符号做的处理,它会将符号名字改成 signature_mismatch:<NAME> ,并将实现替换成 unreachable。lld 会对这种情况会发出 warn 或者 error,而我们 Rust 默认不会打印 warning 信息,现在我们往 Cargo.toml 写入并重新编译:

[lints.rust]
linker_messages = "warn"
warning: linker stderr: rust-lld: function signature mismatch: __gttf2
>>> defined as (i64, i64, i64, i64) -> i32 in libcall_mismatch_on_wasm64.libcall_mismatch_on_wasm64.329d4cb22e829c3b-cgu.0.rcgu.o
>>> defined as (i64, i64, i64, i64) -> i64 in libcompiler_builtins-98dc221c6c1a2013.rlib

可以看到问题已经显现,出现了不同签名的符号,前者的返回值是 i32,后者的返回值是 i64。接下来我们需要看看 _gttf2 从何而来。

libcall 与 compiler-rt

当我们使用 > 对 f128 进行比较时,LLVM 会将它变成对 _gttf2 的调用,这被称为 「lowering to libcall」,然后 _gttf2 的实现是由 compiler-rt 提供。对于 Rust 的 wasm64-unknown-unknown 目标来说,compiler-rt 就是 compiler-builtins

经过简单搜索,可以找到 compiler-builtins_gttf2 的返回值定义

cfg_if! {
    if #[cfg(any(target_arch = "aarch64", target_arch = "arm64ec"))] {
        // Aarch64 uses `int` rather than a pointer-sized value.
        pub type CmpResult = i32;
    } else if #[cfg(target_arch = "avr")] {
        // AVR uses a single byte.
        pub type CmpResult = i8;
    } else {
        // The default is word-sized. In LLVM's compiler-rt, this is done by using `long long` on
        // LLP64 ABIs and `long` on everything else.
        pub type CmpResult = isize;
    }
}

不难看出 wasm64 目标下,CmpResult 的类型是 i64。但是这个类型也不是 Rust 拍脑袋定的,它严格和 LLVM 的定义对齐:

// GCC uses long (at least for x86_64) as the return type of the comparison
// functions. We need to ensure that the return value is sign-extended in the
// same way as GCC expects (since otherwise GCC-generated __builtin_isinf
// returns true for finite 128-bit floating-point numbers).
#if defined(__aarch64__) || defined(__arm64ec__)
// AArch64 GCC overrides libgcc_cmp_return to use int instead of long.
typedef int CMP_RESULT;
#elif __SIZEOF_POINTER__ == 8 && __SIZEOF_LONG__ == 4
// LLP64 ABIs use long long instead of long.
typedef long long CMP_RESULT;
#elif __AVR__
// AVR uses a single byte for the return value.
typedef char CMP_RESULT;
#else
// Otherwise the comparison functions return long.
typedef long CMP_RESULT;
#endif

结论已经清晰,LLVM 的 libcall 对于比较 f128 的返回类型为 i32,但是 compiler-rt 的返回值按照字长处理,它们不一致导致了这个问题。不过另一个广泛使用的目标 wasm64-unknown-emscripten 为什么没这个问题?原来是它们已经打了补丁

#if defined(__aarch64__) || defined(__arm64ec__) || defined(__wasm__)
typedef int CMP_RESULT;

如何修复

现在有两种解决方案:一种是将 libcall 的返回值也改成字长,另一种是将 compiler-rt 的 wasm 的返回值固定成 i32。结论是固定成 i32 比较划算,因为这个比较返回的值不需要那么大的范围。以下是相关 issue 和 PR:

https://github.com/rust-lang/compiler-builtins/issues/1199

https://github.com/rust-lang/compiler-builtins/pull/1203

https://github.com/llvm/llvm-project/issues/192416

https://github.com/llvm/llvm-project/pull/194093

让我感到诧异的是,这个 Bug 应该一开始就存在,但是这么久了都没人发现,看来在 wasm64-unknown-unknown 中使用 f128 的人还是太少了。

#wasm #llvm

Reply to this post by email ↪