链接、装载与共享库

程序执行简述

    由下图编译的过程可以看出,源程序经过预处理,编译,汇编等步骤形成可重定位目标文件,再由多个可重定位目标文件链接成可执行目标文件。

    每个步骤做的事情:

(1)预处理(预编译),处理源代码中以“#”开头的预处理指令,如#include,#define。具体工作如下:

  • 将所有#define删除,并展开所有宏定义
  • 处理所有条件预编译指令,如#if,#ifndef,#endif等
  • 处理#include预编译指令,将所包含的文件插入到该指令的位置,这个过程是递归进行的
  • 删除注释,添加行号和文件名标识

(2)编译过程是将预处理完成的文本进行词法分析,语法分析,语义分析,优化后生成相应的汇编程序。

  • 词法分析,源代码被输入扫描器,扫描器将源代码的字符序列分割成一系列的记号(记号分为关键字,标识符,字面量和特殊符号)
  • 语法分析,利用语法分析器对扫描器产生的符号进行语法分析并生成语法树
  • 语义分析,编译器只能做静态语义分析,主要是声明和类型的匹配,类型的转换
  • 编译器对源码和目标代码进行优化

(3)汇编器将汇编代码转换成二进制机器指令,汇编语句与机器指令一一对应。

(4)链接是将各种代码和数据片段收集并组合成一个单一可执行目标文件的过程,可执行文件可被加载到内存中直接执行。链接可以执行于编译时(源代码被翻译成机器码)、加载时(程序被加载器加载到内存并执行)、运行时(由应用程序来执行)

    链接的好处在于①模块化(可构建共享库)②效率高,时间上可分开编译(只需要重新编译修改过的文件即可),空间上可执行文件运行时内存只需包含所调用函数所在目标文件的代码(是以目标文件为基本单位)。

链接操作步骤

   现代的链接器一般分成两步,第一步空间和地址分配,分配虚拟地址空间,扫描所有的可重定位目标文件,获取各个段的长度、属性、位置,并将符号表中所有符号定义和符号引用收集起来统一放到全局符号表中。linux下,ELF可执行文件默认从地址0x8048000开始分配,确定好每个符号的地址,后面的步骤就容易了。

    第二步符号解析和重定位,具体如下,

  1.  确定符号引用关系
  2. 合并相关.O文件
  3. 确定每个符号的地址
  4. 在指令中填入新地址

    同时也可以分成两个步骤,符号解析(步骤1,即把每个符号引用和一个符号定义关联起来)和重定位(步骤2\3\4)。

    符号解析:连接器解析符号引用的方法就是将每个引用与他输入的可重定位目标文件的符号表中的符号定义关联起来。每个可重定位目标模块m都有一个符号表,其包含了m定义和引用的符号信息,符号表中有三种不同的符号:

  1. 由模块m定义并能被其他模块引用的全局符号(非静态函数和非静态全局变量)
  2. 由其他模块定义并被模块m引用的全局符号,也成为外部符号(其他模块中定义的非静态函数和非静态全局变量)
  3. 只能被模块m定义和引用的局部符号(静态函数和静态变量)

    注:函数内部局部变量不在符号表中,在函数栈帧中

    对于多重定义的全局符号,在编译时编译器会向汇编器输出每个全局符号,分为强符号(函数和已初始化的全局变量)和弱符号(未初始化的全局变量),汇编器则会把这个信息放在可重定位文件的符号表中,链接器根据以下规则处理多重定义的符号名:

  1. 不允许有多个同名的强符号
  2. 如果有一个强符号和多个弱符号同名,那么选择强符号
  3. 如果由多个弱符号同名,那么从这些弱符号中任意选择一个。

    重定位:合并输入模块并为每个符号分配运行时地址。有两步骤组成:①重定位节和符号定义,将所有相同类型的节合并为同一类型的新的聚合节②重定位节中的符号引用,链接器修改代码节和数据节中对每个符号的引用,使其指向正确的运行地址(依赖于重定位条目)

可重定位目标文件

    图中是一个ELF可重定位目标文件格式。

    首先,为什么要把ELF中指令与数据分开放?原因是

(1)权限问题,数据大部分是可读可写的,代码指令只是可读的

(2)局部性问题,指令段与代码段的分离,有利于提高程序的局部性,现代cpu的缓存大部分都分为指令缓存和数据缓存

(3)当系统运行多个该程序副本时,内存中只需保存一份该程序的代码段,可通过内存映射实现,而数据段是每个进程私有且不同的

    接下来对一些ELF文件中的关键节(段)做具体分析。

    ELF文件头描述了整个ELF文件的基本属性,主要包含系统字大小,字节顺序,ELF头大小、目标文件类型(如可重定位、可执行或共享的)、机器类型、节头部表的文件偏移、以及节头部表中条目的大小和数量。

    节头部表描述不同节的位置和大小。比如每个段的段名,段长度,在文件中的偏移,读写权限以及段的其他属性等

  • .text已编译程序的机器代码
  • .rodata只读数据
  • .data已初始化的全局和静态C变量
  • .bss未初始化的全局和静态C变量
  • .symtal符号表,存放程序中引用和定义的函数和全局变量的信息
  • .rel.text存放代码的重定位条目,.rel.data存放已初始化数据重定位条目
  • .debug是一个调试符号表,.line是原始C源程序中行号和.text节中机器指令之间的映射,
  • .strtab一个字符串表,用来保存普通的字符串

可执行目标文件

    段头部表(程序头部表)是将连续的文件节映射到运行时内存段,即节与段的映射并包含每个段的信息。可执行目标文件的ELF头和可重定位文件的ELF头类似,还包括程序的入口点(即当程序运行时候执行的第一条指令的地址),其他.text节等已重定位到他们最终运行时内存地址。

装载可执行目标文件

    Linux系统中每个程序都运行在一个进程上下文中,有自己的虚拟地址空间,shell运行一个可执行目标文件的过程:

  • 读入命令(可执行文件名称)及参数
  • 构造argv和envp
  • 调用fork系统调用,从父进程生成一个子进程
  • 调用execve系统调用启动加载器,加载器删除子进程现有的虚拟内存段,并创建一组新的代码段、数据段、堆和栈,新的堆和栈初始化为零,通过虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码段和数据段就会被初始化为可执行文件的内容
  • 加载器跳转到_start地址,最终会调用应用程序的main函数

静态库与动态链接共享库

    所有的相关目标模块(.o文件)打包成一个单独的文件即为静态库(.a文件)。静态库的缺点:①静态库函数被包含在每个进程的代码段,造成主存的浪费②静态库函数被合并在可执行目标中,磁盘中空间浪费③更新困难

    动态链接共享库是一个目标模块,在运行或加载时,可以加载到任意内存地址,并和一个在内存中的程序链接起来。Linux下用.so后缀表示,Windows下用.dll表示。

    所有引用共享库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库中被复制到引用他们的可执行文件中,除此之外,在内存中一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。

linux > gcc -shared -fpic -o lib.so add1.c add2.c

linux > gcc -o a.out main.c ./lib.so

//main.c中引用add1.c或者add2.c

 

参考《CSAPP》《TLPI》《程序员的自我修养》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值