Home

Cursed C++: Printing text with an empty main

Here's a tidbit of obscure C++ knowledge: You don't need to write any code in main() to print text to stdout (or stderr). Most of this article also applies to C.

Calling code through global constructors

#include <iostream>
// This program prints "Hello World!"
int hello = printf("Hello World!");
int main(int argc, char** argv) {}
printf returns an int which corresponds to the number of characters written. Exploiting this fact allows us to neatly call printf to initialize a global variable.

Global variables are initialized in the order that they are defined. They are initialized before the main function is called, even if they are defined below the main function. This is demonstrated in the following code snippet that prints 123main().
#include <iostream>

// Outputs: "123main()"
int one = printf("1");
int two = printf("2");

int main() {
    printf("main()");
}

int three = printf("3");
The initialization of global variables requiring some setup might need what's referred to as a "global constructor". We can get clang to complain about the usage of global constructors by providing -Wglobal-constructors.
$ clang++ empty_main.cpp -Wglobal-constructors
1.cpp:3:5: warning: declaration requires a global constructor [-Wglobal-constructors]
int hello = printf("Hello World!");
    ^       ~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.


Is main() the program's entry point?

You might be wondering how it is possible for code to be called before main, if main is the entry point then this should not be possible. If main is the entry point, then it shouldn't be possible to have code printing to stdout before main's execution. You might think the compiler is inserting code on top of main, but this isn't the case either.

It turns out that main() is not the program's entry point.

Let's take a look at a minimal x86_64 program. Notice the _start: label.
section .text
    global _start

_start:
    mov rax, 60 ; set exit system call
    xor rdi, rdi ; set exit status to 0
    syscall ; call system call
On Linux, ELF "Executable and Linkable Format" specifies that the program's entry point is the _start: label. (Note: It is possible to change this using the --entry option for ld or gcc/g++). Let's look at the initial snippet on Godbolt using clang with -O3 optimization.
main:                                   # @main
        xor     eax, eax
        ret
_GLOBAL__sub_I_example.cpp:             # @_GLOBAL__sub_I_example.cpp
        push    rbx
        lea     rbx, [rip + std::__ioinit]
        mov     rdi, rbx
        call    std::ios_base::Init::Init()@PLT
        mov     rdi, qword ptr [rip + std::ios_base::Init::~Init()@GOTPCREL]
        lea     rdx, [rip + __dso_handle]
        mov     rsi, rbx
        call    __cxa_atexit@PLT
        lea     rdi, [rip + .L.str]
        xor     eax, eax
        call    printf@PLT
        mov     dword ptr [rip + hello], eax
        pop     rbx
        ret
hello:
        .long   0                               # 0x0

.L.str:
        .asciz  "Hello World!"
You might observe that _start: is not in the above snippet. This means that this code does not contain the start of the program.

The GNU C Library, also known as glibc does some setup before the main function. The _start: label is in /lib64/ld-linux-x86-64.so.2. Using ldd on the executable, ldd shows that the shared object containg the _start: label is required.
linux-vdso.so.1 (0x00007ffe2638c000)
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00007f72fde00000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007f72fdd18000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f72fe09b000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f72fdb31000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f72fe0f5000)


When the program starts, the first code to run is the one in the _start: label, this will call a function from glibc named __libc_start_main which does a few things. Notably, it will call the global constructors, which is how the printf code runs first, then it will eventually call the C/C++ main() function with the program arguments.

Interestingly, the main() function is not required to compile. However, glibc expects a main() function, as it calls main() in the initialization sequence, the linker will fail.
#include <iostream>
// Compiles successfully but the linker will complain
// because glibc wants to call main() 
int hello = printf("Hello World!");

Other ways to create global constructors

The GCC attribute constructor allows you to specify a code block as a global constructor. The documentation can be found here. This also works with clang
#include <iostream>

// The program prints "Hello World!"
__attribute__((constructor)) void fake_init()
{
   printf("Hello World!\n");
}

int main() {}
A global constructor can be created by initializing an object in a global scope, this forces this object's constructor to be called.
#include <iostream>
int main() {}
// Calls X's constructor due to "a" being declared
// The program prints "Hello"
struct X {
    X() { printf("Hello"); }
} a;

Questions? Comments? Insults? Send me an email! moc.snetramcirdec@cirdec

Created: 2023-01-20 - Last Edited: 2023-01-22

Other articles

Please consider joining the mailing list to receive an email when a new article is posted. 😎