Skip to content

Case 12: Function Removed from Shared Library

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds func_removed
Source files examples/case12_function_removed/

Category: Symbol API | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

Any binary dynamically linked against v1 fails to resolve fast_add after upgrading to v2. The .so no longer exports the symbol, so pre-built binaries have nowhere to resolve it. Even if the function were moved into a header as an inline, already-compiled binaries can't benefit โ€” they still need the dynamic symbol that used to exist.

Old/new diff

v1.c v2.c
int fast_add(int a, int b) { return a + b; } (removed)
int other_func(int x) { return x; } int other_func(int x) { return x; }

abicheck command

gcc -shared -fPIC -g v1.c -o libfoo_v1.so
gcc -shared -fPIC -g v2.c -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so

Expected abicheck finding

Verdict: BREAKING (exit 4)

- func_removed: Public function removed: fast_add
  > Old binaries call a symbol that no longer exists; dynamic linker will
    refuse to load or crash at call site.

Minimum evidence

min_evidence: L0 โ€” the exported-symbol table alone is enough: fast_add is present in v1's .dynsym and absent from v2's. No debug info or headers needed; -g above is only there so the Runtime failure demonstration below can build a matching app.

Why abicheck catches it

The dynamic symbol table is authoritative L0 evidence โ€” abicheck diffs the exported-symbol sets directly, no debug info or headers required.

Runtime failure demonstration

Severity: CRITICAL

Scenario: app calls fast_add() compiled against v1. v2 removes it from the .so.

# Build v1 + app
gcc -shared -fPIC -g v1.c -o libfoo.so
gcc -g app.c -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ fast_add(3, 4) = 7
# โ†’ other_func(5)  = 5

# Swap in v2 (fast_add gone from .so)
gcc -shared -fPIC -g v2.c -o libfoo.so
./app
# โ†’ ./app: symbol lookup error: ./app: undefined symbol: fast_add

Why CRITICAL: with default lazy binding (RTLD_LAZY), the error surfaces on the first call through the PLT โ€” the app starts but immediately dies when fast_add is called. With LD_BIND_NOW=1 or RTLD_NOW, it fails at load time instead. Either way, every binary that ever called fast_add is broken until recompiled against v2.

Safe redesign

Keep the exported wrapper in the .so even if the implementation moves to an inline โ€” the wrapper can simply call the inline: int fast_add(int a, int b) { return _fast_add_impl(a, b); }. Only remove it on a SONAME-bumping major release.

Real-world example: several C++ standard library implementors have moved functions to inlines for performance and then had to keep exported stubs for ABI compatibility โ€” libstdc++'s std::string refactor in GCC 5 is the canonical cautionary tale.

Cross-tool comparison

abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml
echo "exit: $?"   # โ†’ 12 (= 4 | 8: ABI change detected + breaking change)

References


Source files

  • CMakeLists.txt
  • app.c
  • v1.c
  • v1.h
  • v2.c
  • v2.h

See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.