Chinaunix首页 | 论坛 | 博客
  • 博客访问: 296407
  • 博文数量: 71
  • 博客积分: 30
  • 博客等级: 民兵
  • 技术积分: 217
  • 用 户 组: 普通用户
  • 注册时间: 2012-07-31 15:43
文章分类

全部博文(71)

文章存档

2016年(4)

2015年(2)

2014年(2)

2013年(63)

分类: LINUX

2013-05-10 13:18:12

书的第四章开始真正直面静态链接的过程细节了,在此纯粹根据自己的理解和头脑中的逻辑思维路线梳理读书所得
  •     经过编译器的a.c和b.c得到了可重定位的两个目标文件a.o和b.o,链接器执行 ld a.o b.o -e main -o ab 命令后得到了从main函数作为程序入口的可执行程序ab —— 默认地,ld将以_start作为程序入口。
  •     ab的ELF文件结构由a.o和b.o合成得到,链接器ld的工作过程为“两步链接”,第一步就是为输出目标文件进行空间和地址的分配——此处的空间和地址的概念既包括在ab ELF文件本身的空间,更重要的是可执行文件装载后在进程虚拟地址空间中的分配。
        要完成这第一步链接工作,必须首先明确ab的ELF文件的空间策略:链接器扫描输入的目标文件,通用的段组合方式是将a.o和b.o中对应的各个段分别进 行叠加组合,即a.o和b.o的.text段叠加合成ab的.text段,a.o和b.o的.data段叠加合成ab的.data段……,然后将输入目标 文件的符号表中的所有符号收集起来,创建一个新的额全局符号表,又由于*.o文件中各个符号相对于其所在段的偏移量是固定的,从而经过第一步之后,输出可 执行文件ab各个段的偏移量、段长度,以及所有定义的符号在ab中的位置都可以确定下来。
        第一步工作另外要为ab分配加载后的虚拟地址空间,因此链接器输出的ab程序中,各个段的虚拟地址VMA都已经不再是*.o中的0x0了,而是根据不同操作系统进程虚拟地址空间分配原则进行了虚拟地址的分配,例如Linux操作系统中ELF可执行文件默认地从虚拟地址0x80480000开始分配,相应的所有定义的符号也有了自己相应的虚拟地址。
  •     链接器的第二步工作就是最重要的符号解析和重定位。作为链接器输入的可重定位目标文件中,涉及对本模块中未定义符号的引用时,均使用的是假地址,链接器需要根据新得到的全局符号表对这些假地址进行修正,每一个这样需要重定位的地方都称为一个“重定位入口”
        首先,链接器是怎么知道输入目标文件中哪些地方需要进行修正的呢?这完全归功于编译器单元创建的“重定位表”——在段表section header table中类型为SHT_REL。可重定位ELF文件中每一个需要进行重定位的section都对应这一个重定位表,例如.text对应 的.rel.text,.data对应的.rel.data。与ELF文件中其他的“表”一样,重定位表也是结构体Elf32_Rel(重定位入口)的数 组,其中每个Elf32_Rel结构体包含两个域:
    r_offset;  ——该重定位入口相对于所在段起始的偏移量
    r_info;    —— 低8位表征重定位入口类型(不同类型,计算修正量地址的方法是不一样的),高24位表征重定位入口的符号在新的输出可执行文件中的实际地址
        其次,如果链接器在ab的新全局符号表中没有找到某个重定位入口符号的定义,则会报错——这就就是常见的连接错误类型;如果在全局符号表中找到了这些未定 义的符号,则会根据重定位入口指令的类型进行符号引用地址的修正,32位6平台下的计算方式包括绝对近址寻址修正和相对近址寻址修正两种方法,分别对应于 R_386_32类型的重定位入口和R_386_PC32类型的重定位入口。相对寻址修正方式中,重定位入口所在指令的下一条指令的地址 + 修正后的地址值 = 该符号的实际定义地址。
  •     当前的链接器并不支持符号类型,即链接器并不知道各个符号的数据类型究竟是什么,而仅仅知道该符号的大小而已。当输入目标文件中存在着多个同名符号时,链 接器需要根据强符号/弱符号机制进行处理,对于均为弱符号的情况,则选择最大的那一个进行连接——这也就是所谓的COMMON块机制。
        未初始化的全局变量之所以被编译器归为COMMON而不是在.bss段中分配空间(即使.bss段在编译时并未分配真正的空间),这是因为这种变量被划定 为弱符号,在编译时并不能知道是否在其他目标文件中存在相同的未定义全局变量,编译器不能确定在链接时这个名字的符号究竟占多大的空间。但是链接器却能够 知道最终这个符号占多少空间,所以在最终的输出可执行文件中,这样的未初始化全局变量会在.bss段中被分配空间,即最终还是和未初始化的局部静态变量一 起放在.bss段中的。
        GCC中可以通过 -fno_common 编译选项,或者在源码中使用 __attribute__((nocommon)) 定义未初始化全局变量,这样该变量就不会以COMMON符号方式被处理,而变成了一个强符号。

       因为对C++不懂,所以书中提到的纯粹针对C++的一些内容尚不能读懂,汗颜~~~~~~~

  •     在一些编程场合,程序的一些特定操作必须要在main函数执行之前执行,另外还有一些操作必须要在main函数结束之后执行;而在Linux系统中程序的 执行都是从_start函数作为入口的,该函数是Linux系统库Glibc中的一部分,当程序与Glibc库链接生成可执行文件后,_start函数就 是作为程序初始化部分的入口,完成必要的初始化之后main函数才开始执行,因此对于那些必须要在main之前执行的操作,可以安排Glibc的初始化部 分来完成。
        ELF文件中对于这种情况也额外定义了 .init 和 .fini 两个特殊的段,其中 .init 段中的代码会在main之前被执行,.fini 段中的代码会在main退出之后被执行
  •     在第一章曾经提到过,软件架构中所谓的“层次间接口”其实就是层次之间通信的某种协议和规定,应用程序和运行时库的接口API就是这样的接口——API所表述的通信协议往往是从源代码级来考虑,即接口函数的原型。
        人们希望能够使用某种链接器将不同编译器编译得到的目标文件链接到一起,从而实现软件的复用,这就是所谓的“二进制兼容性”——这其实也是一种协议和规 定,因此将这些与二进制兼容性相关的内容(符号修饰标准、变量内存布局、函数调用方式等)称之为ABI,相对于API概念,二者性质是一样的,只是ABI 规定的内容是从二进制层面来考虑的,并且由于软硬件平台的五花八门,ABI需要考虑的兼容性内容比API复杂得多,硬件平台、编程语言类型、编译器、链接 器、OS都会影响ABI的定义。
        编程语言越复杂其ABI兼容性越难实现,C++就是典型的例子,其二进制兼容性较差一直为人所诟病,甚至于同一个编译器的不同版本编译出来的目标文件 ABI都不能兼容。ABI之所以重要的一个现实因素是:众多的库厂商向用户提供的往往只是一个二进制库文件而不是源码,得到这个库文件使用的编译器型号和 版本如果与用户应用程序使用的编译器不一致,就很可能不兼容,库厂商不可能提供所有的编译器型号和版本的库给用户,而且一旦厂商停止对该库的升级维护之 后,这些古老的库就更加难以与新版本编译器编译出来的应用程序二进制兼容了。

  •     现代某种语言的开发环境都会提供该语言的“language library”,其实质就是对OS系统调用的包装。所谓的静态库可以简单理解为“一组目标文件打包后的集合”。例如Linux平台下,使用ar工具就可 以将成百上千的*.o文件打包成一个库文件,例如最常用的/usr/lib/libc.a(这个库文件是Linux系统库Glibc项目的一部 分),Windows平台中类似的在VC/lib/下有libcmt.lib等系统库文件。
        Linux下使用
    ar -t 库文件名 可以查看该库文件中包含有哪些目标文件;使用 ar -x 库文件名 可以将库中所有的目标文件解压出来。
  •     从概念上说,我们使用ld将libc.a和hello.o程序链接之后就可以得到可执行文件hello了,但事实上这是远远不够的,因为现代linux系 统中的库非常复杂,我们编译和链接一个普通的C应用程序时,不仅需要使用libc.a,还需要其他的很多辅助性目标文件和库。
  •     通常情况下,使用链接器提供的默认链接规则对目标文件进行链接即可,但是对于OS kernel、BIOS、嵌入式系统中的BootLoader和特定程序等,需要对输出文件的各个段的虚拟地址、段名称、段存放顺序等有特殊要求,这时就 需要人工地对链接过程进行控制了,控制的方法包括三种:
    1、命令行模式下的特别参数,例如 ld -e -o等
    2、编译器将链接控制指令存放于目标文件中特定的段内
    3、最灵活和最常用方式是使用链接控制脚本,可以通过
    ld -T 脚本文件 来指定链接控制脚本,系统默认地使用 /usr/lib/ldscript/下特定平台和输出文件格式的脚本来进行链接。
  • 使用gcc编译器时,常常使用 -fno-builtin 参数来禁止gcc自动进行的一些内置函数优化(所谓的内置函数,即gcc在编译时可能将某些常用的C库函数调用替换成效率更高的内置函数),使用 -static 参数表明是静态链接。
  •     编译器和链接器与目标文件格式密切相关,而不同硬件和软件平台下生成的目标文件格式相差很大,即使同属ELF文件格式但在不同平台上也有不同的变形,对于 gcc、binutils这样的跨平台工作工具而言,应该有一个处理目标文件的统一接口,从而将编译器和链接器本身与实际的目标文件格式隔离开——作为 binutils项目子项目的GNU BFD就实现了这个要求。BFD向编译器和链接器提供抽象的统一的目标文件格式,gcc和binutils工具直接处理的目标文件都是BFD格式的,当需 要添加一个新的目标文件格式时,只需要按照统一的规则修改BFD库即可而不用修改gcc和binutils工具。
        在ubuntu系统中,BFD库的软件包为binutils-dev,在新立得中安装这个软件包后即可在在gcc和binutils等工具中使用BFD库了,例如 gcc -lbfd -o Target Target.c
阅读(1765) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~