静态链接

来源:互联网 发布:画原型的软件 编辑:程序博客网 时间:2024/05/01 19:18

一、在Linux下一个程序的编译过程可分为:预编译、编译、汇编、链接。

1、预编译

gcc -E hello.c -o hello.i

预编译过程注意处理那些源代码文件中的以#开始的预编译指令。比如“#include”“#define”“#ifdef”等。删除注释,​添加行号。

2、编译

编译过程就是把预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件。

gcc -S hello.i -o hello.s

3、汇编

汇编过程是根据汇编指令和机器指令的对照表一一翻译。

as hello.s -o hello.o

4、链接

将模块间的符号引用进行重定位。

​二、目标文件的格式

1、可执行文件的格式分为:Windows下PE、Linux下为ELF,都是COFF格式的变种。​

不光是可执行文件,动态库和静态库文件都是按照可执行文件格式存储。

​2、ELF文件类型

可重定位文件(.o或.obj)、可执行文件(.exe)共享目标文件(.so或.dll)、核心转储文件(core dump)。在Linux下可以使用file命令查看文件格式,file xxx.o。

3、目标文件存放方式

程序指令存放在代码段(.text),已初始化的全局变量和局部变量存放在数据段(.data),未初始化的全局变量和局部变量存放在.bss段。默认情况下未初始化的全局变量和局部变量值都为0,所以没有必要放在.data段分配空间存放数据0,只需要可执行文件只需要记录所有未初始化的变量的大小总和并在.bss段为未初始化的全局和局部变量预留位置,.bss段没有内容不占据空间。​

​4、ELF文件结构描述

(1)文件头

使用命令readelf -h a.o可以查看a.o文件的文件头。

文件头中定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量。

(2)​段表

ELF文件中有很多段,段表就是保存这些段的基本属性的结构。段表是ELF文件中除了文件头之外最重要的结构。描述了ELF各个段的信息,如段名、段的长度、编译、权限及其他属性。段表本身在ELF文件中的位置由ELF文件头成员e_shoff决定。

​查看段表结构有两个命令:

objdump -h a.o  只显示文件中的关键段。

readelf -S a.o​    显示真正的段表结构。

readelf -s a.o​    显示文件的符号。

(3)重定位表

链接时需要进行重定位的代码和数据存放在此表中。.rel.text就是针对.text段的重定位表, .rel.data就是针对.data段的重定位表。

(4)其他表及表结构

  

​三、符号

我们将函数和变量统称为符号。链接的过程就是符号的粘合。

1、符号的修饰和函数的签名

在C语言使用之前由于已经有很多汇编等机器语言的库,为了能使用这些库必须解决符号冲突的问题,否则库中使用的符号名在C语言中就不能使用,所以有了符号修饰。不同的语言有不同的修饰方法,但是当工程太大时仍然存在符号冲突的问题,C++为此引入了命名空间的概念。同时C++有类、继承等机制,所以C++的符号修饰方法更为复杂。

2、extern "C"

C++为了与C兼容,在符号管理上,C++有一个用来声明和定义C符号的关键字​extern "C",编译器会把此声明的全部代码当成C语言处理。编译时按C语言修饰方法进行修饰。

但是由于C语言不支持extern "C"​语法,为了C语言和C++兼容使用C++宏__plusplus C++编译器在编译C++时默认会定义此宏。

3、强弱符号

在编程中时常还是会碰到符号冲突的问题,编译器默认函数和初始化的全局变量为强符号,默认未初始化的全局变量为弱符号​,可以使用GCC的__attribute__((weak))来定义(不是声明)一个强符号为弱符号。如果定义了两个强符号则编译会报错重复定义。

针对强弱符号,链接器会做如下处理:

(1)不允许强符号被多次定义

(2)如果一个符号在某个目标文件中是强符号在其他文件中是弱符号那么选择强符号。

(3)如果一个符号在所有目标文件中都是弱符号,那么选择占用空间最大的一个。
在链接时一个符号若没有找到则会报错,这时强引用,与之相对应的还有一个弱引用。对于弱引用如果符合有定义在链接,如果符合未定义不会报错。链接器处理强弱引用的过程一样,只是对于未定义的弱引用链接器不认为是一个错误。对于未定义的弱引用链接器会默认为0,或者一个特殊值。但是运行时会发生错误。强弱引用:可以使用GCC中的__attribute__((weakref))​

4、调试信息

GCC编译时加上-g选项可以产生有调试信息的可执行程序,对应的ELF文件中会有很多个debug相关的段。这些段占用很大空间,所以发布版本一般需要去掉调试信息,Linux中使用strip命令可以去掉调试信息,strip a.o

​四、链接

1、空间和地址分配

链接器链接时将各个目标文件合并成一个文件的过程,采用合并相识段的方式,链接器为目标文件分配地址空间其实有两个地址:

(1)​各个段合并之后再输出的可执行文件中的地址

(2)可执行程序装载后在进程的虚拟地址空间中的地址。​

​整个链接过程分为两步:

(1)空间与地址分配

获取所有输入目标文件中各个段的长度、属性和位置,建立全局符号表,计算合并后各个段的位置和长度并建立映射关系。​

(2)符号解析与重定位

​读取输入文件中段的数据、重定位信息、并且进行符号解析和重定位、调整代码中的地址等。

如下图展示是链接和加载时地址变迁,a.o + b.o​->ab->进程


目标文件->可执行程序->进程虚拟地址空间

链接器根据重定位表中的信息进行重定位,可以使用命令查看目标文件中的重定位表:objdump -r a.o

​2、COMMON块

目前链接器并不支持符号的类型,即变量类型对于链接器来说是透明的,链接器只知道符号名称并不知道其类型是否一致。所以当定义多个名称相同的强符号时编译器会报错尽管符号的类型值不同。​

处理规则:

(1)一个强符号,多个弱符号出现类型不一致时,以强符号类型为最终类型

(2)多个弱符号出现类型不一致时,以​占用空间最大的为准。

3、C++相关问题

(1)函数级别的链接

链接器在链接静态库的时候是以目标文件为单位的,比如引用了静态库printf函数,那么链接器就会把库中包含printf函数的那个目标文件链接进来,如果很多函数放在目标文件中很可能很多没用的函数都被一起链接进来,这样造成空间浪费,所以libc.a中printf等函数每个函数就是一个目标文件。而当应用程序的库中的包含成千上万个函数,当使用到其中一个函数时或者变量时整个目标文件都会被链接进来,​这样输出文件会变的很大,造成空间浪费。

VC++编译器提供了一个编译选项叫函数级别链接,这个选项作用就是让所有的函数和变量保存到独立的段中,当链接器需要用到某个函数时将它合并到输出文件中,没用到的函数则抛弃。GCC编译器提供了类似的机制,他有两个选项-ffunction-section和-fdata-section分别将函数或者变量保存到独立的段中。​

C++全局对象构造函数在main之前被执行,全局对象的析构在main之后被执行。

4、静态库链接

静态库可以简单看成一组目标文件的集合,即很多目标文件压缩打包后形成一个文件。通常使用ar压缩程序 将这些目标文件压缩到一起,并且对其进行编号和索引,以便查找和检索。​

查看链接器ld默认的链接脚本:ld -verbose

0 0
原创粉丝点击