Linking Executables and Libraries
Chapter 8: Linking Executables and Libraries
1. Basics of Linking
Key Points:
- Linking combines object files and libraries into executable binaries
- Two main stages: symbol resolution (matching declarations/definitions) and relocation (memory address assignment)
- Static vs dynamic linking differences:
- Static linking embeds dependencies at compile time
- Dynamic linking resolves dependencies at runtime
CMake Code Example:
# Static library
add_library(MyStaticLib STATIC src/mylib.cpp)
target_include_directories(MyStaticLib PUBLIC include)
# Shared library
add_library(MySharedLib SHARED src/mylib.cpp)
target_include_directories(MySharedLib PUBLIC include)
# Executable linking both
add_executable(MyApp src/main.cpp)
target_link_libraries(MyApp PRIVATE MyStaticLib MySharedLib)
2. Library Types and PIC
Key Points:
- Static libraries (.a/.lib): Compiled into executable
- Shared libraries (.so/.dll): Loaded at runtime
- Position-Independent Code (PIC) required for shared libraries
- CMake automatically enables PIC for shared libs via
POSITION_INDEPENDENT_CODE
Code Implementation:
add_library(MyShared SHARED src.cpp)
set_property(TARGET MyShared PROPERTY POSITION_INDEPENDENT_CODE ON)
3. One Definition Rule (ODR)
Violation Example:
// lib1.cpp
int helper() { return 1; }
// lib2.cpp
int helper() { return 2; } // ODR violation
CMake Solution:
# Use anonymous namespaces
namespace {
int helper() { return 1; } // Unique per translation unit
}
4. Symbol Conflicts in Dynamic Linking
Conflict Scenario:
// libA.cpp
void print() { std::cout << "A"; }
// libB.cpp
void print() { std::cout << "B"; }
Resolution Strategy:
// Proper namespacing
namespace LibA { void print() {...} }
namespace LibB { void print() {...} }
5. Linking Order Matters
Problematic Order:
target_link_libraries(MyApp LibA LibB) # LibA might need symbols from LibB
Correct Approach:
target_link_libraries(MyApp LibB LibA) # Dependencies first
6. Handling Unreferenced Symbols
CMake Solution:
# Force linking all symbols (GCC/Clang)
target_link_options(MyApp PRIVATE "-Wl,--no-as-needed")
7. Separating main() for Testing
Project Structure:
project/
├── src/
│ ├── main.cpp
│ └── core/
├── tests/
│ └── test_main.cpp
CMake Configuration:
# Main executable
add_executable(MainApp src/main.cpp)
target_link_libraries(MainApp PRIVATE CoreLib)
# Test executable
add_executable(TestRunner tests/test_main.cpp)
target_link_libraries(TestRunner PRIVATE CoreLib)
enable_testing()
add_test(NAME Tests COMMAND TestRunner)
Advanced: Interface Libraries
Usage Example:
add_library(InterfaceLib INTERFACE)
target_include_directories(InterfaceLib INTERFACE include)
target_link_libraries(MainApp PRIVATE InterfaceLib)
Key CMake Practices:
- Always use
target_link_libraries
with modern CMake syntax - Prefer target-based commands over global commands
- Use
PRIVATE/PUBLIC/INTERFACE
keywords appropriately - Leverage
find_package
for external dependencies - Utilize generator expressions for conditional linking
Complete Example (Static + Shared):
cmake_minimum_required(VERSION 3.20)
project(LinkingDemo)
# Core library
add_library(Core STATIC src/core.cpp)
target_include_directories(Core PUBLIC include)
# Plugin system (shared)
add_library(Plugin SHARED src/plugin.cpp)
target_link_libraries(Plugin PUBLIC Core)
set_target_properties(Plugin PROPERTIES POSITION_INDEPENDENT_CODE ON)
# Main executable
add_executable(MainApp src/main.cpp)
target_link_libraries(MainApp PRIVATE Core Plugin)
# Test executable
add_executable(TestRunner tests/test_main.cpp)
target_link_libraries(TestRunner PRIVATE Core)
add_test(NAME CoreTests COMMAND TestRunner)
This configuration demonstrates:
- Clear separation of components
- Proper linking order
- Mixed static/shared library usage
- Test integration
- PIC handling for shared libraries
- Modern target-based dependency management
Remember to:
- Use
$<BUILD_INTERFACE:...>
generator expressions for include paths - Consider visibility attributes (
__declspec(dllexport)
/visibility
defaults) - Regularly run
cmake --build . --target help
to verify targets - Use link-time optimization (
INTERPROCEDURAL_OPTIMIZATION
) where appropriate
Multiple-Choice Questions
Question 1: Static vs. Shared Libraries
Which statements are true about static and shared libraries?
A) Static libraries are linked at compile time.
B) Shared libraries reduce disk space usage compared to static libraries.
C) Static libraries use the .so
extension on Linux.
D) Shared libraries require Position-Independent Code (PIC).
E) Static libraries cannot contain unresolved symbols at link time.
Question 2: Position-Independent Code (PIC)
When is PIC required?
A) Always required for static libraries.
B) Required for shared libraries on most platforms.
C) Needed when compiling code for dynamic linking.
D) Optional for executables using -fPIC
on Linux.
E) Only necessary for header-only libraries.
Question 3: One Definition Rule (ODR)**
Which scenarios violate the ODR and cause undefined behavior?
A) Two translation units defining a static
global variable with the same name.
B) A class definition differing between two translation units.
C) Two shared libraries exporting the same symbol name.
D) A header file included in multiple sources defining a inline
function.
E) A static library and a shared library defining the same symbol.
Question 4: Linking Order Issues
What happens if library A
depends on library B
, but B
is listed after A
in the linker command?
A) The linker resolves all symbols correctly.
B) Unresolved symbols from B
may cause linker errors.
C) The linker automatically reorders dependencies.
D) Circular dependencies are detected and reported.
E) CMake handles this automatically via target dependencies.
Question 5: Unreferenced Symbols
How can you ensure unreferenced symbols are retained in a shared library?
A) Use -Wl,--no-undefined
in the linker flags.
B) Mark symbols with __attribute__((used))
in GCC/Clang.
C) Use -Wl,--gc-sections
to enable garbage collection.
D) Declare symbols as static
.
E) Use CMake’s OBJECT
libraries to force inclusion.
Question 6: Shared Modules
What distinguishes a shared module from a regular shared library?
A) Shared modules cannot be loaded dynamically.
B) Shared modules are designed for runtime loading via dlopen()
.
C) CMake uses add_library(MODULE)
for shared modules.
D) Shared modules always require PIC.
E) Shared modules are linked into executables at compile time.
Question 7: Namespace Usage
Why should namespaces be used instead of relying on the linker?
A) To avoid ODR violations across translation units.
B) To ensure symbols are unique within a project.
C) To improve compiler optimization opportunities.
D) To allow dynamic loading of shared libraries.
E) To reduce the need for static linking.
Question 8: Linker Symbol Resolution
Which factors influence linker symbol resolution?
A) The order of libraries in the linker command.
B) The use of extern "C"
in C++ code.
C) The optimization level (-O1
, -O2
, etc.).
D) The visibility attributes (__attribute__((visibility))
).
E) The compiler used (GCC vs. Clang).
Question 9: CMake Target Properties
Which CMake target properties affect linking behavior?
A) LINK_DEPENDS
B) LINK_FLAGS
C) INTERFACE_LINK_LIBRARIES
D) COMPILE_DEFINITIONS
E) POSITION_INDEPENDENT_CODE
Question 10: Separating main()
for Testing
What are valid strategies for separating main()
when unit testing?
A) Define main()
in a separate translation unit excluded from test builds.
B) Use preprocessor directives to conditionally exclude main()
.
C) Link main()
into a static library and omit it in test targets.
D) Use CMake’s add_executable
for production and test binaries.
E) Dynamically load main()
via a shared module during testing.
Answers and Explanations
Question 1:
Correct Answers: A, D, E
- A) Static libraries are linked at compile time.
- D) Shared libraries require PIC for relocation at runtime.
- E) Static libraries must resolve all symbols during linking.
Incorrect: - B) Shared libraries reduce memory usage (not disk space).
- C) Static libraries use
.a
on Linux;.so
is for shared libraries.
Question 2:
Correct Answers: B, C
- B) Shared libraries generally require PIC.
- C) PIC is required for code intended for dynamic linking.
Incorrect: - A) Static libraries do not need PIC.
- D) Executables do not require PIC unless being used as shared libraries.
- E) Header-only libraries do not involve linking.
Question 3:
Correct Answers: B, C, E
- B) Differing class definitions violate ODR and cause UB.
- C) Duplicate exported symbols in shared libraries cause UB at runtime.
-E) A static library and a shared library defining the same symbol also violate ODR. During static linking, the linker may merge symbols from both libraries, causing ambiguity. At runtime, the dynamic loader might prioritize the shared library’s symbol, leading to inconsistencies with the static library’s code
. This is particularly problematic for global variables, where conflicting initializations can corrupt program state
Incorrect: - A)
static
variables have internal linkage and are not shared. - D)
inline
functions are allowed in headers.
Question 4:
Correct Answers: B, E
- B) Incorrect order causes unresolved symbols.
- E) CMake’s target dependencies (
target_link_libraries(A B)
) ensure correct order.
Incorrect: - A) Linkers process libraries sequentially; unresolved symbols cause errors.
- C) Linkers do not reorder libraries automatically.
- D) Circular dependencies are unrelated to ordering.
Question 5:
Correct Answers: B, E
- B)
__attribute__((used))
prevents GCC/Clang from discarding symbols. - E) CMake
OBJECT
libraries force inclusion of all symbols.
Incorrect: - A)
--no-undefined
checks for missing symbols but does not retain unused ones. - C)
--gc-sections
removes unreferenced sections, worsening the problem. - D)
static
limits visibility to the translation unit.
Question 6:
Correct Answers: B, C
- B) Shared modules are designed for runtime loading (e.g., plugins).
- C) CMake uses
add_library(MODULE)
for shared modules.
Incorrect: - A) Shared modules are loaded dynamically.
- D) PIC is required for shared modules on most platforms.
- E) Shared modules are not linked at compile time.
Question 7:
Correct Answers: A, B
- A) Namespaces prevent ODR violations by scoping symbols.
- B) Ensures symbol uniqueness within a project.
Incorrect: - C) Namespaces do not directly affect optimization.
- D) Namespaces are unrelated to dynamic loading.
- E) Namespaces do not influence static vs. dynamic linking.
Question 8:
Correct Answers: A, B, D
- A) Linker order affects resolution (dependent libraries must come after users).
- B)
extern "C"
disables name mangling, affecting symbol names. - D) Visibility attributes control symbol export in shared libraries.
Incorrect: - C) Optimization levels do not affect symbol resolution.
- E) Symbol resolution rules are compiler-agnostic.
Question 9:
Correct Answers: B, C, E
- B)
LINK_FLAGS
adds custom linker flags. - C)
INTERFACE_LINK_LIBRARIES
propagates dependencies. - E)
POSITION_INDEPENDENT_CODE
enables PIC for shared libraries.
Incorrect: - A)
LINK_DEPENDS
specifies file dependencies, not linking behavior. - D)
COMPILE_DEFINITIONS
affects preprocessing, not linking.
Question 10:
Correct Answers: A, B, D
- A) Separating
main()
into a distinct file excluded from tests. - B) Using
#ifdef
to conditionally compilemain()
. - D) Creating separate executables for production and tests.
Incorrect: - C) Static libraries include all symbols; excluding
main()
is impractical. - E) Dynamic loading adds unnecessary complexity.
CMake Build Problems
Problem 1: Static/Shared Library Symbol Collision
Scenario:
A project contains two libraries: libutils.a
(static) and libutils.so
(shared). Both define a global function int calculate()
. When linking an executable that uses both libraries, the linker reports “multiple definition” errors. How would you resolve this conflict while preserving both library types?
Solution:
# For the static library target:
add_library(utils_static STATIC utils.cpp)
target_compile_definitions(utils_static PRIVATE CALCULATE_IMPL=1)
# For the shared library target:
add_library(utils_shared SHARED utils.cpp)
target_compile_definitions(utils_shared PRIVATE CALCULATE_IMPL=2)
target_compile_options(utils_shared PRIVATE -fvisibility=hidden)
target_link_options(utils_shared PRIVATE -Wl,--exclude-libs=ALL)
# Executable
add_executable(app main.cpp)
target_link_libraries(app PRIVATE utils_static utils_shared)
Explanation:
- Symbol Renaming: Use preprocessor macros (
CALCULATE_IMPL
) to create distinct symbol names in each library version. - Visibility Control: Hide all symbols in the shared library by default (
-fvisibility=hidden
), then explicitly export required symbols using__attribute__((visibility("default")))
in code. - Linker Exclusion:
--exclude-libs=ALL
prevents the shared library from re-exporting symbols from the static library. - Modern CMake Practice: Use target-specific properties to isolate compilation/linking settings per library type.
Problem 2: Cross-Library ODR Violation
Scenario:
Two shared libraries (libnetwork.so
and libgraphics.so
) both include a third-party header-only math library that defines const double PI = 3.1415
. At runtime, applications linking both libraries crash due to ODR violations. How would you resolve this using CMake?
Solution:
# For both libraries:
add_library(network SHARED network.cpp)
add_library(graphics SHARED graphics.cpp)
# In source code:
namespace internal {
constexpr double PI = 3.1415;
}
# CMake for each library:
target_compile_options(network PRIVATE -fvisibility-inlines-hidden)
target_compile_definitions(network PRIVATE MATH_NAMESPACE=internal)
target_link_options(network PRIVATE -Wl,--version-script=exports.map)
# exports.map content:
{
global: *network_api*;
local: *;
};
Explanation:
- Namespace Isolation: Wrap the constant in a unique namespace per library using compile definitions.
- Symbol Visibility: Use
-fvisibility-inlines-hidden
to hide inline symbols and version scripts to control exported symbols. - Inline Variables: For C++17+, use
inline constexpr
to avoid ODR issues with constants. - CMake Integration: Combine compile options, definitions, and linker scripts to enforce symbol visibility boundaries.
Problem 3: Circular Dependency Resolution
Scenario:
Three libraries (A
, B
, C
) have circular dependencies: A depends on B, B depends on C, and C depends on A. The linker fails with “undefined reference” errors. How would you resolve this using modern CMake?
Solution:
# Library A
add_library(A OBJECT a.cpp)
target_include_directories(A PUBLIC include)
# Library B
add_library(B OBJECT b.cpp)
target_include_directories(B PUBLIC include)
# Library C
add_library(C OBJECT c.cpp)
target_include_directories(C PUBLIC include)
# Combine into interface library
add_library(combined INTERFACE)
target_link_libraries(combined INTERFACE
$<TARGET_OBJECTS:A>
$<TARGET_OBJECTS:B>
$<TARGET_OBJECTS:C>
)
# Final executable
add_executable(main main.cpp)
target_link_libraries(main PRIVATE combined)
Explanation:
- Object Libraries: Use
OBJECT
libraries to delay linking until all dependencies are resolved. - Interface Aggregation: Create an INTERFACE library to collect all object files simultaneously.
- CMake Generator Expressions: Use
$<TARGET_OBJECTS>
to handle object file dependencies. - Dependency Ordering: The combined approach lets the linker see all objects at once, resolving circular references through deferred linking.
Key Chapter 8 Concepts Tested:
- Linking Order Mechanics: Understanding how linkers process inputs sequentially.
- Symbol Visibility: Controlling what symbols are exposed across library boundaries.
- ODR Compliance: Ensuring single definitions across translation units.
- Modern CMake Patterns: Using target-specific properties, object libraries, and generator expressions.
- Advanced Linking Techniques: Version scripts, visibility attributes, and circular dependency resolution.