########################
# 6、可执行文件的装载与进程
########################
# 进程虚拟地址空间
程序(或者狭义上讲可执行文件)是一个静态的概念,它就是一些预先编译好的指令和数据集合的一个文件;
进程则是一个动态的概念,它是程序运行时的一个过程。
每个程序被运行起来以后,它将拥有自己独立的虚拟地址空间(Virtual Address Space),这个虚拟地址空间的大小由计算机的硬件平台决定,具体地说是由CPU的位数决定。
32的硬件平台决定了虚拟地址空间的地址为0到2^32 - 1,即0x00000000~0xFFFFFFF,也就是4G虚拟空间。
64位的硬件平台具有64位寻址能力,它的虚拟空间地址达到了2^64字节。
一般来说,C语言指针大小的位数与虚拟空间的位数相同,如32位平台下的指针为32位,即4字节;64位平台下的指针为64位,即8字节。
32位平台上的4GB虚拟空间被划分为两部分:从0xC0000000到0xFFFFFFFF共1GB留给操作系统本身用;从0x00000000到0xBFFFFFFF共3GB空间留给进程使用。
# 装载的方式
程序执行时所需要的指令和数据必须在内存中才能够正常运行,为了在不添加内存的情况下让更多的程序运行起来,我们将程序最常用的部分驻留在内存中,
将一些不常用的数据存放在磁盘里面,这就是动态装入的基本原理。
覆盖装入(Overlay)和
页映射(Paging)是两种典型的动态装载方法。
页映射是虚拟存储机制的一部分,将内存和所有磁盘中的数据和指令按照"页(Page)"为单位划分成若干个页,以后所有的装载和操作的单位是就是页。
假设32位机器有16KB的内存,每个页大小为4096字节则共有4个页,编号为F0~F3。假设程序所有的指令和数据总和为32KB,那么程序总共被分为8个页,编号为P0~P7。
# 从操作系统角度看可执行文件的装载
进程的建立:
(1)创建一个独立的虚拟地址空间。
虚拟空间---物理内存的映射关系。
创建虚拟地址空间实际上只是分配一个页目录,甚至不设置页映射关系,这些映射关系等到后面程序发生页错误的时候再进行设置。
(2)读取可执行文件,并且建立虚拟空间与可执行文件的映射关系。
虚拟空间---可执行文件的映射关系。
这种映射关系只是保存在操作系统内的
一个数据结构,Linux中将进程虚拟空间中的一个段叫做虚拟内存区域(VMA,Virtual Memory Area)。
由于可执行文件在装载时实际上是被映射的虚拟空间,所以可执行文件很多时候又被叫做映射文件(Image)。
假设ELF可执行文件只有一个代码段".text",它的虚拟地址为0x08048000,它在文件中的大小为0x000e1,对齐为0x1000。由于虚拟存储的页映射是以页为单位的,
32位CPU一般为4096字节,所以32位ELF的对齐粒度为0x1000。由于.text段大小不到一个页,考虑到对齐所以占用一个段。
(3)将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。操作系统通过设置CPU的指令寄存器将将控制权转交给进程,由进程开始执行。
页错误
假设在上面的例子中,程序的入口地址为0x08048000,即刚好是.text段的起始地址。当CPU开始打算执行这个地址的指令时,发现页面0x08048000~0x08049000是个空页面,于是它认为这是一个页错误(Page Fault)。CPU将控制权交给操作系统,操作系统有专门的页错误处理程序来处理这种情况。
操作系统将查询这个数据结构(
虚拟空间---可执行文件的映射关系所保存的),找到空页面所在的VMA,计算出相应的页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射关系,然后把控制权再还回给进程,进程从刚才页错误的位置重新开始执行。
# 进程虚存空间分布
ELF文件被映射时,是以系统的页长度作为单位的,那么每个段在映射时的长度应该都是系统页长度的整数倍。如果不是,那么多余部分也将占用一个页。
一个ELF文件中往往有十几个段,那么内存空间的浪费非常大。
操作系统在装载可执行文件时,只关心一些跟装载相关的问题,最主要的是段的权限(可读、可写、可执行)。
ELF文件中,段的权限主要有几种组合:
(1)代码段:可读可执行的段
(2)数据段和BSS段:可读可写的段
(3)只读数据段:只读的段
对于相同权限的段,把它们合并到一起当作一个段进行映射。
从链接的角度看,ELF文件是按"Section"存储的;从装载的角度看,ELF文件可以按照"Segment"划分。
一个"Segment"包含一个或多个属性类似的"Section"。
// SectionMapping.c
#include <unistd.h>
#include <stdlib.h>
int main()
{
while(1)
{
sleep(1000);
}
return 0;
}
// 使用静态连接的方式将其编译连接成可执行文件
# gcc -static SectionMapping.c -o SectionMapping.elf
// 可以看到这个可执行文件中总共有33个段(Section),描述"Section"属性的结构叫做段表。
# readelf -S SectionMapping.elf // 查看可执行文件的 Section
There are 33 section headers, starting at offset 0xde570:
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
000000000009e4e4 0000000000000000 AX 0 0 16
[ 7] __libc_freeres_fn PROGBITS 000000000049e880 0009e880
0000000000002529 0000000000000000 AX 0 0 16
[ 8] __libc_thread_fre PROGBITS 00000000004a0db0 000a0db0
00000000000000de 0000000000000000 AX 0 0 16
[ 9] .fini PROGBITS 00000000004a0e90 000a0e90
0000000000000009 0000000000000000 AX 0 0 4
[10] .rodata PROGBITS 00000000004a0ea0 000a0ea0
000000000001d244 0000000000000000 A 0 0 32
[11] __libc_subfreeres PROGBITS 00000000004be0e8 000be0e8
0000000000000050 0000000000000000 A 0 0 8
[12] __libc_atexit PROGBITS 00000000004be138 000be138
0000000000000008 0000000000000000 A 0 0 8
[13] .stapsdt.base PROGBITS 00000000004be140 000be140
0000000000000001 0000000000000000 A 0 0 1
[14] __libc_thread_sub PROGBITS 00000000004be148 000be148
0000000000000008 0000000000000000 A 0 0 8
[15] .eh_frame PROGBITS 00000000004be150 000be150
000000000000af8c 0000000000000000 A 0 0 8
[16] .gcc_except_table PROGBITS 00000000004c90dc 000c90dc
00000000000000a3 0000000000000000 A 0 0 1
[17] .tdata PROGBITS 00000000006c9eb8 000c9eb8
0000000000000020 0000000000000000 WAT 0 0 8
[18] .tbss NOBITS 00000000006c9ed8 000c9ed8
0000000000000030 0000000000000000 WAT 0 0 8
[19] .init_array INIT_ARRAY 00000000006c9ed8 000c9ed8
0000000000000010 0000000000000000 WA 0 0 8
[20] .fini_array FINI_ARRAY 00000000006c9ee8 000c9ee8
0000000000000010 0000000000000000 WA 0 0 8
[21] .jcr PROGBITS 00000000006c9ef8 000c9ef8
0000000000000008 0000000000000000 WA 0 0 8
[22] .data.rel.ro PROGBITS 00000000006c9f00 000c9f00
00000000000000e4 0000000000000000 WA 0 0 32
[23] .got PROGBITS 00000000006c9fe8 000c9fe8
0000000000000010 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 00000000006ca000 000ca000
0000000000000068 0000000000000008 WA 0 0 8
[25] .data PROGBITS 00000000006ca080 000ca080
0000000000001ad0 0000000000000000 WA 0 0 32
[26] .bss NOBITS 00000000006cbb60 000cbb50
0000000000001878 0000000000000000 WA 0 0 32
[27] __libc_freeres_pt NOBITS 00000000006cd3d8 000cbb50
0000000000000030 0000000000000000 WA 0 0 8
[28] .comment PROGBITS 0000000000000000 000cbb50
0000000000000034 0000000000000001 MS 0 0 1
[29] .note.stapsdt NOTE 0000000000000000 000cbb84
0000000000000f18 0000000000000000 0 0 4
[30] .shstrtab STRTAB 0000000000000000 000de401
0000000000000169 0000000000000000 0 0 1
[31] .symtab SYMTAB 0000000000000000 000ccaa0
000000000000b0e8 0000000000000018 32 711 8
[32] .strtab STRTAB 0000000000000000 000d7b88
0000000000006879 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)
// 查看ELF的"Segment",描述"Segment"的结构叫做程序头(Program Header),它描述了ELF文件该如何被操作系统映射到进程的虚拟空间。
# readelf -l SectionMapping.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
0x00000000000c917f 0x00000000000c917f R E 200000
LOAD 0x00000000000c9eb8 0x00000000006c9eb8 0x00000000006c9eb8
0x0000000000001c98 0x0000000000003550 RW 200000
NOTE 0x0000000000000190 0x0000000000400190 0x0000000000400190
0x0000000000000044 0x0000000000000044 R 4
TLS 0x00000000000c9eb8 0x00000000006c9eb8 0x00000000006c9eb8
0x0000000000000020 0x0000000000000050 R 8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x00000000000c9eb8 0x00000000006c9eb8 0x00000000006c9eb8
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
可以看到这个可执行文件中共有6个segment,从装载的角度看,
我们只关心两个"LOAD"类型的Segment,因为只有它是需要被映射的,其他都是在装载时起辅助作用的。
可读可执行的段,它们被统一映射到VMA0;可读可写的段,它们被映 射到了VMA1;还有一部分段在程序装载时没有被映射的,它们是一些包含调试信息和字符串表等段,
这些段在程序执行时没有用,所以不需要被映射。所有相同属性的"Section"被归类到一个"Segment",并且映射到同一个VMA。
堆和栈
操作系统通过使用VMA来对进程的地址空间进行管理,进程在执行的时候还需要用到栈(Stack)、堆(Heap)等空间,
它们在进程的虚拟空间中的表现也是以VMA的形式存在的。很多情况下,一个进程中的栈和堆分别都有一个对应的VMA。
通过"/proc"来查看进程的虚拟空间分布:
第一列是VMA的地址范围;第二列是VMA的权限,"p"表示私有,"s"表示共享;
第三列是偏移,表示VMA对应的Segment在映像文件中的偏移;
第四列表示映像文件所在设备的主设备号和次设备号;
第五列表示映像文件的节点号;
第六列是映像文件的路径。
# ./SectionMapping.elf &
[1] 3519
# cat /proc/3519/maps
00400000-004ca000 r-xp 00000000 08:01 1835499 /home/xl/xiaoloaw_study/compile_study/partThree/SectionMapping.elf
006c9000-006cc000 rw-p 000c9000 08:01 1835499 /home/xl/xiaoloaw_study/compile_study/partThree/SectionMapping.elf
006cc000-006ce000 rw-p 00000000 00:00 0
01f95000-01fb8000 rw-p 00000000 00:00 0 [heap]
7fff4e1e1000-7fff4e202000 rw-p 00000000 00:00 0 [stack]
7fff4e24a000-7fff4e24c000 r--p 00000000 00:00 0 [vvar]
7fff4e24c000-7fff4e24e000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
只有前两个是映射到可执行文件中的两个Segment。
其他段的文件所在设备主设备号和次设备号以及文件节点号都是0,则表示它们没有映射到文件中,这种VMA叫做匿名虚拟内存区域。
堆(Heap)和栈(Stack) 它们的大小分别为140KB(01fb8000 - 01f95000=0x23000=140KB)和 132KB(7fff4e202000 - 7fff4e1e1000=0x21000=132KB)
每个线程都有属于自己的堆栈,对于单线程的程序来说,这个VMA堆栈就全部归它使用。
堆是在用户空间中(小于0xC0000000的地址),栈是在内核空间中(大于0xC0000000的地址)。// 需要再确认一下
"vdso"是一个特殊的VMA,它的地址是在内核空间中,它是一个内核的模块,进程可以通过访问这个VMA来跟内核进行通信。
一个进程基本上可以分为如下几种VMA区域:
(1)代码VMA:权限只读、可执行;有映像文件。
(2)数据VMA:权限可读写、可执行;有映像文件。
(3)堆VMA:权限可读写、可执行;无映像文件,匿名,可向上扩展。
(4)栈VMA:权限可读写、不可执行;无映像文件,匿名,可向下扩展。