Ana*_*dis 5 x86 x86-64 virtual-memory linux-kernel
通过运行一个简单的程序,less /proc/self/maps我发现大多数映射都以55and开头7F。我还注意到每当我调试任何二进制文件时都会使用这些范围。
此外,该评论在这里表明,内核的确有些范围的偏好。
这是为什么?上述范围是否有更深层次的技术原因?如果我mmap在这些前缀之外手动翻页会有问题吗?
首先,假设你说的是 x86-64,我们可以看到 x86-64的虚拟内存映射是:
========================================================================================================================
Start addr | Offset | End addr | Size | VM area description
========================================================================================================================
| | | |
0000000000000000 | 0 | 00007fffffffffff | 128 TB | user-space virtual memory, different per mm
__________________|____________|__________________|_________|___________________________________________________________
... | ... | ... | ...
Run Code Online (Sandbox Code Playgroud)
用户空间地址在 x86-64 中始终采用规范形式,仅使用低 48 位。看:
这将用户空间虚拟内存的末尾置于0x7fffffffffff. 这是新程序堆栈开始的地方:即0x7ffffffff000(减去由于ASLR引起的一些随机偏移)并增长到较低的地址。
让我先解决一个简单的问题:
如果我
mmap在这些前缀之外手动翻页会有问题吗?
一点也不,mmap系统调用总是检查被请求的地址,它会拒绝映射与已经映射的内存区域重叠的页面或完全无效地址(例如addr < mmap_min_addr或addr > 0x7ffffffff000)的页面。
现在...直接进入 Linux 内核代码,正是在内核 ELF 加载器 ( fs/binfmt_elf.c:960) 中,我们可以看到一个很长且带有解释性的注释:
========================================================================================================================
Start addr | Offset | End addr | Size | VM area description
========================================================================================================================
| | | |
0000000000000000 | 0 | 00007fffffffffff | 128 TB | user-space virtual memory, different per mm
__________________|____________|__________________|_________|___________________________________________________________
... | ... | ... | ...
Run Code Online (Sandbox Code Playgroud)
简而言之,ELF位置无关可执行文件有两种类型:
普通程序:它们需要加载器才能运行。这基本上代表了普通 Linux 系统上 99.9% 的 ELF 程序。加载器的路径在 ELF 程序头中指定,程序头类型为PT_INTERP。
加载器:加载器是一个ELF,不指定PT_INTERP程序头,负责加载和启动正常程序。在实际启动正在加载的程序之前,它还在幕后做一些花哨的事情(解决重定位、加载所需的库等)。
当内核通过execve系统调用执行新的 ELF 时,它需要将程序本身和加载器映射到内存中。然后控制将传递给加载器,加载器将解析和映射所有需要的共享库,最后将控制传递给程序。由于程序及其加载器都需要映射,内核需要确保这些映射不重叠(并且加载器未来的映射请求不会重叠)。
为了做到这一点,加载器被映射到堆栈附近(位于比堆栈低的地址,但有一定的容忍度,因为如果需要,可以通过添加更多页面来允许堆栈增长),从而将应用 ASLR 的职责留给mmap本身。然后使用 a 映射程序load_bias(如上面的代码段所示)以将其放置在离加载器足够远的位置(在一个低得多的地址处)。
如果我们看一下ELF_ET_DYN_BASE,我们会看到它依赖于架构,并且在 x86-64 上它评估为:
/*
* This logic is run once for the first LOAD Program
* Header for ET_DYN binaries to calculate the
* randomization (load_bias) for all the LOAD
* Program Headers, and to calculate the entire
* size of the ELF mapping (total_size). (Note that
* load_addr_set is set to true later once the
* initial mapping is performed.)
*
* There are effectively two types of ET_DYN
* binaries: programs (i.e. PIE: ET_DYN with INTERP)
* and loaders (ET_DYN without INTERP, since they
* _are_ the ELF interpreter). The loaders must
* be loaded away from programs since the program
* may otherwise collide with the loader (especially
* for ET_EXEC which does not have a randomized
* position). For example to handle invocations of
* "./ld.so someprog" to test out a new version of
* the loader, the subsequent program that the
* loader loads must avoid the loader itself, so
* they cannot share the same load range. Sufficient
* room for the brk must be allocated with the
* loader as well, since brk must be available with
* the loader.
*
* Therefore, programs are loaded offset from
* ELF_ET_DYN_BASE and loaders are loaded into the
* independently randomized mmap region (0 load_bias
* without MAP_FIXED).
*/
if (interpreter) {
load_bias = ELF_ET_DYN_BASE;
if (current->flags & PF_RANDOMIZE)
load_bias += arch_mmap_rnd();
elf_flags |= MAP_FIXED;
} else
load_bias = 0;
Run Code Online (Sandbox Code Playgroud)
基本上大约 2/3 的TASK_SIZE. 这load_bias是再调整增加arch_mmap_rnd()字节,如果ASLR启用,最后页对齐。归根结底,这就是为什么我们通常会看到以0x55for 程序开头的地址的原因。
当控制权传递给加载器时,进程的虚拟内存区域已经定义,mmap没有指定地址的连续系统调用将返回从加载器附近开始递减的地址。由于我们刚刚看到加载器映射到堆栈附近,并且堆栈位于用户地址空间的最末端,这就是为什么我们通常看到地址以0x7ffor libraries开头的原因。
上述情况有一个常见的例外。在直接调用加载器的情况下,例如:
/lib/x86_64-linux-gnu/ld-2.24.so ./myprog
Run Code Online (Sandbox Code Playgroud)
./mpyprog在这种情况下,内核不会映射,并将其留给加载器。因此,加载器./myprog将映射到某个0x7f...地址。
你可能想知道:为什么内核不总是让加载程序映射程序,或者为什么程序不在加载程序之前/之后映射?对此我没有 100% 明确的答案,但我想到了几个原因:
一致性:让内核自己将 ELF 加载到内存中,而不依赖于加载器,避免了麻烦。如果不是这种情况,内核将完全依赖于用户空间加载器,这是不可取的(这也可能部分是安全问题)。
效率:我们确信至少可执行文件和它的加载器都需要被映射(不管任何链接库),也可以节省宝贵的时间并立即完成,而不是等待另一个具有关联上下文切换的系统调用。
安全性:在默认情况下,将程序映射到与加载程序和其他库不同的随机地址处,在程序本身和加载的库之间提供了一种“隔离”。换句话说,“泄漏”任何库地址都不会泄露程序在内存中的位置,反之亦然。以与加载程序和其他库的预定义偏移量映射程序将部分违背 ASLR 的目的。
在理想的安全驱动方案中,每个mmap(即任何需要的库)也将被放置在一个独立于先前映射的随机地址,但这会显着损害性能。保持分配分组会导致更快的页表查找:请参阅了解 Linux 内核(第 3 版),第 606 页:表 15-3。每个基数树高度的最高索引和最大文件大小。它还会导致更大的虚拟内存碎片,成为需要将大文件映射到内存的程序的真正问题。程序代码和库代码之间隔离的实质部分已经完成,进一步的弊大于利。
易于调试:立即查看RIP=0x55...vsRIP=0x7f...有助于找出要查找的位置(程序本身或库代码)。