链接装载与库 第10章 内存

10.1 程序的内存布局

现代的应用程序都运行在一个内存空间里,在32位系统里,这个内存空间拥有4GB的寻址能力。
一般来讲,应用程序的内存空间里有如下默认的区域:

  • 栈 用于维护函数调用的上下文
  • 堆 用来容纳应用程序动态分配的内存区域
  • 可执行文件映像
  • 保留区 并不是一个单一的内存区域,而对受到保护而禁止访问的内存区域的总称。如地址为0的内存。
  • 动态链接库映射区 在linux下,如果可执行文件依赖于其他共享库,则会从0x4000 0000 开始的地址分配相应的空间。

10.2 栈与调用惯例

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

栈保存了一个函数调用所需要维护的信息,被称为堆栈帧。堆栈帧一般包括以下几个方面:

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

函数调用过程中的栈:
https://blog.csdn.net/qq_31567335/article/details/84782202

10.2.2调用惯例

函数的调用方和被调用方对函数如何调用有着统一的理解。比如参数的入栈顺序,寄存器的使用与恢复。
这称之为调用惯例,一个调用惯例一般会规定如下几个方面人内容:

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

在c语言中,存在多个调用惯例,默认的调用惯例是cdecl。
参数传递 从右至左的顺序压参数入栈。
出栈方 函数调用方
名字修饰 直接在函数名称前加1个下划线

10.2.3 函数返回值传递

eax是函数传递返回值的通道。
但是eax本身只有4个字节。
对于返回5-8字节的情况,几乎所有的调用惯例都是采用eax和edx联合返回的方式。

超过8个字节的,比较复杂。

/*returnTest.c*/
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();
}

反汇编结果:

00000560 <return_test>:
 560:	55                   	push   %ebp
 561:	89 e5                	mov    %esp,%ebp
 563:	57                   	push   %edi
 564:	56                   	push   %esi
 565:	53                   	push   %ebx
 566:	83 c4 80             	add    $0xffffff80,%esp
 569:	e8 71 00 00 00       	call   5df <__x86.get_pc_thunk.ax>
 56e:	05 92 1a 00 00       	add    $0x1a92,%eax
 573:	c6 85 74 ff ff ff 00 	movb   $0x0,-0x8c(%ebp)
 57a:	8b 45 08             	mov    0x8(%ebp),%eax
 57d:	89 c2                	mov    %eax,%edx
 57f:	8d 85 74 ff ff ff    	lea    -0x8c(%ebp),%eax
 585:	b9 80 00 00 00       	mov    $0x80,%ecx
 58a:	8b 18                	mov    (%eax),%ebx
 58c:	89 1a                	mov    %ebx,(%edx)
 58e:	8b 5c 08 fc          	mov    -0x4(%eax,%ecx,1),%ebx
 592:	89 5c 0a fc          	mov    %ebx,-0x4(%edx,%ecx,1)
 596:	8d 5a 04             	lea    0x4(%edx),%ebx
 599:	83 e3 fc             	and    $0xfffffffc,%ebx
 59c:	29 da                	sub    %ebx,%edx
 59e:	29 d0                	sub    %edx,%eax
 5a0:	01 d1                	add    %edx,%ecx
 5a2:	83 e1 fc             	and    $0xfffffffc,%ecx
 5a5:	c1 e9 02             	shr    $0x2,%ecx
 5a8:	89 ca                	mov    %ecx,%edx
 5aa:	89 df                	mov    %ebx,%edi
 5ac:	89 c6                	mov    %eax,%esi
 5ae:	89 d1                	mov    %edx,%ecx
 5b0:	f3 a5                	rep movsl %ds:(%esi),%es:(%edi)
 5b2:	8b 45 08             	mov    0x8(%ebp),%eax
 5b5:	83 ec 80             	sub    $0xffffff80,%esp
 5b8:	5b                   	pop    %ebx
 5b9:	5e                   	pop    %esi
 5ba:	5f                   	pop    %edi
 5bb:	5d                   	pop    %ebp
 5bc:	c2 04 00             	ret    $0x4

000005bf <main>:
 5bf:	55                   	push   %ebp
 5c0:	89 e5                	mov    %esp,%ebp
 5c2:	83 c4 80             	add    $0xffffff80,%esp
 5c5:	e8 15 00 00 00       	call   5df <__x86.get_pc_thunk.ax>
 5ca:	05 36 1a 00 00       	add    $0x1a36,%eax
 5cf:	8d 45 80             	lea    -0x80(%ebp),%eax
 5d2:	50                   	push   %eax
 5d3:	e8 88 ff ff ff       	call   560 <return_test>
 5d8:	b8 00 00 00 00       	mov    $0x0,%eax
 5dd:	c9                   	leave  
 5de:	c3                   	ret   

相比于书中的反汇编结果,main函数没有拷贝返回结果这一步骤。应该是编译器做了优化。

简单总结:
(书上介绍的步骤)
main函数先申请足够大的栈空间(add $0xffffff80,%esp)
除了够本地变量n存储外,还为返回结果分配了一个临时空间temp。
将temp的地址作为隐含参数传入return_test函数
return_test在函数结束处,将对象拷贝到temp
return_test用eax返回temp的地址
main将temp中的值拷贝到n对应的地址。

用gcc( 6.3.0)反汇编的结果:
直接将变量n的地址作为参数传入,所以只需要一次拷贝。

10.3 堆与内存管理

因为栈上的数据在函数返回的时候会被释放掉,无法传递至函数外部。需要返回至函数外部的数据,可以申请放入堆中。

在c语言中,使用malloc函数申请内存空间。那么malloc是如何实现的呢?

  1. 把内存管理交给操作系统内核。因为内核本身就具有内存管理的功能。
    这样做性能会比较差。因为系统调用的开销大。
  2. 程序向操作系统申请一块适当大小的堆空间,然后由自己对内存进行管理。具体来讲,管理着程序堆空间分配的,往往是程序的运行库。

10.3.2 linux进程管理

linux提供了两种堆空间分配的系统调用:brk(), mmap()

brk()的作用实际上就是设置进程数据段的结束地址。即它可以扩大或者缩小数据段。
如果将数据段的结束地址向高地址移动,那么扩大那部分空间就可以被我们使用,把这块空间拿来作为堆空间是最常见的做法之一。
mmap()的使用是向操作系统申请一段虚拟地址。这块虚拟地址空间可以映射到某个文件,当它不将地址空间映射到某个文件时,这块空间称为匿名空间,可以拿来作为堆空间。

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

### 10.3.3 windows进程堆管理

10.3.4 堆分配算法

  1. 空闲链表
    把堆中各个空闲的块按照链表的方式连接起来,当用户申请一块空间时,可以遍历整个链表,直到找到合适大小的块并将它拆分。当用户释放空间时将它加入到空闲链表中。

空闲链表的实现非常简单,但在释放空间的时候,给定一个已经分配块的指针,堆无法确定这个块的大小。
一个简单的解决办法是用户申请k个字节的空间的时候,实际分配k+4个字节,这4个字节用于存储块的大小。但是这种思路存在很多问题,例如,一旦链表或者记录长度的那4字节被损坏,整个堆就无法工作。而这些数据恰恰容易被越界读写接触到。

  1. 位图
    位图是一种更加稳健的分配方式。
    核心思想是将整个堆划分为大量大小相同的块。当用户请求内存的时候,问题分配整数个块的空间给用户。第一个块称之为已分配空间的头。
    因此每个块只有可能是三种状态中的一种:头/主体/空闲。因此仅需要2位即可表示一个块。

位图的优点:
速度快。由于整个堆的空闲信息保存在一个数组中,访问数组时容易命中cache。
稳定性好。简单备份位图即可
易于管理
缺点:
分配内存时容易产生碎片
如果堆很大,但是块又很小,那么位图会很大,失去cache命中高的优势。而且位图本身也浪费一定空间。

  1. 对象池

写到后面。
这本书读到这一章就不准备继续下去了。

  1. 最主要的原因是没时间。当前工作实在是太饱和了。
    从9.9号拿到这本书,到现在勉强看到这么多。3个多月过去了。而且这段时间,我就只在看这一本书。这个现状实在是令人担忧,希望工作能有所改变吧。
  2. 后面的内容与自己现在掌握的知识相差更远了。而且当前来看,无任何应用的机会。所以先把过于有限的时候投入更紧迫的需要学习的内容当中去。

现在养成了看书就要记笔记的习惯,虽然记得内容有时候看起来挺傻逼的。但是我相信这确实是一个好习惯。对于后面的复习,深入理解一定是有帮助的。

2018/12/18 于湖畔花园

猜你喜欢

转载自blog.csdn.net/qq_31567335/article/details/85059365