diff --git a/namecollision/.gitignore b/namecollision/.gitignore index f1b57a5..73ab5e1 100644 --- a/namecollision/.gitignore +++ b/namecollision/.gitignore @@ -1,2 +1,6 @@ libhello.so +libhello.dll +libhello.lib main +main.exe +libhello.exp diff --git a/namecollision/README.md b/namecollision/README.md index dd46a63..90abdf5 100644 --- a/namecollision/README.md +++ b/namecollision/README.md @@ -6,8 +6,8 @@ `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`), and `main.cpp` is -linked against it. +`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 @@ -39,7 +39,7 @@ 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 +- No collision occurs - each call resolves correctly - Output: correct This is why the bug was **optimization-dependent** and hard to spot. @@ -58,7 +58,7 @@ This is why the bug was **optimization-dependent** and hard to spot. 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 +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 @@ -77,10 +77,37 @@ 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 +overridden by any strong symbol of the same name anywhere in the process - even from the main executable. -## The Fix +## 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`: @@ -103,14 +130,20 @@ 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 +- 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 + namespaces to prevent symbol leakage on Linux - Use `objdump -t` or `nm` to inspect symbol visibility and catch these issues early diff --git a/namecollision/build.ps1 b/namecollision/build.ps1 new file mode 100644 index 0000000..c7b789b --- /dev/null +++ b/namecollision/build.ps1 @@ -0,0 +1,15 @@ +param( + [string]$Opt = "-O0" +) + +$ErrorActionPreference = "Stop" + +# Compile hello.cpp as a shared library (also generates libhello.lib import library) +clang++ -std=c++17 $Opt -shared -o libhello.dll hello.cpp "-Wl,/IMPLIB:libhello.lib" +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Compile and link main.cpp with the shared library +clang++ -std=c++17 $Opt -o main.exe main.cpp libhello.lib +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +Write-Host "Build successful" diff --git a/namecollision/hello.cpp b/namecollision/hello.cpp index 6d0a5b2..efcc288 100644 --- a/namecollision/hello.cpp +++ b/namecollision/hello.cpp @@ -1,5 +1,11 @@ #include +#ifdef _WIN32 +#define EXPORT __declspec(dllexport) +#else +#define EXPORT +#endif + using namespace std; class nt { @@ -9,7 +15,7 @@ public: } }; -void print_obj() { +EXPORT void print_obj() { cout << "Hello from function\n"; nt obj; obj.print();