前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《程序员的自我修养》笔记

《程序员的自我修养》笔记

作者头像
小柔
发布2024-07-24 08:14:43
810
发布2024-07-24 08:14:43
举报
文章被收录于专栏:小柔博客园

使用objdump查看mach-o文件

objdump -h:查看段信息

vma是指的不同段的地址入口,可以看到虽然段有很多,但是type类型大部分都一样,比如代码段类型分为了两个段描述更加细致;数据段更夸张用了五个段存储初始化了的变量

objdump -s:查看段内容

-s命令可以看到 段里面的具体内容

这里其实有个非常尴尬的地方就是 mac的gcc编译出来的目标文件不是Elf文件格式的,而是Mac-o格式的,所以无法使用readelf查看

虽然不一样但是天无绝人之路,有一个otool的工具可以实现类似readelf命令的功能,也是查看各段的命令

使用otool查看mach-o文件

这个是节选了一点打印的段的内容:

主要有几个关键的section(合并多个segement)

text段里面会写当前段地址和大小还有对齐大小

ELF组成

除了.text、.data、.bss这3个最常用的段之外,ELF文件也有可能包含其他的段,用来保存与程序相关的其他信息

符号表也是段

elf文件头结构

elf文件类型:重定位/可执行文件/共享文件类型
EM:可运行的CPU平台
eshotoff:段表的偏移

使用readelf -S可以查看 详细的段表内存储的段信息

每个“ Elf32_Shdr ”结构体对应一个段表里面存储的短信息。“ Elf32_Shdr ”又被称为段描述符(Section Descriptor)。

Elf32_shdr:段表内记录的段信息

会记录段的一些关键信息

sh_type/sh_flag:段类型和段标志位
sh_type:段类型:程序段还是符号表还是重定位表...
sh_flag:段标志位:可写/可执行/运行要分配内存空间,
辅助段的类型和标志位信息
shlink,shinfo:段链接的相关信息

如果段的类型是与链接相关的(不论是动态链接或静态链接),比如重定位表、符号表等就有下面的意义,对于其他不需要链接的段来说 这两个属性没有意义。

重定位表段:Elf32_Rel

修正就直接修改成了符号的虚拟地址了,不使用便宜了,等到所有段,所有符号都有了虚拟地址之后,就可以去重定位表里面去找到重定位入口吧地址修改为符合的虚拟地址

偏移量就是这个符号在这个段里面的 偏移量,通过这个偏移量找到这个符号来进行修正

可以理解为如果重定位表的符号在最终链接后的全局符号表里面没有找到,就可以认定是链接失败,没有找到外部文件定义的符合,一般是少链接了库

虽然处理器的寻址方式有很多种,但对于elf重定位入口 修正的地址来说只考虑近址寻址里面的绝对地址和相对地址,而这两个就是重定位入口的类型,只有两个。

对于每个须要重定位的代码段或数据段,都会有一个相应的重定位表。比如SimpleSection.o中的“.rel.text”就是针对“.text”段的重定位表,因为“.text”段中至少有一个绝对地址的引用,那就是对“printf”函数的调用;而“.data”段则没有对绝对地址的引用,它只包含了几个常量,所以SimpleSection.o中没有针对“.data”段的重定位表“.rel.data”。

一个重定位表同时也是ELF的一个段,那么这个段的类型( sh_type )就是“ SHT_REL ”类型的,

它的“ sh_link ”表示符号表的下标,

它的“ sh_info ”表示它作用于哪个段。

比如“.rel.text”作用于“.text”段,而“.text”段的下标为“1”,那么“.rel.text”的“ sh_info ”为“1”。

重定位表符号地址修复案例

shared是变量,而swap是函数。可以看到shared采用的地址修正方式是绝对寻址,而swap函数式相对寻址

首先申城全局符号表并且符号的地址已经分配之后,就可以去重定位表里面找到需要修复的符号了,这些符号在编译之前由于不知道地址所以用的值一般是0,又因为是绝对寻址修复所以就是S+A,直接吧该符号的值替换成虚拟地址的值,这样所有使用该符号的地方都会使用这个地址

字符串表段

把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串,所以用到的字符串都是用的字符串里面的下标而不是字符串

一般字符串表在ELF文件中也以段的形式保存,常见的段名为“.strtab”或“.shstrtab”。

这两个字符串表分别为字符串表(String Table)和段表字符串表(Section Header String Table)。

顾名思义,字符串表用来保存普通的字符串,比如符号的名字;

段表字符串表用来保存段表中用到的字符串,最常见的就是段名( sh_name )。

符号表:记录符号信息的数组

使用命令查看的符号表的结构
符号结构体:Elf32_Sym
stinfo:低4位代表符号类型,高28为代表符号绑定类型
st_shndx:符号所在段

如果符号是在当前文件,那么这个值就是符号所在段 在段表中的索引;如果不在当前文件(代表外部符号,值是未定义)或者特殊的符号(比如初始化了全局的符号)那就找不到符号所处端的信息,所以取下面的值:

st_value:符号值(对应符号在文件中的偏移)所处段+符号的段偏移
  • 在目标文件中:

如果“符号所在段(st_shndx)”的值不是COMMON类型的,那么这个符号值代表的就是这个符号在文件中的偏移,即符号所对应的函数或变量位于由st_shndx指定的段,偏移st_value的位置;

如果是COMMON类型的符号,则st_value表示该符号的对齐属性。

  • 在可执行文件中:

stvalue指的是符号加载到内存后的虚拟地址,这个地址对于连接器非常有用

使用命令查看符号表的内容

num就是符号在符号表中的下标

value就是符号值,

size:代表符号的数据类型需要占用多少空间(比如对于int类型的符号那么就是4字节;对于double就是8字节)

type和bind:代表符号的类型(数据对象,文件名,函数,段)和符号的绑定类型(local或者global代表局部符号和全局符号)

Vis:c和c++没有使用

Ndx(sh_ndx):代表符号使用的段

Name:就是符号名称

第一个符号 永远是未定义的符号,所以略过这个即可

fun1和main是第一个段索引也就是代码段,fun类型代表函数类型,GLOBAL代表全局可见,27和64指的是

可以看到最上面的就是符号结构体里面的属性可以一一对应上

windows-PE

Windows和PE的兼容

windows因为当时早起并不能独立运行只能依托于dos系统,所以PE文件要支持这两种系统识别,所以PE得开头多了两个专用于dos可执行文件wz格式的段,这两个段一个是dos的文件头,一个是dos插妆的段。

PE可执行映像在DOS下被加载的时候,DOS系统检测该文件,发现最开始两个字节是“MZ”,于是认为它是一个“MZ”可执行文件。然后DOS系统就将PE文件当作正常的“MZ”文件开始执行。DOS系统会读取“e_cs”和“e_ip”这两个成员的值,以跳转到程序的入口地址,这个地址就是dos插妆的段里面的内容,打印一行改程序不能再dos上运行就退出程序。

DOS文件头里面唯一值得关心的是“ e_lfanew ”成员,这个成员表明了PE文件头(IMAGE_NT_HEADERS)在PE文件中的偏移,我们须要使用这个值来定位PE文件头。这个成员在DOS的“MZ”文件格式中它的值永远为0,所以当Windows开始执行一个后缀名为“.exe”的文件时,它会判断“ e_lfanew ”成员是否为0。如果为0,则该“.exe”文件是一个DOS“MZ”可执行文件,Windows会启动DOS子系统来执行它;如果不为0,那么它就是一个Windows的PE可执行文件,“ e_lfanew ”的值表示“IMAGE_NT_HEADERS”在文件中的偏移。

也就是说 :

dos运行windows的pe文件会打印一行不能再dos上运行就退出程序,而windows运行dos的wz文件会启动dos子系统执行

elf里面记录的是一个个的section,内存加载的时候会把这些相似的section合并成一个个segement,这些segement只有在需要加载到内存的文件才有用,所以只有 可执行文件或者共享库文件才会有可执行文件头记录这些segement表的信息

segement表也就是 可执行文件头记录的内容 和 ELF记录section表(段表)的内容差不多,segement里记录的两种不同类型的segement:是否会记录该段内容在可执行文件中的位置

只不过segement是加载到内存里面的段所以会有堆segement和栈segement,而且这种segement不是可执行文件的里面的内容,所以也不会记录他们在可执行文件中的偏移和可执行文件路径和名称,加载的时候只要申请内存即可(仅仅是第一次,后续有内容就需要进行换入换出到磁盘)

相比之下code和data segement是会记录的因为他们的内容是在可执行文件中,第一次发生页终端之后就会通过偏移量找到内容加载到内存里面并在页表中建立映射

Coff文件

什么是镜像文件, 就是这个文件是被映射到 进程的虚拟空间 运行就是镜像,注意是映射不是加载(好像也没有区别吧,区别可能是一个不能修改一个可以修改吗),一面镜子映射着你的脸,他并没有真正拥有你的脸(加载到内存作为私有的数据可以修改)只是进行了映射(只读)

镜像的反义词就是实体,不是实体文件的就是镜像文件!实体文件是真正加载到内存里面可以修改的,而不是镜像只是模仿不可以修改

原文:映像(Image):因为PE文件在装载时被直接映射到进程的虚拟空间中运行,它是进程的虚拟空间的映像。所以PE可执行文件很多时候被叫做映像文件(Image File)。

ELF静态链接

符号地址是什么

符号地址原来指定的是在段里面的偏移(我理解成偏移量就是 地址就行。具体位置就是段的起始地址+偏移量就可以找到这个符号值,段起始也是根据段表的偏移得到)

如果是这样的话,那这个符号如果是data段的话,data段里面只会记录变量的值,所以符号地址也就是说 这个符号的值是多少

静态链接大致原理

我想的其实就和 现在链接的过程差不多一样,也是吧很多个 文件里的同类型段进行拼接,然后存储这些文件的符号表为一个大的符号表有了这些符号和符号表就可以进行重定位分配了(进行拼接之后原来引用的偏移就要发生变化,如何进行偏移的重定位呢,这个就是第二个链接部分重定位的内容了);

链接中还有一个最重要的过程就是 分配虚拟地址空间,没想到在这里面就进行分配了地址空间,难道后面就不根据偏移来做了吗,直接用虚拟地址来代替符号的引用吗(可能是这样的,因为最终都是要用地址而不是用符号,先给不同段的符号分配好虚拟地址然后再重定位);除了虚拟地址还有加载地址(一般好虚拟地址空间一样)

事实证明确实是这样的,偏移只在编译的时候有用,链接的时候就会去确认符号的地址然后重定位了

链接器就会去查找由所有输入目标文件的符号表组成的全局符号表找到相应的符号后进行重定位

“符号未定义错误”是如何产生的

原来符号未定义错误 不是 通过比对重定位段里面的符号 是否在全局符号表中找到 为判断条件检测的

而是直接在扫描整个符号表,如果符号所处的段是未定义的就是在其他目标文件中,所以直接找这种符号是否可以在全局符号表中找到就行,不需要对比重定位,效率确实可以高一点

原文:“undefined”未定义类型,这种未定义的符号都是因为该目标文件中有关于它们的重定位项。所以在链接器扫描完所有的输入目标文件之后,所有这些未定义的符号都应该能够在全局符号表中找到,否则链接器就报符号未定义错误。

对于知道地址的在编译期间就通过偏移量 来访问,不知道的就用临时地址访问,等连接器收集到所有的目标文件 之后计算虚拟地址然后用虚拟地址重定位之前置为0的

原文:编译器把这两条指令的地址部分暂时用地址“0x00000000”和“0xFFFFFFFC”代替着(因为是外部库的符号,所以在编译程序的时候会先用固定的地址代替),把真正的地址计算工作留给了链接器。我们通过前面的空间与地址分配可以得知,链接器在完成地址和空间分配之后就已经可以确定所有符号的虚拟地址了,那么链接器就可以根据符号的地址对每个需要重定位的指令进行地位修正。

如何对多个输入文件进行链接?

其实就是对多个输入段进行操作,生成输出段,最终输出段成型之后就可以确定符号的虚拟地址,每个符号虚拟地址确定好后,就可以进行修改重定位表里面的符号的符号值

也就是他的地址去修正这样不管是文件内部的符号还是文件外部的符号,最终都会变成一个段。

这一个段里面的符号的偏移量确定自然地址是确定的。那么对于外部和内部符号就可以去用这个地址去访问这快数据

书中原文:静态链接中的第一个步骤,即目标文件在被链接成最终可执行文件时,输入目标文件中的各个段是如何被合并到输出文件中的,链接器如何为它们分配在输出文件中的空间和地址。一旦输入段的最终地址被确定,接下来就可以进行符号的解析与重定位,链接器会把各个输入目标文件中对于外部符号的引用进行解析,把每个段中须重定位的指令和数据进行“修补”,使它们都指向正确的位置。

ELF动态链接

静态链接的缺点

  1. 静态链接是程序私有的,相同的内容每个进程都存储一份就会浪费空间,只存储一份即可,这就是动态链接

原文:特别是多进程操作系统情况下,静态链接极大地浪费了内存空间,想象一下每个程序内部除了都保留着 printf() 函数、 scanf() 函数、 strlen() 等这样的公用库函数,还有数量相当可观的其他库函数及它们所需要的辅助数据结构。在现在的Linux系统中,一个普通程序会使用到的C语言静态库至少在1 MB以上,那么,如果我们的机器中运行着100个这样的程序,就要浪费近100 MB的内存;如果磁盘中有2 000个这样的程序,就要浪费近2 GB的磁盘空间,很多Linux的机器中,/usr/bin下就有数千个可执行文件

  1. 静态连接的另一个缺点:更新麻烦,如果一个库发生了变化需要整个程序重新编译和链接,如果大型程序里面只要是有一个小改动都药重新获取整个程序是非常痛苦的,相当于重新下载

原文:一旦程序中有任何模块更新,整个程序就要重新链接、发布给用户。比如一个程序有20个模块,每个模块1 MB,那么每次更新任何一个模块,用户就得重新获取这个20 MB的程序。如果程序都使用静态链接,那么通过网络来更新程序将会非常不便,因为一旦程序任何位置的一个小改动,都会导致整个程序重新下载。

解决办法

原文:简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态地链接在一起。简单地讲,就是不对那些组成程序的目标文件进行链接,等到*程序要运行时才进行链接。 *也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基本思想。

根本原因是链接其他外部文件的符号是在链接阶段进行的,如果能够推迟到运行的时候的话 其他文件可以随意更新 (前提需要保存和原来的动态库导出的符号保存兼容)而不需要重新连接一遍,因为是运行时链接的。

说白了一种就是绑定死在程序里面了,会占用文件大小;如果是运行时链接的话,系统只需要往内存里加载一份内容即可,这块空间进行运行时链接

这份独立的程序只需要在内存里面加载一次就好,然后动态链接的苦里面会记录自己符号的虚拟地址。然后程序里面记录动态链接符号是记录的什么呢?程序如何找到动态链接的符号地址?

使用动态链接的符号的地址会指向一个特殊的got表,动态链接符号都会存储在got表里面,动态链接器加载完成符号的时候就会去got里修正符号地址

关于共享库的错误认知:只能节省磁盘空间,如果要节省内存空间需要使用地址无关技术实现(针对代码段)

看到这里的时候我好像对于之前的理解有一些错误的认识了:

首先共享库节省的只是磁盘占用空间而且解决了三方库改动导致的重新连接问题,放到运行时是因为只需要占用一份磁盘空间且 由于独立出来了所以即使是微小的改动也无需对源程序重新编译链接。

默认情况下,当程序需要动态链接库符号地址的时候会把共享库加载到自己进程的内存地址里面(因为程序需要使用动态链接的符号地址,但是这个地址怎么知道呢,就是把库加载到各自进程的内存里面),也就是说每个进程还会进行加载这些内容到进程私有的虚拟内存里面,所以共享库加载到内存就是加载到程序的进程内存空间里面,系统不会保留这个共享库到内存里面,只是节省磁盘。

总结就是只能节省磁盘空间,每个进程还是需要 加载动态库到进程自己的内存里,因为每个进程使用的数据段里面存储的数据是不同的;程序里面要使用动态库里的代码段地址(跳转动态库的函数地址)但是因为 不同进程不能不能保证这个地址一定就是没有被占用的所以不能写死,所以代码段也需要都加载到内存,无法做到进程共享复用代码段。

但是对于加载到不同进程的代码段部分 除了程序跳转动态库·里函数的地址 部分不同以外,其他的指令数据都是一模一样的,只是因为程序使用的地址不同就需要浪费内存加载不划算,因此出现了地址无关技术,简单说就是程序里面使用动态库里的符号地址都会指向数据段里的got表运行的时候动态链接器 把动态库加载起来之后(这个时候已经知道了动态符号的地址)就会去修复got表这样代码段就做到了地址无关。

链接器如何识别是静态符号还是动态符号:静态符号未找到会报错,但是动态符号未找到不会报错。

动态链接的时候 不能直接连接共享库的目标文件(因为这个文件是在其他地方不能再这个文件里面存储因为会增大占用的磁盘空间),但是连接器在链接的时候需要知道这是个 动态链接的符号,不然整个目标文件里面找不到这个符号 就会链接报错:该符号未找到; 那么针对这些外部链接的符号找不到很正常所以不会报错,但是如何知道哪些是动态链接符号哪些是必须要进行修正的符号呢?

SO里面会存储完整的动态库符号信息:也就是导出符号表

就是通过编译共享库的时候可以指定编译器参数 打出共享库目标文件(.o)和 共享库链接信息(.so) ,这个so里面会记录共享库中完整的符号信息,这样连接器在查找符号的时候如果发现可以在so里面找到就会标识他是动态链接符号,不会在链接的时候就进行Í加载到内存运行的时候再进行重定位


链接器如何知道foobar的引用是一个静态符号还是一个动态符号?这实际上就是我们要用到Lib.so的原因。Lib.so中保存了完整的符号信息(因为运行时进行动态链接还须使用符号信息),把Lib.so也作为链接的输入文件之一,链接器在解析符号时就可以知道:foobar是一个定义在Lib.so的动态符号。

这样链接器就可以对foobar的引用做特殊的处理,使它成为一个对动态符号的引用。

这种动态符号会存储在单独的got表和plt表里面

动态链接的可执行文件里 不仅仅只有可执行文件,还有单独的文件so

所以查看这种可执行文件的segement分布时,就可以看到segement所处的文件和文件路径和文件inode节点号有很多种。

可以理解为把重复代码抽出来放到单独的文件,程序里面虽然不存储代码但是需要记录哪些内容是提出来的(其他文件不是本文件,其他文件就是共享库动态链接),如果要用就加载这个文件到进城的虚拟空间。

b7fd9000-b7fda000 r-xp 00000000 08:01 1343290    ./Lib.so 

b7fda000-b7fdb000 rwxp 00000000 08:01 1343290    ./Lib.so 

b7fdb000-b7fdd000 rwxp b7fdb000 00:00 0 

b7fdd000-b7ff7000 r-xp 00000000 08:01 1455332    /lib/ld-2.6.1.so 

b7ff7000-b7ff9000 rwxp 00019000 08:01 1455332    /lib/ld-2.6.1.so

可执行文件头需要记录动态链接器的segement

静态链接是吧这个过程放到了连接器里面进行,而 动态链接是运行时进行。

有动态链接的可执行文件他的程序文件头里面不仅有共享库的segement记录(连接的时候需要把so链接进来 标识哪些符号是动态链接符号这些地址会在运行时重定位,每个so的segement都会给分配虚拟地址区域和文件节点和文件路径)

还会有动态链接器的segement记录,程序加载的时候先运行动态链接去把共享库加载到内存然后重定位之后再运行主程序

地址无关技术:GOT表

访问共享库中的地址对于不同进城来说是不同的,所以代码段部分也都要加载到不同的进城地址内存吗?数据段部分加载到不同进城内存里是可以理解的,因为本身每个程序的数据就不同;

但是代码段都是相同的,仅仅因为地址不同就要再次加载到所有进城的内存里吗?有点太浪费内存了,如果地址的问题可以解决就好了。

也就是和地址无关的代码,但是程序最终又是需要访问动态库地址的,怎么做到和动态库地址无关的代码呢?

这里说的是程序要用动态库符号的地址(这个地址再目标文件里是不知道的,得等到加载到 每个进程里面就知道了),动态库本身是通过偏移来引用的所以动态库用自己的符号是知道自己地址的不需要做重定位。

重定位的只是使用动态库的程序,他们不知道动态库符号地址所以采用got来代替

我一直没想明白的是 动态库如果不知道自己符号的地址他自己怎么使用他的符号,其实根本就错了,他自己当然知道自己的符号再哪个地址了(通过偏移),但是你的程序不知道这个偏移是多少所以就出现了got

动态库使用固定的加载地址让代码段做到“地址无关”(PE格式采用)

静态共享库要求 模块必须加载到程序指定的虚拟内存中,这种方式的缺点就是

  1. 对于模块的提供者来说必须得提供指定的加载的虚拟地址,不同模块之间如果用的是同一个虚拟地址就会发生地址冲突的问题,陈旭使用的模块非常多交给程序员来管理是极度不安全的;
  2. 其次由于制定了虚拟地址的空间,所以以后升级只能让占用空间没有发生变化的升级,如果空间改动了就需要重新源程序链接,重新规划不同模块的虚拟内存区域

虽然这种方式没有应用在ELF中,但是却用在了PE文件中,PE会通过记录动态库指定的固定加载地址和实际的加载地址做个差值偏移的记录,后面这个动态库里面的地址都会加上这个偏移量 以此来实现地址无关公用代码段

缺点也是有的,对于权限问题由于是内核操作所以安全,但是如果空间发生变化确实需要重新连接。这种方式比ELF的gotplt表实现起来简单快捷。

GOT实现地址无关(ELF格式采用)

因为数据段是可以修改的,因此got存放在数据段中,那么自然got的偏移是知道的,然后*把这些外部链接的符号 统一放到got表里面(so记录的是共享库的导出符号表,而程序使用的是导入符号表并不是所有共享库的符号陈序都药使用,所以需要记录程序使用了哪些动态符号,so导出符号只是为了让连接器对于未定义的动态符号不报错;程序使用的导入符号会放在rel.got和rel.plt段中,后面会介绍) *,用的地方怎么知道在got表里的偏移呢?

got表生成的时候 去修改程序里使用动态链接的符号地址 重定位到got表里面对应符号的地址。

也就是说访问动态链接��号的地址就是访问got表里面记录这个符号的地址,而got里会记录动态库加载后符号的地址因此通过got可以间接访问运行时加载的符号地址。

运行的时候动态链接器会首先加载动态链接的共享库到内存里,此时虚拟地址和物理地址都确定了,接着就可以修改数据段里面的got了,got中匹配符号 记录符号真正存储的虚拟地址(数据段的重定位运行时不同进城有副本切可以随意修改)

类似stub占桩 ,放一个变量在内存里,运行的时候给这个变量设置动态加载库的地址,程序用stub的就是变相的在用地址stub存根很适合这个例子,数据段又是可以随意修改

got简单总结,如何实现代码段共享

程序访问stub的时候,编译的时候会设置为访问got中的符号地址,这块地址存储的数据只有运行的时候才会赋值,运行时加载号之后,地址上存了值,程序访问got中指定地址的符号存储的地址值就是动态加载库被加载到内存的虚拟地址。

Fpic 就是让编译器产生 got段的,因为 编译的代码里面会用到动态链接的符号,让这些符号先用数据段里面got表里面的地址(算是一个占坑),等到真正的动态库加载起来后再修改got表里面真正加载到内存的地址。

这样got表里面存的就是 真实的内存地址,也就实现了 共享库的代码指令部分 是真正共享的,只需要在内存一次即可

延迟加载技术:PLT

为什么出现延迟加载

动态链接比静态链接慢的两个主要原因:

  1. 动态链接的符号 访问是间接访问got表然后拿到真实的地址再去访问,这就需要两次内存访问。(一次简介访问got拿到真实地址,第二次访问拿到的真实地址)

2.动态链接的程序 每次运行的时候都会先运行动态链接器把所有的动态库加载起来之后再去修复got表里面的动态符号地址,再然后才会运行程序,如果动态库很多,这个过程会非常非常耗时。

根据28定律优化

原文:动态链接比静态链接慢的主要原因是动态链接下对于全局和静态的数据访问都要进行复杂的GOT定位,然后间接寻址;对于模块间的调用也要先定位GOT,然后再进行间接跳转,如此一来,程序的运行速度必定会减慢。另外一个减慢运行速度的原因是动态链接的链接工作在运行时完成,即程序开始执行时,动态链接器都要进行一次链接工作,正如我们上面提到的,动态链接器会寻找并装载所需要的共享对象,然后进行符号查找地址重定位等工作,这些工作势必减慢程序的启动速度

plt项内部的实现

首先会跳转到 一个函数中,该函数会找到存储函数符号的got项地址:

  • 如果got没有加载这个符号(这个符号没有记录地址,因为符号对应的模块还没有加载到内存)那么函数就会返回继续执行之后的代码,首先把该符号在got.plt中的下标索引和该符号所处的动态共享库 路径 压入栈中;
  • 接着跳转到dl_runtime-resovle函数去动态解析这个符号的地址(该函数内部会通过刚站里面压入的库路径 找到这个库并加载到内存中 接着对该符号在plt.got中指定下标 找到改符号的plt项 设置真实的地址),后面第二次调用就会直接在got中找到该符号地址就不会执行后面的函数了

如果got已经加载了这个符号那么 就会返回这个符号的地址 不会执行后面的程序而是跳转到 刚在got中获取到的函数真正地址执行。

bar@plt:



jmp *(bar@GOT)



push n



push moduleID



jump _dl_runtime_resolve

访问延迟绑定符号的实现

调用延迟绑定的符号调用到got.plt表里该符号在got.plt中的对应项,每个延迟绑定的符号在got.plt段中都有一个 相应的plt项存储,会存储dynamic段的地址(关于动态链接相关的参数和设置项,在这不考虑)和该符号所在的模块地址(因为如果在got里找不到存储的地址 就代表没有加载到内存中就会把这个符号所在的模块加载到内存中)还有动态链接解析程序的函数地址(通过该函数才会执行加载模块到内存的过程)。

具体来说访问一个延迟绑定的符号的过程是:

找到该符号在plt中的存储的信息,查找该符号在got标中能否找到对应的地址,如果能找到就跳转到got中存储的地址执行;如果找不到就会把plt项中该符号的信息压入栈中 并 调用动态链接解析器函数 去加载模块到内存并把加载后的符号地址 存入到got中,这样下一次就会从got中找到地址然后跳转运行。

简单总结

总结还是在家一层转换层处理延迟绑定:跳转到plt查找got中是否存储着符号地址----不存储代表没有加载到内存因此加载 模块到内存并设置got地址信息---访问got地址信息也就是跳转到了符号的真正地址,对于模块加载到内村的延迟绑定符号来说:跳转到plt,跳转到hot,got能找到在跳转到got中存储的地址。

简单链路

也就是plt----got---真实地址,对于没有延迟绑定运行就全部加载的符号来说是got---真正地址。加的一层plt就是延迟绑定对处理器如果没有地址就加载模块到内存在重定位符号地址设置到got中

原文:当我们调用某个外部模块的函数时,如果按照通常的做法应该是通过GOT中相应的项进行间接跳转。PLT为了实现延迟绑定,在这个过程中间又增加了一层间接跳转。调用函数并不直接通过GOT跳转,而是通过一个叫作PLT项的结构来进行跳转。每个外部函数在PLT中都有一个相应的项,比如bar()函数在PLT中的项的地址我们称之为bar@plt。

动态链接中的重定位表

程序里面会使用动态链接的符号和函数,这些都需要运行的时候才能知道,所以运行的时候加载完库需要把程序里面用的符号重定位到加载后的符号地址

虽然动态链接的可执行文件使用的是PIC方法,但这不能改变它需要重定位的本质

对于使用PIC技术的可执行文件或共享对象来说,虽然它们的代码段不需要重定位(因为地址无关,动态链接器加载完模块后会修正got里面记录的地址) ,但是数据段每个进程都会加载,所以地址也不同,因此需要对数据段进行重定位。

动态链接的文件中,也有类似的重定位表分别叫做“.rel.dyn”和“.rel.plt”,它们分别相当于 “.rel.text”和“.rel.data”。

“.rel.dyn”实际上是对数据引用的修正,它所修正的位置位于“.got”以及数据段;

而“.rel.plt”是对函数引用的修正,它所修正的位置位于“.got.plt”。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-07-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 使用objdump查看mach-o文件
    • objdump -h:查看段信息
      • objdump -s:查看段内容
      • 使用otool查看mach-o文件
      • ELF组成
          • elf文件头结构
            • elf文件类型:重定位/可执行文件/共享文件类型
            • EM:可运行的CPU平台
            • eshotoff:段表的偏移
          • Elf32_shdr:段表内记录的段信息
            • sh_type/sh_flag:段类型和段标志位
            • shlink,shinfo:段链接的相关信息
          • 重定位表段:Elf32_Rel
            • 重定位表符号地址修复案例
          • 字符串表段
            • 符号表:记录符号信息的数组
              • 使用命令查看的符号表的结构
              • 符号结构体:Elf32_Sym
          • windows-PE
            • Windows和PE的兼容
              • Coff文件
          • ELF静态链接
            • 符号地址是什么
              • 静态链接大致原理
                • “符号未定义错误”是如何产生的
                  • 如何对多个输入文件进行链接?
                  • ELF动态链接
                    • 静态链接的缺点
                      • 解决办法
                      • 关于共享库的错误认知:只能节省磁盘空间,如果要节省内存空间需要使用地址无关技术实现(针对代码段)
                    • 链接器如何识别是静态符号还是动态符号:静态符号未找到会报错,但是动态符号未找到不会报错。
                      • SO里面会存储完整的动态库符号信息:也就是导出符号表
                      • 动态链接的可执行文件里 不仅仅只有可执行文件,还有单独的文件so
                      • 可执行文件头需要记录动态链接器的segement
                    • 地址无关技术:GOT表
                      • 动态库使用固定的加载地址让代码段做到“地址无关”(PE格式采用)
                      • GOT实现地址无关(ELF格式采用)
                    • 延迟加载技术:PLT
                      • 为什么出现延迟加载
                      • plt项内部的实现
                      • 访问延迟绑定符号的实现
                    • 动态链接中的重定位表
                    相关产品与服务
                    轻量应用服务器
                    轻量应用服务器(TencentCloud Lighthouse)是新一代开箱即用、面向轻量应用场景的云服务器产品,助力中小企业和开发者便捷高效的在云端构建网站、Web应用、小程序/小游戏、游戏服、电商应用、云盘/图床和开发测试环境,相比普通云服务器更加简单易用且更贴近应用,以套餐形式整体售卖云资源并提供高带宽流量包,将热门软件打包实现一键构建应用,提供极简上云体验。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档