静态编译的尽头,是手搓一个动态链接器

Linux圈有个心照不宣的政治正确,静态编译天下第一。

打包成一个文件,零依赖,丢进U盘走到哪跑到哪。这种纯粹的部署方式,曾让无数开发者高潮。

但现实往往很骨感。只要你的程序想调用GPU跑个Vulkan或OpenGL,这美好的幻觉瞬间破灭。

因为GPU驱动是宿主机用glibc编译的动态库。一个纯静态的musl二进制程序,面对这些动态库时就像个瞎子,根本没法直接加载它们。

为了一个驱动,难道要打包整个系统?

遇到这堵叹息之墙,业界的常规操作是什么?

Flatpak、AppImage、Docker容器。为了让你用上显卡,这些方案选择把大半个Linux发行版塞进你的程序里。

老实讲,这逻辑非常荒谬。这就好比你出国旅游发现插头不匹配,于是你直接把整栋房子打包搬到了国外。

巨大的体积、 duplicated的库文件、复杂的挂载和命名空间,让基础的调试和性能分析变成一场噩梦。

GitHub上有个叫Solo的项目,决定换个活法。他们不想搬家,他们想自己造个转换器。

不请第二个libc,自己手搓ELF加载器

Solo的思路相当野。

市面上也有其他方案试图解决静态程序加载动态库的问题,但大多是在进程里塞进第二个libc,或者依赖宿主机的动态链接器,导致线程状态和TLS互相打架。

AI配图

Solo反其道而行之。进程里从头到尾只有一个musl libc。

它自己手搓了一个ELF loader,外加一个glibc到musl的ABI桥接层。

结果就是,你的程序依然是个纯粹的静态二进制文件,但它能在运行时把宿主机的显卡驱动借过来用。

这可不是简单的接口转发。Solo实现了C++异常跨边界穿透,支持全部四种TLS模型,连底层的符号版本控制和延迟绑定都自己写了一遍。

没实现的glibc函数不会静默崩溃,而是直接抛出精确的符号名和版本号报错。

在CI测试里,Solo成功加载了Debian前1000个最常用包里的2100多个动态库,甚至在AMD、Intel、NVIDIA和苹果M1芯片上都跑通了Vulkan渲染。

硬核缝合怪的社区翻车现场

项目看着挺美,评论区却直接炸了锅。

有意思的是,最尖锐的批评往往来自同行。

有人发出灵魂拷问:你都动态加载.so文件了,还装什么静态二进制?这难道不是动态链接器穿了件静态的马甲?

有人嘲讽这是开历史倒车。当年UNIX发明动态链接,就是为了拯救你们这些搞静态链接的。现在你们又自己手搓一个加载器,图啥?

更致命的是前向兼容性炸弹。有开发者指出,如果glibc未来加了新符号,而显卡驱动又依赖了这个新符号。老程序用Solo去跑新驱动,由于Solo的ABI桥接层没覆盖这个新符号,程序会直接原地崩溃。

用魔法打败魔法的无奈之举

个人觉得,吐槽归吐槽,这帮程序员确实是被逼急了。

有个写过类似loader的开发者在评论区大吐苦水,说glibc的底层逻辑简直是个黑洞,想自己搞加载器分分钟踩进 undocumented 的ABI大坑里。

其实,如果构建工具足够聪明,部分静态链接就能完美解决GPU驱动的问题。但现实是,现有的工具链太烂了,大家只能捏着鼻子用容器。

Solo等于用极致的硬核,掩盖了生态的懒惰。他们把静态和动态之间的那堵死墙,硬生生砸出了一扇门。

只是,当静态编译被迫长出动态加载的器官,我们到底是在进化,还是在向糟糕的现实妥协?

【锐评】:用最硬核的手搓代码,去填补工具链拉胯留下的坑,这很Linux,也很无奈。

参考链接:
https://github.com/pg83/solo