I'm not sure if this is useful to anyone, but I hacked together the equivalent code for Linux (might also work on macOS, haven't tested yet).
!!UPDATE! THERE'S A REASON NOT TO USE INT $0x80 ON x64!
//this is a wrapper around the Linux exit syscall.
//!! If you want to save an instruction, make status a long,
// replace the middle two lines of the asm block with
// "movq %0, %%rdi\n\t", and use the compiler option "-fno-builtin"
__attribute__((noreturn)) void exit(int status)
//exit() is normally implemented in glibc,
// but since we're not using that, we have to
// do the syscall manually.
asm("movq $60, %%rax\n\t"
"xorq %%rdi, %%rdi\n\t"
"movl %0, %%edi\n\t"
"syscall" :: "r" (status) : "rax", "rdi");
//Using int $0x80 here causes Linux to use the older x86 syscall table
// which, notably, has exit at eax==1, and passes the first param in ebx.
//syscall will use the newer x64 table and API.
//This is a compiler intrinsic that does nothing but
// indicate that execution will never reach a certain area.
// Mostly required to avoid warnings about a noreturn function returning.
//This is where we will tell the linker to start executing our code.
__attribute__((noreturn)) void start(void)
//... Our code goes here.
//All done, call our exit syscall wrapper.
Compile this using:
gcc -o nortlinux -nostdlib -Wl,-estart main.c
The resulting executable, when run, will do nothing and immediately exit.
What are the implications of building Linux applications without glibc?
Well, the big one is that no syscall wrappers are provided.
See, unlike Windows, Linux doesn't have a kernel library separate from its standard C library. glibc implements both Linux's syscall interface as well as the C stdlib. Avoiding glibc means rolling your own syscall wrappers. Fortunately, unlike Windows, Linux's syscalls are all very well documented.
Allocating large arrays seems to be no problem, floating-point seems to work without extra code, but zero-initializing large arrays does create an "undefined reference to 'memset'" linker error. Using the memset implementation from the original post mostly solves the problem, though (at least in pure C mode), size_t is not defined by default -- use unsigned long or typedef it.
I would think it's safe to assume that if you use this method with C++, all the 'features' it provides that are backed by the runtime -- exceptions, new/delete, RTTI, global object ctors/dtors, pure virtuals -- would not be available.
UPDATE: macOS has so far stymied my efforts to dodge the startup code.
For Linux, it seems that there are two other compiler options you can
use: "-nodefaultlibs" and "-nostartfiles", but documentation on GCC seems to suggest "-nostdlib" implies both. Docs also suggest that linking with libgcc.a may be required if it starts complaining about routines GCC includes.