Dialysis of C++ Compilation & Linking under Linux from Four Problems

Abstract: Compilation & linking is both familiar and unfamiliar to C&C++ programmers. Familiarity lies in the compilation & linking process of every code. The unfamiliarity is that most people do not deliberately pay attention to the principle of compilation & linking. This article explores the issues of C++ compilation & linking under 64-bit Linux through four typical problems encountered during the development process.

Compilation principle:

The following simplest C++ program (main.cpp) is compiled into an executable target program. In fact, it can be divided into four steps: preprocessing, compilation, assembly, and linking.

g++ main.cpp -v see the detailed process, but now the compiler has merged the preprocessing and compilation process.

Preprocessing: g++ -E main.cpp -o main.ii, -E means only preprocessing. The preprocessing mainly deals with the expansion of various macros; adding line numbers and file identifiers to facilitate the compiler to generate debugging information; deleting comments; retaining the compiler instructions used by the compiler, etc.

Compile: g++ -S main.ii -o main.s, -S means only compile. Compilation is to generate assembly code after a series of lexical analysis, syntax analysis and optimization on the basis of preprocessed files.

Assembly: g++ -c main.s -o main.o. Assembly is the conversion of assembly code into instructions that can be executed by the machine.

Link: g++ main.o. Linking generates executable programs. The reason why we need to link is because our code cannot be as simple as main.cpp. Modern software has hundreds of millions of lines. If written in a main.cpp, it is not conducive to division of labor and cooperation, nor can it be maintained. , So it is usually composed of a bunch of cpp files. The compiler compiles each cpp separately. These cpp will reference functions or global variables in other modules. When compiling a single cpp, it is impossible to know their exact addresses, so After the compilation is completed, the linker is required to set various symbols (functions, variables, etc.) that do not have accurate addresses to the correct values, so that they can be assembled together to form a complete executable program.

Question 1: Header file occlusion

The most weird problem in the compilation process is the occlusion of the header file. In the following code, main.cpp contains the header file common.h, and the header file that you really want to use is the one on the far right in the figure that contains name.

The file of the member (the directory is ./include), but common.h (the directory is ./include1) in the middle of the compilation process was discovered first, causing the compiler to report an error: The Test structure has no name member. For the programmer , I clearly defined the member of name, but said that there is no member of name. If you encounter this situation for the first time, you may doubt your life. To deal with this weird problem, we can use the -E parameter to see the output of the compiler after preprocessing, as shown in the figure below.

The format of the preprocessing file is as follows: # linenum filename flag, which means that the following content is expanded from the linenum line of the file named filaname, the value of flag can be 1, 2, 3, 4, and can be separated by spaces Multi-value, 1 means that a new file will be expanded next; 2 means that a file is expanded; 3 means that the next content comes from a system header file; 4 means that the next content should be imported in the form of extern C.

From the expanded output, we can clearly see that the Test structure does not define the name member, and the Test structure is defined in common.h in ./include1. The truth is now clear, the compiler is useless at all. The defined Test structure was truncated by another header file with the same name. We can solve the problem by adjusting -I or adding a partial path to the header file to specify the location of the header file in more detail.

Target file:

The compilation link will eventually generate various target files. The target file format under Linux is ELF (Executable Linkable Format). For detailed definitions, see the header file /usr/include/elf.h. Common target files include: relocatable target files, and That is, the object files ending with .o, of course static libraries are also classified into this category; executable files, such as the a.out file compiled by default; shared object file .so; core dump files, which are output after core dump file. The Linux file format can be viewed through the file command.

A typical ELF file format is shown in the figure below. The file has two perspectives: the compilation perspective, which uses the section header table as the core organization program; the operation perspective, the program header table uses the segment as the core organization program. This is mainly to save storage. Many fragmented sections will cause a lot of memory waste due to alignment requirements at runtime. At runtime, sections with similar permissions are usually organized into segments and loaded together.

You can view the contents of the ELF file through the commands objdump and readelf.

Common sections for relocatable target files are:

Symbol resolution:

The linker will modify the reference to the external symbol to the correct address of the referenced symbol. When the corresponding definition cannot be found for the referenced external symbol, the linker will report an error of undefined reference to XXXX. Another case is that the definition of multiple symbols is found. In this case, the linker has a set of rules. Before describing the rules, you need to understand the concepts of strong symbols and weak symbols. Simply speaking, functions and initialized global variables are strong symbols, and uninitialized global variables are weak symbols.

The linker processing rules for multiple definitions of symbols are as follows (the author looks like rules 2 and 3 are handled as 1 on gcc 7.3.0):

1. Multiple strong symbol definitions are not allowed, the linker will report a seemingly error of repeated definitions

2. If a strong symbol and multiple weak symbols have the same name, select the strong symbol

3. If the symbol is weak in all target files, choose the one that takes up the largest space

With these foundations, let's first look at the static linking process:

1. The linker scans object files and static libraries in the order in which the command line appears from left to right

2. The linker maintains a collection E of object files, a collection U of unresolved symbols, and a collection of symbols D defined in E. The initial states E, U, and D are all empty

3. For each file f on the command line, the linker will determine whether f is an object file or a static library. If it is an object file, then f is added to E, and undefined symbols in f are added to U, and symbols are defined Add to D, continue to the next file

4. If it is a static library, the linker tries to match an undefined symbol in U in the object file of the static library. If m matches a symbol in U, then m will be processed in the same way as file f in the previous step, for each member Files are processed in sequence until U and D are unchanged, and the member files not included in E are simply discarded

5. After all input files are processed, if there are still symbols in U, an error occurs, otherwise the link is normal and the executable file is output

Question 2: Static library order

As shown in the figure below, main.cpp depends on liba.a, and liba.a depends on libb.a. According to the static linking algorithm, if you use g++ main.cpp liba.a libb.a, the order of libb.a can be linked normally because liba.a is resolved. When the undefined symbol FunB is added to the U of the above algorithm, and then the definition is found in libb.a, if you compile in the order of g++ main.cpp libb.a liba.a, you cannot find the definition of FunB because it is statically linked Algorithm, U is empty when parsing libb.a, so there is no need to do any analysis, and libb.a is simply discarded, but when parsing liba.a, it is found that FunB is not defined, resulting in U being not empty and linking errors. Therefore, when doing static linking, you need to pay special attention to the order of libraries. Static libraries that reference other libraries need to be placed first. When linking many libraries, you may need to make some library adjustments to make the dependencies clearer.

Dynamic link:

Most of the previous content is related to static linking, but static linking has many shortcomings: it is not conducive to update, as long as there is a change in a library, it needs to be recompiled; it is not conducive to sharing, each executable program is reserved separately, which is not good for memory And disk is a great waste.

To generate a dynamic link library, you need to use the parameter "-shared -fPIC" to indicate that you want to generate a position-independent PIC (Position Independent Code) shared object file. For static linking, the entire linking process is completed when the executable object file is generated, but if you want to achieve the effect of dynamic linking, you need to split the program into relatively independent parts according to the modules, and link them into one when the program is running A complete program. At the same time, in order to realize the code sharing between different programs, it is necessary to ensure that the code is position-independent (because the virtual address of the shared object file is loaded in each program is different, ensure that it can be loaded no matter where it is Work), and in order to achieve position independence, it relies on a premise: the distance between the data segment and the code segment always remains the same.

Since no matter how a target module is loaded in the memory, the distance between the data segment and the code segment is unchanged, the compiler introduces a global offset table GOT (Global Offset Table) before the data segment, the referenced global variable or The function has a record in the GOT, and the compiler generates a relocation record for each entry in the GOT. Because the data segment can be modified, the dynamic linker will relocate each entry in the GOT when it is loaded. PIC is realized.

The general principle is basically the same, but in the specific implementation, the handling of functions and global variables are different. Since there are thousands of large-scale program functions, and the program may only use a small part of them, it is not necessary to relocate all functions when loading, and only revise the addresses when they are used. For this reason, the compiler introduces the procedure link table PLT (Procedure Linkage Table) to realize delayed binding. In the code segment, PLT points to the address corresponding to the function in the GOT. When it is called for the first time, GOT stores not the actual address of the function, but PLT jumps to the next instruction address of the GOT code, so that the first pass PLT jumps to GOT, and then transfers back to the next instruction of PLT through GOT, which is equivalent to doing nothing. The code immediately after PLT will put the parameters needed for dynamic linking into the stack, and then call the dynamic linker to correct the GOT. From then on, the address where the code in the PLT jumps to the GOT is the real address of the function, thus realizing the so-called delayed binding.

For shared target files, there are several sections that need attention:

With the above foundation, let's look at the process of dynamic linking :

1. Program execution will jump to the dynamic linker during loading

2. The dynamic linker bootstrapping completes its own relocation work through GOT and .dynamic information

3. Load the shared object file: merge the executable file and the linker's own symbols into the global symbol table, traverse the shared object files in breadth order, and their symbol tables will be continuously merged into the global symbol table. If multiple shared objects have the same Symbol, the shared object file loaded first will shield the following symbols

4. Relocation and initialization

Question 3: Global symbol intervention

The most critical step 3 in the dynamic linking process can be seen, when multiple shared object files contain the same symbol, the symbol that is loaded first will occupy the global symbol table, and the same symbol in subsequent shared object files will be ignored . When our code does not handle the naming well, it will cause very strange errors. If you are lucky, you will immediately core dump. Unfortunately, you won’t get an inexplicable core dump until the program runs for a long time, and you will never get a core dump but the result is incorrect.

As shown in the figure below, the symbols of two dynamic libraries libadd.so and libadd1.so will be used in main.cpp. We will focus on

In the processing of the Add function, when we compile with g++ main.cpp libadd.so libadd1.so, the program outputs "Add in add lib" indicating that Add is the symbol (add.cpp) in libadd.so, when When we compile with g++ main.cpp libadd1.so libadd.so, the program outputs "Add in add1 lib" indicating that Add is a symbol in libadd1.so. At this time, the problem is big, and the caller main.cpp thinks that Add There are only two parameters, and add1.cpp thinks that Add has three parameters. If there are such codes in the program, it can be foreseen that it may cause huge confusion. For specific symbol analysis, we can observe the parsing process of Add through LD_DEBUG=all ./a.out, as shown in the following figure: The left side corresponds to the situation where libadd.so is placed in the front during compilation, and Add is bound in libadd.so. The right corresponds to the situation where libadd1.so is put in front, and Add is bound in libadd1.so.

Load dynamic library at runtime:

With the support of dynamic linking and shared object files, Linux provides a more flexible module loading method: by providing dlopen, dlsym, dlclose, and dlerror APIs, modules can be dynamically loaded at runtime, thereby achieving plug-in Features.

The following code demonstrates the process of dynamically loading the Add function, add.cpp compiles "g++ -fPIC –shared –o libadd.so add.cpp" as normal to libadd.so, and main.cpp compiles with "g++ main.cpp -ldl" As a.out. In main.cpp, a handle void *handle is first obtained through the dlopen interface, and then the symbol Add is searched from the handle through dlsym, and after it is found, it is converted into the Add function, and then it can be used according to the normal function, and finally dlclose closes the handle. Any errors can be obtained through dlerror.

Question 4: Static global variables and dynamic libraries lead to double free

After fully understanding the knowledge of dynamic linking, let's look at a problem caused by the entanglement of static global variables and dynamic libraries. The code is as follows. There is a static global object foo_ in foo.cpp, and foo.cpp will be compiled into a libfoo. a, bar.cpp depends on libfoo.a library, it will be compiled into libbar.so, main.cpp depends on both libfoo.a and libbar.so.

The compiled makefile is as follows:

Running a.out will cause a double free error. This is caused by calling the destructor twice in one location. The reason for this is because the static library that is linked first when linking resolves the symbol of foo_ into a global variable in the static library. When libbar.so is dynamically linked, because there is already a symbol foo_ globally, the global symbol is involved, The reference to foo_ in the dynamic library will point to the version in the static library, resulting in the final destruction of the same object twice.

The solution is as follows:

1. Do not use global objects

2. Reverse the order of the libraries when compiling, put the dynamic library first, so that there will only be one foo_ object globally

3. All use dynamic library

4. Control the visibility of symbols through compiler parameters.

to sum up:

Through the four problems encountered in the compilation and linking, these matters of the compilation and linking have been basically covered. With these foundations, it should be possible to deal with the general compilation and link problems in daily work. Due to limited space, the article omits a lot of details, mainly focusing on the principle of the large framework. If you want to dig deeper into the relevant details, you can participate in the relevant references and read the header files related to elf.h.

references:

1. "Linker and Loader"

2. "In-depth understanding of computer systems"

3. "Programmer's Self-cultivation"

4.   http://www.gnu.org/software/binutils/

Note 1 : The tools involved in this article can be obtained from http://www.gnu.org/software/binutils/ for detailed information

Note 2 : In the sample code picture of this article, the white area below each window has the file name corresponding to this code, please note that the corresponding text is described

 

Click to follow and learn about Huawei Cloud's fresh technology for the first time~

Guess you like

Origin blog.csdn.net/devcloud/article/details/108822827