How to use va_arg without crt on amd64

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.

va_arg & friends do not depend on CRT in msvc/clang/gcc. They are compile time transformation in compiler codegen, no runtime functionality of CRT is used. Just include stdarg.h and use them as usual.


Edited by Mārtiņš Možeiko on

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 (-nodefaultlib).


Replying to mmozeiko (#29772)

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.


Edited by Mārtiņš Možeiko on
Replying to longtran2904 (#29774)

I was asking specifically how the macros are defined. They seem to be defined with __builtin_va_xxx extension. That is what I was asking.


Replying to mmozeiko (#29775)

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.

https://github.com/llvm/llvm-project/blob/main/clang/lib/Headers/__stdarg_va_list.h
https://github.com/llvm/llvm-project/blob/main/clang/lib/Headers/__stdarg_va_arg.h


Edited by Mārtiņš Možeiko on
Replying to longtran2904 (#29776)

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?


Replying to mmozeiko (#29777)

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.


Edited by Mārtiņš Možeiko on
Replying to longtran2904 (#29780)

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?


Replying to mmozeiko (#29781)

I meant that in "if you use -nodefaultlib then linker won't use CRT libs" context.


Replying to longtran2904 (#29783)

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. fopen, 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?


Replying to mmozeiko (#29784)

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


Edited by Mārtiņš Možeiko on
Replying to longtran2904 (#29785)

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?


Replying to mmozeiko (#29786)

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 - mainCRTStartup or WinMainCRTStartup, and not CRT entry points main or 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.


Edited by Mārtiņš Možeiko on
Replying to longtran2904 (#29787)

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?


Replying to mmozeiko (#29788)