进程眼中的线性地址空间

从文章的题目我们就知道今天是以一个进程的角度来看待自身的运行环境。我们先提出第一个问题,什么是进程?对于这个问题,各种参考资料上给出的定义都显得过于抽象而难以理解,下面是我自己的定义:

进程是一个动态的概念,它是静态的可执行文件执行过程的描述,其包含了一个静态程序运行时的状态和其所占据的系统资源的总和。

还是很抽象吗?那么,我们可以这样比喻,如果说菜谱是程序代码,厨具是硬件的话,那么炒菜的整个过程就是一个进程。这下理解了吧?那我们继续。

每个程序在启动之后都会拥有自己的虚拟地址空间(Virtual Address Space),这个虚拟地址空间的大小由计算机平台决定,具体一点说由操作系统的位数和CPU的地址总线宽度所决定,其中CPU的地址总线宽度决定了地址空间的理论上限(先不考虑主板…)。

比如32位的硬件平台可编址范围就是0x00000000~0xFFFFFFFF,即就是4GB。而64位的硬件平台达到了理论上0x0000000000000000~0xFFFFFFFFFFFFFFFF的寻址空间,即就是17179869184GB的大小(事实上我自己的64位Intel Core i3处理器也仅有36位地址总线,加上虚拟地址扩展功能也才48位,并非64位)。

为了行文的简单,我就以32位硬件平台来描述吧(事实上我对64位所知甚少,不敢信口开河…),同时指定环境为32位的Linux操作系统。

可能看到这里你反而更迷惑了,我一直在说一个进程拥有4GB的线性地址空间(以下只讨论32位),可是操作系统上同时在运行着N个进程,难不成每个都有4GB的线性地址空间不成?没错,每个都有。我们一直在使用术语“线性地址空间”而非“主存储器(内存)”,因为线性地址空间并非和主存等价。我们平时只要一提到“地址”这个概念,想必大家自然而然的就想到了主存储器。但事实上并非线性地址就一定指向主存储器的物理地址,如果你对“线性地址空间”不理解的话,我建议你先去看看我的另一篇博文《基于Intel 80×86 CPU的IBM PC及其兼容计算机的启动流程》。

其实说到线性地址空间,就不得不提到Intel CPU保护模式下的内存分段和分页,但这偏离了文章的主旨。我们暂时只需要知道,之所以进程拥有独立的4GB的虚拟地址,是因为CPU和操作系统提供了一种虚拟地址到实际物理地址的映射机制,在页映射模式下,CPU发出的是虚拟地址,即进程看到的虚拟的地址,经过MMU(Memory Management Unit)部件转换之后就成了物理地址。

好了,下文中我将假定读者理解了线性地址空间的概念,并认可了每个进程拥有4GB线性地址空间这一事实(物理地址扩展(PAE:Physical Address Extension)技术后面再说)。那么,这4GB的线性地址空间里都有些什么呢?我们画一张图来说明一下。

内存高地址区域是被操作系统内核所占据的,Linux操作系统占据了高地址区域的1GB内存(Windows系统默认保留2GB给操作系统,但是可以配置为保留1GB)。如果我们想知道一个进程具体的内存空间布局的话,可以去/proc目录找以进程的pid所命名的目录下一个叫maps的文件,使用cat命令查看即可(需要root权限)。

我们从图中可以看到,32位Linux系统中,代码段总是从地址0x08048000处开始的。数据段一般是在下一个4KB(分页机制默认选择4KB一个内存页)对齐的地址处开始。运行时堆是在数据段之后又一个4KB对齐处开始的,并通过malloc()函数调用向上增长(Linux下的malloc()一般依靠调用brk()或者mmap()系统调用实现)。再接着跳过动态链接库的区域就是进程的运行时栈了,需要注意的是栈是由高地址向着低地址增长的。栈空间再往上就是操作系统保留区域了,用于驻留内核的代码和数据。即就是在一个进程的眼里,只有它和操作系统在一起。

也许你会问,那么一个进程如何修改另外一个进程的运行时数据呢?比如所谓的外挂程序。我们想想,一个进程不知道另一个进程,那谁知道所有的进程呢?操作系统呗,没错,操作系统提供了这种抽象,它也就拥有访问所有进程地址空间的能力。答案就是,一个进程倘若要修改不属于自己的进程空间的数据,就需要操作系统提供相关的系统调用(或API函数)的支持来实现。

我们具体来看看代码段,以C语言为例,程序代码段的入口_start地址处的启动代码(startup code)是在目标文件ctr1.o(属于C运行时库的部分)中定义的,对于特定平台上的C程序都一样。其执行流程如下:

0x080480c0 <_start>:

    调用 __libc_init_first 函数
    调用 _init 函数
    调用 atexit 函数
    调用 main 函数
    调用 _exit 函数
 

而我们平时写的main函数只是整个C程序运行过程中所调用的一环而已。

我们给出一段代码来看看一个C语言程序编译链接之后如何安排各个元素的内存位置吧,代码和注释如下:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int a = 1;              // 栈区
    static int b = 2;       // 全局静态区(读/写段)
    const int c = 3;        // 只读段
    char *str1 = "Hello";   // str1指针在栈区,"hello"字符串在只读段
    char str2[] = "world";             // 只在栈区(字符串)
    char *str3 = (char *)malloc(20);   // str3变量在栈区,指针指向堆区
   
    return EXIT_SUCCESS;
}
 

注释中我们看到了各个元素所在内存段的位置。而编译好的main函数本身是存在于代码区的(一般代码段也是只读段)。我们这个程序运行后如果是动态链接的C语言运行时库的话,动态库会存在图示的动态库映射区。其实无论使用C语言运行时库的程序无论有多少,运行时库的代码在内存里只会有一份。对于不同的程序,进行地址映射即可。

接下来我们简单说说栈(stack),关于栈的基本概念到处都是,如果大家不明白可以自己去查查。其实这里的栈就是把一段位于用户线性地址空间最高处的一段连续内存以栈的思想来使用罢了。大家不要觉得线性空间有4GB,栈占据了很大。其实栈大小默认就几MB罢了。Linux可以在终端下执行 ulimit -a命令查看限制。如图所示:

我这里不过也就默认8192KB(8MB)大小,不过可以使用ulimit命令调整(调整只在本次bash执行过程中有效,下次需要重新设置)。

栈也经常被叫做栈帧(Stack Frame)或者活动记录(Activate Record)。栈里通常存储以下内容:

函数的临时变量;
函数的返回地址和参数;
函数调用过程中保存的上下文。

在i386中,使用esp和ebp寄存器划定范围。esp寄存器始终指向栈顶,随着压栈和出栈操作而改变值。ebp寄存器随着调用过程,暂时的指向一个固定的栈位置,便于寻址操作的进行。

我们画一张图来看看吧:

这里照抄网上的函数调用流程:

  1. 把所有的参数压入栈(有时候是一部分参数,剩余参数通过寄存器传递)
  2. 把当前指令的下一条指令的地址压入栈
  3. 跳转到函数体执行

我继续续上后面的操作:

  1. 在栈里继续创建该函数的临时变量和其他数据
  2. 函数代码执行完之后栈后退到局部变量之上的位置
  3. 恢复之前保存的所有寄存器
  4. 取出原先保存的返回地址,跳转回去
  5. eax寄存器保存了函数的返回值(浮点数是把返回值放在第一个浮点寄存器上%st(0) )

为了不让大家变的过于纠结,我就不贴出相关的汇编代码了,有兴趣的同学可以自己研究编译器生成的汇编语言。具体方法在《编译和链接那点事》和《浅谈缓冲区溢出之栈溢出》中有详细的描述。

好了,本篇暂时结束,下文以后再说。

时间: 2024-12-30 17:40:07

进程眼中的线性地址空间的相关文章

线程眼中的线性地址空间

以前写过一篇<进程眼中的线性地址空间>,这是她的姊妹篇线程篇.而且和以前一样我们只谈32位Linux下的实现.另外读者可能还需要之前的一篇文章<Linux线程的前世今生>作为前期的辅助资料. 如果读者已经看过这两篇文章,那么我们就可以继续往下说了. 我简单列出上述文章中的几个要点: 32位操作系统下的每个进程拥有4GB的线性地址空间. 从Linux内核的角度来说,它并没有线程这个概念.在内核中,线程看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,比如地址空间). 暂

聊聊内存管理

这篇文章我们聊聊内存管理. 本来我想不针对于任何具体的操作系统来谈内存管理,但是又觉得不接地气.言之无物.所以我决定在阐述概念的同时,还针对IA32平台Linux下的内存管理做简要的介绍,并且以实验来证明结论.以下内容分拆为几个大标题和小节,内容前后承接. 物理地址空间 首先,什么是物理地址空间?我们知道CPU与外部进行信息传递的公用通道就是总线,一般而言,CPU有三大总线:控制总线.数据总线.地址总线.这三类总线在一定程度上决定了CPU对外部设备的控制和数据传送能力.其中地址总线决定了CPU能

linux内核分析之进程地址空间【转】

转自:http://blog.csdn.net/bullbat/article/details/7106094 版权声明:本文为博主原创文章,未经博主允许不得转载. 本文主要介绍linux内核中进程地址空间的数据结构描述,包括mm_struct/vm_area_struct.进程线性地址区间的分配流程,并对相应的源代码做了注释.  内核中的函数以相当直接了当的方式获得动态内存.当给用户态进程分配内存时,情况完全不同了.进程对动态内存的请求被认为是不紧迫的,一般来说,内核总是尽量推迟给用户态进程分

Linux内核剖析 之 进程地址空间(三)

本节主要讲述缺页异常处理程序和堆的管理等内容. 缺页异常处理程序 触发缺页异常程序的两种情况: 1. 由编程错误引起的异常(如访问越界,地址不属于进程地址空间). 2. 地址属于线性地址空间,但内核还未分配相应的物理页,导致缺页异常. 缺页异常处理程序总体方案: 线性区描述符可以让缺页异常处理程序非常有效的完成它的工作. do_page_fault()函数是80x86上的缺页中断服务程序,它把引起缺页的线性地址和当前进程的线性区相比较,从而根据具体方案选择适当的方法处理此异常. 标识符vmall

Linux内核剖析 之 进程地址空间(一)

绪论     内核获取内存方式--直接了当:     1. 从分区页框分配器获取内存(__get_free_pages()或alloc_pages()):     2. 使用slab分配器为专用或通用对象分配内存(kmem_cache_alloc()或kmalloc()):     3. 使用vmalloc或vmalloc_32获取一块非连续内存区.     如果申请的内存得以满足,这些函数返回一个页描述符地址或线性地址.     *内核申请内存使用这些简单方法基于以下两个原因:     1.内

Linux内核剖析 之 进程地址空间(二)

//接前一章,本节主要介绍线性区以及相关线性区的操作. 线性区 Linux通过类型为vm_area_struct的对象实现线性区. vm_area_struct: struct vm_area_struct { struct mm_struct * vm_mm; /* The address space we belong to. */ unsigned long vm_start; /* Our start address within vm_mm. */ unsigned long vm_e

Linux内核设计与实现笔记(二) 内存管理、进程地址空间

内存管理 1.页   物理页作为内存管理的基本单位.内存管理单元通常以页为单位进行处理. 通过结构体page来表示系统中的每个物理页. 2.区 由于页位于内存中特定的物理地址上,所以不能将其用于一些特定的任务,故内核把页划分为不同的区. 硬件在内存寻址方面的问题: 一些硬件只能通过内存地址来执行直接内存访问(DMA) 一些体系结构其内存的物理寻址范围大于虚拟寻址范围,故,内存不能永久地映射到内核空间 解决方法,通过创建三种不同的分区: ZONE_DMA--专门执行DMA ZONE_NORMAL-

linux进程地址空间--vma的基本操作【转】

转自:http://blog.csdn.net/vanbreaker/article/details/7855007 版权声明:本文为博主原创文章,未经博主允许不得转载.        在32位的系统上,线性地址空间可达到4GB,这4GB一般按照3:1的比例进行分配,也就是说用户进程享有前3GB线性地址空间,而内核独享最后1GB线性地址空间.由于虚拟内存的引入,每个进程都可拥有3GB的虚拟内存,并且用户进程之间的地址空间是互不可见.互不影响的,也就是说即使两个进程对同一个地址进行操作,也不会产生

linux系统编程基础(五) Linux进程地址空间和虚拟内存

一.虚拟内存 先来看一张图(来自<Linux内核完全剖析>),如下: 分段机制:即分成代码段,数据段,堆栈段.每个内存段都与一个特权级相关联,即0~3,0具有最高特权级(内核),3则是最低特权级(用户),每当程序试图访问(权限又分为可读.可写和可执行)一个段时,当前特权级CPL就会与段的特权级进行比较,以确定是否有权限访问.每个特权级都有自己的程序栈,当程序从一个特权级切换到另一个特权级上执行时,堆栈段也随之改换到新级别的堆栈中. 段选择符:每个段都有一个段选择符.段选择符指明段的大小.访问权