Recently I ran into some issues with a C++ application I was working on. The official Linux distribution we supported for the application is Ubuntu 20.4. This version of Ubuntu is pretty outdated now a days, and is even nearing its EoL date. On my development machine I prefer to run the latest LTS version. Building the application in a newer Ubuntu version (f.e. 24.4) however, produced a binary that would instantly crash on the officially supported platform. Not only that was an issue though. If I were to send a build of the application that happened to be built in Debug mode to a colleague, the application also crashed instantly, even though it worked fine on my machine. The reason for this: shared libraries.

*Note: *This text assumes Linux is used, surprisingly, creating static applications on Windows is a lot simpler.

shared libraries

Shared libraries (also called dynamic libraries) are external libraries that aren’t part of the final executable or library that your project produces. An application that requires one or more shared libraries will look for them in the current system, as soon as it is started. This also means that if the required shared libraries are not available on the system where the application is running, it will crash right away. It will show an error like this, which you’ve probably come across before: "./app: error while loading shared libraries: lib_a.so.: cannot open shared object file: No such file or directory". When using Ubuntu, if the missing library is available in the package manager apt, you could usually fix this issue by installing the missing package, whose name would probably look something like lib*library_name*. One way you could visualize an application that uses shared libraries, is as if your application is missing some puzzle pieces to make it complete, and it expects those puzzle pieces to be installed in the system where it is deployed:

Shared libraries

Shared libraries have some nice advantages. For example, they can provide a generic interface to your application for some functionalities. This allows the user to install shared libraries on its system which can include specific optimizations for the hardware that its using, while being able to install a generic version of the application that can work on any system. This also makes it easier if you are developing an application and want to provide a pre-built executable to your users. You would only need to build a single version of the application for every OS + architecture combination (for example x86_64 Linux). This can already turn into a lot of targets to build for (think macos, 32bit Windows, 64 bit Windows, ARM Linux, etc.). Now imagine if performance is very important to your application, and you also need to start adding specific builds so that you can leverage hardware optimizations. You would need to build an additional executable for all of the previous targets that contains optimizations for an Intel CPU, ARM CPU, NVidia GPU, etc. The number of targets you have to build for would explode. All configurations that need to be tested as well of course. That would be a lot of work. It is much easier to depend on a shared library, where the user can choose a specific version of the library that includes the correct optimizations.

This flexibility also comes back in the versions of the dependencies used. When you depend on a shared library, you typically depend on only the major version, or the major and minor version of that library. This gives a bit more flexibility in what libraries your application can work with. Shared libraries are also efficient in terms of used disk space. If you have many applications that use the same shared library (and the same version), that library only has to be installed on your system once, and many applications can make use of it.

Static libraries

Static libraries are the opposite of shared libraries. Static libraries get included into your application when you build it. This means that the libraries don’t need to be present on the system where your application will run, since it’s already part of your application. This already sounds pretty good, but I think its important to highlight how big of an advantage this is. This allows you to not have any requirements on, for example, glibc. More on glibc later, but what is important to know is that if you build a non-static application in Ubuntu 24.4, it won’t be able to run in Ubuntu 20.4 or Alpine Linux, and probably many other distros. Exactly because non-static application will have a specific requirement on a glibc version, depending on what system it was built on (assuming you are always using Ubuntu). Static applications don’t have this requirement. This means that if you build a static application on one distribution, it will work on all other distributions. The only requirement is that the hardware architecture is the same (f.e. amd64). If you deploy your application using docker images, this can also make your Dockerfiles a lot more simple, since you basically don’t need to install any dependencies into your container.

Statically linking against a library also means there is less flexibility in the versions of your dependencies, you are linking against a specific version, so there is no room for the target system to provide code with hardware optimizations. On the other hand, static libraries can make your applications behavior on any system more predictable. If your applications uses slightly different versions on various systems (through shared libraries), this can alter the behavior of the application on each system. This could be through hardware specific optimizations, but also just different versions in general. Shared libraries typically target a specific major version, or a major and a minor version. But if you depend on a library, the behavior between v1.1.0 and v1.1.1 of that library could differ quite a bit. Let alone v1.0.0 versus v1.6.0 if you only depend on a specific major version. This could cause bugs for your applications which you wouldn’t be able to reproduce yourself. That makes fixing the bugs very hard.

Shared & static libraries

Mixing and matching

It is also possible to link some of your dependencies statically to your application, and for other dependencies to use shared libraries. You could for example link all of your dependencies in your application statically, except for one, in which hardware specific optimizations can make a big difference. While in general you would still get some of the advantages of static libraries (more predictable behavior on any system), I think you would be missing out on the most important advantage, being able to deploy on any distribution. Since your target system must support the library you depend on. And depending on your users or the way you deploy, the library must also be easily installable, since not everyone is happy to build a dependency from source. It can require quite some effort to create or procure static libraries, so if you do want to depend on a shared library, I would say you should judge on a case-by-case basis on whether it is worth the effort. I think the case for static libraries is a lot easier to make when you can create a fully static application.

Mixing static and shared libraries

Why most applications aren’t statically built

Every application needs to interact with the OS of the system it is running on in some way or another, whether its allocating memory, printing output, or anything to do with the filesystem. These interactions are of course not reimplemented in every single application, but they are implemented in a family of libraries called libc (standard C library. The most popular implementation of libc is glibc (GNU C library). This is installed by default on many popular operating systems like Ubuntu, Debian and Fedora. If you build an application on any of these operating systems, the default libc library that the application will be linked against will be glibc. Unfortunately glibc is not designed to be statically linked. glibc dynamically resolves system calls at runtime to stay compatible with different Linux kernel versions, and it has other components which depend on shared libraries to function properly. Technically it might be possible to do some workarounds, but at that point I wonder if it is still worth the benefit of statically linking. Luckily though, even though glibc is the default libc implementation, it isn’t the only one. musl is another libc implementation, which is more light-weight than glibc and, luckily for us, it is designed for static linking. So if we want to create a statically linked application, the easiest route to go tends to be to use musl as your libc implementation, instead of the default glibc.

But just linking your application against musl won’t be enough, if your application is linked against pre-built binaries of your dependencies. If those pre-built binaries are linked against glibc (which, the overwhelming majority are) you will run into problems. Of course, part of your application will still depend on shared libraries, and thus it will not be static. But it won’t even be possible to use 2 different libc applications in a single binary. The linker will not be able to figure out what function call to link against what libc implementation. So, in order to create a fully static application, you must build all your dependencies (and their dependencies, and their dependencies, etc.) from source, and link them against musl. Furthermore, you should also make sure they don’t depend on any other shared libraries if you really want to create a fully static application. I think this is the biggest deterrent to building statically linked applications. But honestly, I feel like you should be able to build your entire dependency chain regardless of whether or not you want to build a static application, just so that you are in control over all the code that ends up in your application. That’s why for me this cost is actually not that high, and the benefits of statically linked applications really speak to me. This is why, unless I have an application that could really benefit from one or more shared libraries, my default choice is to go for statically linked applications. It just makes life a lot easier, once you’ve gone through the struggle of getting everything ready once.

In the next post, I will describe my experience converting a pretty complex C++ project with many dependencies into a statically linked application. It took some work, but in the end it wasn’t too bad, and I think after having done it once, it will be a lot easier to convert other applications in the future.