150 lines
5.1 KiB
Markdown
150 lines
5.1 KiB
Markdown
# Name Collision / ODR Violation with Shared Libraries
|
|
|
|
## The Setup
|
|
|
|
`hello.cpp` defines a class `nt` with a method `print()`, used internally by
|
|
`print_obj()`. `main.cpp` defines a `namespace nt` with a free function
|
|
`print()`, and calls both `nt::print()` and `print_obj()`.
|
|
|
|
`hello.cpp` is compiled into a shared library (`libhello.so` on Linux,
|
|
`libhello.dll` on Windows), and `main.cpp` is linked against it.
|
|
|
|
## The Problem
|
|
|
|
C++ name mangling encodes both **namespaces** and **classes** identically in the
|
|
Itanium ABI (used on Linux):
|
|
|
|
```
|
|
namespace nt { void print() } → _ZN2nt5printEv
|
|
class nt { void print() } → _ZN2nt5printEv
|
|
```
|
|
|
|
Both produce the exact same mangled symbol. The compiler has no way to
|
|
distinguish them at link time.
|
|
|
|
## What Actually Happens
|
|
|
|
With `-O0` (no optimization):
|
|
|
|
- `libhello.so` exports `_ZN2nt5printEv` as a **weak** symbol (the class method)
|
|
- `main` defines `_ZN2nt5printEv` as a **strong/global** symbol (the namespace
|
|
function)
|
|
- When `print_obj()` in the shared library calls `nt::print()`, the dynamic
|
|
linker resolves `_ZN2nt5printEv` at runtime and the **strong symbol in `main`
|
|
wins**
|
|
- Result: `print_obj()` calls `main`'s `nt::print()` instead of the class method
|
|
- Output: `"Hello from namespace"` printed twice
|
|
|
|
With `-O3` (optimizations on):
|
|
|
|
- The compiler inlines the class `nt::print()` directly into `print_obj()`
|
|
- `_ZN2nt5printEv` is never emitted as a standalone symbol in `libhello.so`
|
|
- No collision occurs - each call resolves correctly
|
|
- Output: correct
|
|
|
|
This is why the bug was **optimization-dependent** and hard to spot.
|
|
|
|
## Symbol Evidence
|
|
|
|
```
|
|
# -O0: libhello.so exports the class method as weak
|
|
00000000000011d0 w F .text _ZN2nt5printEv ← weak, can be overridden
|
|
|
|
# -O3: libhello.so does NOT export it at all (inlined away)
|
|
# only _Z9print_objv is present
|
|
```
|
|
|
|
## One Definition Rule (ODR)
|
|
|
|
The C++ standard (basic.def.odr) requires that any entity with more than one
|
|
definition across translation units must have **identical** definitions.
|
|
Violating this is **ill-formed, no diagnostic required** - the compiler and
|
|
linker are not required to warn you.
|
|
|
|
The consequences are undefined behavior: the program may crash, produce wrong
|
|
output, or appear to work correctly depending on compiler flags, optimization
|
|
level, or link order.
|
|
|
|
## Linux Dynamic Linker Behavior
|
|
|
|
On Linux, the dynamic linker (`ld.so`) uses a **flat symbol namespace** by
|
|
default. When a shared library references a symbol, the linker searches all
|
|
loaded objects (including the main executable) and resolves to the **first
|
|
strong symbol found**, regardless of which DSO the symbol logically belongs to.
|
|
|
|
This is different from macOS (which uses two-level namespaces by default) and
|
|
Windows (where DLL symbols are explicitly imported/exported via import tables
|
|
and don't collide this way).
|
|
|
|
This flat namespace behavior means a weak symbol in a `.so` can be silently
|
|
overridden by any strong symbol of the same name anywhere in the process - even
|
|
from the main executable.
|
|
|
|
## Why Windows Is Not Affected
|
|
|
|
On Windows, this collision does not occur. The DLL symbol model is fundamentally
|
|
different:
|
|
|
|
- Only symbols explicitly marked `__declspec(dllexport)` are visible outside the
|
|
DLL - everything else is private by default
|
|
- The linker generates an import library (`.lib`) with stubs that reference the
|
|
DLL by name, so the loader knows exactly which DLL each symbol comes from
|
|
- There is no flat global symbol namespace - each DLL is its own isolated scope
|
|
|
|
This means the internal `class nt` in `hello.cpp` was never at risk on Windows
|
|
regardless of optimization level. The bug is Linux (and ELF) specific.
|
|
|
|
To keep `hello.cpp` portable, the `__declspec(dllexport)` on `print_obj` is
|
|
wrapped in a macro:
|
|
|
|
```cpp
|
|
#ifdef _WIN32
|
|
#define EXPORT __declspec(dllexport)
|
|
#else
|
|
#define EXPORT
|
|
#endif
|
|
```
|
|
|
|
On Linux `EXPORT` expands to nothing. On Windows it marks the symbol for export.
|
|
|
|
## The Linux Fix
|
|
|
|
Wrap internal-use classes in an **anonymous namespace** in `hello.cpp`:
|
|
|
|
```cpp
|
|
namespace {
|
|
class nt {
|
|
public:
|
|
void print() { ... }
|
|
};
|
|
}
|
|
```
|
|
|
|
This gives the class **internal linkage**. The mangled symbol becomes:
|
|
|
|
```
|
|
_ZN12_GLOBAL__N_12nt5printEv
|
|
```
|
|
|
|
The `_GLOBAL__N_1` prefix is the ABI encoding for the anonymous namespace,
|
|
making it unique per translation unit and invisible to the dynamic linker. No
|
|
collision is possible.
|
|
|
|
The Linux and Windows fixes solve the same problem from opposite defaults:
|
|
- Linux: **opt-in to hiding** symbols (anonymous namespace, `-fvisibility=hidden`)
|
|
- Windows: **opt-in to exporting** symbols (`__declspec(dllexport)`)
|
|
|
|
## Key Takeaways
|
|
|
|
- Class and namespace names mangle identically in the Itanium C++ ABI
|
|
- Weak symbols in shared libraries can be silently hijacked by strong symbols in
|
|
the executable
|
|
- ODR violations are UB with no required diagnostic - bugs may only appear at
|
|
certain optimization levels
|
|
- This is a Linux/ELF-specific problem - Windows DLLs use explicit export tables
|
|
and are not affected
|
|
- Internal implementation details in shared libraries should use anonymous
|
|
namespaces to prevent symbol leakage on Linux
|
|
- Use `objdump -t` or `nm` to inspect symbol visibility and catch these issues
|
|
early
|