离久的小站

简述虚拟内存

2020/06/01 Share

虚拟内存实现的简单介绍

虚拟内存是什么 ?

操作系统通过虚拟内存的映射机制,使得每个进程拥有看起来连续且完全隔离和独立的内存空间。

操作系统对虚拟内存的分配和管理是以页为单位。

当将一个可执行文件或者动态库加载到内存中执行时,操作系统会将文件中的代码段部分和数据段部分的内容通过内存映射文件的形式映射到对应的虚拟内存区域中。

虚拟内存的权限

代码段在不同的操作系统中权限不一样。

在 iOS 中,可执行代码所在的虚拟内存区域的权限只能是可执行的。

这也就是说我们不可以在具有可读写权限的内存区域中(比如堆内存或者栈内存空间)动态的构造出指令来供 cpu 执行。

iOS 系统中不支持将某段内存的保护机制先设置为读写以便填充好数据后再设置为可执行的保护机制来实现动态的指令构造(也就是所谓的 JIT 技术)。

操作系统提供了虚拟内存的 remap 机制来解决这个问题。

remap

所谓虚拟内存的 remap 机制就是可以将新分配的虚拟内存页重新映射到已经分配好的虚拟内存页中。

新分配的虚拟内存页可以和已经存在的虚拟内存页中的内容保持一致,并且可以继承原始虚拟内存页面的保护权限。

虚拟内存的 remap 机制使得进程之间或者进程内中的虚拟内存共享相同的物理内存。

  1. 虚拟内存分配其本质就是在页表中建立一个从虚拟内存页到物理内存页的映射关系。而 remap 就是将不同的虚拟页号映射到同一个物理页号而已。就如例子中进程1的第1页和第4页都是映射在同一个6号物理页中。

  2. 不同进程之间的不同虚拟页号可以映射到相同的物理页号。这样的一个应用是解决动态库的共享加载问题。上面的例子中进程1的第5页和进程2的第7页共享相同的物理内存第9页。

  3. 操作系统还会维持一个全局物理页空闲信息表,用来记录当前未被分配的物理内存。这样一旦有进程需要分配虚拟内存空间时就从这个表中查找空闲的区域进行快速分配。

相关代码

完整例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/// 因为新分配的虚拟内存是以页为单位的,所以要被映射的内存也要页对齐,所以这里的函数起始地址是以页为单位对齐的。
int __attribute__ ((aligned (PAGE_MAX_SIZE))) testfn(int a, int b)
{
int c = a + b;
return c;
}

int main(int argc, char *argv[])
{
/// 获取一页虚拟内存的大小
vm_size_t page_size = 0;
host_page_size(mach_host_self(), &page_size);
vm_address_t addr = 0;

/// vm_allocate 在当前进程内的空闲区域中分配出一页虚拟内存出来 ,addr指向虚拟内存的开始位置。
kern_return_t ret = vm_allocate(mach_task_self(), &addr, page_size, VM_FLAGS_ANYWHERE);
if (ret == KERN_SUCCESS)
{
/// addr被分配出来后,我们可以对这块内存进行读写操作,但不可作为执行
memcpy((void*)addr, "Hello World!\n", 14); /// 文本长度为14

/// 执行上述代码后,这时候内存 addr 的内容除了最开始有 "Hello World!\n" 其他区域是一篇空白,而且并不是可执行的代码区域。
printf((const char*)addr);

/// 执行完 vm_remap 重映射后 addr 的内存将被重新映射到 testfn 函数所在的内存页中。
/// 这时候 addr 所指的内容和函数 testfn 的代码保持一致。
/// "Hello World!\n" 被覆盖了
vm_prot_t cur, max;
ret = vm_remap(mach_task_self(), &addr, page_size, 0, VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE, mach_task_self(), (vm_address_t)testfn, false, &cur, &max, VM_INHERIT_SHARE);

if (ret == KERN_SUCCESS)
{
/// 执行testfn函数
int c1 = testfn(10, 20);
/// addr重新映射后将和testfn函数具有相同内容,所以这里可以将addr当做是testfn函数一样被调用。
int c2 = ((int (*)(int,int))addr)(10,20);
}

/// 释放虚拟内存
vm_deallocate(mach_task_self(), addr, page_size);
}

return 0;
}

上述代码其实是需要对 testfn 的函数体所有约束的,这个约束就是testfn中不能出现一些常量以及全局变量以及不能再出现函数调用。

原因是这些操作在编译为机器指令后访问这些数据都是通过相对偏移来实现的,因此如果addr映射成功后因为函数实现的基地址有变化,如果通过addr进行访问时,那么指令中的相对偏移值将是一个错误的结果,从而造成函数调用时的崩溃发生。

vm_allocate 与 malloc

vm_allocate可以用来实现虚拟内存的分配,malloc也可以用来实现堆内存的分配,这两者之间有什么关系呢?

vm_allocate是更加底层的内存管理API,而且分配的内存的尺寸都是以页的倍数作为边界的。

malloc是高级内存管理API,一个进程的堆内存区域在实现中其实是先通过 vm_allocate 分配出来一大片内存区域(包括栈内存也如此)。然后再在这块大的内存区域上进行分割管理以及空闲复用等等高级操作来实现一些零碎和范围内存分配操作。

CATALOG
  1. 1. 虚拟内存实现的简单介绍
    1. 1.1. 虚拟内存是什么 ?
    2. 1.2. 虚拟内存的权限
    3. 1.3. remap
    4. 1.4. 相关代码
      1. 1.4.1. 完整例子
      2. 1.4.2. vm_allocate 与 malloc