您的位置:首页 > 运维架构 > Linux

linux 生成可执行文件的链接过程和原理

2011-05-16 19:37 597 查看

2.2 Overview of Linkers and the Linking Process

Figure 2.2

illustrates how different tools take various input files and generate
appropriate output files to ultimately be used in building an executable
image.

Figure 2.2:

Creating an image file for the target system.

The developer writes the program in the C/C++ source
files and header files. Some parts of the program can be written in
assembly language and are produced in the corresponding assembly source
files. The developer creates a makefile

for the make

utility to facilitate an environment that can easily track the file
modifications and invoke the compiler and the assembler to rebuild the
source files when necessary. From these source files, the compiler and
the assembler produce object files that contain both machine binary code
and program data. The archive utility concatenates a collection of
object files to form a library. The linker takes these object files as
input and produces either an executable image or an object file that can
be used for additional linking with other object files. The linker
command file instructs the linker on how to combine the object files and
where to place the binary code and data in the target embedded system.

The main function of the linker is to combine multiple
object files into a larger relocatable object file, a shared object
file, or a final executable image. In a typical program, a section of
code in one source file can reference variables defined in another
source file. A function in one source file can call a function in
another source file. The global variables and non-static functions are
commonly referred to as global symbols.

In source files, these symbols have various names, for example, a global variable called foo_bar

or a global function called func_a

.
In the final executable binary image, a symbol refers to an address
location in memory. The content of this memory location is either data
for variables or executable code for functions.
The compiler creates a symbol table containing the
symbol name to address mappings as part of the object file it produces.
When creating relocatable output, the compiler generates the address
that, for each symbol, is relative to the file being compiled.
Consequently, these addresses are generated with respect to offset 0.
The symbol table contains the global symbols defined in the file being
compiled, as well as the external symbols referenced in the file that
the linker needs to resolve. The linking process performed by the linker
involves symbol resolution and symbol relocation.
Symbol resolution

is the process
in which the linker goes through each object file and determines, for
the object file, in which (other) object file or files the external
symbols are defined. Sometimes the linker must process the list of
object files multiple times while trying to resolve all of the external
symbols. When external symbols are defined in a static library, the
linker copies the object files from the library and writes them into the
final image.
Symbol relocation

is the process
in which the linker maps a symbol reference to its definition. The
linker modifies the machine code of the linked object files so that code
references to the symbols reflect the actual addresses assigned to
these symbols. For many symbols, the relative offsets change after
multiple object files are merged. Symbol relocation requires code
modification because the linker adjusts the machine code referencing
these symbols to reflect their finalized addresses. The relocation table
tells the linker where in the program code to apply the relocation
action. Each entry in the relocation table contains a reference to the
symbol table. Using this reference, the linker can retrieve the actual
address of the symbol and apply it to the program location as specified
by the relocation entry. It is possible for the relocation table to
contain both the address of the symbol and the information on the
relocation entry. In this case, there is no reference between the
relocation table and the symbol table.
Figure 2.3

illustrates these two concepts in a simplified view and serves as an example for the following discussions.

Figure 2.3:

Relationship between the symbol table and the relocation table.

For an executable image, all external symbols must be
resolved so that each symbol has an absolute memory address because an
executable image is ready for execution. The exception to this rule is
that those symbols defined in shared libraries may still contain
relative addresses, which are resolved at runtime (dynamic linking).

A relocatable object file may contain unresolved
external symbols. Similar to a library, a linker-reproduced relocatable
object file is a concatenation of multiple object files with one main
difference—the file is partially resolved and is used for further
linking with other object files to create an executable image or a
shared object file. A shared object file has dual purposes. It can be
used to link with other shared object files or relocatable object
modules, or it can be used as an executable image with dynamic linking.
 
开发者在c/c++源文件和头文件中编写程序。部分程序可用汇编编写,并放在相应的汇编源文件中。开发者为make工具创建一个makefile文件,这
样可以轻松追踪文件的修改并根据需要调用编译器和汇编器重新build源文件。编译器和汇编器从这些源文件中生成包含机器码和程序数据的目标文件。归档工
具连接一组目标文件形成一个库。链接器把这些目标文件作为输入并生成一个可执行映像或是可被用于和其他目标文件链接的目标文件。链接器命令文件指示链接器
怎样组合目标文件以及把二进制码和数据放在目标嵌入式系统的哪个位置。

 

链接器的主要功能是把多个目标文件组合成一个更大的可重定位
的目标文件、一个共享目标文件或是一个最终的可执行映像。在一个典型的程序里,一个源文件中的一段代码可引用另一个源文件中定义的变量。一个函数在某个源
文件中可调用另一个源文件中的函数。这些全局变量和非静态函数就是通常所说的全局符号。在源文件中,这
4000
些符号有不同的名称。在最终的可执行二进制映像中,
一个符号对应一个内存中的地址。这个内存地址的内容是变量的数据或者是函数的可执行代码。

 

编译器创建一个包含符号名到地址的映射的
符号表作为它生成的目标文件的一部分。当创建可重定位的输出文件时,编译器为每个符号生成一个与被编译文件相关的地址。因此,这些地址是按照0偏移量生成
的。符号表包含定义在被编译文件中的全局符号以及在此文件中被引用的外部符号,这些外部符号要由链接器解析。由链接器执行的链接过程调用符号解析和符号重
定位。

 
符号解析

是这样一个过程:链接器进入每个目标文件并为该目标文件检查其外部符号在哪个或哪些其他目标文件中定义的。有时,当链接器试图解析所有的外部符号时,它必须多次处理这些目标文件。若外部符号定义在一个静态库中,链接器将从库中复制目标文件并写入最终的映像(静态链接)。

 
符号重定位


链接器把一个符号引用映射到它的定义的过程。链接器修改被链接目标文件的机器代码,以便对应于那些符号的代码反映出分配给这些符号的实际地址。对于很多符
号,在多个目标文件被合并后,它的相对偏移量就变了。符号重定位要求代码修改,因为链接器调整引用这些符号机器的代码来反映它们最终的地址。重定位表告诉
链接器在程序代码中哪里要求重定位。重定位表中的每个入口包含一个对符号表的引用。链接器可以使用这个引用检索符号的实际地址并把它用于程序中那个由重定
位入口指定的位置。对于重定位表,同时包含符号地址和重定位入口信息是可以的,这种情况下就没有重定位表和符号表之间的引用了。

 

对于可执行映像,所有的外部符号必须被解析以便每个符号有一个绝对的内存地址,因为可执行映像是准备执行的。也有例外,那些符号如果被定义在共享库中将依然包含相对地址。这些符号将在运行时解析(动态链接)。
 
 
以上是别人翻译的链接的过程,静态库和目标文件编译链接成可执行文件比较简单,就是进行符号解析和符号重定准,通过一定编译出来的最后的目标文件,修改最终的符号地址,生成可执行文件,最麻烦的就是动态链接库,其主要思想就是在程序加载时,通过重定位表的地址信息,修改目标文件的符号地址,把符号地址映射到真实的函数地址去。
 
 
首先,来看一下可执行文件的加载过程:
1:内核首先读ELF文件的头部,然后根据头部的数据指示分别读入各种数据结构,找到标记为可
加载(loadable)的段,并调用函数 mmap()把段内容加载到内存中。在加载之前,内核把段的标记直接传递给
mmap(),段的标记指示该段在内存中是否可读、可写,可执行。显然,文本段是只读可执行,而数据段是可读可写。这种方式是利用了现代操作系统和处理器
对内存的保护功能。著名的Shellcode(参考资料 17)的编写技巧则是突破此保护功能的一个实际例子。
2:内核分析出ELF文件标记为 PT_INTERP 的段中所对应的动态连接器名称,并加载动态连接器。现代 LINUX 系统的动态连接器通常是 /lib/ld-linux.so.2,相关细节在后面有详细描述。
3:内核在新进程的堆栈中设置一些标记-值对,以指示动态连接器的相关操作。
4:内核把控制传递给动态连接器。
5:动态连接器检查程序对外部文件(共享库)的依赖性,并在需要时对其进行加载。
6:动态连接器对程序的外部引用进行重定位,通俗的讲,就是告诉程序其引用的外部变量/函数的地址,此地址位于共享库被加载在内存的区间内。动态连接还有一个延迟(Lazy)定位的特性,即只在"真正"需要引用符号时才重定位,这对提高程序运行效率有极大帮助。
7:动态连接器执行在ELF文件中标记为 .init 的节的代码,进行程序运行的初始化。在早期系统中,初始化代码对应函数 _init(void)(函数名强制固定),在现代系统中,则对应形式为
void
__attribute((constructor))
init_function(void)
{
……
}

其中函数名为任意。
8:动态连接器把控制传递给程序,从 ELF 文件头部中定义的程序进入点开始执行。在 a.out 格式和ELF格式中,程序进入点的值是显式存在的,在 COFF 格式中则是由规范隐含定义。
 
 
从以上看来,其主要做两件事情,回载程序到内存,对外部符号进行重定位,让人陌生的还是重定位,不过也好,现在两个链接终于碰到了一起。
 
 
让我看看怎么实现动态链接的:
1。整个系统有一张POT,全局的Global Offset Table,有且只有一个,里面每条项目都代码一个全局的函数和变量的入口地址。
2.每个进程有一个PLT,局部的Procedure Linkage Table,第个进程有且只有一个,里面每期对应的本地符号对应全局的地址值,一个跳转语句。
 
为什么要有两个表?got表中每一项都是本运行模块要引用的全局变量或函数的地址,可以用got表来间接引用全局变量。函数也可以把got表的首地址作为一个基准,用相对该
基准偏移量来引用静态函数。由于动态链接器(ld-linux.so)不会把运行模块加载到固定地址,在不同进程的地址空间中各运行模块的绝对地址、相对
地址都不同。这种不同反映到got表上,京是每个进程的每个运行模块都有独立的got表,所以进程间不能共享got表。
 
 
 





内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐