Some points in both directions I want to address:
Quote:
There is also dynamic loading, which is quite nice. It allows programs written for that to be extended with new plugins after they were written, and it requires dynamic linking to work.
Quote:
the plugin thing can be solved statically if everything is open source
You don't need dynamic linking, and you don't need things to be open-source - you can distribute plugins as object files and use initializers to make the rest of the program aware of them when they are linked in.
Quote:
Dynamic libraries are a prototypical example of a simple idea that got away from people. Basic idea was to share library code among multiple processes. But the plumbing to make this all work ended up taking up more space than it saved. There's the need to use position-independent code, which at least in those architectures not using PC-relative addressing is still relatively expensive. There's the GOT and the PLT. There's the dynamic linker, which is no small piece of code. There's symbol tables, symbol visibility, symbol version, init/fini ordering. There is the matter of the dynamic linker having to relocate itself on startup, which is no mean feat. And of course, dynamic TLS modules are quite complicated to support right. All of the relocation types are arch-specific, but also kinda not, since most archs recycle the same ideas. But finding the right abstractions is a chore.
Technically, several of these things aren't necessary for dynamic linking. You can be build shared objects that aren't PIC, you can make a dynamic loader that is fully static and loaded at a fixed offset so it doesn't need to relocate itself (I do this!)...
Quote:
If you look at it closely, you see that dynamic linking only saves on disk space, since the library functions do not need to be in the program image file. But the space is still taken up in memory when the program is loaded.
This is wrong - or at least, it should be wrong in theory. First, when the library is mapped, it's not all loaded into memory (on a real OS, at least...): it gets loaded page by page as it is accessed by some process using it. Part of why GOTs and PLTs are a thing is to avoid touching as much of the code as possible by centralizing the places that need to be written to by relocations, minimizing the number of modified pages but also minifying the number of accessed pages. You can even potentially save memory over static linking if you reference a large function (or tree of functions) but then never end up calling them based on runtime conditions. There's also lazy binding - symbol relocations that aren't actually done until needed.
This is the caveat for what I mentioned earlier, though: If you have non-PIC shared libraries, they'll contain relocations within the code, and suddenly you have to write to all of it and it's no longer shared between processes by CoW
and this really messes up the lazy binding benefits so you probably brought in far more of the library than you would have liked...
Quote:
With statically linked programs, there is a certain degree of compatibility. The only form of dynamic linking going on in there is the linkage to the kernel, and that interface is well specified. If you try a call that doesn't exist, you get -ENOSYS back. If you try a call that did exist, you get the same behavior you always got if that is an option, or else -ENOSYS. A statically linked program for Linux 0.x, compiled in the mid-90ies can be executed without changes on the most modern laptop in the world, and work as well as it ever did, even if the exact same source code would get you a different program today.
This benefit of static linking relies on the kernel providing a backwards-compatible interface - something Linus has regularly gone on rampages to defend for Linux, but allow me to offer a counterpoint: I changed the ABI for my syscall interface recently, to support the
syscall instruction. In order to do that, I had to reorder argument registers. I was able to do this without recompiling a single application in my package repository
because they all made system calls through the dynamically-linked libc.
And that's not something limited to the libc: My windowing system uses client-side decorations. All applications that want title bars link with a library that provides them. A bit back, I added support for a minimize button: All I had to do was change that library - again, I didn't have to recompile anything in the package repository and suddenly all of my applications had this button. Around the same time, I added a hover highlight for those buttons, and again, I didn't have to recompile anything but the decorator library to support this in all of my applications.
Quote:
This means you can implement graceful fallback to older algorithms. You tried statx() on an old kernel and it didn't work? OK, try fstatat() instead. But with dynamic linking? Well, since they introduced symbol versions, you cannot downgrade libraries. If your library doesn't have a symbol version, the program just fails. Recently had a program sent to me that was compiled for Ubuntu, and I'm running Debian, and the program couldn't run. I had to install a Ubuntu jail just for that one program. That is bloody well insane!
I feel like this is a failure of conventions (and a failure of glibc, in particular) more than it is one of dynamic linking in general - why do we require that symbols resolve? Why can't we have an interface to check if a function was successfully bound at runtime? Why is there no easy way to target an older version when I build so we don't run into this with symbol versioning? Bah!