链接过程中的符号重定位_C底层18/2/25

前情提要

  • 对于编译和链接的基本过程,这里只对链接过过程的符号重定位做了解释,因为个人认为在链接过程中符号的重定位是最重要的一步,也是其精华所在,知道了这一步的实现过程可以解决很多问题,包括面试中可能会问到的关于extern等的方面。所以这里只有对符号重定位进行了详细的说明,如果想了解更多可以参看《程序员的自我修养》第2,3,6 章节。里面有很详细的解释。
    如果对虚拟地址空间的内存分布还不够了解的建议先看上一篇博客IA32位Linux内核中的虚拟地址映射
    其中的前面一部分有对虚拟内存分布的分析。

一,编译和链接的总体流程:

  1. 预编译:处理预编译指令(包括宏替换等#指令),删除注释。
    具体如下:
    1)将所有的“#define”删除,并且展开所有的宏定义
    2)处理所有的条件预编译指令,将被包含的文件插入到该预编译指令的位置,注意:这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
    3)删除所有的注释”//”,”/* */”。
    4)添加行号和文件名标识。e.g #2 “hello” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
    5)保留所有的#pragma 编译器指令。

  2. 编译阶段:进行一系列词法,语法,语义分析及优化后生成相应的汇编代码文件。(gcc -01最高级别优化),汇总符号。各个源文件的编译都是独立的。
    1)扩充gcc
    链接过程中的符号重定位_C底层18/2/25_第1张图片
    gcc命令只是对这些后台程序的包装,它会根据不同的参数要求去调用预编译程序ccl,汇编器as,链接器ld
    2)编译分为6步:
    (1)扫描:源代码输入到扫描器,进行简单的词法分析并将源代码的字符序列分割成一系列的记号。
    (2)语法分析:进入语法分析器,对扫描器产生的记号进行语法分析,产生语法树。
    (3)语义分析:由语义分析器完成,(编译器所能分析的语义是静态语义——在编译期间可以确定的语义。与之对应的动态语义就是 只有在运行期才能确定的语义)
    (4)源代码优化:在源代码级别上的优化(三地址码),生成中间代码,中间代码使得编译器可以被分为前端和后端,前端负责产生机器无关的中间代码,后端将中间代码转化为目标机器代码。
    后面的步骤为编译器后端
    (5)代码生成:将中间代码转化成目标机器代码。
    (6)目标代码优化:对上述目标代码进行优化(选择合适的寻址方式等)

  3. 汇编:把汇编指令翻译成二进制格式,生成各个段(section)符号表。这一步可生成二进制可重定位目标文件(main.o、main.obj)

  4. 链接
    知识储备:
    符号:用来表示一个地址,这个地址可能是一段子程序的起始地址,也可以是一个变量的起始地址。
    模块化程序:C/C++中有1,模块间的函数调用。2,模块间的变量访问。
    1)合并各个段(section),并调整其起始位移(段偏移)和段大小
    2)合并符号表并进行符号解析。将.o/.obj文件和.lib/.a库链接为.exe等文件。在解析完成之后就给符号分配虚拟内存地址。并且编译器不看局部(local)变量
    3)符号重定位。修改符号地址,每个要被修正的地方叫一个重定位入口.E.g给例如外部定义的符号(内存中为UND段)找到其定义的地址,并在链接阶段进行强弱符号的合并(C++中无强弱符号一说)原来外部声明的地址更新为具体定义的地址


看完了一大段晦涩的理论,来清新以下,看一下图文结合:

  • 代码
a.c文件:
#include 
extern int data_one;//外部声明变量
int sum(int a,int b);//外声明函数

int main()
{
int b = data_one;
int c = 12;
sum(b.c);
return 0;  
} 

b.c文件
int data_one = 10;
int sum(int a,int b)
{
   return a+b;
}
  • Linux中的内存段查看
    链接过程中的符号重定位_C底层18/2/25_第2张图片
  • 在看一下可执行程序run中的内存布局:
    链接过程中的符号重定位_C底层18/2/25_第3张图片
    强弱符号:
    只要初始化都是强符号,未初始化为弱符号,出现多个同名强符号,编译出错;出现多个同名弱符号,选择内存占用字节最大的,若占用都一样大看编译器。出现同名的强符号和弱符号,选弱符号。(C++没有强弱符号)

这个流程为:
1. 在a.c和b.o编译时都是独立编译,在生成.o文件后,只要在本文件中的变量或指令都在.data 或 .text段,外部声明的都为UND(undefine)段,第一幅图就是编译过程之后的内存布局,也就是没有进行符号合并和解析之前的布局。
2. 在执行 ld -e a -o run *.o 命令之后,生成可执行程序run,在该过程中,链接器先将各个段合并(就是将同名符号按照强弱符号选择规律合并),再并调整其起始位移(段偏移)和段大小,然后进行符号的解析,解析完成之后为符号分配虚拟内存地址,最后进行符号的重定位。
在符号的重定位中总的流程又有如下:(重定位一般对外部声明)
1)在编译过程中先在内存中为外部声明分配一个无效的等待更新的地址。(因为编译器在编译过程中只看声明是不知道其真实值的)
2)在链接时文件已经是二进制可重定位目标文件了,这时会进行符号的合并,因为链接是对多个文件的操作,这时,编译器就会在别的文件中寻找extern声明的变量(这时编译器就会知道你声明的变量到底是啥值),得到值之后就将其移动到相应的段中。原来的待更新地址也会变为实际有效的虚拟地址。

你可能感兴趣的:(C基础,计算机基础,编译基础)