读书-程序员的自我修养-链接、封装与库(15: 第六章:可执行文件的装载与进程(3)进程虚存空间分布

1. ELF 文件链接视图和执行视图

1.1 问题出现–多段不整页映射浪费空间

当一个可执行文件中包含不止代码段,数据段,BSS段等,所以映射到进行虚拟空间有很多段。那么,当段数量增多时,就会产生空间浪费问题。
ELF文件映射是按照页为单位进行的,每个段在影射时的长度都是系统页长度的整数倍。如果不是整数倍,那么多余部分也会占用一个页。
一个ELF文件中有十几个段,那么空间浪费可想而知。

那么有没有办法解决这种内存浪费呢?

1.2 解决问题–同权限的段合并Segment映射

1.2.1 ELF段的三种权限

ELF段的权限基本上是三种:可读,可写,可执行

  1. 以代码段为代表的权限为可读可执行的段
  2. 以数据段和BSS段为代表的段为可读可写的段
  3. 以只读数据为代表的段为只读的段

1.2.2 解决方案

那么我们可以找到一个简单的方案就是:对于相同权限的段,把他们合并到一起当做一个段进行映射。
图

1.2.3 Segment

  1. Segment概念:
    ELF可执行文件引入了一个概念叫做: Segment
    一个 Segment 包含一个或者多个属性类似的 Section。

  2. Segment本质:
    Segment从装载角度重新划分了ELF的各个段。
    系统按照 Segment 来影射可执行文件的。
    相同属性的 section 被归类到一个 Segment,并且映射到同一个VMA中。

  3. 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叫做虚拟段。

  1. 在OS里面,VMA 被用来映射可执行文件中的各个 Segment。
  2. 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 总结-进程虚拟地址空间

  1. 操作系统通过给进程空间划分出一个个VMA来管理进程的虚拟空间
  2. 基本原则是将相同权限属性的,有相同映像文件的映射成一个 VMA
  3. 一个进程基本上可以分为如下几种 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

在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lqy971966/article/details/107029960