Is there any way to use variadic functions without the crt in clang? It seems to be impossible on amd64 based on this blog. The blog is old, so I don't know if anything has changed.
So how are va_xxxs implemented inside
stdarg.h on clang? On msvc, they're just simple macros that can be implemented yourself, so
include stdarg.h is optional. Can you do something similar on clang?
Also, a question I've been meaning to ask after reading your crt-free guide is: how can you still use safe headers like
stddef.h, stdint.h, stdarg.h, etc after explicitly telling the compiler to stop using the default lib (
They are implemented in whatever way compiler wants. I mean the similar question would be about how + operator for "a+b" integers are implemented. That's the job of compiler - to implement them for target architecture you're compiling, using opcodes target architecture supports, using ABI target arch supports. It emits relevant opcodes, puts variables/arguments on proper locations on stack, and reads them back as needed. Etc. It's how everything else works in compiler. How does it implement
__rdtsc() intrinsic? By emitting code that target arch has for RDTSC opcode. So same thing about va_xxx things - it emits code that makes them work (because compiler knows how varargs works for target architecture you're compiling).
You can copy out declarations from stdarg.h into your own code. It's not like stdarg.h is special header on its own. No header is special. Preprocessor literally does "copy & paste" when you're compiling your .c/cpp file that includes headers. But I don't see any reason to do that. Because each compiler implements va_args in their own way, it's way better to rely on stdarg.h functionality, than copy things out of it and do many ifdefs yourself. Same thing about stdint, stddef, and similar headers that do not depend on CRT. They contain just compiler/architecture specific typedefs/macros/intrinsics. No reason to not use them. Just use them how they are supposed to be used.
Yes, they are using custom __builtins. You can always just open header and look how they are defines, same way as you did for MSVC.
When you pass in -nodefaultlib to msvc, how can you not include
stdlib, stdio, etc but can include
stdarg, stdint, etc? Do those files have
#if NO_DEFAULT_LIB inside them or something? What is the difference between these CRT files and safe headers that make them behave differently when passing in that flag?
You can actually include stdlib/stdio. But you cannot call functions from it - as linker won't use CRT libs where these functions are defined, so you will get unresolved external error.
But you can use defines & typedefs from these headers just fine. For example, stdlib.h has EXIT_SUCCESS define that you can use with -nodefaultlib just fine.
stdarg/stdint does not have any functions, so including them is "safe" in a way that you can use anything from them.
It is less obvious for MSVC, but typically for C/C++ compiler there are two type of standard headers - ones that is provided by CRT, and ones that are provided by compiler. CRT ones are the ones you cannot use, as they provide runtime functionality (like fopen from stdio, or malloc from stdlib). But compiler ones you can use, as they just provide internal compiler specifics to you - for example, intXX_t types from stdint, va_arg from stdarg, or intrinsics. in MSVC they are all kind of mushed together. But in gcc/clang world they are very separate. Typically glibc or mingw is one that provides CRT headers & libs, and gcc/clang has their own stdarg/stdint & similar headers.
When you said the linker wouldn't use CRT functions, did you mean, for example,
stdio.h just forward-declare
fopen while its implementation gets
#if out? Or are all the CRT functions implemented inside the compiler directly, and the file just declares its signature?
Yes, I was asking how the linker wouldn't use CRT libs. Is the implementation of CRT functions inside the compiler and the linker chooses not to use them, or is the implementation inside the header but gets, for example,
#if HAS_DEFAULT_LIB out? How are those functions (e.g.
malloc, etc) actually defined in the file? That is what I was confused by. Is
fopen defined with
__builtin_fopen or something? Where does the code for these functions live?
CRT functions live in .lib file that provides implementation - either static lib, or dynamic import library. It's just a regular library, nothing special about it.
Some of CRT headers have
#pragma comment (lib, "XYZ") or similar directive. Compiler uses these pragmas to put
/DEFAULTLIB:XYZ.lib arguments in special place inside .obj file that linker will use. Additionally using -MT/MD arguments add also
/DEFAULTLIB:name arguments inside .obj file. They reference CRT library names or user32.lib & similar libs.
Then linker reads .obj files and gets the list of libs to use from /DEFAULTLIB arguments. But if you use
-nodefaultlib argument then linker ignores all .lib files from /DEFAULTLIB arguments from these pragmas/.obj files. Even if you put custom pragma for you own libs in your own code.
For MSVC this page documents which libraries provide CRT functionality: https://learn.microsoft.com/en-us/cpp/c-runtime-library/crt-library-features?view=msvc-170
So, the linker will link all the CRT .lib files by default unless you tell it otherwise. When you call, for example,
malloc, the linker will search for that in all its .lib files. If you pass in
-nodefaultlib, the linker won't find any implementation and will throw out an
unresolved external symbol error. If that's the case, is there any way to link a couple of specific CRT libs, rather than the whole thing?
Yes, that is how it will work. What do you mean by "couple libs"? Whole CRT is typically one lib (with few minor extra helpers). It's not separated by functions.
But what you can do is not worry about it at all, and let linker strip out unused CRT parts. If your entry point is your own code -
WinMainCRTStartup, and not CRT entry points
WinMain - then no CRT startup code will run, and linker will strip it out. It won't be present in exe file. Linker only takes code from libs that is actually called, which means it will take only functions those function you call from CRT. Obviously you won't be able to use functionality in CRT that depends on startup code - so things like TLS or address sanitizer won't work. But other basic stuff will work fine.
Whole CRT is typically one lib
So it's not like
math.h is in one lib file while
stdio.h is in another. If it's inside one big lib, then what's the point of
#pragma comment (lib, "XYZ")?
Linker only takes code from libs that is actually called, which means it will take only functions those function you call from CRT
In your avoiding CRT guide, does the executable size go from 68KB to 2.5KB because of the startup code? If, in that guide, you used
WinMainCRTStartup without passing in
nodefaultlib, would the size still stay at 2.5KB?