【Linux底层探究】虚拟空间布局模型&地址回填&数据段合并(万字详解)

Ⅰ、虚拟空间布局模型

理论模型

包括上节的动态库与静态库,加上本节后面两个内容其实都是对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,这两个函数在链接阶段进行地址回填。那么怎么进行地址回填呢?这通常涉及到以下几个步骤:

  1. 符号解析:将符号名称解析为实际的内存地址。
  2. 地址替换:在目标文件中占位符的地址被实际计算出的地址所替换。
  3. 重定位:处理程序在不同内存地址加载时,需要调整地址代码和数据。

完成链接后得到的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-64


Disassembly of section .init:

0000000000001000 <_init>:
    1000:	f3 0f 1e fa          	endbr64 
    1004:	48 83 ec 08          	sub    $0x8,%rsp
    1008:	48 8b 05 d9 2f 00 00 	mov    0x2fd9(%rip),%rax        # 3fe8 <__gmon_start__@Base>
    100f:	48 85 c0             	test   %rax,%rax
    1012:	74 02                	je     1016 <_init+0x16>
    1014:	ff d0                	call   *%rax
    1016:	48 83 c4 08          	add    $0x8,%rsp
    101a:	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   $0x0
    1039:	f2 e9 e1 ff ff ff    	bnd jmp 1020 <_init+0x20>
    103f:	90                   	nop
    1040:	f3 0f 1e fa          	endbr64 
    1044:	68 01 00 00 00       	push   $0x1
    1049:	f2 e9 d1 ff ff ff    	bnd jmp 1020 <_init+0x20>
    104f:	90                   	nop
    1050:	f3 0f 1e fa          	endbr64 
    1054:	68 02 00 00 00       	push   $0x2
    1059:	f2 e9 c1 ff ff ff    	bnd jmp 1020 <_init+0x20>
    105f:	90                   	nop

Disassembly 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,%ebp
    10a6:	49 89 d1             	mov    %rdx,%r9
    10a9:	5e                   	pop    %rsi
    10aa:	48 89 e2             	mov    %rsp,%rdx
    10ad:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp
    10b1:	50                   	push   %rax
    10b2:	54                   	push   %rsp
    10b3:	45 31 c0             	xor    %r8d,%r8d
    10b6:	31 c9                	xor    %ecx,%ecx
    10b8:	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,%rax
    10e1:	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,%rax
    10ed:	74 09                	je     10f8 <deregister_tm_clones+0x28>
    10ef:	ff e0                	jmp    *%rax
    10f1:	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,%rsi
    1111:	48 89 f0             	mov    %rsi,%rax
    1114:	48 c1 ee 3f          	shr    $0x3f,%rsi
    1118:	48 c1 f8 03          	sar    $0x3,%rax
    111c:	48 01 c6             	add    %rax,%rsi
    111f:	48 d1 fe             	sar    %rsi
    1122:	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,%rax
    112e:	74 08                	je     1138 <register_tm_clones+0x38>
    1130:	ff e0                	jmp    *%rax
    1132:	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   %rbp
    114e:	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,%rbp
    1159:	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    %rbp
    1174:	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   %rbp
    118e:	48 89 e5             	mov    %rsp,%rbp
    1191:	89 7d fc             	mov    %edi,-0x4(%rbp)
    1194:	89 75 f8             	mov    %esi,-0x8(%rbp)
    1197:	8b 45 fc             	mov    -0x4(%rbp),%eax
    119a:	0f af 45 f8          	imul   -0x8(%rbp),%eax
    119e:	5d                   	pop    %rbp
    119f:	c3                   	ret    

00000000000011a0 <main>:
    11a0:	f3 0f 1e fa          	endbr64 
    11a4:	55                   	push   %rbp
    11a5:	48 89 e5             	mov    %rsp,%rbp
    11a8:	48 83 ec 20          	sub    $0x20,%rsp
    11ac:	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),%edx
    11c4:	8b 45 f8             	mov    -0x8(%rbp),%eax
    11c7:	89 d6                	mov    %edx,%esi
    11c9:	89 c7                	mov    %eax,%edi
    11cb:	e8 a0 fe ff ff       	call   1070 <add@plt>
    11d0:	89 c1                	mov    %eax,%ecx
    11d2:	8b 55 fc             	mov    -0x4(%rbp),%edx
    11d5:	8b 45 f8             	mov    -0x8(%rbp),%eax
    11d8:	89 c6                	mov    %eax,%esi
    11da:	48 8d 05 23 0e 00 00 	lea    0xe23(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
    11e1:	48 89 c7             	mov    %rax,%rdi
    11e4:	b8 00 00 00 00       	mov    $0x0,%eax
    11e9:	e8 92 fe ff ff       	call   1080 <printf@plt>
    11ee:	8b 55 fc             	mov    -0x4(%rbp),%edx
    11f1:	8b 45 f8             	mov    -0x8(%rbp),%eax
    11f4:	89 d6                	mov    %edx,%esi
    11f6:	89 c7                	mov    %eax,%edi
    11f8:	e8 93 fe ff ff       	call   1090 <sub@plt>
    11fd:	89 c1                	mov    %eax,%ecx
    11ff:	8b 55 fc             	mov    -0x4(%rbp),%edx
    1202:	8b 45 f8             	mov    -0x8(%rbp),%eax
    1205:	89 c6                	mov    %eax,%esi
    1207:	48 8d 05 04 0e 00 00 	lea    0xe04(%rip),%rax        # 2012 <_IO_stdin_used+0x12>
    120e:	48 89 c7             	mov    %rax,%rdi
    1211:	b8 00 00 00 00       	mov    $0x0,%eax
    1216:	e8 65 fe ff ff       	call   1080 <printf@plt>
    121b:	8b 55 fc             	mov    -0x4(%rbp),%edx
    121e:	8b 45 f8             	mov    -0x8(%rbp),%eax
    1221:	89 d6                	mov    %edx,%esi
    1223:	89 c7                	mov    %eax,%edi
    1225:	e8 5f ff ff ff       	call   1189 <mul>
    122a:	89 c1                	mov    %eax,%ecx
    122c:	8b 55 fc             	mov    -0x4(%rbp),%edx
    122f:	8b 45 f8             	mov    -0x8(%rbp),%eax
    1232:	89 c6                	mov    %eax,%esi
    1234:	48 8d 05 e5 0d 00 00 	lea    0xde5(%rip),%rax        # 2020 <_IO_stdin_used+0x20>
    123b:	48 89 c7             	mov    %rax,%rdi
    123e:	b8 00 00 00 00       	mov    $0x0,%eax
    1243:	e8 38 fe ff ff       	call   1080 <printf@plt>
    1248:	b8 00 00 00 00       	mov    $0x0,%eax
    124d:	c9                   	leave  
    124e:	c3                   	ret    

Disassembly of section .fini:

0000000000001250 <_fini>:
    1250:	f3 0f 1e fa          	endbr64 
    1254:	48 83 ec 08          	sub    $0x8,%rsp
    1258:	48 83 c4 08          	add    $0x8,%rsp
    125c:	c3                   	ret    

看一下main函数中的调用语句:

#11cb:	e8 a0 fe ff ff       	call   1070 <add@plt>
    11d0:	89 c1                	mov    %eax,%ecx
    11d2:	8b 55 fc             	mov    -0x4(%rbp),%edx
    11d5:	8b 45 f8             	mov    -0x8(%rbp),%eax
    11d8:	89 c6                	mov    %eax,%esi

#1225:	e8 5f ff ff ff       	call   1189 <mul>
    122a:	89 c1                	mov    %eax,%ecx
    122c:	8b 55 fc             	mov    -0x4(%rbp),%edx
    122f:	8b 45 f8             	mov    -0x8(%rbp),%eax
    1232:	89 c6                	mov    %eax,%esi

所以说,我们的调用方式是看不出任何问题的,因为我们说了区别在于调用函数之后的处理方式。

我们将mul和add(或sub)拉出来对比

  我们发现动态库中的函数后面会有一个@plt后缀,而且执行的语句也不一样。调用mul函数后,利用push压栈,mov赋值,pop出栈等常规操作处理该函数,但调用add函数后,利用的时bnd jmp跳转到动态库中进行操作。@plt是一个与程序链接和运行时符号解析相关的概念

PLT(Procedure linkage Table--过程链接表)是动态链接的一个结构,主要用于解决动态函数调用(尤其是未解析的外部函数)。工作原理:

  1. 动态链接:当一个程序调用一个动态链接库中的函数时,这个函数的地址在编译时并不确定,因此在编译时并没有直接的地址。相反,编译器写入指向 PLT 中相应入口的调用。

  2. PLT 表项:每个外部函数(例如,一个来自共享库的函数)都有一个 PLT 表项。当一个函数被调用时,程序首先跳转到 PLT 表项。

  3. 首次调用:在首次调用该函数时,PLT 将控制权传递给动态链接器(通常是 ld-linux.so),该链接器负责解析该函数的实际地址。这时动态链接器会更新 PLT 中的表项,将实际地址填入,以便后续调用可以直接跳转到该地址。

  4. 性能优势:使用 PLT,可以在不需要提前链接所有外部符号的情况下启动和运行程序,简化了链接过程并支持共享库的动态更新

当程序第一次调用 动态库函数add 时,它会跳转到 add 的 PLT 条目,这样动态链接器会处理真正的地址解析。

可以使用 objdump 或 gdb 等工具查看编译后的程序的 PLT 表。例如,使用以下命令:

objdump -D test.shared | grep add

 

 这是运行结果截出来的两段内容,这就是程序跟add有关的PLT表,显示了add在PLT中的地址

因为调用动态库的函数比程序文件自己的函数慢,所以有些地方会描述动态库为延迟绑定

结论:生成.o文件时要求生成与位置无关的代码,也就是加-fPIC选项的目的。


假设我们有多个目标文件,每个文件都有一个全局变量count。链接器会将这些变量合并,只保留一份,并分配一个内存地址,之后所有对count的引用将在运行时替换为这个地址。


感谢大家!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值