内存

以下是百度百科对内存的介绍:
内存是计算机的重要部件之一。

它是外存与CPU进行沟通的桥梁,计算机中所有程序的运行都在内存中进行。
内存性能的强弱影响计算机整体发挥的水平。

内存也称内存储器和主存储器,它用于暂时存放CPU中的运算数据,与硬盘等外部存储器交换的数据。

只要计算机开始运行,操作系统就会把需要运算的数据从内存调到CPU中进行运算。当运算完成,CPU将结果传送出来。所以,内存的运行也决定计算机整体运行快慢的程度。

程序的内存布局

现代的应用程序都运行在一个内存空间中,在32位系统中,这个空间有着2^32字节大小,也就是4G。

但是!也不是说这4G空间全都是给用户用的。操作系统会将其中的一部分分配给内核使用。这一部分也就叫内核空间
Linux平台下,会将1G的空间分配给内核;在Windows上则会分配2G的空间。

剩下的空间则被称为用户空间
在用户空间里,有许多低值区间有特殊的地位:

  • :用户维护函数调用的上下文。栈通常在用户空间的最高地址处分配,通常有几兆字节大小。
  • :堆是用来容纳应用程序动态分配的内存区域。堆通常存在于栈的下方。一般有几十至数百兆的字节
  • 可执行文件映像:存储着可执行文件在内存里的映像
  • 保留区:对内存中受保护而禁止访问的内存区域的总称。
  • 动态链接库映射区:用于映射装载的动态链接库。在Linux下,如果可执行文件依赖其他共享库,那么系统就会为它从0x40000000开始的地址分配相应的空间,并将共享库载入到该空间。

通常C语言将指针赋值为0,就是这因为保留区禁止访问。

从图中我们可以看出:栈向低地址增长,堆向高地址增长

栈与调用惯例

栈是什么

在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈push,也可以将已经压入栈中的数据弹出pop
当然数据结构中栈的先进后出特性也是保留的

在操作系统中,栈总是向下增长的。栈顶由称为esp的寄存器进行定位。压栈的操作使栈顶的地址减小,弹出的操作使栈顶地址增大

栈在程序中的使用过程

栈在程序运行中具有举足轻重的地位,栈保存了一个函数调用所需要的的维护信息,这被称之为栈帧,栈帧一般包括如下几方面:

  • 函数的返回地址和参数
  • 临时变量
  • 保存的上下文

如何保存活动记录

在i386中,一个函数的活动记录用ebpesp两个寄存器划定范围。

esp始终指向栈的顶部,同时也就指向了当前函数的活动记录的顶部。
ebp指向了函数活动记录的一个固定位置,ebp寄存器又被称为帧指针。

函数调用过程

我们根据图可以看到,在ebp之前会往栈中压入参数和函数调用之后的的返回地址。

在一个i386下的函数总是这样调用的:

  • 把所有或一部分参数压入栈中,如果有其他参数没有入栈,则使用某些特定的寄存器传递
  • 把当前指令的下一条指令的地址压入栈中
  • 跳转到函数体执行

Q:为什么会打印出“烫烫烫烫”或“屯屯屯屯”

栈中将所有初始化的字节都初始化0XCCCC,在汉字编码中就是“”字;
当然,有时会使用0XCDCDCD,这样就会打印出“屯屯屯屯”;

调用惯例

为了保证函数被正确的调用,调用和被调用双方都需要遵守同样的约定:

  • 函数参数的传递顺序和方式
  • 栈的维护方式
  • 名字修饰的策略

以下是调用惯例:

以以下函数为例:

int foo(int n,int m)
{
   
	int a = 0,b = 0;
	...
}
  • 将m压入栈
  • 将n压入栈
  • 将返回地址压入栈
  • 跳转到_foo执行

函数返回值的传递

除了参数的传递之外,函数与调用方的另一个交互渠道就是返回值

eax就是一个传递返回值的通道。
但是!eax只有四字节,对于返回5~8字节的情况又该怎么处理呢?

没错,那就是eaxedx联动一起返回!
eax返回值要低4字节,edx返回值要搞1~4字节。

那么对于返回值大于8字节呢?
增加更多的寄存器?

当然不是:用指针啊!
以下是具体思路:

  • main函数会在栈上额外开辟一片空间用作传递返回值的临时对象temp
  • temp对象的地址作为隐藏参数传递给函数
  • 函数将数据拷贝给temp对象,并将temp对象的地址用eax传出

堆与内存管理

什么是堆

堆是一块巨大的内存空间,常常占据整个虚拟空间的绝大部分。
在这里,程序可以请求一块连续内存并自由的使用,这块内存在程序主动放弃之前都会一直保持有效

堆管理

申请堆的比较好的方法就是程序向操作系统申请一块适当大小的堆空间,然后由程序自己管理这块空间,具体的说,管理着堆空间分配的往往是程序的运行库

相当于运行库向系统“批发”了一块较大的堆空间,然后“零售”给程序用。当然,用完了再"进货"

Linux进程堆管理

Linux有两种堆空间分配的方式:

  1. brk()系统调用
  2. mmap()

brk()

int brk(void* end_data_segment)

brk()的作用实际上就是设置进程数据段的结束地址,即它可以扩大或缩小数据段。如果我们将数据段的结束地址向高地址移动,那么被扩大的部分空间就可以被我们使用。

mmap()

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

mmap()的作用是向操作系统申请一段虚拟地址空间。
mmap()的前两个参数分别用于指定需要申请的起始地址和长度,如果起始地址设置为0,那么Linux自动挑选何时的起始地址。

注意:申请的空间的起始地址和大小都必须是系统页的整数倍

而malloc函数就是这样的来处理用户的空间请求:对于小于128k的请求,它会在现有的堆空间里,按照堆分配算法为它分配一块空间并返回;对于大于128kb的请求,它会使用mmap()函数为它返回一块匿名空间,然后在匿名空间中分配空间

参考文献

 [1] 俞甲子 石凡 潘爱明.程序员的自我修养.电子工业出版社,2009.4.