I think there is a more fundamental architectural issue here, beyond simply choosing a different dynamic linker.
Look at the Windows NT architecture. ntoskrnl and ntdll.dll form a relatively stable system foundation. The kernel is responsible for core process and image management, while NTDLL provides essential user-space system functionality, including the user-mode loader. Higher-level APIs such as Win32 and C runtime libraries are built on top of this foundation. They can evolve independently without fundamentally changing the executable loading architecture.
I think this separation of responsibilities is much cleaner.
There are two things about the Linux ELF architecture that I find particularly problematic:
1. Why are static and dynamic executables loaded through fundamentally different paths?
The kernel directly loads a static executable, but for a dynamically linked executable, it hands control over to a user-space ld.so, which then takes responsibility for loading the remaining dependencies and preparing the program.
In my opinion, executable loading should be a unified system mechanism. Whether an executable is static or dynamic, it should go through the same fundamental loader. The difference should simply be whether additional shared libraries need to be loaded.
2. Why is the dynamic linker so closely tied to libc?
A dynamic linker is a fundamental system component. Its responsibilities include loading shared objects, symbol resolution, and relocation. These are core executable-loading functions, not ordinary C library functionality.
Libc, on the other hand, is an application runtime library. It is perfectly reasonable for libc implementations to evolve, change, and be replaced. But the fundamental executable-loading mechanism should not be tied to the development cycle or implementation details of a particular libc.
Ideally, ld.so should be an independent, stable system component, with a small and well-defined interface to the kernel. It should not depend on a particular libc implementation.
This is why I find the Windows NT separation between the kernel, NTDLL, and higher-level runtime libraries interesting. I am not suggesting that Linux should copy Windows internally, but I do think the underlying separation of responsibilities is worth learning from.
Regarding your question about testing different linkers: I don’t think this requires making every executable independently choose its own interpreter through PT_INTERP. Explicitly invoking another linker is a much more straightforward way to test an alternative runtime environment:
/path/to/ld.so ./program
Of course, this is not a complete substitute for testing the normal kernel execution path. But it demonstrates that alternative linkers can still be tested without making per-executable linker selection a fundamental system requirement.
I have always felt that Linux has accumulated too many implicit behaviors and overlapping mechanisms. Consider chroot, for example. It is useful for creating an alternative filesystem environment, but it is not a security boundary. Its behavior is easy to misunderstand, and it can create a false sense of isolation. More mechanisms do not necessarily mean better security; they can also make systems much harder to understand and audit.
My goal has always been to simplify Linux: use fewer mechanisms to accomplish the same things, preferably in a more predictable and reliable way.
To be clear, I am not claiming to have a complete implementation of this architecture. My current ld= kernel parameter and modifications to musl’s ld.so are only pragmatic experiments toward this direction.
What I really want is a cleaner separation of responsibilities: the kernel provides the execution foundation, ld.so provides a stable and independent executable-loading mechanism, and libc remains an ordinary, replaceable user-space runtime library.
Do subtraction, get Linux back to KISS.