The _start symbol is the symbol to which the OS transfers control to a userspace program. The ABI of this function call is not the same ABI as a C function, so there's necessary code to translate those functions to main. Additionally, there's a decent amount of stuff that has to be done in main before you run _start, mainly running static constructors, and there's stuff that has to be done after main.
This code is provided by the C runtime. If you use gcc as the linker, the necessary object files to include _start are included in the link line. But when you drive the linker manually, you have to provide those libraries yourself.
(Author here) Thanks for explaining this more succinctly than I can :)
Just one nitpick:
> decent amount of stuff that has to be done in main before you run _start
should read
> decent amount of stuff that has to be done in _start before you run main
Analogously, cleanup code like running global destructors usually happens in _start/CRT after main returns, but I skipped that in the post for the sake of brevity.
> decent amount of stuff that CAN be done in _start before you run main
It’s all optional. Before I found the Linux kernel’s nolibc [0], I wrote a minimal C runtime based on bunch of stuff I read [1]. Only thing you really need to do is align the stack (at least on x86, amd64). You don’t get argc/argv/envp but main will still work.
Recently there is a very bad trend in dev tools that IDEs etc. tend to hide as many details as possible. Unfortunately, some of those details are vital to understand how things work in the first place. The latest example is in dotnet6, Main method is now by default synthetized. Kinda off topic though.