Guide - How to avoid C/C++ runtime on Windows

Oh, I'm blind and misread the condition - did not notice ! operator. You're right, for native code it will call exit(...) from this place.

Edited by Mārtiņš Možeiko on
Thank you both for your investigations.

I suppose that I will simply do like the VS 2019 CRT runtime do and use exit.
Hey!

Is there something similar to this on Linux? How to avoid all default libraries on Linux?

I know that that is a "-nodefaultlibs" option on Clang (probably on GCC too) in Linux that does the same as "/NODEFAULTLIB", but I have no idea what to do after using it. The entry point, the things I have to replace, stack sizes, etc.

If anyone writes some guide like this for Linux would be awesome!
Awesome! Thank you Simon!
mmozeiko
4) Global objects with C++ constructors and destructors - it's possible to implement it, but it's not needed for us.


I have a couple of global objects, from a very simple template structure I created for DirectX COM objects, just to take advantage of destructor to call ->Release() in the COM objects, since I hate keeping track of each one I create, and I don't like the warnings that DirectX Debug Layer gives me if I don't Release them. So, without the CRT, I'm having a error LNK2019: unresolved external symbol atexit referenced in function "void __cdecl `dynamic atexit destructor which I can guess is the exact same problem you mention in item 4).

How can I work around this? How to implement atexit? Also, since we use ExitProcess() to exit, how will the destructors be called? Is it where atexit comes in?

EDIT:

I just found the definition of the atexit function:
1
2
3
int atexit(
   void (__cdecl *func )( void )
);


Didn't know it is at stdlib.h

Edited by Leonardo on
atexit won't be called by anybody if you don't use C runtime. C runtime calls atexit registered function pointers after main returns. Because there's no "main" anymore, there's nothing to call afterwards. You need to manually keep list of function pointers that are registered with atexit and call them yourself. ... or don't call anything. OS will release all process resources on exit anyway.

If you have global objects with destructors, you'll need to implement global destructor stuff to, not only atexit. Otherwise nobody will call global constructors/destructors.

Check out "C:\Program Files (x86)\Windows Kits\10\Source\10.0.19041.0\ucrt\internal\initialization.cpp" file which is part of Universal CRT source code you can install. It is how MSVC runtime does constructors & destructors - "initialize_c" and "uninitialize_c" functions. __xi_a & other symbols are defined in "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.28.29333\crt\src\vcruntime\internal_shared.h" file.

atexit callbacks are called from "C:\Program Files (x86)\Windows Kits\10\Source\10.0.19041.0\ucrt\startup\exit.cpp" file - with "_execute_onexit_table(&__acrt_atexit_table)"

I know this is an old post, but I thought this might be a helpful addition. Let me know if I can improve any of the formatting as I didn't see a format guide, so I just started throwing Markdown at it when I noticed it worked. :)

I have been playing around with no MSVCRT/UCRT using clang on Windows (LLVM for Windows, not under MinGW64) with the Windows 10 SDK. I'm not sure if this is because I am using a pure C compiler and not a C++ compiler, or clang vs MSVC, but I do not run into any of the issues mentioned in this thread (using floats or large arrays on stack). As such I didn't need to include any of the helper .cpp files provided.

Executable size is around 4KB when compiled using -Os.

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2022-03-01    17:07           3072 win32.exe

Compiling

Command-line:

clang win32.c -o"build/win32.exe" -nostdlib -ffreestanding -fuse-ld=lld -Xlinker /SUBSYSTEM:windows -Xlinker /STACK:0x100000,0x100000 -fno-stack-check -fno-stack-protector -mno-stack-arg-probe -std=c2x -Wpedantic -Wall -Wextra -Os -lkernel32 -lshell32 -luser32

This is using the LLVM lld-link.exe interface to actually link the executable, so I'm passing the /SUBSYSTEM arg via -Xlinker.

Test Code

I have to manually include shellapi.h because WIN32_LEAN_AND_MEAN removes the include from Windows.h.

The meat of WinMainCRTStartup is converting the Windows UTF-16 command line into UTF-8 for main to consume. The reason I'm passing to main is that, ideally, I would use it for entry on every other OS and WinMainCRTStartup is just for bootstrapping Windows to provide anything main would be provided by the other OSs.

Any constructive criticism is encouraged. :D I know I went a little nuts with the NO..... defines. :D

My quick and dirty testing code, compiles with no errors or warnings and runs without issue on Windows 10 21H2 19044.1566, YMMV.

#define WIN32_LEAN_AND_MEAN
#define UNICODE
#define NOMINMAX
#define NOCOMM
#define NOMCX
#define NODRAWTEXT
#define NOHELP
#define NOMENUS
#define NOCTLMGR
#define NOKANJI
#include <Windows.h>
#include <shellapi.h>

typedef unsigned long long usize;
#define nullptr ((void*) 0)

int main(int argc, char** argv);

void __stdcall WinMainCRTStartup(void) {
    int argc         = 0;

    wchar_t** argv_w = CommandLineToArgvW(GetCommandLineW(), &argc);
    if (argv_w == nullptr) {
        ExitProcess(GetLastError());
    }

    char** argv = {0};
    argv        = HeapAlloc(GetProcessHeap(), 0, argc * sizeof(char*));
    if (argv == nullptr) {
        ExitProcess(GetLastError());
    }

    for (usize i = 0; i < (usize) argc; ++i) {
        int bufferSize = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, argv_w[i], -1, nullptr,
                                             0, nullptr, nullptr);
        if (bufferSize == 0) {
            MessageBox(nullptr, L"Unable to get buffer size", L"Win32 Error", MB_OK);
            ExitProcess(GetLastError());
        }

        argv[i] = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufferSize + 1);
        if (argv[i] == nullptr) {
            MessageBox(nullptr, L"Unable to allocate memory for item in argv", L"Win32 Error",
                       MB_OK);
            ExitProcess(GetLastError());
        }

        int bytesWritten = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, argv_w[i], -1,
                                               argv[i], bufferSize, nullptr, nullptr);
        if (bytesWritten == 0) {
            MessageBox(nullptr, L"Unable to convert argv", L"Win32 Error", MB_OK);
            ExitProcess(GetLastError());
        }
    }

    LocalFree(argv_w);

    // NOTE(ddouglas - 2022-03-01): Pass to main()
    int result = main(argc, argv);

    // Exit with the result from main
    ExitProcess(result);
}

int main(int argc, char** argv) {
    for (usize i = 0; i < (usize) argc; ++i) {
        MessageBoxA(nullptr, argv[i], "Win32", MB_OK);
    }

    return 0;
}

Edited by Derek_Nine80 on Reason: Cleaned up the code, removed unecessary frees on exit

I'm not sure if this is because I am using a pure C compiler and not a C++ compiler, or clang vs MSVC, but I do not run into any of the issues mentioned in this thread (using floats or large arrays on stack).

_fltused thing is cl.exe only issue. For clang it is not a problem.

And for large arrays it is not a problem for you because you use -mno-stack-arg-probe argument that disables stack probes - which prevents chkstk() function calls. But remember to set call stack large enough in linker arguments, otherwise default 4KB commit will not be enough.

In your example there's some useless code - HeapFree() after return of main. There's no reason to run extra code when process is terminating, just waste of time.

strlen_w is not really needed in your code, because WideCharToMultiByte accepts -1 for length, then it will use fact that strings are zero terminated.

This is using the LLVM lld-link.exe interface to actually link the executable, so I'm passing the /SUBSYSTEM arg via -Xlinker.

It is not. For clang to actually use lld linker you need to pass -fuse-ld=lld argument. Otherwise it will use link.exe from MSVC for windows builds. You can see what you're using by passing -v argument to clang.


Edited by Mārtiņš Možeiko on
Replying to dd-nine80 (#26056)

_fltused thing is cl.exe only issue. For clang it is not a problem.

And for large arrays it is not a problem for you because you use -mno-stack-arg-probe argument that disables stack probes - which prevents chkstk() function calls. But remember to set call stack large enough in linker arguments, otherwise default 4KB commit will not be enough.

Thank you for the clarification! I'll adjust to make sure my stack is set properly.

In your example there's some useless code - HeapFree() after return of main. There's no reason to run extra code when process is terminating, just waste of time.

I agree that the OS will clean it up on process exit, but if you are running it through an analyzer (e.g. an equivalent to valgrind on Windows) it should complain about leaked memory at exit. I guess that's more of an OCD thing then a real concern as the user won't care of leaked memory on exit. ;)

strlen_w is not really needed in your code, because WideCharToMultiByte accepts -1 for length, then it will use fact that strings are zero terminated.

That was my misunderstanding, I was just using the -1 to find the buffer size (edit: I lied, I was not, even though the docs said you could - fixed) and thought I would need the actual length on subsequent call. Obviously if the -1 works on determining the buffer size, it should work in the other call. :D Thanks!

It is not. For clang to actually use lld linker you need to pass -fuse-ld=lld argument. Otherwise it will use link.exe from MSVC for windows builds. You can see what you're using by passing -v argument to clang.

Crap, you are 100% correct. I'll edit my post above to include the -fuse-ld=lld in the command. I ran it with -v this time to confirm:

"C:\\Program Files\\LLVM\\bin\\lld-link" -out:build/win32.exe 

Edited by Derek_Nine80 on Reason: I was not using -1 at all in either calls to WideCharToMultiByte
Replying to mmozeiko (#26058)

WARNING This text is old, and a bit outdated. Some of information is not relevant anymore, and many things can be done in better way (read the new guide). WARNING

@mmozeiko, I searched around for a new guide on this website, but didn't find one. Where is the new guide you mentioned?

Where is the new guide?

I did same thing to build Console application in C using visual studio and msbuild options. Here is my settings. Compiler settings:
0.png

1.png

2.png

3.png

And Linker settings:
4.png

5.png

6.png

7.png

and my entry point is:

void __stdcall mainCRTStartup()
{

}  

Basiclly, disable all features :)

Does anyone know of a similar guide that could work for MSYS2 (with GCC) instead of visual studio?

In general I would recommend using clang on Windows - as it can produce native debug info in pdb format, so you will have much better time debugging, profiling & using other tools on windows. And for clang-cl same logic applies as for cl.exe to not depend on CRT runtime code.

But for gcc it really depends on what kind of gcc build you have. Different gcc builds are made differently, so they may depend on different runtime dll files. In general running gcc -nostdlib file.c where file.c has _start function for entry point will prevent of including any CRT runtime code. But as I mentioned earlier - it will depend on actual gcc build, you might need extras there.


Replying to trf2. (#30298)