ABI Cheat Sheet¶
Quick-reference card for shared-library maintainers. Scannable in 2 minutes.
For deeper explanations see ABI/API Compatibility 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 (v1 → v2); 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 Break families and where each is explained below for the family-by-family index.
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 (FULL → PARTIAL/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 compare --no-baseline libfoo.so lints a single build for bad
ABI hygiene — problems you can see without a previous version. --no-baseline
is what selects the one-build audit; there is no separate --audit flag, and
no --against option on compare at all (that was the retired scan
command's spelling). 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 |
Break families and where each is explained¶
Every detected change maps to one of these families, each pointing to the
Learning Series page that explains its mechanism. The verdict column shows
the typical classification; the exact verdict per fixture lives in
catalog/ground_truth.json and the
Examples Encyclopedia. "mixed" means the
verdict is case-dependent.
| Family | Representative cases | Typical verdict | Explained in |
|---|---|---|---|
| Symbol/function removal & rename | 01, 12, 58, 66 | 🔴 BREAKING | Part 2 |
| Signature changes (params, return, pointer level) | 02, 10, 33, 46 | 🔴 BREAKING | Part 2 |
| Global variable type/qualifier/removal | 11, 39, 58 | 🔴 BREAKING | Part 2 |
| Struct/class layout, alignment & packing | 07, 14, 40, 42, 43, 56, 117 | 🔴 BREAKING | Part 3 |
| Enum value/underlying changes | 08, 19, 20, 57 | 🔴 BREAKING | Part 3 |
| Union layout | 24, 26 (grows) · 26b (no growth) | mixed — 🔴 if size grows, else 🟢 | Part 3 |
| C++ vtable & virtual methods | 09, 23, 38, 68, 72 | 🔴 BREAKING | Part 4 |
| C++ qualifiers, mangling & ABI tags | 21, 22, 30, 71, 86, 101, 113 | mixed — 🔴 BREAKING or 🟠 API_BREAK | Part 4 |
| Trivial → non-trivial (calling convention) | 64, 69 | 🔴 BREAKING | Part 4 |
| Templates, inline & ODR | 16, 17, 47, 59, 79, 85, 87 | mixed — 🔴 BREAKING or 🟢 COMPATIBLE | Part 4 |
| Modern C/C++ contract shifts (char8_t, _BitInt, _Atomic, concepts) | 105, 114, 115, 116 | mixed — 🔴 BREAKING or 🟢 COMPATIBLE | Modern C/C++ and Toolchain ABI Hazards |
| ELF/linker metadata (SONAME, visibility, versioning, RPATH, TLS) | 05, 06, 13, 49, 51, 52, 65, 67 | mixed — 🔴 BREAKING or 🟢 COMPATIBLE | Part 5 |
Transitive/dependency & detail:: leaks |
18, 48, 74, 75, 76, 77, 80, 97, 104, 112 | 🔴 BREAKING | Part 6 |
| Source-only / API-level (rename, access, explicit, default args, hidden friends) | 31, 34, 96, 106, 123, 124 | 🟠 API_BREAK | Part 6 §Source-only API breaks |
| Deployment risk (noexcept, ISA dispatch, version-require) | 15, 83 | 🟡 COMPATIBLE_WITH_RISK | Part 4 |
| Dependency / runtime floors & environment drift (glibc/libstdc++ floor, DT_RELR, RPATH type) | 170 | 🟡 COMPATIBLE_WITH_RISK — 🔴 or 🟢 once a floor is declared; the 32-bit time64/LFS flip (time64_abi_changed) is always 🔴 BREAKING |
Dependency & Runtime Floors + Environment & Toolchain Drift |
| Compatible additions & quality signals | 03, 25, 26b, 27, 29, 61, 62, 99 | 🟢 COMPATIBLE | Part 7 |
| Scoped/non-public internal changes | 118, 119, 120 | ✅ NO_CHANGE | Part 6 |
Security-hardening & deployment metadata (RELRO, canary, exec-stack, RUNPATH, DT_NEEDED, TLS model, symbol binding) — artifact/linker facts (L0/L3) |
128, 133, 134, 135, 136, 137, 138 | mixed — 🟡 risk (RELRO/canary/TLS) or 🟢 COMPATIBLE (exec-stack/RUNPATH/DT_NEEDED/binding) |
Part 5 |
| Build-flag & toolchain drift (L3) — the flags the library was built with, as a finding on their own | 130, 131, 132 | 🟡 COMPATIBLE_WITH_RISK | Source & Build Data |
Source-only bodies & macros (L4) — #define macro values, inline/template/constexpr bodies, uninstantiated templates (none header-reachable) |
122 (the documented NO_CHANGE gap — even L4 can't close it; a detected macro/body change is 🟠 API_BREAK / 🟡 risk) |
mixed — 🟠 API_BREAK / 🟡 risk, or ✅ NO_CHANGE (residual gap) | Source & Build Data |
| Intra-version ABI hygiene / audit — accidental export, private-header leak, unversioned export, RTTI leak (no baseline needed) | 143, 144, 145, 146 | 🟡 risk | § source scan |
| Cross-source validation — one fact, two sources: header↔build mismatch, ODR variant, export↔decl pair | 148, 149, 150, 151 | mixed — 🟠 API_BREAK or 🟡 risk | § source scan |
The security-hardening & deployment row is artifact/linker coverage
(L0/L3, mixed verdicts — an object-size change like
case127 is a
separate 🔴 BREAKING layout finding, not a hardening risk). The last four
rows are the families a plain two-version compare of L0–L2 artifacts does
not produce on its own — build-flag drift needs the build data (L3),
source-only bodies & macros need the sources (L4), and the intra-version
hygiene and cross-source families need the scan's cross-source pass. All of
them are walked in What Each Level Sees.
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 legend¶
🔴 BREAKING · 🟠 API_BREAK · 🟡 COMPATIBLE_WITH_RISK / COMPATIBLE (quality) · 🟢 COMPATIBLE (addition) · ✅ NO_CHANGE — what each means and how it maps to an exit code is owned by Verdicts.
All calibration cases: Compatibility Catalog.
Ladder: ← How a Break Shows Up · Step 1 · Start Here · Glossary →