1

If you go to the download site of nodejs you can select Linux Binaries (x64) which is a tar archive that (among other files) contains a bin/ folder with a binary called nodejs.

How is nodejs able to provide a "generic" binary that supposedly (or seemingly) is able to run on any linux distro? I know they don't make that claim on their site, but that is what I'm assuming, since there isn't a specific OS release (like nodejs for Ubuntu, Debian etc.).

Reading up on the topic (binary compatibility between Linux Distros) I came across multiple answers that basically say that this isn't really a thing:

No, Debian and Ubuntu are not binary compatible. Debian and Ubuntu may use different compilers with different ABI, different kernel versions, different libraries, different packages/version etc. As not all Ubuntu packages are in Debian (and vice versa) deb packages may also depend on uninstallable versions.

...

So no technically they are not binary compatible.

Answer to a question "Is Ubuntu LTS binary compatible with Debian?"

I understand some parts about ABI like architecture, calling conventions, system calls etc. and it makes sense to me, that if the same architecture and calling conventions are used, that there's some compatibility given because at the heart of every linux distro is the linux kernel.

However, I'm still struggling to understand why people say that two linux distros are not binary compatible.

What is it that is not compatible?

And why is nodejs seemingly able to provide just one binary release ''for all'' linux distros?

This is more an educational question and not about "how can I make a binary X compatible with distros x, y and z".

1 Answer 1

4
+100

There are ABIs (application binary interfaces) at multiple levels. To make a binary that works everywhere, one of these levels must be targeted. The two levels to target are either the kernel ABI or the LSB ABI.

A .deb package may not be compatible between Debian and Ubuntu because it may have dependencies on other things that are in one flavor or the other but not both. However, a lot of .deb packages will run in some range of versions of Ubuntu and Debian. Worse, an rpm from RedHat is unlikely to work on an Ubuntu system just because the packaging format is not understood.

The dependencies are what makes executables not run in different linux flavors. Almost all programs rely on external libraries. When executables are being built, Linux (and many other operating systems including windows and other unixes) instead of including those external libraries as part of the executable, they only link it to a stub, and the actual library is shared between many executables. This reduces the size of the executables both on disk and in memory, and in some cases, allows bugs in the libraries to be fixed without rebuilding the executables. However, if you don't have the correct version of the shared library in your flavor of linux, the executable won't work.

Executables that use shared libraries are said to be dynamically linked (meaning, final linkage to the shared libraries is done dynamically at runtime). It is also possible to statically link executables. The result is a much larger executable, but it will run on a much wider variety of systems. At that point, the ABI is not the flavor of linux, but the kernel itself. Generally, kernels are backwards compatible, and executables are forwards compatible, meaning that (within some wide range of kernel versions), old executables will run on newer kernels, and newer kernels can run older executables. There have been a very few times in the history of linux where executable format itself changed, so there are limits to this as well.

The architecture of the cpu the operating system is running on must also match the executable, with a few narrow exceptions. (Generally i386 binaries can run on x86_64 systems if you have the relevant matching libraries.) It is possible to make a fat binary that contains code for multiple architectures of cpus in it. This is very common in some operating systems, but rare in linux.

It's also possible to have containerized executables. In this case, like a turtle, the executable is built into a filesystem (like squashfs) and carries around all of its dependent libraries and support files in its binary as separate files. This is what appimage does.

The file and ldd commands can sometimes tell you how a binary is constructed as described above. For example:

$ file /bin/ls
/bin/ls: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=897f49cafa98c11d63e619e7e40352f855249c13, for GNU/Linux 3.2.0, stripped
$ ldd /bin/ls
    linux-vdso.so.1 (0x00007fff95ae1000)
    libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007fc6aebb8000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc6ae990000)
    libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007fc6ae8f9000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fc6aec50000)

This binary is for the x86_64 architecture and is dynamically linked for version 2 of the dynamic linker and requires the specific versions of the various shared libraries listed above... (Note: major versions must match, minor versions aren't even listed above.)

Compare this to:

$ file /bin/busybox
/bin/busybox: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=36c64fc4707a00db11657009501f026401385933, for GNU/Linux 3.2.0, stripped
$ ldd /bin/busybox
    not a dynamic executable

This statically linked binary will run on any linux kernel that supports x86_64 ELF version 1.

$ file prusaslicer.AppImage
prusaslicer.AppImage: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.18, stripped
$ ldd prusaslicer.AppImage
    not a dynamic executable

This appears to be a dynamically linked executable, but doesn't have any shared library dependencies other than the dynamic linker itself. This is actually a containerized executable, and when it is run, it mounts the internal filesystem and runs the real executable inside. The only clue to this (without running it) is the filename (and size). After running it, it is possible to examine the fuse mounted filesystem, although this behind the scenes work is somewhat hidden.

Looking at the specific example you asked about... I downloaded a x86_64 linux version of node.js and found inside the following:

$ file bin/node 
bin/node: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=e8b23b15ec6280a0d4838fbba1171cb8d94667c5, with debug_info, not stripped
$ ldd bin/node
    linux-vdso.so.1 (0x00007ffd633f0000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007efec3198000)
    libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007efec2f6c000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007efec2e85000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007efec2e65000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007efec2e60000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007efec2c36000)
    /lib64/ld-linux-x86-64.so.2 (0x00007efec31e5000)

So, being a dynamically linked executable with a fair number of external shared library dependencies, this will limit what systems it can run on, but likely the range of versions that have these specific libraries (or can get them installed) is probably fairly wide. These are all base libraries that most linux systems should have, and they have been stable enough that a wide range of OS versions have these versions of the libraries. Sometimes if you are missing some of these, installing the lsb-base or lsb-core package for your linux flavor will make them available. The LSB (Linux Standard Base) is an ABI standard that includes a base set of standard libraries and other items that becomes a lowest common denominator for all linux distributions that want to provide a common target for third party developers.

Note that if you have an executable that won't run but says it should, you can do detective work like the above and determine what is missing on your system and install the missing libraries.

4
  • Ahhhh, so The dependencies are what makes executables not run in different linux flavors. that's the short version of why it's not considered "compatible" if I understand you correctly, they are runnable IF everything is included in the binary (or the needed libraries are installed with the correct version) and the kernel recognizes the executable format? And ... I guess everything (from an abstraction level point of view) else is dependent on higher level specifications (like communicating with a specific system service) and has nothing to do with direct binary compatibility?
    – Marco
    Aug 18, 2022 at 23:59
  • I'm very very thankful for your lengthy response by the way! If you don't mind, I'll wait 2 days to give you a little more than just 15 reputation! I really, really appreciate the effort you've put into your answer!
    – Marco
    Aug 19, 2022 at 0:00
  • 1
    The catch is, some libraries are unobtainable for operating systems other than the one they came with. And any mismatched piece along the way can mess things up and make it not work. There are even instances where one flavor has a service not available elsewhere and makes a particular binary not portable.
    – user10489
    Aug 19, 2022 at 0:19
  • 2
    Note also the kernel requirements listed by file — your ls needs 3.2.0, nodejs was built for the older 2.6.32 (technically, those are kernel versions, but the requirements are more about C library / kernel ABIs). Aug 19, 2022 at 2:46

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .