从 C 到 Rust:GDEV GPU 运行时移植纪实

在 QEMU 环境进行测试


背景

前四个阶段的翻译工作已全部完成,196 个测试在无硬件环境下全部通过。接下来的目标是将编译出的动态库部署到 QEMU 模拟的 CentOS 7 虚拟机中,用真实的 C 测试程序验证能否正常调用。

测试环境由跳板机上的 CentOS 7 QEMU 模拟器提供,其中已包含原版的 libgdev.solibusdaa.so,以及配套的 C 测试样例(如 launch.c)。


第一步:让 Rust 代码产出 C 可链接的 .so

为了让 C 测试程序能调用 Rust 写的 gdev 库,我们在现有代码上做了三个最小化改动:

  1. 启用 cdylib 输出
# layer2/Cargo.toml + layer4/Cargo.toml  
[lib]  
crate-type = ["cdylib", "rlib"]  

Rust 默认只产出 .rlib(Rust 专用格式),"cdylib" 则指示 Cargo 额外生成一个 C 兼容的动态库(.so),遵循系统 ABI,使 gcc/ld 可以直接链接。

  1. 暴露 C ABI 符号
    新建 layer2/src/ffi.rs(约 200 行),为 layer2 的 33 个内部函数加上 #[no_mangle] pub unsafe extern "C" fn。例如:
#[no_mangle]  
pub unsafe extern "C" fn gopen(minor: u32) -> u64 {  
    // 内部实现返回 Result<u64,i32>,在 FFI 边界转为 C 兼容的 u64  
}  

这样 C 链接器就能看到 gopengclosegmallocglaunch 等符号,并且使用 C 的调用约定。

  1. 模块声明
    layer2/src/lib.rs 中加入 pub mod ffi;,将 FFI 入口纳入编译。

构建结果:cargo build --release 0 errors,产出了两个 .so 文件——libgdev_layer2.solibgdev_layer4.so,分别对标原版的 libgdev.solibusdaa.so


第一次尝试:替换 .so 并编译测试

我们将编译好的两个 .so 传到 QEMU 虚拟机,替换掉 /usr/local/gdev/lib64 下的原版库,然后执行 make 编译 C 测试程序:

[root@qemu-ai-ep ~]# cd test/sdaa/launch2
[root@qemu-ai-ep launch2]# make
g++ -o user_test -std=c++11 -L /usr/local/gdev/lib64 -I /usr/local/gdev/include main.c launch.c parse_args.c -lusdaa -lgdev
launch.c: In function ‘int sdaa_test_launch(long unsigned int, int)’:
launch.c:97:57: warning: deprecated conversion from string constant to ‘char*’ [-Wwrite-strings]
    __sdaaRegisterFunction( sdaaFatCubinHandle,"add_test");
                                                         ^
/tmp/cce9af1r.o: In function `sdaa_test_launch(unsigned long, int)':
launch.c:(.text+0x2ce): undefined reference to `__sdaaRegisterFunction'
launch.c:(.text+0x2f9): undefined reference to `sdaaGetErrorString'
launch.c:(.text+0x357): undefined reference to `sdaaGetErrorString'
launch.c:(.text+0x3b5): undefined reference to `sdaaGetErrorString'
launch.c:(.text+0x419): undefined reference to `sdaaGetErrorString'
launch.c:(.text+0x47d): undefined reference to `sdaaGetErrorString'
/tmp/cce9af1r.o:launch.c:(.text+0x639): more undefined references to `sdaaGetErrorString' follow
/usr/local/gdev/lib64/libusdaa.so: undefined reference to `fstat64@GLIBC_2.33'
/usr/local/gdev/lib64/libusdaa.so: undefined reference to `pthread_key_delete@GLIBC_2.34'
/usr/local/gdev/lib64/libusdaa.so: undefined reference to `pthread_setspecific@GLIBC_2.34'
/usr/local/gdev/lib64/libusdaa.so: undefined reference to `pthread_key_create@GLIBC_2.34'
/usr/local/gdev/lib64/libusdaa.so: undefined reference to `stat64@GLIBC_2.33'
collect2: error: ld returned 1 exit status
make: *** [all] Error 1  

结果是一连串的链接错误:

  • undefined reference to '__sdaaRegisterFunction'
  • undefined reference to 'sdaaGetErrorString'
  • undefined reference to 'fstat64@GLIBC_2.33'
  • undefined reference to 'stat64@GLIBC_2.33'
  • undefined reference to 'pthread_key_create@GLIBC_2.34'
  • undefined reference to 'pthread_key_delete@GLIBC_2.34'
  • undefined reference to 'pthread_setspecific@GLIBC_2.34'

问题分两类:缺失的函数符号(__sdaaRegisterFunctionsdaaGetErrorString)和 glibc 版本不匹配。


修复一:补齐缺失的 API

我们很快在 layer4/src/entry.rs 中补上了两个函数:

// 测试代码中实际调用的是双下划线版本  
#[no_mangle]  
pub unsafe extern "C" fn __sdaaRegisterFunction(  
    handle: *mut c_void,  
    name: *const c_char,  
) {  
    sdaaRegisterFunction(handle, name);  
}  

// 错误码转字符串,供测试宏使用  
#[no_mangle]  
pub unsafe extern "C" fn sdaaGetErrorString(error: SdaaError) -> *const c_char {  
    match error {  
        0 => b"sdaaSuccess0".as_ptr() as *const c_char,  
        1 => b"sdaaErrorInvalidValue0".as_ptr() as *const c_char,  
        // ...  
        _ => b"sdaaErrorUnknown0".as_ptr() as *const c_char,  
    }  
}  

修复二:glibc 符号版本

真正的麻烦在后面。QEMU 环境是 CentOS 7,glibc 版本为 2.17(发布于 2012 年),而我们的开发机是 glibc 2.33+。Rust 标准库在编译时,宿主机 pthread.h 声明的 pthread_key_create 被标记为 @@GLIBC_2.34,这个版本标签被刻进了 Rust std 的 rlib 中,最终传染给我们的 .so

因此,即使补齐了所有函数,链接器依然会报告:

undefined reference to `fstat64@GLIBC_2.33'  

等等。这些符号在 CentOS 7 的 libc.so.6 中实际存在,只是版本标签是 @GLIBC_2.2.5


尝试:用 .symver 创建兼容库

我们想到了一个折中方案——编写一个微型共享库 libglibc_compat.so,利用汇编器指令 .symver 为高版本符号创建别名,指向系统提供的低版本实现。

┌────────────────┐      需要 fstat64@GLIBC_2.33  
│ libgdev_layer4 │ ──────────────────────┐  
└────────────────┘                       ▼  
                              ┌──────────────────────┐  
                              │ libglibc_compat.so    │  
                              │ .symver fstat64,      │  
                              │   fstat64@GLIBC_2.33  │  
                              │ 内部调用 →             │  
                              │   fstat64@GLIBC_2.2.5 │  
                              └──────────────────────┘  
                                        │  
                                        ▼  
                              ┌──────────────────────┐  
                              │   系统 libc.so.6      │  
                              │   (glibc 2.17)       │  
                              └──────────────────────┘  

这一招在链接阶段确实生效了——编译通过,不再报符号未定义。


新的关卡:ELF 头部版本锁

然而,编译成功后的程序运行时直接报错:

[root@qemu-ai-ep launch2]# make
g++ -o user_test -std=c++11 -L /usr/local/gdev/lib64 -I /usr/local/gdev/include main.c launch.c parse_args.c -lgdev_layer4 -lglibc_compat
launch.c: In function ‘int sdaa_test_launch(long unsigned int, int)’:
launch.c:97:57: warning: deprecated conversion from string constant to ‘char*’ [-Wwrite-strings]
    __sdaaRegisterFunction( sdaaFatCubinHandle,"add_test");
                                                         ^
#g++ -o user_test -std=c++11 -L /usr/local/gdev/lib64 -I /usr/local/gdev/include  add.c -lgdev_layer4 -lglibc_compat
[root@qemu-ai-ep launch2]# 
[root@qemu-ai-ep launch2]# 
[root@qemu-ai-ep launch2]# ./user_test 
./user_test: /lib64/libc.so.6: version `GLIBC_2.18' not found (required by /usr/local/gdev/lib64/libgdev_layer4.so)
./user_test: /lib64/libc.so.6: version `GLIBC_2.25' not found (required by /usr/local/gdev/lib64/libgdev_layer4.so)
./user_test: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by /usr/local/gdev/lib64/libgdev_layer4.so)
./user_test: /lib64/libc.so.6: version `GLIBC_2.30' not found (required by /usr/local/gdev/lib64/libgdev_layer4.so)
./user_test: /lib64/libc.so.6: version `GLIBC_2.33' not found (required by /usr/local/gdev/lib64/libgdev_layer4.so)
./user_test: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by /usr/local/gdev/lib64/libgdev_layer4.so)

这次不是某个符号找不到,而是动态链接器 ld.so 在加载 .so 时检查其 ELF 头部中的 DT_VERNEED 段,发现文件“要求 libc.so.6 >= GLIBC_2.34”。无论有没有兼容库,版本检查不通过就直接拒绝加载。

.symver 只能给函数加上兼容标签,但无法抹去 ELF 头部中记录的最低版本需求。


目前后续解决方案:在测试环境上编译gdev

问题的根源在于:Rust 编译时链接的宿主 glibc 将其自身的版本需求写入了产出物的 .dynamic 段。要真正产出与 glibc 2.17 兼容的二进制,编译器必须直接链接 glibc 2.17 的库

因此,最直接的方案是:在 QEMU 虚拟机(CentOS 7)上安装 Rust 工具链,将源代码传上去,本地编译。

这样产出的 libgdev_layer4.so 会自动链接到系统的 glibc 2.17,无需任何兼容层,也无需 LD_PRELOAD,零额外开销,完全符合原生部署的要求。目前qemu上与外部centos网络不联通不能安装rust环境,可能还有会有centos版本过低导致安装的rustc版本低的情况,还要继续进一步调整好环境进行测试。


更多推荐