← BackJan 5, 2026

Eliminating CRT Symbols in LLVM: Building a Custom Runtime on Windows

When creating a domain‑specific language that needs its own runtime, the Windows CRT can unexpectedly inject symbols such as _fltused into a library build. This article explains why the CRT appears, how to disable the default library linkage, and practical compiler/linker flags that allow an LLVM‑based toolchain to generate a clean .lib or .dll containing only the user‑defined symbols.

### The Problem A language developer using LLVM’s `llc`/`clang` toolchain found that every attempt to compile a stub library into a `.lib` file pulled in the Windows CRT, specifically the `_fltused` symbol. Even after defining `_fltused` in the source IR, the linker raised multiple‑definition errors because the CRT library supplied its own version. The goal was to create a lightweight runtime without the standard C runtime, but all usual attempts—passing `-nodefaultlibs`, `-nostdlib`, and various MinGW/Clang flags—seemed to be ignored. Understanding why the CRT gets linked requires a look at Windows’ ABI and the default behaviour of the LLVM linker (`lld`). ### Why the CRT Appears 1. **Floating‑point helper** – `_fltused` is a symbol that the MSVC run‑time uses to indicate that floating‑point support is required. Any function that references floating‑point built‑ins or any `libm` function implicitly pulls in this symbol. 2. **Implicit default libs** – The Windows compiler drivers (clang.exe, clang++.exe, and the GNU `gcc`/`g++` tools that ship with MinGW) automatically request the default libraries: `libcmt.lib`, `msvcrt.lib`, `kernel32.lib`, etc. These libraries contain `_fltused`, a variety of system APIs, and the standard C initialisation code. 3. **LLVM’s `lld` default behaviour** – `lld` is designed to mirror the behaviour of the system linker. Unless explicitly told otherwise, it will always link the default Windows libraries. ### Disabling CRT Linkage with LLVM The solution is to invoke the compiler and linker with the right options so that none of the default CRT symbols are pulled in. For a purely LLVM‑based build the following steps work reliably under Windows: 1. **Compile the LLVM IR to an object file** – `llc -march=host -filetype=obj foo.ll -o foo.o` 2. **Assemble the object file into a static library** – `llvm-ar rcs libfoo.a foo.o` * `llvm-ar` is the counterpart of `ar` for ARM cross‑compilation. 3. **Linker flags** – When you build an executable that depends on `libfoo.a`, disable the system libraries: ```bash clang++ -nostdlib -nodefaultlibs \ -Wl,--entry=_main \ -Wl,--subsystem,windows \ -o main.exe foo.o ``` * `-nostdlib` tells clang not to search for the default libraries. * `-nodefaultlibs` is honoured by `lld` and prevents linking `kernel32.lib`, `msvcrt.lib`, etc. * `-Wl,--entry=_main` provides a custom entry point; replace `_main` with whatever you need. If a dynamic library (`.dll`) is desired, use: ```bash clang++ -shared -nostdlib -nodefaultlibs \ -Wl,--out-implib,libfoo.lib \ -o foo.dll foo.o ``` ### Handling the Special Case of `_fltused` Sometimes a library still ends up pulling in `_fltused` even though you explicitly disabled the CRT. A robust workaround is to provide a *strong* definition of the symbol that the linker will prefer: ```c extern "C" __declspec(dllexport) unsigned _fltused = 0x0; ``` Place that in a separate `.c` or `.cpp` file, compile it with `-c`, and add the resulting module to the static library **after** adding your own object files: ```bash llvm-ar rcs libfoo.a foo.o fltused.o ``` Because the symbol is now defined with strong linkage, the linker will ignore the weak implementation in any default library that might still be present. ### What If You Need Standard Library Functions? If the runtime you are building requires a subset of the CRT (e.g., `printf`, `malloc`), you can manually link only those parts: ```bash clang++ -nostdlib -nodefaultlibs \ -Wl,--whole-archive $(clang -print-file-name=libcmt.a) \ -Wl,--no-whole-archive \ -o main.exe foo.o ``` Here `--whole-archive` forces the entire library to be linked; you can then strip unused symbols with tools like `llvm-objcopy --keep-symbol`. ### Summary * The Windows CRT is automatically linked by default, bringing in symbols like `_fltused`. * Use `-nostdlib` / `-nodefaultlibs` and `lld`’s linker switches (`--entry`, `--subsystem`) to avoid the CRT. * When necessary, provide a strong definition of `_fltused` to override any accidental inclusion. * For minimal runtime support, manually link only the needed CRT components. By combining these flags with a clean build pipeline, developers can build custom runtimes in LLVM without the overhead or conflicts of the standard Windows C runtime.