可执行文件的装载与进程
1。进程虚拟地址空间
每个进程运行起来以后,都将有一个自己的虚拟地址空间,其实这还是计算机制造出来的假象,让进程误以为此时只有它自己在运行,所有内存都只有他自己在使用,一般来说,C语言指针的大小的位数与虚拟空间的位数相同,32位平台下指针为32位,4字节。64位平台下的指针为8字节,但是有些特殊的CPU,并不是这样,暂且不考虑特殊的CPU。
暂时以32位平台为例,这个4G 的虚拟地址空间被分为两个部分,其中1G 为操作系统的空间,也就是之前说的内核代码是共享的,一些常驻在内存中的内核代码,动态库等等都是映射在这1G内存中的,剩下的3G 就用来映射程序的地址空间,另外值的一提就是:在程序被编译链接完成后其各个部分的地址已经被确定了。另外其实这个过程主要进行了两个部分工作
空间与地址分配
符号解析与重定位(重定位:有些函数的出入口需要修正地址所以需要重定位)
下面是一个例子:
#include<stdio.h> int globvar = 3; int main() { int a = 3; int b = 4; int c ; c = a + b; printf("a+b = %d,hello world\n",c); return 0; }
这是一个非常简单的例子,现在我们把他编译成可执行文件使用objdump -h 查看各个段的不同信息
这是其中一部分截图,VMA 就是虚拟地址,就是程序假想3G地址空间的逻辑地址,其他参数这里不详细解释,有兴趣可以man objdump 查看相关详细信息
另外有人可能有疑问,为什么不是从0x000000000000开始呢? 其实这是根据不同操作系统决定的,不同版本也有可能不一样。我的fredoa 21 4.0.6 是从400238开始的
这就是虚拟地址空间的来历以及分配。
2.可执行文件装载的方式:
现代装载方式大体上有两种:覆盖装入 和 页映射
覆盖装入这个技术比较早了,想在基本不用了,但是在嵌入式方面号还有很大的用武之地,这个机制就是程序员担任起管理内存的责任,需要自己写一个代码控制内存的分配。就是将内存分成一个树状结构,根据不同需要载入相应大小的内存。其中有两点是需要注意的:
1.从树开始直到最终调用的结构为一个调用路径,运行时这个路径上的内存必须都存在。、
2.禁止跨树之间调用
由于这个机制太过于麻烦,对程序员要求太高,所以大师门就开发了页映射这个新机制。
页映射:
它时虚拟存储机制的一部分,随着虚拟内存的发明而诞生,一个页在X86_64 下是4KB 。每次将需要使用的程序段映射到页内,不需要的就换出,更重要的是一个进程并不是一个页,而是由多页组成的。
3.可执行文件的装载
windos 与 linux 的装载方式是不一样的,这里我们只讨论Linux的情况,首先是物理地址与虚拟地址转换的过程,现在计算机都有一个MMU 这个东西就是个进行虚拟地址与真实地址换工作的。地址转换的问题已经解决,现在需要看下进程的建立。
创建一个进程首先:(当然要调用 fork())
1.创建一个虚拟的地址空间,这就是这前说的那4G 内存空间。
2.读取可执行文件头,将虚拟地址与可执行文件映射起来建立映射关系
3,将CPU指令寄存器指针指向程序的入口
待执行这些步骤后,其实内存并没有载入什么东西其实只是建立了关系而已,然后开始运行进程,当然并没有发现代码,所以就会产生一个 页错误,
操作系统发现这个页错误之后就去读相应的代码,加入内存页,接着继续执行当又有页错误的时候继续加入页,一直这样运行下去最终完成任务。
4.进程虚拟空间分布:
操作系统在装载代码的时候其实是按照权限装载的,比如:代码段为可读可执行,数据段可读可写,BSS段可读可写,数据段只读。
在加载的时候数据段和BSS 就会被加载到一起。
可以看到它有9个段,其中我们只关心LOAD段,因为只有这个段才是需要被映射的,由此就可以看出在虚拟内存中它是怎样的。
从段来看我们就是OBJDUMP 称之为链接视图,从READELF 看我们称值之执行视图,相信都能有所体会
段地址对齐:
其实在物理地址中,是连续存储的,但是映射在页上却是对齐的,原因是操作系统将段与段相接壤但是没有对齐的部分映射了两个边,一个映射在第一个段后,一个在第二个段前。这样虚拟地址空间地址对齐后有利于查找与执行同时物理内存也达到了节省空间的目的。
5.linux 内核装载ELF 的过程
首先fork( )创建一个进程——>调用execve系统调用(进入内核态,栈为内核态栈)————>调用sys_execve( ) 进行一些参数复制检查————>调用do_execve( )读取可执行文件的最前边的128字节,其实最重要的是读取开头的魔数,由此判断是哪一种程序,例如(ELF,cafe,#!/bin/bash..........)——>例如现在是一个ELF 文件,那么调用
load_elf_binary( ).开始检查参数,设置动态链接,读取数据,初始化栈空间,修改系统调用的返回地址为ELF 可执行文件的入口,然后程序开始返回。
自此,ELF 可执行文件装载完成。
版权声明:本文为博主原创文章,未经博主允许不得转载。
原文地址:http://blog.csdn.net/zmrlinux/article/details/47166385