Ⅰ、虚拟空间布局模型
理论模型
包括上节的动态库与静态库,加上本节后面两个内容其实都是对gcc的扩展与补充知识,也是需要了解和掌握的知识。在开讲之前,我们先来说一下在32位x86的Linux系统中,虚拟地址空间布局模型:(系统编程阶段有重要意义)
①text段:用来存放程序执行代码的内存区域。这部分区域的大小在程序运行前确定,并且内存区域通常是只读(某些架构也允许代码段为可写,即允许修改程序)。
②rodata段:read only data segment,即只读数据段。存放一些只读的常数变量,例如字符串常量等。
③data段:用来存放程序中已初始化的全局变量的内存区域,属于静态内存分配。
④bss段:Block Started by Symbol的简称,用来存放程序中未初始化的全局变量的内存区域,属于静态内存分配。
⑤heap:堆区空间,向上增长;stack:栈区空间,向下增长。
⑥stack和heap中间的部分,用于存放我们的.so等文件
⑦stack上方与内核中间那段空间:是用来存放环境变量的。
tips:真正的段空间不止这几个,大概在30多个的样子吧。但那些对我们来说并没有很直接的关联,暂时不提。(例如,@plt就有一个与自己对应的段空间)
当关闭终端时,我们的虚拟内存就会“消失”,然后我们重新打开终端时,虚拟内存就会重新“出现”。所以我们使用export导入的环境变量,例如前文提到的LD_LIBRARY_PATH,所以这就是这个环境变量临时性的本质。(注意,这里并不是真的消失和出现,而是说:像我们运行C程序时一样,定义的变量并不会在下次重新启动时依旧保存上次运行的状态)
在初始化时,bss段部分将会清零。bss段属于静态内存分配,即程序一开始就将其清零了。比如,在C语言之类的程序编译完成后,已初始化的全局变量保存在.data段中,未初始化的全局变量保存在.bss段中。
text和data段都在可执行文件中,由系统从可执行文件中加载。
而bss段不在可执行文件中,由系统初始化。
让我们先写一段代码来进行探究:
实验一
使用vim test.c:完成下面的代码的编写。完事后 :wq保存并退出。
使用objdump(为object-dump的缩写) -t 反汇编查看变量的存储位置:
可以看到,变量在内存中的位置与注释一致。上图中,l代表local,局部的;g代表global,全局的。然后我们再来运行一下test程序看一下打印出来的内存与上图有什么关联:
我们再来对比一下两图,找到bss_1,在objdump -t查看的内存为:000000000000401c,而打印出来的是:0x61e88502401c,取末四位:401c,401c,对应上了吧。经过这个实验,我们可以发现,data段确实是位于bss段的下方的。
实验二
使用vim创建m1.c,然后再在末行模式输入::vsp m2.c分屏再建一个m2.c文件,然后分别编写程序如下:(左m2.c,右m1.c)。最后在末行模式输入:wqall,保存并退出所有打开的文件。
此时已经退出了vim模式,我们对这两个文件进行编译:
我们发现,m2编译之后比m1的文件要大很多。m1大小只有15800b,m2大小却达到了27816b。所以,此时我们打算继续使用objdump -t 反汇编来查看变量arr所在的存储位置
突然就发现不对劲了,m1没有初始化arr,所以m1中的arr处于bss段中,而m2相反,初始化为非零值,所以m2的arr处于data段中。因为text、bss、data段在编译时已经决定了进程将占用多少VM,所以我们使用size命令来查看一下两个文件的段的大小。
我们发现:data段和bss段的大小的差别很大。所以问题肯定是出现在了这个上面。
然后,我们使用objdump -s来查看两个文件中.data段中的数据,可以看到,有一长串的数据:
m2: 文件格式 elf64-x86-64
//省略
Contents of section .data:4000 00000000 00000000 08400000 00000000 .........@......//....此处为个人删除省略内容....//此处省略了大约六万的字符数6ef0 00000000 00000000 00000000 00000000 ................
//省略
m1: 文件格式 elf64-x86-64
//省略
Contents of section .data:4000 00000000 00000000 08400000 00000000 .........@......
//省略
.bss 是不占用.exe文件空间的,其内容由操作系统初始化(清零);
.data 却需要占用,其内容由程序初始化。因此造成了上述情况。
数据段合并和地址回填通常是操作系统和编译器在内存管理和程序加载时涉及的概念。
在 使用动态库 时就涉及到这两个操作:
Ⅱ、数据段合并
数据段合并主要是在程序链接的过程中进行的。当多个目标文件被链接时,它们可能包含多个相同类型的数据段(例如 .data
、.bss
)。链接器会将这些数据段合并成一个统一的数据段,减少内存使用和提高访问效率。
将上面的图拿过来进行说明:
提前声明:每个段的空间为1个页(page)的大小--4K。
在该内存中,每个段都会有访问权限,对于该图中的text段和rodata段都是ro权限,而data和bss段是rw权限。如果text段和rodata都单独占一个段空间,那么就会占用两个4K的空间,所以为了节省空间,在链接阶段,我们有一个数据段合并的过程,并不是说只合并数据段,很多段都会根据标准进行合并,这里给出四个段:
text段与rodata段说你也是只读我也是只读,咱俩凑合凑合过吧,然后就进行合并了,data和bss段也一样进行合并了,那么就节省了至少2个4K的空间。
Ⅲ、地址回填
地址回填是指在程序的加载或链接过程中,将目标文件中使用的符号地址转换为实际内存地址的过程。当程序被加载到内存中时,操作系统需要将符号(如函数或变量)的地址填充到代码和数据段中,以便程序能顺利运行。
在main函数中,里面有两个函数fuc1和fuc2,这两个函数在链接阶段进行地址回填。那么怎么进行地址回填呢?这通常涉及到以下几个步骤:
- 符号解析:将符号名称解析为实际的内存地址。
- 地址替换:在目标文件中占位符的地址被实际计算出的地址所替换。
- 重定位:处理程序在不同内存地址加载时,需要调整地址代码和数据。
完成链接后得到的a.out的地址是以main函数地址为依据的。
假设main的地址为1000,那么func1依据main而定义,假设间隔为100,那么func1的地址就为1000+100,假设func2与main间隔为200,那么func2的地址就为1000+200。
但是我们也说了,这是完成链接后的事情,在完成编译后的main.o文件中main的地址标记为0。但下面的关系依然存在,那也就是说:
当从hello.o执行到a.out的操作过程中,我们就完成了链接,在链接的过程中,进行了一个地址回填,看一下,填的是什么?其实填的就是main的地址,本来main的地址以main符代替,链接完然main具有地址后,就会将地址进行回填。
那么制作动态库呢?制作动态库时,func1、func2被制成了.o文件,那func1和func2还是以main作为依据吗?不是了。因为动态库内的函数调用方法与文件内的函数的调用时处理方法不同。
先创建一个test.c文件
然后我们使用objdump -dS 来查看反汇编:
test.shared: 文件格式 elf64-x86-64Disassembly of section .init:0000000000001000 <_init>:1000: f3 0f 1e fa endbr64 1004: 48 83 ec 08 sub $0x8,%rsp1008: 48 8b 05 d9 2f 00 00 mov 0x2fd9(%rip),%rax # 3fe8 <__gmon_start__@Base>100f: 48 85 c0 test %rax,%rax1012: 74 02 je 1016 <_init+0x16>1014: ff d0 call *%rax1016: 48 83 c4 08 add $0x8,%rsp101a: c3 ret Disassembly of section .plt:0000000000001020 <.plt>:1020: ff 35 8a 2f 00 00 push 0x2f8a(%rip) # 3fb0 <_GLOBAL_OFFSET_TABLE_+0x8>1026: f2 ff 25 8b 2f 00 00 bnd jmp *0x2f8b(%rip) # 3fb8 <_GLOBAL_OFFSET_TABLE_+0x10>102d: 0f 1f 00 nopl (%rax)1030: f3 0f 1e fa endbr64 1034: 68 00 00 00 00 push $0x01039: f2 e9 e1 ff ff ff bnd jmp 1020 <_init+0x20>103f: 90 nop1040: f3 0f 1e fa endbr64 1044: 68 01 00 00 00 push $0x11049: f2 e9 d1 ff ff ff bnd jmp 1020 <_init+0x20>104f: 90 nop1050: f3 0f 1e fa endbr64 1054: 68 02 00 00 00 push $0x21059: f2 e9 c1 ff ff ff bnd jmp 1020 <_init+0x20>105f: 90 nopDisassembly of section .plt.got:0000000000001060 <__cxa_finalize@plt>:1060: f3 0f 1e fa endbr64 1064: f2 ff 25 8d 2f 00 00 bnd jmp *0x2f8d(%rip) # 3ff8 <__cxa_finalize@GLIBC_2.2.5>106b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)Disassembly of section .plt.sec:0000000000001070 <add@plt>:1070: f3 0f 1e fa endbr64 1074: f2 ff 25 45 2f 00 00 bnd jmp *0x2f45(%rip) # 3fc0 <add@Base>107b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)0000000000001080 <printf@plt>:1080: f3 0f 1e fa endbr64 1084: f2 ff 25 3d 2f 00 00 bnd jmp *0x2f3d(%rip) # 3fc8 <printf@GLIBC_2.2.5>108b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)0000000000001090 <sub@plt>:1090: f3 0f 1e fa endbr64 1094: f2 ff 25 35 2f 00 00 bnd jmp *0x2f35(%rip) # 3fd0 <sub@Base>109b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)Disassembly of section .text:00000000000010a0 <_start>:10a0: f3 0f 1e fa endbr64 10a4: 31 ed xor %ebp,%ebp10a6: 49 89 d1 mov %rdx,%r910a9: 5e pop %rsi10aa: 48 89 e2 mov %rsp,%rdx10ad: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp10b1: 50 push %rax10b2: 54 push %rsp10b3: 45 31 c0 xor %r8d,%r8d10b6: 31 c9 xor %ecx,%ecx10b8: 48 8d 3d e1 00 00 00 lea 0xe1(%rip),%rdi # 11a0 <main>10bf: ff 15 13 2f 00 00 call *0x2f13(%rip) # 3fd8 <__libc_start_main@GLIBC_2.34>10c5: f4 hlt 10c6: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)10cd: 00 00 00 00000000000010d0 <deregister_tm_clones>:10d0: 48 8d 3d 39 2f 00 00 lea 0x2f39(%rip),%rdi # 4010 <__TMC_END__>10d7: 48 8d 05 32 2f 00 00 lea 0x2f32(%rip),%rax # 4010 <__TMC_END__>10de: 48 39 f8 cmp %rdi,%rax10e1: 74 15 je 10f8 <deregister_tm_clones+0x28>10e3: 48 8b 05 f6 2e 00 00 mov 0x2ef6(%rip),%rax # 3fe0 <_ITM_deregisterTMCloneTable@Base>10ea: 48 85 c0 test %rax,%rax10ed: 74 09 je 10f8 <deregister_tm_clones+0x28>10ef: ff e0 jmp *%rax10f1: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)10f8: c3 ret 10f9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)0000000000001100 <register_tm_clones>:1100: 48 8d 3d 09 2f 00 00 lea 0x2f09(%rip),%rdi # 4010 <__TMC_END__>1107: 48 8d 35 02 2f 00 00 lea 0x2f02(%rip),%rsi # 4010 <__TMC_END__>110e: 48 29 fe sub %rdi,%rsi1111: 48 89 f0 mov %rsi,%rax1114: 48 c1 ee 3f shr $0x3f,%rsi1118: 48 c1 f8 03 sar $0x3,%rax111c: 48 01 c6 add %rax,%rsi111f: 48 d1 fe sar %rsi1122: 74 14 je 1138 <register_tm_clones+0x38>1124: 48 8b 05 c5 2e 00 00 mov 0x2ec5(%rip),%rax # 3ff0 <_ITM_registerTMCloneTable@Base>112b: 48 85 c0 test %rax,%rax112e: 74 08 je 1138 <register_tm_clones+0x38>1130: ff e0 jmp *%rax1132: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)1138: c3 ret 1139: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)0000000000001140 <__do_global_dtors_aux>:1140: f3 0f 1e fa endbr64 1144: 80 3d c5 2e 00 00 00 cmpb $0x0,0x2ec5(%rip) # 4010 <__TMC_END__>114b: 75 2b jne 1178 <__do_global_dtors_aux+0x38>114d: 55 push %rbp114e: 48 83 3d a2 2e 00 00 cmpq $0x0,0x2ea2(%rip) # 3ff8 <__cxa_finalize@GLIBC_2.2.5>1155: 00 1156: 48 89 e5 mov %rsp,%rbp1159: 74 0c je 1167 <__do_global_dtors_aux+0x27>115b: 48 8b 3d a6 2e 00 00 mov 0x2ea6(%rip),%rdi # 4008 <__dso_handle>1162: e8 f9 fe ff ff call 1060 <__cxa_finalize@plt>1167: e8 64 ff ff ff call 10d0 <deregister_tm_clones>116c: c6 05 9d 2e 00 00 01 movb $0x1,0x2e9d(%rip) # 4010 <__TMC_END__>1173: 5d pop %rbp1174: c3 ret 1175: 0f 1f 00 nopl (%rax)1178: c3 ret 1179: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)0000000000001180 <frame_dummy>:1180: f3 0f 1e fa endbr64 1184: e9 77 ff ff ff jmp 1100 <register_tm_clones>0000000000001189 <mul>:1189: f3 0f 1e fa endbr64 118d: 55 push %rbp118e: 48 89 e5 mov %rsp,%rbp1191: 89 7d fc mov %edi,-0x4(%rbp)1194: 89 75 f8 mov %esi,-0x8(%rbp)1197: 8b 45 fc mov -0x4(%rbp),%eax119a: 0f af 45 f8 imul -0x8(%rbp),%eax119e: 5d pop %rbp119f: c3 ret 00000000000011a0 <main>:11a0: f3 0f 1e fa endbr64 11a4: 55 push %rbp11a5: 48 89 e5 mov %rsp,%rbp11a8: 48 83 ec 20 sub $0x20,%rsp11ac: 89 7d ec mov %edi,-0x14(%rbp)11af: 48 89 75 e0 mov %rsi,-0x20(%rbp)11b3: c7 45 f8 0a 00 00 00 movl $0xa,-0x8(%rbp)11ba: c7 45 fc 05 00 00 00 movl $0x5,-0x4(%rbp)11c1: 8b 55 fc mov -0x4(%rbp),%edx11c4: 8b 45 f8 mov -0x8(%rbp),%eax11c7: 89 d6 mov %edx,%esi11c9: 89 c7 mov %eax,%edi11cb: e8 a0 fe ff ff call 1070 <add@plt>11d0: 89 c1 mov %eax,%ecx11d2: 8b 55 fc mov -0x4(%rbp),%edx11d5: 8b 45 f8 mov -0x8(%rbp),%eax11d8: 89 c6 mov %eax,%esi11da: 48 8d 05 23 0e 00 00 lea 0xe23(%rip),%rax # 2004 <_IO_stdin_used+0x4>11e1: 48 89 c7 mov %rax,%rdi11e4: b8 00 00 00 00 mov $0x0,%eax11e9: e8 92 fe ff ff call 1080 <printf@plt>11ee: 8b 55 fc mov -0x4(%rbp),%edx11f1: 8b 45 f8 mov -0x8(%rbp),%eax11f4: 89 d6 mov %edx,%esi11f6: 89 c7 mov %eax,%edi11f8: e8 93 fe ff ff call 1090 <sub@plt>11fd: 89 c1 mov %eax,%ecx11ff: 8b 55 fc mov -0x4(%rbp),%edx1202: 8b 45 f8 mov -0x8(%rbp),%eax1205: 89 c6 mov %eax,%esi1207: 48 8d 05 04 0e 00 00 lea 0xe04(%rip),%rax # 2012 <_IO_stdin_used+0x12>120e: 48 89 c7 mov %rax,%rdi1211: b8 00 00 00 00 mov $0x0,%eax1216: e8 65 fe ff ff call 1080 <printf@plt>121b: 8b 55 fc mov -0x4(%rbp),%edx121e: 8b 45 f8 mov -0x8(%rbp),%eax1221: 89 d6 mov %edx,%esi1223: 89 c7 mov %eax,%edi1225: e8 5f ff ff ff call 1189 <mul>122a: 89 c1 mov %eax,%ecx122c: 8b 55 fc mov -0x4(%rbp),%edx122f: 8b 45 f8 mov -0x8(%rbp),%eax1232: 89 c6 mov %eax,%esi1234: 48 8d 05 e5 0d 00 00 lea 0xde5(%rip),%rax # 2020 <_IO_stdin_used+0x20>123b: 48 89 c7 mov %rax,%rdi123e: b8 00 00 00 00 mov $0x0,%eax1243: e8 38 fe ff ff call 1080 <printf@plt>1248: b8 00 00 00 00 mov $0x0,%eax124d: c9 leave 124e: c3 ret Disassembly of section .fini:0000000000001250 <_fini>:1250: f3 0f 1e fa endbr64 1254: 48 83 ec 08 sub $0x8,%rsp1258: 48 83 c4 08 add $0x8,%rsp125c: c3 ret
看一下main函数中的调用语句:
#11cb: e8 a0 fe ff ff call 1070 <add@plt>11d0: 89 c1 mov %eax,%ecx11d2: 8b 55 fc mov -0x4(%rbp),%edx11d5: 8b 45 f8 mov -0x8(%rbp),%eax11d8: 89 c6 mov %eax,%esi#1225: e8 5f ff ff ff call 1189 <mul>122a: 89 c1 mov %eax,%ecx122c: 8b 55 fc mov -0x4(%rbp),%edx122f: 8b 45 f8 mov -0x8(%rbp),%eax1232: 89 c6 mov %eax,%esi
所以说,我们的调用方式是看不出任何问题的,因为我们说了区别在于调用函数之后的处理方式。
我们将mul和add(或sub)拉出来对比
我们发现动态库中的函数后面会有一个@plt后缀,而且执行的语句也不一样。调用mul函数后,利用push压栈,mov赋值,pop出栈等常规操作处理该函数,但调用add函数后,利用的时bnd jmp跳转到动态库中进行操作。@plt是一个与程序链接和运行时符号解析相关的概念
PLT(Procedure linkage Table--过程链接表)是动态链接的一个结构,主要用于解决动态函数调用(尤其是未解析的外部函数)。工作原理:
动态链接:当一个程序调用一个动态链接库中的函数时,这个函数的地址在编译时并不确定,因此在编译时并没有直接的地址。相反,编译器写入指向 PLT 中相应入口的调用。
PLT 表项:每个外部函数(例如,一个来自共享库的函数)都有一个 PLT 表项。当一个函数被调用时,程序首先跳转到 PLT 表项。
首次调用:在首次调用该函数时,PLT 将控制权传递给动态链接器(通常是
ld-linux.so
),该链接器负责解析该函数的实际地址。这时动态链接器会更新 PLT 中的表项,将实际地址填入,以便后续调用可以直接跳转到该地址。性能优势:使用 PLT,可以在不需要提前链接所有外部符号的情况下启动和运行程序,简化了链接过程并支持共享库的动态更新
当程序第一次调用 动态库函数add
时,它会跳转到 add 的 PLT 条目,这样动态链接器会处理真正的地址解析。
可以使用 objdump
或 gdb
等工具查看编译后的程序的 PLT 表。例如,使用以下命令:
objdump -D test.shared | grep add
这是运行结果截出来的两段内容,这就是程序跟add有关的PLT表,显示了add在PLT中的地址
因为调用动态库的函数比程序文件自己的函数慢,所以有些地方会描述动态库为延迟绑定。
结论:生成.o文件时要求生成与位置无关的代码,也就是加-fPIC选项的目的。
假设我们有多个目标文件,每个文件都有一个全局变量count
。链接器会将这些变量合并,只保留一份,并分配一个内存地址,之后所有对count
的引用将在运行时替换为这个地址。
感谢大家!!!