龙芯上的 Fun:从跑起来到机器码

bash 复制代码
作者: 一了 <1liao3@funlang.org>
日期: 2026-11-10

一句话总结: 从 FPC trunk 自举到 musl sysroot,从 qemu 回归到与 GNU as 逐字节对拍的龙芯编码器,这一个月把龙芯这一关过掉了大半。
  

龙芯上的 Fun:从跑起来到机器码

十月六日那篇《ARM 上的 Fun:只差一层机器码》末尾留了一句,说龙芯排在 ARM 后面,Fun 源码几乎不用动,门槛在 FPC:稳定版没有这个目标,得用 trunk 自举,TCC 更是压根没有 LoongArch 后端。最后给了一个很窄的验收条件,先看 FPC trunk 能不能稳定编出能跑的 funcmd。

一个月后,这一关过了。11 月 9 日晚上到 10 日深夜,上游十一笔龙芯提交,把交叉工具链、musl 运行库、qemu 回归和 lib-asm-loongarch64 编码器都落了地。ROADMAP 里拆成两行:P-09(交叉构建、解释器/标准库、汇编后端)记 DONE,P-16(真机验收、CI、发布包、TCC 策略)还开着。

先把边界说清楚。能跑,指的是交叉编出来的 funcmd 在 qemu-user 加 musl sysroot 里跑通了 115 项 Linux 回归;能编机器码,指的是编码器与 GNU as 逐字节一致,QEMU 解码器做了独立复核。至于在真正的 3A5000/3A6000 上执行 #!asm,还没验。编码器写得再对,也得有台机器听一遍才算数。

一、门槛确实在 FPC

bootstrap-linux-loongarch64.sh 干的是自举,不是「装个包」。FPC 稳定版 3.2.4 仍然没有 loongarch64 目标,只有 trunk(3.3.1)有 compiler/loongarch64/、rtl/loongarch64/ 和 rtl/linux/loongarch64/。脚本先用 3.2.x 的 ppcx64 把 trunk 的本地编译器 cycle 一遍,再以它为宿主编交叉编译器,最后补三组单元:RTL、rtl-objpas(variants / dateutils / strutils)、hash(md5 / sha1)。

汇编器和链接器借用 GNU binutils 的 loongarch64-linux-gnu-*。脚本在 $HOME/.fun/linux-loongarch64/tools 下按 FPC 的命名习惯建一组软链,loongarch64-linux-as、-ld、-ar、-ranlib,再让 ppcrossloongarch64 用 -XP、-FD 指过去。整套东西放在用户目录,不碰系统,也不要求 root。

(吐槽:FPC 稳定版到现在还不认识龙芯,得像拼家具一样先从 trunk 自举一套工具链,说明书还得自己写。)

二、两个构建:只靠系统调用,和带 musl 的完整版

第一个构建是自足的。RTL 不链 libc,运行时直接走 Linux 系统调用,编出来的 funcmd-linux-loongarch64 只有解释器和可移植标准库。它适合 bring-up,不适合真用,因为正则、FFI、压缩、数据库一个都还没有。

完整版多一步 prepare-linux-loongarch64-sysroot.sh:从 Alpine 3.21 的 loongarch64 仓库下载 musl、libffi、pcre、zlib、OpenSSL、SQLite、libxml2、curl 这些 apk,直接 tar xzf 到一个普通目录。不要 root,不要 chroot,也不要 apk 包管理器。脚本还补了兼容链接,把 libc.so.6、libm.so.6、libpthread.so.0 这些 glibc 名字指向 musl 的 libc.musl-loongarch64.so.1,动态加载器则软链成 ld-linux-loongarch-lp64d.so.1。

有了 sysroot,FPC_USE_LIBC=1 重编 RTL,FULL=1 打开 -dRegex -dLinuxFFI -dFPC_USE_LIBC,链接时加 -lc -lpcre -lffi -ldl。funcmd.dpr 也补了一行 {$linklib c},只在 FPC_USE_LIBC 下生效。

两个「没有」照旧摆着。正则走 sysroot 里的 libpcre.so.1,不编捆绑的 C 源码;PCRE 8 的 sljit 本来就没有 LoongArch 后端,JIT 这条路直接不通,于是正则解释执行,功能齐全,性能略低。TCC 没有 LoongArch 后端,lib-tcc 和 NewJit #!c 直接跳过,不假装能用。

三、qemu 里的回归

run-loongarch64-qemu.sh 把 funcmd 包一层 QEMU_LD_PREFIX=$SYSROOT,套进 qemu-loongarch64,再交给原来的 run-linux-regression.sh,平台名报 linux-loongarch64-qemu。FFI 测试要加载 libffitest.so,构建脚本本身只会编主机版,所以这里用 zig cc -target loongarch64-linux-musl 编目标文件,再用龙芯的 ld 链成 .so,最后 touch 一下,免得 build.sh 又把它覆盖回去。

结果 115/115。跳过表里写的不是「架构编错了」,而是模拟器的边界:lib-fun-pack、package、test、uuid 这些 shell 测试直接调用目标二进制,没走 FUNCMD,qemu-user 下跑不了;/proc/<pid>/comm 和 /proc/<pid>/exe 在 qemu 里指向宿主进程,两条进程名断言只能等真机;Alpine sysroot 里没有 ODBC 驱动;lib-loadcheck 依赖完整主机工具链。真正的架构差异一条都没有。

四、编码器:定长 32 位,表从 binutils 生成

lib-asm-loongarch64.fun 502 行,结构照抄 AArch64 那一套:定长 32 位字,表值写成「基字|槽位」,槽位负责把操作数移位后或进固定位段。x86 那种「前缀加后缀」的追加模型在这里完全用不上。

指令表 377 条,来自 fun/lib/tools/gen-asm-list-loongarch64.py,数据源是 GNU binutils 的 loongarch-opc.c,没有一条手写机器码。槽位字母分 r(GPR)、f(FPR)、c(FCC)、v(无符号立即数)、y(有符号立即数)。有符号没用 V/O 这类大小写成对的字母,因为 Fun 的字符串比较不看大小写,'v' = 'V' 为真,两个槽位会撞在一起。

分支位移是 LoongArch 特有的拆法:b/bl 是 26 位,拆成 [10@0][16@10];beqz/bnez 是 21 位,[5@0][16@10];beq/bne/blt 这一组只剩中间 16 位 [16@10]。标签两遍解析,数字操作数按 GNU as 的规矩当有符号字节偏移。

li.w 和 li.d 没有照抄 AArch64 的 movz/movk,而是复刻 gas 的切块算法:把值拆成 12/20/20/12 四段,按全零、全一、符号位挑最短的 lu12i.w、ori、addi.w、lu32i.d、lu52i.d 组合。这里也有一个只在这种地方才会犯的错:li.d $a3, 0x80000000 的高 32 位是 0,不能因为低 32 位的符号位是 1 就把高半填成全一,只有字面量真带负号才补 0xffffffff。

还有一条不能省的规矩。LoongArch 和 AArch64 一样不保证指令缓存与数据缓存一致,代码写进 mmap 页之后必须刷一次。基类的 _asm_flush 钩子在这里绑 __clear_cache,Load() 在拷贝字节后、建可调用指针前调用它。要是符号没解析到,lib-asm-loongarch64 直接拒绝执行,不赌运气。ret 的尾巴是 2000004C,小端字节对应 jirl $r0, $r1, 0。

五、三重对拍,外加一个反向核对

第一重是逐字节。asm-loongarch64-diff.sh 拿 164 条精选指令,一边走 Fun 编码器,一边走 loongarch64-linux-gnu-as 加 objdump,零不符。

第二重是全表。loongarch64-tablecases.py 给每一条表项各造一个用例,361/361 与 GNU as 一致。随机模式再加 1083 条,寄存器随机、立即数非零、AMO 三个操作数互不相同,1083/1083 一致,没有一条是汇编器拒掉而我们放过去的。

第三重是 QEMU。这一重要单说,因为前两重有一个共同的盲点:指令表和 GNU as 都来自 binutils 的 loongarch-opc.c,同源的表验不出同源的错。QEMU 的 target/loongarch/insns.decode 是按 LoongArch 手册另写的解码表,loongarch64-qemu-crosscheck.py 拿它逐字段核对,236 条共享指令全部对上,再用 QEMU 的字段布局生成具体操作数,反验 Fun 编码器,全程不经过 binutils。

最后还有一个反向核对:loongarch64-gas-suite.py 不信任 as,直接跑 binutils 自带的 LoongArch gas 测试集,把每个 X.s 汇编、反汇编,再和 X.d 里的期望字比对。等于先证明尺子是准的,再拿尺子量东西。

六、Fun 自己的两个坑

第一个坑很 Fun。_LA_GPR[s] 这张寄存器名表,键是 a0、sp、ra 这些字符串。但 Fun 的 map 下标遇到数字字符串会当成位置索引,_LA_GPR['4'] 返回第五个值,正好是 4。于是 break 1 里的 1、preld 0, $sp, 0 里的 0,在还没查到任何命名寄存器之前就被归类成 GPR 4 和 GPR 0,编出来的字当然不对。

第一版修法加了个正则,只允许标识符形状的键去查表。十几分钟后换成了正解:用 _LA_GPR.[s],.[key] 强制按字符串键查,不再走位置索引。测试里专门加了一组 imm_first,break 1、syscall 2、dbar 0、ibar 0、preld 0, $sp, 0、rdtime.d $a0, $a1,并把这组放在用例文件最前面。为什么放最前面,注释写得很直白:没有标识符守卫的话,这些立即数会在任何命名寄存器之前被误判。

第二个坑是 AMO。amadd.w $a0, $a0, $a1 这种写法,as 直接拒,因为读改写会把旧值冲掉,目的寄存器不能和基址或源寄存器同名,$r0 除外。编码器现在也拒,宁可报错,也不编出参考汇编器不认的码。

七、还剩什么

P-09 记 DONE 不等于龙芯版能发。P-16 列得很清楚:lib-asm 的 live 执行要在 qemu 或真机上验一次,CI 要加 loongarch 作业,package.py 和 capabilities 要出发布包,TCC 无后端要定策略(显式报错还是回退系统 cc),最后是真机验收。qemu 能证明编码和大部分运行路径,证明不了浮点、原子和缓存行为跟真芯片一致。

(吐槽:评估报告按人周写,A 要 2 到 3.5 周,A 加汇编后端 4 到 7 周。实际从 11 月 9 日晚上到 10 日深夜,两天。倒不是评估虚高,评估里最贵的本来就是「FPC trunk 这一关能不能过」,过不去按周算,过去了剩下的都是收尾。)

从 ARM 那篇算起,一个月里两套机器码后端先后落地。x86 是变长追加,AArch64 是定长位域,LoongArch 又是另一种定长拆法,同一个 #!asm 接口下面,三种脾气。Fun 自己这扇窗,又开大了一点。只是机器码能编出来,不等于能在龙芯上跑起来。编码器把话说明白了,还得找台真机器听一遍。

相关推荐
总要学点什么11 分钟前
第104篇:三维特效-流动墙体效果——让贴图沿墙流动,它却只肯走一条对角线
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
旖旎夜光34 分钟前
【C++ 基础入门】C++11 新特性(中):右值引用、移动语义与完美转发
开发语言·c++·笔记·学习
ShineWinsu1 小时前
对于Qt:QTabWidget常见属性的解析
开发语言·数据库·c++·qt·浏览器·qtabwidget·标签页
普通人62 小时前
WebGL与WebGPU入门指南
开发语言·webgl·1024程序员节
꧁细听勿语情꧂2 小时前
直接选择排序,堆排序,冒泡排序,递归快速排序、hoare版本、挖坑法、lomuto前后指针,非递归快速排序
c语言·开发语言·数据结构·算法
SomeOtherTime2 小时前
Rust集合(AI回答)
开发语言·后端·rust
曦樂~2 小时前
【Web Development】03JavaScript 基础和事件
开发语言·前端·javascript
郝学胜-神的一滴2 小时前
LangChain Skill 实现(渐进披露 On-Demand Skill,LangChain 官方 Deep Agent Skill 模式)
开发语言·python·langchain·numpy
程序员Query3 小时前
day01-认识Go语言 + golang环境的安装
开发语言·后端·golang