Skip to content

ABI Cheat Sheet

Quick-reference card for shared-library maintainers. Scannable in 2 minutes.

For deeper explanations see ABI/API Handling & Recommendations and Verdicts. To see which evidence level proves each row below (symbols → debug → headers → build → sources), see What Each Level Sees.


Safe Changes (COMPATIBLE)

These changes preserve binary compatibility. Existing consumers continue to work without recompilation.

Change Why Safe Example
Add new exported function Existing binaries never reference it; linker ignores unknown symbols case03
Append enum member (end, no value shift) Compiled binaries use integer values; existing values unchanged case25
Add union field without growing size Union size = max(fields); fits within existing allocation case26b
Weaken symbol binding (GLOBAL to WEAK) Symbol still resolves; interposition semantics relax case27
Add IFUNC dispatch Transparent to callers; resolver picks implementation at load time case29
Outline an inline function (add export) New symbol appears; callers with inlined copy still work case47
Add new global variable No existing code references it case61
Add field to opaque struct Callers access through pointers only; layout is hidden case62
Tighten a C++20 concept (still satisfied) Existing callers compile; no symbol or layout change case105
Graduate experimental:: → stable (keep old alias) New stable surface added; old symbols still resolve case99
Change a non-public, scoped internal struct Not part of the public surface — no consumer can observe it case118, case119, case120
Strengthen symbol binding (WEAK → GLOBAL) Symbol still resolves; the intended definition wins. Context note: if a consumer relied on interposing the weak symbol, tightening it removes that hook case128
Add hardening / deployment metadata (drop exec-stack, add DT_NEEDED, change RUNPATH) Loader still resolves the existing contract; posture improves or is deployment-local. Context note: a new DT_NEEDED / changed RUNPATH can select a different provider or fail on hosts missing the dependency — a deployment concern, not a symbol-contract break case136, case137, case138

Scoped to the public surface. Changes to internal/private types that never reach the public header surface are reported as ✅ NO_CHANGE under public-surface scoping (cases 118–120). This is why feeding abicheck the real public headers matters — it lets the tool tell internal churn apart from a real break.


Breaking Changes (NEVER do in a minor release)

These cause crashes, wrong results, or link failures in pre-compiled consumers.

Change What Happens at Runtime Example
Remove exported symbol undefined symbol on dlopen/startup case01
Change parameter types Caller passes args in wrong registers/format; garbage or crash case02
Change struct layout/size Stack corruption; reads/writes past allocation boundary case07
Change enum member values Switch/lookup tables use stale integer values; wrong branch taken case08
Reorder virtual methods Vtable slot mismatch; call dispatches to wrong method silently case09
Change return type Caller interprets return register/memory as wrong type case10
Change class size (add members) new/stack allocation undersized; heap corruption, SIGSEGV case14
Remove enum member Code referencing removed constant fails at compile time or uses stale value case19
Change type alignment (alignas) Misaligned access; SIGBUS on strict-alignment architectures case42
Change struct packing (pragma pack) Field offsets shift; every member read is wrong case56
Change calling convention Parameters read from wrong registers; total data corruption case64
Remove symbol version node Dynamic linker refuses to load; version 'FOO_1.0' not found case65
Remove extern "C" (language linkage) Symbol re-mangles (parse_config_Z12parse_configPKc); old binaries fail to resolve case66
Change TLS variable size/layout Per-thread storage corruption in existing consumers case67
Add first virtual method to a class A vptr is prepended; every member shifts by sizeof(void*), sizeof grows case68
Make a trivially-copyable type non-trivial Pass-by-value flips register↔memory; callee dereferences a value as a pointer case69
Change flexible-array element type sizeof(header) matches, but every data[i] indexes with the wrong stride case70
Bump an inline namespace Every symbol re-mangles (v1v2); pre-compiled callers can't resolve case71, case101
Change typedef underlying type Width/representation shifts under callers compiled against the old alias case73
Leak an internal detail:: type through a public API Library symbols look identical; a hidden base/embedded layout shift corrupts consumers case74, case77
Flip libstdc++ dual ABI (_GLIBCXX_USE_CXX11_ABI) std::string re-layout; mixed-flavor binaries fail to link or corrupt case104
Switch integer model (LP64 → ILP64) MKL_INT 32→64 silently doubles every integer field/argument case112
Change an ABI tag ([abi:cxx11]) Symbol re-mangles on the tagged entity; old callers can't resolve case113
Migrate char family → char8_t (C++20) New distinct type re-mangles signatures and changes overload resolution case114
Change _BitInt(N) width (C23) 64→128 changes size, alignment, and register passing case115
Add _Atomic qualifier (C11) Size/alignment and access semantics change under old callers case116
[[no_unique_address]] layout overlay Empty-member overlap shifts subsequent field offsets case117
Return-by-value type became non-trivial (destructor added) Return convention flips register→hidden-pointer (sret); caller reads a value as a pointer. Mangled name unchanged case129
Empty base gains a member (EBO lost) The empty base subobject now takes space; every derived member offset shifts and sizeof grows case140
Vtable slot count changed (from a stripped binary) _ZTV size alone reveals the slot count changed (no DWARF) — a slot-renumbering risk: some existing slots may have moved, so old callers can dispatch to the wrong method. Pinpointing which slot / whether it was a mid-insert vs. append needs debug info (L1) case142
Exported data object grew (symbol_size_changed) Consumers sized their copy/relocation to the old st_size; a larger object overruns case127
Remove a symbol version node Dynamic linker refuses to load; version 'FOO_1.0' not found case139
Kernel struct field added (BTF) In-tree/out-of-tree modules baked the old layout; field offsets shift case121

See the full breaking catalog in ABI/API Handling & Recommendations.


Source-Only Breaks (API_BREAK)

Binary-compatible, but recompilation against new headers fails. Verdict: 🟠 API_BREAK.

Change Impact Example
Rename enum member (same value) LOG_ERR no longer compiles; binary still uses integer 1 case31
Narrow access level (public to private) Downstream code calling helper() gets compile error case34
Make a converting constructor/operator explicit Implicit conversions at call sites stop compiling; ABI unchanged case106
Remove a hidden-friend operator ADL call sites fail to compile; no symbol was ever exported case96
Remove default parameter Call sites relying on default fail to compile; ABI unchanged case123
Mark a class final Downstream code deriving from it stops compiling; ABI unchanged case125
Change a public const/constexpr constant value Header-baked constant differs from prebuilt binaries; recompilation shifts behavior case124
Remove a public #define macro (needs source — L4) #ifdef FOO / FOO-using call sites fail to compile; no symbol trace case156
Remove a header-only inline function (L4) Callers that inlined it still run, but recompiles fail to find it case157
Remove a public typedef (L4) Every use of the alias stops compiling; binary is untouched case158
Rename a Python extension keyword arg (.pyi API) importing callers passing the old kwarg raise TypeError; the .so is byte-identical case163

Risk Changes (deployment concern)

Binary-compatible, but may break at deployment time. Verdict: 🟡 COMPATIBLE_WITH_RISK.

Change Risk Example
New GLIBC/GLIBCXX version requirement Binaries won't load on older distros missing the required symbol version -- (detected via SYMBOL_VERSION_REQUIRED_ADDED)
Leaked dependency symbol changed Transitive dependency update shifts symbols your consumers never directly linked --
noexcept removed Callers compiled assuming noexcept omit landing pads; a real throw calls std::terminate case15
Drop a CPU-dispatch ISA family Binaries still load, but the optimized path the consumer expected is gone case83
Weaken RELRO (FULLPARTIAL/none) GOT stays writable; hardening regressed process-wide case134
Drop the stack canary (-fstack-protector) Overflow detection removed from the shipped binary case135
Change the TLS access model Per-thread access sequence changes; risky when mixed with old callers case133

Build-Flag & Toolchain Drift (needs build data — L3)

The flags the library was built with are an ABI input the shipped binary barely shows. Feed abicheck the build data (-p build/ / scan --depth build) and it diffs them. On their own — when no public symbol changes — these are 🟡 COMPATIBLE_WITH_RISK: the flag delta explains and localizes churn but never manufactures a break (the authority rule). If the same flag flip actually remangles public symbols, the L0 symbol diff proves a 🔴 BREAKING on its own (that is why case104 is classified BREAKING, not risk). See What Each Level Sees § L3.

Flag drift Why it matters Example
_GLIBCXX_USE_CXX11_ABI flipped libstdc++ string/list ABI changes. Risk-only when no public symbol changes; 🔴 BREAKING if it re-mangles exported std::string/std::list signatures case104
-fexceptions mode flipped EH tables/landing pads differ across the boundary case130
-frtti mode flipped typeinfo/dynamic_cast support diverges case131
Thread-safe statics (-fthreadsafe-statics) flipped Function-local static init guards change case132
-fshort-enums flipped Enum underlying size changes → struct layout shifts case152
-fpack-struct / packing mode flipped Every field offset moves case153
LTO mode flipped Cross-TU inlining/visibility interactions change case154
char signedness flipped char-typed values reinterpret sign case155

Intra-Version Hygiene (audit — no baseline needed)

abicheck scan libfoo.so (with no --against) lints a single build for bad ABI hygiene — problems you can see without a previous version. Absence of --against is already a one-build audit; there is no separate --audit flag. All 🟡 COMPATIBLE_WITH_RISK.

Finding What it flags Example
Accidental export Symbol exported but in no public header case143
Private-header leak Public API pulls an unshipped header case144
Unversioned export Export with no version node though a scheme exists case145
Exported RTTI for internal type _ZTI/_ZTV leaked for a private-header type case146

Cross-Source & Reachability (two sources beat one)

Findings that surface only when abicheck crosschecks two sources, or derives the L5 reachability graph. A conflict invisible to any single source resolves by comparing them.

Finding What it catches Example
Header ↔ build mismatch Headers parsed without the build's ABI flags → wrong recorded layout case148
ODR type variant One type, two per-TU layouts case149
Export ↔ decl mismatch Exported-not-public / public-not-exported, both directions case150
Public API gained an internal dependency A public entry newly reaches a non-public entity through the L5 graph — a risk signal (later changes to the internal become hidden behavioral risk), not a proven ABI dependency. It is only a hard break if it surfaces via a public header, inline body, or link-time symbol case160
Exported symbol's declaring file moved Stable symbol, but its owning header changed (L5 graph) case162

Quality Warnings

No immediate breakage, but these compromise the ABI contract or security posture. abicheck flags these as 🟡 COMPATIBLE quality checks (SONAME_MISSING, VISIBILITY_LEAK, EXECUTABLE_STACK, RPATH_CHANGED). Fixing them later often causes 🔴 BREAKING changes.

Warning Why It Matters Example
Missing SONAME Consumers record bare filename; library versioning breaks case05
Visibility leak (no -fvisibility=hidden) Internal symbols become public ABI surface you must maintain forever case06 (fixing later = BREAKING)
Executable stack (GNU_STACK RWX) Disables NX protection process-wide; trivial exploit target case49
RPATH leak (hardcoded build path) Library only works on the build machine; deployment fails everywhere else case52
Namespace pollution (generic names) Unprefixed symbols like init() collide across libraries case53 (fixing later = BREAKING)

Prevention Patterns

Pattern Protects Against How
-fvisibility=hidden + explicit exports Visibility leaks, accidental ABI surface Only annotated symbols enter .dynsym
Pimpl / opaque handles Struct layout breaks Callers see T* only; fields are private
Symbol versioning (version script) Symbol removal, version node breaks Map file controls what's exported per version
SONAME with major-version bump All breaking changes libfoo.so.1 to libfoo.so.2 on ABI break
Reserved fields in public structs Future field additions void *_reserved[4] absorbs growth without size change
CI ABI check with abicheck All of the above Catches regressions before merge (see below)

CI One-Liner

abicheck compare libfoo.so.old libfoo.so.new \
  --header old=include/old/foo.h \
  --header new=include/new/foo.h \
  --policy strict_abi

Exits non-zero on any 🔴 BREAKING or 🟠 API_BREAK finding. Add --suppress suppressions.yaml to allowlist known acceptable changes. See CLI Usage and Policies for options.


Verdict Quick Reference

Icon Verdict Meaning
🔴 BREAKING Binary incompatible -- consumers crash or misbehave
🟠 API_BREAK Source incompatible -- recompilation fails, binary works
🟡 COMPATIBLE_WITH_RISK Binary works, deployment risk present
🟡 COMPATIBLE (quality) Binary works, bad practice detected
🟢 COMPATIBLE (addition) New API surface, fully backward-compatible
NO_CHANGE Identical ABI

Full verdict semantics: Verdicts | All example cases: Scenario Catalog