They are a problem because gcc automatically links to the latest version of glibc.
As to why they don't add an option to specify an older version? I don't know either and it is rather annoying to have to use docker images of older OSes to target older glibc versions. It's just one of many things that prevents linux from being as popular as windows for desktop users.
When linking with this library before glibc, the resulting binary will not depend on the new symbol versions. It will run on glibc 2.4 and on systems as old as Ubuntu 8.04 and CentOS 5 even when built on the most modern system.
An easier option is to build against musl, remove the DEPENDS with patchelf, and use that. There are a few incompatibilities with glibc (more on aarch64 than am64), but it's manageable (you might need to patch a few headers in musl and provide missing/incompatible functions). And you'll have a binary that works against musl and glibc both.
Hmm. The MSVCRT.DLL/MSVCRTD.DLL not being binary compatible between releases of Visual Studio is the same thing, except of course you can't even combine some modules compiled for debug with modules compiled without debug in the same executable. The Windows problem has always been so so much worse that pretty much all developers simple resorted to shipping the OS system runtime with every package and it's just expected nowadays. It's where the phrase "DLL hell" originated, after all.
Not to say the ABI problem isn't real if you want to combine binary packages from different Linux-based OSes. Plenty of solutions for that have cropped up as well: containers, flatpacks, snaps, the list goes on.
> The Windows problem has always been so so much worse
Hard, hard disagree. The problems are somewhat comparable. But if any platform is more painful it's Linux. Although they're similar if you exclude glibc pain. At least in my personal experience of writing lots of code that needs to run on win/mac/linux/android.
> pretty much all developers simple resorted to shipping the OS system runtime with every package
Meanwhile Linux developers have resorted to shipping an entire OS via docker to run every program. Because managing Linux environment dependencies is so painful you have to package the whole system.
Needing to docker to simply launch a program is so embarrassing.
> except of course you can't even combine some modules compiled for debug with modules compiled without debug in the same executable
That's not any different on Linux. That has more to do with C++.
> Meanwhile Linux developers have resorted to shipping an entire OS via docker to run every program.
> Needing to docker to simply launch a program is so embarrassing.
I have never needed docker "just to launch a program". Docker makes it easy to provide multiple containerised copies of an identical environment. Containers are a light alternative to VM images.
> Docker makes it easy to provide multiple containerised copies of an identical environment.
Correct. The Linux architecture around a global of dependencies is, imho, bad and wrong. The thesis is it's good because you can deploy a security fix to libfoo.so just once for the whole system. However we now live in a world where you actually need to deploy the updated libfoo.so to all your various hierarchical Docker images. sad trombone
> Containers are a light alternative to VM images.
A light alternative to Docker is simply deploy your dependencies and not rely on a fragile, complicated global environment.
> I assume you find the existence of Windows containers just as embarrassing?
Yes.
I know my opinion is deeply unpopular. But I stand by it! Running a program should be as simple as downloading a zip, extracting, and running the executable. It's not hard!
I wish you all had experienced the golden age when running a program was as simple as apt-get install program and run it.
I find it hard to discuss the merits of Linux va windows with regard to deploying software without addressing the elephant in the room which is the replacement of a collectively maintained system that ensure software cohabitation by the modern compartmentalised collection of independent programs.
> Correct. The Linux architecture around a global of dependencies is, imho, bad and wrong. The thesis is it's good because you can deploy a security fix to libfoo.so just once for the whole system. However we now live in a world where you actually need to deploy the updated libfoo.so to all your various hierarchical Docker images. sad trombone
Only if you choose to use docker. As other people have pointed out most things on Linux can be deployed using a package manager.
> A light alternative to Docker is simply deploy your dependencies and not rely on a fragile, complicated global environment.
So like flatpak?
> I know my opinion is deeply unpopular. But I stand by it! Running a program should be as simple as downloading a zip, extracting, and running the executable. It's not hard!
How is that better than apt install? That (or equivalent on other distros) is how I install everything other than custom code on servers (that I git pull or similar).
Static linking is a good way to avoid the problem, but I’d hardly call it “elegant” to replicate the same runtime library in every executable. It’s very wasteful - we’re just fortunate to have enough storage these days to get away with it.
> I’d hardly call it “elegant” to replicate the same runtime library in every executable. It’s very wasteful - we’re just fortunate to have enough storage these days to get away with it.
I think you need to quantify "very wasteful". Quite frankly it's actually just fine. Totally fine. Especially when the alternative has turned out to be massive Docker images! So the alternative isn't actually any better. Womp womp.
An actually elegant solution would be a copy-on-write filesystem that can deduplicate. It'd be the best of both worlds.
Containers are a relatively recent phenomenon. Outside of distro packages, distributing statically linked binaries, dynamic binaries wrapped in LD_LIBRARY_PATH scripts or binaries using $ORIGIN based rpath are all options I have seen.
This comment is full of inaccuracies and mistakes, and is a terrible travesty of the Windows situation.
> except of course you can't even combine some modules compiled for debug with modules compiled without debug in the same executable.
There's a good reason for this, which IMO Unix-like compilers and system libraries should start adopting, too. Debug and Release binaries like the standard C and C++ runtimes and libraries cannot be inter-mixed because they have have different ABIs. They have different ABIs because the former set of binaries have different type layouts, many of which come with debug-specific assertions and tests like bounds-checking, exception try-catch, null-dereference tests, etc.
> The Windows problem has always been so so much worse that pretty much all developers simple resorted to shipping the OS system runtime with every package and it's just expected nowadays.
This is not true at all. There are several layers to the counterargument.
Firstly, UCRT has supplanted MSVCRT since Visual Studio 2015, which is a decade old this year. Additionally, UCRT can be statically linked: use `/MT` instead of `/MD`. And linking back to the previous quote, to statically link a debug CRT, use `/MTd`. Set this up in MSBuild or CMake using release/debug build configurations. UCRT is available for install (and maintained with Windows Update) in older versions of Windows going back to Vista.
Next, Windows by default comes with several versions of C++ redistributables going back to Visual Studio 2005. All of these redistributables are also regularly maintained with Windows Update.
Finally, Windows SDK versions targeting various versions of Windows are available for all supported Visual Studio developer environments. The oldest currently available in Visual Studio 2022 is Windows XP SP3[1].
These all serve to thoroughly solve both combinations of backward-forward compatibility, where (a) the runtime environment is newer than the developer environment, and (b), the runtime environment is older than the developer environment.
It is perfectly possible to compile a single `.exe` on Visual Studio 2022 in Windows 11, and expect it to run on Windows XP SP3, and the vice versa: compile a single `.exe` on Visual Studio 6, and expect it to run on Windows 11. No dynamic libraries, no DLLs, nothing; just a naked `.exe`. Download from website, double-click to run. That's it. No git clone, no GNU Autotools, no configure, no make, no make install (and then f*cking with rpaths), nothing. Prioritising binary-only software distribution means Windows has prioritised the end-user experience.
> It's where the phrase "DLL hell" originated, after all.
This is also incorrect. 'DLL hell' is a pre-NT problem that originated when packagers decided it was a good idea to overwrite system binaries by installing their own versions into system directories[2]. Sure, the versioning problem was there too, but this is itself a result of the aforementioned DLL stomping.
I fully daresay writing C and C++ for Windows is an easier matter than targeting any Unix-like. For context, see what video game developers and Valve have done to check off the 'works on Linux' checkbox: glibc updates are so ridiculously painful that Valve resorted to simply patching WINE and releasing it as Proton, and WINE is the ABI target for video games on Linux.
That Windows is generally better at handling compatibility is one thing, but I'm curious about the following part:
> There's a good reason for this, which IMO Unix-like compilers and system libraries should start adopting, too.
Why? I'm understanding from your comment that you can't mix debug and release objects on Windows because the ABI is different. That's not a feature, that's a limitation. If it works on Linux to mix debug-enabled objects with "release", what use would it have to make it not work anymore?
IIUC debug symbols can be totally separated from the object code, such that you can debug the release if you download the debug symbols. A well configured GDB on distros that offer this feature is able to do it automatically for you. It seems very useful and elegant. Why can't Windows do something like this and how is it an advantage?
(Genuine question, I have a remote idea on how ELF works (wrote a toy linker), not much how DWARF works, and not the slightest idea on how all this stuff works on Windows)
> If it works on Linux to mix debug-enabled objects with "release", what use would it have to make it not work anymore?
There is no difference between Linux and Windows here. The debug/release issue is ultimately up to the API developer.
C++ has has the standard template library (STL). libstdc++, libc++, and MSVC STL are three different implementations. STL defines various iterators. A common choice is for a release-mode iterator to be a raw pointer, just 8 bytes on 64-bit. But the debug-mode iterator is a struct with some extra information for runtime validation, so it's 24 bytes!
The end result is that if you pass an iterator to a function that iterator is effectively two completely different types with different memory layouts on debug and release. This is a common issue with C++. Less so with C. But it's not a platform choice per se.
> IUC debug symbols can be totally separated from the object code, such that you can debug the release if you download the debug symbols. A well configured GDB on distros that offer this feature is able to do it automatically for you. It seems very useful and elegant. Why can't Windows do something like this and how is it an advantage?
MSVC always generates separate .pdb files for debug symbols. Windows tooling has spectacular tooling support for symbol servers (download symbols) and source indexing (download source code). It's great.
> The difference is that on Linux, "compile my program in debug mode"
"Linux" does not have a "compile my program in debug mode" magic toggle (or Release or whatever for what it's worth). Different IDEs and toolchains may have different defaults and expectations. "g++ -g" is not debug mode, it's debug symbols.
The expectation that debug libraries are are compiled with ABI changing flags is, while not fundamental, an significant platform difference.
Historically this might be because MSVC didn't even preserve the standard library ABI across versions (although it has done so for the last 10 years at least), so there were little ABI stability expectations.
> If it works on Linux to mix debug-enabled objects with "release"
it definitely does not. MSVC's debug mode is akin to for instance using libstdc++ with -D_GLIBCXX_DEBUG which does change the ABI. Just passing "-g" which enable debug symbols is very different from what Microsoft calls Debug mode, which adds very extensive checks at all levels of the standard library (for instance, iterators become fat objects which track provenance, algorithms check preconditions such as "the input data is sorted", etc.)
Note tough that libstdc++ has an ABI compatible debug mode that still adds a significant amount debug checks (and it is meant for production deployment).
Yes, I wonder that too. The comment says that debug and release builds have ABIs because they have different type layouts. But why do they have different type layouts? Bounds checking and assertions shouldn’t change the type layout. It seems to me that debug flags should generally only modify code generation & asserts. This is usually the case on Linux, and it’s extremely convenient.
If windows is going to insist on different libraries in debug and release mode, I wish the development version of the library bundled debug and release builds together so I could just say “link with library X” and the compiler, linker and runtime would just figure it out. (Like framework bundles on the Mac). Windows could start by having a standard for library file naming - foo.obj/dll for release and foo-debug.obj/dll for debug builds or something. Then make the compiler smart enough to pick the right file automatically.
Seriously. It’s 2024. We know how to make good compiler tooling (look at go, Swift, rust, etc). There’s no sane reason that C++ has to be so unbelievably complex and horrible to work with.
Windows debug builds add extra sanity checks for which it needs extra members in types. For instance, a vector<T>::iterator is just a T* in a regular build, but in a debug build it also keeps a pointer to the vector so it can check bounds on every access.
But yes, C++ punts a lot of things to the build system, partly because the standard has to work on embedded systems where shared libraries don’t exist. A better build system could fix most of these things, but every build system that tries ends up massively complicated and slow, like Bazel.
But fixing it in Windows doesn’t mean they also need to fix it in embedded systems. Microsoft is in control of their whole ecosystem from the kernel to userland to visual studio. Microsoft could make C++ on windows sane without anyone else’s permission. Their failure to do that is on them and them alone.
I think browser vendors have the right idea when it comes to evolving standards. Vendors experiment using their own products and then come together and try and standardise their work at committee. I think that would be a much better idea than either doing nothing or, as you say, trying to boil the ocean.
> Bounds checking and assertions shouldn’t change the type layout.
Any bounds checks and assertions that rely on storing additional data such as valid iterator ranges or mutation counters would need to change the type layout, wouldn't they?
Even if the STL were purely a header-only library (and influenced only by code generation changes for debug builds), there's still the problem of ABI compatibility across different translation units--or different libraries--which might be built with different options.
EDIT: One of your sibling comments goes into greater detail!
There's a bit of a conflation here; partially my fault. Allow me to clarify...
To generate debug symbols for a given binary (whether executable or library) on Windows and MSVC's cl.exe (and Clang on Windows), compile with `/DEBUG`[1] and one of `/Z7`, `/Zi`, or `/ZI`[2]. This is equivalent to `-g` on Linux gcc/clang. In particular, `/Z7` generates separate `.pdb` files, which contain debug symbols for the binary in question.
The options that the parent commenter and I were discussing, i.e. `/MD`, `/MDd`, /MT`, and `/MTd`[3] have to do with the C and C++ runtime link configuration. These correspond to multithreaded dynamic, multithreaded dynamic debug, multithreaded static, and multithreaded static debug respectively. Therefore, the small `d` refers to debug versions of the C and C++ runtimes. The differences between the debug and release versions of the C and C++ runtimes are listed in the following links[4][5][6][7][8]. The last link in particular demonstrates the debug CRT's functionality.
Conventionally on Windows, debug binaries are linked to the debug versions of the C and C++ runtimes; ergo the requirement that 'Release and Debug binaries on Windows cannot be combined'. This convention is respected by all maintainers who release binary libraries on Windows.
There is no equivalent on Unix-likes: it'd be like having 'debug' versions of libc.so.6/libstdc++.so/libc++.so/libpthread.so with different ABIs. If you wanted to change between release/debug here, you would have to at least re-link (if not re-compile) everything. Imagine having `-cstdlib=libc-debug` and `stdlib=libc++-debug` options.
Both sets of options (debug symbol options and C runtime link options) are orthogonal, and may be freely combined. Hence, it is perfectly possible to link the debug versions of the C and C++ runtimes to a 'release' executable, although it would be pretty weird. For instance, `/O2 /LTCG /arch:AVX2 /MTd`. Equivalent imaginary GNU-style command: `-O3 -flto=thin -march=x86-64-v3 -cstdlib=libc-debug stdlib=libc++-debug -static`. You can see what I mean, I hope.
I think the main reason they don't offer a `--make-my-binary-compatible-with-the-ancient-linux-versions-users-often-have` is that GCC/glibc is a GNU project and the are philosophically against distributing software as binaries.
I don't think there's any technical reason why it couldn't be done.
To be fair to them though, Mac has the same problem. I worked at a company where we had to keep old Mac machines to produce compatible binaries, and Apple makes it hard to even download old versions of MacOS and Xcode.
I guess the difference is MacOS is easy to upgrade so you don't have to support versions from 13 years ago or whatever like you do with glibc.
I used to think that binary compatibility benefits proprietary applications, but I'm not so sure anymore. From a commercial perspective, when we break binary compatibility (not that we want to), it's an opportunity for selling more stuff.
Many distributions do periodic mass rebuilds anyway and do not need that much long-term ABI compatibility. Binary compatibility seems mostly for people who compile their own software, but have not automated that and therefore couldn't keep up with updates if there wasn't ABI compatibility.
I agree. It's annoying for closed source apps but they generally have the resources to deal with it anyway. E.g. with Questa I can just unzip it and run it. No trouble.
It's disproportionately annoying for open source projects who don't want to waste their time dealing with this.
> I think the main reason they don't offer a `--make-my-binary-compatible-with-the-ancient-linux-versions-users-often-have` is that GCC/glibc is a GNU project and the are philosophically against distributing software as binaries.
You don't have to statically compile glibc, gcc just needs an option to tell the compiler to target say, version 2.14 instead of the latest one.
The newest glibc has all the older versions in it. That's why you can compile on say ubuntu 14 and have it run on ubuntu 24.
No like, the point is that the only reason you (and I: I do this all the time, including with my open source software... like: no judgment) want to target some old version of glibc is so you can distribute that binary to people without caring as much about what version of the OS they have; but that would be unnecessary if you just gave them the source code and have them compile their own copy for their system targeting the exact libraries they have.
Unfortunately most people don't want to bother compiling, myself included. I tried gentoo one time and it took 1 hour to compile 5 minutes worth of apt-get on ubuntu.
Only the dynamically linked bits, the statically linked startup code and libc_nonshared.a are missing from newer versions. Most programs don't need them (who needs working ELF constructors in the main program?). The libc_nonshared.a bits can be reimplemented from scratch easily enough (but we should switch them over to header-only implementations eventually).
> They are a problem because gcc automatically links to the latest version of glibc. As to why they don't add an option to specify an older version?
Because glibc and ld/lld are badly designed. glibc is stuck in the 80s with awful and unnecessary automagic configure steps. ld/lld expect a full and complete shared library to exist when compiling even though it expects a different shared library to exist in the future.
Zig solves the glibc linking issue. You can trivially target any old version for any supported target platform. The only thing you actually need are headers and a thin, implementation free lib that contains stub functions. Unfortunately glibc is not architected to make this trivial. But this is just because glibc is stuck with decades of historic cruft, not because it's actually a hard problem.
The zig compiler can compile C and C++, using llvm, and it also packages various libc implementations, including glibc, musl, mingw, and msvc, and more for other OSes. Some people use it as a more convenient golang-like cross-compiler. And this whole combination is a smaller download and install than most other toolchains out there. It just took some inspiration and grunt work, to dissect the necessary (processed) headers and other bits of each libc ... hats of to the zig devs.
Linking to the latest version of glibc is, in itself, not a problem -- glibc hasn't bumped its soname in ages, it is using symbol versioning instead. So you only get a problem if you use a symbol that doesn't exist in older glibc (i.e., some specific interface that you are using changed).
As for using an older version of glibc, _linking_ isn't the problem -- swapping out the header files would be. You can probably install an old version of the header files somewhere else and just -I that directory, but I've never tried. libstdc++ would probably be harder, if you're in C++ land.
Recent libstdc++ has a _dl_find_object@GLIBC_2.35 dependency, so it's not exactly trivial anymore to link a C++ program against a older, side-installed glibc version because it won't have that symbol. It's possible to work around that (link against a stub that has _dl_find_object@GLIBC_2.35 as a compat symbol, so that libstdc++ isn't rejected), but linking statically is much more difficult because libstc++.a (actually, libgcc_eh.a) does not have the old code anymore that _dl_find_object replaces (once GCC is built against a glibc version that has _dl_find_object).
This applies to other libraries as well because there are new(ish) math functions, strlcpy, posix_spawn extensions etc. that seem to be quite widely used already.
I find it's best to treat this as a case of "cross-compilation to old glibc version".
That is, you don't want to just link against an old glibc version, you want a full cross-compilation toolchain where the compiler does not pick up headers from `/usr/include` by default (but instead has its own copy of the system headers), and where libstdc++ is also linked against that old glibc version.
We used https://crosstool-ng.github.io/ to create such a "cross-compiler". Now it doesn't matter which distribution our developers use, the same source code will always turn into the same binary (reproducible builds, yay!) and those binaries will work on older distributions than the developers are using. This allows us to ship dynamically linked linux executables; our linux customers can just unzip + run, same as our windows customers, no need to mess with docker containers.
The downside is that we can't just use a library by `apt install`ing it, everything needs to be built with the cross compilation toolchain.
As to why they don't add an option to specify an older version? I don't know either and it is rather annoying to have to use docker images of older OSes to target older glibc versions. It's just one of many things that prevents linux from being as popular as windows for desktop users.