Just spitballing on the compiler motivations here as I'm no designer of them, but if nothing else, consider this a possible explanation. Also, somewhat curious myself, and, Godwin's Law and all, y'know?
Compilers prefer to keep things as general as possible; avoiding special cases means less stuff to check, and there's already so many thousands of things to check, in so many combinations (in something this complex, you're constantly fending off the combinatorial explosion). Sometimes this isn't possible, and special cases must be made (e.g.
__attribute__((signal)) or whatever platform-specific equivalent defines an ISR). For startup, it is only ever needed exactly once, and in a form specific to that platform. There's no point in making the compiler go through all the semantics of compiling machine code, when it's starting from an ill-defined state. Just shove all that off to one side and be done with it.
Actually, what's shown in the link isn't all that special. Pure C is used to define data structures such as control registers and IVT; which are then mapped in by the linker (the linker script will give the physical addresses for the sections e.g.
.isr_vector -- although not the registers, as those are accessed by raw pointer ("
#define RCC_BASE 0x58024400", "
#define RCC ((rccStruct*) RCC_BASE)"), but could be done this way as well), and the only ASM is the very bare bones stuff -- stack pointer and memory init, and at that, mainly because 1. ASM can be better optimized, 2.
memcpy/set have semantics that don't add anything here, and 3. if overridden by an arbitrary (read: inappropriate, buggy, or out-and-out wrong or malicious) user function, might ruin the memory state entirely.
That leaves the compiler operating on the assumptions that all data is initialized (memory is already magically set up) as expected, the stack, well, works; and the ABI can be followed (register allocation conventions, sharing/pushing between function calls, etc.).
Finally, optimizations can be performed, like inlining main() and init() and etc. (they're only called once), stripping out function pre/postambles (particularly when inlined, and particularly that main() does not return), so although these are separated out as functions, they all end up in one block with almost no overhead.
Meanwhile, many of these are "weak" so they can be overridden by the user -- perhaps one wants slightly different inits, maybe rolling register inits into a block (rather than using the mess of HAL calls), perhaps optimizing memory init for faster startup (more often done by placing variables in .noinit or something like that; if .data and .bss end up zero length, LTO should remove the for() loop -- the asm (in this syntax) cannot be removed though), who knows. Wel,, most of
these that are weak are the ISRs of course, but other platforms have more init()s, or you can add them to this file of course.
And, as far as some asm vs. linker vs. compiler routes, it depends how much visibility and generality is needed (e.g. symbols with named types and semantics and optimizations available) versus single-use, special-case objects and getting a faster build time (probably registers are done by naked pointers because burdening the linker with the allocation of fixed addresses would be both trivial and a waste of time?). Remember, the compile chain itself is subject to optimization too -- developers looove a quick build cycle.
Related question: why can't it be handled by the hardware?
I think there might actually be some platforms with a .data section in the hardware. CPU/system registers of course can be set up with default (on reset) values for a given typical operating configuration. It could very well be that init() is stubbed out on such a platform, and RESET_VECTOR jumps right into main(). But these are going to be more specialized or limited platforms as well.
Tim