Where <stdint.h> actually comes from
Source references in this chapter are to GCC 15.2.0 (releases/gcc-15.2.0).
You are building a sysroot for a cross target. You have copied in glibc's headers,
you have stdio.h and pthread.h and fcntl.h, and you check for stdint.h and
it is not there. You go looking for the glibc build step that produces it, and
there isn't one.
<stdint.h> is not the C library's. It is the compiler's, and it lives under
GCC's own prefix, in $prefix/lib/gcc/<target>/<version>/include/. So do
<stddef.h>, <limits.h>, <stdarg.h> and about a dozen others.
$ gcc -print-file-name=include
$ ls "$(gcc -print-file-name=include)"
This chapter is the compile-time twin of Chapter 1.8: one chapter for what reaches the linker, one for where headers come from. The sysroot rule from Chapter 1.2 — the sysroot belongs to the C library, the prefix belongs to GCC — applies here without exception, and this is the case where it surprises people most.
Why the compiler has to provide them
The C standard defines two kinds of execution environment:
| Environment | Means | __STDC_HOSTED__ |
|---|---|---|
| Hosted | a full OS with a complete C library — files, threads, <stdio.h> | 1 |
| Freestanding | bare metal, kernels, early boot — no OS, maybe no libc at all | 0 |
A conforming implementation must work in a freestanding environment, where by definition there is no C library to supply headers. So the standard requires the compiler itself to provide a small core set — the freestanding headers.
Which is why these headers exist at all as a separate category: they must be available when nothing else is.
The sources are in the tree, under gcc/ginclude/, and the list of
which ones get installed is a makefile variable:
USER_H = $(srcdir)/ginclude/float.h \
$(srcdir)/ginclude/iso646.h \
$(srcdir)/ginclude/stdarg.h \
$(srcdir)/ginclude/stdbool.h \
$(srcdir)/ginclude/stddef.h \
$(srcdir)/ginclude/varargs.h \
$(srcdir)/ginclude/stdfix.h \
$(srcdir)/ginclude/stdnoreturn.h \
$(srcdir)/ginclude/stdalign.h \
$(srcdir)/ginclude/stdatomic.h \
$(srcdir)/ginclude/stdckdint.h \
$(EXTRA_HEADERS)
Listing 9-1: the headers GCC installs (gcc/Makefile.in:473-484).
Note $(EXTRA_HEADERS) on the last line: that is where a target adds its own, which
is how <arm_neon.h> and <immintrin.h> get there.
What each of them contributes, and why a libc could not:
| Header | Provides | Why it must be the compiler's |
|---|---|---|
<stddef.h> | size_t, ptrdiff_t, NULL, offsetof, wchar_t | These are compiler properties; offsetof is __builtin_offsetof |
<stdarg.h> | va_list, va_start, va_arg, va_end | Expands to __builtin_va_*; the varargs ABI is generated code, not a library |
<stdint.h> | int32_t, uint64_t, INTPTR_MAX, … | Must match the target data model exactly — ILP32, LP64, LLP64 |
<limits.h> | INT_MAX, CHAR_BIT, LONG_MIN | Derived from the target's integer widths |
<float.h> | FLT_MANT_DIG, DBL_MAX, … | Derived from the target's floating-point formats |
<stdbool.h> | bool, true, false | Maps to the built-in _Bool |
<stdalign.h> | alignas, alignof | Maps to _Alignas / _Alignof |
<stdatomic.h> | atomic_int, atomic_load, … | Atomics are codegen plus libatomic, not libc |
<iso646.h> | and, or, not, … | Pure macros; nothing for a libc to contribute |
| arch intrinsics | <arm_neon.h>, <immintrin.h>, … | One-to-one mappings onto instructions the back end knows |
The pattern is consistent: every one of these encodes something only the compiler knows — type widths, alignment rules, calling conventions, instruction sets. A libc physically cannot get them right for an arbitrary compiler.
If you are on GCC 13 or earlier,
<stdckdint.h>is not there. C23's checked integer arithmetic header arrived in GCC 14;gcc/ginclude/stdckdint.hdoes not exist atreleases/gcc-13.3.0and does atreleases/gcc-14.3.0.While you are checking that list against your own compiler, note that
<stdbit.h>is not one of GCC's headers, despite being C23 and despite what you may read. At the pin it exists only as a libstdc++ C-compatibility header (libstdc++-v3/include/c_compatibility/stdbit.h), not ingcc/ginclude/.
In C23, bool, alignas, alignof and noreturn became keywords, so
<stdbool.h>, <stdalign.h> and <stdnoreturn.h> are now largely vestigial. They
still exist, and still come from GCC. As always: run ls on your own
-print-file-name=include rather than trusting any list, including Listing 9-1.
include/ versus include-fixed/
Both sit in libsubdir, and both come before the sysroot in the search order:
| Directory | Contains |
|---|---|
include/ | GCC's own headers — everything above |
include-fixed/ | patched copies of the system's headers |
include-fixed/ is the output of fixincludes, a build-time pass that copies
system headers found at build time and mechanically repairs constructs GCC dislikes
— ancient K&R declarations, macros that break under a modern preprocessor. The
machinery lives in fixincludes/, and its README describes the
workflow, which is a rule database compiled by AutoGen:
If you are having some problem with a system header that is either broken by the manufacturer, or is broken by the fixinclude process, then you will need to alter or add information to the include fix definitions file,
inclhack.def.
Two consequences worth knowing.
include-fixed/ is a snapshot taken when GCC was built. If your sysroot's
headers were updated afterwards, the fixed copies can be stale, and you will be
compiling against a patched version of a header that no longer exists.
On a modern glibc or musl target it is usually near-empty. The machinery is mostly historical; on older or unusual targets it can matter a great deal.
The driver knows about it as a separate directory in the compiled-in list, and the entry has its own multilib behaviour:
#ifdef FIXED_INCLUDE_DIR
/* This is the dir for fixincludes. */
#ifndef SYSROOT_HEADERS_SUFFIX_SPEC
{ FIXED_INCLUDE_DIR, "GCC", 0, 0, 0, 2 },
#endif
{ FIXED_INCLUDE_DIR, "GCC", 0, 0, 0,
/* A multilib suffix needs adding if different multilibs use
different headers. */
Listing 9-2: the include-fixed entries
(gcc/cppdefault.cc:75-83).
The trailing 2 is the multilib field, meaning "append the multiarch path" —
the same three-way distinction from Chapter 1.7, reaching into header search.
The search order, and why GCC's copy wins
$ gcc -v -E - < /dev/null
The <...> section looks like this, in this order:
#include <...> search starts here:
/opt/gcc-arm/lib/gcc/arm-none-eabi/15.2.0/include ← GCC's own
/opt/gcc-arm/lib/gcc/arm-none-eabi/15.2.0/include-fixed ← fixincludes output
/opt/gcc-arm/arm-none-eabi/include ← tooldir
<sysroot>/usr/include ← the C library's
End of search list.
The order comes from one compiled-in array, cpp_include_defaults
(gcc/cppdefault.cc:38), and it is literally an array — the search
order is the declaration order, from the C++ header directories at the top down to
a final entry the source comments on with admirable brevity:
#ifdef NATIVE_SYSTEM_HEADER_DIR
/* /usr/include comes dead last. */
{ NATIVE_SYSTEM_HEADER_DIR, NATIVE_SYSTEM_HEADER_COMPONENT, 0, 0, 1, 2 },
{ NATIVE_SYSTEM_HEADER_DIR, NATIVE_SYSTEM_HEADER_COMPONENT, 0, 0, 1, 0 },
#endif
Listing 9-3: the last entries in the list
(gcc/cppdefault.cc:98-101).
The macros that populate the interesting entries are #defined into the compiler
by its makefile:
| Macro | Directory |
|---|---|
GCC_INCLUDE_DIR | $libsubdir/include — GCC's own |
FIXED_INCLUDE_DIR | $libsubdir/include-fixed |
TOOL_INCLUDE_DIR | $tooldir/include — gcc/Makefile.in:2613 |
NATIVE_SYSTEM_HEADER_DIR | /usr/include, sysrooted |
GPLUSPLUS_INCLUDE_DIR | the C++ headers — Chapter 1.12 |
TOOL_INCLUDE_DIR is a survivor of the pre-sysroot --with-headers mechanism from
Chapter 1.2. It is still searched on a pure-sysroot toolchain that never asked for
it.
GCC's directories come first, and that ordering is the whole basis of the next
section: when you write #include <limits.h>, you get GCC's copy, and GCC's copy
then decides whether to bring libc's in as well.
The sysroot decision is per entry
Each entry in the array carries its own flag saying whether the sysroot applies:
const char add_sysroot; /* FNAME should be prefixed by
cpp_SYSROOT. */
Listing 9-4: the field (gcc/cppdefault.h:48-49).
and the consuming code branches on it:
/* Should this directory start with the sysroot? */
if (sysroot && p->add_sysroot)
{
...
str = concat (sysroot_no_trailing_dir_separator, p->fname, NULL);
...
}
else if (!p->add_sysroot && relocated
&& !filename_ncmp (p->fname, cpp_PREFIX, cpp_PREFIX_len))
{
Listing 9-5: sysroot or relocate, per directory
(gcc/incpath.cc:181-193).
Read the else if. GCC's own header directories take that branch: not
sysrooted, but relocatable — if the path starts with the configure-time prefix and
the compiler has been relocated, the run-time prefix is substituted instead. That is
the mechanism behind "they are GCC's, not libc's", stated in code, and it is the
consumer of the set_std_prefix call you saw in Listing 6-2.
This is exactly parallel to add_prefix versus add_sysrooted_prefix for
libraries, and it is a completely separate implementation. Header search and library
search are two mechanisms that happen to be relocated by the same prefix.
#include_next, the wrapper trick
Some of these headers are not replacements but wrappers. They need to add compiler knowledge and let the C library contribute its own definitions. The GNU extension that makes that possible:
#include <foo.h>searches the path from the beginning.#include_next <foo.h>searches from just after the directory the current file was found in.
So a header can include the next file of the same name further down the search path — itself, one layer down — without knowing where that layer is and without risk of including itself.
<stdint.h> is the clearest case, because the whole hosted/freestanding split is
right there in the shipped source:
#ifndef _GCC_WRAP_STDINT_H
#if __STDC_HOSTED__
...
# include_next <stdint.h>
...
#else
# include "stdint-gcc.h"
#endif
#define _GCC_WRAP_STDINT_H
#endif
Listing 9-6: stdint-wrap.h (gcc/ginclude/stdint-wrap.h, abridged
— the omitted lines define __STDC_LIMIT_MACROS for C++11 and silence a -Wpedantic
warning about include_next).
In a hosted build GCC's copy defers to the system's. In a freestanding
build there may be no system copy at all, so it falls back to a self-contained
version GCC ships alongside, stdint-gcc.h. That is precisely the freestanding
requirement being honoured, in eight lines.
Which of the two gets installed as include/stdint.h is a build-time decision:
if [ $(USE_GCC_STDINT) = wrap ]; then \
cp $(srcdir)/ginclude/stdint-wrap.h include/stdint.h; \
...
elif [ $(USE_GCC_STDINT) = provide ]; then \
cp $(T_STDINT_GCC_H) include/stdint.h; \
Listing 9-7: wrap or provide (gcc/Makefile.in:3514-3524;
USE_GCC_STDINT comes from configure, :816).
<limits.h> is assembled, not shipped
<limits.h> works the same way but is stranger: there is no single
gcc/limits.h in the tree to read, because the installed file is concatenated at
build time from three fragments:
if $(LIMITS_H_TEST) ; then \
cat $(srcdir)/limitx.h $(T_GLIMITS_H) $(srcdir)/limity.h > tmp-xlimits.h; \
else \
cat $(T_GLIMITS_H) > tmp-xlimits.h; \
fi; \
Listing 9-8: building limits.h (gcc/Makefile.in:3528-3532).
and the test is one line:
LIMITS_H_TEST = [ -f $(BUILD_SYSTEM_HEADER_DIR)/limits.h ]
Listing 9-9: (gcc/Makefile.in:591).
So: if the system has its own limits.h, you get the wrapper form — prologue,
GCC's own limits, epilogue. If it does not, you get GCC's limits alone with no
#include_next at all. The fragments say so themselves:
/* This administrivia gets added to the beginning of limits.h
if the system has its own version of limits.h. */
Listing 9-10: (gcc/limitx.h:24-25; the matching
gcc/limity.h:1-2 says "the end of limits.h".)
There is one more hop, and it is a nice piece of indirection.
limitx.h does not use #include_next directly:
/* Use "..." so that we find syslimits.h only in this same directory. */
#include "syslimits.h"
Listing 9-11: (gcc/limitx.h:33-34).
and syslimits.h is a copy of gsyslimits.h
(gcc/Makefile.in:3540) whose entire body is:
#define _GCC_NEXT_LIMITS_H /* tell gcc's limits.h to recurse */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic" // include_next
#include_next <limits.h>
#pragma GCC diagnostic pop
#undef _GCC_NEXT_LIMITS_H
Listing 9-12: gsyslimits.h (gcc/gsyslimits.h:6-11).
The reason for the extra file is that include-fixed/ may contain a patched
syslimits.h instead — that is one of fixincludes' jobs — so the hop gives the
build a place to intervene without touching limits.h itself.
Watching it happen
$ echo '#include <limits.h>' | gcc -H -E -x c - > /dev/null
-H prints the include tree, one dot of indentation per nesting level. You will
see GCC's limits.h at depth one, syslimits.h below it, and the sysroot's
limits.h below that — the hop, visible.
Which headers are wrappers and which are standalone varies by target, so run -H
rather than trusting a table. Roughly: <stddef.h>, <stdarg.h>, <stdbool.h>,
<iso646.h> and the arch intrinsics are standalone; <limits.h> and <stdint.h>
are wrappers when the system has its own.
Gotchas
Do not go looking in the sysroot for stdint.h. It is not there and it is not
supposed to be. gcc -print-file-name=include is where to look.
Copying a sysroot does not copy these. A sysroot is complete for libc and useless on its own; the compiler's own headers travel with the compiler.
They are version-locked. The path contains <version> (Listing 2-2), so two
GCC releases have separate copies. Mixing GCC 12's headers with GCC 15's cc1 is a
broken toolchain, not merely an unwise one.
-nostdinc removes these too, not only the system directories
(gcc/c-family/c.opt:2523). On bare metal you almost always want
them back:
$ gcc -nostdinc -isystem "$(gcc -print-file-name=include)" …
-ffreestanding changes nothing about header search. This is the pairing people
most reliably get backwards. Its whole help text is:
ffreestanding
C ObjC C++ ObjC++
Do not assume that standard C libraries and "main" exist.
Listing 9-13: (gcc/c-family/c.opt:1986-1988).
It changes what the compiler may assume — it stops printf("x\n") becoming
puts("x"), relaxes the requirements on main, and sets __STDC_HOSTED__ to 0.
It moves no directory. Chapter 1.10 puts it side by side with -nostdlib and
-nostdinc, which are the two options it is most often confused with.
$ gcc -dM -E - < /dev/null | grep __STDC_HOSTED__
$ gcc -ffreestanding -dM -E - < /dev/null | grep __STDC_HOSTED__
-B moves them. A -B<dir> adds <dir>/<target>/<version>/include and
.../include-fixed to the header search, because -B feeds include_prefixes
(Listing 5-2) and %I expands that list:
info.option = "-isystem";
info.append = "include";
...
for_each_path (&include_prefixes, false, info.append_len,
spec_path, &info);
Listing 9-14: %I emitting GCC's header directories
(gcc/gcc.cc:6604-6612).
That is why -B can change which <stdint.h> you compile against. It is also
exactly why it cannot change which <vector> you get: only GCC's own two
directories are in include_prefixes.
include-fixed/ can be stale relative to an updated sysroot.
Checking
$ gcc -print-file-name=include # GCC's own header dir
$ ls "$(gcc -print-file-name=include)" # what it actually ships
$ gcc -v -E - < /dev/null # the full search list
$ echo '#include <limits.h>' | gcc -H -E -x c - >/dev/null # the include_next chain
$ echo '#include <stdint.h>' | gcc -E -x c - | grep -m1 stdint
The last one tells you which file a specific include actually resolved to, which is occasionally the only question you have.
Documentation coverage
-nostdinc, -ffreestanding and -I/-isystem/-idirafter are documented under
Directory Options (gcc/doc/invoke.texi:19484) and in the preprocessor
manual, which also covers #include_next and -H.
What is not documented:
- That
<limits.h>is assembled from three fragments at build time, and that which form you get depends on a filesystem test. Thesyslimits.hhop in particular exists only in the source. - That the sysroot decision is per include-directory, carried in a struct field, and therefore not something a flag can change after the fact.
- That GCC's own header directories are relocated rather than sysrooted — the
else ifin Listing 9-5, which is the precise statement of why--sysrootcannot move them.
That closes Part I's account of search paths. The remaining three chapters of Part I are about deliberately taking pieces of the runtime away, and about the one mechanism in the driver that rewrites your command line behind your back.
All source references in this chapter are to GCC 15.2.0
(releases/gcc-15.2.0). Line numbers in other releases will differ; the
surrounding code rarely does. Where behaviour itself changed across a major
version, it is flagged inline.