Linux Dynamic Linking 4: Runtime Linking

Recently I plan to take time to study “Android Development Master Course” by teacher Zhang Shaowen.
I want to thoroughly understand the implementation principle of the framework for local monitoring of APP memory.
I found that it was not that easy to understand. During the process of reading the code, I found that my skills in C++, Linux, native hooks, frameworks, etc. were all insufficient.
Teacher Zhang Shaowen once said: “No matter how many articles you read, it is useless if you don’t think about the content and intention of the article; no matter how much you think, it is useless if you don’t actually practice it.”
“Connecting advanced topics from point to line, but there must be some basic and underlying knowledge to support this.”
Here we will fill in the gaps in knowledge.

Article directory

  • Explicit runtime linking
    • Example of explicit runtime linking
    • The difference between dynamic libraries and shared objects
      • Shared objects
      • dynamic library
  • dlopen()
    • Parameter 1
    • Parameter 2
      • RTLD_LAZY
      • RTLD_NOW
      • RTLD_GLOBAL
    • return value
    • dlopen executes initialization code when loading a module
    • dlsym()
    • dlerror()
    • dlclose()

Explicit runtime linking

Systems that support dynamic linking often support a more flexible module loading method called Explicit Run-time Linking, sometimes also called run-time loading. That is, the program can control the loading of the specified module at runtime, and can uninstall the module when it is not needed.

From what we learned earlier, if the dynamic linker can load shared modules into memory at runtime and perform relocation and other operations, then this kind of runtime loading is theoretically easy to implement.

Moreover, general shared objects can be loaded at runtime without any modification. This kind of shared object is often called a dynamic loading library (Dynamic Loading Library). In fact, it is essentially the same as a general shared object. It is only used by program developers. It’s at a different angle.

This kind of runtime loading makes the module organization of the program very flexible and can be used to implement functions such as plug-ins and drivers. When the program needs to use a certain plug-in or driver, the corresponding module is loaded instead of loading them all from the beginning, thus reducing program startup time and memory usage. And the program can reload a module while running, so that the program itself can add, delete, update, etc. modules without having to restart. This is a great advantage for many programs that need to run for a long time.

Explicit runtime linking example

The most common example is a Web server program. For a Web server program, it needs to select different script interpreters, database connection drivers, etc. according to the configuration. Different script interpreters are made into independent modules. When the Web server The script interpreter can be loaded when the server needs it; the same principle applies to drivers for database connections.

In addition, for a reliable Web server, long-term operation is a necessary guarantee. If we need to add a certain script interpreter, or a certain script interpreter module needs to be upgraded, we can notify the Web server program to reload the shared module. achieve the corresponding purpose.

The difference between dynamic libraries and shared objects

In Linux, from the format of the file itself, dynamic libraries are actually no different from general shared objects.
The main differences are:

Shared objects

Shared objects are loaded and linked by the dynamic linker before the program starts. This series of steps are automatically completed by the dynamic linker and are transparent to the program itself.

Dynamic library

The dynamic library is loaded through a series of APIs provided by the dynamic linker. Specifically, there are 4 functions:

1. Open the dlopen
2. Find symbols (dlsym)
3.Error handling (dlerror)
4. Close the dynamic library (dlclose)

Programs can operate dynamic libraries through these APIs. The implementation of these APIs is in /lib/libdl.so.2, and their declarations and related constants are defined in the system standard header file .

dlopen()

The dlopen function is used to open a dynamic library and load it into the address space of the process to complete the initialization process. Its C prototype is defined as:

void*dlopen (constchar*filename, int flag);

Parameter 1

The first parameter is the path to the loaded dynamic library.

Parameter 2

RTLD_LAZY

The second parameter flag represents the parsing method of function symbols. The constant RTLD_LAZY represents the use of delayed binding. The function is bound only when it is used for the first time, that is, the PLT mechanism:

RTLD_NOW

RTLD_NOW indicates that all function binding work will be completed when the module is loaded. If the binding work of any undefined symbol reference cannot be completed, then dlopen will return an error.

You must choose one of the two binding methods above.

RTLD_GLOBAL

There is also a constant RTLD_GLOBAL that can be used with either of the above two (through the “or” operation of the constant, it means that the global symbols of the added block are merged into the global symbol table of the process, so that modules loaded later can Use these symbols. We can use RTLD_NOW as the load parameter when debugging the program,

Because if any symbols are unbound when the module is loaded, we can use dlerror() to capture the corresponding error information immediately; and if RTLD_LAZY is used, this unbound symbol error will occur after loading, which is difficult to capture.
Of course, using RTLD_NOW will result in slower loading of dynamic libraries.

Return value

The return value of dlopen is the handle of the loaded module. This handle will be used later when using dlsym or dlclose.
If loading the module fails, NULL is returned. If the module has already been loaded via dlopen, the same handle is returned.
In addition, if there is a dependency relationship between the loaded modules, such as module A depends on module B. Then the programmer needs to manually load the dependent modules, such as loading B first and then loading A.

dlopen executes initialization code when loading a module

In fact, dlopen will also execute the code in the initialization part of the module when loading the module. As we mentioned earlier, when the dynamic linker loads the module, it will execute the code in the “.init” section to complete the initialization of the module. dlopen The loading process is basically the same as that of the dynamic linker. After loading, mapping and relocation are completed, the code in the “.init” section will be executed and then returned.

dlsym()

The dlsym function is basically the core part of runtime loading. We can find the required symbols through this function. It is defined as follows:

void * dlsym(void *handle, char *symbol);

The definition is very simple, two parameters
The first parameter is the handle of the dynamic library returned by dlopen(,
The second parameter is the name of the symbol to be found, a C string ending with “0”.

If dlsym() finds the corresponding symbol, it returns the value of the symbol;
If the corresponding symbol is not found, NULL is returned.

The value returned by dlsym() has different meanings for different types of symbols. If the symbol being searched is a function, it returns the address of the function; if it is a variable, it returns the address of the variable; if the symbol is a constant, it returns the value of the constant. There is a question here: What if the value of the constant happens to be NULL or 0?
How do we tell whether dsym() found the symbol?

This requires the use of the dlerror() function we introduce below. If the symbol is found, dlerror() returns NULL. If it is not found, dlerror() returns the corresponding error message.

dlerror()

Every time we call dlopen(dlsym0 or dlclose(), we can call the dlerror() function to determine whether the previous call was successful. The return value type of dlerror0 is char*. If NULL is returned, it means the last call was successful; if If not, the appropriate error message is returned.

dlclose()

The function of dclose is exactly the opposite of dlopen. Its function is to unload a loaded module.
The system will maintain a load reference counter. Each time dlopen is used to load a module, the corresponding counter is incremented by one.
Each time diclose0 is used to uninstall a module, the corresponding counter is decremented by one. Only when the counter value decreases to 0, the module is truly unloaded.
The unloading process is just the opposite of loading. First execute the code in the “.finit” section, then remove the corresponding symbols from the symbol table, cancel the mapping relationship between the process space and the module, and then close the module file.

References –
“Programmer’s self-cultivation – linking, loading and libraries”