Skip to content

Instantly share code, notes, and snippets.

Last active April 1, 2024 19:49
Show Gist options
  • Save mmozeiko/81e9c0253cc724638947a53b826888e9 to your computer and use it in GitHub Desktop.
Save mmozeiko/81e9c0253cc724638947a53b826888e9 to your computer and use it in GitHub Desktop.
How to avoid linking to CRT with MSVC in modern way

How to avoid linking to CRT with MSVC in modern way

Compile & link like this:

cl.exe main.c /nologo /W3 /WX /O2 /GS- /link /fixed /incremental:no /opt:icf /opt:ref /subsystem:windows libvcruntime.lib

and put your code in:

  • int WinMainCRTStartup() if you use /subsystem:windows
  • int mainCRTStartup() if you use /subsystem:console

For C++ code add extern "C" in front of these functions.

Instead of relying on various hacks and manually declaring implicit symbols for functions or globals that compiler needs, just let it use them from libvcruntime.lib - there are very few things it will take from there. Linker will take only needed symbols, and remove unreferenced ones (due to /opt:ref).

Explanation of arguments:

  • /nologo - do not display cl.exe/link.exe headers in output, less garbage to look at
  • /W3 /WX - enable warning as errors, to better catch errors in your code
  • /O2 - enables optimizations, can use /O1 for fewer optimizations, but smaller size
  • /GS- - prevents adding buffer check code that requires extra CRT runtime functionality
  • /fixed - does not add relocation section to .exe file, it's not really needed for .exe (so you get a bit smaller size), but do NOT use this when building .dll files, those need relocations
  • /incremental:no - does not generate extra code needed for incremental linking
  • /opt:icf - perform identical code folding, in case multiple functions generates identical code bytes merge them into one copy in output binary
  • /opt:ref - remove unreferenced functions/globals
  • /subsystem:windows - create "gui" executable without console attached

Don't forget to add any extra .lib files you might need - like kernel32.lib, user32.lib, etc...


cl.exe likes to generate calls to memcpy and memset when you initialize & use larger arrays or structures. It also expects _chkstk function when local variables on stack exceed page size (4KB). Also it expects to have initialized global _fltused variable when you use floating point due to some obsolete reasons. All of this can be handled manually in your code, but instead you can simply allow linker to take code for this from CRT (libvcruntime.lib) and not deal with it at all. All of these functions are small and standalone, they won't pull in rest of the CRT. As long as you don't call any CRT function, there won't be other CRT code called than these few implicit calls to memcpy/memset/_chkstk functions.


There are limited set of headers you can include and use functionality from them. Few common ones that are OK to use:

  • stddef.h - if you want size_t and NULL
  • stdint.h - various u/intXX_t typedefs
  • stdarg.h - va_arg, va_start, va_end, va_arg things
  • intrin.h and few other headers for intrinsic functions (rdtsc, cpuid, SSE, SSE2, etc..)

Because there will be no CRT code now that initializes on startup, you cannot use multiple things from compiler that depends on this global initialization, for example:

  • no thread local storage (TLS)
  • no global constructors & destructors in C++
  • no RTTI in C++
  • no pure virtual member functions in C++
  • no exceptions

It would be possible to add extra code to handle these, but alternative is simply to manually handle these things - TlsAlloc/TlsGetValue & other functions for TLS, manually calling functions for initialization instead of global constructors, etc..

Debug builds & address sanitizer

Unfortunately to use address sanitizer you should use normal CRT runtime. Without proper startup it will not catch all the errors only some of them. So to run properly with debug CRT, use the normal entry point main or WinMain in your code, and compile as:

cl.exe main.c /nologo /W3 /WX /MTd /Od /Zi /RTC1 /fsanitize=address /link /incremental:no /subsystem:windows

The different arguments from before:

  • /MTd - request debug runtime
  • /Od - disables optimizations
  • /Zi - enables debug info generation (automatically passes /debug to linker to create .pdb file)
  • /RTC1 - enables extra run-time error checks for simple variables/arrays on stack
  • /fsanitize=address - enables address sanitizer

No additional libraries needed, because compiler will automatically select from /MTd argument.

Formatting strings, the easy way

Sometimes you want to use snprintf or similar C formatting functions. There are various options for this:

  • use wsprintfA/W from user32.dll - limits functionality, max 1024 output, no floats, and no C99 formatters (like %zu)
  • use wnsprintfA/W from shlwapi.dll - similar limits to wsprintfA/W as above
  • use snprintfA/W from msvcrt.dll - this dll is present on Windows since forever, but it does not support C99 formatters
  • use stb_sprintf.h or c99-snprintf (alternative location) or nanoprintf.h standalone single-file libraries, just adds a bit more code to your executable
  • use stdio functions from Universal CRT dll

In UCRT case we can allow linker to link to dynamic UCRT dll file, which is always present on Windows since Windows 10 version. If you don't need to support older than Win10 then you get all the goodies like c99 formatters for free. Just add ucrt.lib to linker arguments and you can call sprintf and similar functions. Same as above - it won't add much of CRT to your executable, just one function call which takes no space at all.

In general you can use extra functions from UCRT if you don't feel like implementing them yourself. For example, sinf or cosf from math.h - they will be linked to UCRT .dll files. Be careful about debug dll runtime - it may not work if you don't use proper entry point with regular CRT runtime setup (see section above debug builds)


#include <windows.h>
#include <stdio.h>

#pragma comment (lib, "kernel32")
#pragma comment (lib, "user32")

int WinMainCRTStartup()
	char str[8192];
	snprintf(str, sizeof(str), "Hello %s!\n", "World");
	MessageBoxA(0, str, "Example", 0);
	return 0;

Run the following:

cl.exe main.c /nologo /W3 /WX /O2 /GS- /link /fixed /incremental:no /opt:icf /opt:ref /subsystem:windows libvcruntime.lib ucrt.lib

It will produce ~3KB exe with only two import functions:

  • __stdio_common_vsprintf from api-ms-win-crt-stdio-l1-1-0.dll
  • MessageBoxA from user32.dll

You can use dumpbin /nologo /imports main.exe to check for this.

Bonus round - clang-cl

If you're using clang-cl compiler, you can add extra /clang:-fno-asynchronous-unwind-tables argument to omit generation of unwind tables in optimized binaries. This will drop few extra KBs off your executable size. But it will prevent debugger working properly in your code - also no exceptions, no profiler call stacks & other tools that use call stack unwinding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment