程序是一个静态的概念,是一些预先编译好的指令和数据集合的一个文件
进程是一个动态的概念,是程序运行时的一个过程
C语言指针大小的位数与虚拟空间的位数相同
#include
int main()
{
int a = 10;
int* p = &a;
printf("%ld\n",sizeof(p));//8 可得出64位平台下指针位64位,即8字节
return 0;
}
程序在运行时处于操作系统的监督下,操作系统为了达到监控程序运行等一系列目的,进程的虚拟空间都在操作系统的掌握之中。进程只能使用操作系统分配给进程的地址,如果访问未经允许的空间,操作系统回捕获到这些访问,将进程的这种访问当作非法操作,强制结束进程。Windows下的"进程因非法操作需要关闭"和Linux下的"Segmentation fault"大部分都是因为进程访问了未经允许的地址
在32位平台下,一个进程分配了4GB的虚拟内存
进程最多可以使用3GB的虚拟空间,即整个进程在执行的时候,所有的代码、数据包括通过申请的虚拟空间之和不得超过3GB。但进程不能完全使用这3GB的虚拟空间,其中有一部分时预留给其他用途的
Windows系统的进程虚拟空间划分操作系统占2GB,可修改Windows系统盘跟目录下的Boot.ini,加上"/3G"参数即可将操作系统占用的虚拟地址空间减少到1GB
如果程序只能运行在32位系统下,但是需要占用的内存空间超过3GB,怎么办
32位的CPU下,程序使用的空间能不能超过4GB
如果空间指的是虚拟地址空间,那么不能。因为32位的CPU只能使用32位的指针,其最大的寻址范围是0到4GB
如果空间指的是计算机的内存空间,那么可以。Intel采用36位的物理地址,可以访问高达64GB的物理内存
原先的32位地址线只能访问最多4GB的物理内存,但扩展至36位地址线后,Intel修改了页映射的方式,使得新的映射方式可以访问到更多的物理内存,Intel将这个地址扩展方式叫做PAE
操作系统提供一个窗口映射的方法,将额外的内存映射到进程地址空间中。应用程序可以根据需要来选择申请和映射,比如一个应用程序中0x100000000x20000000这一端256MB的虚拟地址空间用来做窗口,程序可以从高于4GB的物理空间中申请多个大小为256MB的物理空间,编号为A、B、C等,然后根据需要将这个窗口映射到不同的物理空间块,用到A是将0x100000000x20000000映射到A,用到B、C再映射过去
在Windows下这种访问内存的操作方式叫做AWE
Linux等Unix类操作系统采用mmap()
系统调用实现
程序执行时所需要的指令和数据必须在内存中才可以正常运行,但大多数情况下程序所需要的内存数量大于物理内存数量
程序运行时是有局部性原理,可以将程序最常使用的部分驻留在内存中,将一些不太常用的数据存放在磁盘中,这就是动态装入的基本原理
覆盖装入将挖掘内存潜力的任务交给了程序员,程序员在编写程序的时候必须手工将程序分割为若干块,然后编写一个覆盖管理器(Ovetlay Manager)来管理模块合适应该驻留在内存何时应该被替换掉
覆盖管理器一般很小,从数十字节到数百字节不等,一般常驻在内存中
一般程序员需要手工将模块按照调用依赖关系组织成树状结构
覆盖管理器必须保证两点
由于跨模块间的调用都需要经过覆盖管理器,以确保所有被调用到的模块都能够正确地驻留在内存中,一旦模块没有在内存中,需要从磁盘或其他存储器读取相应的模块,覆盖装入的速度比较慢。时间换空间
装载管理器就是现代操作系统的存储管理器
目前几乎所有的主流操作系统都是按照页映射的方式装载可执行文件
一个虚拟空间有一组页映射函数将虚拟空间中的各个页映射至相应的物理空间,创建一个虚拟空间实际上并不是创建空间而是创建映射函数所需要的相应的数据结构。在i386的Linux下,创建虚拟地址空间实际上只是分配一个页目录,不是指页映射关系,映射关系等后面程序发生页错误时再进行设置
由于可执行文件在装载时实际上是被映射的虚拟空间,所以可执行文件又被称为映像文件
这种映射关系只是保存在操作系统内部的一个数据结构。Linux中将进程虚拟空间中的一个段叫做虚拟内存区域VMA,Windows中叫虚拟段
在上例中,操作系统创建进程后回在进程相应的数据结构中设置一个有.text段的VMA,在虚拟空间中的地址为0x08048000~0x08049000,对于ELF文件中偏移为0的.text,属性为只读
操作系统通过设置CPU的指令寄存器将控制权转交给进程,由此进程开始执行。可以认为操作系统执行了一条跳转指令,直接跳转到可执行文件的入口地址,即ELF文件头中保存的入口地址
当段的数量增多时,就会产生空间浪费的问题。当ELF文件被映射时,是以系统的页长度作为单位的,那么每个段在映射时的长度时系统页长度的整数倍,如果不是,那么多余部分将占用一个页
对于拥有相同权限的段,合并到一起当作一个段进行映射
一个Segment包含一个或多个属性类似的Section
Segment和Section是从不同角度来划分同一个ELF文件,在ELF中被称为不同的视图
从Section角度看ELF文件就是链接视图
从Segment角度看就是执行视图
当讨论ELF装载是,段专门指Segment,其他情况下指Section
ELF可执行文件中有一个专门的数据结构叫程序头表用于保存Segment信息
是一个结构体数组
VMA除了用于映射可执行文件中的各个Segment,还可以对进程的地址空间进行管理
在Linux下可以通过查看/proc
查看进程的虚拟空间分布
进程有5个VMA,只有前两个是映射到可执行文件中的Segment,另外三个段的文件所在设备主设备号和次设备号及文件节点号都是0,表示没有映射到文件中,这种VMA为匿名虚拟内存区域
vdso的地址已经在内核空间了(大于0xC0000000的地址),事实上是一个内核的模块,进程可以通过访问这个VMA来与内核进行一些通信
一个进程基本上可以划分为一下几种VMA区域
malloc的最大申请数量受到操作系统版本、程序本身大小、用到的动态/共享库数量、大小、程序栈数量、大小等
每一次运行结果可能不同,因为操作相同使用随机地址空间分布技术(出于安全考虑,防止程序受恶意攻击),使得进程的堆空间变小
将段分开映射,长度不足一个页的部分则占一个页,造成内存碎片,浪费磁盘空间
进程刚开始启动的时候必须知道一下进程运行的环境,最基本的就是系统环境变量和进程的运行参数
操作系统在进程启动之前将这些信息提前保存到进程的虚拟空间的栈中
进程启动以后,程序的库部分会把堆栈里的初始化信息中的参数信息传递给main()函数的argc和argv参数,这两个参数分别对应命令行参数数量和命令行参数字符串指针数组
首先在用户层面,bash进程会调用fork()系统调用创建一个新的进程,然后新的进程调用execve系统调用执行指定的ELF文件,原先的bash进行继续发挥等待刚才启动的新进程借宿,然后继续等待用户输入命令
在进入execve()系统调用之后,Linux内核就开始进行真正的装载工作
在内核中,execve()系统调用的相应入口是sys_execve(),sys_execve()进行一系列参数的检查复制之后调用do_execve()读取被执行文件的前128个字节判断文件的格式,之后调用search_binary_handle()去搜索和匹配合适的可执行文件装载处理过程。ELF可执行文件的装载处理过程为load_elf_binary(),主要步骤为
当load_elf_binary()执行完毕,返回到do_execve()在返回到sys_execve(),上面的第五步中已经将系统调用的返回地址改成了被装载的ELF程序的入口地址,所以当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转到ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件装载完成
改成了被装载的ELF程序的入口地址,所以当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转到ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件装载完成