读书-程序员的自我修养-链接、封装与库(15: 第六章:可执行文件的装载与进程(3)进程虚存空间分布)
1. ELF 文件链接视图和执行视图
1.1 问题出现–多段不整页映射浪费空间
当一个可执行文件中包含不止代码段,数据段,BSS段等,所以映射到进行虚拟空间有很多段。那么,当段数量增多时,就会产生空间浪费问题。
ELF文件映射是按照页为单位进行的,每个段在影射时的长度都是系统页长度的整数倍。如果不是整数倍,那么多余部分也会占用一个页。
一个ELF文件中有十几个段,那么空间浪费可想而知。
那么有没有办法解决这种内存浪费呢?
1.2 解决问题–同权限的段合并Segment映射
1.2.1 ELF段的三种权限
ELF段的权限基本上是三种:可读,可写,可执行
- 以代码段为代表的权限为可读可执行的段
- 以数据段和BSS段为代表的段为可读可写的段
- 以只读数据为代表的段为只读的段
1.2.2 解决方案
那么我们可以找到一个简单的方案就是:对于相同权限的段,把他们合并到一起当做一个段进行映射。
1.2.3 Segment
-
Segment概念:
ELF可执行文件引入了一个概念叫做: Segment
一个 Segment 包含一个或者多个属性类似的 Section。 -
Segment本质:
Segment从装载角度重新划分了ELF的各个段。
系统按照 Segment 来影射可执行文件的。
相同属性的 section 被归类到一个 Segment,并且映射到同一个VMA中。 -
Segment 和 Section。
很难将它们两个分开,他们都可以翻译成段的概念。
从链接角度看,ELF文件是按照 Section 存储的;
从装载角度,ELF文件由可以按照 Segment 进行划分。
1.2.4 sectionMap.elf 例子说明
sectionMap.c
#include<stdlib.h>
int main()
{
while(1){
sleep(1000);
}
return 0;
}
gcc -static sectionMap.c -o sectionMap.elf
readelf -S sectionMap.elf //查看ELF头信息的段信息
root@ubuntu:/home/6Chapter# readelf -S sectionMap.elf
There are 33 section headers, starting at offset 0xdd568:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.ABI-tag NOTE 0000000000400190 00000190
0000000000000020 0000000000000000 A 0 0 4
[ 2] .note.gnu.build-i NOTE 00000000004001b0 000001b0
0000000000000024 0000000000000000 A 0 0 4
[ 3] .rela.plt RELA 00000000004001d8 000001d8
00000000000000f0 0000000000000018 AI 0 24 8
[ 4] .init PROGBITS 00000000004002c8 000002c8
000000000000001a 0000000000000000 AX 0 0 4
[ 5] .plt PROGBITS 00000000004002f0 000002f0
00000000000000a0 0000000000000000 AX 0 0 16
[ 6] .text PROGBITS 0000000000400390 00000390
000000000009d9a4 0000000000000000 AX 0 0 16
[ 7] __libc_freeres_fn PROGBITS 000000000049dd40 0009dd40
0000000000002529 0000000000000000 AX 0 0 16
[ 8] __libc_thread_fre PROGBITS 00000000004a0270 000a0270
00000000000000de 0000000000000000 AX 0 0 16
[ 9] .fini PROGBITS 00000000004a0350 000a0350
0000000000000009 0000000000000000 AX 0 0 4
[10] .rodata PROGBITS 00000000004a0360 000a0360
000000000001d224 0000000000000000 A 0 0 32
[11] __libc_subfreeres PROGBITS 00000000004bd588 000bd588
0000000000000050 0000000000000000 A 0 0 8
[12] __libc_atexit PROGBITS 00000000004bd5d8 000bd5d8
0000000000000008 0000000000000000 A 0 0 8
[13] .stapsdt.base PROGBITS 00000000004bd5e0 000bd5e0
0000000000000001 0000000000000000 A 0 0 1
[14] __libc_thread_sub PROGBITS 00000000004bd5e8 000bd5e8
0000000000000008 0000000000000000 A 0 0 8
[15] .eh_frame PROGBITS 00000000004bd5f0 000bd5f0
000000000000af8c 0000000000000000 A 0 0 8
[16] .gcc_except_table PROGBITS 00000000004c857c 000c857c
00000000000000a3 0000000000000000 A 0 0 1
[17] .tdata PROGBITS 00000000006c8eb8 000c8eb8
0000000000000020 0000000000000000 WAT 0 0 8
[18] .tbss NOBITS 00000000006c8ed8 000c8ed8
0000000000000030 0000000000000000 WAT 0 0 8
[19] .init_array INIT_ARRAY 00000000006c8ed8 000c8ed8
0000000000000010 0000000000000000 WA 0 0 8
[20] .fini_array FINI_ARRAY 00000000006c8ee8 000c8ee8
0000000000000010 0000000000000000 WA 0 0 8
[21] .jcr PROGBITS 00000000006c8ef8 000c8ef8
0000000000000008 0000000000000000 WA 0 0 8
[22] .data.rel.ro PROGBITS 00000000006c8f00 000c8f00
00000000000000e4 0000000000000000 WA 0 0 32
[23] .got PROGBITS 00000000006c8fe8 000c8fe8
0000000000000010 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 00000000006c9000 000c9000
0000000000000068 0000000000000008 WA 0 0 8
[25] .data PROGBITS 00000000006c9080 000c9080
0000000000001ad0 0000000000000000 WA 0 0 32
[26] .bss NOBITS 00000000006cab60 000cab50
0000000000001878 0000000000000000 WA 0 0 32
[27] __libc_freeres_pt NOBITS 00000000006cc3d8 000cab50
0000000000000030 0000000000000000 WA 0 0 8
[28] .comment PROGBITS 0000000000000000 000cab50
0000000000000034 0000000000000001 MS 0 0 1
[29] .note.stapsdt NOTE 0000000000000000 000cab84
0000000000000f18 0000000000000000 0 0 4
[30] .shstrtab STRTAB 0000000000000000 000dd3fd
0000000000000169 0000000000000000 0 0 1
[31] .symtab SYMTAB 0000000000000000 000cbaa0
000000000000b0e8 0000000000000018 32 711 8
[32] .strtab STRTAB 0000000000000000 000d6b88
0000000000006875 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
root@ubuntu:/home/6Chapter#
由上可知**,elf一共有33个段 Secion。**
readelf -l sectionMap.elf
显示程序头表信息,包扩有几个段,每个段的属性,以及每个段中包含有哪几个节(Section)
root@ubuntu-admin-a1:/home/6Chapter# readelf -l sectionMap.elf
Elf file type is EXEC (Executable file)
Entry point 0x400890
There are 6 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000c861f 0x00000000000c861f R E 200000
LOAD 0x00000000000c8eb8 0x00000000006c8eb8 0x00000000006c8eb8
0x0000000000001c98 0x0000000000003550 RW 200000
NOTE 0x0000000000000190 0x0000000000400190 0x0000000000400190
0x0000000000000044 0x0000000000000044 R 4
TLS 0x00000000000c8eb8 0x00000000006c8eb8 0x00000000006c8eb8
0x0000000000000020 0x0000000000000050 R 8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x00000000000c8eb8 0x00000000006c8eb8 0x00000000006c8eb8
0x0000000000000148 0x0000000000000148 R 1
Section to Segment mapping:
Segment Sections...
00 .note.ABI-tag .note.gnu.build-id .rela.plt .init .plt .text __libc_freeres_fn __libc_thread_freeres_fn .fini .rodata __libc_subfreeres __libc_atexit .stapsdt.base __libc_thread_subfreeres .eh_frame .gcc_except_table
01 .tdata .init_array .fini_array .jcr .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs
02 .note.ABI-tag .note.gnu.build-id
03 .tdata .tbss
04
05 .tdata .init_array .fini_array .jcr .data.rel.ro .got
root@ubuntu-admin-a1:/home/6Chapter#
由上可知,这个可执行中共有6个 Segment。
两个LOAD和NOTE,TLS,GNU_STACK,GNU_RELRO。
其中,只有LOAD段会映射到虚拟空间中。其他段在装载时起辅助作用。
映射图:
2. 堆和栈
2.1 VMA 的作用
虚拟内存区域(VMA: Virtual Memory Area):
linux将进程虚拟空间中的一个段叫做虚拟内存区域。windows叫做虚拟段。
- 在OS里面,VMA 被用来映射可执行文件中的各个 Segment。
- OS通过使用 VMA 来对进程的地址空间进行管理。
2.2 例子-查看进程虚拟空间分布
我们知道进程在执行的时候还需要用到堆栈等空间。
实际上,他们在进程的虚拟空间中的表现也是以VMA 的形式存在的。
堆栈都分别对应一个VMA
root@ubuntu-admin-a1:/home/6Chapter# ./sectionMap.elf &
[1] 2858
root@ubuntu-admin-a1:/home/6Chapter# cat /proc/2858/map
map_files/ maps
root@ubuntu-admin-a1:/home/6Chapter# cat /proc/2858/maps
00400000-004c9000 r-xp 00000000 08:01 280249 /home/6Chapter/sectionMap.elf
006c8000-006cb000 rw-p 000c8000 08:01 280249 /home/6Chapter/sectionMap.elf
006cb000-006cd000 rw-p 00000000 00:00 0
017b1000-017d4000 rw-p 00000000 00:00 0 [heap]
7ffe930e7000-7ffe93108000 rw-p 00000000 00:00 0 [stack]
7ffe93138000-7ffe9313a000 r--p 00000000 00:00 0 [vvar]
7ffe9313a000-7ffe9313c000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
root@ubuntu-admin-a1:/home/6Chapter#
上面每列:
地址 权限 偏移 主设备号和次设备号 节点号 映像文件的路径
可以看到该进程有7个 VMA,只有前两个是映射到可执行文件中的两个Segment。
其他几个段的文件所在设备号和次设备号都是0,则表示它们没有映射到文件中,这种VMA叫做匿名虚拟内存区域。
可以看到 heap 和 stack。 大小分别是: 140k和88k。
2.3 总结-进程虚拟地址空间
- 操作系统通过给进程空间划分出一个个VMA来管理进程的虚拟空间
- 基本原则是将相同权限属性的,有相同映像文件的映射成一个 VMA
- 一个进程基本上可以分为如下几种 VMA 区域:
代码 VMA ,权限只读,可执行;有映像文件
数据 VMA ,权限可读可,写可执行,有映像文件
堆 VMA , 权限可读可写,可执行,无映像文件,匿名,可向上扩展
栈 VMA , 权限可读可写,不可执行,无映像文件,匿名,可向下扩展
2.4 进程虚拟空间映射关系图
3. 堆的最大申请数量
linux下虚拟地址空间分给进程本身的是3GB/windows是2GB。
那么程序正真能分配多少呢?看下面的程序。
3.1 mallocMax.c 例子说明
#include<stdio.h>
#include<stdlib.h>
unsigned max=0;
int main()
{
unsigned blocksize[] = {1024*1024,1024,1};
int i,count;
for(i = 0; i < 3; i++){
for(count = 1;; count ++){
void *block = malloc( max + blocksize[i] * count);
if(block){
max = max + blocksize[i] * count;
free(block);
}else{
break;
}
}
}
printf("max malloc size = %u bytes\n", max);
}
在我的ubuntu虚拟机上运行的结果是:
max malloc size = 1358479110 bytes 大概是1.3个G
4. 段地址对齐
4.1 段地址对齐的缺点-碎片,浪费空间
由于地址空间的映射必须是4K页大小的整数倍。
对于可执行文件来说,它应该尽量地优化自己的空间和地址的安排,以节省空间。
由于这种段地址对齐,导致内部有很多碎片,浪费空间。
4.2 解决-共享物理页面
为了解决这个问题,unix系统采用了一个很取巧的方法,就是让那些各个段接壤的部分共享一个物理页面。
5. 进程栈初始化
进程启动的时候,需要知道一些进程运行的环境,如环境变量和进行的运行参数。
很常见的做法就是OS在进程启动前将这些信息提前保存在进程的虚拟空间栈中,也就是VMA中的stack VMA中。
5.1 linux 进程初始堆栈图
这里假设,程序中有两个环境变量:
HOME=/home/user
PATH=/usr/bin
程序参数: ./prog 123