计算机系统基础-学习记录12

链接(续)

静态链接(续)

  静态链接将公用库单独形成一个库文件。库文件共有两种:静态库文件和动态库文件,正式这两种不同类型的库文件,导致了链接方式也有两种:静态链接和动态链接

  Linux系统下的静态库文件,后缀名为.a,属于二进制序列化文件,链接时会在在里面找到函数的定义

  静态链接和动态链接的区分:静态链接是在库里面找到想要找的代码,并将代码和数据拷贝到可执行目标文件里面

  在(静态)链接解析的时候,就会根据符号表没有找到定义的,到库里面去找。找到了的,就会拷贝到目标文件中,将代码和数据都拿过去

  在程序中引用静态库中的内容时,是把object目标文件直接替换过来

可执行目标文件

  可执行目标文件的主要作用:方便文件的加载/装载(将文件加载到内存中后才能执行。冯诺依曼计算机的最基本原理:存储程序的原理,将程序拷贝到内存中才能执行)。将放在一起的节连在一起,在内存中占有连续存储单元的节。这些节在拷进内存后是连着的,将整体称作“段”,段是可以直读的

  只读部分的节合在一起,称作代码段,而可读可写的节合在一起,则是称作数据段。此外,剩下的节无需装载进内存,所以就不需要了。代码段和数据段一定得装载到内存中,剩下的则不用

  和可重定位目标文件相比,可执行目标文件多了一张独有的表:Segment header table——段头部表。为了方便装载,段头部表是以段为单位来进行描述的。在装载时,是根据段头部表来进行装载(确定是装到哪)的

  关于段寄存器:在intel机器中,有CS、DS——数据段寄存器,SS——代码段寄存器,指向堆栈段(SS+SP),附加段寄存器:ES,FS,GS。这几个寄存器在intel处理器中一直保持16位没变,因为这是从16位机的时候就有的。在32位机下,intel继续保留了这几个寄存器,但这反而复杂化了虚拟到物理地址的转换。而到64位机后,intel坚决打算丢弃32位机,就去除了这几个寄存器做了新的64位机,但是因为不兼容的问题,这个新的64位机并不成功。所以,这几个寄存器依旧被继续保存下来,但几乎就被完全抛弃了,只用其中的三个。保存这几个寄存器,是为了兼容32位/16位的程序,在执行32位代码时,这几个寄存器依旧有用

  可执行目标文件的节的结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QQvtLFMr-1606998033996)(C:\Users\蔡三圈\AppData\Roaming\Typora\typora-user-images\image-20201203194254022.png)]

  段头部表由ELF文件头给出,此头部表给出了偏移地址、段头部表的大小、每一个表项的大小、有多少个表项。找到这个头部,就可以找到段头部表(在文件头里面获取信息去找)。找到段头部表才能进行装载,装载时,一段一段地调入内存

  段头部表每一个表项里面的内容:

typedef struct {
	unsigned int  p_type ;
	unsigned int  p_offset ;
	unsigned int  p_vaddr ;
	unsigned int  p_paddr ;
	unsigned int  p_filesz ;
	unsigned int  p_memsz ;
	unsigned int  p_flags ;
	unsigned int  p_align ;
} Elf64_phdr ;

  p_type:类型。段能不能装载

  p_offset:参数。说明段在文件里面的偏移地址,找到第一个字节在哪

  p_vaddr:这个段要放在内存的哪个位置。装载在虚拟空间里面

  p_paddr:物理地址。看不到,没有用。物理地址在真正执行时才会从虚拟地址转换过来,并不能看到。反汇编跟踪出来的都是虚拟空间的地址

  p_filesz:大小有多大

  p_memsz:占的内存有多大。内存肯定比段要大,但是要对齐,这样一来,中间的空的位置可能就会有碎片。内存大小是大于或等于实际大小的,因为有对齐的问题

  p_flags:段是什么权限:r/w/x:可读/可写/可执行,代码段:可执行,数据段:可写

  p_align:段调用到内存空间中的起始地址,要求地址对齐,按 2 12 = 4 k 2^{12}=4k 212=4k进行对齐,在主存中将内存划分为同样大小的块——页,页的大小一般为:一页4k字节。主存调用辅存,一般一次一页,占有连续的一页。虚拟地址变为虚页号,右边页内地址(页的偏移地址)。物理地址——物理(实)页号。虚拟地址转物理地址:虚页号转物理页号,用页表转换,页表常驻内存转换

  代码段和数据段,在段头部表中最少要有两个表项。根据段头部表,在可执行目标文件里面去找对应的段,并且将段调到虚拟空间,按顺序存放。每一个段占的都是一个连续的空间

  在创建进程时,会为进程分配一个空间,给这个进程用,看起来就像是独占了整个机器,代码段和数据段映射到这个空间中来。用户栈是在运行时才会创建的。内存上下都是给操作系统用的,中间才是给用户用的。不同的段之间可能会有碎片,这是因为要按页对齐( 2 12 2^{12} 212),连续的段一定是整数页。从段以后,再往上就是堆,而最上面就是栈。栈和堆的中间那一块,是动态库映射过来的。虚拟内存便是按图示进行的装载:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r2KIpZFt-1606998033998)(C:\Users\蔡三圈\AppData\Roaming\Typora\typora-user-images\image-20201203195325181.png)]

  数据段和代码段的起始地址是不一样的,不在一个地方

  所有C语言程序都自动链接公用可重定位文件:crtl.o。这也是C语言都有main函数的原因:<_start>里面调用了main

加载可执行目标文件

  在Linux中运行可执行目标文件,只需./+文件名:

./prog # 运行prog

  运行程序时,会唤醒存储在内存中的操作系统代码:loader。在Linux系统中,loader是使用execve函数调用的。loader应该常驻内存,不能调出,否则会出错(例如蓝屏)。通过文件系统,找到可执行目标文件。装载完后,将控制权交给应用程序,随后应用程序开始执行自己的代码,获得CPU主权,在CPU上开始执行程序

  静态库的缺点:复制多个副本,浪费空间。例如标准C函数,在代码中用了很多次,对内存的利用率不高,浪费了很多内存

由此引入概念:共享库(动态库)

动态库

  有些函数(例如printf)出现的频率非常高,在考虑静态库的情况下,就会每运行一个进程都把代码复制到内存中,这样一来就会消耗内存的宝贵空间。而如果用动态库,则只需要在内存中有一个副本,只要调用一个就行了。动态库文件后缀:Linux:.so,Windows:.dll

  动态库是一种非常特殊的库,数据段在内存中只有一个副本,这个副本被所有的可执行目标文件共用。同一个函数在内存中永远只有一个副本,对内存利用率较高

创建动态库

  gcc编译器即可完成这一过程,使用各种ide也可以生成

gcc -shared -fPIC -o xxx

  -shared:表示创建动态库

  -fPIC:表示与位置无关

  在链接器链接时,是找不到定义的(所有的引用都找不到定义)。在重定位信息表中,增加一条,在装载时重定位。部分链接可执行目标代码:有些地方没有真正地址

  动态链接在Load的时候,将代码装载到空间中,重新进行链接。在Loader段代码里,调用动态链接器代码(也是常驻内存的——动态库)。根据找到的代码和数据,如果不在内存中则装载进入,映射到内存中的共享库区域(位于堆和栈之间),随后就可以计算地址了。Load完以后才是真正完全链接的。动态链接库会将数据和代码拷到空间中,然后重新计算引用的地址,链接结束后,共享库就固定在了一个区域中,不会再进行改变,执行时就不会错了

  如何找到动态链接器?通过路径寻找

  动态链接在可执行目标文件中会出现很多其他的节。因为要和动态链接库进行链接,所以在可执行目标文件中会出现附加的节,这些节是给动态链接器用的。会增加的节包括:

  .interp segment:给出动态链接器代码的位置

  .dynamic section:动态节。存的内容在数据段里面。增加了为动态链接构成的很多数据

  常规的节大家都有,而附加的节则是不一样的

  动态链接将代码从内存中的某个位置,映射到了堆和栈的中间每个位置(实际上是放在内存的公用区域的,因为还得让其他的程序使用)

  需要生成位置无关代码——动态库是位置无关的,在哪里都能调用它

  位置无关:在之前静态链接时,call给出的是相对PC行址。一旦开始执行,就不能改了。只给了偏移地址,而没有给最后的物理地址。所以代码放在任何位置都没关系,只要加一个相对行址就行。位置无关代码则是为了多个运行的进程,通过共享节约内存资源

  (?)需要分配地址,管理起来比较困难。在Loader执行时,生成一个位置无关代码。位置无关代码——PC相对行址,相对位置永远不会变。间接引用会在数据段(位于data的最前面)里面生成一个全局偏移量表(GOT),这张表中存的是相对地址。调用与位置无关变量时,表中存了这个符号的位置

  调用过程:找到表的起始地址,在表中的相对地址,随后就找到了位置,就可以进行调用。通过GOT表,可以找到位置信息

  延后(懒惰)绑定:在链接时给的不是实际的地址,而是随便给的一个一般地址,实际地址在第一次装载时才去计算实际地址。GOT表中的每个表项,只存一个64位的地址。前三项是固定的,分别给动态节的地址,动态链接器的实名信息,装载时调用的动态链接器的第一行代码的地址。随后才是用户的。查表得到地址,这个地址并不是真正的代码的地址,而是指向代码段中的过程链接表(PLT),每一个表项存的是代码。每一个要链接的函数,在PLT中都有对应的表项。第二次调用时,会变成真正的代码的地址。第一次时会跳回来,随后压栈,再跳转到PLT的另外一项,再对全局偏移量表的第一个进行压栈,再跳到动态链接器的代码,将代码和数据拷进来,随后映射到这个空间中,就能修改GOT中的地址,下次调用时就能直接找到代码的位置了

  程序已经执行后再进行链接也是可以的,这时必须要调用系统函数dlopen(),打开动态库来进行动态链接,一般用于高性能的web服务器,运行时再去得到位置

  dlopen()时注意使用相对地址,因为绝对地址一变就不能用了,返回句柄如果为空,则说明没有装载成功。装载成功后,在库中寻找函数,返回指针,指向那个位置,如果找到了,就可以直接进行调用。使用结束后需要将资源进行返还

  shell:Linux的一个壳/界面,用于输入命令的界面

操作系统简介

  操作系统集成了公用的内容,因此较为复杂

  Windows操作系统非常复杂:是一个图形化界面,完成了很多复杂的事情,同时也有很多问题,因此经常进行补丁修复。而Linux操作系统是开源的,并且核非常简洁,只包含了关键部分。Linux的核被不同的公司进行包装,就形成了不同版本的Linux系统

  应用程序和硬件中间的中介——操作系统

  bios:驱动代码,装载完后有个引导程序,引导程序装载操作系统(操作系统在硬盘上的固定位置),装载进内存后就开始执行操作系统,以Windows为例,商标出现后便说明操作系统开始运行

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值