The Holy Grail of Linux Binary Compatibility: Musl and Dlopen

7 hours ago (github.com)

Binary comparability extends beyond the vide that runs in your process. These days a lot of functionality occurs by way of IPC which has a variety of wire protocols depending on the interface. For instance there is dbus, Wayland protocols, varlink, etc. Both the wire protocol, and the APIs built on top need to retain backwards comparability to ensure Binary compatibility. Otherwise you're not going to be able to run on various different Linux based platforms arbitrarily. And unlike the kernel, these userspace surfaces do not take backwards compatibility nearly as important. It's also much more difficult to target a subset of these APIs that are available on systems that are only 5 years old. I would argue API endpoints on the web have less risk here (although those break all the time as well)

So what we need is essentially a "libc virtualization".

But Musl is only available on Linux, isn't it? Cosmopolitan (https://github.com/jart/cosmopolitan) goes further and is available also on Mac and Windows, and it uses e.g. SIMD and other performance related improvements. Unfortunately, one has to cut through the marketing "magic" to find the main engineering value; stripping away the "polyglot" shell-script hacks and the "Actually Portable Executable" container (which are undoubtedly innovative), the core benefit proposition of Cosmopolitan is indeed a platform-agnostic, statically-linked C standard (plus some Posix) library that performs runtime system call translation, so to say "the Musl we have been waiting for".

  • I desperately want to write C/C++ code that has a web server and can talk websockets, and that I can compile with Cosmopolitan.

    I don't want Lua. Using Lua is crazy clever, but it's not what I want.

    I should just vibe code the dang thing.

`dlopen`'ing system libraries is an "easy" hack to try to maintain compatibility with wide variety of libraries/ABIs. It's barely used (I know only of SDL, Small HTTP Server, and now Godot).

Without dlopen (with regular dynamic linking), it's much harder to compile for older distros, and I doubt you can easily implement glibc/musl cross-compatibility at all in general.

Take a look what Valve does in a Steam Runtime:

    - https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/blob/main/docs/pressure-vessel.md
    - https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/blob/main/subprojects/libcapsule/doc/Capsules.txt

Is there a tool that takes an executable, collects all the required .so files and produces either a static executable, or a package that runs everywhere?

  • There are things like this.

    The things I know of and can think of off the top of my head are:

    1. appimage https://appimage.org/

    2. nix-bundle https://github.com/nix-community/nix-bundle

    3. guix via guix pack

    4. A small collection of random small projects hardly anyone uses for docker to do this (i.e. https://github.com/NilsIrl/dockerc )

    5. A docker image (a package that runs everywhere, assuming a docker runtime is available)

    6. https://flatpak.org/

    7. https://en.wikipedia.org/wiki/Snap_(software)

    AppImage is the closest to what you want I think.

    • It should be noted that AppImages tend to be noticeably slower at runtime than other packaging methods and also very big for typical systems which include most libraries. They're good as a "compile once, run everywhere" approach but you're really accommodating edge cases here.

      A "works in most cases" build should also be available for that that it would benefit. And if you can, why not provide specialized packages for the edge cases?

      Of course, don't take my advice as-is, you should always thoroughly benchmark your software on real systems and choose the tradeoffs you're willing to make.

  • 15-30 years ago I managed a lot of commercial chip design EDA software that ran on Solaris and Linux. We had wrapper shell scripts for so many programs that used LD_LIBRARY_PATH and LD_PRELOAD to point to the specific versions of various libraries that each program needed. I used "ldd" which prints out the shared libraries a program uses.

  • You can "package" all .so files you need into one file, there are many tools which do this (like a zip file).

    But you can't take .so files and make one "static" binary out of them.

    • > But you can't take .so files and make one "static" binary out of them.

      Yes you can!

      This is more-or-less what unexec does

      - https://news.ycombinator.com/item?id=21394916

      For some reason nobody seems to like this sorcery, probably because it combines the worst of all worlds.

      But there's almost[1] nothing special about what the dynamic linker is doing to get those .so files into memory that it can't arrange them in one big file ahead of time!

      [1]: ASLR would be one of those things...

    • Well not a static binary in the sense that's commonly meant when speaking about static linking. But you can pack .so files into the executable as binary data and then dlopen the relevant memory ranges.

      1 reply →

  • I don't think you can link shared objects into a static binary because you'd have to patch all instances where the code reads the PLT/GOT, but this can be arbitrarily mangled by the optimizer, and turn them back into relocations for the linker to then resolve them.

    You can change the rpath though, which is sort of like an LD_LIBRARY_PATH baked into the object, which makes it relatively easy to bundle everything but libc with your binary.

    edit: Mild correction, there is this: https://sourceforge.net/projects/statifier/ But the way this works is that it has the dynamic linker load everything (without ASLR / in a compact layout, presumably) and then dumps an image of the process. Everything else is just increasingly fancy ways of copying shared objects around and making ld.so prefer the bundled libraries.

I'd never heard of detour. That's a pretty cool hack.

  • they were prominent in game hacking 2005ish windows

    made hooking into game code much easier than before

    • Aren't all DLLs on the Windows platform compiled with an unusual instruction at the start of each function? This makes it possible to somehow hot patch the DLL after it is already in memory

Isn't this asking for the exact trouble musl wanted so spare you from by disabling dlopen()?

It's funny how people insist on wanting to link everything statically when shared libraries were specifically designed to have a better alternative.

Even worse is containers, which has the disadvantage of both.

  • Dynamic libraries have been frowned upon since their inception as being a terrible solution to a non-existent problem, generally amplifying binary sizes and harming performance. Some fun quotes of quite notable characters on the matter here: https://harmful.cat-v.org/software/dynamic-linking/

    In practice, a statically linked system is often smaller than a meticulously dynamically linked one - while there are many copies of common routines, programs only contain tightly packed, specifically optimized and sometimes inlined versions of the symbols they use. The space and performance gain per program is quite significant.

    Modern apps and containers are another issue entirely - linking doesn't help if your issue is gigabytes of graphical assets or using a container base image that includes the entire world.

    • Imagine a fully statically linked version of Debian. What happens when there’s a security update in a commonly used library? Am I supposed to redownload a rebuild of basically the entire distro every time this happens, or else what?

    • Statically linked binaries are a huge security problem, as are containers, for the same reason. Vendors are too slow to patch.

      When dynamically linking against shared OS libraries, Updates are far quicker and easier.

      And as for the size advantage, just look at a typical Golang or Haskell program. Statically linked, two-digit megabytes, larger than my libc...

      1 reply →

  • Dynamic linking exists to make a specific set of tradeoffs. Neither better nor worse than static linking in the general sense.

  • Dynamic libraries make a lot of sense as operating system interface when they guarantee a stable API and ABI (see Windows for how to do that) - the other scenarios where DLLs make sense is for plugin systems. But that's pretty much it, for anything else static linking is superior because it doesn't present an optimization barrier (especially for dead code elimination).

    No idea why the glibc can't provide API+ABI stability, but on Linux it always comes down to glibc related "DLL hell" problems (e.g. not being able to run an executable that was created on a more recent Linux system on an older Linux system even when the program doesn't access any new glibc entry points - the usually adviced solution is to link with an older glibc version, but that's also not trivial, unless you use the Zig toolchain).

    TL;DR: It's not static vs dynamic linking, just glibc being a an exceptionally shitty solution as operating system interface.

  • It's easier to distribute software fully self-contained, if you ignore the pain of statically linking everything together :)

  • That would be a good point if said shared libraries did not break binary backwards compatibility and behaved more like winapi.

I've been statically linking Nim binaries with musl. It's fantastic. Relatively easy to set up (just a few compiler flags and the musl toolchain), and I get an optimized binary that is indistinguishable from any other static C Linux binary. It runs on any machine we throw it at. For a newer-generation systems language, that is a massive selling point.

  • I have an idea for a static linux distribution based on musl, with either an Alpine rebuild or Gentoo-musl:

    http://stalinux.wikidot.com

    The documentation to make static binary with GLibc is sparce for a reason, they don't like static binaries.

This seems interesting even regardless of go. Is it realistic to create an executable which would work on very different kinds of Linux distros? e.g. 32-bit and 64-bit? Or maybe some general framework/library for building an arbitrary program at least for "any libc"?

  • Appimage exists that packs linux applications into a single executable file that you just download and open. It works on most linux distros

    • I vaguely remember that Appimage-based programs would fail for me because of fuse and glibc symbol version incompatibilties.

      Gave up them afterwards. If I need to tweak dependencies might as well deal with the packet manager of my distro.

  • Yup. Just compile it as static executable. Static binaries are very undervalued imo.

    • We had a time when static binaries where pretty much the only thing we had available.

      Here is an idea, lets go back to pure UNIX distros using static binaries with OS IPC for any kind of application dynamism, I bet it will work out great, after all it did for several years.

      Got to put that RAM to use.

      7 replies →

That seems mostly useful for proprietary programs. I don't like it.

  • Yeah, in my 20 years of using and developing on GNU/Linux the only binary compatibility issues I experienced that I can think of now were related to either Adobe Flash, Adobe Reader or games.

    Adobe stuff is of the kind that you'd prefer to not exist at all rather than have it fixed (and today you largely can pretend that it never existed already), and the situation for games has been pretty much fixed by Steam runtimes.

    It's fine that some people care about it and some solutions are really clever, but it just doesn't seem to be an actual issue you stumble on in practice much.

    • The solution to games is to load Windows games instead of Linux binaries.

      Basically the way for the year of the Linux desktop is to become Windows.

      1 reply →

  • Why? Foss software also benefits from less dependency hell.

    • For distro-packaged FOSS, binary compatibility isn't really a problem. Distributions like Debian already resolve dependencies by building from source and keeping a coherent set of libraries. Security fixes and updates propagate naturally.

      Binary compatibility solutions mostly target cases where rebuilding isn't possible, typically closed source software. Freezing and bundling software dependencies ultimately creates dependency hell rather than avoiding it.

      3 replies →

If you're using dlopen(), you're just reimplementing the dynamic linker.

  • that's cute, but dismissive, sort of like "if you use popen(), you are reimplementing bash". There is so much hair in ld nobody wants to know about — parsing elf, ctors/dtors, ...