Skip to content

Instantly share code, notes, and snippets.

@MangaD
Last active February 5, 2026 15:24
Show Gist options
  • Select an option

  • Save MangaD/506a0f3273724ef3af26b8c085accdcb to your computer and use it in GitHub Desktop.

Select an option

Save MangaD/506a0f3273724ef3af26b8c085accdcb to your computer and use it in GitHub Desktop.
Comprehensive Guide to the Application Binary Interface (ABI) in C and C++

📌 Comprehensive Guide to the Application Binary Interface (ABI) in C and C++

CC0

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


📌 1. What Is an ABI?

🔹 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.


📌 2. ABI vs. API: What’s the Difference?

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.


📌 3. Function Calling Conventions

Function calling conventions define how function arguments are passed and how return values are handled. Different architectures and compilers use different conventions.

✔️ Common Calling 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

✔️ Example: How Different ABIs Affect Function Calls

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!


📌 4. Name Mangling (C++ ABI)

C++ supports function overloading, so the compiler mangles function names to make them unique.

✔️ Example of Name Mangling

// 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.


📌 5. Object Layout and Class ABI

C++ classes and structs have specific memory layouts defined by the ABI.

✔️ Example: Struct Layout

struct A {
    int x;
    char y;
    double z;
};

Depending on the ABI:

  • Padding bytes may be inserted to align double z correctly.
  • Different compilers may place y differently in memory.

🚨 Different compilers or compiler versions may layout structs differently, breaking ABI compatibility!


📌 6. Virtual Tables (vtables)

C++ virtual functions use vtables to support dynamic dispatch.

✔️ Example: Virtual Function Layout

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.


📌 7. Exception Handling ABI

C++ exceptions rely on runtime support from the ABI.

✔️ How Exception Handling Works

  1. Try block is entered.
  2. If an exception is thrown, a special runtime mechanism (__cxa_throw in GCC) looks for a matching catch block.
  3. 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!


📌 8. Common ABI Compatibility Issues

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

📌 9. Ensuring ABI Stability

  1. Use extern "C" for stable function signatures

    extern "C" void my_function(int);
  2. 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!
  3. Use std::unique_ptr and std::shared_ptr carefully across binary boundaries.

  4. Follow Itanium ABI for GCC/Clang (if using Linux).

  5. On Windows, use MSVC-compatible settings.


📌 10. Summary

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!


📌 Debugging ABI Mismatches Using nm, objdump, and c++filt

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.


📌 1. Common ABI Mismatch Errors

Before debugging, let’s see real-world ABI mismatch errors.

✔️ Error 1: Undefined Symbol

/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.


✔️ Error 2: Segmentation Fault

Segmentation fault (core dumped)

🔹 Cause: Different compilers use different vtable layouts.
🔹 Solution: Use objdump to inspect vtables.


📌 2. Inspecting Symbols with nm

nm lists symbols in object files to detect name mangling issues.

✔️ Example Code

// library.cpp
#include <iostream>

void foo(int x) {
    std::cout << "foo: " << x << "\n";
}

Compile:

g++ -c library.cpp -o library.o

Check 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.


📌 3. Using objdump to Inspect Vtables

If a segmentation fault happens with polymorphism, inspect the vtable layout.

✔️ Example Code

// 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.o

Use 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!


📌 4. Detecting Library ABI Mismatches

✔️ Problem: A library was built with GCC 7, but we are using GCC 11

ldd --version

🔹 If different versions of libstdc++ are linked, ABI compatibility issues occur.

✔️ Fix: Check Which libstdc++.so Is Used

ldd my_program

🔹 If the library is incompatible, recompile everything with the same GCC version.


📌 5. Summary

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.


📌 Debugging an Incompatible Shared Library at Runtime

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.


📌 1. The Problem: A Shared Library Mismatch

Imagine we have a shared library (libmath.so) and a program (main.cpp) that uses it.

✔️ libmath.cpp (Shared Library)

// 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 (Program Using libmath.so)

// 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?


📌 2. Introducing an ABI Incompatibility

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.cpp

Now, 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.


📌 3. Debugging the Issue

✔️ Step 1: Check Linked Libraries Using ldd

ldd shows which shared libraries are loaded at runtime.

ldd ./main

Sample 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.


✔️ Step 2: Check Symbols in libmath.so Using nm

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.


✔️ Step 3: Check Exported Functions with objdump

objdump -T shows exported symbols in shared libraries.

objdump -T libmath.so | grep add

Output:

0000000000001140  w   DF .text  0000000000000010  Base        _Z3adddd

Confirms that add(double, double) exists, but add(int, int) is gone.


✔️ Step 4: Inspect Runtime Errors with gdb

If the program crashes, we can use gdb:

gdb ./main

Inside gdb, run:

run

Expected 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.


📌 4. Fixing the ABI Issue

✔️ Option 1: Recompile Everything

g++ -o main main.cpp -L. -lmath

This ensures symbols match.

✔️ Option 2: Use a Versioned Symbol

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.


📌 5. Summary

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.


📌 Ensuring ABI Stability with Symbol Versioning in Shared Libraries

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


📌 1. The Problem: Breaking ABI Compatibility

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.

✔️ Original Library (libmath.cpp)

#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

✔️ Program Using libmath.so (main.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!


📌 2. Breaking ABI: Changing add(int, int) to add(double, double)

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 main expects _Z3addii, but libmath.so only provides _Z3adddd.

📌 3. Fixing the Issue with Symbol Versioning

Symbol versioning allows both old and new functions to coexist.

✔️ Step 1: Create a Version Script (libmath.map)

LIBMATH_1.0 {
    global:
        add;
    local:
        *;
};
  • LIBMATH_1.0 defines a version.
  • global: add; exposes add(int, int).
  • local: \*; hides other internal symbols.

✔️ Step 2: Modify libmath.cpp

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.

✔️ Step 3: Compile with Symbol Versioning

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).


📌 4. Checking Versioned Symbols in libmath.so

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).


📌 5. Ensuring Backward Compatibility with objdump

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.


📌 6. Summary

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.


📌 Ensuring ABI Stability in Windows DLLs Using .def Files

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.


📌 1. The Problem: Breaking ABI Compatibility in a DLL

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.

✔️ Original DLL Code (math.cpp)

#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)?


📌 2. Breaking ABI: Changing add(int, int) to add(double, double)

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, but math.dll now provides _Z3adddd.

📌 3. Fixing the Issue with a .def File

✔️ Step 1: Create a Module Definition File (math.def)

A .def file explicitly defines exported functions, preventing them from being removed in later versions.

math.def

LIBRARY math
EXPORTS
    add@8
  • LIBRARY math → Defines the DLL name.
  • EXPORTS add@8 → Forces add(int, int) to be included.

🚀 The @8 is the *stdcall name decoration* (2 arguments × 4 bytes each).


✔️ Step 2: Modify math.cpp to Preserve Old Symbols

#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.


📌 4. Checking Exported Symbols in a DLL

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.


📌 5. Summary

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.


📌 Debugging DLL Loading Issues in Windows

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.


📌 1. Common DLL Errors and Their Causes

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

📌 2. Checking DLL Dependencies with Dependency Walker

Dependency Walker (depends.exe) shows which DLLs an executable requires.

✔️ Step 1: Download Dependency Walker

🔹 Download: Dependency Walker 🔹 Install and open depends.exe.

✔️ Step 2: Load Your Executable

  1. Open depends.exe
  2. Click File → Open
  3. Select main.exe
  4. Check for missing DLLs (red X) in the Module List.

✔️ Example

🔹 Missing Dependency:

Error: MSVCR120.dll was not found

🔹 Fix: Install the correct Visual C++ Redistributable:

choco install vcredist2013

📌 3. Checking Exported Symbols with dumpbin

If a DLL is present but the function is missing, use dumpbin to inspect exports.

✔️ Step 1: Check Available Symbols

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!

✔️ Step 2: Fix ABI Mismatches

Option 1: Recompile main.cpp with the new DLL
Option 2: Use .def files to preserve old symbols


📌 4. Debugging DLL Loading Failures with WinDbg

If an application crashes when loading a DLL, use WinDbg.

✔️ Step 1: Install WinDbg

choco install windbg

✔️ Step 2: Attach WinDbg to the Program

  1. Open WinDbg
  2. Click File → Open Executable
  3. Select main.exe
  4. Run the program inside WinDbg

✔️ Step 3: Check DLL Load Failures

Use the following command in WinDbg:

lm

🔹 This lists all loaded modules (DLLs).
🔹 If math.dll is missing, Windows couldn’t find it.


📌 5. Fixing Common DLL Issues

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()? 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment