Skip to content

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.exe header-AST path (see Dependency Summary below); neither Windows CI lane actually exercises that path: the native-compare CI step runs compare on MinGW-built DLLs without headers or castxml (symbol/DWARF data only), and the non-blocking msvc step validates MSVC+PDB debug-info parsing, not the castxml+cl.exe header 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

abicheck compare libmylib.1.dylib libmylib.2.dylib

What you get: exported symbol diff. What you miss: type-level analysis (no DWARF walk cross-platform today).

Scan a Linux .so from macOS

abicheck compare libmylib.so.1 libmylib.so.2

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

  • castxml with cl.exe backend is untested in CI — may work but is not validated
  • MSVC vtable layout differs from Itanium ABI; vtable diff results may be inaccurate
  • __stdcall/__cdecl calling-convention changes appear as func_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 emit SONAME_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 → long parameter 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 as func_removed + func_added, not func_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 changed T) — 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 as func_params_changed from L1 debug info alone (see case02, a headerless -g comparison) — 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.exe handles common cases, but complex SDK-specific declarations are not yet validated in project CI.
  • #50 (calling conventions): __stdcall/__cdecl deltas 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