Platform Support¶
abicheck runs on Linux, macOS, and Windows and can analyze binaries from any of these platforms. However, the depth of analysis depends on the host platform, whether the binary carries debug info, and whether headers are available — see "What 'No Headers' Actually Means" below for why those last two are independent axes, not one.
Quick Reference: What Works Where¶
Scanning Linux ELF binaries¶
| Host OS | Symbol diff | Type/param diff | Requires |
|---|---|---|---|
| Linux ✅ | ✅ Full | ✅ Full | castxml, g++/gcc |
| macOS | ✅ Yes | ❌ No | — |
| Windows | ✅ Yes | ❌ No | — |
Best results: run on Linux with headers provided.
Scanning Windows PE (DLL) binaries¶
| Host OS | Symbol diff | Type/param diff | Requires |
|---|---|---|---|
| Linux | ✅ Yes | ❌ No | pefile |
| macOS | ✅ Yes | ❌ No | pefile |
| Windows ✅ | ✅ Full | ✅ Full | castxml + cl.exe |
Scanning macOS Mach-O (dylib) binaries¶
| Host OS | Symbol diff | Type/param diff | Requires |
|---|---|---|---|
| Linux | ✅ Yes | ❌ No | macholib |
| macOS ✅ | ✅ Full | ✅ Full | castxml (Xcode clang) |
| Windows | ✅ Yes | ❌ No | macholib |
"Full" means implemented capability, not CI-proven maturity. The table above tracks two booleans (symbol diff, type/param diff) per host/format — it does not encode how much automated validation backs a "Full" cell, or which toolchain you used to get there. The Windows PE "Full" type/param cell nominally means the
castxml+cl.exeheader-AST path (see Dependency Summary below); neither Windows CI lane actually exercises that path: thenative-compareCI step runscompareon MinGW-built DLLs without headers or castxml (symbol/DWARF data only), and the non-blockingmsvcstep validates MSVC+PDB debug-info parsing, not the castxml+cl.exeheader route either. See Validation status and the Windows Toolchain Support Matrix below before relying on a specific toolchain in production.
Validation status (what is actually exercised in CI)¶
The matrices above describe intended capability. The depth of automated validation differs sharply by platform, and you should calibrate trust accordingly:
| Platform | Binary/metadata parsing | Workflow end-to-end (compare / appcompat / …) |
|---|---|---|
| Linux / ELF | Unit and integration tests | Validated in CI (the baseline) |
| Windows / PE+PDB | Unit tests for the PE/PDB parsers | Validated in CI for MinGW: the native-compare step (integration.yml, Windows leg) runs compare on MinGW-built DLLs. The msvc step additionally asserts MSVC+PDB verdicts (PDB layout depth best-effort) but runs non-blocking (continue-on-error, informational) until proven stable |
| macOS / Mach-O | Unit tests for the Mach-O/ARM64 layer | Validated in CI: the native-compare step (integration.yml, macOS leg) runs compare on Apple-clang-built dylibs; AArch64 AAPCS64 HFA/HVA passing drift is not detected (see below) |
Concretely: the core compare workflow is now exercised end-to-end on native
PE and Mach-O binaries (built by the platform's own toolchain) in the
native-compare CI step (gap G1 closed). What remains a deliberate
Linux-anchored subset is the example catalog: every entry in
catalog/ground_truth.json
is validated on Linux, and a platforms tag of macos/windows expresses
intended portability rather than a per-case CI result — some cases carry an
explicit known_gap describing where the non-Linux path diverges. This
invariant (Linux = universal baseline; macOS/Windows = strict subset) is guarded
by tests/test_platform_coverage_honesty.py. See
Use-Case Coverage Evaluation
(gap G1) for context.
Castxml-free validation (no external tools)¶
While the full header-driven pipeline uses castxml, a large slice of the
catalog needs no castxml at all: a plain -g build embeds DWARF in the
shared object, and abicheck reads type/layout/calling-convention facts
straight from it. tests/test_castxml_free_examples.py validates 40 catalog
cases end-to-end on the Linux baseline using only a C/C++ compiler — building
v1/v2, dumping with no headers (DWARF + symbol table only), and asserting the
ground_truth.json verdict. This guards the pure-Python, drop-in path that many
CI environments and developer machines actually run (no castxml installed). The
~11 cases that genuinely require castxml (concept tightening, explicit-ctor
mangling, header-only scoping) remain covered by the castxml integration lane.
What "No Headers" Actually Means¶
"No headers" is not the same as "symbols only." Whether a headerless scan
still sees type/layout facts depends on whether the binary carries debug
info (DWARF/PDB) — a separate axis from whether headers were given. Don't
conflate the two; see Evidence & Detectability
for the full L0–L5 model this table summarizes:
| Input | Evidence layer | What's available |
|---|---|---|
| Stripped binary, no headers | L0 — symbols only | Exported symbols, SONAME/install-name, symbol versions, visibility, binding, dependency list |
| ELF with DWARF, no headers | L0+L1 — binary + debug | Everything L0 sees, plus struct/class layout, field offsets, enum values, vtable slots, calling convention, packing, and — since DWARF also encodes a function's parameter/return types (DW_TAG_formal_parameter/DW_AT_type) — parameter and return type changes too. No castxml needed (see "Castxml-free validation" above) |
| PE with PDB, no headers | L0+L1 — binary + debug, narrower | Everything L0 sees, plus struct/class layout, field offsets, enum values, and calling convention for class methods only — pdb_metadata.parse_pdb_debug_info() extracts struct/enum/toolchain facts from the TPI stream but does not reconstruct free-function signatures or vtable slots, so parameter/return type changes and vtable changes still need -H/castxml on this platform |
| Mach-O, no headers | L0 only, always | _dump_macho never parses DWARF or a debug map (dSYM/N_OSO) — headerless is always exports + load-command metadata only, no debug-info fallback at all, regardless of whether the dylib carries debug info |
| Binary + headers | L0–L2 — header-aware | Everything above, plus qualifiers/visibility that only a declaration carries (inline, noexcept, final, access level), default-argument values, and public-surface scoping |
| + build/source context | L3–L5 | Build flags, source-only facts (macros, default arguments, inline bodies), the source/impact graph |
A headerless scan of an ELF -g build (L0+L1) does detect:
- Struct/class layout changes (type_size_changed, type_field_*)
- vtable changes (type_vtable_changed)
- Enum value changes, calling-convention changes
- Parameter type changes (func_params_changed), return type changes (func_return_changed)
A headerless scan of a PE /Zi build (L0+L1, PDB) is narrower — it detects
struct/class layout and enum value changes, plus calling-convention changes on
class methods, but not vtable changes or free-function parameter/return
type changes (those still need -H/castxml).
A headerless scan of a Mach-O dylib is always L0 only, even if it carries
DWARF/dSYM debug info — abicheck has no Mach-O debug-map reader today, so
-H/castxml is the only way to get past exports-only on this platform.
A genuinely stripped binary with no debug info (L0 only) still detects, from
export-table/load-command metadata alone (see
tests/test_binary_only_pe_macho.py):
- Function added / removed (func_added, func_removed)
- SONAME changed (soname_changed)
- Symbol visibility changed (func_visibility_changed)
- Variable added / removed
- PE: ordinal reassignment, forwarder-target repoint, machine/architecture drift
- Mach-O: CPU type / architecture drift
❌ Neither L0 nor L1 alone detects on any platform (needs L2 headers — a
declaration-only qualifier or a default-argument value that leaves no
DWARF/PDB/symbol-table trace):
- Inline/noexcept/final/access-level changes
- Default-argument value changes
❌ PDB (PE) additionally misses, even with L1 debug info (needs L2
headers to recover, unlike the ELF DWARF path above):
- Parameter type changes (func_params_changed), return type changes (func_return_changed) for free functions
- vtable changes (type_vtable_changed)
- Calling-convention changes on free (non-method) functions
❌ Mach-O misses everything past L0 without headers, unconditionally —
struct/class layout, enum values, vtable slots, calling convention, and
parameter/return types all need -H/castxml on this platform; there is no
debug-info-only middle ground the way ELF/PE have.
Recommendation: for complete ABI analysis, provide -H <header_dir> and run on the
native platform (Linux for ELF, macOS for Mach-O, Windows for PE). If you
can't, a debug build (-g on ELF, /Zi on PE) alone already recovers
layout-level breaks that a fully stripped binary would miss — but this
fallback does not extend to Mach-O, which stays L0-only without headers
regardless of debug info; -H/castxml is the only way past exports-only
there.
Cross-Platform Examples¶
Scan a Windows DLL from Linux¶
# Symbol-level diff (works cross-platform)
abicheck compare mylib_v1.dll mylib_v2.dll
# With headers (only useful if castxml+cl.exe is available)
abicheck compare mylib_v1.dll mylib_v2.dll \
-H include/
What you get: func_removed, func_added, ordinal changes.
What you miss: parameter type changes, struct layout changes.
Scan a macOS dylib from Linux¶
What you get: exported symbol diff. What you miss: type-level analysis (no DWARF walk cross-platform today).
Scan a Linux .so from macOS¶
ELF parsing is pure Python — works on macOS. DWARF walk also works.
Full type analysis requires castxml and headers available on the host.
GitHub Actions: Multi-Platform CI¶
To get full analysis on each platform:
jobs:
abi-check:
strategy:
matrix:
os: [ubuntu-24.04, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: conda-incubator/setup-miniconda@v3
with:
activate-environment: abicheck
- name: Install abicheck (source)
shell: bash -el {0}
run: python -m pip install -e .
- name: Install CastXML (Linux/macOS only)
shell: bash -el {0}
run: |
# On Ubuntu, avoid the Clang-17 apt package; use conda-forge or the
# checksum-pinned Superbuild installed by the abicheck Action.
conda install -y -c conda-forge castxml
- name: ABI check
shell: bash -el {0}
run: |
abicheck compare old/libmylib.so new/libmylib.so \
-H include/
Known Limitations by Platform¶
Windows host¶
castxmlwithcl.exebackend is untested in CI — may work but is not validated- MSVC vtable layout differs from Itanium ABI; vtable diff results may be inaccurate
__stdcall/__cdeclcalling-convention changes appear asfunc_removed + func_added(mangled-name churn) — no dedicated change kind; see #50 below- Tracked: abicc upstream issues #9, #50, #56, #121
macOS host¶
- ARM64 Apple AAPCS differs from Itanium for small structs (≤16 bytes passed in registers)
install_name(LC_ID_DYLIB, macOS SONAME equivalent) changes are tracked and emitSONAME_CHANGED- Two-level namespace (
LC_LOAD_DYLIB) not fully analyzed - Tracked: abicc upstream issues #116, #119
All platforms (no headers — applies whether or not debug info is present)¶
int → longparameter change on a C++ export whose Itanium/MSVC mangled name embeds the parameter type: the mangled name itself changes, so old and new are matched as two distinct symbols and detected asfunc_removed + func_added, notfunc_params_changed— L1 debug info doesn't recover this either, since the mismatch is in symbol identity, not missing type information.- Template inner-type changes (
std::vector<T>with changedT) — not detected (tracked: #38)
ELF with DWARF, no headers (the one platform this reaches from L1 alone)¶
- A plain C function or an
extern "C"export, whose exported name never encodes its parameter types, is correctly detected asfunc_params_changedfrom L1 debug info alone (see case02, a headerless-gcomparison) — this is the one exception to the "All platforms" bullet above. It does not extend to PE (PDB reconstructs no free-function signatures at all, per the PDB row above) or Mach-O (no debug-info path at all without headers, per the Mach-O row above): on those two, a plain-C parameter change still needs-H/castxml.
Dependency Summary¶
| Feature | Required tools | pip / system install | conda-forge install |
|---|---|---|---|
| ELF analysis | pyelftools |
pip install -e . |
conda install -c conda-forge abicheck |
| PE analysis | pefile |
pip install -e . |
conda install -c conda-forge abicheck |
| Mach-O analysis | macholib |
pip install -e . |
conda install -c conda-forge abicheck |
| Type/param analysis (Linux) | castxml + C/C++ compiler |
pip install -e . + apt/yum (castxml, gcc/g++) |
conda install -c conda-forge abicheck |
| Type/param analysis (macOS) | castxml + Apple toolchain |
pip install -e . + brew install castxml (+ Xcode CLT) |
conda install -c conda-forge abicheck |
| Type/param analysis (Windows) | castxml + cl.exe |
pip install -e . + Visual Studio Build Tools + castxml |
conda install -c conda-forge abicheck |
For conda-based workflows, install only abicheck from conda-forge.
Recipe dependencies pull required analysis tooling automatically.
Windows Toolchain Support Matrix¶
| Toolchain | castxml backend | Type/param diff | Calling-convention tracking | Status | Notes |
|---|---|---|---|---|---|
| MinGW (GCC) | --castxml-cc-gnu gcc |
✅ Yes | ⚠️ Partial (__cdecl/__stdcall not a dedicated kind, #50) |
Experimental | Covered by CI smoke tests; full MinGW integration coverage on windows-latest is best-effort and not guaranteed on every run. |
MSVC (cl.exe) |
--castxml-cc-msvc cl.exe |
✅ Yes | ⚠️ Partial (#9, #50) | Untested in CI | May work locally with Visual Studio Build Tools; ABI details differ from Itanium assumptions in several detectors. |
Known Limitations (Windows)¶
- #9 (MSVC headers / Windows SDK edge cases): castxml+
cl.exehandles common cases, but complex SDK-specific declarations are not yet validated in project CI. - #50 (calling conventions):
__stdcall/__cdecldeltas are represented as mangled-name churn (func_removed + func_added) instead of a dedicated calling-convention change kind. - #56 (PE visibility semantics):
__declspec(dllexport/dllimport)transitions are currently reflected at symbol-level only; no dedicated PE export-visibility change kind. - #121 (MinGW-specific behavior): MinGW export/import edge-cases (import libs, ordinals, toolchain flags) are only partially covered by current smoke/integration tests.
macOS ARM64 — Known ABI Differences¶
ARM64 (Apple Silicon) has a different calling convention from x86-64 that affects how small structs and floating-point aggregates are passed.
| Feature | x86-64 (System V) | ARM64 (Apple AAPCS) | Detected by abicheck? |
|---|---|---|---|
| Small struct (≤16 B) | Stack or reg pair | Passed in GP registers | ✅ TYPE_SIZE_CHANGED catches size delta |
| HFA (Homogeneous Floating-point Aggregate ≤4 floats) | Stack | SIMD/FP registers | ⚠️ Size same, registers differ — NOT detected |
| HVA (Homogeneous Vector Aggregate) | Stack | SIMD/FP registers | ⚠️ Size same, registers differ — NOT detected |
| Return in registers | RDX:RAX (x86-64) | x0:x1 (ARM64) | ⚠️ Not tracked (no calling-convention change kind) |
Tracked: abicc issues #116 (small-struct register passing) · #119 (install_name).
install_name tracking (#119)¶
install_name (LC_ID_DYLIB) is the macOS equivalent of ELF SONAME.
abicheck now tracks this — a change emits SONAME_CHANGED with symbol="LC_ID_DYLIB".
| Scenario | Status |
|---|---|
| install_name changes between versions | ✅ SONAME_CHANGED emitted |
| install_name absent → set | ✅ tracked |
| No change | ✅ no false positive |
Support claim¶
ARM64/macOS: Experimental
- Symbol diff: fully supported (export table via macholib)
- Type/param diff: requires Xcode clang + castxml (≥ 0.9.0 with Apple toolchain backend)
- HFA/HVA calling-convention drift: not directly detected — workaround: always check TYPE_SIZE_CHANGED on structs