This is very clear, concise technical writing. Thanks for sharing!
It’s similar to, but not quite the same, as DLL export directives on Windows, which has a much more Multics-like view of shared libraries. The main difference is that ELF symbol resolution doesn’t differentiate between undefined symbols that come from the current program and imported ones. With Windows DLLs, you have a table of exported symbols (similar to the dynamic symbol table in ELF) and a table of imported addresses (similar to the GOT in ELF) for each EXE or DLL. Everything is local to the current library unless explicitly stated otherwise. In most cases, you can define export and import macros where export expands to the dllexport attribute on Windows and the visibility-default attribute on *NIX, import expands to dllimport on Windows and nothing on *NIX.
UNIX didn’t go down this path because shared libraries were added quite late and so needed to fit into existing static library workflows. Static libraries on *NIX aren’t really a thing, they’re just a small optimisation over a pile of .o files. There’s no link step involved in creating a static library, ar is just an archive creation tool (like tar, without the assumption that it’s working with tape and so allowed to do random access on its inputs and outputs) with some extra logic that will (optionally) combine all of the symbol tables from the .o files into a summary for the .a file. You can replace a .a file on a *NIX linker invocation with the list of .o files that it contains. *NIX systems that are not AIX go out of their way to maintain the illusion that a shared library is just like a static library and so doesn’t need any explicit imports or exports because you don’t need them for the .o files that, logically, you’re linking together.
Part of this is due to the fact that C/C++ don’t have a notion of libraries in their standard definitions and so have static / anonymous namespace definitions for local-to-the-translation-unit and extern for everything else. This lack caused some problems with C++11, where thread-local destructors were added with no thought as to how they’d interact with shared-library unloading (the two possible answers are: they prevent unloading the library or they silently fail to run).