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:

EnvironmentMeans__STDC_HOSTED__
Hosteda full OS with a complete C library — files, threads, <stdio.h>1
Freestandingbare metal, kernels, early boot — no OS, maybe no libc at all0

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:

HeaderProvidesWhy it must be the compiler's
<stddef.h>size_t, ptrdiff_t, NULL, offsetof, wchar_tThese are compiler properties; offsetof is __builtin_offsetof
<stdarg.h>va_list, va_start, va_arg, va_endExpands 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_MINDerived 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, falseMaps to the built-in _Bool
<stdalign.h>alignas, alignofMaps 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.h does not exist at releases/gcc-13.3.0 and does at releases/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 in gcc/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:

DirectoryContains
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.

fixincludes/README:5-8

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:

MacroDirectory
GCC_INCLUDE_DIR$libsubdir/include — GCC's own
FIXED_INCLUDE_DIR$libsubdir/include-fixed
TOOL_INCLUDE_DIR$tooldir/includegcc/Makefile.in:2613
NATIVE_SYSTEM_HEADER_DIR/usr/include, sysrooted
GPLUSPLUS_INCLUDE_DIRthe 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. The syslimits.h hop 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 if in Listing 9-5, which is the precise statement of why --sysroot cannot 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.