一个可执行文件被装载到内存变成程序后(进程和程序的区别在于一个是静态的一个是动态的,程序就是菜谱,进程就是厨师参考菜谱做菜的过程),拥有自己独立的地址空间。该地址空间是一个虚拟的地址空间,在该进程看来该空间内包含内核和自身,32bit系统该空间的大小是4GB,64bit系统是2的64次方-1bit。也就是说,一个程序实际上能够用到的虚拟内存空间实际上是小于理论值的,因为操作系统需要占用。
静态装入:将程序执行时所需要的指令和数据全部载入到内存中。
动态装入:利用程序的局部性原理,仅仅将程序中需要运行的部分装入,类似虚拟页的换入换出。
动态转入的两种实现:
一般进程的创建需要做三件事情:
创建进程时,由于可执行文件往往存在多个段,如果针对每个段都单独进行内存映射,这样很容易造成内存的浪费。比如数据段可能就2个字节,但是如果依然按照一个页大小分配,剩余的4k-2的内存都浪费了。如果站在操作系系统的角度看,操作系统并不关心不同段的内容,只关心段的权限(可读?可写?可执行?)。因此可以通过将相同权限的段合并统一进行映射减少内存的浪费。在ELF中多个段的合并的集合就是一个Segment,而Segment中每个段都是相同类型的Section。
下面利用下面简单的程序展示如何合并
//编译命令clang -static main.c -o main.elf
#include
int main(){
while(1){
sleep(1000);
}
return 0;
}
使用readelf -S main.elf
查看详细的Section。
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
readelf: Warning: [ 2]: Link field (0) should index a symtab section.
[ 2] .rela.plt RELA 00000000004001b0 000001b0 0000000000000228 0000000000000018 AI 0 19 8
[ 3] .init PROGBITS 00000000004003d8 000003d8 0000000000000017 0000000000000000 AX 0 0 4
[ 4] .plt PROGBITS 00000000004003f0 000003f0 00000000000000b8 0000000000000000 AX 0 0 8
[ 5] .text PROGBITS 00000000004004b0 000004b0 000000000008f3d0 0000000000000000 AX 0 0 16
[ 6] __libc_freeres_fn PROGBITS 000000000048f880 0008f880 0000000000001523 0000000000000000 AX 0 0 16
[ 7] __libc_thread_fre PROGBITS 0000000000490db0 00090db0 00000000000010eb 0000000000000000 AX 0 0 16
[ 8] .fini PROGBITS 0000000000491e9c 00091e9c 0000000000000009 0000000000000000 AX 0 0 4
[ 9] .rodata PROGBITS 0000000000491ec0 00091ec0 000000000001926c 0000000000000000 A 0 0 32
[10] .stapsdt.base PROGBITS 00000000004ab12c 000ab12c 0000000000000001 0000000000000000 A 0 0 1
[11] .eh_frame PROGBITS 00000000004ab130 000ab130 000000000000a578 0000000000000000 A 0 0 8
[12] .gcc_except_table PROGBITS 00000000004b56a8 000b56a8 000000000000008e 0000000000000000 A 0 0 1
[13] .tdata PROGBITS 00000000006b6120 000b6120 0000000000000020 0000000000000000 WAT 0 0 8
[14] .tbss NOBITS 00000000006b6140 000b6140 0000000000000040 0000000000000000 WAT 0 0 8
[15] .init_array INIT_ARRAY 00000000006b6140 000b6140 0000000000000010 0000000000000008 WA 0 0 8
[16] .fini_array FINI_ARRAY 00000000006b6150 000b6150 0000000000000010 0000000000000008 WA 0 0 8
[17] .data.rel.ro PROGBITS 00000000006b6160 000b6160 0000000000002d94 0000000000000000 WA 0 0 32
[18] .got PROGBITS 00000000006b8ef8 000b8ef8 00000000000000f8 0000000000000000 WA 0 0 8
[19] .got.plt PROGBITS 00000000006b9000 000b9000 00000000000000d0 0000000000000008 WA 0 0 8
[20] .data PROGBITS 00000000006b90e0 000b90e0 0000000000001af0 0000000000000000 WA 0 0 32
[21] __libc_subfreeres PROGBITS 00000000006babd0 000babd0 0000000000000048 0000000000000000 WA 0 0 8
[22] __libc_IO_vtables PROGBITS 00000000006bac20 000bac20 00000000000006a8 0000000000000000 WA 0 0 32
[23] __libc_atexit PROGBITS 00000000006bb2c8 000bb2c8 0000000000000008 0000000000000000 WA 0 0 8
[24] __libc_thread_sub PROGBITS 00000000006bb2d0 000bb2d0 0000000000000008 0000000000000000 WA 0 0 8
[25] .bss NOBITS 00000000006bb2e0 000bb2d8 00000000000016f8 0000000000000000 WA 0 0 32
[26] __libc_freeres_pt NOBITS 00000000006bc9d8 000bb2d8 0000000000000028 0000000000000000 WA 0 0 8
[27] .comment PROGBITS 0000000000000000 000bb2d8 000000000000005f 0000000000000001 MS 0 0 1
[28] .note.stapsdt NOTE 0000000000000000 000bb338 0000000000001638 0000000000000000 0 0 4
[29] .symtab SYMTAB 0000000000000000 000bc970 000000000000a980 0000000000000018 30 677 8
[30] .strtab STRTAB 0000000000000000 000c72f0 0000000000006916 0000000000000000 0 0 1
[31] .shstrtab STRTAB 0000000000000000 000cdc06 0000000000000163 0000000000000000 0 0 1
使用readelf -l main.elf
查看合并后的Segment。
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000b5736 0x00000000000b5736 R E 0x200000
LOAD 0x00000000000b6120 0x00000000006b6120 0x00000000006b6120 0x00000000000051b8 0x00000000000068e0 RW 0x200000
NOTE 0x0000000000000190 0x0000000000400190 0x0000000000400190 0x0000000000000020 0x0000000000000020 R 0x4
TLS 0x00000000000b6120 0x00000000006b6120 0x00000000006b6120 0x0000000000000020 0x0000000000000060 R 0x8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x00000000000b6120 0x00000000006b6120 0x00000000006b6120 0x0000000000002ee0 0x0000000000002ee0 R 0x1
Section to Segment mapping:
Segment Sections...
00 .note.ABI-tag .rela.plt .init .plt .text __libc_freeres_fn __libc_thread_freeres_fn .fini .rodata .stapsdt.base .eh_frame .gcc_except_table
01 .tdata .init_array .fini_array .data.rel.ro .got .got.plt .data __libc_subfreeres __libc_IO_vtables __libc_atexit __libc_thread_subfreeres .bss __libc_freeres_ptrs
02 .note.ABI-tag
03 .tdata .tbss
04
05 .tdata .init_array .fini_array .data.rel.ro .got
从上面的结果能够看出32个Section被映射成了5个Segment,而我们只关心其中涉及LOAD的Segment,可以看到分别被合并为00和01两个Segment。
对应到ELF文件中(ELF文件和动态库文件,目标文件不涉及转载不存在)都有一个程序表头来表示Segment的信息,其基本结构如下:
typedef struct elf32_phdr{
Elf32_Word p_type; //段的类型,LOAD,DYNAMIC等
Elf32_Off p_offset; //段在文件中的偏移量
Elf32_Addr p_vaddr; //段的物理地址
Elf32_Addr p_paddr; //段的虚拟地址
Elf32_Word p_filesz; //段在文件中的大小
Elf32_Word p_memsz; //段在内存中的大小
Elf32_Word p_flags; //读写执行标记
Elf32_Word p_align; //段的对齐
} Elf32_Phdr;
在操作系统里面,VMA(虚拟内存区域)除了被用来映射可执行文件中的各个Segment以外,它还可以有其他的作用,操作系统通过使用VMA来对进程的地址空间进行管理。我们知道进程在执行的时候它还需要用到堆和栈等空间,事实上它们在进程的虚拟空间中的表现也是以VMA的形式存在的,很多情况下,一个进程中的堆和栈分别都有一个对应的VMA。
执行刚才编译的可执行文件,查看linux系统中的proc目录下的maps就能看到VMA的映射情况。
➜ Desktop ./main.elf &
[1] 265
➜ Desktop cat /proc/265/maps
00400000-004b5000 r-xp 00000000 00:00 207690 /mnt/c/Users/ares/Desktop/main.elf
004b5000-004b6000 r-xp 000b5000 00:00 207690 /mnt/c/Users/ares/Desktop/main.elf
006b6000-006bc000 rw-p 000b6000 00:00 207690 /mnt/c/Users/ares/Desktop/main.elf
006bc000-006bd000 rw-p 00000000 00:00 0
01d4f000-01d72000 rw-p 00000000 00:00 0 [heap]
7fffcfdf5000-7fffd05f5000 rw-p 00000000 00:00 0 [stack]
7fffd0baa000-7fffd0bab000 r-xp 00000000 00:00 0 [vdso]
能够看到栈空间大小是1Mb,堆是17Kb。
根据上面的对比,可以将VMA划分为下面几种(划分按照我的机器上的结果划分,而不是按照书上来的):
Linux通过bash创建进程的方式:
fork()
创建当前进程的复制品,当前进程也就是父进程一般是bash
;execve
系列函数装载对应的可执行文件;.interp
段,设置动态链接的路径;PE文件的装载跟ELF有所不同,由于PE文件中,所有段的起始地址都是页的倍数,段的长度如果不是页的整数倍,那么在映射时向上补齐到页的整数倍,我们也可以简单地认为在32位的PE文件中,段的起始地址 和长度都是4096字节的整数倍。由于这个特点,PE文件的映射过错会比ELF简单得多,因为它无需考虑如ELF里面诸多段地址对齐之类的问题,虽然这样会浪费一些磁盘和内存空间。PE可执行文件的段的数量一般很少,不像ELF中经常有十多个"Section",最后不得不使用“Segment”的概念把它们合并到一起装载,PE文件中,链接器在生产可执行文件时,往往将所有的段尽可能地合并,所以一般只有代码段、数据段、只读数据段和BSS等为数不多的几个段。