当前位置: 首页 > news >正文

【链接、装载和库】三、目标文件详解

编译后产生的文件我们称之为目标文件,目标文件中到底有什么东西,这是本文主要探讨的内容。

一、目标文件格式浅析

现在PC主要使用的可执行文件格式主要是Linux下的ELF和Win下的PE。可执行文件格式不仅仅包含可执行文件,还包含动态链接库、静态链接库等文件,具体如下表:

ELF文件类型说明实例
可重定位文件包含代码和数据,可以被链接为可执行文件或者共享目标文件Linux的.o和win的.obj
可执行文件可以直接执行的程序,典型的ELF可执行文件就是这类win的exe
共享目标文件共享库Linux的.so,Windows的DLL
核心转储文件进程意外终止时候的进程快照Linux下的core dump

1.1 目标文件的内部组成

目标文件包含了编译后的指令和数据,内部的信息使用**段(Segment)**进行了分割分类,如下图,一个简单的代码编译链接后生成的可执行文件包括:文件头、代码段、数据段、bbs段等。
在这里插入图片描述一般C语言编译后的执行语句保存在.text段;已经初始化的全局变量和局部静态变量保存在.data段;未初始化的全局变量和局部静态变量保存在.bss段里。将数据和指令分离为两个段主要是因为:

  1. 数据是可读写的,但是指令是只读的,可以防止程序被篡改
  2. 现代计算机的缓存被设计为数据缓存和指令缓存分开,从而提高缓存的命中率
  3. 当系统中运行着多个相同程序副本的时候,他们的指令都是一样的,可以只保留一份指令数据。

当我们使用objdump工具查看其段信息的时候,可以看到如下信息:
在这里插入图片描述
可以看到各个段的大小,顺便再看看text段中的指令存储内容:
在这里插入图片描述
可以看到指令被编译成了汇编,截图中除了上述的段,还有三个段:只读数据段,注释信息段和堆栈提示段(.note.GNU-stack)。.rodata段存放程序中的只读数据,比如被const修饰的,单独设立rodata段可以保证只读数据的安全,一些嵌入式平台可以将数据存入ROM这种只读存储器中,可以硬件上保证数据安全。

上文提到未初始化的全局变量和局部静态变量保存在.bss段里,而未初始化的全局变量和局部静态变量默认值都是0,所以在bbs段中也不会记录这些变量的实际值,只负责记录未初始化的全局变量和局部静态变量并且预留位置

除了上面列出来的段,还有一些其他段,如下表
在这里插入图片描述

二、ELF文件结构描述

文件头位于ELF为念的最前部,文件头中描述了整个文件的属性:是否可执行、是静态链接还是动态链接、它的硬件属性是什么,以及一个记录了ELF文件包含所有段信息的段表。

2.1 文件头

使用readelf指令查看ELF文件头
在这里插入图片描述
文件头中存储了ELF魔数,文件的字节长度、数据存储方式、硬件平台、入口地址、段表位置等信息。这个结构一般定义在“/usr/include/elf.h”中,结构体源码如下:

typedef struct {unsigned char e_ident[EI_NIDENT]; // ELF 魔数和文件类信息等Elf64_Half e_type; // 文件类型Elf64_Half e_machine; // 指定的架构Elf64_Word e_version; // 对象文件版本Elf64_Addr e_entry; // 程序执行的入口地址Elf64_Off e_phoff; // 程序头表文件偏移量Elf64_Off e_shoff; // 段头表文件偏移量Elf64_Word e_flags; // 处理器特定的标志Elf64_Half e_ehsize; // ELF 头的字节数Elf64_Half e_phentsize; // 一个程序头表项的字节数Elf64_Half e_phnum; // 程序头表中的表项数Elf64_Half e_shentsize; // 一个段头表项的字节数Elf64_Half e_shnum; // 段头表中的表项数Elf64_Half e_shstrndx; // 包含节头字符串表的节头索引
} Elf64_Ehdr;

2.2 段表

ELF中有各种各样的段,段表就是保存这些段的基本信息的结构,描述的段名字、长度、偏移等信息,使用readelf命令查询段表
在这里插入图片描述
段表的结构比较简单,是一个Elf32_Shdr结构体数组,数组元素就是段个数,因此Elf32_Shdr又称为段描述符,这个结构体的成员如下:
在这里插入图片描述

2.3 重定位表

段表中有一个类型为SHT_REL的段.rel.text,也就是说他是一个重定位表。链接器在处理目标文件的时候,需要对目标文件中的某些部位进行重定位,也就是代码段和数据段中那些对绝对地址的引用位置。这些重定位信息都记录在重定位表中,每一个需要重定位的代码段或者数据段都会有一个相应的重定位表,比如.rel.text对应的是text代码段的重定位表,这是因为代码中包含了printf函数,这是一个库函数,需要在链接的过程中重新连接到对应的库中。具体结构请见下一篇“静态链接”相关内容

2.4 字符串表

ELF文件中使用到了很多字符串,比如变量名,段名之类的,字符串长度不定,因此不适合使用固定结构来表示,因此使用字符串在表中的偏移来引用字符串。一般字符串表也是以段的形式保存的,常见段名位.strtab.shstrtab,这两个分别代表字符串表和段表字符串表,段表字符串表比较特殊,专门用于存放段表中用到的字符串。因为刚开始进入ELF文件的时候,连段表字符串表在哪里都不知道,因此是无法直接解析段表的,所以ELF头文件中加了e_shstrndx成员变量,用于专门指向段表字符串表shstrndx的下标,通过解析头文件,就可以得到段表和段表字符串表的位置,从而解析整个ELF文件

2.5 链接的接口——符号

链接的本质是将多个不同的不妙文件相互粘在一起,在链接中,目标文件之间的相互拼合实际上是目标文件之间对地址的引用,也就是对函数和变量的地址引用。比如目标文件B要用到了自标文件A中的函数“foo”,那么我们就称目标文件A 定义(Define)了函数“foo”,称目标文件B 引用(Reference)了目标文件A中的函数“foo”。这两个概念也同样适用于变量。每个函数或变量都有自已独特的名字,才能避免链接过程中不同变量和弱数之间的混济。在链接中,我们将函数和变量统称为符号,函数名或变量就是符号名(SymbolName)。我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(SymbolTable),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(SymbolValue),对于变量和函数来说,符号值就是它们的地址。除了函数和变量之外,还存在其他几种不常用到的符号。

3.5.1 ELF符号表结构

ELF符号表往往是文件中的一个段,名字为.symtab,符号表的结构是一个Elf32_Sym结构体的数组,每一个结构体对应一个符号。

typedef struct{Elf32_Word st_name;Elf32_Addr st_value;Elf32_Word size;unsigned char st_info;unsigned char st_other;Elf32_Half st_shndx;
}Elf32_Sym;

st_info的低四位用于表示符号类型,高28位表示符号绑定信息。
在这里插入图片描述

特殊符号

当我们使用ld链接器来链接生储层可执行文件的时候,它会为我们定义很多特殊符号,这些特殊符号并没有在源代码中定义但是可以直接声明并且引用他们,所以称为特殊符号。具有代表性的几个有:

  • __executable_start: 程序起始地址,注意,不是程序入口,是程序在内存中的起始地址。
  • __etext: 代码段结束地址
  • __edata: 数据段结束地址
  • __end: 程序结束地址

符号修饰和函数签名

C++拥有面向对象的众多特性,包括类、继承、重载等特性,当函数重载的时候,最简单就是同样的函数名拥有不同的参数,那么符号表只记录函数名就区分不开两个重载函数了,除此之外还有命名空间机制,可以使得不同的命名空间有同名的函数。因此引入了符号修饰的机制

在符号修饰中有一个概念叫做函数签名,其中会包含一个函数的函数名、参数类型、所在的类和命名空间,因此可以很好地支持上面提到的各种机制。在编译器和链接器处理这些函数签名的时候,会使用某种名称修饰的方法,使得每一个函数签名对应一个修饰后名称。也就是说,源代码编译后的目标文件中使用的符号名是经过符号修饰后的符号名,同名的重载函数也会被当作是不同名字的重载函数来进行处理。下表展示了不同命名空间、不同类、不同参数下的同名重载函数,在经过符号修饰后的符号名
在这里插入图片描述
此外cpp提供了extern C关键字来实现对C语言的兼容,因此在extern C内的内容是不会进行符号修饰的。

弱符号和强符号

我们在编程中经常碰到的一种情况叫做符号重复定义,多个目标文件中含有相同名字的全局符号定义,就会在链接的时候出现符号重复定义错误。对于C++来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号,也可以使用__attribute__((weak))来将一个强符号显式指定为弱符号。针对强弱符号,有如下规定:

  • 规则1:不允许强符号被多次定义,否则报错
  • 规则2:如果一个符号名有一个强符号和多个弱符号,则选择其强符号定义
  • 规则3:如果一个符号在所有目标文件中都是弱符号,则选择其中占用空间最大的一个。

此外还引入了强引用和弱引用两个概念:
在目标文件中的对外部目标文件的符号引用被最终链接成可执行文件时,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用。与之相对应还有一种弱引用,在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器对于该引用不报错。链接器处理强引用和弱引用的过程几乎一样,只是对于未定义的弱引用,链接器不认为它是一个错误。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值,以便于程序代码能够识别。可以通过__attribute__((weakref))显式指明一个弱引用。

这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用;如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得程序的功能更加容易裁剪和组合。弱引用和弱符号主要用于库的链接过程,我们将在“库”这一章再来详细讲述。

相关文章:

  • [Java恶补day20] 54. 螺旋矩阵
  • RK3568/RK3588 KVM系统虚拟化解决方案
  • 吉客云ERP集成金蝶ERP(云星空、云星辰、云星瀚、KIS、K3、EAS)
  • 全面解析数据库:从基础概念到前沿应用​
  • 条件语句 if语句 + if...else+switch语句+三元运算符
  • 构建欺诈事件的结构化威胁建模框架
  • Invalid context structure解决Dify框架中图像推理错误:一步步排查与修复指南
  • 软件开发工程师如何在项目开发中了解学习 ISO 13485(2)
  • 编程工具点亮效率之光
  • 中小企业碳账本管理指南
  • 瞬移--BFS+set去重
  • 【DVWA系列】——xss(Reflected)——Medium详细教程
  • sql server连接遇到的问题
  • Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)
  • 《Minio 分片上传实现(基于Spring Boot)》
  • LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
  • 第一章 空间解析几何与向量代数 ~ 空间直角坐标系
  • 人工智能100问☞第43问:什么是提示工程(Prompt Engineering)?
  • Python 训练 day46
  • LeetCode - 3. 无重复字符的最长子串
  • 做网站用什么语言/网站收录免费咨询
  • 为什么我的电脑有些网站打不开/百度搜索收录
  • 专业网站建设必要性/南宁百度seo优化
  • 青岛城市建设局网站/临沂百度公司地址
  • 开发一个网站要多久/怎么做一个网站平台
  • 网站建设图片上传/今日头条官网首页