倒着来理这个过程。
首先我们明确最终要生成一个可执行文件: .out. 他的最终运行必须包含所有的指令(BINARY)、所有的数据、所有的变量、所有的子函数。
问题1就来了:这些内容在执行件里面有没有什么“区别”对待?
有的。因为关心LINUX下的ELF类型可执行件比较多,这里以它为例来看看。当然对于一个目标文件,可以是可重定位的,也可以是可执行的。
我们马上来看看可重定位和可执行:
Object files comes in three forms:
Relocatable object file, which contains binary code and data in a form that can be combined with other
relocatable object files at compile time to create an executable object file.
Executable object file, which contains binary code and data in a form that can be directly loaded into
memory and executed.
Shared object file, which is a special type of relocatable object file that can be loaded into memory and
linked dynamically, either at load time or at run time.
我们从连接的角度来看ELF文件的内容:ELF header, Program header table(optional),Sections...,Section header table。
ELF header:包括了整个文件的总体信息,比如:section header table从哪里开始,有多少个section header,每个section header多少字节,同样还有关于Program header的等等。对于32位系统的ELF文件来说,它的前52个字节就是ELF header了。同理,64位系统的ELF文件的前64个字节是它的ELF header。
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
Section header table:由于Program header table是不一定有的,所以这里重点看一下Section header table。这也是回答问题1的关键。为什么要有这个,因为里面有不止一个section,具体有:
.text, the machine code of the compiled program.
.rodata, read-only data, such as the format strings in printf statements.
.data, initialized globalvariables.
.bss, uninitialized global variables. BSS stands for block storage start, and this sectionactually occupies no
space in the object file; it is merely a placer holder.
.symtab, a symbol table with information aboutfunctions and global variables defined and referenced in the
program. This table does not contain any entries for local variables; those are maintained on the stack.
.rel.text, a list of locations in the .text section thatneed to be modified when the linker combines this object
file with other object files.
.rel.data, relocation information for global variables referenced but not defined in the current module.
.debug, a debugging symbol table with entries for local and global variables. This section is present only if
the compiler is invoked with a -g option.
.line, a mapping between line numbers in the original C source program and machine code instructions in
这样就清楚了不同代码最终的角色。这些Sections是这个ELF文件的主要内容,在使用ELF文件时,从ELF header找到Section header table,并且通过这个Section header table里面关于每个section的位置信息等定位到具体的section。
问题2怎么形成可执行的ELF文件?
来理一下编译连接这个过程:在LINUX下,首先我们的源文件.c文件经过cpp预处理生成中间件.i,接着经过ccl编译生成汇编代码.s,然后经as汇编器编译生成.o;这里的几个工具都是标准GCC的部分。多个.o文件经过连接器连接生成.out可执行文件。如通过./*.out来执行该可执行文件,SHELL将调用loader来将可执行文件的数据和代码复制到内存,然后跳转到程序的第一条指令处将控制权交给程序。
因为这里主要在分析LINKER和LOADER,来看看这两个工具在整个过程中的工作。
LINKER:因为前面已经讲到了,一个程序里面可能会调用各种子函数,对子函数的调用是通过符号来实现的,LINKER就通过标记这些符号的位置然后用相应的调用信息来取代它们。
LOADER:它需要将disk上的程序拷贝到内存,使其处于准备运行状态。这个过程中也可能会重新分配存储空间或者映射虚拟地址到DISK页。
但是在两个过程之间还缺少了一个过程,因为在汇编器生成的.o文件中,代码都是从0地址开始的,而实际的ELF文件是经过“整理”了的(分成了各个段)。这就需要将相同类型不同section合并来指定加载域,同时 code and data section 也重新调整过,使得指向正确的运行域。在这个过程中基本的操作单位是section。这个过程叫做Relocation。实际上LINKER和LOADER都可以用来做这个事情。
问题3,细化一下LINKER的符号操作和库连接。
符号: 首先来看一下符号的种类。
Global symbols defined by the module and referenced by other modules. All non-static functions and global
variables fall in this category.
Global symbols referenced by the input module but defined elsewhere.All functions and variables with
extern declaration fall in this category.
Local symbols defined and referenced exclusively by the input module. All static functions and static
variables fall here.
LINKER是通过将没一个符号替换为一个独一无二的定义来实现符号操作的。符号的性质有强弱之分:At compile time, the compiler exports each global symbol as either strong or weak.Functions and initialized globalvariables get strong weight, while global uninitialized variables are weak. Now, the linker resolves the symbols using
the following rules:
Multiple strong symbols are not allowed.
Given a single strong symbol and multiple weak symbols, choose the strong symbol.
Given multiple weak symbols, choose any of the weak symbols.
静态库archive:静态库(.a)是一系列类似的目标文件的集合,它有固有的格式和相应的指令来使其更好地被LINKER使用。连接的时候我们将Relocatable object file和静态库列在命令行中,LINKER从左到右首先判断该文件是哪个类型的,如果是an object file,那么LINKER提取其中的各部分:a set of O, relocatable object files that go into the executable; a set U, unresolved symbols; and a set of D, symbols defined in previous input modules. 如果是静态库,那么在其中查找各模块来匹配set U中的unresolved symbols,匹配到的就将该模块加入到set of O中,每次找到这样的模块的同时更新set U和set D,这个过程要对每一个object files执行。命令行中所有的文件都经过以上2个操作之后,看一下U是否为空,空了再把相同的段合并并relocates (下文) object files in O,生成可执行文件,否则报错并终止。所以静态库需要放在LINKER命令最后,避免有些符号找不到库中相应的模块。如果库之间也有相互依赖,那也需要考虑先后顺序。如果库中有多处定义同一个符号,则选择第一个定义。
问题4,细化一下LINKER的Relocation。
这之前,汇编器在遇到一个未定义符号的时候,会生成一个相应的relocation entry,并存放在.relo.text/.relo.data sections。帮助接下来对未定义符号的relocate。relocation entry中包括了以下信息:
Offset, a section offset of the reference that needs to be relocated. For a relocatable file, this value is the byteoffset from the beginning of the section to the storage unit affected by relocation.
Symbol, a symbol the modified reference should point to. It is the symbol table index with respect to which
the relocation must be made.
Type, the relocation type, normally R_386_PC32, that signifies PC-relative addressing. R_386_32 signifies
absolute addressing.
1 LINKER在处理完符号和库的操作后,需要进行Relocation。将所有相同性质的section合并,并给这些合并后的section、其中的每个section以及每个符号指定运行时的内存地址。
2 Relocating symbol reference within sections. In this step, linker modifies every symbol reference in the code
and data sections so they point to the correct loadtime addresses.将调用和参考的符号进行替换(用地址或者内容)。
问题5,连接动态库。
以上是基于静态库的机理。至于动态库和静态库的优缺点这里就不多说了。
在指定生成动态连接的可执行文件时,事实上这个可执行件并不完全,还得依赖于相应动态库.so的存在,它只包含了关于怎么在运行时调用.so中的信息的some relocation and symbol table information。但是它特别包括的是.interp section(that contains the name of the dynamic linker, which itself is a shared object on Linux systems (ld-linux.so). So, when the executable is loaded intomemory, the loader passes control to the dynamic linker.The dynamic linker contains some start-up code that maps the shared libraries to the program's address space. It then does the following:
relocates the text and data of libfoo.so into memory segment; and
relocates any references in a.out to symbols defined by libfoo.so.
Finally, the dynamic linker passes control to the application. From this point on, location of shared object is fixed in the memory.)由dynamic linker来做接下来的连接工作。