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

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

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 Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS fun2.c6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 s_b.22897: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 s_a.228811: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 f_a12: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 f_b13: 0000000000000000    50 FUNC    GLOBAL DEFAULT    1 func214: 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-64Sections:
Idx Name          Size      VMA               LMA               File off  Algn0 .text         00000032  0000000000000000  0000000000000000  00000040  2**0CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE1 .data         00000008  0000000000000000  0000000000000000  00000074  2**2CONTENTS, ALLOC, LOAD, DATA2 .bss          00000008  0000000000000000  0000000000000000  0000007c  2**2ALLOC3 .rodata       0000000e  0000000000000000  0000000000000000  0000007c  2**0CONTENTS, ALLOC, LOAD, READONLY, DATA
root@ubuntu:~/c_test/04# objdump -h hello_world.o hello_world.o:     file format elf64-x86-64Sections:
Idx Name          Size      VMA               LMA               File off  Algn0 .text         000000a3  0000000000000000  0000000000000000  00000040  2**0CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE1 .data         00000008  0000000000000000  0000000000000000  000000e4  2**2CONTENTS, ALLOC, LOAD, DATA2 .bss          00000008  0000000000000000  0000000000000000  000000ec  2**2ALLOC3 .rodata       0000001e  0000000000000000  0000000000000000  000000ec  2**0CONTENTS, ALLOC, LOAD, READONLY, DATA
root@ubuntu:~/c_test/04# objdump -h hello_world hello_world:     file format elf64-x86-64Sections:
Idx Name          Size      VMA               LMA               File off  Algn13 .text         00000242  0000000000400430  0000000000400430  00000430  2**4CONTENTS, ALLOC, LOAD, READONLY, CODE15 .rodata       00000030  0000000000400680  0000000000400680  00000680  2**2CONTENTS, ALLOC, LOAD, READONLY, DATA24 .data         00000020  0000000000601028  0000000000601028  00001028  2**3CONTENTS, ALLOC, LOAD, DATA25 .bss          00000018  0000000000601048  0000000000601048  00001048  2**2ALLOC

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

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

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

4.1.3 空间分配

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

那就分糖果啊!!

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

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

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

root@ubuntu:~/c_test/04# objdump -h fun2.o fun2.o:     file format elf64-x86-64Sections:
Idx Name          Size      VMA               LMA               File off  Algn0 .text         00000032  0000000000000000  0000000000000000  00000040  2**0CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE1 .data         00000008  0000000000000000  0000000000000000  00000074  2**2CONTENTS, ALLOC, LOAD, DATA2 .bss          00000008  0000000000000000  0000000000000000  0000007c  2**2ALLOC3 .rodata       0000000e  0000000000000000  0000000000000000  0000007c  2**0CONTENTS, ALLOC, LOAD, READONLY, DATA4 .comment      00000036  0000000000000000  0000000000000000  0000008a  2**0CONTENTS, READONLY5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000c0  2**0CONTENTS, READONLY6 .eh_frame     00000038  0000000000000000  0000000000000000  000000c0  2**3CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

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

root@ubuntu:~/c_test/04# objdump -h hello_worldhello_world:     file format elf64-x86-64Sections:
Idx Name          Size      VMA               LMA               File off  Algn0 .interp       0000001c  0000000000400238  0000000000400238  00000238  2**0CONTENTS, ALLOC, LOAD, READONLY, DATA13 .text         00000242  0000000000400430  0000000000400430  00000430  2**4CONTENTS, ALLOC, LOAD, READONLY, CODE14 .fini         00000009  0000000000400674  0000000000400674  00000674  2**2CONTENTS, ALLOC, LOAD, READONLY, CODE15 .rodata       00000030  0000000000400680  0000000000400680  00000680  2**2CONTENTS, ALLOC, LOAD, READONLY, DATA16 .eh_frame_hdr 00000044  00000000004006b0  00000000004006b0  000006b0  2**2CONTENTS, ALLOC, LOAD, READONLY, DATA17 .eh_frame     00000134  00000000004006f8  00000000004006f8  000006f8  2**3CONTENTS, ALLOC, LOAD, READONLY, DATA24 .data         00000020  0000000000601028  0000000000601028  00001028  2**3CONTENTS, ALLOC, LOAD, DATA25 .bss          00000018  0000000000601048  0000000000601048  00001048  2**2ALLOC26 .comment      00000035  0000000000000000  0000000000000000  00001048  2**0CONTENTS, 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:                             ELF64Data:                              2's complement, little endianVersion:                           1 (current)OS/ABI:                            UNIX - System VABI Version:                       0Type:                              EXEC (Executable file)Machine:                           Advanced Micro Devices X86-64Version:                           0x1Entry point address:               0x400430Start of program headers:          64 (bytes into file)Start of section headers:          6976 (bytes into file)Flags:                             0x0Size of this header:               64 (bytes)Size of program headers:           56 (bytes)Number of program headers:         9Size of section headers:           64 (bytes)Number of section headers:         31Section 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-64Disassembly of section .text:0000000000000000 <func1>:0: 55                      push   %rbp1:   48 89 e5                mov    %rsp,%rbp0000000000000026 <main>:26:   55                      push   %rbp27:  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.oRelocation 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 - 4root@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   %rbp27:  48 89 e5                mov    %rsp,%rbp2a: 48 83 ec 20             sub    $0x20,%rsp2e:    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 = %edi42: 8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 48 <main+0x22>        s_b.2294 = %edx48: 01 c2                   add    %eax,%edx4a: 8b 45 f8                mov    -0x8(%rbp),%eax4d:   01 c2                   add    %eax,%edx4f: 8b 45 fc                mov    -0x4(%rbp),%eax52:   01 d0                   add    %edx,%eax54: 89 c7                   mov    %eax,%edi56: e8 00 00 00 00          callq  5b <main+0x35>            # func15b:  8b 15 00 00 00 00       mov    0x0(%rip),%edx        # 61 <main+0x3b>    s_a.229361: 8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 67 <main+0x41>    s_b.229467: 01 c2                   add    %eax,%edx69: 8b 45 f8                mov    -0x8(%rbp),%eax6c:   01 c2                   add    %eax,%edx6e: 8b 45 fc                mov    -0x4(%rbp),%eax71:   01 d0                   add    %edx,%eax73: 89 c7                   mov    %eax,%edi75: b8 00 00 00 00          mov    $0x0,%eax7a: e8 00 00 00 00          callq  7f <main+0x59>            # func27f:  c7 05 00 00 00 00 64    movl   $0x64,0x0(%rip)        # 89 <main+0x63>  f_a86:   00 00 00 89:    8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 8f <main+0x69>  g_a8f:    8b 4d fc                mov    -0x4(%rbp),%ecx92:   8b 55 f8                mov    -0x8(%rbp),%edx95:   89 c6                   mov    %eax,%esi97: bf 00 00 00 00          mov    $0x0,%edi9c: b8 00 00 00 00          mov    $0x0,%eaxa1: e8 00 00 00 00          callq  a6 <main+0x80>        # printfa6: b8 00 00 00 00          mov    $0x0,%eaxab: 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   %rbp40057f:  48 89 e5                mov    %rsp,%rbp400582: 48 83 ec 20             sub    $0x20,%rsp400586:    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,%edx4005a2: 8b 45 f8                mov    -0x8(%rbp),%eax4005a5:   01 c2                   add    %eax,%edx4005a7: 8b 45 fc                mov    -0x4(%rbp),%eax4005aa:   01 d0                   add    %edx,%eax4005ac: 89 c7                   mov    %eax,%edi4005ae: 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  000000b0000000000000000c  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. 重学计算机(六、程序是怎么运行的)

    今天我们又来肝一个重要的主题.不知道大家有没有思考过,程序是怎么运行起来的? 肯定有同学说在linux下./hello_world就可以执行了,在windows下双点hello_world.exe文件 ...

  2. 重学Java(四):操作符

    之前我写了一篇<重学Java(四):对象.引用.堆.栈.堆栈>,本以为凭借自己8年的Java编程经验足够把这些"吃人"的Java名词解释清楚了,但有网友不以为然,在文章 ...

  3. 从底层重学 Java 之四大整数 GitChat链接

    从底层,从原理,我们来重学一次 Java.四大 Java 整数类 Byte.Short.Integer.Long 是我们比较常用的对象,他们的源码及实现是怎样的呢? 本系列秉承所有结论尽量从源码中来, ...

  4. 努力学计算机四年,终于进腾讯了!

    大家好,我是鱼皮,20 届本科毕业,目前是鹅厂的一名全栈应用开发. 前几天在某乎上看到一个问题:大学计算机系最努力的同学都是如何学习的? 看了几个高赞回答后,真的是感同身受,也想和大家分享一下自己大学 ...

  5. 重学计算机组成原理(三)- 进击,更强的性能!

    在上一篇中,我们谈到过 程序的CPU执行时间 = 指令数×CPI×Clock Cycle Time 要提升计算机的性能,可以从上面这三方面着手. 通过指令数/CPI,好像都太难了. 因此工程师们,就在 ...

  6. 【重学计算机】计组D1章:计算机系统概论

    1.冯诺依曼计算机组成 主机(cpu+内存),外设(输入设备+输出设备+外存),总线(地址总线+数据总线+控制总线) 2.计算机层次结构 应用程序-高级语言-汇编语言-操作系统-指令集架构层-微代码层 ...

  7. 重学计算机组成原理(一) —— 冯诺伊曼结构

    背景介绍 第一台通用电子计算机 ENIAC EDVAC -> 冯诺伊曼关于EDVAC的报告草案,即是冯诺伊曼结构计算机的起始 EDSAC UNIVAC 冯诺伊曼结构要点 在冯诺依曼署名的< ...

  8. 南京邮电大学计算机学院程序,2016年南京邮电大学计算机学院(软件学院)数据结构考研复试题库...

    一.选择题 1. 下列有关浮点数加减运算的叙述中,正确的是( ). 对阶操作不会引起阶码上溢或下溢 右规和尾数舍入都可能引起价码上溢 左规时可能引起阶码下溢 尾数溢出时结果不一定溢出 A. 仅 B. ...

  9. 重学计算机组成原理(十二)- 加法器

    下面这些门电路的标识,你需要非常熟悉,后续的电路都是由这些门电路组合起来的. 这些基本的门电路,是我们计算机硬件端的最基本的"积木" 包含十亿级别晶体管的现代CPU,都是由这样一个 ...

最新文章

  1. AntiXSS - 支持Html同时防止XSS攻击
  2. Java基础笔记18
  3. Windows Powershell的一些常规操作命令
  4. 程序员的自我反省-十条原则
  5. 合并多个commit记录
  6. 关于vue项目中添加less,less-loader不能运行的问题
  7. 如何让css与js分离
  8. java 可变参数_90.Java可变参数
  9. bzoj3159 决战
  10. 【算法笔记】:区间覆盖问题:贪心算法
  11. Ubuntu18.04 安装wine
  12. 前端工程师-JavaScript
  13. Microsoft Agent技术在Delphi中的应用
  14. sonar8.9.1导出扫描结果pdf 实操
  15. 天翼网关服务器无响应,教你使用天翼网关软件突然打不开的解决方法
  16. c语言更改记事本改为大写,记事本里的字母大写转换成小写怎么弄 编写一个汇编程序要...
  17. 三星note10 android q,【极光ROM】-【三星NOTE10/NOTE10+/5G N97XX-9825】-【V8.0 Android-Q-TJ4】...
  18. failed to solve with frontend dockerfile.v0: failed to create LLB definition: failed to copy: httpRe
  19. 计算机函数sumif求平均值,用sumif函数如何求平均值
  20. OpenWrt固件实现路由器定时重启方法

热门文章

  1. 微信小程序如何获取用户昵称性别地区等信息
  2. Python 29 描述符
  3. 基于Python的国际绝对音名标准频率C语言宏定义(32位无符号整型精度、十二等律体系、A4=440.01000Hz)
  4. <Input />输入框及input的相关属性
  5. solr 高并发_精妙绝伦!阿里资深架构师撰写这份:并发编程,可谓“独具匠心”...
  6. 小米5s 原生android,羡慕谷歌Pixel?其实就是国外版小米5s
  7. ArcGis Pro | 建筑3D视线可见性:构造视线 计算通视性
  8. 6-2 抽象类Shape (10 分)
  9. 《霍元甲》:用心去打,用心去说教
  10. HDU4262 Juggler