程序员的自我修养(二)静态链接,装载,动态链接与Linux共享文件

*本网站图片外链出现错误(懒得修),如需要图文并茂请移步我的blog:una.cetacis.dev *

第四章 静态链接

4.1 空间与地址分配

输出文件(可执行文件)的空间怎么分配给输入文件

这里的空间分配可以指在可执行文件中空间的分配,也可指装载后的虚拟地址中的虚拟地址空间

但是.data其实在可执行文件中是不存在的,它的分配空间的意义仅局限于虚拟地址空间

事实上,我们谈空间分配只关注于虚拟地址空间的分配

  1. 按序叠加

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WdVZd3q8-1581501317847)(http://47.101.139.155:23333/images/2020/02/09/Screen-Shot-2020-02-09-at-11.20.35-PM.png)]

过于零散。浪费空间:x86硬件,段的装载地址和空间的对齐单位是页(4096字节),如果一个段长度只有1字节,也会占用4096字节

  1. 相似段合并(常用)

    .text放在一起,.data放在一起,等。

    方法:两步链接

    • 第一步 空间与地址分配 链接器获得所有输入文件的段长度,并且合并,建立映射关系。

    • 第二步 符号解析与重定位 (核心)符号解析、重定位、调整代码地址

      $ld a.o b.o -e main -o ab
      

      -e main 表示将main作为程序入口,ld链接器默认入口为_start。

      -o ab 链接输出文件为ab 默认a.out

      **VMA **: Virtual Memory Layout 虚拟地址

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yXJgLiKx-1581501317848)(http://47.101.139.155:23333/images/2020/02/10/Screen-Shot-2020-02-10-at-12.17.14-PM.png)]

4.2 符号解析与重定位
  1. 重定位(链接关键)

    将目标文件反编译以后读文件会发现,一些符号(定义在其他文件中)的地址是00000或者其它

暂定的替代地址。链接后,重定位的入口都会被修正到正确的位置。

  1. 重定位表

    ELF有一个段叫做重定位段(重定位表),用于描述如何修改相应段的内容。

    .text有.rel.text的重定位段。.data有.rel.data的重定位段。

    $ objdumo -r a.o
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0u8dn2og-1581501317848)(http://47.101.139.155:23333/images/2020/02/10/Screen-Shot-2020-02-10-at-2.58.53-PM.png)]

    可以查看a.o需重定位的地方。每个要被重定位的地方叫relocation entry(重定位入口)

    **偏移量(offset)**表示该入口在要被重定位的段中的位置,0000001c和00000027是代码段中”mov“和”call“指令的地址

    此表为代码段的重定位表。

4.3 C++相关问题
  1. 重复代码消除

    • 代码级别链接

      **问题:**C++编译器可产生很多重复代码,如模板(Templates)、外部关联函数(Extend Inline Function)、虚函数表(Virtual Function Table)。如模板,他在一个编译单元被实例化,再其他单元也被实例化,会产生重复代码。会造成空间浪费/地址出错/指令运行效率低。

      **解决方法:**方法即将每个模板的实例化代码放在一个段。同一个模板的实例化代码会有相同的名字,因此在链接器连接时,相同的模板实例段会被合并入最后的代码段。这个机制被广泛运用。GCC编译器(Link Once)和VISUAL C++(COMDAT)

      **缺点:**当相同名称的段拥有不同的内容,不同编译单元使用不同的编译器版本/优化选项,会使一个函数编译出来的代码有所不同,会随机选择其中一个副本作为连接输入并提供一个警告信息。

    • 函数级别链接

      **问题:**目前库/程序庞大,一个目标文件包含很多函数/变量。我们需要某目标文件的任意函数/变量,必须将它整个链接,将没用的函数同时链接,这样链接输出会很大。

      **解决方法:**VISUAL C++提供函数界别链接。将每个函数保存到独立段,当链接器需要某函数,将其合并到输出文件,其他函数舍弃。可以减少输出文件长度。GCC提供相应机制:”-ffunction-sections“和"-fdata-sections",将每个函数/变量分别保持在独立段中。

      **缺点:**优化选项减慢编译/链接过程,链接器计算各个函数的依赖关系;目标函数的段数量增加,重定位过程因段的数目增加而复杂,目标文件因段增加而增大。

  2. 全局构造与析构

    C/C++源代码层面上是从main开始,到main结束。实际上,main调用前,需初始化进程执行环境。如分配初始化、线程子程序。

    C++全局对象构造函数在这一时期执行、C++全局对象的析构函数在main之后执行。

    _start是可执行文件程序初始化部分的入口。

    因此ELF文件还定义可两种特殊段:

    • .init 保存可执行指令。main调用之前,Glibc初始化部分安排执行此段代码。
    • .fini 保存进程终止代码指令。main正常退出,Glibc安排执行此段代码。
  3. C++与ABI

    ABI(Application Binary Interface):二进制层面接口。符号修饰标准、变量的内存布局、函数调用方式等与可执行代码二进制兼容性相关的内容。

    API(Application Programming Interface):源代码层的接口。如POSIX

    API相同不一定ABI相同。ABI不兼容会导致目标文件无法相互连接。硬件、编译语言、编译器、链接器、操作系统都会影响ABI,对于C的目标代码来说,下面几个方面会决定目标文件是否二级制兼容:

    • 内置类型(inr float char)的大小和存储器放置方式(大小端、对齐方式)
    • 组合类型 (struct、union、array)的存储方式和内存分布
    • 外部符号与用户定义符号之间的命名方式和解析方式。如函数名func在c语言的目标文件中是否被解析成_func.
    • 函数调用方式,如参数入栈顺序、返回值保存
    • 堆栈分布方式
    • 寄存器使用约定

    类似还有很多内容,C++在C的基础上增添了很多内容,使其兼容更为不易

    • 继承类体系的内存分布,如基类在继承类中的位置

    • 指向成员函数的指针的内存分布,如何荣国成员函数的指针来调用成员函数。

    C++的二进制兼容性不好。不仅不同编译器不好,甚至一个编译器兼容性不好。C++ ABI标准一直没有统一,现在形成了微软VISUAL C++和GCC(Intel Itanium C++ ABI)为首的两大派系。

4.4 静态库链接

程序会有输入输出,输入输出的可以是程序、人、另一台计算器。操作系统提供的**应用程序编程接口(API)**可进行输入。

语言开发环境会附带语言库,库即是操作系统API的包装。如C的printf,此函数对字符串进行一些处理后,调用操作系统API。各个操作系统,调用API不同,在Linux,调用”write“,windows调用”WriteConsole“。同时,一些库函数也不需要调用API。

一个静态库即一组目标文件的集合。如Linux中C的静态库libc;对于windows,常见的c库由IDE附带的运行库组成,一般由编异器厂提供,如Visual C++附带的C/C++运行库。

对于一个helloworld!文件,本身编译为hello.o,然后还需要libc.a解压缩后得到的printf.o,以及printf.o所依赖的各种.o文件。collect2 是ld链接器的一个包装,调用链接器来完成链接,并对链接结果处理:收集与初始化相关的信息并且构造初始化结构。

4.5 连接过程控制

操作系统内核:从本质来说 他就是一个程序。比如Windows内核 ntoskrnl.exe就是常见的PE文件。

连接控制脚本

控制链接器产生用户所需的文件的方法:

  • 命令行指定参数。比如ld的-o(指定名称),-e(指定入口函数),-static(静态链接)-s(禁止链接器产生符号表)就属于这类
  • 链接指令放在目标文件里边。比如VISUAL C++会把参数放在PE目标文件.drectve段来传递参数。
  • 使用链接控制脚本。(*)

ld在链接时会默认链接脚本。

$ld -verbose
$ld -T link.script

我们也可以自己写一个脚本,使用-T参数:

具体内容与语法可以查本书4.6.3 4.6.4

ld链接脚本有专门的的语言

4.6 BFD库

现代硬件软件平台繁多,包括cpu位数、字节序大小端、MMU有无、内存地址对齐与否… 种种差异导致编译器、链接器很难处理不同平台的目标文件。对于GCC、binutils等跨平台文件,统一接口很重要。

BFD库(Binary File Descriptor library)就是一个GNU项目,目标在于通过统一接口处理不同目标文件格式。它将编译器、链接器本身同具体的目标文件格式隔离开来,一旦我们需要支持新的目标文件格式,只需要修改BFD。目前为止,BFD库支持大约25种处理器平台。

第六章 可执行文件的装载与进程

装载:可执行文件装载到内存。

6.1 进程虚拟地址空间
  1. 程序进程区别

    • 程序(狭义为可执行文件)是一个静态概念,由预编译好的指令和数据集合的文件。
    • 进程:动态概念,程序运行的过程。Runtime
  2. 虚拟空间

    32位,一共4GB空间。64位,2^32*4GB。

    以32位为例,一般会分配1GB给操作系统,3GB给程序使用空间。但是这个比例是可以改的。

  3. PAE

    PAE(Physical Address Extension):一种地址扩展方式。可以将32位扩展至36位地址线,Intel修改了页映射的方式,使得新的映射方式可访问更多的物理内存。

    但是这是补救方式,真正解决办法还是使用64位处理器和操作系统。

6.2 装载方式

动态载入:内存的大小是一定的,如果将指令、数据全部装入内存中,内存数量不够。经研究,程序运行是有局部性原理的,我们可将程序最常用的部分驻留在内存中,将不常用的数据的存放在磁盘,这是动态载入的原理。

覆盖装入(Overlay) **页映射(Paging)**是两种典型动态装载方式。

  1. 覆盖装入

    由程序员手动分割程序,然后编写一个小辅助代码管理模块何时应驻留内存何时被替换。

  2. 页映射

    是虚拟存储机制的一部分。页一般是4096字节。

    装载管理器即是现代的操作系统的存储管理器。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R1k2fO7S-1581501317849)(http://47.101.139.155:23333/images/2020/02/11/Screen-Shot-2020-02-11-at-3.47.13-PM.png)]

    当物理内存满了之后,可通过FIFO(先入先出算法)也可通过LUR(最少使用算法)进行覆盖。

    windows对PE文件的装载、Linux对ELF文件的装载都是这样完成的。

6.3 操作系统角度理解可执行文件装载
  1. 进程的建立

    进程最关键特征在于它拥有独立的虚拟地址空间,这使它有别其他进程。一个程序被执行同时伴随一个新的进程的创建。上述过程开始于三件事:

    • 创建独立的虚拟地址空间

      虚拟空间由一组页映射函数将虚拟空间的各个页映射至相应的屋里空间,这一步实际上是创建映射函数所需要的相应数据结构,即虚拟空间到物理内存的映射

    • 读取可执行文件头,建立虚拟空间和可执行文件的映射关系

      建立虚拟空间和可执行文件的映射关系。即操作系统知道程序当前所需要的页在可执行文件中的哪一个位置。这一步是装载中最重要的一步。可执行文件也可称为image(映像文件)

      VMA(Virtual Memory Area)虚拟内存区域

      如下图,操作系统建立进程后,会在进程相应数据结构中设置一个.text段的VMA。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i2t14b37-1581501317849)(http://47.101.139.155:23333/images/2020/02/11/Screen-Shot-2020-02-11-at-4.15.58-PM.png)]

    • 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行

      CPU执行跳转指令,跳转至可执行文件的入口地址。

第七章 动态链接

  1. 动态链接(Dynamic Linking):程序运行时链接。

    解决问题:共享的目标文件多个副本浪费磁盘、内存的问题;更新困难的问题

    缺点:新的模块与旧的模块不兼容,导致原有程序无法运行。称为”DLL Hell“;动态链接在每次装载时重新进行链接,会有相应的时间损失和性能损失。

  2. 程序可扩展性和兼容性

    因为动态链接有在程序运行时动态地选择加载各个程序模块的特点,**插件(plug-in)**应运而生。

    作用:(1)实现程序功能的扩展

    ​ (2)加强程序兼容性。动态地链接到由操作系统提供的动态链接库,这些动态链接库相当于在程序和操作系统增加了一个中间层,消除了程序对不同平台之间依赖的差异性。

  3. 动态链接的基本实现

    主流操作系统支持动态链接:Linux的ELF动态链接文件称为动态共享对象(DSO,Dynamic Shared Objects),简称共享对象,以".so"为扩展名。windows系统中,动态链接文件称为动态链接库(Dynamical Linking Library),他们就是我们平时常见的".dll"为扩展名的文件。

    比如C语言库的运行库glibc,它的动态链接形式的版本保存在”/lib“目录下,文件名叫做”libc.so“。

    程序被装载->系统的动态链接器将程序所需要的所有动态链接库(libc.so)装载到进程的地址空间,并且将程序中所有未决议的符号绑定到相应的动态链接库中,并进行重定位。

  4. 装载时重定位和地址无关代码是解决绝对地址引用问题的两个方法,装载时重定位的缺点是无法共享代码段,但运行速度快。地址无关代码缺点是运行速度慢,但可实现代码段在各个进程之间的共享。

第八章 Linux共享库组织

共享库和共享对象从文件结构看,没有区别,Linux下的共享库即是普通的ELF共享对象。

  1. 共享库的兼容性

    基于动态链接的灵活性,开发者可以不断更新共享库版本。如libfoo.so的开发者更新新版的libfoo.so后,理论上我们只需要将新版替换旧版。

    但是实际上共享库版本的更新可能导致接口的更改/删除。

    因此,共享库更新分为:

    • 兼容更新
    • 不兼容更新 改变原有接口

    这里讨论的接口是ABI,这里主要包括一些堆栈结构、符号命名、参数规则、数据结构的内存分布风规则。

    ABI十分难以保持兼容,特别是C++,它支持诸如模板等一些高级特性,他们对于ABI的兼容影响很大。

    尽量遵循下列原则:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zd7AXDLs-1581501317849)(http://47.101.139.155:23333/images/2020/02/11/Screen-Shot-2020-02-11-at-6.29.10-PM.png)]

  2. 共享版本库的命名 libname.so.x.y.z

    x是主版本号,不同的主版本号会不兼容

    y是次版本号,新版保存原有所有接口,同时增加一些新的接口符号。

    z是发布版本号,不添加接口,进行错误修正

    SO-NAME 机制,libfoo.so.x.y.z的SO-NAME是libfoo.so.x,即只保存主版本号,并且产生软连接,使每一次y.z进行改变时,始终保持x不变的最新版。同时,如果主版本号升级,系统会产生新的SO_NAME,SO-NAME不同不会对已有的程序影响。

    Linux提供了一个工具叫做”Idconfig“,当系统安装/更新一个共享库,他会遍历所有默认共享库目录(/usr/lib),然后更新所有软;链接,指向最新的共享库。如果安装新的,则会为其创建相应的软连接

  3. 链接名

    libname.so.x.y.z 中,name即为链接名。使用共享库时:

    -l name 
    

    查找最新版本的”name“库

    -lc 可以根据输出文件情况(静态/动态)选择适合版本的库

    ld -static(静态链接)时,"-lc"会查找libc.a;

    ld -Bdynamic(动态链接)时,"-lc"会查找最新版本的libc.so.x.y.z(默认)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值