重学计算机(四、程序是怎么链接的)
前面做了那么多铺垫,这次终于来到了程序是怎么链接的,应该看到前面几节的应该都可以猜测的都,链接是怎么链接的,其实程序链接也没那么难。接下来我们来分析一波。
本来觉得程序链接是比较简单,当我去准备的时候,才发现有一些细节不是很明白,所以才去看了《深入理解计算机系统》这本书,总算把我的疑惑给解决了,疑惑解决了,接下来就按照自己思路写出来。
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函数未链接之前的反汇编代码,其中在后面加了注释的就是需要重定位的。
重定位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。
就会这样的。
重定位绝对引用
重定位绝对引用应该简单一点,我们来分析一下吧,好像代码里面没有,那就直接说公式。
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有以下规则来处理多个重定义符号:
- 不允许有多个同名的强符号。
- 如果有一个强符号和多个弱符号同名,选择强符号。(如果强弱类型不一样,链接器会报警告)
- 如果有多个弱符号同名,从这些弱符号类型大的选择一个。(感觉太危险了)
感觉写这种代码,太不可控了,所以还是要跟我一样,随手初始化个初值,这样就都是强符号了。
或者在链接的时候,添加GCC-fno-common,这个标记,也不会安排到commom段了。
4.1.7 c++相关问题
c++语言相比c语言来说就复杂很多,所以c++的编译器相对有会做很多事情。
4.1.7.1 重复代码消除
c++编译的时候就会产出很多重复代码,比如模板,外部内联函数、虚函数表。
可以拿最简单的模板说,模板在程序中只是模板,只有在编译的时候,才会转化成真正的代码,如果不同的.cpp文件都使用同一个模板的话,编译出来就会造成空间的浪费,所以目前主要是把模板实例相同的类型,单独设置成一个段,到时候链接的时候就可以直接合并相似段了。
举例:add() 其中有int类型,和float类型。
编译出来的段就有.temp.add 和.temp.add。
相同的段就可以直接合并,还真是厉害。
这篇写的真不容易,好久一篇没写这么多字了,只要是链接过程确实比较多事,没事,能讲解清楚就好。
参考文章:
《程序员的自我修养——链接、装载和库》
《深入理解计算机系统》
重学计算机(四、程序是怎么链接的)相关推荐
- 重学计算机(六、程序是怎么运行的)
今天我们又来肝一个重要的主题.不知道大家有没有思考过,程序是怎么运行起来的? 肯定有同学说在linux下./hello_world就可以执行了,在windows下双点hello_world.exe文件 ...
- 重学Java(四):操作符
之前我写了一篇<重学Java(四):对象.引用.堆.栈.堆栈>,本以为凭借自己8年的Java编程经验足够把这些"吃人"的Java名词解释清楚了,但有网友不以为然,在文章 ...
- 从底层重学 Java 之四大整数 GitChat链接
从底层,从原理,我们来重学一次 Java.四大 Java 整数类 Byte.Short.Integer.Long 是我们比较常用的对象,他们的源码及实现是怎样的呢? 本系列秉承所有结论尽量从源码中来, ...
- 努力学计算机四年,终于进腾讯了!
大家好,我是鱼皮,20 届本科毕业,目前是鹅厂的一名全栈应用开发. 前几天在某乎上看到一个问题:大学计算机系最努力的同学都是如何学习的? 看了几个高赞回答后,真的是感同身受,也想和大家分享一下自己大学 ...
- 重学计算机组成原理(三)- 进击,更强的性能!
在上一篇中,我们谈到过 程序的CPU执行时间 = 指令数×CPI×Clock Cycle Time 要提升计算机的性能,可以从上面这三方面着手. 通过指令数/CPI,好像都太难了. 因此工程师们,就在 ...
- 【重学计算机】计组D1章:计算机系统概论
1.冯诺依曼计算机组成 主机(cpu+内存),外设(输入设备+输出设备+外存),总线(地址总线+数据总线+控制总线) 2.计算机层次结构 应用程序-高级语言-汇编语言-操作系统-指令集架构层-微代码层 ...
- 重学计算机组成原理(一) —— 冯诺伊曼结构
背景介绍 第一台通用电子计算机 ENIAC EDVAC -> 冯诺伊曼关于EDVAC的报告草案,即是冯诺伊曼结构计算机的起始 EDSAC UNIVAC 冯诺伊曼结构要点 在冯诺依曼署名的< ...
- 南京邮电大学计算机学院程序,2016年南京邮电大学计算机学院(软件学院)数据结构考研复试题库...
一.选择题 1. 下列有关浮点数加减运算的叙述中,正确的是( ). 对阶操作不会引起阶码上溢或下溢 右规和尾数舍入都可能引起价码上溢 左规时可能引起阶码下溢 尾数溢出时结果不一定溢出 A. 仅 B. ...
- 重学计算机组成原理(十二)- 加法器
下面这些门电路的标识,你需要非常熟悉,后续的电路都是由这些门电路组合起来的. 这些基本的门电路,是我们计算机硬件端的最基本的"积木" 包含十亿级别晶体管的现代CPU,都是由这样一个 ...
最新文章
- AntiXSS - 支持Html同时防止XSS攻击
- Java基础笔记18
- Windows Powershell的一些常规操作命令
- 程序员的自我反省-十条原则
- 合并多个commit记录
- 关于vue项目中添加less,less-loader不能运行的问题
- 如何让css与js分离
- java 可变参数_90.Java可变参数
- bzoj3159 决战
- 【算法笔记】:区间覆盖问题:贪心算法
- Ubuntu18.04 安装wine
- 前端工程师-JavaScript
- Microsoft Agent技术在Delphi中的应用
- sonar8.9.1导出扫描结果pdf 实操
- 天翼网关服务器无响应,教你使用天翼网关软件突然打不开的解决方法
- c语言更改记事本改为大写,记事本里的字母大写转换成小写怎么弄 编写一个汇编程序要...
- 三星note10 android q,【极光ROM】-【三星NOTE10/NOTE10+/5G N97XX-9825】-【V8.0 Android-Q-TJ4】...
- failed to solve with frontend dockerfile.v0: failed to create LLB definition: failed to copy: httpRe
- 计算机函数sumif求平均值,用sumif函数如何求平均值
- OpenWrt固件实现路由器定时重启方法
热门文章
- 微信小程序如何获取用户昵称性别地区等信息
- Python 29 描述符
- 基于Python的国际绝对音名标准频率C语言宏定义(32位无符号整型精度、十二等律体系、A4=440.01000Hz)
- <Input />输入框及input的相关属性
- solr 高并发_精妙绝伦!阿里资深架构师撰写这份:并发编程,可谓“独具匠心”...
- 小米5s 原生android,羡慕谷歌Pixel?其实就是国外版小米5s
- ArcGis Pro | 建筑3D视线可见性:构造视线 计算通视性
- 6-2 抽象类Shape (10 分)
- 《霍元甲》:用心去打,用心去说教
- HDU4262 Juggler