程序员的自我修养 您所在的位置:网站首页 rodx是什么格式 程序员的自我修养

程序员的自我修养

2023-03-10 04:18| 来源: 网络整理| 查看: 265

4.1 空间与地址分配相关知识

首先,我们需要有a.c和b.c这两个源文件,用gcc -c a.c b.c方式来将它们编译成目标文件,它们的内容分别如下:

a.c:

extern int shared; int main(){ int a = 100; swap(&a, &shared); }

b.c:

int shared = 1; void swap(int *a,int *b){ *a ^= *b ^= *a ^= *b; }

对于它们生成的目标文件a.o和b.o,考虑到第三章所提到的知识。我们观察可以发现在a.c文件中含有一个对外部全局变量shared和外部函数swap的引用。根据之前的知识,在没有进行链接的情况下,编译器会先将它们的地址置为0。使用objdump -d a.o可以看到:

a.o的反汇编结果

(其实我不懂汇编...只是书上是这么说的,我也只能大概去猜大概24和29附近应该说的就是此时main和swap暂时的地址)

4.1.1 按序分配地址给各个段

这个标题的意思其实指的是一种链接时分配地址给段的方式,按序分配的意思就是说,我们简单的把进行链接的目标文件的各个段排好直接放在一起链接成可执行文件。例如对于我们的a.o和b.o,假设它们都只包含.text和.data段,按照这种方式,链接后的段就是:

.text .data .text .data

很显然这看起来就不像一个好的方式。更让人糟心的是我们的段是有对齐的要求的(我猜是因为偏移量的原因,如果段的大小不一样偏移量可能不太好计算,但是是不是这个原因我不知道)。

4.1.2 相似段合并

这个的意思就是说,把相同性质的段合并到一起,比如a.o的.text段和b.o的.text段就可以合并到一起。这里书上提到了说.bss段虽然不占用空间,但是也需要合并。理由其实很简单,它里面记录了每个目标文件中各自包含的未初始化的全局变量和局部静态变量占用的字节数,合并当然是为了让程序在执行时正确的给这些未初始化的变量分配空间。

4.2 实际的链接过程

对于链接空间分配的过程,基本都采用上述方法中的第二种。更具体的说,链接器采用一种两步链接(Two-pass Linking)法来完成整个链接的过程。

4.2.1 空间与地址分配

首先,链接器扫描所有的输入目标文件。获取它们各个段的长度、属性和位置(第三章就有提到,这些信息保存在ELF文件头中)。并且将输入目标文件符号表中的所有符号定义和符号引用收集起来(使用readelf -s a.o命令就可以查看a.o这个目标文件中包含的符号,其中Ndx这个段就决定了该符号是什么类型,例如如果是对外部变量或函数的引用,那么这个参数的值就是UND,表示undefined),放到一个全局符号表中。并计算输出文件中各个段合并后的长度与位置,建立映射关系。

这里也有一个小坑,书上使用的链接命令是:ld a.o b.o -e main -o ab,但是在运行这个命令的过程中,链接器报出了一个错误:

这个错误的原因是(直接引用他人回答,感谢作者的贡献):

链接后,我们可以使用objdump -h ab来查看可执行文件ab,其中我们可以关注一个名叫VMA(Virtual Memory Address 虚拟内存地址)的列,这与我们第一章提到的内存采用隔离(就是分配虚拟内存)的方式有关。链接之前如果用这个命令查看目标文件,所有段的VMA这一列全都为0,毕竟没必要进行分配。链接后会根据各个段的Size大小分配合适的空间,VMA此时代表的就是各个段的起始位置。

第三章的笔记已经提到过了,符号表中记录了关于符号的一些信息。其中的Value字段当符号是一个数据类型或者函数时,符号的位置相对于它所在段的偏移位置。

那么在链接好的文件中,这个部分表示的就是符号所对应的虚拟地址了。

4.3 符号解析与重定位

这部分内容结合了汇编代码,不懂汇编的我其实有点看不懂。但是借助Chat-GPT,我尝试写写个人的理解。(可能错的会比较多,我会尽量将我猜测的部分加粗标注出来

4.3.1 哪些地方需要进行重定位?

完成了空间和地址分配以后,就进入了符号解析和重定位的步骤。首先我们看看objdump -d a.o的反汇编结果:

a.o: 文件格式 elf64-x86-64 Disassembly of section .text: 0000000000000000 : 0: f3 0f 1e fa endbr64 4: 55 push %rbp 5: 48 89 e5 mov %rsp,%rbp 8: 48 83 ec 10 sub $0x10,%rsp c: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp) 13: 48 8d 45 fc lea -0x4(%rbp),%rax 17: 48 8d 15 00 00 00 00 lea 0x0(%rip),%rdx # 1e 1e: 48 89 d6 mov %rdx,%rsi 21: 48 89 c7 mov %rax,%rdi 24: b8 00 00 00 00 mov $0x0,%eax 29: e8 00 00 00 00 call 2e 2e: b8 00 00 00 00 mov $0x0,%eax 33: c9 leave 34: c3 ret

因为不懂汇编,所以我尝试让Chat-GPT解释了一下这段汇编代码:

这是一段x86-64架构的汇编代码,可能是一个C程序编译后生成的汇编代码。下面是对每条指令的简要解释: 0:f3 0f 1e fa:这是一个特殊的指令,用于将程序控制权从先前执行的代码传递到新的指令序列。这条指令是一种"endbr64"指令,用于指示CPU当前在使用“控制流执行追踪(Control Flow Enforcement Technology,CET)”特性。 4:55:将当前函数的帧指针%rbp压入栈中。 5:48 89 e5:将栈指针%rsp复制到帧指针%rbp中,用于保存当前函数的栈帧,并为后续操作准备好堆栈空间。 8:48 83 ec 10:为栈帧分配16字节的空间,用于保存临时变量或者函数调用时需要保存的寄存器。 c:c7 45 fc 64 00 00 00:将值0x64(十进制下100)保存到%rbp-4的位置,即在本函数中分配的临时变量的栈空间位置。 13:48 8d 45 fc:将%rbp-4的位置的地址保存到%rax寄存器中。 17:48 8d 15 00 00 00 00:将下一条指令的地址保存到%rdx寄存器中,作为将要调用的函数的地址。 1e:48 89 d6:将%rdx寄存器的值(即将要调用的函数的地址)复制到%rsi寄存器中。 21:48 89 c7:将%rax寄存器的值(临时变量的地址)复制到%rdi寄存器中。 24:b8 00 00 00 00:将立即数0x0(十进制下为0)复制到%eax寄存器中。 29:e8 00 00 00 00:调用%rsi寄存器所指向的函数,即通过%rax寄存器和%rdi寄存器提供的临时变量指针作为参数传递给函数。 2e:b8 00 00 00 00:将立即数0x0(十进制下为0)复制到%eax寄存器中。 33:c9:恢复栈帧指针。 34:c3:函数返回。

值得注意的当然就是c,13,17,29这几行。

第c行将我们的局部变量a 的值保存在了%rbp-4的位置。

(%rbp根据我个人的理解,它似乎指向了一个用来保存局部变量的栈的底部的指针。这么形容似乎不是非常准确,但是我觉得可以理解成当我们需要分配若干个字节大小的空间用来保存一个变量时,对应的指令就是%rbp-已保存的内容占据的字节数-字节大小。与此同时经过我的试验,当我们拥有多个变量时,例如在我们的整个函数中包含一个short类型和一个int类型。这条指令会根据给这两个变量赋值的顺序,我赋值的顺序是先int后short,逐个插入到%rbp-4和%rbp-6的位置。因为int类型4个字节,short类型2个字节)

第13行将%rbp-4位置的地址保存到%rax寄存器中去(lea这条汇编命令全称load effective address,取有效地址的意思)。 前面的48 8d 45 fc是一个操作码(Operation code) ,其含义为计算出临时变量的地址,并将其保存到%rax寄存器中。具体来说,48表示指令前缀,8d表示指令类型是"lea",45表示内存地址的偏移量是-4(即%rbp-4),%rbp是基地址寄存器,fc表示该指令操作数的偏移量是-4(%rbp),%rax是目标寄存器,用于存储临时变量的地址。

第17行这里说的应该就是swap函数的地址,由于我们的swap函数是外部目标文件定义的,所以在未进行链接之前地址应该为0。前面的48 8d 15是一个操作码(Operation code) ,其含义为获取代码段中下一条指令的地址,并将其保存到%rdx寄存器中。具体来说,48表示指令前缀,8d表示指令类型是"lea",15表示操作数是一个32位的立即数(即下一条指令的地址),%rdx是目标寄存器,用于存储下一条指令的地址。

第29行是对swap这个函数的调用,这里的操作码e8和书上的一致。这是一条近址相对位移调用指令(Call near,relative,displacement relative to next instruction)。 后面四个字节表示被调用函数的相对于调用指令的下一条指令的偏移量,在这里指的就是函数swap相对于2e位置的mov指令的偏移量。还是由于swap 函数未被定义,所以暂时这里的偏移量设置为0 。(为什么要搞这个看起来有点复杂的东西?我的猜测是为了在调用函数后,找到汇编程序中下一条指令的位置,这样的话就能让被调用的函数正确的把返回值送去应该去的地方)

总结以上,对于外部引用的变量和函数,我们都需要进行重定位的操作,以便它们能够找到自己正确的地址。

4.3.2 如何进行重定位?

记得我们第三章的笔记中,提到过一个重定位表(Relocation Table)的概念。其形式是.rel.TableName的形式,例如,如果在.data表中有需要重定位的地方,那么它的重定位表就是.rel.data。

使用objdump -r a.o可以查看a.o中需要重定位的符号,在这里我们需要进行重定位的就是swap函数和变量shared。

每个需要重定位的地方叫做重定位入口(Relocation Entry),重定位入口的偏移(Offset)表示该入口在要被重定位的段中的位置。

观察前面的RELOCATION RECORDS FOR [.text],它表明这是一个.text段中的重定位。这里的shared的偏移1a指的就是相对于.text的起始位置的偏移。.text是程序的代码段, 所以我们可以通过观察上面的反汇编代码,找到1a位置来观察结果。

17: 48 8d 15 00 00 00 00 lea 0x0(%rip),%rdx # 1e

可以看出,从1a开始的四个字节都是00,这里保存的就是我们shared在未被重定位之前的地址。

除此之外,对于可执行文件来说,这个值就是该重定位入口所要修正的位置的第一个字节的虚拟地址。由于这里是静态链接,链接完之后的可执行文件既然能成功链接,那就没有需要重定位的符号了(我们暂不考虑弱引用的问题)。

在重定位的过程中,每个重定位的入口都是对一个符号的引用。

4.3.3 指令修正方式

不同的处理器指令对指令的格式和方法都不相同。我们这里仅考虑绝对寻址与上面提到的相对寻址。这两种重定位方式指令修正方式每个被修正的位置长度都是32位,4字节

这里引用书上的表:

宏定义值重定位修正方法R_386_321绝对寻址方法 S+AR_386_PC322相对寻址修正 S+A-P

A = 保存在被修正位置的值

P = 被修正的位置(相对于段开始的偏移量或虚拟地址),该值可通过计算r_offset得到。

S = 符号的实际地址,即由r_info的高24位指定的符号实际地址。

绝对寻址修正和相对寻址修正的区别就是绝对地址修正后的地址是符号的实际地址,而相对寻址修正后的地址为符号距离被修正位置的地址差

to be continue..



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有