您的位置:首页 > 职场人生

读《程序员的自我修养 —— 静态链接》乱摘

2016-05-05 23:03 447 查看
2016.05.05 – 05.13

《程序员的自我修养 —— 链接、装载与库》的“静态链接”部分。

- 余甲子 石凡 潘爱民编

个人选读笔记 - 学点表皮。

05.05

PART I 静态连接

1 编译和链接

IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并到一起的过程称为构建

1.1 编译器编译程序的一般过程

编译器就是将高级语言翻译成机器语言的一个工具程序。

使用gcc编译源程序到可执行程序过程分解



05.07

预编译过程主要处理源代码文件中的以“#”开始的预编译指令。处理/替换掉编译后期不需要的所有信息(如#define、#include、条件预编译);保留/添加编译后期所需要的信息(如#pragma、添加行号和文件名标识)。

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

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编指令都对应一条机器指令。(目标文件)

(静态)链接器将各个目标文件拼接在一块形成可执行文件。

1.2 编译器的编译过程

源代码文件到目标代码文件的过程。

扫描器/词法分析器将源代码字符序列分割成一系列的记号。该记号一般可以分为这几类:关键字、标识符、字面量(包括数字、字符串等)和特殊符号(如加号)。在识别记号的同时,扫描器也进行了其它诸如将标识符放到符号表、将数字、字符串常量放到文字表等的工作,以备后面的步骤使用。

语法分析器 扫描器产生的记号进行语法分析(如’+’记号应有两个操作数)从而产生语法树(表达式为节点的树)。

语义分析器完成由语法分析器产生的语法树中的表达式是否具有意义的分析(如分析出两指针做乘法不合法)。经过语义分析器后,整个语法树的表达式会被标识类型,如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点。

源码级优化器对源代码进行优化输出中间代码(中间代码是经优化的语法树的顺序表示,它跟目标机器和运行时环境无关)。

代码生成器将中间代码转换成目标机器代码(这个过程十分依赖于目标机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数数据类型等)。

目标代码优化器对代码生成器生成的目标代码进行优化(如选择合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等)得到最终的目标代码。[源码级和目标代码级都有优化]

编译器的编译过程图简示。



这里的目标机器代码指得是由汇编指令组成的代码(即汇编.s文件)。

1.3 模块拼接 —— 静态链接

程序设计的模块化是程序员一直在追求的目标,因为当一个系统十分复杂的时候,我们不得不将一个复杂的系统逐步分割成小的系统以达到各个突破的目的。一个复杂的软件也如此,人们把每个源代码模块独立地编译,然后按照需要将它们“组装”起来,这个组装模块的过程就是链接。链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。这个过程主要包括地址和空间分配符号决议(绑定)重定位等步骤。

最基本的静态链接过程。



其实是一组目标文件的包,就是一些常用的代码编译成目标文件后打包存放。

比如在程序模块main.c中使用另外一个模块func.c中的函数foo()。在main.c模块中每一处调用foo的时候都必须确切知道foo这个函数的地址,但是由于每个模块都是单独编译的,在编译器编译main.c的时候它并不知道foo函数的地址,所以它暂时把这些调用foo()指令的目标地址搁置(由foo声明告知编译器foo函数在其它文件中被定义),等待链接的时候由链接器去将这些指令的目标地址修正(当foo函数的地址发生改变后,重新链接一次即可)。这就是静态链接最基本的过程和作用。

2 目标文件的内容

目标文件中的内容由经编译的程序内容(和调试信息)组成。

2.1 目标文件格式常见种类

目标文件指汇编器输出还未经过链接的文件(如window下的.obj、linux下的.o),其内是高级语言源程序对应的机器指令/二进制/高低电平。PC平台流行的可执行文件格式主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format),它们都是COFF(Common file format)格式的变种。目标文件跟可执行文件的内容和结构相似,所以目标文件一般跟可执行文件格式一起采用一种格式存储。动态链接库(DLL,Dynamic Linking Library)(Windows的.dll和Linux的.so)及静态链接库(Static Linking Library)(Windows的.lib和Linux的.a)文件都按照可执行文件格式存储。

2.2 目标文件的一般结构/轮廓

目标文件中有编译后(预处理过程到汇编过程)的指令代码、数据,还包括链接是所需要的信息(符号表、调试信息、字符串等)。一般目标文件将内容按不同的属性,以“节”(Section)(或段-Segment)的形式组织

目标文件内容的基本结构如下图所示。



“文件头”描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址(可执行文件)、目标硬件、目标操作系统等信息,文件头还包括一个段表(Section Table)[见2.4下的段表 – ELF段表不在ELF文件头中,文件头记录段表在文件中的偏移],它可被看作是描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等,从段表里面尅得到每个段的所有信息(这些信息由编译器生成)。文件头后面就是各个段的内容。

一般C语言的编译后执行语句都编译成机器代码,保存在.text段;已经初始化的全局变量和局部静态变量都保存在.data段;未初始化的全局变量和局部静态变量一般放在一个叫“.bss”的段里(未初始化的全局变量和静态局部变量默认值都为0,本来它们也可以被放在.data段里,但是因为它们都是0,就专门设立一个“.bss”段来记录所有未初始化的全局量和静态变量以及它们大小的总和,“.bss”段只是为记录未初始化全局变量和局部静态变量预留空间而已,除了这些记录外这些变量并不占用空间(指存储目标文件的空间),载入程序时/程序运行时根据该记录为这些变量分配内存空间)。

05.08

2.3 例 - 简单查看ELF目标文件程序的程序段

[objdump的参数](利用这些参数可以查看目标文件中各段的内容)

[“-h”参数把ELF文件的各个(关键)段打印出来]

[“-s”参数可以将所有段的内容以16进制的方式打印出来]

[“-d”参数可以将所有包含指令的段反汇编]

[“-x”参数会显示所有头(all – headers,段表、符号表、重定位表)]

目标文件内容/段分布

编译器(汇编器)以段(section)的形式将源码编译后的内容输出/组织到目标文件中

摘书中的SimpleSection.c。



在Linux平台下将SimpleSection.c转换为目标文件,并查看目标文件内容。



关注“.text”、“.data”、“.rodata”和“.comment”这4个段,并将它们的大小和在文件中的偏移位置标识在下图。



[目标文件SimpSection.o文件大小可用“ls –l”命令查看]

目标文件各段内为源程序对应的二进制内容。

2.4 2.4 描述“ELF头”、“段表”、“重定位表”的数据结构 – elf.h

(Linux下)ELF目标文件的生成过程•简。



ELF头和各个段表中的属性由“/usr/include/elf.h”中的Elf32_Ehdr结构体描述。

[readelf参数]

[“-h”参数显示ELF头]

[“-S”参数显示ELF段表]

(1) ELF头

以SimpleSection.o为例,查看其ELF头。



不用关心目标文件内程序入口等信息,关注下“ELF魔数”、“ELF文件类型”、“ELF所在的机器类型”。

ELF魔数(Magic)

最开始的4个字节是所有ELF文件都必须相同的标识符,分别为0x7F、0x45、0x4C、0x46,(ASCII字符里的DEL控制符、E、L、F) [这4个字节被称为魔数,用来确认文件的类型,OS在加载可执行文件的时候会确认魔数是否正确]。接下来的一个字节用来标识ELF文件类型,0x01/0x02表示32/64位;第6个字是字节序,规定该ELF文件是大端还是小端的;第7个字节规定ELF文件的主版本号;后面9个字节ELF标准没有定义,一般填0,有些平台会使用这9个字节作为扩展标志。

文件类型(Type)

表示ELF文件类型:目标文件(可重定位文件)、可执行文件还是共享目标文件。

机器类型(Machine)

表示ELF文件的平台属性(该ELF文件只能在兼容Intel 80386的平台上使用)。

(2) 段表(Section Header Table)

段表描述了ELF各个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限以及段的其他属性。编译器、链接器和装载器都是依靠段表来定位和访问各个段(的属性)的。段表在EFL文件中的位置有ELF文件头的start of section headers(e_shoff成员)值决定。

用“readelf –S SimpleSection.o”命令查看SimpleSection.o段表信息。



每一个段表由“/usr/include/elf.h”内的Elf32_Shdr结构体描述,其每一列对应该结构体内的一个元素。由该段表可得到SimpleSection.o的段表和所有段的位置和长度如下图。



[有的段会跟前一个段相隔一两个字节是因为对齐的原因]各个段的大小可有偏移地址计算得来。

05.09

(3) 重定位表

在SimpleSection.o中有一个名为“.rel.text”的段,它的类型(Type – Elf32_Shdr.sh_type)为重定位表(REL – SHT_REL)。对于每个需要重定位(修改对绝对地址引用)的段都会有一个重定位表,在SimpleSection.o中,只有“.text”中有一个对“printf”函数的绝对引用,故而只有一个“.rel.text”重定位表。重定位表也是ELF的一个段,其Type - Elf32_Shdr.sh_type为REL – SHT_REL类型,其Lk - Elf32_Shdr.sh_link表示符号表(.symtab)的下标,其Inf - Elf32_Shdr.sh_info表示它作用于哪个段(.rel.text作用于.text段,.text段的下标为1,所以Inf的值为1)。

(4) 字符串表

ELF文件中用到了很多字符串,比如段名、变量名等。因字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。一般字符串表在ELF文件中也以段的形式保存,常见的段名为“.strtab”或“.shstrtab”,这两个字符串表分别为字符串表和段表字符串表,它们分别用来保存普通的字符串(如符号名)和段表中用到的字符串(如段名)。

2.5 链接的接口 —— 符号(描述EFL符号表的数据结构 – elf.h)

每一个目标文件都会有一个相应的符号表,该表里记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值 —— 符号值,对于变量和函数来说(还有编译器自身产生的符号),符号值就是他们的地址(虚拟地址)。

(1) ELF符号表

ELF文件中的符号表往往是文件中的一个段 —— “.symtab”。其结构由“/usr/include/elf.h”中的Elf32_Sym结构体数组描述。

(2) (ld)链接器的链接脚本中的特殊符号

当使用ld作为链接器来链接生成可执行文件时,它会为我们定义很多特殊的符号,这些符号并没有在你的程序中定义,但可以在程序中直接声明并引用它们。其实这些符号是被定义在链接器的链接脚本中的,链接器将在最终生产可执行文件的时候将这些特殊符号解析成正确的值。ld中几个具有代表性的特殊符号如下。

__executable_start,该符号为程序的起始地址(不是程序的入口地址,是程序的最开始的地址)。

__etext或_etext或etext,该符号为代码段结束地址,即代码段最末尾的地址。

_edata或edata,该符号为数据段结束地址,即数据段最末尾的地址。

_end或end,该符号为程序结束地址。

(3) 强符号和弱符号

强弱符号是对定义而非引用来说的。对于C/C++来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。也可以通过特殊的方法如gcc的“__attribute _((weak))”(两个下划线)来定义任何一个强符号为弱符号。

2.6 调试信息

目标文件里面还有可能保存的是调试信息。几乎所有现代的编译器都支持源代码级别的调试,比如可以在函数里面设置断点,可以监视变量变化,可以单步运行等,前提是编译器必须提前将源代码与目标代码之间的关系对应,比如目标代码中的地址对应源代码中的哪一行、函数和变量的类型、结构体的定义、字符串保存到目标文件里。(gcc编译器加“-g”参数就会在目标文件中加入调试信息)。

3 静态链接

05.11

将多个目标文件链接起来形成可执行文件的过程中包含了一个基本的过程即静态链接

用a.c和b.c两个文件的目标文件为例,说明静态链接过程。



首先用gcc生成a.c和b.c分别对应的目标文件a.o和b.o —— “gcc -c a.c b.c”。

3.1 空间与地址分配

对于链接器来说,整个链接过程中,就是将几个输入目标文件加工后合并成一个输出文件。对于多个输入目标文件,现代的链接器一般是将各目标文件的相似段合并(为目标文件分配在可执行文件中的空间 —— 除.bss段),并为目标文件中的内容分配虚拟地址空间。这个过程可以分为两步:空间与地址分配符号解析与重定位

(1) 可执行文件空间、地址分配和段的虚拟地址

用“ld a.o b.o -e main -o ab”命令(-e表示将main作为程序入口)生成ab可执行程序。用“objdump -h *”查看链接前后各个段的属性。



[1] 各目标文件内容在可执行文件中的空间(内容)与地址(在文件中的偏移地址)分配。相似段合并(.text与.text);偏移地址发生变化。

[2] 虚拟地址的分配。在目标文件中,虚拟地址(VMA)始终为0;在可执行文件中,VMA有了具体的值。

这个过程可由下图简示。



(2) 符号虚拟地址

各段符号的虚拟地址为符号所在段的虚拟段地址加上该符号在该段中的偏移(该值在可执行文件中已知 —— 链接器记录)。可用“nm ab”命令查看可执行文件中各个符号的虚拟地址。



05.13

3.2 符号解析与重定位

[objdump]

[“-r”参数显示目标文件中的重定位表/段]

总过程。在目标文件中,引用声明在其它目标文件中定义的全局变量的地址为0;调用声明在其它目标文件中定义的函数如“近址相对偏移调用指令call”后面跟的调用地址为-4(0xFFFFFFFC)。这些值都不是这些符号的真实虚拟地址,编译器也不知道它们真实的虚拟地址。

链接器在完成地址和空间分配之后就已经确定所有符号的虚拟地址了,那么链接器就可以根据符号的地址对每个需要重定位的指令进行地址修正。引用全局变量的虚拟地址变成链接器给该变量分配的虚拟地址。“近址相对偏移调用指令call”所调用的函数的地址变成该函数相对于call指令下一条指令的偏移地址(依call指令含义链接)。

重定位表。引用地址需要被修改的地方保存在目标文件的重定位表/段中。[如代码“.text”段有要被重定位的地方,就有一个叫“.rel.text”的重定位段;数据段要有被重定位的地方,就有一个叫“.rel.data”的重定位段]。在重定位表/段中,每个要被重定位的地方(重定位入口)包含需要被修正的引用地址在所在段的偏移地址和对该引用地址进行重定位的方式(直接修改成符号的虚拟地址还是修改成偏移地址 —— 依据指令含义修改)。描述重定位表/段的数据结构为“/usr/include/elf.h”中的Elf32_Rel

符号解析。每个目标文件都可能定义一些符号,也可能引用到定义在其他目标文件的符号。重定位过程中,每个重定位入口都是对一个符号的引用,那么当链接器需要对每个符号的引用进行重定位时,它就要确定这个符号的目标地址。这时候链接器就会去查找由所有输入目标文件符号表组成的全局符号表,找到相应的符号后进行重定位。

指令/引用地址修正方式。根据汇编寻址方式和汇编指令含义进行修正。



objdump -d -S ab





[gcc -c -g a.c b.c],[ld -e main -o ab] 。

3.3 静态库链接

在一般情况下,一种语言的开发环境往往会附带有语言库(Language Library)。库中有许多函数是由对操作系统的API包装而成的。

一个静态库可以简单的看成一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件。[例-Linux下的libc.a。在一个C语言的运行库中,包含了很多跟系统功能相关的代码,比如输入输出、文件操作、时间日期、内存管理等。glibc本身是用C语言开发的,它由成百上千个C语言源代码文件组成,即编译完成以后具有相同数量的目标文件,比如输入输出有printf.o,scanf.o等;文件操作有fread.o,fwrite.o等;时间日期有date.o,time.o等;内存管理有malloc.o等;等。把这些零散的目标文件直接提供给库的使用者,很大程度上会造成文件传输、管理和组织方面的不便,于是通常使用“ar”压缩程序将这些目标文件压缩到一起,并且对其进行编号和索引,以便于查找和检索,就形成了libc.a这个静态库文件]。

调用静态库中的某函数。每个函数对应一个目标文件(如printf函数存在于printf.o中),当程序调用某函数如printf时,改程序编译出来的目标文件只和libc.a中的printf.o以及其它需要用到的目标文件链接在一起(查找和检索到printf.o) —— 链接器自动寻找所有需要的符号及它们所在的目标文件,将这些目标文件从libc.a中“解压”出来,最终将它们链接在一起形成可执行文件 —— 以目标文件为单位进行链接(故而每个函数对应一个目标文件)。

3.4 链接过程控制

绝大部分情况下,我们使用链接器提供的默认链接规则对目标文件进行链接。这在一般情况下是没有问题的,但对于一些特殊要求的程序,比如操作系统内核、BIOS或一些诸如Boot Loader、嵌入式系统程序等在没有操作系统的情况下运行的程序,或有一些需要特殊链接过程如内核驱动程序,它们往往受限于一些特殊的条件,如需指定输出文件中的各个段虚拟地址、段的名称、段存放顺序等,因为这些特殊的环境,特别是某些硬件条件的限制,往往对程序的各个段的地址有着特殊的要求。

链接器一般提供多种控制整个连接过程的方法,以用来产生用户所需要的文件。一般链接器有以下三种方法。

使用命令行来给链接器指定参数(ld -e main -o ab a.o b.o)。

将链接指令存放在目标文件里,编译器经常会通过这种方法像链接器传递指令。

使用链接控制脚本(ld为例)。



TinyHelloWorld.lds





[.text,.data.,.rodata合成了一个tinytext段]

[2016.05.05 - 23:03]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: