The intricate world of software development often involves leveraging shared libraries to optimize resource usage and modularize code. Dynamic linking, a cornerstone of modern operating systems, allows programs to load these libraries at runtime, significantly reducing executable sizes and facilitating easier updates. However, for developers working at a lower level, a critical question often arises: what happens to global and static variables in a shared library when it is dynamically linked? Understanding their lifecycle, memory allocation, and visibility is crucial for writing robust, efficient, and bug-free applications. This exploration delves into the underlying mechanisms, from the initial loading process to symbol resolution, shedding light on how these fundamental data types behave within the dynamic environment of a shared library.
Understanding Shared Libraries and Dynamic Linking
Shared libraries, often referred to as dynamic-link libraries (DLLs) on Windows or shared objects (SOs) on Unix-like systems, are collections of compiled code and data that can be used by multiple programs simultaneously. Instead of each executable containing its own copy of common functions, these functions are stored in a single shared library file. When an application needs a function from a shared library, the dynamic linker (or loader) maps the library into the application’s process memory space at runtime.
This dynamic linkage offers significant advantages, including reduced memory footprint (as only one copy of the library’s code segments needs to be in physical RAM for multiple processes), smaller executable file sizes, and simplified maintenance. If a bug is found in a shared library, it can often be fixed by updating just the library file, without recompiling or redeploying every application that uses it. The dynamic linker plays a pivotal role, resolving symbols (function names and variable names) from the library and linking them to the application’s code.
The process of loading a shared library involves several steps, starting with the operating system locating the library file on disk. Once found, the dynamic linker loads the library into memory and performs necessary relocations. These relocations adjust memory addresses within the library to reflect its actual load address in the process’s virtual memory. For instance, a library might be compiled with the assumption it starts at address 0, but at runtime, it might be loaded at 0x7f… due to address space layout randomization (ASLR) or other loaded modules. This mechanism ensures that the library’s code and data can execute correctly regardless of where they are placed in memory.
Memory Allocation for Global and Static Variables
When a shared library is dynamically linked, its global and static variables are allocated within the data segment and BSS (Block Started by Symbol) segment of the process memory space, just like those in a main executable. The key distinction lies in their initialization and how they are managed across different processes or multiple loadings within the same process. Specifically, initialized global and static variables reside in the data segment, while uninitialized ones are placed in the BSS segment. These segments are typically part of the library’s memory mapping within the application’s virtual address space.
Each process that loads a shared library receives its own private copy of the library’s data and BSS segments. This crucial design choice ensures isolation: changes made to a global variable by one process using the shared library will not affect other processes using the same library. For example, if two separate applications, AppA and AppB, both dynamically link to libcommon.so, and libcommon.so has a global variable int counter;, then AppA will have its own counter, and AppB will have its own independent counter. This prevents unintended side effects and maintains data integrity across different execution contexts.
The dynamic linker handles the initialization of these variables. Initialized global and static variables receive their default values from the library’s image during the loading phase, before any code from the library (or the main executable) begins to execute. Uninitialized variables in the BSS segment are typically zero-initialized by the operating system. This ensures a predictable state for these variables upon library loading, which is a critical aspect for program correctness and reliability, especially in multi-threaded or complex application environments. For a deeper dive into memory management, consider exploring resources on data segments and BSS.
Symbol Resolution and Relocation Mechanisms
The behavior of global and static variables in a shared library is heavily influenced by symbol resolution and relocation. When a shared library is compiled, the compiler and linker don’t know the exact memory address where the library will be loaded at runtime. Instead, they generate relocation entries. These entries are instructions for the dynamic linker, indicating which parts of the library’s code or data need to be adjusted once the library’s actual load address is known.
For global variables, especially those accessed from outside the library or by the main executable, the dynamic linker performs symbol resolution. This process involves looking up the variable’s name (symbol) in the library’s symbol table and, if necessary, in the main executable’s symbol table or other loaded libraries. The goal is to find the definitive memory location for that symbol. If a global variable with the same name exists in both the main executable and the shared library, the dynamic linker typically prioritizes the symbol in the main executable, a behavior known as “first-found” or “default” symbol resolution. This can lead to subtle bugs if not carefully managed.
Position-Independent Code (PIC) is a crucial technique used in shared libraries to make their code executable regardless of its absolute address in memory. For global and static variables, this often involves the Global Offset Table (GOT) and the Procedure Linkage Table (PLT). When a shared library accesses a global variable, it doesn’t directly use its absolute address. Instead, it accesses an entry in the GOT, which the dynamic linker populates with the variable’s actual memory address at load time. This indirection allows the library to be loaded at any address without needing to rewrite its code segment, a significant benefit for memory efficiency and ASLR compatibility.
Hereβs a simplified flow of how symbol resolution works for a global variable access:
- The shared library code needs to access a global variable, let’s say
global_counter. - Instead of a direct memory address, the instruction references an entry in the Global Offset Table (GOT).
- When the library is loaded, the dynamic linker finds the actual address of
global_counter. - The dynamic linker then updates the corresponding entry in the library’s GOT with this actual address.
- Subsequent accesses to
global_counterby the library’s code go through the GOT, retrieving the correct, resolved address.
This indirection ensures that the library’s code remains position-independent, a cornerstone of efficient shared library design. Understanding this mechanism is vital for optimizing shared library performance and debugging. Further details Question & Answer :
I’m trying to understand what happens when modules with globals and static variables are dynamically linked to an application. By modules, I mean each project in a solution (I work a lot with visual studio!). These modules are either built into *.lib or *.dll or the *.exe itself.
I understand that the binary of an application contains global and static data of all the individual translation units (object files) in the data segment (and read only data segment if const).
- What happens when this application uses a module A with load-time dynamic linking? I assume the DLL has a section for its globals and statics. Does the operating system load them? If so, where do they get loaded to?
- And what happens when the application uses a module B with run-time dynamic linking?
- If I have two modules in my application that both use A and B, are copies of A and B’s globals created as mentioned below (if they are different processes)?
- Do DLLs A and B get access to the applications globals?
(Please state your reasons as well)
Quoting from MSDN:
Variables that are declared as global in a DLL source code file are treated as global variables by the compiler and linker, but each process that loads a given DLL gets its own copy of that DLL’s global variables. The scope of static variables is limited to the block in which the static variables are declared. As a result, each process has its own instance of the DLL global and static variables by default.
and from here:
When dynamically linking modules, it can be unclear whether different libraries have their own instances of globals or whether the globals are shared.
Thanks.
This is a pretty famous difference between Windows and Unix-like systems.
No matter what:
- Each process has its own address space, meaning that there is never any memory being shared between processes (unless you use some inter-process communication library or extensions).
- The One Definition Rule (ODR) still applies, meaning that you can only have one definition of the global variable visible at link-time (static or dynamic linking).
So, the key issue here is really visibility.
In all cases, static global variables (or functions) are never visible from outside a module (dll/so or executable). The C++ standard requires that these have internal linkage, meaning that they are not visible outside the translation unit (which becomes an object file) in which they are defined. So, that settles that issue.
Where it gets complicated is when you have extern global variables. Here, Windows and Unix-like systems are completely different.
In the case of Windows (.exe and .dll), the extern global variables are not part of the exported symbols. In other words, different modules are in no way aware of global variables defined in other modules. This means that you will get linker errors if you try, for example, to create an executable that is supposed to use an extern variable defined in a DLL, because this is not allowed. You would need to provide an object file (or static library) with a definition of that extern variable and link it statically with both the executable and the DLL, resulting in two distinct global variables (one belonging to the executable and one belonging to the DLL).
To actually export a global variable in Windows, you have to use a syntax similar to the function export/import syntax, i.e.:
#ifdef COMPILING_THE_DLL #define MY_DLL_EXPORT extern "C" __declspec(dllexport) #else #define MY_DLL_EXPORT extern "C" __declspec(dllimport) #endif MY_DLL_EXPORT int my_global;
When you do that, the global variable is added to the list of exported symbols and can be linked like all the other functions.
In the case of Unix-like environments (like Linux), the dynamic libraries, called “shared objects” with extension .so export all extern global variables (or functions). In this case, if you do load-time linking from anywhere to a shared object file, then the global variables are shared, i.e., linked together as one. Basically, Unix-like systems are designed to make it so that there is virtually no difference between linking with a static or a dynamic library. Again, ODR applies across the board: an extern global variable will be shared across modules, meaning that it should have only one definition across all the modules loaded.
Finally, in both cases, for Windows or Unix-like systems, you can do run-time linking of the dynamic library, i.e., using either LoadLibrary() / GetProcAddress() / FreeLibrary() or dlopen() / dlsym() / dlclose(). In that case, you have to manually get a pointer to each of the symbols you wish to use, and that includes the global variables you wish to use. For global variables, you can use GetProcAddress() or dlsym() just the same as you do for functions, provided that the global variables are part of the exported symbol list (by the rules of the previous paragraphs).
And of course, as a necessary final note: global variables should be avoided. And I believe that the text you quoted (about things being “unclear”) is referring exactly to the platform-specific differences that I just explained (dynamic libraries are not really defined by the C++ standard, this is platform-specific territory, meaning it is much less reliable / portable).