【链接装载与库】目标文件里有什么

目标文件

编译器编译源代码后生成的文件叫做目标文件
目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程, 其中可能有些符号或有些地址还没有被调整。

目标文件的格式

现在PC平台流行的可执行文件格式主要是Windows下的PE和Linux的ELF, 它们都COFF格式的变种。
不光是可执行文件(Windows的.exe和Linux下的ELF可执行文件)按照可执行文件格式存储。动态链接库(Windows 的.dll 和 Linux的 .so) 及静态链接库(Windows 的.lib 和Linux 的.a)文件都按照可执行文件格式存储。

ELF文件类型说明实例
可重定位文件(Relocatable File)这类文件包含了代码和数据,可以被用来 链接成可执行文件或共享目标文件,静态 链接库也可以归为这一类Linux的.o Windows的.obj
可执行文件(Executable File)这类文件包含了可以直接执行的程序,它 的代表就是ELF可执行文件,它们一般都 没有扩展名比如/bin/bash文件 Windows的.exe
共享目标文件(Shared Object File)这种文件包含了代码和数据,可以在以下 两种情况下使用。 一种是链接器可以使用 这种文件跟其他的可重定位文件和共享目 标文件链接,产生新的目标文件。第二种 是动态链接器可以将几个这种共享目标文 件与可执行文件结合,作为进程映像的一 部分来运行Linux的.so ,Windows的DLL
核心转储文件(Core Dump File)当进程意外终止时,系统可以将该进程的 地址空间的内容及终止时的一些其他信息 转储到核心转储文件Linux下的core dump

我们可以在Linux 下使用 file命令来查看相应的文件格式
在这里插入图片描述

目标文件是什么样的

我们大概能猜到,目标文件中的内容至少有编译后的机器指令代码、数据。没错,除了 这些内容以外,目标文件中还包括了链接时所须要的一些信息,比如符号表、调试信息、字 符串等。 一般目标文件将这些信息按不同的属性,以的形式存储,有时候 也叫
程序源代码编译后的机器指令经常被放在代码段,代码段常见的名 字有.code或.text;全局变量和局部静态变量数据经常放在数据段,数据段的一般名字都叫.data

ELF文件的开头是一个“文件头”,它描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址等信息,文件头还 包括一个段表, 段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等

在这里插入图片描述

一般C 语言的编译后执行语句都编译成机器代码,保存在.text段;已 初始化的全局变量和局部静态变量都保存在.data段;未初始化的全局变量和局部静态变量 一般放在一个叫.“bss”的段里。

.bss 段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。

总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于 程序指令,而数据段和.bss 段属于程序数据。

为什么把程序的指令和数据的存放分开?

  • 数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,这样可以防止程序的指令被有意或无意 地改写。
  • 现代CPU有着极为强大的缓存体系,这样可以提高缓存的命中率
  • 最重要的原因,就是当系统中运行着多个该程序的副本时,它 们的指令都是一样的,所以内存中只须要保存一份改程序的指令部分。

挖掘SimpleSection.o

SimpleSection.c

int printf(const char* format, ...);

int global_initvar = 84;
int global_uninitvar_var;

void func1(int i)
{
    printf("%d" , i);
}

int main(void)
{
    static int static_var = 85;
    static int static_var2;

    int a = 1;
    int b;

    func1(static_var + static_var2 + a + b);

    return a;
}

使用gcc编译

gcc -c SimpleSection.c

我们可以使用binutils的工具objdump来查看object内部的结构,参数“-h”就是把ELF 文件的各个段的基本信息打印出来

objdump -h SimpleSection.o

在这里插入图片描述

在这里插入图片描述

除了最基本的代码段、数据段和BSS 段以外,还有3个段分别是只读数据段(.rodata)、注释信息段(.comment)和堆栈提示段(.note.GNU-stack), 这3个额外的段的意义我们暂且不去细究。先来看看几个重要的 段的属性,其中最容易理解的是段的长度(Size)和段所在的位置 (File Offset), 每个段的第2行中的 “CONTENTS”,“ALLOC” 等表示段的各种属性“CONTENTS” 表示该段在 文件中存在。我们可以看到 BSS 段没有 “CONTENTS”, 表示它实际上在 ELF 文件中不存 在内容。“note.GNU-stack”段虽然有 “CONTENTS”, 但它的长度为0,这是个很古怪的段, 我们暂且忽略它,认为它在ELF 文件中也不存在。那么ELF 文件中实际存在的也就是“.text”、 “.data”、“.rodata”和“.comment” 这4个段
它们在ELF中的结构

在这里插入图片描述

代码段

objdump的“-s”参数可以将 所有段的内容以十六进制的方式打印出来,“-d”参数可以将所有包含指令的段反汇编。

objdump -s -d SimpleSection.o

在这里插入图片描述

在这里插入图片描述

最左面一列是偏移量,中间4列是十六 进制内容,最右面一列是.text段的ASCII码形式,.text 段 的第一个字节“0x55” 就是 “func1()”函数的第一条“push %ebp”指令而最后一个字节 0xc3 正是main()函数的最后条指令“ret”。

数据段和只读数据段

  • .data段保存的是那些已经初始化了的全局静态变量和局部静态变量。
  • SimpleSection.c里面我们在调用“printf”的时候,用到了一个字符串常量“%d\n”, 它是一种只读数据,所以它被放到了“.rodata”段
  • “.rodata”段存放的是只读数据,一般是程序里面的只读变量(如const修饰的变量)和字符串常量。
  • 有时候编译器会把字符串常量放到“ .data”段,而不会单独放在 “.rodata”段

在这里插入图片描述

BSS段

.bss段存放的是未初始化的全局变量和局部静态变量,如上述代码中 global uninit var和static var2就是被存放在.bss段,其实更准确的说法是.bss段为它们预留了空间。
有些编译器会将全 局的未初始化变量存放在目标文件.bss段,有些则不存放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss 段分配空间
值得一提的是编译单元内部可见的静态变量(比如给global uninit var加上 static修饰)的确是存放在.bss段的

在这里插入图片描述

以下代码

static int x1 = 0;
static int x2 = 1;

x1和x2会被放在什么段中呢?

x1 会被放在.bss中 ,x2会被放在.data中。为什么一个在.bss段,一个在.data段 ? 因为 x1为0,可以认为是未初始化的,因为未初始化的都是0,所以被优化掉了可以放在.bss,这样可以节省磁盘空间,因为.bss不占磁盘空间。

其他段

在这里插入图片描述

GCC 提供了一个扩展机制,使得程序员可以指定变量所处的段

在这里插入图片描述

ELF文件结构描述

ELF 目标文件格式的最前部是 ELF 文件头 (ELF Header), 它包含了描述整个文件的 基本属性,比如ELF文件版本、目标机器型号、程序入口地址等。紧接着是ELF文件各个段。其中ELF文件中与段有关的重要结构就是段表(Section Header Table), 该表描述了ELF文件包含的所有段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。

文件头

我们可以用readelf命令来详细查看ELF文件

readelf -h simpleSection.o

在这里插入图片描述

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

ELF 文件头结构及相关常数被定义在“/usr/include/elf.h”里,ELF文件有32位版本和64位版本,它的文件头结构也有这两种版本,分别叫 做“EIf32 Ehdr”和“Elf64 Ehdr”。

  • ELF魔术 Magic
    这16个字节被 ELF 标准规定用来标识ELF文件的平台属性,比如这个ELF 字长(32位/64位)、字节序、ELF文件版本

段表

我们知道ELF 文件中有很多各种各样的段,这个段表 (Section Header Table)就是保存这些段的基本属性的结构,它描述了ELF的各个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。段表在ELF 文件中的位置由ELF 文件头的“e shoff” 成员决定,比如 SimpleSection.o中,段表位于偏移0x118
前文“objdump-h”命令只是把 ELF 文件中关键 的段显示了出来,而省略了其他的辅助性的段,我们可以使用 readelf工具来查看ELF文件的段,它显示出来的结果才是真正 的段表结构:

readelf -S SimpleSection.o

在这里插入图片描述
在这里插入图片描述

段表的结构比较简单,它是一个以 “EIf32 Shdr”结构体为元素的数组。数组元素 的个数等于段的个数,每个 “EIf32 Shdr”结构体对应一个段。“EIf32 Shdr”又被称为段描述符,ELF段表的这个数组的第一个元素是无效的段描述符,它的类型为 “NULL”

段的位置和长度:

在这里插入图片描述

  • 段的类型
  • 段的标志位
    段的标志位表示该段在进程虚拟地址空间中的属性,比如是否可写
  • 段的链接信息
    如果段的类型是与链接相关的(不论是动态链接或静态链接),比如重定位表、符号表等,那么sh Jlink和sh info 这两个成员所包含的意义如表图所示。对于其他类型的段,这两个成员没有意义

在这里插入图片描述

重定位表

我们注意到,SimpleSection.o中有一个叫做“.rel.text”的段,它的类型为 “SHT REL”,也就是说它是一个重定位表,对于每个须要重定位的代码段或数据段,都会有一个相应的重定位表。比如 SimpleSection.o中的“.rel.text”就是针对“.text”段的重定位表

字符串表

ELF文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的, 所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串

在这里插入图片描述
在这里插入图片描述

在ELF文件中引用字符串只须给出一个数字下标即可,不用考虑字符 串长度的问题。 一般字符串表在ELF 文件中也以段的形式保存,常见的段名为“.strtab”或 “ .shstrtab” 。 这两个字符串表分别为字符串表和段表字符串表

链接的接口—符号

在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用
比如目标文件B要用到了目标文件A中的函数 “foo”, 那么我们就称目标文件A 定义了函数 “foo”,称目标文件B引用了目标文件A 中的函数“foo”
在链接中,我们将函数和变量统称为符号, 函数名或变量名就是符号名
我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成
每一个目标文件都会有一个相应的符号表,这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值,对于变量和函数来说,符号值就是它们的地址。
我们将符号表中所有的符号进行分类,它们有可能是下面这些类型中的一种:

  • 定义在本目标文件的全局符号,可以被其他目标文件引用(“func1”、“main”和 “global init var”)
  • 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(“printf”)
  • 段名(“.text"、".data”)
  • 局部符号(“static var”和"static var2")
  • 行号信息

使用 “nm” 来查看 “SimpleSection.o”的符号结果

在这里插入图片描述
在这里插入图片描述

符号表结构

ELF文件中的符号表往往是文件中的一个段,段名一般叫“.symtab”。符号表的结构很简单,它是一个EIf32 Sym结构(32位ELF文件)的数组,结构如下
在这里插入图片描述

  • 符号类型和绑定信息
  • 符号所在段
    如果符号定义在本目标文件中,那么这个成员表示符号所在的 段在段表中的下标;但是如果符号不是定义在本目标文件中,或者对于有些特殊符号,sh_shndx 的值有些特殊
    这里使用 readelf 工具来查看 ELF 文 件的符号
readelf -s SimpleSection.o

在这里插入图片描述
在这里插入图片描述

第一列 Num 表示符号表数组的下标,从0开始,共15个符号;第二列Value 就是符号值,即st_value; 第 三列Size为符号大小,即st size;第四列和第五列分别为符号类型和绑定信息,即对应st_info 的低4位和高28位;第六列 Vis 目前在C/C++ 语言中未使用,我们可以暂时忽略它;第七 列 Ndx 即st_shndx, 表示该符号所属的段;当然最后一列也最明显,即符号名称。

特殊符号

  • __executable_start, 该符号为程序起始地址,注意,不是入口地址,是程序的最开始的地址
  • __etext 或 _etext 或etext, 该符号为代码段结束地址
  • _edata 或edata, 该符号为数据段结束地址
  • _end 或 end, 该符号为程序结束地址
  • 以上地址都为程序被装载时的虚拟地址

符号修饰与函数签名

众所周知,强大而又复杂的C++拥有类、继承、虚机制、重载、名称空间等这些特性,它们使得符号管理更为复杂,为了支持C++这些复杂的特性,人们发明了符号修饰符号改编的机制

int func(int);
float func(float);

class C{
    int func(int);
    class C2{
        int func(int);
    };
};

namespace N{
    int func(int);
    class C{
        int func(int);
    };
}

我们引入一个术语叫做函数签名, 函数签名包含了一个函数的信 息,包括函数名、它的参数类型、它所在的类和名称空间及其他信息,函数签名用于识别不 同的函数
它们使用某种名称修饰的方法,使得每个函数签名对应一个修饰后名称C++编译器和链接器都使用符号来识别和处理函数和变量

在这里插入图片描述

弱符号与强符号

符号的定义可以被称为强符号。有些符号的定义可以被称为弱符号对于C/C++ 语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。我们也可以通过 GCC 的“ _attribute_((weak))”来定 义任何一个强符号为弱符号。

注意,强符号和弱符号都是针对定义来说的不是针对符号的引用

链接器会按如下规则处理与选择被多次定义的全局符号:

  • 不允许强符号被多次定义
  • 如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选 择强符号。
  • 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一 个。

弱引用和强引用
如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用,与之相对应还有一种弱引用, 在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果 该符号未被定义,则链接器对于该引用不报错。一般对于未定义的弱引用,链接器 默认其为0,或者是一个特殊的值,以便于程序代码能够识别。弱引用和弱符号主要用于库的链接过程

这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强 符号所覆盖,从而使得程序可以使用自定义版本的库函数

调试信息

目标文件里面还有可能保存的是调试信息
如果我们在GCC 编译时加上“-g”参数,编译器就会在产生的目标文件里面加上调试信息现在的ELF文件采用一个叫DWARF(Debug With Arbitrary Record Format) 的标准的调试信息格式在Linux下,我们可以使 用“strip”命令来去掉ELF文件中的调试信息

strip foo

参考资料<<程序员的自我修养>>

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值