The problem

Have you ever tried to distribute C/C++ applications to Linux users? If you haven’t, you might think it is as simple as grabbing the binary executable and distributing it to users. Unfortunately, distributing Linux binaries like this rarely works, here is why:

C/C++ applications on Linux are often linked with glibc as their C runtime, which consists of a set of dynamic libraries such as libc.so.6, libpthread.so.0 and etc. The original idea of dynamic libraries is to share code between applications, which is as great as it sounds. However, it also creates new problems: compatibility problems. The glibc took an unusual approach. Applications linked with an old version of glibc will continue to work with a newer version of glibc, which is good, however, applications linked with newer glibc refuse to work with old versions of glibc, which sucks. If a Linux binary works on your computer, you can not copy it to another computer and expect it to work!

Suboptimal solutions

Some people advise that you just build your applications on a very old distro with a very old version of glibc, which should produce working binaries for most people. Some people also package their applications for all major distros in a distro-specific format, such as deb for debians and rpm for redhats. All of this is madness, why should application distribution be distro-specific? The Linux kernel maintains excellent compatibility for user space applications, there must be a way to produce portable applications for Linux.

Failed attempts

Since the glibc are just a set of dynamics libraries. You may be wondering why can’t we just bundle the glibc with the application, like Windows applications bundling their VC runtimes. Unfortunately, this does not work.

There are some extra steps when the Linux kernel runs a dynamically linked executable. The kernel looks for a field in the ELF file, called “ELF interpreter”, which is an absolute path to another ELF file, often /lib64/ld-linux-x86-64.so.2. The purpose of this thing is to handle dynamic libraries, and it is the first thing that executes. You can read more in this article.

As you may have guessed, this “ELF interpreter” thing (ld-linux-x86-64.so) is also part of the libc, and different versions are incompatible with each other. And it is an absolute path embedded in the ELF binary. Thus, you can not bundle the glibc with your apps.

To create portable Linux binaries, dynamic linking can not be used. So what about static linking? Most C/C++ libraries have no problem linking statically unless it is glibc. People who tried static linking with glibc often have horrible stories to tell. Sometimes the statically linked program seems to work until the name resolution doesn’t work, or other stuff like vdso breaks.

The solution

Luckily, the static linking problem is only a glibc problem. Another popular libc implementation, the musl libc, has no problem with static linking. musl is the default libc used by the Alpine Linux distro. To produce a statically linked binary, simply build it on Alpine Linux, and add the -static flag to gcc. Simple and effective!

Alpine Linux is often used as the base image for containerized applications. You can try it with docker: docker pull alpine.

Installing the C/C++ toolchain on Alpine:

apk update
apk upgrade
apk add coreutils gcc g++

Thanks to the musl-based toolchain, the static linking works out of the box, even for C++ applications with libstdc++ dependency.

For third-party libraries, sometimes, Alpine Linux already has a static library package with the name *-static, like xz-static or sqlite-static, which can be grabbed with the apk add command.