Architecture¶
Overview¶
abicheck is a Python CLI tool that compares two versions of a C/C++ shared library
to detect ABI and API incompatibilities. Its core design idea is to reason over
five independent sources of information about a library — the binary, its
debug symbols, its public headers, its build-system data, and (optionally) its
sources — instead of relying on a single data source. Each source is an additive
evidence layer (L0–L4); feeding more layers both finds breaks the
weaker layers are blind to and suppresses false positives they would raise. See
Evidence layers: the five sources below for
the model, and Evidence & Detectability for the
conceptual companion.
abicheck supports Linux (ELF), Windows (PE/COFF), and macOS (Mach-O), each with implemented binary-metadata, header-AST, and debug-info cross-check support — see the platform support matrix for the per-platform tool/format breakdown and per-toolchain CI-validation maturity (implemented capability and CI-proven maturity are not the same thing, especially on Windows).
Analysis pipeline¶
The CLI dumps each input into a normalized snapshot, enriches it with header AST and debug-info layers, then diffs the two snapshots to produce a verdict:
flowchart TD
CLI["abicheck CLI<br/>(dump · compare · compat check/dump)"]
FMT{"Format detection<br/>(ELF / PE / Mach-O)"}
ELF["ELF<br/>pyelftools"]
PE["PE/COFF<br/>pefile"]
MACHO["Mach-O<br/>macholib"]
SNAP["L0 — Binary metadata<br/>Snapshot (JSON model)"]
AST["L2 — Header AST<br/>castxml (all platforms)"]
DBG["L1 — Debug-info cross-check<br/>DWARF (Linux, macOS) · PDB (Windows)"]
CHK["Checker → Changes → Verdict"]
CLI --> FMT
FMT --> ELF
FMT --> PE
FMT --> MACHO
ELF --> SNAP
PE --> SNAP
MACHO --> SNAP
SNAP --> AST
AST --> DBG
DBG --> CHK
The analysis layers are independent and additive — each catches changes the others miss, and the checker reconciles them into a single verdict. The artifact layers (L0/L1/L2) are described in detail below; the build/source layers (L3/L4, plus the optional L5 reachability graph) are covered in Build & Source Packs.
Evidence layers: the five sources¶
abicheck's accuracy comes from treating compatibility analysis as a question of
evidence: the more independent sources of information you give it about a
library (binary, debug symbols, headers, build data, sources — abicheck
additionally derives a sixth, the L5 reachability graph), the more it can
prove and the fewer false positives it raises. Artifact-backed L0/L1/L2
evidence is authoritative for the shipped-ABI verdict; build/source
L3/L4/L5 evidence may explain, localize, or add confidence to a finding,
but never silently deletes an artifact-proven break (the authority rule,
ADR-028). See Evidence & Detectability for
the full model (all six layers, the --depth dial, and worked examples).
Artifact layers in detail¶
Layer L0: Binary metadata¶
Reads native binary metadata using format-specific parsers:
ELF (Linux, via pyelftools):
- Exported symbols (functions, variables) from .dynsym
- SONAME, symbol binding (GLOBAL, WEAK, LOCAL), symbol versioning
- NEEDED dependencies, visibility attributes
PE/COFF (Windows, via pefile):
- Exported functions and ordinals from the export table
- Imported DLLs and functions from the import table
- Machine type, characteristics, DLL characteristics
- File and product version from VS_FIXEDFILEINFO resource
Mach-O (macOS, via macholib):
- Exported symbols from the symbol table (including weak definitions)
- Install name (LC_ID_DYLIB — equivalent of ELF SONAME)
- Dependent libraries (LC_LOAD_DYLIB — equivalent of ELF DT_NEEDED)
- Re-exported libraries (LC_REEXPORT_DYLIB)
- Current and compatibility versions, minimum OS version
- Fat/universal binary support (automatic architecture selection)
Layer L2: Header AST (castxml / Clang) — all platforms¶
Parses C/C++ headers through a selectable frontend — --ast-frontend
auto|castxml|clang|hybrid (or ABICHECK_AST_FRONTEND). auto always
resolves to castxml first and never silently switches producer — not
even on a host with no castxml at all — unless --allow-ast-frontend-fallback
(or ABICHECK_ALLOW_AST_FALLBACK=1) is also given; with that opt-in, a
recognized castxml toolchain-version mismatch or direct-include-guard
failure falls back to clang -ast-dump=json (ADR-003, G16). Without the
opt-in, and on a clang-only host, run explicitly with --ast-frontend clang
instead of relying on auto. hybrid (G28 Phase 3) runs both and merges
them. The rest of this section describes the castxml backend. The clang
backend exposes the same
declaration surface (signatures, classes/bases, enums, typedefs, access,
noexcept, templates) but is a syntactic AST: it does not compute record
layout, so size_bits/offset_bits/vtable slots stay unset and the layout
detectors skip an unknown-vs-unknown comparison — DWARF (L1) remains the layout
authority on a clang-only host. With that caveat, the header AST extracts:
- Function signatures (parameters, return types)
- Class/struct definitions; layout when backed by castxml or DWARF evidence
- Virtual method tables (vtable slot ordering) when backed by castxml or DWARF evidence
- Enum values and member names
- Typedefs and template instantiations
noexceptspecifications- Access levels (public, protected, private)
castxml is a cross-platform tool maintained by Kitware (available via conda-forge,
system packages, or direct download for Linux, Windows, and macOS). It is the primary
source for type-level analysis, catching changes invisible to debug-info-only tools:
noexcept, static qualifier, const qualifier, access level changes.
Compiler support: castxml uses an internal Clang compiler for parsing but
emulates the preprocessor defines, include paths, and target platform of an external
compiler via --castxml-cc-<id> <compiler-binary>. At invocation castxml calls the
external compiler to discover its built-in defines (e.g. __GNUC__, __GNUC_MINOR__,
_MSC_VER) and default include search paths, then injects those into its internal Clang
so the resulting AST matches what the external compiler would produce.
| Compiler ID | Compiler | Typical platforms |
|---|---|---|
gnu |
GCC / g++ | Linux, macOS, Windows (MinGW) |
gnu-c |
GCC / gcc (C mode) | Linux, macOS, Windows (MinGW) |
msvc |
Microsoft Visual C++ (cl) | Windows |
msvc-c |
Microsoft Visual C (cl, C mode) | Windows |
Auto-detection logic (see dumper.py:_castxml_dump()): abicheck extracts the
filename from the compiler binary path (via Path(cc_bin).name), lower-cases it, and
checks whether it is cl or cl.exe. If so, it passes --castxml-cc-msvc; otherwise it
passes --castxml-cc-gnu. The comparison is case-insensitive so CL.EXE, Cl.exe, etc.
are all correctly detected on Windows.
Compiler resolution priority (highest to lowest):
--gcc-path /path/to/compiler— explicit path override, used as-is--gcc-prefix <prefix>— cross-toolchain prefix; abicheck appendsg++(C++ mode) orgcc(C mode) automatically- Default mapping — logical name (
c++→g++,cc→gcc,clang++→clang++)
Scanning with a specific compiler version: use --gcc-path to point at the exact
binary. castxml queries that binary for its version-specific predefined macros and include
paths, so the parse reflects exactly what that compiler version defines:
abicheck dump libfoo.so -H foo.h --gcc-path /usr/bin/g++-9 # GCC 9
abicheck dump libfoo.so -H foo.h --gcc-path /usr/bin/g++-12 # GCC 12
Limitations — non-C/C++ languages and compiler extensions:
castxml can only parse C and C++ because its internal engine is Clang. It cannot parse
Fortran, Rust, Ada, or other languages — there is no --castxml-cc-fortran equivalent.
For compilers that add language extensions beyond standard C/C++ (e.g. Intel DPC++/SYCL
__attribute__((sycl_kernel)), CUDA __global__, OpenACC pragmas), castxml can query
the external compiler's preprocessor state but its internal Clang will reject
extension-specific syntax during parsing. To scan such headers you would need either a
CastXML build linked against the matching Clang fork (e.g. Intel's DPC++ Clang for SYCL)
or a different AST extraction tool that uses that compiler's libclang directly.
Layer L1: Debug info cross-check (optional)¶
When debug info is available in the binary:
DWARF (Linux .so, macOS .dylib — via pyelftools):
- Cross-validates struct/class sizes against header-computed sizes
- Verifies member offsets (catches #pragma pack or -march-specific alignment differences)
- Checks vtable slot offsets
- Detects calling convention and frame register changes
PDB (Windows .dll — via built-in PDB parser):
- Extracts struct/class/union sizes and field layouts from TPI stream
- Extracts enum underlying types and member values
- Detects calling convention changes (__cdecl, __stdcall, __fastcall,
__thiscall, __vectorcall) from LF_PROCEDURE / LF_MFUNCTION records
- Extracts MSVC toolchain info (version, machine type, ABI flags) from DBI stream
- Auto-discovers PDB files from PE debug directory; use --pdb-path to override
Debug artifact resolution (via debug_resolver module):
When debug info is not embedded, abicheck searches a configurable resolver
chain: split DWARF (.dwo/.dwp), build-id trees, path mirrors, dSYM bundles,
PDB files, and optionally debuginfod servers. Use --debug-root to point at
separate debug file directories, or --debuginfod for network-based resolution.
Layers L3 / L4: Build & source evidence (optional)¶
The build (L3) and source (L4) layers are post-build, opt-in, and never authoritative on their own — abicheck reads existing build outputs and build-system query interfaces; it does not rebuild your project. They are collected into a content-addressed build/source pack and attached to a snapshot:
- L3 — build context (
build_context.py, ADR-029): parses acompile_commands.json(-p build/) or a CMake/Ninja/Bazel/Make graph to recover the exact ABI-relevant flags and toolchain the library was built with. Diffs emit context/risk kinds likeabi_relevant_build_flag_changed,toolchain_version_changed, andlink_export_policy_changed. - L4 — source ABI replay (ADR-030): parses selected TUs and public headers
under their real per-TU build context and links the result against the
exported surface, catching
public_macro_value_changed,default_argument_changed,constexpr_value_changed, and the uninstantiated templates that no artifact carries.
Both are described in full in Build & Source Packs. Per the
authority rule, every L3/L4 finding defaults to API_BREAK or risk and carries
an explicit evidence-tier boundary so it is never read as a proven shipped-ABI
break.
Key modules¶
For the module-by-module map — every source file grouped by area (data model, input resolution, binary/debug metadata, core diffing, policy, post-processing, workflows, reporting, compatibility) — see the Codebase Overview, which is the contributor-facing source of truth for the package layout.
Policy model¶
Policies control how detected changes are classified (BREAKING, API_BREAK, COMPATIBLE).
Built-in profiles:
| Profile | Behavior |
|---|---|
strict_abi (default) |
Every ABI change at maximum severity |
sdk_vendor |
Source-only changes downgraded to COMPATIBLE |
plugin_abi |
Calling-convention changes downgraded to COMPATIBLE |
Custom policies: YAML files with per-kind break|warn|ignore overrides.
Source of truth: BREAKING_KINDS, API_BREAK_KINDS, COMPATIBLE_KINDS, and RISK_KINDS sets in checker_policy.py.
Verdict system¶
| Verdict | Exit code | Meaning |
|---|---|---|
NO_CHANGE |
0 | Identical snapshots |
COMPATIBLE |
0 | Safe changes (new symbols, weak binding) |
COMPATIBLE_WITH_RISK |
0 | Binary-compatible but deployment risk present |
API_BREAK |
2 | Source-level break, binary-safe (rename, access change) |
BREAKING |
4 | Binary ABI break — old binaries will fail |
Error model¶
Public exceptions are defined in abicheck/errors.py. Tool errors produce exit code 1.