可执行文件的装载与进程
在第一章中讲到,程序直接使用物理内存地址有以下缺点:
- 地址空间不隔离。恶意程序可以很容易的改写其他程序的数据。
- 内存使用效率低。一个程序需要执行时,需要将整个程序装入内存之中。
- 程序运行地址的不确定。因为无法保证每次都将程序加载到相同的地址,会涉及到程序重定位的问题。
解决方法是使用分页的方式:
操作系统将内存分成大小固定的页(最常用的页大小为4kb)。进程使用虚拟地址,虚拟地址也按页分割。由操作系统负责将虚拟地址的页映射到物理地址。
cpu-(virtual address)->MMU-(physical address)->Physical memory
疑问:
既然所有进程都使用虚拟地址,那么cpu建起一样的虚拟地址,是谁保存了额外的信息,将不同进程的虚拟地址映射到正确的物理地址?
大致理解:
内核内存管理模块保存了额外的信息。cpu在执行某个进程时,内核负责将MMU的对应的寄存器修改为正确的值,确保MMU将虚拟地址与寄存器的值结合起来得到正确的物理地址。
6.1 进程虚拟地址空间
每个进程拥有独立的虚拟地址空间。虚拟地址空间的大小由CPU的位数确定。32位cpu的最大虚拟地址空间为2^32字节。
并且这部分虚拟空间还有一部分是给操作系统预留的,应用程序访问此部分地址为非法操作。
疑问:
既然是虚拟地址,为什么还需要给操作系统预留呢?这不是相当于每个进程都给操作系统预留了部分空间???
PAE
在32位的CPU下,程序无法使用超过4GB的虚拟地址空间。但是却有办法使用超过4GB的物理空间。常用的方法叫窗口映射,在windows下叫做AWE(address windowing extensions),linux采用mmap()系统调用来实现。
以下理解参考:
物理地址扩展
物理地址扩展(PAE)分页机制
虚拟地址映射到物理地址,在传统的32位保护模式中,x86处理器使用一种两级的转换方案。
cr3寄存器指向一个4KB大的页目录。页目录包含1024个4KB大小页表,每个页表包含1024个页。所以共有2^20次方个页。
每个进程都有一个独立的页目录,因为拥有独立的页目录,从而也拥有了独立的虚拟地址空间。
在以上方案中,每个地址的大小都是4个字节,所以最大能使用的物理地址也只有4G。所以32位的cpu,要支持大于4G的物理内存,就必须使用PAE。
使用PAE扩展之后(设置CR4寄存器的第5位),地址变为8个字节。页目录和页表的大小没变,所以表示的项变少为一半。为了解决这个问题,增加了一级:cr3不再指向页目录表,而是指向一个大小为4的页目录指针表。(32字节对齐,所以只需要27位从便足够表示)
为了寻址超过4GB的空间,就需要对cr3设置不同的值。
通过设置cr3不同的值,就可以访问总共超过4GB大小的物理空间。
只有内核能够修改进程的页表,所以用户态下运行的进程不能使用大于4GB的物理空间。
6.2 装载的方式
6.2.1 覆盖装入
将内存管理的工作交给了程序员。
- 将模块按它们之间的调用依赖关系组织成树状结构。
- 从任何一个模块到树的根模块叫调用路径
- 禁止跨树间调用
因为子树间有没有调用依赖关系,所以需要使用的最大内存比整个程序实际的内存要小。
6.2.2 页映射
将内存和磁盘上的数据都按页进行划分(最常见的页大小为4096)。操作系统不再需要将整个程序加载到内存中,而是缺少哪个页就加载哪个页。内存管理的事情完全由操作系统来完成,程序员不需要操心。
6.3 从操作系统看可执行文件的加载
在使用页映射的机制中,程序的某个页被加载到内存中的物理地址都是不确定的。如果程序中直接使用物理地址,那么每一个页加载之后,都需要对整个程序进行重定位。
现代操作系统使用虚拟地址进行操作,由MMU将虚拟地址映射为物理地址。
6.3.1 进程的建立
从操作系统的角度来看,一个进程最关键的特征是它拥有独立的虚拟地址空间。
在有虚拟存储的情况下,创建一个进程最开始只需要做3件事情:
- 创建虚拟地址空间。并不是创建空间,而是创建页映射函数所需要的数据结构,即页目录。
- 读取可执行文件文,并且建立虚拟地址空间与可执行文件的映射关系。第一步是虚拟空间到物理地址的映射,这一步所做的是虚拟空间与可执行文件的映射。当程序访问某个虚拟地址发生缺页错误时,操作系统分配物理内存页,并将物理内存页与虚拟地址映射起来。然后将可执行文件加载到对应的虚拟地址。显然,必须要保存虚拟地址与可执行文件的映射关系,才能由虚拟地址找到对应的可执行文件。操作系统具体用什么结构来保存这个映射关系呢?书中并没有提到。
- 将cpu指令寄存器设置成可执行文件入口,启动运行。
6.3.2 页错误
略
6.4 进程虚拟空间分布
6.4.1 ELF文件链接视图和执行视图
ELF文件被映射时,是以系统的页的长度作为单位。每个段在映射时的长度都应该是系统页的长度的整数倍。一般ELF可以执行文件都有十多个段,会造成相当的内存浪费。
解决方法就是对于相同权限的段,把它们合并到一起当作一个段进行映射。
所以ELF文件引入一个segment的概念,一个segment包含一个或多个属性类似的section。
链接器在把目标文件链接成可执行文件时,会尽量把相同权限属性的段分配在同一空间。
示例程序:
/*SectionMapping.c*/
#include <stdlib.h>
int main()
{
while(1)
{
sleep(1000);
}
return 0;
}
段表结构:
root@debian:~# readelf -S SectionMapping.elf
There are 31 section headers, starting at offset 0xb16c4:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .note.ABI-tag NOTE 080480f4 0000f4 000020 00 A 0 0 4
[ 2] .note.gnu.build-i NOTE 08048114 000114 000024 00 A 0 0 4
readelf: Warning: [ 3]: Link field (0) should index a symtab section.
[ 3] .rel.plt REL 08048138 000138 000078 08 AI 0 23 4
[ 4] .init PROGBITS 080481b0 0001b0 000023 00 AX 0 0 4
[ 5] .plt PROGBITS 080481e0 0001e0 0000f0 00 AX 0 0 16
[ 6] .text PROGBITS 080482d0 0002d0 073644 00 AX 0 0 16
[ 7] __libc_freeres_fn PROGBITS 080bb920 073920 000a6d 00 AX 0 0 16
[ 8] __libc_thread_fre PROGBITS 080bc390 074390 00009e 00 AX 0 0 16
[ 9] .fini PROGBITS 080bc430 074430 000014 00 AX 0 0 4
[10] .rodata PROGBITS 080bc460 074460 01a46c 00 A 0 0 32
[11] __libc_subfreeres PROGBITS 080d68cc 08e8cc 000028 00 A 0 0 4
[12] __libc_IO_vtables PROGBITS 080d6900 08e900 000354 00 A 0 0 32
[13] __libc_atexit PROGBITS 080d6c54 08ec54 000004 00 A 0 0 4
[14] __libc_thread_sub PROGBITS 080d6c58 08ec58 000004 00 A 0 0 4
[15] .eh_frame PROGBITS 080d6c5c 08ec5c 012c90 00 A 0 0 4
[16] .gcc_except_table PROGBITS 080e98ec 0a18ec 0000af 00 A 0 0 1
[17] .tdata PROGBITS 080eaf5c 0a1f5c 000010 00 WAT 0 0 4
[18] .tbss NOBITS 080eaf6c 0a1f6c 000018 00 WAT 0 0 4
[19] .init_array INIT_ARRAY 080eaf6c 0a1f6c 000008 04 WA 0 0 4
[20] .fini_array FINI_ARRAY 080eaf74 0a1f74 000008 04 WA 0 0 4
[21] .jcr PROGBITS 080eaf7c 0a1f7c 000004 00 WA 0 0 4
[22] .data.rel.ro PROGBITS 080eaf80 0a1f80 000070 00 WA 0 0 32
[23] .got.plt PROGBITS 080eb000 0a2000 000048 04 WA 0 0 4
[24] .data PROGBITS 080eb060 0a2060 000f20 00 WA 0 0 32
[25] .bss NOBITS 080ebf80 0a2f80 000e0c 00 WA 0 0 32
[26] __libc_freeres_pt NOBITS 080ecd8c 0a2f80 000018 00 WA 0 0 4
[27] .comment PROGBITS 00000000 0a2f80 00002d 01 MS 0 0 1
[28] .symtab SYMTAB 00000000 0a2fb0 007c20 10 29 846 4
[29] .strtab STRTAB 00000000 0aabd0 00699a 00 0 0 1
[30] .shstrtab STRTAB 00000000 0b156a 000159 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specif)
文件居然多达31个段。
描述section属性的结构叫做段表,描述segment的结构叫做程序头。描述ELF文件该如何被操作系统映射到内存空间。
root@debian:~# readelf -l SectionMapping.elf
Elf file type is EXEC (Executable file)
Entry point 0x804887f
There are 6 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0xa199b 0xa199b R E 0x1000
LOAD 0x0a1f5c 0x080eaf5c 0x080eaf5c 0x01024 0x01e48 RW 0x1000
NOTE 0x0000f4 0x080480f4 0x080480f4 0x00044 0x00044 R 0x4
TLS 0x0a1f5c 0x080eaf5c 0x080eaf5c 0x00010 0x00028 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
GNU_RELRO 0x0a1f5c 0x080eaf5c 0x080eaf5c 0x000a4 0x000a4 R 0x1
Section to Segment mapping:
Segment Sections...
00 .note.ABI-tag .note.gnu.build-id .rel.plt .init .plt .text __libc_freeres_fn __libc_thread_freeres_fn .fini .rodata __libc_subfreeres __libc_IO_vtables __libc_atexit __libc_thread_subfreeres .eh_frame .gcc_except_table
01 .tdata .init_array .fini_array .jcr .data.rel.ro .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
只有6个segment,其中两个需要装载。section到segement的对应关系也补显示了出来。
从section的角度来看ELF文件就是链接视图。从segment的角度来看就是执行视图。
ELF可执行文件和共享库文件中有一个专门的数据结构叫做程序头表,用来保存segment的信息。因为ELF目标文件不需要被装载,所以它没有程序头表。
程序头表是一个结构体数组:
typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;
结构体成员与readelf -l的输出一一对应。
但是文章没讲到,section与segment的对应关系是保存在什么数据结构里面的。
6.4.2 堆和栈
VMA除了被用来映射可执行文件的各个Segment外,还用来对进程和地址空间进行管理。
堆和栈都有对应的VMA。可以通过cat /proc/pid/maps查看。
一个进程基本上可以分为如下几种VMA区域:
代码VMA 权限只读,可执行。有映射文件
数据VMA 权限可读写,可执行。有映像文件
堆VMA 权限可读写,可执行。无映像文件,匿名,可向上扩展
栈VMA 权限可读写,不可执行 无映像文件 可向下扩展
6.4.3 堆的最大申请数量
用malloc测试,32位机器。作者最大能申请2.9GB左右。
每次运行结果可能不同,因为一些操作系统使用了ASLR技术,使得进程堆空间变小。
但是我在我的虚拟机上跑这个程序,只能申请到1.9G。
/*mallocTest1.c*/
#include <stdio.h>
#include <stdlib.h>
unsigned int maximum = 0;
int main(void)
{
unsigned blocksize[] = {1024 * 1024, 1024, 1};
void *block;
int i, count;
for(i = 0; i < 3; i++) {
for(count = 1; ; count++) {
block = malloc(maximum + blocksize[i] * count);
if (block) {
maximum = maximum + blocksize[i] * count;
free(block);
} else {
break;
}
}
}
printf("maximum malloc size = %u bytes.\n", maximum);
printf("maximum malloc size = %f MB\n",((float)maximum)/1024/1024);
printf("maximum malloc size = %f GB\n",((float)maximum)/1024/1024/1024);
}
测试结果:
maximum malloc size = 2021424851 bytes.
maximum malloc size = 1927.781006 MB
maximum malloc size = 1.882599 GB
root@debian:~# ./mallocTest
maximum malloc size = 2021432980 bytes.
maximum malloc size = 1927.788696 MB
maximum malloc size = 1.882606 GB
root@debian:~# ./mallocTest
maximum malloc size = 2021453807 bytes.
maximum malloc size = 1927.808594 MB
maximum malloc size = 1.882626 GB
root@debian:~# ./mallocTest
maximum malloc size = 2021494727 bytes.
maximum malloc size = 1927.847656 MB
maximum malloc size = 1.882664 GB
6.4.4 段地址对齐
待补充…