在早期的计算机中,程序都是直接运行在物理内存上的,意思是运行时访问的地址都是物理地址,而这要求程序使用的内存空间不超过物理内存的大小

在现代计算机操作系统中,为了提高CPU的利用率计算机同时运行多个程序,为了将计算机上有限的物理内存分配给多个程序使用,并做到隔离各个程序的地址空间和提高内存利用率,操作系统应用虚拟内存机制来管理内存

本文介绍的是一些与虚拟内存相关的概念,包括虚拟内存和物理内存之间的映射,一个进程的虚拟内存空间的划分等。

目录

1.物理内存 vs 虚拟内存

2.物理内存空间 和 虚拟内存空间

3.4GB虚拟内存

cpu位宽 vs cpu的地址总线位宽

4.虚拟内存的地址空间划分

Windows 系统下,4G虚拟地址空间被分成了 4 部分: NULL 指针区+用户区+ 64KB 禁入区+内核区。

1)NULL指针区和64KB禁入区:略

2)用户区每个进程私有使用的,大约 2GB 左右 ( 最大可以调整到 3GB,3GB模式) ,称为用户地址空间。

3)内核区是所有进程共享的,为 2GB (3GB模式下就是1GB),称为系统地址空间。

Linux下和Windows下的差不多,只不过Linux默认就是1GB内核空间:

1.保留区:

2.可执行文件映像,堆,栈,动态库

栈空间(Stack)——函数调用:

通过例子1看汇编:

例子2:(VC9,i386,Debug模式)

PS1:编译器 生成的函数 的 标准进入和退出指令序列

PS2:编译器 实现的hook技术

PS3:函数调用之调用惯例

PS4:函数调用之返回值的传递

PS5:函数调用之C++对象

堆空间(heap)——动态申请内存:

5.虚拟地址和物理地址的映射

6.物理内存和硬盘之间的置换

7.虚拟内存的重要性

8.进程的虚存空间分布——装载(程序员的自我修养-链接装载库 第6.4节)

readelf -S链接视图——25个section头

执行视图:7个program头——程序头表记录着程序头

VMA

Stack VMA[stack]

动态链接时的进程堆栈初始化信息


1.物理内存 vs 虚拟内存

物理内存就是内存条,实实在在的内存,即RAM。

虚拟内存是内存管理中的一个概念。对于一个进程来说,虚拟内存是进程运行时所有内存空间的总和,包括共享的,非共享的,存在物理内存中,存在分页内存中(分页后面会介绍),提交的,未提交的。【进程运行起来以后,虚拟内存映射=PP物理空间+DP硬盘空间+未使用使用映射的。】【虚拟内存映射的可能有一部分不在物理内存中,另外一部分在其他介质中,比如硬盘,举个例子,当你的程序需要创建一个1G的数据区,但是此时剩余500M的可用物理内存了,那么此时势必不是所有数据都能一起加载到内存(物理内存)中,势必有一部分数据要放到其他介质中(比如硬盘),待进程需要访问那部分在其他介质中的数据时,再通过调度进入物理内存。】


2.物理内存空间 和 虚拟内存空间

物理内存大小组成的地址空间就叫物理内存空间。物理内存空间表示的是实实在在的RAM物理内存,其地址空间可以看成一个由 M 个连续的字节大小的单元组成的数组,每个字节都有一个唯一的物理地址。【存储单元,也就是每个字节都有其地址,cpu进行访问的时候需要知道其地址。M就是RAM的大小

虚拟内存大小组成的地址空间就叫虚拟内存空间。跟物理内存一样,也是有地址空间的,用地址标识哪个内存位置,也看成一个连续的字节大小的单元组成的数组。【虚拟内存空间实际上并不存在的,需要的只是虚拟内存空间到物理内存空间的映射关系的一种数据结构。】

上面说的数组的大小,就是地址空间的长度

即地址空间是一个抽象的概念,可以想象成一个很大的数组,每个数组的元素是一个字节,而这个数组的大小就是由地址空间的地址长度决定。一般画图也是这么画的。

比如16G的物理内存条,具有0~0x3FFFFFFFF(16G)的地址长度的寻址能力。【另一方面,cpu的地址总线位宽决定了可以直接进行寻址物理内存空间最大值

4G虚拟内存,具有0~0xFFFFFFFF的地址长度的寻址能力。

在一个系统中,物理内存空间只有一个,但是虚拟内存空间有很多个(运行着多个进程)每个虚拟内存空间都有必须有一个映射关系对应于物理内存。【在进程启动的时候会建议一个映射关系,在运行中维护这个关系,后面会讲】

对于一般程序而言,虚拟内存空间中的很大部分的都是未使用的,【虚拟内存是4G的空间】

每个虚拟内存空间往往只能映射到很少一部分物理空间上。每个物理页(将整个物理空间划分成多个大小相等的页)可能只是被映射到一个虚拟地址空间中,也有可能存在一个物理页被映射到多个虚拟地址空间中,那么这些地址空间将共享此页面(如果在一个虚拟地址空间改写了此页面的数据,这在其他的虚拟地址空间也可以看到变化


3.4GB虚拟内存

操作系统(32位)会为每一个新创建的进程分配一个 4GB 大小的虚拟内存,从0到2^32-1。(这里说的分配4GB的虚拟内存并不是分配4GB的空间,而是创建一个映射表。映射表存放在物理内存中。以后会介绍到一级页表和二级页表结构,这个映射表肯定加载在物理内存中,这个映射表最好是设计得当然是越小越好了)

之前一直说是4G的虚拟地址空间,那么为什么是分配一个4GB的虚拟地址空间,因为在32位操作系统一个32位的程序的一个指针长度是 4 字节(指针即为地址,指针里面存储的数值被解释成为内存里的一个地址。64位程序指针是64位,寻址能力2^64-1), 4 字节指针(地址)的寻址能力是从 0x00000000~0xFFFFFFFF ,最大值 0xFFFFFFFF 表示的即为 4GB 大小的容量。

这个虚拟空间地址大小和 实际物理内存RAM大小没有关系。

补充:一个进程的虚拟内存的大小是由操作系统的位宽程序是32位还是64位决定的。

下面解释一下cpu位宽cpu地址总线位宽的以及他们的位宽大小会影响什么。

cpu位宽 vs cpu的地址总线位宽

1.我们说的16位cpu,32位cpu,64位cpu,指的都是cpu的位宽,表示的是一次能够处理的数据宽度,即一个时钟周期内cpu能处理的2进制位数,即分别是16bit,32bit和64bit。不是地址总线的数目。

通用寄存器的宽度决定了cpu可以直接表示的数据范围。我们说的16位cpu,32位cpu,64位cpu,指的就是通用寄存器的位数(宽度)。见 汇编语言 那篇文章)

2.cpu的地址总线位宽决定了可以直接进行寻址物理内存空间。所以如果cpu的地址总线是32位的,也就是它可以寻址0~0xFFFFFFFF(4G)的物理内存空间。但是如果你的计算机上只装了512M的内存条,那么物理地址的有效部分只有0x00000000~0x1FFFFFFF,其他部分都是无效的物理地址。(这里无视一些外部的I/O设备映射到物理空间。)

3.cpu位宽不等于cpu的地址总线位宽16位cpu的地址总线位宽可以是20位32位cpu地址总线可以是36位64位cpu的地址总线位宽可以是36位或者40位(cpu能够寻址的物理地址空间为64GB或者1T)。

4.在cpu访问任何存储单元必须知道其物理地址,所以在一定程度上,cpu的地址总线宽度影响了最大支持的物理内存RAM大小(操作系统的位数也会影响,32位系统,最大就是支持4GB物理内存RAM)

32位系统最大只能支持4GB内存之由来 - Matrix海子 - 博客园


4.虚拟内存的地址空间划分

Windows 系统下,4G虚拟地址空间被分成了 4 部分: NULL 指针区+用户区+ 64KB 禁入区+内核区。

为了高效的调用和执行操作系统的各种服务,Windows会把操作系统的内核数据和代码(内核提供了各种服务供应用程序使用)映射到所有进程的进程空间中。即内核态,也称为系统空间,也可以叫做系统空间。

所以内核态只有一个,会映射到所有的进程空间中。从这个角度来看,用户空间是每个进程独立的,内核空间是共享的。

1)NULL指针区64KB禁入区:略

2)用户区每个进程私有使用的,大约 2GB 左右 ( 最大可以调整到 3GB,3GB模式) ,称为用户地址空间。

用户区存放的是程序代码和数据, 堆, 共享库, 栈

默认的windows配置下是2GB+2GB,称为2GB模式。可以修改windows配置,可以设置3GB用户地址空间+1GB的系统地址空间称为3GB模式

如上图:(以下都是用户态的,用户态的,用户态的,ntdll.dll是用户态的,而且都是共享,每个进程都是同一个虚拟地址的。一些固定的地址。)

0x80000000附近的:如系统库ntdll.dll:windows操作系统用户层的最底层的dll,负责windows子系统与windows内核之间接口,比如堆管理器的核心接口,HeapCreate、HeapAlloc、HeapFree和HeapDestroy,向操作系统申请内存的时候WindowsAPI:VirtualAlloc进行申请。

ntdll.dll是NT内核派驻到用户空间的领事馆,ntdll.dll是用户态和内核态沟通的桥梁。用户空间的代码通过这个dll,来调用内核空间的系统服务。操作系统会在启动阶段将这个加载到内存中,并把他映射到进程空间的同一个虚拟地址

0x10000000:如运行时库(msvcr90d.dll,microsoft的c运行时库,运行时库这这篇文章介绍过

0x00400000:可执行程序映像exe,即程序代码和数据(为什么叫映像,在这个文章介绍过

stack栈的位置:在0x00300000exe后面都有分布。因为有多少个线程就有多少个栈,线程默认栈大小1MB,也可以启动线程的时候自行设定大小。(所以栈,是相对于线程来说的,这在调试的时候可能看出来)

heap堆的位置:在剩下的空间中进行分配。

3)内核区是所有进程共享的为 2GB (3GB模式下就是1GB),称为系统地址空间

内核区保存的是系统线程调度内存管理设备驱动等数据,这部分数据供所有的进程共享以及操作系统的使用——程序在运行的时候处于操作系统的监管下,监管进程的虚拟空间,当进程进行非法访问时强制结束程序。

进程只能使用操作系统分配给进程的虚拟空间。错误提示“进程因非法操作需要关闭”就是访问了未经允许的地址。)

Linux下和Windows下的差不多,只不过Linux默认就是1GB内核空间:

1.保留区

对内存中收到保护而禁止访问的内存区域的总称。在大多数操作系统中,极小的地址都是不允许访问的,如果访问了就会抛出错误。如NULL。 通常C语言将无效指针赋值为0,因为0地址上正常情况下不可能有有效的可访问的数据的。

2.可执行文件映像,堆,栈,动态库

1)可执行文件映像:装载的时候涉及到。可执行文件的装载,进程和线程,运行时库的入口函数(第六章)_u012138730的专栏-CSDN博客_运行时动态装载链接至少需要用到以下哪些函数

2):应用程序动态分配的 通常在栈的下方。地址从低到高分配。

3)动态链接库(共享库)映射区。动态链接的时候介绍Linux之前是默认从0x4000000开始装载的,但是在linux内核2.6中,共享库的装载地址已经挪到了靠近栈的位置,即0xbfxxxxxx附近。。

4):用于维护函数调用的上下文。有函数调用就要用到栈。地址从高到低分配。

栈空间(Stack)——函数调用:

栈是虚拟内存空间的一段连续的地址。里面的内容是 调用函数的活动记录,记录目前为止函数调用之前的那些函数的维护信息。(函数调用,维护信息,堆栈帧,活动记录

一个函数的活动记录堆栈帧)可以用 ebp 和 esp 划定范围(其中参数函数返回地址用ebp+x 可以得到,这两个也是包括在活动记录中的):

ebp和esp都是寄存器,详情看汇编指令和寄存器_u012138730的专栏-CSDN博客。

以上就是一个堆栈帧bp指向当前的堆栈帧底部,sp指向当前的栈帧顶部。随着函数调用的进行以及返回,bp和sp也就是随着改变,指向新的堆栈帧或者旧的堆栈帧。

如上图,一条活动记录包括一下几个方面:

  • 1)函数的返回地址(地址 ebp + 4)和函数的参数(地址 ebp + 8,ebp + 12等等)、

其中函数的返回地址是在call指令中push 的

其中函数的输入实参是在call之前的指令压入的

  • 2)调用该函数之前的ebp的值(旧ebp值,上一个堆栈帧的底部的地址),目的是为了:这个函数返回的时候ebp的值可以恢复到上一个位置,回到上一个堆栈帧现场。
  • 3)【可选】需要保持不变的寄存器的值(地址 ebp - 4,ebp - 8等等)
  • 4)临时数据,局部变量,调试信息——扩大的栈空间中。

通过例子1看汇编:

SimpleSection.c中的func1

看func1函数的反汇编的实现(这里是.AT&T的汇编格式,看文章的最后):——其实就是创建了一个堆栈帧的过程,从ebp开始,不包括输入实参和返回地址。

前面几句的含义写到了图片上,接下去几句:

  • movl $0x0,(%esp) 的含义:

在esp指针寄存器指向的位置存入0x0(其实代表的是第一个参数“%d\n”,那为什是0呢,因为要重定位。这条指令就是printf函数的第一个实参,上一条指令就是printf的第二个参数参数i。相当于从右到左压入实参

(所以printf的两个输入参数就分别存在当前堆栈帧的esp+4 和 esp 中,下面一条语句就是call了再次进入函数调用了,所以这就是之前说的,输入实参是在call之前压入的。)

可以看到.text段(代码段)offset0x10的地方正好是指令movl $0x0,(%esp)0x0的地址。(上上个图中数数,第16个字节)

需要重定位的符号是.rodata,可以看到.rodata里存的正好就是%d

【.rodata, .data, .bss应该也是链接的时候重定位,跟普通的符号printf和func1一样,因为链接完了以后这些段的VMA已经确定了,就可以填上正确的值了。这里看的是目标文件.o,还没有重定位过

  • call 15<func1+0x15>的含义:

15<func1+0x15>,就是跳转到func1+0x15 这里的的内存的地址进行执行,call指令做了两件事情:

1)把当前指令的下一条指令的地址压入栈中,即函数的返回地址

2)进入新的函数调用了(新的函数开头都是,重复开头的ebp入栈ebp=esp等等,像进入func1一样的——进入printf的例行操作)

  • leave 的含义:(相当于执行了出栈的操作,把栈恢复到函数调用以前的样子

等价于下面两条指令:

movl %ebp %esp  恢复esp=ebp的值,即此时esp指向的是ebp的位置,就一开始的时候那样。

popl %ebp 从栈中弹出值,其实就是旧的ebp的值,赋值给ebp寄存器,即ebp = 旧ebp。弹出旧ebp以后此时esp指向的就是函数的返回地址那个位置了。对应上面活动记录的图。

  • ret 的含义:

等效于以下汇编指令

popl %eip  从栈中取出函数的返回地址,并跳转到该位置执行

这个例子中没有push寄存器的值,退出的时候也就不需要pop回来。

例子2:(VC9,i386,Debug模式)

int foo()
{return 123;
}

下面是汇编:(第四步,在debug模式下会加入调试信息,把栈空间上的内容都初始化为CC,两个连续排列的CC就是中文“烫”

例子1是AT&T汇编格式,例子2是Intel汇编格式。

比例子1多了第3,4,6步

下的ps可以不用看,跟本章内容无关只是做个记录。

PS1:编译器 生成的函数 的 标准进入和退出指令序列

不知道不懂蓝色划线的原因。

PS2:编译器 实现的hook技术

钩子技术:

下面是可被替换的函数(FUNCTION)进入指令序列

nop指令占用一个字节,一共5个字节的nop

比如一替换函数(REPLACEMENT_FUNCTION)如下:

替换以后如下:

进入到Function以后,先执行了jmp LABEL(这个jump是近跳指令只占用两个字节,覆盖原来的mov edi,edi)

LABEL下是jmp 到一个新的标签替换函数这个jmp占5个字节,,覆盖原来nop的五个字节

这样就实现了函数的替换了。

说实话,没有很懂,具体实际应用中的流程。

PS3:函数调用之调用惯例

函数声明的时候可以用关键字声明调用惯例默认是cdecl(C语言中):

可以看到调用惯例规定了 :

  • 出栈方:出栈可以是调用方,也可是函数本身(将函数实参压入栈的肯定是调用方)。上面例子中的leave就是调用方执行的。
  • 参数传递:即调用方将实参压入栈序 需要和函数本身 有一致的规定,这样才能取到正确的值。
  • 名字修饰:不同的调用惯例有不同的名字修饰规则,所以,如果不一致的话,就会找不到符号了

不同的编译器同一种调用惯例的实现也不尽相同,比如gcc和vc对于C++的thiscall(p298)

调用惯例调用方被调用方不一致的例子:p299 ,要在动态链接中说明如何链接成功,这里先略了。

例子:

main函数:1 2-调用方对函数参数进行压栈,从右到左,4-调用方对函数参数进行出栈。

f函数也是:栈在调用完以后都栈都恢复到原来的调用之前

vs中设置默认调用惯例:

cdecl调用惯例的用途——p337变长参数。

常用的调用约定类型有__cdecl、stdcall、PASCAL、fastcall。除了fastcall可以支持以寄存器的方式来传递函数参数外,其他的都是通过堆栈的方式来传递函数参数的。(这是网上看到的,不是书里面写的)

PS4:函数调用之返回值的传递

上面的——例子2:(VC9,i386,Debug模式)——中可以看到返回值是由寄存器eax来传递的。但是如果返回值大于4个字节,不能存放在eax寄存器中应该怎么办呢——解决办法是:eax指向一个地址,返回值就存放在这个这个地址中。下面是返回是类的例子,其中 big_thing 大小为 128个字节。

typedef struct big_thing
{char buf[128];
}big_thing;big_thing return_test()
{big_thing b;b.buf[0] = 0;return b;
}int main() {big_thing n = return_test();return 0;
}

main函数的汇编:

int main() {
01041720  push        ebp  
01041721  mov         ebp,esp  
01041723  sub         esp,258h   ---》开辟258h的栈空间
01041729  push        ebx  
0104172A  push        esi  
0104172B  push        edi  
0104172C  lea         edi,[ebp-258h]  
01041732  mov         ecx,96h  
01041737  mov         eax,0CCCCCCCCh  
0104173C  rep stos    dword ptr es:[edi] 
 ----》把栈空间都初始化为cch,即汉字烫烫烫。。。 96h*4=258h。就是栈空间的大小。

 big_thing n = return_test();
0104173E  lea         eax,[ebp-254h]  ---》lea指令看文章汇编指令和寄存器_u012138730的专栏-CSDN博客
01041744  push        eax   ----》eax值是栈空间的最后一个地址,把eax压入栈,紧接着下面就调用call,说明把这个当作了输入参数 return_test(ebp-254h)
01041745  call        return_test (010410E1h)  
0104174A  add         esp,4  --》函数调用方负责压栈参数还原
0104174D  mov         ecx,20h  
01041752  mov         esi,eax  
01041754  lea         edi,[ebp-1CCh]  
0104175A  rep movs    dword ptr es:[edi],dword ptr [esi] 
---》把eax的内容复制到ebp-1CCh中,ebp-1CCh是栈上的一个临时的地址。20h*4=80h 就是正好128字节就是big_thing的大小。rep movs指令汇编指令和寄存器_u012138730的专栏-CSDN博客
0104175C  mov         ecx,20h  
01041761  lea         esi,[ebp-1CCh]  
01041767  lea         edi,[n]  
0104176D  rep movs    dword ptr es:[edi],dword ptr [esi]  ---》再把临时的地址的内容复制到真正的n中。
    return 0;
0104176F  xor         eax,eax  
}

return_test的汇编

big_thing return_test()
{
01041690  push        ebp  
01041691  mov         ebp,esp  
01041693  sub         esp,148h  ---》开辟148h的栈空间
01041699  push        ebx  
0104169A  push        esi  
0104169B  push        edi  
0104169C  lea         edi,[ebp-148h]  
010416A2  mov         ecx,52h  
010416A7  mov         eax,0CCCCCCCCh  
010416AC  rep stos    dword ptr es:[edi]   ---》初始化148h的栈空间  rep stos  指令见文章 汇编指令和寄存器_u012138730的专栏-CSDN博客
    big_thing b;
    b.buf[0] = 0;
010416AE  mov         eax,1  
010416B3  imul        ecx,eax,0  
010416B6  mov         byte ptr b[ecx],0  ---》假汇编 b其实是栈空间的一个地址 
    return b;
010416BE  mov         ecx,20h  
010416C3  lea         esi,[b]  
010416C9  mov         edi,dword ptr [ebp+8]  
---》ebp+8就是之前main函数调用return_test时,压入了一个作为隐形参数出入到return_test中的,在main函数的栈的地址。 (数据应该是存入 [旧的ebp-254h]的内存地址中了 
010416CC  rep movs    dword ptr es:[edi],dword ptr [esi]  ---》把b的内容复制到ebp+8中
010416CE  mov         eax,dword ptr [ebp+8]  ---》把epb+8中存的地址复制给eax,也就是main函数的中的栈空间的某个地址,也就是返回值。
}

但是如果return_test返回类型太大main中的栈空间也无法满足要求,那么就是会使用一个临时的栈上的内存作为中转,返回值对象就会被拷贝2次。

即如果是函数的返回值大于4字节,调用的时候相当于多传入一个输入参数——一个指针,函数里面的返回值指向传入的这个指针。这个指针赋值给eax。返回以后,调用方通过这个指针获取到真正的返回值来进行使用。

PS5:函数调用之C++对象

#include <iostream>
using namespace std;struct cpp_obj
{cpp_obj(){cout << "ctor\n";}~cpp_obj(){cout << "dtor\n";}cpp_obj(const cpp_obj& ){cout << "copy ctor\n";}cpp_obj& operator=(const cpp_obj& rhs){cout << "operator=\n";return *this;}
};cpp_obj return_test()
{cpp_obj b;cout << "before return\n";return b;
}int main() {cpp_obj n = return_test();return 0;
}

输出:(断点下在main中的return那里,并且不启用任何优化

把函数return_test()换成(用了返回值优化技术Return Value Optimization RVO优化将对象的拷贝减少一次):

cpp_obj return_test()
{return cpp_obj();
}

输出:

总结:如果是c++对象的话,有临时对象需要调用构造和析构函数

函数传递大尺寸的返回值,在不同编译器 ,不同平台,不同的编译参数,下都不相同,上述是win10,vs2015下的输出。

堆空间(heap)——动态申请内存:

占据虚拟内存空间的绝大部分,在程序运行过程中进行动态的申请,在主动放弃申请的空间之前一直有效。(栈上的数据出了函数就无效了,全局变量要在编译期间就定义好)

linux系统下堆分配,两个系统调用(就是操作系统,提供的api的调用)

1)brk——扩大或缩小数据段(data段和bss段的合称)

int brk(void* end_data_segment);

2)mmap——申请虚拟地址空间,可以申请的虚拟内存空间映射到文件,也可以不映射到文件(不映射到文件的叫匿名空间

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

windows系统下堆分配,的系统调用

VirtualAlloc——空间大小必须是页的整数倍x86系统必须是4096字节

LPVOID VirtualAlloc{

LPVOID lpAddress, // 要分配的内存区域的地址

DWORD dwSize, // 分配的大小

DWORD flAllocationType, // 分配的类型

DWORD flProtect // 该内存的初始保护属性

};

运行时库malloc函数

第一次先通过系统调用(申请虚拟地址空间:linux调用mmap申请,windows调用VirtualAlloc申请申请一块大的虚拟地址空间,再在这个虚拟地址空间中根据空闲进行分配。

分配算法有:空闲链表法,位图法,对象池法

glibc的malloc

小于64字节,采用类似于对象池的方法;

大于64,小于512字节,采用上述方法中的最佳折中策略;

大于512字节,采用最近适配算法;

大于128kb的申请,直接用mmap向操作系统申请内存。

msvc的入口函数使用了alloca(链接到入口函数的实现)

因为一开始的时候堆还没有初始化

alloca是唯一可以不使用堆的动态分配机制,

在栈上分配任意空间,在函数返回时候释放,跟局部变量一样

(那跟定义局部变量有什么区别?)


5.虚拟地址和物理地址的映射

一个程序要想执行(指令被cpu执行,cpu访问内存),光有虚拟内存是不行的,必须运行在真实的内存上,所以必须在虚拟地址与物理地址间建立一种映射关系

虚拟地址与物理地址间的映射关系由操作系统建立,当程序访问虚拟地址空间上的某个地址值时,就相当于访问了物理地址空间中的另一个值

这种映射机制需要硬件的支持——cpu中的内存管理单元。cpu拿到一个需要虚拟地址(virtual address,VA),经过内存管理单元(Memory Management Unit, MMU)利用存放在物理内存中的映射表(页表,由操作系统管理该表的内容)动态翻译,转换成物理内存地址,进而访问物理内存。这也叫做cpu虚拟寻址方式。【对比文章开头说的,在早期的计算机中,程序都是直接运行在物理内存上的,运行时访问的地址都是物理地址】

整个的映射过程是由软件(操作系统)来设置,而实际的地址转换是由硬件(MMU)来完成。

应用程序和许多系统进程都是使用虚拟寻址

只有操作系统内核的核心部分会使用cpu物理寻址,即地址翻译,直接使用实际的物理内存地址

虚拟寻址中的具体的映射方案后面介绍。段机制(段描述符)和页机制(内存分页)_u012138730的专栏-CSDN博客


6.物理内存和硬盘之间的置换

物理内存RAM不够使用的时候,操作系统会根据一些置换算法将物理内存中的数据置换到磁盘上的交换空间(swap),腾出空闲的物理内存页来存储需要在内存中运行的程序和相关数据。

前面文章说到任务管理器中的提交大小包含两部分,一部分是独占的物理内存(即专用工作集内存),另一部分是在分页文件中的独占内存映射。后者分页文件即是这里说的交换空间。

windows下分页文件叫,pagefile.sys,一般在硬盘的操作系统所在的分区中。

那么如何设置可以写入硬盘的内存大小呢——我的电脑 右键 选择【属性】,左侧栏里选择【高级系统设置】,然后点击如下图所示:正如解释的是,操作系统把这个分页文件当作RAM使用,即硬盘中的虚拟内存的概念。(70526/1024=68G)

ps: windows下有两种虚拟内存文件

1)专用的页面文件,位于磁盘根目录,即上面说的pagefile.sys 。

2)使用文件映射机制加载过的磁盘文件,比如加载了的dll和exe,即成了映像文件(虚拟内存的映像文件)。一旦被加载到内存中,该文件就不能删除了(这是内存管理器和文件系统之间的重要约定)。所以很多有时候删除不了某文件,一般都是正在用着呢。


7.虚拟内存的重要性

1.每个进程使用的是一个一致的地址空间(从0到2^32-1),降低了程序员对内存管理的复杂性。让操作系统来完成虚拟地址空间物理地址空间的转换。(对于程序来说,不需要关心物理地址的变化,最后被分配到哪对程序来说是透明的)

2.每个进程有自己独立的虚拟地址空间,只能访问自己的地址空间,有效地做到进程之间的隔离,保证进程的地址空间不会被其他进程破坏。(从进程角度来看,独占cpu,独占内存有单一的地址空间。)

3.提高物理内存的利用率。


8.进程的虚存空间分布——装载(程序员的自我修养-链接装载库 第6.4节)

在进程创建的时候操作系统会根据可执行文件的头部信息(记录着这个可执行文件有哪些段可执行文件中的段虚拟空间之间进行映射,这个过程就是装载最重要的一步。

可执行文件从链接角度看,是按Section分段的,一般都有十几个Section,二十几个,三十几个——链接视图

可执行文件从装载角度看,是按Segment分段的,一般就是五六七八个——执行视图

我们可以用readelf命令看同一个执行文件(elf格式的)两个视图

readelf -S 命令看链接视图(也就是看段表的命令)

readelf -l 命令看执行视图

下面的例子不是书中的,是我自己电脑上之前编的一个可执行程序,angular的。然后是64位的不是32位的,书本中的例子是32位的。所以我这边看地址都是8个字节的。

readelf -S链接视图——25个section头

执行视图:7个program头——程序头表记录着程序头

程序头的类Elf32_Phdr,其数据成员对应下面打印出来的8列:(ELF的目标文件不需要被装载,所以他没有程序头表,而ELF的可执行文件共享库文件(linux下的so文件)都有。)

只有LOAD类型的Segment是真正需要被映射虚拟空间的。其他类型的都是在装载的时候起到辅助作用。

可以看到LOAD类型的Segment是按权限划分的:

  • RE代码段
  • R只读数据段
  • RE数据段和BSS段(因为有BSS段,所以FileSiz 和 MemSiz不一样的大小)

可以看到其实权限相同的Section可执行文件中的位置都是放在一起的。

对于权限相同的Section把他们的合并到一起称为一个Segment进行映射,因为从装载的角度操作系统不关心各个段的实际内容,最关心的是权限

ELF可执行文件被映射到虚拟空间的时候,是以系统的页长度(4K)为单位的,所以每个段在映射虚拟空间的时候都会按页的整数倍的,多余部分占一个页。按Segment作为段进行映射明显(比按section进行映射)减少了内存的浪费。

VMA

Linux中将进程虚拟空间的一个段叫做虚拟内存区域VMA,windows叫虚拟段VirtualSection。

上述例子中,可执行文件映射到虚拟空间的就有三个VMA。除了有实际文件映射的VMA,还有匿名虚拟空间区域AVMA

使用命令行看进程的虚拟空间的分布情况,就看上面例子中的main(9个VMA):

第1列VMA的地址范围 。这个例子中跟执行视图中的Virtual地址范围一样,有的例子会略有不同(6.4.4节中的段地址对齐)。

第2列VMA的权限 最后一个p是私有,s是共享。

第3列VMA中的Segment在映像文件中偏移。(详情看6.4.4节中描述道VMA中的Segment可执行文件中的Segment其实不是完全对应的)

第4列:映像文件所在设备的主设备号:次设备号

第5列:映像文件的节点号

第6列:映像文件的路径

可以看到除了前3个的 最后几列都是没有的,说明最后的都是没有映射到文件中,这种VMA也叫做匿名虚拟内存区域AVMA(怎么没有 [heap])

Stack VMA[stack]

操作系统进程启动前系统环境变量进程的运行参数提前保存到虚拟空间的栈中。

我们熟知的main()函数中的argc和argv就是从这里获取的。

动态链接时的进程堆栈初始化信息

详情看动态联接 可执行文件的装载,进程和线程,运行时库的入口函数(第六章)_u012138730的专栏-CSDN博客_运行时动态装载链接至少需要用到以下哪些函数   C/C++的编译和链接过程_u012138730的专栏-CSDN博客

进程堆栈初始化信息中包含了动态链接器所需要的一些信息

可执行文件的段(程序头表),

可执行文件的入口地址等等。

这些辅助信息位于环境指针变量的后面,用一个辅助信息数组表示。

辅助信息结构

辅助信息结构中的类型和值含义:

写一个小程序打印出这些数据:


9.windows打开任务管理器

内存项含义

打开任务管理--详细信息---右键 选择列,选择下面这4个。

1.工作集(内存)Working Set = 内存(专用工作集)+ 内存(共享工作集)【第2列=第3列+第4列】

工作集(内存)——进程当前正在使用的物理内存量——表示进程此时所占用的总物理内存(即占用RAM内存)。

这个值是由两部分组成专用工作集 和 共享工作集

专用工作集内存——由该进程正在使用,而其他进程无法使用的物理内存量——是此进程独占的物理内存

共享工作集内存——由该进程正在使用,且可与其他进程共享的物理内存量——是指这个进程与其它进程共享的物理内存,比如加载了某一个dll所占用的内存。

2.提交大小 Comitted Memory——进程独占的内存

提交大小是操作系统为该进程保留的虚拟内存量

Committed Memory is the number of bytes that have been allocated by processes, and to which the operating system has committed a RAM page frame or a page slot in the page file (or both).

Windows allocates memory for processes in two stages. In the first stage, a series of memory addresses is reserved for a process. The process may reserve more memory than it actually needs or uses at one time, just to maintain ownership of a contiguous block of addresses. At any one time, the reserved memory addresses do not necessarily represent real space in either the physical memory (RAM) or on disk. In fact, a process can reserve more memory than is available on the system.

Before a memory address can be used by a process, the address must have a corresponding data storage location in actual memory (RAM or disk). Commit memory is memory that has been associated with a reserved address, and is therefore generally unavailable to other processes. Because it may be either in RAM or on disk (in the swap file), committed memory can exceed the RAM that is installed on the system

是进程独占的内存。这个值也是包含两部分,一部分是独占的理内存(即专用工作集内存),另一部分是分页文件中的独占内存映射。分页文件是硬盘中的虚拟内存,当RAM物理内存资源紧张,或者有数据长时间未使用时,操作系统通常会将数据占用的物理内存先映射到页面文件(pagefile.sys)中,并拷贝数据到硬盘中,然后将本来占用的RAM空间释放。这个也就是虚拟内存技术。

下面是一个Windows API,功能是向操作系统发送请求, 将此进程的不常用的内容从物理内存中换出到分页文件中保存

EmptyWorkingSet

提交大小这部分内存在虚拟内存的线性地址中是连续的,不过在物理内存或者分页内存中,不一定是连续的。提交但未使用的内存一般都在分页内存里面,只有去使用的时候,才会换到物理内存里面。

提交大小 大于 专用工作集

3.PROCESS_MEMORY_COUNTERS  类 和 GetProcessMemoryInfo 函数

MEMORYSTATUSEX 类 和 GlobalMemoryStatusEx 函数 可以获得还可用的虚拟内存。

PROCESS_MEMORY_COUNTERS  类 和 GetProcessMemoryInfo 函数可以获得当前进程使用的内容,如下:

  MEMORYSTATUSEX  MemoryInfo;memset( &MemoryInfo,0,sizeof(MEMORYSTATUSEX) );MemoryInfo.dwLength = sizeof(MEMORYSTATUSEX);GlobalMemoryStatusEx( &MemoryInfo );PROCESS_MEMORY_COUNTERS pmc;UINT uMemSwapUsed = 0;UINT uMemPhyUsed =0;if ( GetProcessMemoryInfo( GetCurrentProcess(), &pmc, sizeof(pmc)) ){uMemSwapUsed = pmc.PagefileUsage/1024/1024;  // 即提交大小,进程独占内存(物理+交换)uMemPhyUsed = pmc.WorkingSetSize/1024/1024;  // 即工作集WorkingSet的大小,总占的物理内存(独占+共享)}CString szInfo;szInfo.Format( "%s  SVM:%u  PM:%u  AVM:%llu",GetTitle(),uMemSwapUsed,uMemPhyUsed,MemoryInfo.ullAvailVirtual/1024/1024);SetConsoleTitle( szInfo );
    PERFORMANCE_INFORMATION PerformanceInfo = {};GetPerformanceInfo(&PerformanceInfo, sizeof PerformanceInfo);MEMORYSTATUSEX MemoryInfo = {};MemoryInfo.dwLength = sizeof MemoryInfo;GlobalMemoryStatusEx(&MemoryInfo);MemoryInfo.dwMemoryLoad, 系统已使用的物理内存百分比:%dMemoryInfo.ullTotalPhys, 系统实际物理内存(字节):%lld,
= static_cast<unsigned long long>(PerformanceInfo.PhysicalTotal) * PerformanceInfo.PageSize;MemoryInfo.ullAvailPhys, 系统实际可用内存(字节):%lld,
= static_cast<unsigned long long>(PerformanceInfo.PhysicalAvailable) * PerformanceInfo.PageSize;MemoryInfo.ullTotalPageFile, 系统页文件 大小(字节):%lld,
= static_cast<unsigned long long>(PerformanceInfo.CommitLimit) * PerformanceInfo.PageSize;MemoryInfo.ullAvailPageFile, 系统可用页文件 大小(字节):%lld,
= static_cast<unsigned long long>(PerformanceInfo.CommitLimit - PerformanceInfo.CommitTotal) * PerformanceInfo.PageSize;MemoryInfo.ullTotalVirtual, 当前进程用户模式分区大小(字节):0x%llX 。(/LARGEADDRESSAWARE:是 YES 还是 NO(是不是如果是系统开启了3GB模式 这里就是显示3GB以上了。如果是64位系统的话 32位进程的话 估计是不用开始3GB模式的,直接如果是大地址模式就可以使用,测试一下?)MemoryInfo.ullAvailVirtual  当前进程虚拟地址空间中未保留和未提交的量(字节):%lld);typedef struct _PERFORMANCE_INFORMATION {DWORD cb;                   //The size of this structure, in bytes.SIZE_T CommitTotal;         //The number of pages currently committed by the system.SIZE_T CommitLimit;         //The current maximum number of pages that can be committed by the system without extending the paging file(s)SIZE_T CommitPeak;          //The maximum number of pages that were simultaneously in the committed state since the last system reboot.SIZE_T PhysicalTotal;       //The amount of actual physical memory, in pages.SIZE_T PhysicalAvailable;   //The amount of physical memory currently available, in pages.SIZE_T SystemCache;         //The amount of system cache memory, in pages. This is the size of the standby list plus the system working set.SIZE_T KernelTotal;         //核心内存总数(单位:页面)SIZE_T KernelPaged;         //分页核心内存数(单位:页面)SIZE_T KernelNonpaged;      //非分页核心内存数(单位:页面)       核心内存数情况在任务管理器 性能 左下角有显示SIZE_T PageSize;            //页面大小DWORD HandleCount;          //系统当前句柄数DWORD ProcessCount;         //系统当前进程数DWORD ThreadCount;          //系统当前线程数
} PERFORMANCE_INFORMATION, *PPERFORMANCE_INFORMATION, PERFORMACE_INFORMATION, *PPERFORMACE_INFORMATION;typedef struct _PROCESS_MEMORY_COUNTERS {DWORD cb;                           //The size of the structure, in bytes.DWORD PageFaultCount;               //The number of page faults.SIZE_T PeakWorkingSetSize;          //The peak working set size, in bytes.      (对应任务管理器中的 峰值工作设置(内存))SIZE_T WorkingSetSize;              //The current working set size, in bytes.   (对应任务管理器中的 工作设置(内存))SIZE_T QuotaPeakPagedPoolUsage;     //The peak paged pool usage, in bytes.     SIZE_T QuotaPagedPoolUsage;         //The current paged pool usage, in bytes.   (对应任务管理器中的 分页池)SIZE_T QuotaPeakNonPagedPoolUsage;  //The peak nonpaged pool usage, in bytes.SIZE_T QuotaNonPagedPoolUsage;      //The current nonpaged pool usage, in bytes.(对应任务管理器中的 非页面缓存池)SIZE_T PagefileUsage;               /*The Commit Charge value in bytes for this process.    (对应任务管理器中的 提交大小)Commit Charge is the total amount of memory that the memory manager has committed for a running process.*/SIZE_T PeakPagefileUsage;           //The peak value in bytes of the Commit Charge during the lifetime of this process.
} PROCESS_MEMORY_COUNTERS;
typedef PROCESS_MEMORY_COUNTERS *PPROCESS_MEMORY_COUNTERS;//例子:
PROCESS_MEMORY_COUNTERS ProcessMemoryInfo = {};
GetProcessMemoryInfo(GetCurrentProcess(), &ProcessMemoryInfo, sizeof ProcessMemoryInfo);

工作设置(内存):
专用(私有)工作集(当前进程独占)中的物理内存数量与进程正在使用且可以和其他进程共享的物理内存数量的总和。
因此可以这么理解,该值就是该进程所占用的总的物理内存

峰值工作设置(内存):
进程的工作设置(内存)的最大值,可以这么理解,因为工作设置(内存)是波动的,这个项专门记录最大的那个值。

内存(专用工作集):
工作集的子集,它专门描述某个进程正在使用且无法与其他进程共享的物理内存值。这个值对于一个进程来说也是最重要的,它代表了一个进程到底独占了多少物理内存。

内存(共享工作集):
进程和可以和别的进程共享的物理内存值(注意:是可以共享的,不一定共享了)。
比较常见的,譬如,加载系统的一些DLL所占用的物理内存,文件共享内存(文件映射),命名共享内存等等。

提交大小:
给当前进程使用而保留的私有虚拟内存的数量,如果要查内存泄漏,可以关注这个值。new malloc VirtualAlloc 分配的内存大小。

分页池:
由内核或驱动程序代表进程分配的可分页内核内存的数量。
可分页内存是可以写入其他存储媒介(例如硬盘)的内存。
此页面的增长通常是由于打开的句柄没有关闭造成的

非分页缓冲池:
由内核或驱动程序代表进程分配的不可分页的内核内存的数量。
不可分页的内存是不能写入其他存储媒介的内存。

2 其他项目含义

cpu时间是cpu在这个进程用的总时间。

当系统没有任务执行是,cpu就会执行空闲进程。空闲进程的pid总是0,他的线程数就是系统中总的cpu数目。他的运行时间cpu时间占用了绝大部分的时间。如果一个进程占用了小时以上,就算是重度的进程了。

cpu这一列指的是1s的cpu占用百分比。如果这一列的某个进程一直同一个值,可能某个线程陷入了死循环。

磁盘有关的问题:

IO读取和IO写入 次数

IO读取字节和IO写入字节 字节数

分析内存问题:

上文

其他观察进程的exe

比如process explorer


[转载]windows任务管理器中的工作设置内存,内存专用工作集,提交大小详解

10.硬件概念--存储器芯片。

存储器芯片读写属性分类可以分为:

1)RAM 随机存储器(可读可写),必须带电存储

A》主随机存储器(主存)

装在主板上的RAM(主板上插槽,组装电脑的时候)

装在扩展插槽上的RAM(可以扩展到多少内存,自己买内存条)

B》接口卡上的RAM

接口卡(cpu通过地址总线与扩展插槽上的接口卡相连,从而与对应外设相通信)有的需要对大批量的输入输出数据进行暂存,其上需要装RAM,最典型的为显示器的接口卡,显卡,上的RAM,称之为显存。

2)ROM 只读存储器,关机后其内容不丢失。

存储BIOS的ROM。BIOS是软件系统,由主板和各类接口卡厂商提供。通过BIOS,对对应硬件设备进行最基本的输入输出。

主板上的ROM存储系统的BIOS

显卡上的ROM存储显卡的BIOS

网卡上的ROM存储网卡的BIOS

划重点:内存中存储数据和指令用于与cpu沟通。内存是仅次于cpu的部件,性能再好的cpu,没有内存也不能工作。就像再聪明的大脑,没有了记忆也无法进行思考(摘自《汇编语言》)。

那存储器是怎么存储的呢?存储器的容量怎么表示的呢?

存储器被划分为多个存储单元,并从零开始编号。比如一个存储器有128个存储单元,编号即为0~127。

为什么要编号呢,因为编号了就相当于有地址了,cpu可以进行寻址了(通过与之相连的地址总线)。

微型机一个存储单元存1个字节(8bit,8个二进制位),所以,存储器的容量就可以表示了,128个存储单元即128字节容量。

cpu将系统中各类存储器看作一个逻辑存储器。

内存地址空间的概念。内存地址空间总是大于物理内存(物理存储器,真正的存储器芯片所提供的存储单元)的。

虚拟内存以及进程的虚拟内存分布(第六章)相关推荐

  1. 实验五 显示进程的虚拟内存地址空间分布信息

    实验五 显示进程的虚拟内存地址空间分布信息 目录 实验五 显示进程的虚拟内存地址空间分布信息 实验环境 一.实验目的 二.实验内容 三.实验步骤 四.实验总结 实验环境 操作系统版本:ubuntu-1 ...

  2. 详解进程的虚拟内存,物理内存,共享内存

    ​ 目录 写在前面: 一.关于内存的两个概念 1.1 虚拟内存 1.2 驻留内存 二.详解top命令中VIRT.RES和SHR 2.1 top命令中ⅥRT.RES和SHR的含义 三.进程的smaps文 ...

  3. 【Linux】进程概念 —— 虚拟内存地址空间

    目录 一.进程地址空间 1.进程地址空间分布图 2.验证上述进程地址空间 3.Linux vs Windows 二.了解虚拟内存地址空间 0.通过代码引出虚拟内存地址空间概念 1.什么是虚拟内存地址空 ...

  4. Linux虚拟内存和进程虚拟地址空间简述

    后台开发经常会问此类问题,虽说难度不大,但是知道和不知道还是有区别的.以下的内容总结自<深入理解Linux内核>第一章,仅仅是简述,没有深入研究,毕竟内存管理这一块内容超级多,感兴趣的同学 ...

  5. 优化命令之vmstat——监控虚拟内存、进程、cpu

    目录 一:vmstat概述 1.1物理内存和虚拟内存 1.2虚拟内存原理 二:vmstat命令 2.1vmstat格式 2.2vmstat参数 三:案例 3.1显示虚拟内存使用情况 3.2一秒内显示2 ...

  6. 虚拟内存与进程地址空间

    文章目录 虚拟内存与进程地址空间 一.虚拟地址与物理地址的定义 二.虚拟地址的工作原理 1.分页映射 2.虚拟页的分类 3.页表 多级页表 4.地址映射的方式 地址映射示例 5.缺页 缺页中断的处理方 ...

  7. 僵尸和孤儿进程及虚拟内存

    调研进程的调度算法. 根据系统的资源分配策略所规定的资源分配算法.对于不同的的系统和系统目标,通常采用不同的调度算法,例如,在批处理系统中,为了照顾为数众多的段作业,应采用短作业优先的调度算法:又如在 ...

  8. linux 进程 地址空间 内存分布 简介

    目录 一 进程空间分布概述 二 内核空间和用户空间 三 进程内存布局 栈 内存映射段 堆 BBS和数据段 C语言程序实例 栈与堆的区别 一 进程空间分布概述 对于一个进程,其空间分布如下图所示: 程序 ...

  9. 【Linux命令】《鸟哥Linux基础》第十六章 进程管理与SELinux初探

    第十六章 进程管理与SELinux初探 16.1 什么是进程(process) Linux下的所有命令与你能够执行的操作 ===>都与权限有关 如何判断权限? 账号管理中的UID.GID:文件属 ...

最新文章

  1. Veeam Backup Replication v7 安装配置手册
  2. CCF201604-3 路径解析(100分)
  3. Java代码优化思路(JVM的角度)
  4. 多选框向后台传值,多选框的回显,对多选框的各种操作
  5. 塔式Server 服务器ESXI6.5安装
  6. 分享Silverlight/WPF/Windows Phone一周学习导读(4月4日-4月9日)
  7. java.lang.unsatisfiedlinkerror:_java.lang.UnsatisfiedLinkError: 的问题
  8. 汇编语言中可以定义变量吗?怎么定义?有局部变量和全局变量之分吗?作用域是什么?
  9. Codeforces Round #661 (Div. 3)
  10. sql server datetime取年月_快速定位数据库性能问题,RDS推出慢SQL统计分析
  11. mysql 删除process_MySQL中Alter table 你不知道的性能问题
  12. Windows环境下使用CMake编译OpenCV3.0和OpenCV_contrib
  13. scala下划线的用法
  14. create方法 eslint关闭_详解create-react-app 自定义 eslint 配置
  15. cpu频率_CPU频率的提升到底会产生哪些影响?
  16. 简单使用apipost和jmeter 测试接口
  17. linux 查看vcf文件,转载-VCF格式详解
  18. 源码阅读分析 - Window底层原理与系统架构
  19. Win10(64位)系统清除BIOS密码的方法
  20. git 如何回退版本(通俗易懂,简单上手)

热门文章

  1. MFC之MP3音频文件转二进制、十六进制等
  2. Could not find artifact com.aliyun:aliyun-sdk-vod-upload报错解决
  3. 众多互联网薪资一览表【运营岗】
  4. 119只股连跌四周 五大板块成重灾区
  5. 关于一个监听、发送QQ消息的插件的使用部署
  6. 综合小项目1--基于51单片机的温度检测报警系统
  7. 浏览器工作原理学习(二十一)
  8. HBuilderX uni-app简单实现静态登录页面(实例)
  9. motoxt 1085 android8,MOTO X XT1085 5.1.1 ROM刷机包
  10. XJTU2022C春大计基第六周