对于源文件a.c,编译为目标文件a.o要经过预处理、编译和汇编3个步骤。多个目标文件生成可执行文件要经过链接处理。
源文件a.c的源码如下:
1 2 3 4 5 | #define NUM_2 2 int double_num( int num) { return num * NUM_2; } |
预处理一个重要的作用是宏替换,预处理命令如下。
cpp a.c a.i
或
gcc -E a.c -o a.i
输出结果为:
1 2 3 4 5 6 7 8 9 10 11 12 | # 1 "a.c" # 1 "<built-in>" # 1 "<command-line>" # 1 "/usr/include/stdc-predef.h" 1 3 4 # 1 "<command-line>" 2 # 1 "a.c" int double_num( int num) { return num * 2; } |
编译就是将c语言代码编译为汇编代码,编译命令如下:
gcc -S a.i -o a.S
输出结果为汇编文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | .file "a.c" .text .globl double_num .type double_num, @function double_num: .LFB0: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 movl 8(%ebp), %eax addl %eax, %eax popl %ebp .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc .LFE0: .size double_num, .-double_num .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609" .section .note.GNU-stack,"",@progbits |
汇编就是将汇编代码翻译为机器二进制码。汇编命令如下:
as a.S -o a.o
或
gcc -c a.S -o a.o
汇编的结果为目标文件(object file),其为二进制。
如果还有一个源文件b.c,其包含main入口函数,调用double_num(),使用上述相同的方法编译出目标文件b.o,然后和a.o可链接为可执行文件。
1 2 3 4 5 6 7 8 | #include <stdio.h> extern int double_num( int ); void main() { printf ( "%d\n" , double_num(2)); } |
链接命令如下:
gcc a.o b.o -o a.out
也可以用ld命令,gcc做了很多工作,可以使用-v查看gcc的链接过程。
加载器执行程序加载,链接器执行符号解析,它们都能执行重定向。
程序加载,从硬盘拷贝程序镜像到内存,并将程序置为准备运行状态。程序加载也包括分配存储空间和映射虚拟地址到磁盘页。
Relocation,编译器和汇编器产生的每个模块的目标代码从0开始编址,重定向就是为不同部分分配加载地址。代码段和数据段也被调整到正确的运行地址。
Symbol Resolution,子程序引用其它子程序是通过符号实现的,链接器的作用就是解析相关符号。
目标文件有3种形式:
Relocatable object file, 包含二进制码和数据,它们可以和其它的可重定向文件结合成可执行目标文件。
Executable object file, 包含可执行载入到内存并执行的二进制码和数据。
Shared object file, 一种特殊的可重定向目标文件,在加载时或运行时动态的载入内存和链接。
编译器和汇编器产生可重定向目标文件和共享目标文件,链接器产生可执行目标文件。目标文件的格式随系统不同而变化,早期的System V使用COFF格式。Windows使用COFF的变种PE (portable excutable) 格式。IBM使用IBM 360格式,现代Unix系统,包括Linux和Solaris使用Unix ELF (executable and linking format)格式。
ELF Header包括
.text | CPU指令 |
.rodata | 只读数据段 |
.data | 已初始化的全局变量段 |
.bss | block storage start, 未初始化的全局变量。在目标文件中不实际占用空间。 |
.symtab | 函数和全局变量的符号表。定义在目标文件中或者被目标文件引用。 |
.rel.text | .text section的地址列表,当链接器和其它目标文件结合时,这些地址需要重定向。 |
.rel.data | 引用的全局数据的重定向信息。 |
.debug | 使用-g选项时才有,局部和全局变量的调试符号表。 |
.line | .text中机器码和c源码之间对应的行号。 |
.strtab | .symtab和.debug等中的字符串表。 |
等。ELF文件以4字节的magic string: 0x7F ELF 开始。
每一个可重定向目标文件都包含符号表和相关的符号。在链接器上下文中,包含下述类型符号。
本模块定义,可被其它模块引用的全局符号 Global symbols,包括所有的非静态全局变量和函数。
本模块引用,定义在其它地方的全局符号,包括所有的extern声明的函数和变量。
定义并本地引用的局部符号 (Local Symbols), 所有的静态函数和静态变量。
链接器通过关联每个引用到唯一的定义来解析符号。
符号定义分为强定义和弱定义,函数和初始化全局变量是强定义,未初始化全局变量是弱定义。全局符号解析有如下规则:
不允许多个强定义。
单个强定义和多个弱定义,选择强定义符号。
多个弱定义符号,选择任何一个。
如下一个强定义,多个弱定义是允许的:
1 2 3 | // a.c int a = 1; int a; |
1 2 | // b.c int a; |
静态库是目标文件的集合,以archive的形式存储在磁盘上,ELF archive以8字节的!<arch>\n开头。静态库传给链接器和目标文件合并成可执行文件,最终的可执行文件仅包含静态库中被引用到的变量和函数。
在处理包含静态库的符号解析时,链接器按照命令行输入参数的顺序从左自右扫描object files和archives。扫描过程中,链接器维护3个集合。O,可重定向目标文件;U,未解析符号,D,定义在之前输入模块的符号。开始时这3个集合全部为空。
对于命令行中的每一个参数,链接器决定是object file还是archive,如果是object file,添加到O,并更新U和D。
如果输入是archive,链接器扫描archive的成员模块,如果一些archive成员定义了U中未解析的成员,将他们添加到O中。然后根据archive member中的每个符号更新U和D。
所有的输入参数处理后,如果U为非空,打印错误。否则merge and relocates O中的所有目标文件,构件输出可执行文件。
这也解释了为什么静态库通常放在链接器参数的最后。静态库中有未定义符号的需要特别注意。
一旦链接器解析了所有的符号,每个符号引用已经有唯一的定义了。此时,链接器就开始处理重定向,这包括两个步骤。
重定向段和符号定义,链接器合并所有的相同类型的section为同一个section。链接器分配运行时地址给新合并的section、模块输入的section和每一个符号,从而每条指令和全局变量都有唯一的loadtime address。
重定向段中的符号引用。链接器修改代码段和数据段中的符号引用,使他们指向正确的加载地址。
当汇编器统计到未解析引用,他产生那个对象的重定向入口,并放到.rel.text/.rel.data段。一个重定向入口(relocation entry)包含如何解决引用的信息。典型的ELF重定向入口包含下述成员。
段中偏移
指向符号的符号表索引
类型,对于X86架构,R_386_PC32,表示PC相对地址,R_386_32表示绝对地址。
链接器重定向所有的重定向目标,对于R_386_PC32,重定向地址为S+A-P,对于R_386_32,地址位S+A。
共享库可被加载到任何内存地址,编译共享库可以使用下述命令
gcc -shared -fPIC -o libfoo a.o b.o
-fPIC高速编译器生成位置无关的代码。
当执行
gcc bar.o ./libfoo.so
生成可执行文件a.out,但是a.out并不包含目标模块a.o和b.o。a.out仅包含重定向和符号信息。这个信息用于在运行时引用libfoo.so中的代码和数据。
可执行文件也包含.interp段,包含动态链接器(dynamic linker)的名字。动态链接器本身是共享目标文件,在Linux系统中,它是ld-linux.so。
当加载器把可执行文件载入内存后,把控制权交给动态链接器,动态连机器包含一些启动代码,它映射动态链接库到程序的地址空间,然后:
重定向libfoo.so的代码和数据到内存片。
重定向a.out中所有到libfoo.so的引用。
共享库也可以在程序运行时加载,程序可以要求动态加载器加载和链接共享库,深圳不需要链接这些共享库到可执行文件,Linux提供系统调用,如dlopen, dlsym和dlclose来支持动态加载。
ar | 创建静态库 |
objdump | 显示目标文件的所有信息 |
strings | 列出所有可打印字符串 |
nm | 列出所有定义的符号 |
ldd | 列出目标文件依赖的共享库 |
strip | 删除符号表信息 |