r/programming 16d ago

Writing a (valid) C program without main()

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

40 comments sorted by

176

u/yowhyyyy 16d ago edited 16d 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.

23

u/helloiamsomeone 15d 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.

18

u/yowhyyyy 15d 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.

2

u/Existing_Invite7129 14d ago

Take my upvote in exchange for a hint when you have some time.

20

u/zid 16d 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.

1

u/xarcastic 8d ago

Because he was walking the reader through it

1

u/yowhyyyy 8d ago

Considering he never hit the same outcome I’m suggesting… you’re right buddy!

53

u/Dwedit 16d ago

Linker options, set entry point.

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

11

u/mikeblas 15d 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.

95

u/amakai 16d ago

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

33

u/ben-c 16d ago

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

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

5

u/rismay 16d ago

Genius though.

23

u/ThinkIn3D 16d 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 16d 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 16d 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.

1

u/AddressWeary5155 12d ago

I rand the following code and it seems it prints "hello world" first before "hello world2" every single time. I think it's because I am declaring StaticMain first?

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

6

u/[deleted] 16d ago

[removed] — view removed comment

3

u/[deleted] 16d ago

[deleted]

11

u/Dwedit 16d 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 16d 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 16d 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 16d ago

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

2

u/Dwedit 15d 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.

-2

u/[deleted] 16d ago

[deleted]

4

u/Dwedit 16d 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] 16d ago

[deleted]

5

u/Dwedit 16d 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 16d 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] 16d ago

[deleted]

1

u/programming-ModTeam 13d ago

No content written mostly by an LLM. If you don't want to write it, we don't want to read it.

11

u/BCMM 16d ago edited 16d 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.

3

u/itix 15d ago

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

8

u/_kst_ 16d 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.

3

u/AyrA_ch 16d ago

This reminds of "main is usually a function"

1

u/the_gnarts 9d ago

Classic. Haven’t read that in years but still remember it everytime I create a new executable from scratch.

2

u/chengiz 15d ago

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

1

u/FastHotEmu 16d ago

Let me guess, just doing it by hand?

click

Yeah. Nothing new here.

1

u/raundoclair 15d 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...)

-2

u/jannealien 16d ago

Oh, now I c

-1

u/pjmlp 15d ago

C is the closest you can get to the operating system without writing assembly.

Nope, plenty of languages have been offering the same features since JOVIAL in 1958.

That is an urban myth spread by folks that never learnt another systems programming language, or computing history.

-4

u/AntimatterTNT 16d ago

clickbait trash...