ABI/API Handling — A Learning Series¶
This is the conceptual hub for understanding ABI/API compatibility — written to teach the subject, not just catalog it. It is the front door to a nine-part learning series that starts from first principles ("what is a symbol? what does the loader do?") and builds up to the design patterns that keep a C/C++ shared library compatible across releases.
The series is for two audiences at once: developers who maintain or consume shared libraries, and AI agents reasoning about whether a change is safe to ship. Every break is explained as a mechanism — what the compiler baked in, what the loader does, what byte moves — and then as a fix. abicheck's verdicts and change kinds are woven in throughout, so the same page that teaches you why a struct-field insertion corrupts memory also tells you what abicheck will report when it sees one.
New to the topic? Don't start here — start with the on-ramp.
This hub is a navigator: it maps the series, the deep-dive pages, and the break families, and points each to its own page (e.g. the evidence-model walk-through, What Each Level Sees). If binary compatibility is new to you, read the five-minute on-ramp first and follow the series in order:
- ABI in Five Minutes — the gentlest introduction.
- Part 0 — Compatibility as a Product Contract — the framing.
- Part 1 — Foundations — symbols, linking, the loader.
Then come back here to navigate the rest of the series.
Looking for something faster? For a 2-minute scannable card, see the ABI Cheat Sheet. For per-case runnable reproductions with code and a real failure demo, see the Examples & Case Encyclopedia. For verdict semantics and CI exit codes, see Verdicts. For unfamiliar terms (SONAME, vtable, IFUNC, install name, TLS model…), see the Glossary.
Going deep on class layout? The Class Layout ABI & API guide is the single page that maps every class-layout change (base offsets, EBO, vptr, vtable slots, RTTI, standard-layout / trivially-copyable, packing) to the exact
ChangeKindabicheck emits, the evidence tier that reveals it, and a worked example.Shipping one binary to several OS releases? The Dependency & Runtime Floors guide covers the contract below your library — why glibc/libstdc++ version requirements decide which distros can load a release, how a mere rebuild raises that floor, the macOS/Windows parallels, and the CPU-dispatch (oneDAL/OpenBLAS) scenario where a new-hardware kernel moves the floor for every consumer.
Scope & assumptions
- Examples are mostly ELF/Linux and Itanium-C++-ABI flavored unless a section says otherwise. PE/COFF (Windows) and Mach-O (macOS) have their own loader, export, and versioning rules — see the per-platform parallels in Part 5 and the Platform Support reference. For example, the "lookup by name" model in Part 2 is exact for ELF and for most C/C++ exports, but Windows DLLs can also export/import by ordinal, where the contract is a number, not a name.
- Detectability depends on the inputs you give abicheck — symbols only,
DWARF/PDB debug info, or public headers. Some changes (e.g.
#definemacros, inline/template bodies, uninstantiated templates) are invisible to any artifact comparison. See the per-change matrix in Limitations.
How to read this series¶
The parts are ordered. If you're new to ABI compatibility, read them in sequence — each builds on the mental models established by the last. If you're here for a specific problem, jump straight to the relevant part.
| Part | Page | What it covers | Read it when… |
|---|---|---|---|
| 0 | Compatibility as a Product Contract | Public surface, SemVer mapping, contract shapes — the framing | …before anything else: a change is only a "break" if it breaks a promise |
| 1 | Foundations | Source → object → link → load; what a symbol is; API vs ABI | …you want the ground-up mental model (start here) |
| 2 | Symbol Contracts | Removal, rename, signature, pointer-level, globals | …a symbol disappeared or changed meaning |
| 3 | Type Layout | Struct size/offset, alignment, enums, unions, bitfields | …you changed a struct, enum, or union |
| 4 | C++ ABI | Vtables, mangling, templates, noexcept, trivial→non-trivial, bases |
…you maintain a C++ library |
| 5 | Linker & ELF | SONAME, visibility, versioning, calling conv., TLS, security metadata | …a load-time/linker contract changed |
| 6 | Transitive Breaks | Dependency leaks, anonymous structs, type-kind swaps, reserved fields | …the symbol table looks identical but consumers still break |
| 7 | Designing for Stability | Opaque handles, Pimpl, version scripts, CI gating — with full code | …you're designing an API to evolve safely |
| 8 | Detecting Breaks | Tracking approaches, evidence each break family needs, why single-method checkers miss whole families | …you're deciding how to catch all of the above in CI |
flowchart LR
P0["0 · Product<br/>Contract"] --> P1["1 · Foundations"]
P1 --> P2["2 · Symbol<br/>Contracts"]
P1 --> P3["3 · Type<br/>Layout"]
P2 --> P4["4 · C++ ABI"]
P3 --> P4
P4 --> P5["5 · Linker<br/>& ELF"]
P3 --> P6["6 · Transitive<br/>Breaks"]
P5 --> P7["7 · Designing<br/>for Stability"]
P6 --> P7
P7 --> P8["8 · Detecting<br/>Breaks"]
Cross-cutting companion: Evidence & Detectability explains which inputs (symbols, debug info, headers, app, bundle) let a tool see a given change at all — read it alongside any part when you're wondering "why did the tool catch this but not that?"
Pick a reading path for your role¶
The series is ordered, but you rarely need all of it at once. These paths get each audience to the pages that matter for them fastest:
| Audience | Recommended path |
|---|---|
| New C/C++ library author | Product Contract → Foundations → Symbol Contracts → Type Layout → Designing for Stability |
| C++ library maintainer | Foundations → C++ ABI → Type Layout → Transitive Breaks → Designing for Stability |
| CI / release engineer | Product Contract → Detecting Breaks → Tool Comparison → Policy Profiles → Baselines → Exit Codes → Output Formats |
| Distribution / package maintainer | Linker & ELF → Transitive Breaks → Multi-Binary Releases → Application Compatibility |
| Plugin / SDK author | Symbol Contracts → Plugin Systems → Policy Profiles → Product Contract §4 |
| AI agent / automated reviewer | Overview → Evidence & Detectability → Examples Encyclopedia → Change Kind Reference |
Break families at a glance¶
Every detected change maps to one of these families. The verdict column shows the
typical classification; the exact verdict per fixture lives in
examples/ground_truth.json and the Examples Encyclopedia.
The Part column points to where the mechanism is explained.
Case numbers link straight to the generated example page; the Typical verdict column says "mixed" where the verdict is case-dependent (the per-fixture verdict is the source of truth).
| 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 | Part 4 §Modern |
| 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 |
Of these rows 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 (which reads L0/L1/L2 evidence — no L4 source
replay required; the audit fixtures resolve at L0/L2). All five new rows are the
subject of the
level-by-level walk-through
and the source-scan section below.
The one idea to carry through the whole series¶
If you remember nothing else:
The compiler bakes the library's ABI facts — sizes, offsets, register choices, vtable slot numbers, symbol names — into every caller, as immediate constants, and never re-checks them. When the library changes one of those facts in a later release, the old caller keeps using the old number. Nobody re-validates it. That is why an ABI break is silent: no linker error, often no crash, just wrong bytes at the wrong address.
Every fix in Part 7 is therefore a variation on a single move: stop publishing the fact — hide it behind a pointer, a version node, or hidden visibility — so you stay free to change it.
abicheck exists to catch these breaks before they ship: it dumps a snapshot of each binary, diffs them structurally, and classifies every difference into one of five verdicts mapped to CI exit codes. See Part 1 §7 for how that pipeline works, and Verdicts for the exit-code semantics.
Runtime calls are not the same as ABI dependencies¶
A public entry point may call a long chain of private helpers at runtime — but that runtime call graph is not automatically the consumer's ABI contract. Existing binaries are bound only to what crosses the compile / link / load boundary: installed public headers, symbols the consumer object directly references, and what the loader must resolve. An internal helper is safe to change while it stays behind that boundary, and becomes contract the moment a public inline body references it or a public header exposes its type by value.
➡️ The full deep-dive — the safe/unsafe boundary shifts, the six-point
private-change safety checklist, and how public-surface scoping and scan's
baseline-free audit mode (just omit --against) check the boundary — is on
its own page: What Is Part of Your ABI Surface?
App-swap (ASW): the consumer-scoped runtime check¶
The most realistic consumer-level test is application software swap (ASW) —
build an app against the old library, drop in the new one, and run it. abicheck
exposes this as compare --used-by APP (folding
the former standalone appcompat command). ASW is
consumer-scoped compatibility; library compare/scan is
contract-scoped — use both. What app mode can and cannot conclude, and how
it compares to the other methods (libabigail, ABICC, bundle scan), is in
Evidence & Detectability §4.
Feed abicheck .so + debug info + headers for the best result¶
abicheck's analysis tiers are additive, and the highest-coverage setup is a single comparison of debug-enabled libraries with their public headers supplied:
abicheck compare libfoo_v1.so libfoo_v2.so \
--header old=include/v1/foo.h --header new=include/v2/foo.h # both built with -g
Comparing a stripped binary with no headers yields only symbol add/remove
coverage and silently misses every layout and source-level break. The
governing principle: more evidence cuts both error kinds — each layer
you add reduces false negatives and false positives, and more evidence may
scope away a false positive but never hide an artifact-proven break (the
authority rule). What each layer contributes, the tracked per-tier FP/FN
matrix, and the --depth dial that collects the layers are all on the model
page: Evidence & Detectability.
Which input proves which family — and what each level actually sees¶
The three artifact tiers above (L0–L2) are only half the picture, and which input first reveals a given change is worth seeing concretely rather than in the abstract. That entire story — the summary matrices (artifact L0–L2 and source-scan L3–L5) and a single tiny library walked up every evidence level so you can watch each change appear or stay invisible — now lives on its own digestible, diagram-driven page:
➡️ What Each Level Sees — a level-by-level walk-through
The short version, if you only remember one row per level:
| Level | Newly reveals | Blind to |
|---|---|---|
| L0 symbols | symbol add/remove/rename, SONAME, versioning, visibility | anything that keeps the symbol name |
| L1 debug | struct/enum layout, offsets, vtables, calling convention | source intent, macros, public-vs-internal |
| L2 headers | signatures, access, noexcept, default-arg & constexpr values, public scoping |
#define macros, inline/template bodies |
| L3 build | ABI-relevant flags & toolchain (-std, _GLIBCXX_USE_CXX11_ABI) |
anything inside the source |
| L4 sources | macro / constexpr values, inline/template/uninstantiated bodies |
the layout actually emitted (L1's job) |
| L5 graph | reachability / impact ranking | proves nothing on its own — it prioritizes |
Two rules the walk-through makes concrete: no single level sees every change
(a stripped-binary L0 compare calls a genuinely breaking release "clean"), and
the authority rule — the artifact tiers (L0–L2) set any BREAKING gate, while
the build/source tiers (L3–L5) add findings and explanation but never manufacture
or delete a proven binary break. abicheck tracks each layer's FP/FN contribution
as a CI gate — see
Evidence & Detectability → What each layer buys.
Going deeper than artifacts: the source scan¶
Artifact comparison (L0–L2) proves what the shipped binary did. To recover the
source-only facts it cannot see — #define macros, constexpr values,
default-argument values, inline/template bodies, uninstantiated templates —
abicheck can read the build's compile database (L3) and replay the sources
(L4), and fold a source/build reachability graph (L5). The one-shot
driver is abicheck scan. It has one evidence dial — --depth
(binary|headers|build|source) — that selects how far down the L0–L5
evidence layers (what it sees + authority) to collect; fully explained in
Evidence & Detectability. The governing
authority rule: source/build evidence (L3/L4/L5) explains, localizes, scopes,
or raises its own source-/API-level findings, but never deletes an
artifact-proven break.
Orthogonal to depth, omitting --against runs scan in its intra-version
single-build hygiene lint that needs no previous version: accidental exports,
private-header leaks, unversioned symbols, exported RTTI for internal types,
and cross-source mismatches (there is no separate --audit flag — --against
alone selects between the two modes). The worked cases (case143–151) with
commands are in
Source-Scan Depth § single-build audit;
their throughline is that a finding invisible or ambiguous to any single
source resolves only by crosschecking two, and that a scan always states the
depth it actually reached
(case147) — never a bare
"scan failed".
The L5 graph: reachability, not just structure¶
L0–L4 answer what changed: a struct grew a field, a function's signature
changed, a macro's value differs. None of them answer a different question
that turns out to matter just as much: can anything outside this library
actually observe that change? A change buried in a namespace called
detail/impl/internal (by convention, never part of the documented API)
is usually safe to ship without a version bump — unless some public,
consumer-facing declaration depends on it, in which case the "internal"
label is a fiction and the change is exactly as breaking as if it were
public. Telling these two cases apart requires reachability: a graph walk
from the public surface, not a flat list of what's public and what isn't.
That graph is L5 — built by folding two kinds of edges on top of the L4
source-fact surface (--depth source, or the standalone abicheck scan):
- Structural edges —
TYPE_HAS_FIELD_TYPE,TYPE_INHERITS,DECL_HAS_TYPE— a public type embeds, inherits from, or is a field/base of an internal one. This is the same relationshipcompute_leak_paths(the type-layout leak walk) already followed before L5 existed; the graph just makes it explicit and queryable. - Behavioral edges —
DECL_CALLS_DECL/DECL_REFERENCES_DECL— one declaration's body calls or references another. This is the genuinely new capability: it answers "does the compiled code a consumer links against actually execute a call into this internal symbol?", a question no amount of type-layout analysis can answer, because a call is not a type relationship.
Nodes carry visibility (public_header/private_header/source/…) and,
where the L4 surface can tell, decl_kind (ordinary declaration vs.
inline/template) — the signal that decides whether a declaration's own
body is compiled into your binary or only into the library's. Edges are
built from a real build (--sources/--build-info, Clang AST calls/refs) or,
with zero build integration, from a header-only Clang AST tree (built
automatically for --depth headers and above, since G29 Phase A) that
captures in-header inline/template bodies. Either
way, evidence degrades gracefully: no graph, no compiler, no relevant
edges — every feature described below simply sees nothing and stays silent,
per the standing authority rule (L3–L5 evidence explains or corroborates an
artifact-proven break; it never manufactures or deletes one).
Why this distinction matters: the dispatcher scenario¶
The scenario that motivated building this (ADR-044) is a shape real numerics/ML libraries hit constantly — a public inline dispatcher:
// public header — compiled into every consumer's own translation unit
namespace mylib {
inline Result compute(const Descriptor& d) {
return detail::compute_avx2(d); // dispatches to an internal specialization
}
}
detail::compute_avx2 lives in the detail namespace — by convention,
internal. A blanket suppression rule for namespace: "mylib::detail::**"
looks obviously correct: nothing in detail is part of the documented API.
But compute() is inline, so its body — including the call to
compute_avx2— is compiled directly into every consumer's binary. If
compute_avx2's signature changes or it's removed, every consumer that
already compiled against the old header now calls a symbol that no longer
exists (or exists with an incompatible signature) — a real, consumer-visible
break, hiding behind an internal-sounding name. Type-layout analysis alone
cannot catch this: nothing about compute_avx2's type changed from
compute()'s point of view, because the dependency is a call, not a field
or base class.
This is exactly what the L5 call-graph walk is for. abicheck walks
DECL_CALLS_DECL/DECL_REFERENCES_DECL from every entry whose own body is
actually compiled into consumer code, and if that walk reaches the changed
detail::compute_avx2, the change is tagged public_reachable with
reachability_kind: symbol_availability and a proof path
(compute() --[DECL_CALLS_DECL]--> detail::compute_avx2()) before
suppression rules ever run. A broad namespace: "mylib::detail::**" rule is
then refused for this specific change (a suppression_would_hide_public_break
diagnostic explains why, naming the exact path) unless the rule author
explicitly opts in with allow_public_break: true — turning a silent,
accidental hole in the compatibility contract into a reviewable decision. See
Suppressions § Reachability-aware suppression
for the full mechanics (the reachability rule setting, the diagnostic
format) and ADR-044
for the design rationale.
What it does not flag — staying quiet on the common case¶
The flip side matters equally: most functions in most libraries are ordinary, out-of-line, non-inline, non-template — an ordinary ".cpp-defined" exported function whose body compiles into the library's binary only. If that function's out-of-line body calls an internal helper, and the helper is later removed, no consumer is affected: either the library's own build fails first (a vendor-side problem, caught by their own CI, invisible to anyone downstream), or the recompiled function simply stops making that call — a consumer only ever links against the function's own exported symbol and never sees, references, or embeds the helper it happens to call internally. abicheck's call-graph walk only seeds itself from entries whose own body is actually emitted into consumer code (inline functions/methods and templates) — an ordinary exported function is public, but is not one of these entries, so a private helper it calls is never reported as "reachable" through it. A project can suppress broad internal-namespace churn with confidence that the tool is not silently blocking the common case while chasing the rare one.
A safe pimpl (pointer-to-implementation) class follows the same logic from
the type-layout side: an internal Impl type reached only through a public
class's pointer member (std::unique_ptr<Impl>) can freely change its
internal layout, because consumers never see Impl's size or fields — only
the pointer, whose own size/alignment is stable. The call-graph and
type-layout walks agree here: neither reaches into Impl's internals unless
something public actually embeds it by value or calls into it from an
inline/template body.
Other scenarios the L5 graph catches¶
Beyond the headline dispatcher case, the same graph backs several narrower, intra-version and cross-version findings:
internal_symbol_required_by_public_api— the inverse framing of the dispatcher scenario as its own finding: a public declaration's dependency on a specific internal symbol, surfaced even when nothing changed yet, so a reviewer can see the coupling before it becomes a break.public_api_internal_dependency_added(crosscheck.py, intra-version) — a single-build hygiene check: a public declaration or type newly depends on an internal one, elevated when the internal side is among the files a PR actually touched. No previous version is needed for this one — it is a same-build "does your public surface quietly depend on something unexported?" audit, useful even on a first release.call_graph_public_entry_reachability_changed— the set of internal declarations reachable from an exported entry point changed shape between versions, independent of whether any individual declaration's own diff triggered a break — a widening or narrowing of the effective public surface as a fact worth surfacing on its own.include_graph_public_header_drift/build_option_reaches_public_symbol— the graph also folds in include-file and build-option edges, so a public header quietly pulling in a different private header, or a build flag that reaches all the way to a public symbol's ABI-relevant layout, shows up the same way a call-graph leak does.
All of these are RISK_KINDS or (for the intra-version hygiene checks)
advisory findings — per the authority rule, L5 evidence never manufactures a
BREAKING verdict on its own. What it changes is whether a break that is
independently proven (an exported symbol actually removed, an exported
type's layout actually changed) gets correctly attributed to the public
surface instead of silently absorbed by an internal-looking suppression
rule.
Now run it — the practical flow, plugin, and CI guides¶
This page is the concept. When you are ready to enable a source scan on a real project, the tool-track guides carry the exact commands, flags, and CI YAML:
| You want to… | Go to |
|---|---|
| Pick the right command for your situation (binary compare → full source scan → combine evidence → plugin) | Choose Your Workflow |
Run abicheck scan and pin a depth |
Source-Scan Depth |
Produce the source facts — post-build replay (Full source scan), abicheck-cc wrapper (Wrapper injection), or the Clang plugin (Plugin injection) |
Producing Source Facts |
| Fold build/source evidence into a baseline snapshot | Source & Build Data |
Wire a full source scan into GitHub Actions — sources/build-info/depth, audit, estimate, cross-check gating |
GitHub Action: Source Scans |
| Check a host↔plugin ABI contract | Plugin Systems |
| Gate CI on the right verdict tier (binary break vs. source/API break) | CI Gating |
The CI recipes there go beyond the binary-only compare: a minimal PR scan is
four inputs (binary + headers + sources: . + baseline), and the same guide
shows enabling each source layer independently — depth: build for cheap L3
build-flag drift, depth: source for full L4 replay plus the change-scoped L5
graph, and the sources/build-info inputs auto-detecting a build-emitted
(abicheck-cc / Clang plugin) abicheck_inputs/ pack directly — no separate
merge step.
Detection coverage and roadmap¶
abicheck detects a broad and growing set of change kinds (see the
Change Kind Reference for the exact,
up-to-date count), spanning every family in
the table above — including the calling-convention, alignment/packing, bit-field,
dual-ABI (_GLIBCXX_USE_CXX11_ABI), ABI-tag, char8_t, _BitInt, _Atomic,
and CPU-dispatch cases. Areas still deepening: richer cross-compiler ABI-drift
modelling (GCC vs Clang vs MSVC for the same headers) and LTO/visibility
interactions where an inlined symbol disappears. The authoritative, always-current
taxonomy is the generated Change Kind Reference
and Examples Encyclopedia.
➡️ Start the series: Part 1 — Foundations