重学计算机(四、程序是怎么链接的)

本文详细介绍了程序链接的四个关键步骤:符号解析、相似段合并、空间分配和符号地址确定。在符号解析阶段,链接器查找并匹配全局符号;在合并相似段时,将不同.o文件的段整合;空间分配确定每个段的虚拟地址;符号地址确定则涉及重定位,修正代码中符号引用的地址。文章还探讨了C++中的重复代码消除,并给出了C++链接的相关问题。
摘要由CSDN通过智能技术生成

前面做了那么多铺垫,这次终于来到了程序是怎么链接的,应该看到前面几节的应该都可以猜测的都,链接是怎么链接的,其实程序链接也没那么难。接下来我们来分析一波。

本来觉得程序链接是比较简单,当我去准备的时候,才发现有一些细节不是很明白,所以才去看了《深入理解计算机系统》这本书,总算把我的疑惑给解决了,疑惑解决了,接下来就按照自己思路写出来。

4.1 程序是怎么链接的

经过之前的分析,我们明白了,程序是先进行编译,然后才进行链接的,由于之前的例子都是一个.c文件,不太好体现链接,所以这一节,我们就再加一个.c文件,然后跨文件之间的调用,这样更好分析问题。

#include <stdio.h>

int f_a = 0;
int f_b = 84;

int func2(int i)
{
    static int s_a = 0;
    static int s_b = 84;

    printf("i = %d %d %d\n", i, s_a, s_b);
    return 0;
}
#include <stdio.h>

int g_a = 0;
int g_b = 84;

int func1(int i)
{
    printf("i = %d\n", i);
    return 0;
}

int main(int argc, char **argv)
{
    static int s_a = 0;
    static int s_b = 84;

    int a = 1;
    int b;
    func1(s_a+s_b+a+b);
    func2(s_a+s_b+a+b);
    printf("hello world %d %d %d\n", g_a, a, b);

    return 0;
}

贴了两个程序,又骗了几百字,如果是写小说,就赚了,哈哈哈。

代码写好了,肯定是编译了,编译就不说了,编译可以前面几节,还有可重定位文件也可以看前面几节,这一节我们专门讲链接的。废话不多说,我们来进入正题。

4.1.1 符号解析

符号解析是干啥的?说实话几天前,我也给这个东西整懵圈,所以一直没有写的原因。

不懂怎么办呢?没办法,找资料,看视频,这里推荐一下一个b站的视频,可以去看看,还真不错:

【精华】程序员的自我修养视频教程

经过几天的学习,终于搞懂这个符号解析是啥了,符号解析我的理解是:链接器查找整个程序中的符号引用(使用符号的地方,比如使用全局变量,调用其他文件函数),然后通过这个符号引用去找到与他对应的符号定义(代码中的定义的地方)。

这里就会有人问了:我们在同一个文件中定义两个同名的全局变量,是编译器报的错。

没错,编译器会单独对一个.c的文件进行语法检查,两个同名的全局变量肯定会有问题了,这个编译器是能发现的,编译器也会对静态局部变量一个本地链接的符号,编译器不能做的是那些在本文件中有引用,却没有定义的,这些部分编译器做不了,就只能交给链接器了,上一节我们也看到了编译后的.o文件的符号表,这里我们再来复习一下:

root@ubuntu:~/c_test/04# readelf -s fun2.o 

Symbol table '.symtab' contains 15 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS fun2.c
     6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 s_b.2289
     7: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 s_a.2288
    11: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 f_a
    12: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 f_b
    13: 0000000000000000    50 FUNC    GLOBAL DEFAULT    1 func2
    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

这就是编译器给链接器留下的遗产,LOCAL表示着本文件作用域,链接器会忽略,链接器关心的是这种GLOBAL,链接器就会试图通过这些符号引用找到这些符号的定义。

当链接器找不到这些符号的时候呢?链接器就会报错,这个错,我们经常见,下面再来重温一下:

root@ubuntu:~/c_test/04# gcc hello_world.c 
/tmp/ccsjP1Lu.o: In function `main':
hello_world.c:(.text+0x7b): undefined reference to `func2'
collect2: error: ld returned 1 exit status
root@ubuntu:~/c_test/04# 

这个问题一般都是缺少了库文件,或者是缺少了包含这个函数的目标文件,需要修改链接的参数,这个后面会介绍。

4.1.2 相似段合并

经过上面的符号解析完成,链接器接下来要做的就是把相似段合并。

我们在分析可重定位文件的时候,就看到每个可重定位文件都是分好几个段的,我们之前也稍微浏览过可执行文件,里面确实也是很多段,这个想象就能猜测到,我们多个可重定位文件的相似段是不是合并了,其实真的是这样的。

我们把上面的两个例子反汇编回来看看:

root@ubuntu:~/c_test/04# objdump -h fun2.o 

fun2.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000032  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  00000074  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000008  0000000000000000  0000000000000000  0000007c  2**2
                  ALLOC
  3 .rodata       0000000e  0000000000000000  0000000000000000  0000007c  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
root@ubuntu:~/c_test/04# objdump -h hello_world.o 

hello_world.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         000000a3  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  000000e4  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000008  0000000000000000  0000000000000000  000000ec  2**2
                  ALLOC
  3 .rodata       0000001e  0000000000000000  0000000000000000  000000ec  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
root@ubuntu:~/c_test/04# objdump -h hello_world 

hello_world:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
 13 .text         00000242  0000000000400430  0000000000400430  00000430  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 15 .rodata       00000030  0000000000400680  0000000000400680  00000680  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 24 .data         00000020  0000000000601028  0000000000601028  00001028  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 25 .bss          00000018  0000000000601048  0000000000601048  00001048  2**2
                  ALLOC

最后的可执行文件的各个段都是比他们两个段的和大,这就是所谓的对齐。

大家有空可以用objdump -s来查看各自的二进制,就会发现最后的可执行文件的确包含了上面的二个可重定位文件。

这里我就不用这个命令看了。

4.1.3 空间分配

经过上面各个段的合并后,链接器终于知道各个段的大小了,知道了各个段的大小之后,那干啥呢?

那就分糖果啊!!

当然程序中并没有糖果,程序中只有内存,所以确定了大小之后,也确定了每个段的空间分配,空间分配其中有包括可执行文件的位置和偏移,这个好处不大,所以可以不管,另外的就是虚拟地址的分配,这个比较重要,在程序运行的时候,就需要装载这些东西。

是不是听这很懵逼,懵逼的话就上图:

fun2.c 未链接之前的段信息:

root@ubuntu:~/c_test/04# objdump -h fun2.o 

fun2.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000032  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  00000074  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000008  0000000000000000  0000000000000000  0000007c  2**2
                  ALLOC
  3 .rodata       0000000e  0000000000000000  0000000000000000  0000007c  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      00000036  0000000000000000  0000000000000000  0000008a  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000c0  2**0
                  CONTENTS, READONLY
  6 .eh_frame     00000038  0000000000000000  0000000000000000  000000c0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

这是链接后的hello_world的段信息:

root@ubuntu:~/c_test/04# objdump -h hello_world

hello_world:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .interp       0000001c  0000000000400238  0000000000400238  00000238  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 13 .text         00000242  0000000000400430  0000000000400430  00000430  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 14 .fini         00000009  0000000000400674  0000000000400674  00000674  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 15 .rodata       00000030  0000000000400680  0000000000400680  00000680  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 16 .eh_frame_hdr 00000044  00000000004006b0  00000000004006b0  000006b0  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 17 .eh_frame     00000134  00000000004006f8  00000000004006f8  000006f8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 24 .data         00000020  0000000000601028  0000000000601028  00001028  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 25 .bss          00000018  0000000000601048  0000000000601048  00001048  2**2
                  ALLOC
 26 .comment      00000035  0000000000000000  0000000000000000  00001048  2**0
                  CONTENTS, READONLY

这里VMA表示虚拟地址,LMA表示加载地址,正常情况下,这两个值是一样的,除非那些嵌入式系统,可能不一样,这里我们分析的是ubuntu系统,两个值肯定是一样的,所以我们关注VMA即可。size是这个段的大小,file off是在可执行文件中的偏移,这个我们忽略掉。

这两个信息,差别最大的就是VMA,链接之前是没有分配虚拟地址的,链接之后才分配的。

需要看这个可执行文件的布局图,可以看第3篇,有简单的介绍了可执行文件。

可执行文件这么多个段,等到以后慢慢介绍了,路途遥远啊。

有一些眼尖的同学就看到了,.text 的虚拟地址是从0x0000000000400430开始的,没有从0开始,这个也是后面才讲,好奇的同学,收齐好奇心,我们继续讲链接。

这里提供一下重点信息,就是前一节,我们查看ELF头信息中,是不是有一个入口地址,这个地址其实就是.text的入口地址:

root@ubuntu:~/c_test/04# readelf -h hello_world
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400430
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6976 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 28

是不是感觉知识是有联系的,明白的感觉最舒服了。

4.1.4 符号地址确定

既然每个段的地址都确认了,那在段中的符号地址也是可以确认了。

4.1.4.1 .text符号地址确定

我们这次用hello_world.o来举例,我们反汇编得到的hello_world.o:

root@ubuntu:~/c_test/04# objdump -d hello_world.o 

hello_world.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <func1>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp


0000000000000026 <main>:
  26:	55                   	push   %rbp
  27:	48 89 e5             	mov    %rsp,%rbp

通过上一节,我们知道了.text段的起始位置为:0x0000000000400430,那func1函数的地址就是0x0000000000400430+X(x是偏移量)。

然后很多同学都自信的说,func1在hello_world.o中偏移量为0,所以应该是0x0000000000400430+0;

其实并不是这样的,我们似乎忘记了前面的相似段合并,我们还有一个func2.o呢,所以我们应该加上func2.o这个大小,这里就有人问了,为啥func2.o会在hello_world.o之前,这也是必然的,hello_world.o里面有main函数,肯定要在前面都准备好了,才会调用main函数。

反汇编可以得到func2.o的.text段的大小为0x00000032,所以我们func1函数的偏移量就等于:0x0000000000400430+0x32=0x0000000000400462 ?

其实正在的虚拟地址并不是这个,因为链接器又在偷偷的链接了一大堆东西进来,我们反汇编hello_world查看一下:

Disassembly of section .text:

0000000000400430 <_start>:
  400430:	31 ed                	xor    %ebp,%ebp
 ...
  40045a:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)

0000000000400460 <deregister_tm_clones>:
  400460:	b8 4f 10 60 00       	mov    $0x60104f,%eax
 ...
  40049d:	00 00 00 

00000000004004a0 <register_tm_clones>:
  4004a0:	be 48 10 60 00       	mov    $0x601048,%esi
 ...
  4004da:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)

00000000004004e0 <__do_global_dtors_aux>:
  4004e0:	80 3d 61 0b 20 00 00 	cmpb   $0x0,0x200b61(%rip)        # 601048 <__TMC_END__>
  ...
  4004fc:	0f 1f 40 00          	nopl   0x0(%rax)

0000000000400500 <frame_dummy>:
  400500:	bf 20 0e 60 00       	mov    $0x600e20,%edi
  ...
  400521:	e9 7a ff ff ff       	jmpq   4004a0 <register_tm_clones>

0000000000400526 <func2>:
  400526:	55                   	push   %rbp
  ...
  400557:	c3                   	retq   

0000000000400558 <func1>:
  400558:	55                   	push   %rbp
  ...
  40057d:	c3                   	retq   

000000000040057e <main>:
  40057e:	55                   	push   %rbp
 ...
  4005fb:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

0000000000400600 <__libc_csu_init>:
  400600:	41 57                	push   %r15
 ...
  40066d:	00 00 00 

0000000000400670 <__libc_csu_fini>:
  400670:	f3 c3                	repz retq 

通过反汇编发现,前面竟然还有这么多代码,链接器还真是偷偷摸摸之王了,其实编译器做的工作一点也不少,其中头部这些函数,我们以后在讲,这部门代码只要就是程序刚开始执行的代码,并负责调用mian函数,这里我们先忽略。

我们可以从func2开始看:0x0000000000400526,func1的虚拟地址就是通过:0x0000000000400526+0x32=0x0000000000400558。

赶紧回去翻答案,发现是对的,从这一步也证明了相似段的合并。在相似段的时候我是懒的反汇编查看。哈哈。

4.1.4.2 .data符号地址确定

上面是描述了.text段中的函数符号的地址确定,解下来我们看看.data段的符号确定。

.data段不跟.text段是可以看到函数的,.data段我们前面已经讲过,只能反汇编查看存储的值:

# func2.o中的data段
Contents of section .data:
 0000 54000000 54000000                    T...T...
 #  hello_world.o中的data段
 Contents of section .data:
 0000 54000000 54000000                    T...T...
 # 虽然这两个一样,但是代表的变量不一样,刚刚还在想编译器和链接器应该是按照某个规定存储的值和符号,才能正常读取
 # hello_world 中的data段
 Contents of section .data:
 601028 00000000 00000000 00000000 00000000  ................
 601038 54000000 54000000 54000000 54000000  T...T...T...T...

从上面可以看到我们.data段开始虚拟地址0x0000000000601028。

hello_world中的.data段就是两个可重定位文件合并起来的,为啥前面补了这么多个0,这个我也有点疑惑,明面后面的对齐表示着是2**3=8字节对齐,明显16字节也是8字节对齐,这个留着以后吧,或者有谁知道的,评论区告诉我,谢谢。

不纠结对齐问题,我们看到从地址0x601038开始,就是我们两个可重定位文件的合并,所以这4个变量的地址就分别是:0x601038,0x60103C,0x601040,0x601044。

不过按编译器的规定,应该也是按顺序存储的,这里还强调一点,就是编译器会静态局部变量的符号定义是编译器来指定的,我们可以看看两个的静态局部变量:

# func2.o的符号表
YMBOL TABLE:
0000000000000004 l     O .data	0000000000000004 s_b.2289
0000000000000004 l     O .bss	0000000000000004 s_a.2288
0000000000000000 g     O .bss	0000000000000004 f_a
0000000000000000 g     O .data	0000000000000004 f_b
# hello_world.o的符号表
SYMBOL TABLE:
0000000000000004 l     O .bss	0000000000000004 s_a.2292
0000000000000004 l     O .data	0000000000000004 s_b.2294
0000000000000000 g     O .bss	0000000000000004 g_a
0000000000000000 g     O .data	0000000000000004 g_b

所以按初始化的地址,f_b会先给编译器解析,所以f_b的地址是:0x601038

接着是:s_b.2289:0x60103C

g_b:0x601040

s_b.2294:0x601044

这里是不是还有其他同学疑惑,不是还有几个变量呢?

其实剩下的几个变量是bss段的了,分析的方法也是跟.data段是一样的,这里就不分析了。

感觉反汇编出来看看我们得出的结果对不对:

000000000060103c l     O .data	0000000000000004              s_b.2289
0000000000601050 l     O .bss	0000000000000004              s_a.2288
0000000000601058 l     O .bss	0000000000000004              s_a.2292
0000000000601044 l     O .data	0000000000000004              s_b.2294
0000000000601038 g     O .data	0000000000000004              f_b
0000000000601040 g     O .data	0000000000000004              g_b
000000000060104c g     O .bss	0000000000000004              f_a
0000000000601054 g     O .bss	0000000000000004              g_a

经过对比,完全一样,.data的符号地址就是这样确认的。

4.1.4.3 重温符号表

这里还有一个疑问:

我们之前会把符号名,存储到.strtab,其中包含了函数名,变量名,然后这些符号是怎么跟其他段的位置映射起来的,这个我们就要提一下符号表了,符号表就是描述这个信息的,可能上一节也没用到符号表,所以也没仔细看。

在这里插入图片描述

st_name就是表示,这个符号在字符串表(.strtab)中的下标,st_value在可重定位文件中,是段的偏移的,在可执行文件中是虚拟地址,st_size是符号大小,st_other表示着这个符号的信息,比如全局,本地,st_shndx是这个符号所在的段。

现在是不是就全都明白了,原来是这样的。

4.1.5 重定位

在前面完成了符号地址确定之后,是不是就表示着链接的完成。

其实并不是,我们在前面只是把符号的地址给确定了,但是我们代码中引用的符号地址还是原来的,所以这一步就是把代码中的符号引用给修复了。

要修复这一步,链接器依赖于重定位位表,重定位表也是编译器编译的时候生成的,把需要重定位的信息给出来,让链接器精准找到需要重定位的部分。

4.1.5.1 重定位表

老规矩,先来看看重定位表是怎么样的:

root@ubuntu:~/c_test/04# readelf -r fun2.o

Relocation section '.rela.text' at offset 0x290 contains 4 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000d  000300000002 R_X86_64_PC32     0000000000000000 .data + 0
000000000013  000400000002 R_X86_64_PC32     0000000000000000 .bss + 0
00000000001d  00050000000a R_X86_64_32       0000000000000000 .rodata + 0
000000000027  000e00000002 R_X86_64_PC32     0000000000000000 printf - 4

root@ubuntu:~/c_test/04# readelf -r hello_world.o 

Relocation section '.rela.text' at offset 0x3a0 contains 12 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000011  00050000000a R_X86_64_32       0000000000000000 .rodata + 0
00000000001b  000e00000002 R_X86_64_PC32     0000000000000000 printf - 4
00000000003e  000400000002 R_X86_64_PC32     0000000000000000 .bss + 0
000000000044  000300000002 R_X86_64_PC32     0000000000000000 .data + 0
000000000057  000d00000002 R_X86_64_PC32     0000000000000000 func1 - 4
00000000005d  000400000002 R_X86_64_PC32     0000000000000000 .bss + 0
000000000063  000300000002 R_X86_64_PC32     0000000000000000 .data + 0
00000000007b  001000000002 R_X86_64_PC32     0000000000000000 func2 - 4
000000000081  001100000002 R_X86_64_PC32     0000000000000000 f_a - 8
00000000008b  000b00000002 R_X86_64_PC32     0000000000000000 g_a - 4
000000000098  00050000000a R_X86_64_32       0000000000000000 .rodata + 8
0000000000a2  000e00000002 R_X86_64_PC32     0000000000000000 printf - 4

我们来看一下是怎么描述这个重定位表的:

typedef struct {
	long offset;	// 需要被修改的引用的节偏移
	long type : 32;		// 重定位类型
		symbol : 32;	// 标识被修改引用应该指向的符号
	long addend;		// 使用它对被修改引用的值做偏移调整
}Elf64_Rela;

其中的type是比较简单的:

type类型也是比较多的,不过我们只关心两种。

R_X86_64_PC32:重定位一个使用32位PC相对地址的引用。

R_X86_64_32:重定位一个使用32位绝对地址的引用。

4.1.5.2 重定位符号引用

我们先来看看未重定位之前,可重定位文件是怎么保存的这个符号引用。

0000000000000026 <main>:
  26:	55                   	push   %rbp
  27:	48 89 e5             	mov    %rsp,%rbp
  2a:	48 83 ec 20          	sub    $0x20,%rsp
  2e:	89 7d ec             	mov    %edi,-0x14(%rbp)
  31:	48 89 75 e0          	mov    %rsi,-0x20(%rbp)
  35:	c7 45 f8 01 00 00 00 	movl   $0x1,-0x8(%rbp)
  3c:	8b 15 00 00 00 00    	mov    0x0(%rip),%edx        # 42 <main+0x1c>    s_a.2293 = %edi
  42:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax        # 48 <main+0x22>		s_b.2294 = %edx
  48:	01 c2                	add    %eax,%edx
  4a:	8b 45 f8             	mov    -0x8(%rbp),%eax
  4d:	01 c2                	add    %eax,%edx
  4f:	8b 45 fc             	mov    -0x4(%rbp),%eax
  52:	01 d0                	add    %edx,%eax
  54:	89 c7                	mov    %eax,%edi
  56:	e8 00 00 00 00       	callq  5b <main+0x35>			# func1
  5b:	8b 15 00 00 00 00    	mov    0x0(%rip),%edx        # 61 <main+0x3b>	s_a.2293
  61:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax        # 67 <main+0x41>	s_b.2294
  67:	01 c2                	add    %eax,%edx
  69:	8b 45 f8             	mov    -0x8(%rbp),%eax
  6c:	01 c2                	add    %eax,%edx
  6e:	8b 45 fc             	mov    -0x4(%rbp),%eax
  71:	01 d0                	add    %edx,%eax
  73:	89 c7                	mov    %eax,%edi
  75:	b8 00 00 00 00       	mov    $0x0,%eax
  7a:	e8 00 00 00 00       	callq  7f <main+0x59>			# func2
  7f:	c7 05 00 00 00 00 64 	movl   $0x64,0x0(%rip)        # 89 <main+0x63>  f_a
  86:	00 00 00 
  89:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax        # 8f <main+0x69>  g_a
  8f:	8b 4d fc             	mov    -0x4(%rbp),%ecx
  92:	8b 55 f8             	mov    -0x8(%rbp),%edx
  95:	89 c6                	mov    %eax,%esi
  97:	bf 00 00 00 00       	mov    $0x0,%edi
  9c:	b8 00 00 00 00       	mov    $0x0,%eax
  a1:	e8 00 00 00 00       	callq  a6 <main+0x80>		# printf
  a6:	b8 00 00 00 00       	mov    $0x0,%eax
  ab:	c9                   	leaveq 
  ac:	c3                   	retq

这个就是main函数未链接之前的反汇编代码,其中在后面加了注释的就是需要重定位的。

  1. 重定位PC相对引用

    我们先分析这个:

    56: e8 00 00 00 00 callq 5b <main+0x35> # func1

    000000000057 000d00000002 R_X86_64_PC32 0000000000000000 func1 - 4

    func1在代码中的偏移是0x56+1,为啥加1呢,因为e8是callq的操作码。

    所以我们得到的

    r.offset = 0x57;
    r.type = R_X86_64_PC32;
    r.symbol = func1;
    r.addend = -4;
    

    经过上面的处理,我们已经知道了.text的虚拟地址:0x0000000000400430

    还有func1的虚拟地址:0x0000000000400558

    可以计算到这个指令运行时的地址:refaddr = 0x0000000000400430 + r.offset + 修正的地址 = 0x0000000000400430 + 0x57 + 0x128 = 0x00000000004005af(修正地址是.text段到hello_world.o的.text段的起始位置,不要忘记我们是需要合并的)

    然后在更新该应用:*refptr = 0x0000000000400558 -4 - 0x00000000004005af = 0xFFFFFFFFFFFFFFA5。因为这是32位的PC偏移,所以最后的值为:0xFFFFFFA5。

    可以把这个值填入到原来的位置了,当然我们可以直接查看答案:

    000000000040057e <main>:
      40057e:	55                   	push   %rbp
      40057f:	48 89 e5             	mov    %rsp,%rbp
      400582:	48 83 ec 20          	sub    $0x20,%rsp
      400586:	89 7d ec             	mov    %edi,-0x14(%rbp)
      400589:	48 89 75 e0          	mov    %rsi,-0x20(%rbp)
      40058d:	c7 45 f8 01 00 00 00 	movl   $0x1,-0x8(%rbp)
      400594:	8b 15 be 0a 20 00    	mov    0x200abe(%rip),%edx        # 601058 <s_a.2293>
      40059a:	8b 05 a4 0a 20 00    	mov    0x200aa4(%rip),%eax        # 601044 <s_b.2294>
      4005a0:	01 c2                	add    %eax,%edx
      4005a2:	8b 45 f8             	mov    -0x8(%rbp),%eax
      4005a5:	01 c2                	add    %eax,%edx
      4005a7:	8b 45 fc             	mov    -0x4(%rbp),%eax
      4005aa:	01 d0                	add    %edx,%eax
      4005ac:	89 c7                	mov    %eax,%edi
      4005ae:	e8 a5 ff ff ff       	callq  400558 <func1>    #  答案在这里
      4005b3:	8b 15 9f 0a 20 00    	mov    0x200a9f(%rip),%edx        # 601058 <s_a.2293>
      4005b9:	8b 05 85 0a 20 00    	mov    0x200a85(%rip),%eax        # 601044 <s_b.2294>
    

    是不是就是这个答案,是不是有人就疑惑了,这么会是这么大的数,0xffffffa5表示的是-91,因为我们链接的时候,main函数在后面,func1在前面,所以pc指针是肯定需要往前移,往前移就是-91。

    PC指针是指向下一个指令的地址,当程序运行到0x4005ae的时候,PC的值为0x4005b3

    0x4005b3 + 0xffffffa5 = 0x400558。刚好就是这个地址,所以Addend的偏移量好像就是做这个PC偏移的。

    突然发现数据的偏移大部分也是相对偏移,我们接着来分析一下data吧。

    40059d: 8b 05 b1 0a 20 00 mov 0x200ab1(%rip),%eax # 601054 <g_a>

    00000000008b 000b00000002 R_X86_64_PC32 0000000000000000 g_a - 4

    根据这两个信息得到:

    r.offset = 0x81;
    r.type = R_X86_64_PC32;
    r.symbol = f_a;
    r.addend = -8;
    

    .data的地址:0x40059d + 0x02 (指令偏移2个字节)

    变量g_a的地址是:0x601054,

    最后算出偏移量:0x601054 - 0x40059d - 4 = 0x20 0AB1。

    就会这样的。

  2. 重定位绝对引用

    重定位绝对引用应该简单一点,我们来分析一下吧,好像代码里面没有,那就直接说公式。

    r.offset = ;
    r.type = R_X86_64_PC32;
    r.symbol = xxx;
    r.addend = ;
    

    需要确定ADDR(r.symbol)的虚拟地址,然后就直接加了。

    公式:ADDR(r.symbol)+r.addend = 绝对地址了。

4.1.6 common块

虽然在两本书都看到把未初始化的全局变量都定义在COMMON块中,但是我在实践中,未初始化的全局变量也是放在了.bss段,不知道是不是编译器更新了,这个问题留到以后,等再次碰到了,再来分析。

当然我也定义了一个弱类型的变量,不过这个变量好像是直接存在在week段,可能是我设置的一个段把。

__attribute__((weak)) int f_a = 2;

11: 0000000000000000     4 OBJECT  WEAK   DEFAULT    3 f_a

[ 3] .data             PROGBITS         0000000000000000  000000b0
       000000000000000c  0000000000000000  WA       0     0     4

2021/12/05号纠错:

今天发现了为啥定义的变量都不在COMMON段,那是因为习惯了写一个变量就会赋一个初值,如果全局变量赋了一个值,就是强类型了,需要不赋初值。

int gg_a;	// 需要这样子

14: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM gg_a   // 查看符号表,终于看到在common段了

在common定义的都属于弱符号,linux有以下规则来处理多个重定义符号:

  1. 不允许有多个同名的强符号。
  2. 如果有一个强符号和多个弱符号同名,选择强符号。(如果强弱类型不一样,链接器会报警告)
  3. 如果有多个弱符号同名,从这些弱符号类型大的选择一个。(感觉太危险了)

感觉写这种代码,太不可控了,所以还是要跟我一样,随手初始化个初值,这样就都是强符号了。

或者在链接的时候,添加GCC-fno-common,这个标记,也不会安排到commom段了。

4.1.7 c++相关问题

c++语言相比c语言来说就复杂很多,所以c++的编译器相对有会做很多事情。

4.1.7.1 重复代码消除

c++编译的时候就会产出很多重复代码,比如模板,外部内联函数、虚函数表。

可以拿最简单的模板说,模板在程序中只是模板,只有在编译的时候,才会转化成真正的代码,如果不同的.cpp文件都使用同一个模板的话,编译出来就会造成空间的浪费,所以目前主要是把模板实例相同的类型,单独设置成一个段,到时候链接的时候就可以直接合并相似段了。

举例:add() 其中有int类型,和float类型。

编译出来的段就有.temp.add 和.temp.add。

相同的段就可以直接合并,还真是厉害。

这篇写的真不容易,好久一篇没写这么多字了,只要是链接过程确实比较多事,没事,能讲解清楚就好。

参考文章:

《程序员的自我修养——链接、装载和库》

《深入理解计算机系统》

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值