分页、分段和虚拟地址_基础知识

Thursday, February 23, 2023
本文共1816字
4分钟阅读时长
pwn

⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/principle/%E5%88%86%E9%A1%B5%E5%88%86%E6%AE%B5%E5%92%8C%E8%99%9A%E6%8B%9F%E5%9C%B0%E5%9D%80_%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/。商业转载请联系作者获得授权,非商业转载请注明出处!

Always remember that you are absolutely unique. Just like everyone else. — Margaret Mead

直接跑在物理地址

一开始人们将程序直接跑在物理地址上,但这样会出现几个问题:

  1. 地址空间和其他程序不隔离。 想象一下,如果你同时在跑两个不相干的程序,因为其中一个程序溢出了自己的地址空间而干扰到另外一个程序,导致这个程序崩溃……
  2. 内存使用效率低。 当计算机运行时,某个时间段内,它只是使用了一小部分的数据。其他数据在一个时间段内并不会被使用。而此时如果我们忽然需要运行一个新的程序,而此时内存空间不够,那我们要先将原来的程序写入磁盘,等到新程序运行完才能重新读回内存,这种频繁的IO操作极度浪费资源。(还有内存碎片的问题)
  3. 程序运行的地址不确定。 因为程序是直接跑在物理地址上的,而物理地址具有唯一性,那么就很难保证程序运行的时候地址是确定的。这就会对重定向造成很大的麻烦,静态链接的程序需要在程序每次运行时都根据程序当前运行的地址重新进行重定向,这简直是不可理喻的。

虚拟地址

既然不能跑在物理地址上,那我们可不可以将每次都不同的物理地址映射成一个确定的虚拟地址?比如一个程序的基址是0x84000,每次将这个程序装载入内存的时候就将分配到的物理地址映射成这个地址,然后程序就跑在这个虚拟地址上,通过一个映射和物理地址通信。

古早的分段

因为有了虚拟地址的想法,所以人们自然而然地想到将物理地址分成一段一段不同大小的映射成虚拟地址。

文内图片

这种想法和直接跑在物理地址上的想法差别不大,只是进行了地址空间的隔离与确定。

这样就可以解决之前提出的1、3两个问题。

  • 因为地址是虚拟的,所以操作系统可以监控这一段虚拟地址,一旦访问超出了虚拟地址空间,硬件就判定程序产生了非法的访问,并将其上报给操作系统或者监控程序,由它来判决。
  • 同时,因为地址是虚拟的,所以地址可以一直确定。(现在有些程序会使用PIE(Position-Independent Executable,位置无关可执行文件)让程序每次运行的基址都不一样来保护程序,但是这样必然会影响到程序的效率,因此一般只有一些安全性要求比较高的程序使用(但是gcc是默认有PIE的))

但是分段没有办法解决第二个问题,因为它本质上还是跑在物理地址上,只是对地址进行了处理。

分页

因此,人们想出了一种粒度更高的内存分割和映射方法——分页。

分页的思想是指把地址空间人为地分成大小相等的若干份,一份称为一页,就像一本书由很多页面组成,每个页面大小相等。也就是说内存条可以看为一本书,里面的物理内存被分为一页一页来管理,以页为单位对内存进行换入换出。即:

  • 当程序运行时,只需要将必要的数据从磁盘读取到内存,暂时用不到的数据先留在磁盘中,什么时候用到什么时候读取。
  • 当物理内存不足时,只需要将原来程序的部分数据写入磁盘,腾出足够的空间即可,不用把整个程序都写入磁盘。

(上面的内容来自计算机内存的分页机制_一颗日成的博客-CSDN博客

文内图片

(DP = Disk Page, VP = Virtual Page, PP = Physical Page)

对于现代操作系统来说,页的大小一般是4KB(比如 Intel Pentium 系列处理器支持 4KB 或 4MB 的页大小,那么操作系统可以选择每页大小为 4KB,也可以选择每页大小为 4MB,但是在同一时刻只能选择一种大小,所以对整个系统来说,也就是固定大小的。),这也是pwn中libc基址最后三位不变的原因。

程序的分段

在一个程序的虚拟空间之中,由于我们需要实现数据与指令等的隔离,所以我们仍然需要进行分段(分段的目的是为了更好地满足用户的需要

比如,在ELF中,我们会将程序分为text段、data段等,然后每个段再分为不同的节,如.text节、.rodata节、.bss节等,操作系统为他们分配了不同的rwx权限(读、写、可执行),且只有操作系统可以更改。具体可以看我博客的这篇文章ELF详解 - P3troL1er 的个人博客

有一些段是在编译链接期间就确定好的,有一些段,比如堆、栈是在装载的时候才确定的,宏观地看,整个虚拟空间长这样:

文内图片

(图片来源:堆概述 - CTF Wiki

一般来说,操作系统会使用ASLR(Address Space Layout Randomization,地址空间布局随机化)对堆、栈等在装载期间确定地址的段进行随机化保护,如上图中栈和堆的随机偏移量(Random offset)

不同的ASLR等级保护也不同,它常常会和PIE一起实现随机化:

ASLR Executable PLT Heap Stack Shared libraries
0 × × × × ×
1 × × ×
2 × ×
2+PIE

Linux下可以通过更改/proc/sys/kernel/randomize_va_space来设置ASLR等级

文内图片