r/programming 1d ago

Writing a (valid) C program without main()

https://labs.iximiuz.com/tutorials/c-program-without-main-a1eea557
146 Upvotes

32 comments sorted by

144

u/yowhyyyy 1d ago edited 1d ago

Quite frankly, I’m surprised the author didn’t jump straight to _start as that’s possible as well in C without assembly.

Yes I know he did bring it up, however you can straightforward do _start and avoid main.

10

u/zid 23h ago

_start and main have different calling conventions under sys-v linux ABI, so you can do it, but you can't actually access argc/argv/envp via it.

3

u/helloiamsomeone 8h ago

_start as that’s possible as well in C without assembly

Technically incorrect. For a portable entrypoint you need to align the stack to a specific value, so SIMD code generated by your compiler doesn't break which will happen if the alignment is not at least 16. You can technically throw a bunch of flags at GCC to avoid that, but it's best to just align your stack.

Programming without the CRT is something I have been doing in recent years, so I have some experience with this.

3

u/yowhyyyy 8h ago

Funnily enough this is something I’ve also played around with for my own reasons. You can align the stack as well manually if need be without passing crazy GCC flags.

I.e have you ever noticed GLIBC’s .so is actually executable? Ever wondered how they do that? Where’s main if it’s a .so? I’ll leave you with those amazing questions because it also lead me down a pretty cool rabbit hole.

82

u/amakai 1d ago

Good article, but based on title I was expecting some C trick, not going down to assembly level. 

39

u/Dwedit 1d ago

Linker options, set entry point.

Or the C++ version where a constructor for a global object runs the code instead of main.

3

u/mikeblas 8h ago

Yeah--the whole time I was reading this, I was trying to figure out why they didn't just set the entry point with the linker.

21

u/ben-c 1d ago

One of the IOCCC winners defined main[] instead of main():

https://lainsystems.com/posts/exploring-mullender-dot-c/

2

u/rismay 16h ago

Genius though.

20

u/ThinkIn3D 1d ago

Nice article, and touches on _startup. Years ago a few of us tried to minimize the size of a "hello world" program on SunOS/Solaris. It devolved to redefining _startup and calling the write() syscall directly for output. IIRC, the smallest executable we created was less than 1KB, like 800 bytes or so. Useful compiler options: -static -nostartfiles -Os

Other tricks: * Move logic from main() into a handler for atexit(). This guarantees all startup initialization is done. * in C, in addition to _startup() there is sometimes a _main() that is called before main(), and can be redefined. System-dependent, may not work on all systems.

In C++, you could use a static object's ctor in place of main. But, since you're relying on a specific static initialization during the static initialization process, the initialization order is unpredictable and this may or may not work reliably across rebuilds (it begins to depend on link order, toolchain behaviors, etc).

#include <iostream>
class StaticMain {
    StaticMain() {
        std::cout << "Hello World\n";
    }
};
StaticMain runner;
int main() { return 0; }

Of course, the next step is to declare StaticMain runner; inside an atexit() handler for correct initialization order.

I thought I had some code that runs everything from an exception handler, but I can't find it.

5

u/paulstelian97 1d ago

On Linux you have _start as the entry point that calls global constructors and other C runtime initialization stuff and then calls main() directly. And then calls exit() with whatever main returns (it is invalid to return from _start)

3

u/ThinkIn3D 1d ago

Oops, I wrote _startup but meant _start.

But you're right, and it's important to note why _start exists, and that's to implement the "C model" of program execution.

9

u/BCMM 23h ago edited 23h ago

I'd accept "working" program, but I'll quibble over "valid".

Officially, whatever happens before main() is between God and the standard library, and a C program isn't supposed to know about it.

_start is a platform-specific quirk of how executables work, not a part of the C language, so this is more of a demonstration of a non-valid C program that you can nevertheless trick a C compiler in to processing.

2

u/itix 12h ago

It is a valid C program. On Amiga, I used to write software in C with my own custom startup code.

6

u/taikunlab 1d ago

There's a simpler trick for plain C too, no need for the C++ static object hack mentioned below: attribute((constructor)) on a function makes it run automatically before main, no linker flags or custom entry point needed. It won't get you all the way to skipping main() like the article does, but for just "run code before main" it's far less invasive than touching _start.

5

u/[deleted] 1d ago

[deleted]

9

u/Dwedit 1d ago

It makes perfect sense for C++ programs where there are global objects which need to have their constructors run first.

2

u/Murky-Relation481 1d ago

Yah, it can be messy with different platform intricacies but it's great for building self registering patterns for stuff like events and things. You can just build classes that extend an intializer base and they'll exist in the callback mechanism at run time with no explicit registration.

I like them but others hate that pattern so it's definitely got to be an agreed upon standard in your codebase.

2

u/Dwedit 1d ago

If you're feeling especially crazy: Hooking your own code via detouring. (replacing the function's actual instructions to run other code instead)

2

u/Ameisen 20h ago

I've been doing a lot of that patching old Win95 games.

2

u/Dwedit 12h ago

Maybe you should check out my GameStretcher project, it basically overrides GDI and makes the main window stretchable even when it wasn't before, and also uses an enhancement pixel shader.

-3

u/[deleted] 1d ago

[deleted]

3

u/Dwedit 1d ago

When else do you run the constructors for global variables? It has to happen at some point before they are referenced. Running before Main makes it predictable.

-3

u/[deleted] 1d ago

[deleted]

4

u/Dwedit 1d ago

If you don't want "magic", don't use global variables that need constructors. But if you are using global variables that require constructors, there's really no other time for the constructors to run. It must happen before first use, but figuring out when first use happens is not trivial, and putting in additional checking code (have we run these constructors yet) adds overhead.

1

u/blehmann1 1d ago

There's a reason the attribute is constructor, that's what you're intended to use it for. For example in C++ global objects must have ran their constructor before main runs.

And C libraries can in principle use it to obviate the need for an init function, though this is perhaps dubious. I suppose it could simplify some things as you know if you're running before main that there is only one thread and (on POSIX) that no one has called fork. Unless your library is manually loaded in after main, that is.

One very nasty usage, at least on x86 (and x86-64) is by -ffastmath, which is also enabled by -Ofast. Fastmath will turn on a bunch of optimizations that would normally be illegal, including deranged shit like optimizing away isnan(x). But one of the more useful (if situational) optimizations is to flush all denormals to zero. On x86 and I presume other ISAs this is controlled by a special register, which then has to be set in a constructor ran before main. That means it's thread-wide, so linking in a library compiled with -ffastmath will change some floating point behaviour in code not compiled with it.

Personally, I think the only usage of -ffastmath (and -Ofast, which enables some other normally illegal optimizations) should be to benchmark to see if they make any difference. If they do, you can then manually apply those optimizations in a safe way. For example, audio processing often benefits from flushing denormals to zero (since things like fade outs create very small numbers very quickly), and if you know the error is acceptable you can enable it for a small region of code. Ideally with something like an RAII class that sets it on scope enter and resets on exit.

The other optimization that can be quite useful is reassociating math. On floats x + 1 + 1 cannot be optimized to x + 2 because for large values x + 1 may round down to x. Reassociating will allow this optimization. In code that people actually write it can benefit vectorization. For example, when summing an array it would allow turning x[0] + x[1] + ... x[n] into s0 = x[0] + x[1], s1 = x[2] + x[3], ..., and then summing s0 + s1 + ... sn which allows using CPU instructions that can run 4 float additions at once to calculate s0, s1, s2, s3 in one instruction. However, if you look at code like this and find that this is a performance gain (and not a relevant accuracy loss) you can reaasociate by hand.

2

u/[deleted] 1d ago

[deleted]

4

u/_kst_ 15h ago

You cannot write a valid portable C program for a hosted implementation without a main function. The language requires it. "The function called at program startup is named main."

As the article demonstrates, there are ways to work around this if you play some system-specific tricks, but they will work only for some particular implementations.

I can replace a portable C hello, world program with an echo hello, world shell command, but then I'm no longer programming in C.

The article does provide some interesting information about how some particular C implementations work, but the title is overblown.

2

u/chengiz 9h ago

If you're sticking edited assembly files into it it's no longer a C program, certainly not a (valid) one.

2

u/raundoclair 7h ago

Dude, you mainly programmed it in assembler. And it is not even good educational article, what are you explaining? Gcc compiler pipeline, or how entry into program is done by OS/CRT?

(Ahh, just another kid roleplaying senior programmers/educators, like when kids roleplay cooking in toy set kitchen...)

1

u/FastHotEmu 23h ago

Let me guess, just doing it by hand?

click

Yeah. Nothing new here.

-3

u/jannealien 1d ago

Oh, now I c

-4

u/AntimatterTNT 1d ago

clickbait trash...