Disclaimer: ChatGPT generated document.
The Application Binary Interface (ABI) defines how programs interact at the binary level. It is crucial for compatibility, function calling conventions, object layout, name mangling, and exception handling in C and C++.
Understanding the ABI is important for:
✅ Linking object files from different compilers
✅ Ensuring binary compatibility
✅ Interfacing with assembly code
✅ Writing libraries that work across multiple compiler versions
🔹 An ABI (Application Binary Interface) is a contract between compiled code and the system that specifies:
- Function calling conventions (how arguments and return values are passed)
- Name mangling (how function and class names are represented in object files)
- Memory layout of structs, classes, and objects
- Exception handling mechanisms
- Virtual function tables (vtables)
- Stack alignment and register usage
🔹 Think of ABI as the "binary rules" for how compiled code should behave.
| Aspect | API (Application Programming Interface) | ABI (Application Binary Interface) |
|---|---|---|
| Definition | Specifies how to use functions and classes | Specifies how functions and objects interact at the binary level |
| Scope | Source code compatibility | Binary compatibility |
| Affected by | Header files, function signatures | Compiler internals, calling conventions |
| Changes when | Function signatures or class definitions change | Compiler version or optimization settings change |
🔹 API is about source code compatibility → If API changes, recompilation is required.
🔹 ABI is about binary compatibility → If ABI changes, recompilation is NOT enough; everything must be rebuilt.
Function calling conventions define how function arguments are passed and how return values are handled. Different architectures and compilers use different conventions.
| Calling Convention | Description | Used By |
|---|---|---|
| cdecl (C Declaration) | Arguments pushed right to left, caller cleans the stack | Most C/C++ programs on x86 |
| stdcall (Standard Call) | Arguments pushed right to left, callee cleans the stack | Windows API functions |
| fastcall | First few arguments passed via registers, rest on stack | Performance optimization |
| thiscall | this pointer passed in ECX, rest like stdcall |
C++ instance methods on Windows |
| vectorcall | Passes floating-point args via vector registers | Windows, optimized for SIMD |
extern "C" void __stdcall foo(int, int); // Uses stdcall convention
extern "C" void __cdecl bar(int, int); // Uses cdecl convention
extern "C" void __fastcall baz(int, int); // Uses fastcall convention🚨 Mixing calling conventions in linked libraries leads to undefined behavior!
C++ supports function overloading, so the compiler mangles function names to make them unique.
// C++ Code
void foo(int);
void foo(double);GCC generates the following mangled names:
_Z3fooi // foo(int)
_Z3food // foo(double)
Clang and MSVC may generate different mangled names!
🚀 Use extern "C" to disable name mangling when linking with C libraries.
extern "C" void foo(int); // No name mangling, safe for linking with C🔹 ABI incompatibility occurs when different compilers use different name mangling schemes.
C++ classes and structs have specific memory layouts defined by the ABI.
struct A {
int x;
char y;
double z;
};Depending on the ABI:
- Padding bytes may be inserted to align
double zcorrectly. - Different compilers may place
ydifferently in memory.
🚨 Different compilers or compiler versions may layout structs differently, breaking ABI compatibility!
C++ virtual functions use vtables to support dynamic dispatch.
struct Base {
virtual void foo();
int x;
};
struct Derived : Base {
void foo() override;
};The ABI defines how vtables are laid out:
Base:
[ vptr ] -> [ &Base::foo ]
[ x ]
Derived:
[ vptr ] -> [ &Derived::foo ] // Overrides Base::foo
[ x ]
🔹 Each compiler may use a different vtable layout!
🔹 Breaking ABI can cause undefined behavior when using shared libraries.
C++ exceptions rely on runtime support from the ABI.
- Try block is entered.
- If an exception is thrown, a special runtime mechanism (
__cxa_throwin GCC) looks for a matching catch block. - Stack unwinding happens to free resources.
🚨 Different compilers (GCC vs. MSVC) have different exception handling mechanisms:
- GCC and Clang use Itanium ABI (
__cxa_throw,__cxa_begin_catch). - MSVC uses Structured Exception Handling (SEH).
🔹 Mixing exception handling across ABIs leads to crashes!
| Issue | Cause | Solution |
|---|---|---|
| Linking object files compiled with different compilers | Different name mangling, vtable layout | Use the same compiler for all object files |
| Mixing debug and release builds | Different struct alignment and optimizations | Use the same build settings |
| Using different standard libraries (libstdc++ vs. libc++) | Different internal memory layouts | Ensure consistent standard libraries |
| Exception handling mismatch (GCC vs. MSVC) | Different EH implementations | Use the same compiler family |
-
Use
extern "C"for stable function signaturesextern "C" void my_function(int);
-
Avoid exposing STL containers in shared library APIs
(they are ABI-unstable!)
std::vector<int> getNumbers(); // ❌ BAD! Different compilers have different layouts. void getNumbers(std::vector<int>& out); // ✅ BETTER!
-
Use
std::unique_ptrandstd::shared_ptrcarefully across binary boundaries. -
Follow Itanium ABI for GCC/Clang (if using Linux).
-
On Windows, use MSVC-compatible settings.
| Concept | Description |
|---|---|
| ABI | Defines binary compatibility between compiled code |
| Calling Conventions | Define how arguments are passed between functions |
| Name Mangling | Ensures unique function names for overloaded functions |
| Object Layout | Defines how struct/class members are arranged in memory |
| Virtual Tables | Enable polymorphism, but their layout is ABI-dependent |
| Exception Handling | Different compilers use different mechanisms for stack unwinding |
| Compatibility Issues | Occur when mixing compilers, STL, or exception handling implementations |
🚀 Understanding ABI ensures compatibility between shared libraries and compiled programs!
ABI mismatches can cause linking errors, crashes, and undefined behavior when using compiled libraries. We’ll go step by step through debugging ABI issues using nm, objdump, and c++filt.
Before debugging, let’s see real-world ABI mismatch errors.
/usr/bin/ld: undefined reference to `_Z3fooi`
collect2: error: ld returned 1 exit status🔹 Cause: Function names don’t match between object files.
🔹 Solution: Use nm to inspect symbol names.
Segmentation fault (core dumped)🔹 Cause: Different compilers use different vtable layouts.
🔹 Solution: Use objdump to inspect vtables.
nm lists symbols in object files to detect name mangling issues.
// library.cpp
#include <iostream>
void foo(int x) {
std::cout << "foo: " << x << "\n";
}Compile:
g++ -c library.cpp -o library.oCheck symbols:
nm library.o🔹 Sample Output:
0000000000000000 T _Z3fooi # Mangled name for foo(int)
🚀 Solution: Use c++filt to demangle names!
c++filt _Z3fooi🔹 Output:
foo(int)
🔹 If linking fails due to a mismatched name, check whether the symbol exists in the library.
If a segmentation fault happens with polymorphism, inspect the vtable layout.
// class_example.cpp
#include <iostream>
class Base {
public:
virtual void foo() { std::cout << "Base::foo\n"; }
};
class Derived : public Base {
public:
void foo() override { std::cout << "Derived::foo\n"; }
};Compile:
g++ -c class_example.cpp -o class_example.oUse objdump:
objdump -t class_example.o | grep vtable🔹 Sample Output:
0000000000000000 V vtable for Base
0000000000000008 V vtable for Derived
🚨 If two object files have different vtable layouts, the binary ABI is mismatched!
ldd --version🔹 If different versions of libstdc++ are linked, ABI compatibility issues occur.
ldd my_program🔹 If the library is incompatible, recompile everything with the same GCC version.
| Tool | Purpose | Usage Example |
|---|---|---|
nm |
Inspect function symbols | nm library.o |
c++filt |
Demangle C++ names | c++filt _Z3fooi |
objdump |
Inspect vtables and symbols | objdump -t class_example.o |
ldd |
Check linked shared libraries | ldd my_program |
🚀 Using these tools, you can debug ABI mismatches and fix linking issues efficiently.
When a shared library (.so on Linux, .dll on Windows) is incompatible with the compiled binary, it can cause issues like:
✅ Undefined symbol errors
✅ Segmentation faults
✅ Runtime crashes due to mismatched function signatures
In this guide, we’ll debug a real-world shared library issue using ldd, objdump, nm, and gdb.
Imagine we have a shared library (libmath.so) and a program (main.cpp) that uses it.
// libmath.cpp
#include <iostream>
void add(int a, int b) {
std::cout << "Sum: " << a + b << "\n";
}Compile as a shared library:
g++ -shared -fPIC -o libmath.so libmath.cpp// main.cpp
#include <iostream>
extern void add(int, int);
int main() {
add(5, 10);
return 0;
}Compile:
g++ -o main main.cpp -L. -lmath🚀 Runs fine now! But what if libmath.so is updated and the function signature changes?
Now, let’s change libmath.cpp to modify add():
// Updated libmath.cpp (Breaking ABI)
#include <iostream>
void add(double a, double b) { // CHANGED: int → double
std::cout << "Sum: " << a + b << "\n";
}Recompile:
g++ -shared -fPIC -o libmath.so libmath.cppNow, running ./main without recompiling gives:
./main: symbol lookup error: ./main: undefined symbol: add
🚨 The function add(int, int) is missing because the signature changed!
Let's debug this using nm, ldd, and objdump.
ldd shows which shared libraries are loaded at runtime.
ldd ./mainSample output:
libmath.so => ./libmath.so (0x00007f8e50a2f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8e50740000)
✅ libmath.so is correctly linked, but we need to check if the expected symbol exists.
nm lists all functions inside libmath.so.
nm -D libmath.so | grep add🔹 Before modifying libmath.cpp, the output was:
0000000000001130 T _Z3addii # Mangled name for add(int, int)
🔹 After modifying libmath.cpp, the output is:
0000000000001140 T _Z3adddd # Mangled name for add(double, double)
🚨 The original symbol _Z3addii is missing!
Fix: Recompile main.cpp to match the new signature.
objdump -T shows exported symbols in shared libraries.
objdump -T libmath.so | grep addOutput:
0000000000001140 w DF .text 0000000000000010 Base _Z3adddd
✅ Confirms that add(double, double) exists, but add(int, int) is gone.
If the program crashes, we can use gdb:
gdb ./mainInside gdb, run:
runExpected output:
Program received signal SIGSEGV, Segmentation fault.
Check the backtrace:
bt🚀 If the crash is due to an incorrect function signature, ABI mismatch is confirmed.
g++ -o main main.cpp -L. -lmath✅ This ensures symbols match.
Instead of breaking the ABI, rename the new function:
extern "C" void add(int a, int b) { // Preserve old function
std::cout << "Sum: " << a + b << "\n";
}
extern "C" void add_double(double a, double b) { // New function
std::cout << "Sum: " << a + b << "\n";
}✅ Now, add(int, int) remains, preventing ABI breaks.
| Tool | Usage | Example |
|---|---|---|
ldd |
Check linked shared libraries | ldd ./main |
nm -D |
List symbols in a shared library | nm -D libmath.so |
objdump -T |
Inspect exported functions | objdump -T libmath.so |
gdb |
Debug runtime crashes | gdb ./main |
🚀 With these tools, you can detect and fix ABI mismatches efficiently.
When developing shared libraries (.so files in Linux), breaking ABI compatibility can lead to runtime errors and crashes. Symbol versioning allows us to maintain backward compatibility while evolving a library.
✅ Prevents symbol conflicts
✅ Allows multiple versions of a function to exist
✅ Ensures older binaries still work with newer libraries
Let’s assume we have a shared library, libmath.so, that exports a function add(int, int). Later, we modify it to use double instead of int, breaking ABI compatibility.
#include <iostream>
extern "C" void add(int a, int b) {
std::cout << "Sum: " << a + b << "\n";
}Compile as a shared library:
g++ -shared -fPIC -o libmath.so libmath.cpp#include <iostream>
extern "C" void add(int, int);
int main() {
add(5, 10);
return 0;
}Compile and link:
g++ -o main main.cpp -L. -lmath✅ Runs fine!
Now, we update libmath.cpp:
#include <iostream>
extern "C" void add(double a, double b) { // CHANGED: int → double
std::cout << "Sum: " << a + b << "\n";
}Recompile:
g++ -shared -fPIC -o libmath.so libmath.cpp🚨 Now running ./main fails with:
./main: symbol lookup error: ./main: undefined symbol: add
Why?
- The function
add(int, int)is gone! - The program
mainexpects_Z3addii, butlibmath.soonly provides_Z3adddd.
Symbol versioning allows both old and new functions to coexist.
LIBMATH_1.0 {
global:
add;
local:
*;
};LIBMATH_1.0defines a version.global: add;exposesadd(int, int).local: \*;hides other internal symbols.
We create two versions of add(), preserving the old ABI while adding a new one.
#include <iostream>
extern "C" {
void add(int a, int b) { // Old version
std::cout << "Sum (int): " << a + b << "\n";
}
void add_double(double a, double b) { // New version
std::cout << "Sum (double): " << a + b << "\n";
}
}✅ Now both add(int, int) and add(double, double) exist.
g++ -shared -fPIC -o libmath.so libmath.cpp -Wl,--version-script=libmath.map🚀 This keeps add(int, int) available for older programs while adding add(double, double).
Use nm -D to list exported symbols:
nm -D libmath.so | grep add🔹 Before symbol versioning:
0000000000001130 T _Z3addii
🔹 After symbol versioning:
0000000000001130 T _Z3addii (LIBMATH_1.0)
0000000000001140 T _Z3adddd
✅ Older binaries using add(int, int) still work!
✅ Newer binaries can use add(double, double).
Check the exported versions in libmath.so:
objdump -T libmath.so | grep add🔹 Output:
0000000000001130 w DF .text 0000000000000010 LIBMATH_1.0 add
0000000000001140 w DF .text 0000000000000010 Base add_double
✅ Confirms that add(int, int) is still available under version LIBMATH_1.0.
| Issue | Fix |
|---|---|
| Removing an old function breaks ABI | Keep old symbols with symbol versioning |
| Symbol mismatch causes runtime errors | Use nm -D and objdump -T to inspect symbols |
| Older programs crash with new libraries | Define versioned symbols using a .map script |
🚀 Symbol versioning ensures binary compatibility across library versions.
On Windows, breaking ABI compatibility in a shared library (.dll) can lead to:
✅ Missing function errors (The procedure entry point could not be located)
✅ Crashes due to incompatible function signatures
✅ DLL versioning issues when updating a library
To maintain binary compatibility, we use module definition files (.def files), similar to symbol versioning on Linux.
Let’s assume we have a DLL (math.dll) exporting a function add(int, int). Later, we modify it to add(double, double), breaking ABI compatibility.
#include <iostream>
__declspec(dllexport) void add(int a, int b) {
std::cout << "Sum: " << a + b << "\n";
}✅ Compile the DLL:
cl /LD math.cpp /Fe:math.dll✅ Create a program (main.cpp) that uses the DLL:
#include <iostream>
__declspec(dllimport) void add(int, int);
int main() {
add(5, 10);
return 0;
}✅ Compile the main program:
cl main.cpp /link math.lib✅ Runs fine! But what happens if we change add(int, int)?
Now, we modify math.cpp:
#include <iostream>
__declspec(dllexport) void add(double a, double b) { // CHANGED: int → double
std::cout << "Sum: " << a + b << "\n";
}Recompile:
cl /LD math.cpp /Fe:math.dll🚨 Now running main.exe gives:
The procedure entry point add could not be located in the dynamic link library math.dll.
Why?
- The original symbol
add(int, int)is gone. - The program expects
_Z3addii, butmath.dllnow provides_Z3adddd.
A .def file explicitly defines exported functions, preventing them from being removed in later versions.
LIBRARY math
EXPORTS
add@8
LIBRARY math→ Defines the DLL name.EXPORTS add@8→ Forcesadd(int, int)to be included.
🚀 The @8 is the *stdcall name decoration* (2 arguments × 4 bytes each).
#include <iostream>
extern "C" __declspec(dllexport) void add(int a, int b) { // Old version
std::cout << "Sum (int): " << a + b << "\n";
}
extern "C" __declspec(dllexport) void add_double(double a, double b) { // New version
std::cout << "Sum (double): " << a + b << "\n";
}✅ Recompile with .def file:
cl /LD math.cpp /Fe:math.dll /DEF:math.def✅ Now add(int, int) still exists, and add(double, double) is available.
Use dumpbin to inspect DLL symbols:
dumpbin /exports math.dll🔹 Before breaking ABI:
ordinal hint RVA name
1 0 00001000 add@8
🔹 After using .def file:
ordinal hint RVA name
1 0 00001000 add@8
2 1 00001010 add_double@16
✅ Confirms add(int, int) is still available.
| Issue | Fix |
|---|---|
| Removing an old function breaks ABI | Keep old symbols with a .def file |
| Symbol mismatch causes DLL errors | Use dumpbin /exports to inspect symbols |
| Older programs crash with new DLLs | Use extern "C" and .def files to maintain compatibility |
🚀 With .def files, we prevent DLL-breaking changes and ensure older programs still work.
When a Windows application fails to load a DLL, it often results in errors like:
✅ "The procedure entry point could not be located"
✅ "DLL not found"
✅ "Missing dependencies"
✅ Crashes due to version mismatches
We will use Dependency Walker, dumpbin, and WinDbg to debug DLL issues.
| Error Message | Cause | Solution |
|---|---|---|
"The procedure entry point XYZ could not be located in the DLL" |
Symbol mismatch (ABI breakage) | Use dumpbin /exports to check symbols |
"DLL not found" |
Missing DLL | Use Dependency Walker (depends.exe) |
"DLL file not found or corrupt" |
Wrong DLL version | Check C:\Windows\System32 and C:\Windows\SysWOW64 |
Dependency Walker (depends.exe) shows which DLLs an executable requires.
🔹 Download: Dependency Walker
🔹 Install and open depends.exe.
- Open
depends.exe - Click File → Open
- Select
main.exe - Check for missing DLLs (red X) in the Module List.
🔹 Missing Dependency:
Error: MSVCR120.dll was not found
🔹 Fix: Install the correct Visual C++ Redistributable:
choco install vcredist2013If a DLL is present but the function is missing, use dumpbin to inspect exports.
dumpbin /exports math.dll🔹 Expected Output:
ordinal hint RVA name
1 0 00001000 add@8
2 1 00001010 add_double@16
🚨 If add@8 is missing, the DLL ABI has changed!
✅ Option 1: Recompile main.cpp with the new DLL
✅ Option 2: Use .def files to preserve old symbols
If an application crashes when loading a DLL, use WinDbg.
choco install windbg- Open WinDbg
- Click File → Open Executable
- Select
main.exe - Run the program inside
WinDbg
Use the following command in WinDbg:
lm🔹 This lists all loaded modules (DLLs).
🔹 If math.dll is missing, Windows couldn’t find it.
| Issue | Fix |
|---|---|
| Missing DLL | Use Dependency Walker to check dependencies |
| Wrong DLL version | Use dumpbin /exports to verify functions |
| ABI mismatch | Use .def files to maintain function names |
| DLL not loading | Use WinDbg and lm to check loaded modules |
🚀 Now you can debug DLL loading issues in Windows!
Would you like an example of loading DLLs dynamically with LoadLibrary() and GetProcAddress()? 😊
