Limitations & Known Boundaries¶
abicheck is designed to catch real ABI and API breaks with high accuracy, but has specific
limitations you should understand before relying on it in production.
Conceptual companion. This page is the practical boundary list. For the why — which evidence (symbols, debug info, headers, source, runtime, bundle) lets any tool see a given change at all, and what no artifact comparison can prove — see Evidence & Detectability.
Platform support matrix¶
| Platform | Binary format | Binary metadata | Header AST (castxml) | Debug info cross-check |
|---|---|---|---|---|
| Linux | ELF (.so) |
Yes (pyelftools) | Yes (GCC, Clang) | Yes (DWARF) |
| Windows | PE/COFF (.dll) |
Yes (pefile) | Yes (MSVC, MinGW) | Yes (PDB) |
| macOS | Mach-O (.dylib) |
Yes (macholib) | Yes (Clang, GCC) | Yes (DWARF) |
Header AST analysis (via castxml) is available on all platforms. castxml is maintained by Kitware and available via conda-forge, Homebrew, apt, or direct download.
Debug info cross-check uses DWARF (Linux and macOS) and PDB (Windows). PDB
support extracts struct/class/union layouts, enum types, calling conventions, and
toolchain info from PDB files produced by MSVC (/Zi flag). Use --pdb-path to
specify the PDB file location if automatic discovery fails.
Windows toolchain distinction¶
Windows support depends on the compiler/toolchain used for headers and binary production:
Two distinct paths have different maturity — don't read "MSVC" as a single status:
| Toolchain / path | Status | Notes |
|---|---|---|
| MinGW (GCC) | Experimental | Covered by current CI smoke/integration jobs. |
| MSVC PE/COFF + PDB — binary & verdicts | Parsers unit-tested; MSVC e2e non-blocking | The PE/PDB parsers have (blocking) unit tests. The windows-msvc end-to-end lane asserts MSVC+PDB verdicts (PDB layout depth best-effort) but runs continue-on-error (informational, does not block CI) until proven stable — treat MSVC verdicts as experimental. |
MSVC castxml + cl.exe — native header/type analysis |
Untested in CI | Expected to work in many cases, but this native header path is not yet validated end-to-end. |
Tracked ABICC compatibility issues for this area: #9, #50, #56, #121. For detailed matrix + per-issue notes, see Platform Support.
Header / Binary Mismatch Risk¶
The most important limitation. abicheck uses castxml to parse headers and
compares the result against the compiled .so. If the headers passed to analysis
don't exactly match what was compiled, results will be unreliable.
This happens when:
- You pass generic system headers but the library was compiled with custom #define flags
- Preprocessor macros change the public API surface (#ifdef FEATURE_X)
- Third-party dependency headers differ between versions
- Platform-specific code paths (#ifdef __linux__) differ between compile and analysis environments
Mitigation:
- Always use the exact same headers that were used to build the .so
- Pass the build's include roots, dialect, and defines to the header frontend:
abicheck dump libfoo.so -H foo.h -I include/ --gcc-options "-std=c++20 -DFEATURE_X"
(the same flags work on abicheck scan; persist them in a .abicheck.yml
compile: block so every run is reproducible — see
Compile context for header parsing)
- For abicheck compat, use -s (strict mode) to promote COMPATIBLE/API_BREAK to BREAKING:
abicheck compat check -lib foo -old OLD.xml -new NEW.xml -s
(use --strict-mode api to promote only API_BREAK; -s is not available on abicheck compare)
- Cross-check with abicheck compat check (ABICC mode) for independent validation
System-include auto-detection (and what it does not fix)¶
The clang frontend now auto-detects the host C++ standard library the way
castxml always has (it probes g++ -E -v and injects the system include dirs),
so a bare scan -H include/ finds <cstddef> without extra flags. This is
system headers only — it cannot guess your project's own -I roots, -D
feature macros, or the exact -std. Disable it with --nostdinc, an explicit
--sysroot, or ABICHECK_AUTO_SYSTEM_INCLUDES=0.
Scope divergence: missing L2 context can turn internal cleanups into false BREAKING
The header AST (L2) is what tells abicheck which symbols are public. If
the headers cannot be parsed — because the compile context above is missing —
the scan has only the binary, so it treats the export table as the surface and
flags the removal of internal symbols (e.g. macro-guarded detail::/impl::
helpers, or statically-bundled third-party routines) as BREAKING. These are
missing-context artifacts, not real breaks: supply the include roots /
dialect / defines (so L2 parses) and they demote to COMPATIBLE. Always read
the scan's coverage block — if L2 is not_collected, treat any BREAKING on an
impl/detail/internal symbol with suspicion and fix the header context
first.
Stripped Production Binaries¶
Tiers 3 and 4 (DWARF layout + Advanced DWARF) require debug symbols (-g).
Production .so files are typically stripped — in this case:
- Struct field offset changes may be missed (Tier 3 unavailable)
- Calling convention drift, struct packing changes not detected (Tier 4 unavailable)
- Tier 1 (castxml/headers) and Tier 2 (ELF symbols) still run — most critical breaks caught
Mitigation: Use --debug-root to point abicheck at separate debug files
(distro debuginfo packages, build-id trees, or dSYM bundles). abicheck
automatically searches for debug artifacts via a resolver chain. For
Fedora/RHEL, use --debuginfod to fetch debug info by build-id from
debuginfod servers. See the CLI usage guide for
details. For production binaries without debug info, Tier 1+2 analysis covers
the majority of real-world ABI breaks.
Template Instantiation¶
C++ template instantiations with complex type parameters can produce unexpected results:
- Explicit instantiations in .so are analyzed; implicit instantiations in headers are not
- Template specializations may not all be captured
- case17_template_abi in the examples demonstrates a detectable case
Mitigation: Use explicit template instantiation (template class Foo<int>;) for
ABI-sensitive types you want to guarantee are tracked.
COMPATIBLE Does Not Mean "Invisible"¶
COMPATIBLE changes are detected and reported — they are not silent. Examples:
- Adding a new export symbol is COMPATIBLE but grows the library's API surface
(relevant for semver policy: additive changes may still require a minor version bump)
- Enum member addition is COMPATIBLE but can affect exhaustive switch statements
For abicheck compat pipelines, use -s to treat COMPATIBLE as blocking.
For abicheck compare pipelines, enforce via CI exit code logic (treat exit 2 as failure).
compat Mode Verdict Limitations¶
abicheck compat does emit exit code 2 for API_BREAK conditions, but the
report text uses ABICC-style phrasing rather than a bare API_BREAK verdict string.
Source-level-only breaks (e.g. case31_enum_rename, case34_access_level) will
appear as warnings in the compat HTML/text report.
Use abicheck compare --format json for precise machine-readable API_BREAK verdicts.
Inline / Header-Only Code¶
Functions defined entirely in headers (inline, constexpr, template) may not appear
in the .so symbol table. abicheck analyzes the public exported ABI — header-only
changes that don't affect exported symbols will not be detected.
Source-only changes invisible to binary/object analysis¶
Some C++ changes are real source/API breaks that leave no trace in the
compiled object — the two .so files are ABI-identical. Comparing only
binaries (or stripped / DWARF-only builds) reports NO_CHANGE for them. This is
intrinsic to comparing built artifacts, not a bug.
abicheck addresses this with its layered model (see Architecture). Each layer recovers signals the layers below cannot see:
| Tier | Data source | Recovers |
|---|---|---|
elf_only |
symbol table only | symbol add/remove, versioning |
dwarf_aware |
DWARF/PDB (needs -g / /Zi) |
struct layout, field offsets, enum values, calling convention, struct packing |
header_aware |
public headers via castxml | source-level qualifiers — final, access, ref-qualifiers, inline, noexcept, explicit, default-argument values, const/constexpr constant values |
So whether a change is detectable depends on the tier you give abicheck:
| Change | object/DWARF | header (castxml) | source-AST tool |
|---|---|---|---|
Class gains final (case125) |
❌ invisible | ✅ type_became_final |
✅ |
Method access narrowed (case34) |
❌ invisible | ✅ method_access_changed |
✅ |
Ref-qualifier change (& → &&) |
❌ (DWARF has no ref-qual) | ✅ func_ref_qual_changed |
✅ |
Default argument removed/changed (case123, case32) |
❌ invisible | ✅ param_default_value_removed / _changed |
✅ |
const/constexpr constant value changed (case124) |
❌ invisible (internal linkage, no symbol) | ✅ constant_changed |
✅ |
#define macro constant changed |
❌ invisible | ❌ (castxml emits no macros) | ✅ |
Inline/constexpr/template function body change (signature unchanged) |
❌ invisible | ❌ (declaration only; body not modelled) | ✅ |
Uninstantiated template signature (case122) |
❌ invisible | ❌ (castxml omits uninstantiated templates) | ✅ |
The upper rows are recovered by supplying public headers (header mode) — note
that several (default-argument values, const/constexpr constant values) leave
no symbol at all in the binary, so only header analysis can reach them. The
lower three rows are the hard boundary: code that never becomes a symbol and is
not modelled by castxml (#define macros, inline/template bodies,
uninstantiated templates) is invisible to any artifact-based comparison. Only a
pure source-AST tool that diffs the headers directly can observe those; binary
and header analysis are complementary, not substitutes.
Constant extraction is deliberately scoped to the user-provided public headers —
const/constexprvalues pulled in transitively from system or private headers are not reported, so the finding stays a real public-API contract change rather than third-party noise.
Recommendation: feed abicheck .so + debug info + headers for the best result¶
The three tiers are additive, and the maximum-coverage configuration is a single comparison of debug-enabled libraries with their public headers supplied:
# Build (or obtain) BOTH versions with -g, then compare WITH headers:
abicheck compare libfoo_v1.so libfoo_v2.so \
--old-header include/v1/foo.h --new-header include/v2/foo.h
This combination gives you all three tiers at once:
.so+ DWARF (-g//Zi) → ground-truth emitted ABI: struct layout, field offsets, alignment/packing, enum values, calling convention — exactly as the compiler produced them.- public headers (castxml) → source-level API surface the binary cannot carry:
final, access, ref-qualifiers,noexcept/explicit, default-argument values, andconst/constexprconstant values (which have no symbol).
These three artifact tiers are layers L0–L2 of the five-source evidence
model. Two further layers refine the result
without ever overriding an artifact-proven break: L3 build context
(-p build/, the exact ABI-affecting flags) and L4 source/build/source packs
(--old-build-info/--new-build-info, recovering macro/constexpr and
uninstantiated-template facts). They are optional but raise confidence and
localize findings — see Source & Build Data.
Comparing a stripped release binary with no headers gives only elf_only
coverage (symbol add/remove) and will silently miss every layout and
source-level break above. If you ship stripped, build a debug copy purely as an
analysis input and compare that with headers — even though the shipped artifact
stays stripped. (See Stripped Production Binaries
if you can only obtain debug info as separate files.)
Static / import library archives (.a, .lib)¶
abicheck analyses single linkable images — shared libraries (.so,
.dll, .dylib) and individual object files. It does not analyse static
or import library archives (.a on Unix, .lib on Windows). This is a
deliberate non-goal (see Project Goals → Non-goals),
for two reasons:
- A static library has no runtime ABI surface: no
SONAME, no dynamic symbol table, no symbol versioning — the very signals abicheck's verdict semantics are built on. Only object-level symbol/type information would apply, and a link-time API check over the union of members is a different tool with different semantics. - Archives are member containers (
arformat, magic!<arch>\n), not a single image; both.aand MSVC.libshare this format.
Handing a .a/.lib to dump or compare produces a clear, actionable
error rather than a misleading "unknown format" message or a traceback:
'libfoo.a' is a static/import library archive (.a/.lib), which abicheck does
not analyse — it compares single linkable images (shared libraries and
objects). Extract the members (e.g. `ar x lib.a`) and compare the resulting
object files or the shared library built from them instead.
Mitigation: extract the archive members and compare the resulting object files, or compare the shared library built from the same sources:
ar x libfoo-old.a && ar x libfoo-new.a # then compare the .o members
# or, preferred:
abicheck compare libfoo-old.so libfoo-new.so -H include/foo.h
Dependency Limitations & Known Bugs¶
Known issues in third-party dependencies that affect abicheck behavior.
castxml: __has_cpp_attribute not defined on macOS (Xcode 16.4+)¶
Status: Open — upstream castxml issue to be filed.
Affected platforms: macOS with Xcode 16.4+ (Apple Clang headers).
Symptom: When castxml processes a C header that includes <stddef.h>, the
macOS SDK resolves this through the libc++ __config header, which uses the
__has_cpp_attribute preprocessor macro. castxml does not define this macro,
causing parse failures:
.../MacOSX.sdk/usr/include/c++/v1/__config:1009:7: error:
function-like macro '__has_cpp_attribute' is not defined
Multiple lines in __config trigger the same error wherever
__has_cpp_attribute(...) appears in #if / #elif directives.
Root cause: Per the C++ standard, __has_cpp_attribute should be a built-in
macro that evaluates to 0 for unknown attributes. castxml's internal
preprocessor does not predefine it, so the preprocessor treats the bare
identifier as an error rather than defaulting to 0.
Workaround: In castxml-specific shim headers (not general project headers),
replace #include <stddef.h> with typedef __SIZE_TYPE__ size_t; to avoid the
libc++ header chain entirely. __SIZE_TYPE__ is a GCC/Clang built-in that
castxml supports.
Caution: This typedef only supplies
size_t— other<stddef.h>definitions (NULL,ptrdiff_t,offsetof,max_align_t) are not available. Do not use this substitution in normal build headers as it will break compilation that depends on those definitions. Safer alternatives: create an isolated shim header used only by castxml invocations, or provide a minimal custom header that supplies all needed type definitions.
Troubleshooting¶
See troubleshooting.md for a diagnostic decision tree covering common false positives, false negatives, and unexpected verdicts.
ELF-Only Mode and Symbol Filtering¶
When abicheck compare (or abicheck dump) is run without header files — i.e.
directly against .so binaries — the tool operates in ELF-only mode. In this
mode the public ABI surface is inferred entirely from exported ELF symbols (.dynsym),
with no source-level type information available.
Why false positives can occur in ELF-only mode¶
Shared libraries often contain exported symbols that are not part of their intended public ABI:
| Symbol category | Example | Root cause |
|---|---|---|
| GCC / compiler internals | ix86_tune_indices, _ZGVbN2v_sin |
Statically-linked compiler runtime (libgcc, SVML) leaks symbols into .dynsym |
| Transitive C++ stdlib symbols | _ZNSt6thread8_M_startEv, _ZTISt9exception |
Weak-linked libstdc++ / libc++ symbols that appear in .dynsym |
| Private C namespace separators | H5C__flush_marked_entries, MPI__send |
Internal LibPrefix__FunctionName naming convention — globally visible but not public API |
Comparing two versions of a library that differ in which compiler or stdlib they were
built against can trigger hundreds of spurious BREAKING findings (e.g. mpfr 4.2.0→4.2.1
reported 91 false-positive breaks caused by ix86_* symbols).
How abicheck filters these symbols¶
abicheck applies an ABI-relevance filter (_is_abi_relevant_symbol) when parsing
.dynsym in ELF-only mode. Symbols are excluded when they match any of the following:
GCC / compiler-internal prefixes (ix86_, x86_64_, __cpu_model, __cpu_features,
_ZGV*, __svml_*, __libm_sse2_*, __libm_avx_*)
C++ standard-library prefixes (_ZNSt, _ZNKSt, _ZNSt3__1, _ZdlPv, _ZnwSt,
_ZnaSt, _ZdaPv, _ZTVN10__cxxabiv, _ZTI, _ZTS, _ZSt)
Private C double-underscore separator — any non-C++-mangled symbol (i.e. not
starting with _Z) whose name contains __ after the first two characters.
This matches patterns like H5C__flush or MPI__send while leaving system symbols
(which start with __ or _[A-Z]) unaffected.
Limitations of the filter¶
- The filter is heuristic. A library that intentionally exports a symbol matching one of the filtered prefixes (unlikely but possible) will have it silently ignored.
- Non-standard SIMD / math libraries with different naming conventions are not covered; open an issue if you encounter new patterns causing false positives.
- In header mode (when headers are supplied), this filter is not applied — castxml provides accurate type information and the ELF surface is used only for visibility decisions, not for inferring the API surface.
Mitigation for header mode¶
For the most accurate results, always supply public headers:
This eliminates ELF-only mode entirely and removes the need for heuristic filtering.
Header scoping on PE and Mach-O¶
Headers supplied via -H/--header (and the per-side --old-header/--new-header)
are now honored for PE (Windows DLL) and Mach-O (macOS dylib) inputs, not just ELF.
When headers are provided, the export-table surface is scoped to the symbols declared
in those public headers via castxml. This is best-effort:
- If castxml is unavailable, or the headers fail to parse, abicheck emits a warning and falls back to the full export table (the previous behavior).
- For C++ binaries built with MSVC, export names use MSVC mangling while castxml
emits Itanium-mangled names, so declarations may not match the export table. When no
declaration matches, abicheck warns and falls back to the export table.
extern "C"and MinGW-built exports match by plain name and scope correctly.
Reachability-based public-surface filtering (keeping only the symbols and types reachable
from the public API, with an auditable trail of what was filtered and why) is available as
an opt-in mode: pass --scope-public-headers (add --show-filtered to print the audit
ledger) to abicheck compare. Findings about symbols/types not reachable from the
public-header-declared exported API are recorded as filtered rather than reported, while
internal-type leaks are never hidden. Full source-header provenance (distinguishing a
privately-included header from a public one independently of reachability) remains future
work. See ADR-024.