虚拟内存的结构

C语言编译后文件的结构

c语言中总体来说,编译后的程序可以分为在硬盘和在内存中运行两大种情况;每种情况下所启用的结构不一样;

1. 硬盘上:text段 data段 其他段(调试的段,动态库共享库链接表的段)
2. 内存中:text段 data段 bbs段 heap stack

注意:位于硬盘上段其大小不可变(一般情况下)
虚拟内存的结构_第1张图片

如下是一个程序在运行时的4G虚拟内存的组成:
虚拟内存的结构_第2张图片

  1. 栈用于维护函数调用的上下文,离开了栈,函数的调用就办法实现了。栈通常在用户更高的地址空间处分配,通常有数兆字节的大小。栈主要用来存放局部变量,函数调用时会在栈上有一系列的保留现场(保存上下文)及传递参数的操作。自动变量和函数调用时所需保存的信息都存放在此段中。

  2. 堆用来容纳应用 程序动态分配的内存区域,当程序 使用malloc或new 的时候就是得到来自堆中的内存。堆统称在栈的下方(低地址方向,但是不是紧邻的)。堆一般比栈要更大一点,一般会达到几十甚至是数百兆字节。

  3. 代码段(text segment):只读权限;常是指用来存放程序执行代码的一块内存区域这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

  4. 数据段(data segment):读写权限;存已被初始化了的静态数据,包含明确的初始化值,保存在硬盘上,由.exec读取。数据段通常是指用来存放程序中 已初始化的全局变量或者静态变量 的一块内存区域。

  5. BSS 段(Block Started by Symbol )未初始化的数据段,不保存在硬盘上,只是记录数据所需空间的大小,程序开始执行之前,由内核进行初始化为0。

  6. 常量数据段(.rodata):ro表示read only,用于 存放不可变修改的常量数据 ,一旦程序中对其修改将会出现段错误
    (1) 程序中的常量不一定就放在rodata中,有的立即数和指令编码放在.text中
    (2)对于字符串常量,若程序中存在重复的字符串,编译器会保证只存在一个,一般声明形式为const char * xxx
    (3)rodata是在多个进程间共享的

  7. 文件映射区域 :如动态库、共享内存等映射物理空间的内存,一般是 mmap 函数所分配的虚拟地址空间


全局、局部、静态全局、静态局部变量

全局变量、静态全局变量和静态局部变量都存放在内存的静态存储区域,局部变量存放在内存的栈区

  1. 全局变量 关键字:global整个工程 内都可以访问,只能定义一次。是静态存储方式,存储在 静态存储区,全局变量只需在一个源文件中定义,任意文件的任意函数都可以修改全局变量 。如果全局变量定义在源文件a中,其他文件想要使用这个全局变量,需要用extern声明一下。
    如果一个C程序包括两个文件,在两个文件中都需要用到同一个外部变量NUM,此时不能分别在两个文件中各自定义一个外部变量NUM,否则在进行程序链接是会出现 重复定义的错误;正确的做法是:在任一文件中定义全局变量NUM,而在另一文件中用 extern 对NUM 作 “外部变量声明”,即 extern NUM;这样在编译和链接时,系统就会知道NUM有 外部链接,可以从别处找到已定义的NUM,然后将NUM的作用域进行扩展

  2. 局部变量:局部区域内有用,位于栈上。

  3. 静态static的含义:将一个变量放置于静态区,静态限制了这个变量仅在当前文件有效,并且周期和程序周期一样长

  4. 静态全局变量:静态优先于全局,改变了全局变量的作用域,仅能作用域当前文件

  5. 静态局部变量:将变量移动到静态区,不会释放,但是只对当前作用域有用,函数可以不断使用但是静态局部变量就一个


比如:

int a = 0; //.data区
char *p1; //.bbs区
main()
{
int b; //栈
char s[] = “abc”; //栈
char *p2; //栈
char *p3 =123456; //123456\0在常量区,p3在栈上。
static int c = 0//全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
//分配得来得10和20字节的区域就在堆区。
strcpy(p1,123456);
//123456\0放在常量区,编译器可能会将它与p3所指向的”123456”优化成一块。
}

栈是机器系统提供的数据结构,而堆则是C/C++函数库提供的。


  • 栈是机器系统提供的数据结构,有 专门的寄存器(处于CPU内部)
    指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。这就说明了,每一个进程/线程都有自己独立的栈(这些栈会保存上下文信息);这是因为CPU内只允许一个程序运行,所以无法共享寄存器。
  • 同样的对子程序的调用就是直接利用栈完成的,调用子程序把返回地址推入栈,然后跳转至子程序地址进行操作,而子程序推出则是从堆栈中弹出返回地址并跳转然后操作。
  • 栈内存区的地址是连续的。
  • ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶,往低地址方向变化。
  • EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部

栈帧

C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。同时栈帧又是记录在栈上面,是栈的一部分。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。因此栈作用就是用来保持栈帧的活动记录(即函数调用)

当我们定义一个变量或函数时,c语言会在指定区域开辟内存空间。c语言中调用函数时会在栈区相应的开辟一个栈帧,而操作系统会根据函数内的变量决定栈帧的空间大小(主函数也不例外)

栈帧实例

参考:https://www.tenouk.com/Bufferoverflowc/Bufferoverflow2a.html

实例程序:

#include 
int MyFunc(int parameter1, char parameter2)
{
   int local1 = 9;
   char local2 = 'Z';
   return 0;
}

int main(int argc, char *argv[])
{
   MyFunc(7,'8');
   return 0;
}
} 
  • EIP:EIP寄存器,用来存储CPU要读取指令的地址,CPU通过EIP寄存器读取即将要执行的指令;EIP寄存器里的值是地址,地址里的值是汇编指令
  • ESP:栈顶指针。
  • EBP: 当前栈帧 底部指针。
  • 这三个寄存器存放的都是地址

汇编过程,最左侧为EIP的值。

虚拟内存的结构_第3张图片

1、主函数启动,并执行;ebp指向主函数的入口处。

  • 00401078、0040107A:按照从右往左的顺序,将形参的值压入栈中;(所在位置为EBP+8和EBP+12

  • 0040107C:保存下一条指令(子函数返回后的下一条指令)的EIP,并把EIP值压入栈中;(所在位置是EBP+4)
    虚拟内存的结构_第4张图片

  • 0040100A:因为需要执行子程序,EIP需要跳转到子程序的地址00401020

2、运行到子函数func处:虚拟内存的结构_第5张图片

  • 00401020:进入了子程序,将当前EBP(栈上的地址)作为值,存入当前ESP(地址)中;就是当前ESP这个栈地址存放的值是当前EBP
    虚拟内存的结构_第6张图片

  • 00401021:将当前ESP赋值给EBP,因此EBP被更新,形成新的栈帧;这个新的EBP的值是个地址,地址里面的值是旧EBP的地址

  • 00401023:为子程序预留72字节的大小。

  • 00401038、0040103F:子程序中的赋值操作,分别占据栈地址的EBP-4、EBP-8。
    虚拟内存的结构_第7张图片

3、退出子程序
虚拟内存的结构_第8张图片

  • 00401048:将EBP赋值给ESP,栈顶回到当前栈帧底部,等于释放了子程序占据的栈空间;

  • 0040104A:弹出ESP里面的值,也就是之前存入的旧EBP的值(地址,旧栈帧的栈底)并赋值给EBP,使得EBP回到之前的位置;
    虚拟内存的结构_第9张图片

  • 0040104B:取出放在新EBP+4里面的EIP的值;也就是函数返回后的地址00401081。

  • 00401081:继续执行MyFunc(7,‘8’)之后的指令。



内存分配的原理

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

  1. brk是将数据段(.data)的最高地址指针_edata往高地址推;
  2. mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存然后建立虚拟内存和物理内存之间的映射关系
C库提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。

发成缺页中断后,执行了那些操作?
当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作:
1、检查要访问的虚拟地址是否合法 ;
2、查找/分配一个物理页 ;
3、填充物理页内容,读取磁盘(swap)/读取内存条,或者直接置0,或者啥也不干;
4、建立映射关系(虚拟地址到物理地址) 。
5、重新执行发生缺页中断的那条指令
如果第3步,需要读取磁盘(需要交换内存),那么这次缺页中断就是majflt,否则就是minflt。


分配原则

  • 当使用malloc需要分配的 内存小于128k 时,使用 brk分配内存 在堆上分配内存;
  • 大于128k使用mmap文件映射区域分配内存
  • brk分配都只是分配了虚拟内存,并没有物理地址与之对应;当第一次访问到这个虚拟内存会触发缺页中断,再分配物理页。
  • mmap 不仅分配虚拟内存,同时也对应了物理地址;因此在分配的时候已经触发了缺页中断

堆上分配流程

  1. 使用 brk分配内存,将_edata(堆顶指针)往高地址推,扩充堆的大小。
  2. malloc(50k): malloc函数会调用brk系统调用,将_edata指针往高地址推50k,就完成虚拟内存分配。
  3. 依次再分配30k,120k。
    虚拟内存的结构_第10张图片
  4. 如果释放C的30k,_edata会往回退30k;但是如果先释放B的120k,_edata不会回退(因此产生内存碎片),但是B的120k可以被分配给其他;如果C也释放,那此时会回退150k。

文件映射区域上的分配

默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。

  1. 进程调用C=malloc(200K),于文件映射区域上分配内存;
    虚拟内存的结构_第11张图片
    优点在于可以自由释放

关于brk和mmap

  1. 简单来说,每次mmap都会分配物理地址,那就触发缺页中断,需要OS花费资源区对应物理地址;每次munmap之后又会释放回去。
  2. 例如使用 mmap 分配 1M 空间,第一次调用产生了大量缺页中断 (1M/4K次 ) ,当munmap 后再次分配 1M空间,会再次产生大量缺页中断。缺页中断是内核行为,会导致内核态CPU消耗较大。另外,如果使用 mmap分配小内存,会导致地址空间的分片更多,内核的管理负担更大。
  3. 同时堆是一个连续空间,并且堆内碎片由于没有归还 OS ,如果可重用碎片,再次访问该内存很可能不需产生任何系统调用和缺页中断,这将大大降低CPU 的消耗。

你可能感兴趣的:(数据结构)