"Picture + code": the underlying principle of "relocation" in the process of Linux dynamic linking

As usual, a lot of [code + pictures] are used in this article to truly feel the actual memory model.

A lot of pictures are used in this article, it is recommended that you read this article on a computer.

As for why dynamic links are used, there will be no discussion here, just a few points:

Save physical memory;

Can be updated dynamically;


Friends who are interested in knowing the content and more related learning materials, please like and collect + comment and forward + follow me, there will be a lot of dry goods later. I have some interview questions, architecture, and design materials that can be said to be necessary for programmer interviews!
All the materials are organized into the network disk, welcome to download if necessary! Private message me to reply [111] to get it for free

What problem does dynamic linking solve?

The executable program obtained by static linking can be executed after being loaded by the operating system.

Because at the time of linking, the linker has assembled the code, data and other sections in all object files into the executable file.

And all external symbols (variables, functions) used in the code are relocated (that is, the addresses of variables and functions are filled in the places that need to be relocated in the code segment), so the executable program is executed At that time, it can run without relying on other external modules.

That is to say: the process of symbol relocation is to directly modify the executable file.

But for dynamic linking, at the compilation stage, only some necessary information is recorded in the executable file or dynamic library.

The real relocation process is completed at this point in time: after the executable program and the dynamic library are loaded, before calling the entry function of the executable program.

Only after all symbols that need to be relocated have been resolved can the program be executed.

Since it is also a relocation, it is the same as the static link process: it is also necessary to fill in the target address of the symbol to the place in the code segment that needs to be relocated.

Contradiction: code segment is not writable

Here comes the problem!

We know that in modern operating systems, access to memory is controlled by permissions. Generally speaking:

Code segment: readable, executable;

Data segment: readable and writable;

If symbol relocation is performed, the code needs to be modified (fill in the address of the symbol), but the code segment has no write permission, which is a contradiction!

The solution to this contradiction is the core work of the dynamic linker in the Linux system!

Resolving Conflicts: Adding a Layer of Indirection

David Wheeler has a famous quote: "Most problems in computer science can be solved by adding a layer of indirection."

Solving the code relocation problem in dynamic linking can also be solved by adding a layer of indirection.

Since the code segment is not writable after being loaded into memory, the data segment is.

For external symbols referenced in the code segment, a springboard can be added in the data segment: let the code segment first refer to the content in the data segment, and then fill in the address of the external symbol to the corresponding position in the data segment during relocation. Is this contradiction resolved? !

As shown below:

After understanding the solution idea in the above figure, you can basically understand the core idea of ​​relocation in the process of dynamic linking.

sample code

We need 3 source files to discuss the process of relocation in dynamic linking: main.c, ac, bc, where ac and bc are compiled into a dynamic library, and then main.c is dynamically linked with these two dynamic libraries into a executable execute program.

The dependencies between them are:

b.c

code show as below:

#include <stdio.h>

int b = 30;

void func_b(void)
{
    printf("in func_b. b = %d \n", b);
}

Code description:

Define a global variable and a global function, called by ac.

a.c

The code is as follows (slightly more complicated, mainly for exploration: how different types of symbols handle relocation):

#include <stdio.h>

// 内部定义【静态】全局变量
static int a1 = 10;

// 内部定义【非静态】全局变量
int a2 = 20;

// 声明外部变量
extern int b;

// 声明外部函数
extern void func_b(void);

// 内部定义的【静态】函数
static void func_a2(void)
{
    printf("in func_a2 \n");
}

// 内部定义的【非静态】函数
void func_a3(void)
{
    printf("in func_a3 \n");
}

// 被 main 调用
void func_a1(void)
{
    printf("in func_a1 \n");

    // 操作内部变量
    a1 = 11;
    a2 = 21;

    // 操作外部变量
    b  = 31;

    // 调用内部函数
    func_a2();
    func_a3();

    // 调用外部函数
    func_b();
}

Code description:

Two global variables are defined: one static and one non-static;

3 functions are defined:

func_a2 is a static function and can only be called in this file;

func_a1 and func_a3 are global functions that can be called externally;

func_a1 is called in main.c.

main.c

code show as below:

#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>

// 声明外部变量
extern int a2;
extern void func_a1();

typedef void (*pfunc)(void);

int main(void)
{
    printf("in main \n");

    // 打印此进程的全局符号表
    void *handle = dlopen(0, RTLD_NOW);
    if (NULL == handle)
    {
        printf("dlopen failed! \n");
        return -1;
    }

    printf("\n------------ main ---------------\n");
    // 打印 main 中变量符号的地址
    pfunc addr_main = dlsym(handle, "main");
    if (NULL != addr_main)
        printf("addr_main = 0x%x \n", (unsigned int)addr_main);
    else
        printf("get address of main failed! \n");

    printf("\n------------ liba.so ---------------\n");
    // 打印 liba.so 中变量符号的地址
    unsigned int *addr_a1 = dlsym(handle, "a1");
    if (NULL != addr_a1)
        printf("addr_a1 = 0x%x \n", *addr_a1);
    else
        printf("get address of a1 failed! \n");

    unsigned int *addr_a2 = dlsym(handle, "a2");
    if (NULL != addr_a2)
        printf("addr_a2 = 0x%x \n", *addr_a2);
    else
        printf("get address of a2 failed! \n");

    // 打印 liba.so 中函数符号的地址
    pfunc addr_func_a1 = dlsym(handle, "func_a1");
    if (NULL != addr_func_a1)
        printf("addr_func_a1 = 0x%x \n", (unsigned int)addr_func_a1);
    else
        printf("get address of func_a1 failed! \n");

    pfunc addr_func_a2 = dlsym(handle, "func_a2");
    if (NULL != addr_func_a2)
        printf("addr_func_a2 = 0x%x \n", (unsigned int)addr_func_a2);
    else
        printf("get address of func_a2 failed! \n");

    pfunc addr_func_a3 = dlsym(handle, "func_a3");
    if (NULL != addr_func_a3)
        printf("addr_func_a3 = 0x%x \n", (unsigned int)addr_func_a3);
    else
        printf("get address of func_a3 failed! \n");


    printf("\n------------ libb.so ---------------\n");
    // 打印 libb.so 中变量符号的地址
    unsigned int *addr_b = dlsym(handle, "b");
    if (NULL != addr_b)
        printf("addr_b = 0x%x \n", *addr_b);
    else
        printf("get address of b failed! \n");

    // 打印 libb.so 中函数符号的地址
    pfunc addr_func_b = dlsym(handle, "func_b");
    if (NULL != addr_func_b)
        printf("addr_func_b = 0x%x \n", (unsigned int)addr_func_b);
    else
        printf("get address of func_b failed! \n");

    dlclose(handle);

    // 操作外部变量
    a2 = 100;

    // 调用外部函数
    func_a1();

    // 为了让进程不退出,方便查看虚拟空间中的地址信息
    while(1) sleep(5);
    return 0;
}

Correction: In the code, I originally wanted to print the address of the variable, but accidentally added * to print the variable value. It was only discovered during the final inspection, so I was too lazy to modify it.

Code description:

Use the dlopen function (the first parameter is passed to NULL) to print some symbolic information (variables and functions) in this process;

Assign the value to the variable a2 in liba.so, and then call the func_a1 function in liba.so;

Compile into a dynamic link library

Compile the above source files into dynamic libraries and executable programs:

$ gcc -m32 -fPIC --shared b.c -o libb.so
$ gcc -m32 -fPIC --shared a.c -o liba.so -lb -L./
$ gcc -m32 -fPIC main.c -o main -ldl -la -lb -L./

There are a few points to explain:

The -fPIC parameter means: generate position independent code (Position Independent Code), which is also the key in dynamic linking;

Since the dynamic library is loaded at runtime, why do you need to specify it at compile time?

Because at compile time, you need to know which symbols are provided in each dynamic library. The explicit export and import identifiers of dynamic libraries in Windows can better reflect this concept (__declspec(dllexport), __declspec(dllimport)).

At this point, the following files are obtained:

Dynamic library dependencies

For a statically linked executable program, after being loaded by the operating system, it can be considered to directly start from the entry function of the executable program (that is, the address of e_entry specified in the ELF file header), and execute the instruction code therein.

But for a dynamically linked program, before the instruction of the entry function is executed, the dynamic library on which the program depends must be loaded into the memory, and then the execution can start.

For our example code: the main program depends on the liba.so library, and the liba.so library depends on the libb.so library.

You can use the ldd tool to look at the dependencies between dynamic libraries:

As can be seen:

In the liba.so dynamic library, the information is recorded: depends on libb.so;

In the main executable file, the information is recorded: Depends on liba.so, libb.so;

You can also use another tool patchelf to see which other modules an executable program or dynamic library depends on. For example:

So, who will load the dynamic library? Dynamic linker!

Dynamic library loading process

The dynamic linker loads the dynamic library

When executing the main program, the operating system first loads main into the memory, and then checks which dynamic libraries the file depends on through the .interp segment information:

The string /lib/ld-linux.so.2 in the above figure means that main depends on the dynamic link library.

ld-linux.so.2 is also a dynamic link library. In most cases, the dynamic link library has been loaded into the memory (the dynamic link library is for sharing). At this time, the operating system only needs to put the physical The memory can be mapped to the virtual address space of the main process, and then control is given to the dynamic linker.

The dynamic linker finds that main depends on liba.so, so it finds a free space in the virtual address space that can hold liba.so, and then loads the code segments and data segments in liba.so that need to be loaded into memory. load in.

Of course, when liba.so is loaded, it will be found that it depends on libb.so, so find a free space in the virtual address space that can hold libb.so, and put the code segment and data segment in libb.so Waiting to be loaded into the memory, the schematic diagram is as follows:

The dynamic linker itself is also a dynamic library, and it is a special dynamic library: it does not depend on any other dynamic library, because when it is loaded, no one helps it to load the dependent dynamic library, otherwise it will form a chicken It's a chicken-and-egg problem.

The loading address of the dynamic library

The actual loading address (or virtual memory area) of a process at runtime can be read by the command: $ cat /proc/[pid of the process]/maps.

For example: when the main program is executed in my virtual machine, the address information I see is:

The yellow parts are the loading information of the three modules: main, liba.so, libb.so.

In addition, you can also see the virtual address area of ​​the c library (libc-2.23.so), the dynamic linker (ld-2.23.so) and the dynamic loading library libdl-2.23.so, the layout is as follows:

It can be seen that the main executable program is located at a low address, and all dynamic libraries are located in the last 1G space of the 4G memory space.

There is another instruction that also works well with $ pmap [pid of process], which also prints out the memory address of each module:

symbol relocation

global symbol table

As learned in the previous static link, when the linker scans each object file (.o file), it will extract the symbols in each object file to form a global symbol table.

Then in the second scan, look at the symbols that need to be relocated in each object file, and then look up the address where the symbol is arranged in the global symbol table, and then fill in this address to the referenced place, this is static linking time relocation.

However, relocation during dynamic linking is much different from static linking, because the address of each symbol can only be known when it is running.

For example: liba.so refers to the variables and functions in libb.so, and where these two symbols in libb.so are loaded, until the main program is ready to execute, it can be loaded into a certain place in the memory by the linker a random location.

That is to say: the dynamic linker knows the memory address where the code segment and data segment in each dynamic library are loaded, so the dynamic linker will also maintain a global symbol table, which stores the symbols exported in each dynamic library and their memory address information.

In the main.c function of the sample code, we print the address information of some global symbols in the process through the handle returned by dlopen, and the output is as follows:

The above has been corrected: I originally wanted to print the address information of the variable, but the model was accidentally added to the printf statement, and it changed to print the variable value.

It can be seen that in the global symbol table, the two symbols of the variable a1 and the function func_a2 in liba.so are not found, because they are both of static type, and they are not exported to the symbol table when they are compiled into a dynamic library. .

Now that the symbol table is mentioned, let's take a look at the dynamic symbol table information in these 3 ELF files:

Two symbol tables are protected in the dynamic link library: .dynsym (dynamic symbol table: indicates the export and import relationship of symbols in the module) and .symtab (symbol table: indicates all symbols in the module);

.symtab contains .dynsym;

Because the picture is too large, only the .dynsym dynamic symbol table is posted here.

The Ndx column in front of the green rectangle is a number, indicating which segment of the current file the symbol is located in (ie: segment index);

The Ndx column in front of the red rectangle is UND, indicating that the symbol is not found and is an external symbol (needs to be relocated);

Global Offset Table GOT

In our sample code, liba.so is special, it is depended on by both the main executable program and libb.so.

Moreover, in liba.so, static and dynamic global variables and functions are defined, which can give a good overview of many situations, so this part of the content mainly analyzes the dynamic library of liba.so.

As mentioned above: code relocation needs to modify the symbol references in the code segment, and the code segment is loaded into memory without writable permissions. The solution to this contradiction with dynamic linking is: add a layer of indirection.

For example: the code of liba.so refers to the variable b in libb.so. In the code segment of liba.so, it does not directly point to the address of the variable b in the data segment of libb.so at the referenced place, but points to liba A certain position in the data segment of .so itself. In the relocation phase, the linker fills in the address of variable b in libb.so to this position.

Because liba.so's own code segment and data segment are relatively fixed, in this case, after the liba.so code segment is loaded into the memory, it will no longer need to be modified.

The position of this indirect jump in the data segment is called: Global Offset Table (GOT: Global Offset Table).

Focus on:

The code segment of liba.so refers to the symbol b in libb.so. Since the address of b needs to be determined during relocation, a space (called: GOT table) is opened in the data segment, and the The address of b is filled in the GOT table.

In the code segment of liba.so, the address of the GOT table is filled in the place where b is referenced, because the GOT table can be determined at the compilation stage, and the relative address is used.

In this way, the symbol b can be dynamically relocated without modifying the liba.so code segment!

In fact, there are two GOT tables in a dynamic library, which are used to relocate variable symbols (section name: .got) and function symbols (section name: .got.plt).

That is to say: the symbol relocation information of all variable types is located in .got, and the symbol relocation information of all function types is located in .got.plt.

And, in a dynamic library file, there are two special sections (.rel.dyn and .rel.plt) to tell the linker: which symbols in the two tables .got and .got.plt need to be reconfigured Positioning, this issue will be discussed in depth below.

The layout of the liba.so dynamic library file

In order to have a deeper understanding of the two tables .got and .got.plt, it is necessary to disassemble the internal structure of the liba.so dynamic library file.

Use the readelf -S liba.so command to see which sections are in this ELF file:

It can be seen that there are 28 sections in total, of which 21 and 22 are two GOT tables.

In addition, from the perspective of loading, the loader does not process these sections separately, but treats multiple sections as a segment according to different read and write attributes.

Use the command readelf -l liba.so again to check the segment information:

That is to say:

In these 28 sections (focus on the green lines):

Section 0 ~ 16 are all readable and executable permissions, and are regarded as a segment;

Section 17 ~ 24 are all readable and writable permissions, which are used as another segment;

Let's focus on the two sections .got and .got.plt (focus on the yellow rectangle):

It can be seen that .got and .got.plt are both readable and writable like the data segment, so they are loaded into memory as the same segment.

Through the above two pictures (red rectangle frame), the internal structure of the liba.so dynamic library file can be obtained as follows:

The virtual address of the liba.so dynamic library

Let's continue to observe the AirtAddr column in the segment information of the liba.so file, which indicates the address loaded into the virtual memory, and the remapping is as follows:

Because the code position independent parameter (-fPIC) is used when compiling the dynamic library, the virtual address here starts from 0x0000_0000.

When the code segment and data segment of liba.so are loaded into the memory, the dynamic linker finds a free space, and the start address of this space is equivalent to a base address.

All the virtual address information in the code segment and data segment in liba.so, as long as this base address is added, the actual virtual address can be obtained.

We still use the output information in the above figure to draw a detailed memory model diagram, as shown below:

Internal structure of GOT table

Now, we already know the file layout of the liba.so library and its virtual address, so we can take a closer look at the internal structure of the two tables .got and .got.plt.

From the picture just now:

The length of the .got table is 0x1c, indicating that there are 7 entries (each entry occupies 4 bytes);

The length of the .got.plt table is 0x18, indicating that there are 6 entries;

As mentioned above, these two tables are used to relocate all symbols such as variables and functions.

So: how does liba.so tell the dynamic linker: it is necessary to relocate the addresses of the entries in the two tables .got and .got.plt?

During static linking, the object file tells the linker through two relocation tables, .rel.text and .rel.data.

For dynamic linking, the symbol information that needs to be relocated is also passed through two relocation tables, but the names are somewhat different: .rel.dyn and .rel.plt.

View relocation information by command readelf -r liba.so:

As can be seen from the yellow and green rectangles:

liba.so references the external symbol b, the type is R_386_GLOB_DAT, and the relocation description information of this symbol is in the .rel.dyn section;

liba.so references the external symbol func_b, the type is R_386_JUMP_SLOT, and the relocation description information of this symbol is in the .rel.plt section;

From the red rectangular box on the left, we can see that the virtual address corresponding to each entry that needs to be relocated is drawn as a memory model diagram as follows:

For the time being, only focus on the red part of the entry: b in the .got table, func_b in the .got.plt table, both symbols are exported from libb.so.

That is to say:

When operating the variable b in liba.so code, go to the address 0x0000_1fe8 in the .got table to obtain the real address of the variable b;

When calling the func_b function in the liba.so code, go to the address 0x0000_200c in the .got.plt table to obtain the real address of the function;

Disassemble liba.so code

Let's disassemble liba.so and see how the two entries are addressed in the instruction code.

Execute the disassembly command: $ objdump -d liba.so, only the disassembly code of the func_a1 function is posted here:

The function of the first green rectangle (call 490 <__x86.get_pc_thunk.bx>) is to store the address of the next instruction (add) in %ebx, that is:

%ebx = 0x622

Then execute: add $0x19de,%ebx, let %ebx add 0x19de, the result is: %ebx = 0x2000.

0x2000 is exactly the starting address of the .got.plt table!

Take a look at the second green rectangle:

mov -0x18(%ebx),%eax: First subtract the result of 0x18 from %ebx and store it in %eax, the result is: %eax = 0x1fe8, this address is exactly the virtual address of variable b in the .got table.

movl $0x1f,(%eax): Store 0x1f (31 in decimal) in the memory unit corresponding to the address stored in the 0x1fe8 entry (a certain location in the data segment of libb.so).

Therefore, after the linker performs relocation, the real address of the variable b is stored in the 0x1fe8 entry, and the above two steps assign the value 31 to the variable b.

The third green rectangular box is to call the function func_b, which is a little more complicated, jump to the symbol func_b@plt, and look at the disassembly code:

The jmp instruction calls the function pointer at %ebx + 0xc. As can be seen from the above .got.plt layout diagram, the address of the func_b function is stored in this entry after relocation (the code segment in libb.so A certain position), so it jumps to the function correctly.


------ End ------
 

The above is the understanding process I compiled when I was learning dynamic links. If there are any mistakes in understanding or expression in the text, please correct me, thank you very much!

Guess you like

Origin blog.csdn.net/m0_69424697/article/details/125103561