进程虚拟地址空间的内存映射段默认向哪个方向增长?

问题描述 投票:0回答:1

我目前正在研究将 ELF 从磁盘加载到内存的代码,这对应于 Linux 内核中的函数

load_elf_binary()

此类函数设置不同段的地址(例如文本、数据、bss、堆、堆栈、映射区域)。通过跟踪代码,我注意到一个函数:

setup_new_exec()
,它是在 /fs/exec.c 中定义的。在这样的函数内部,它调用arch_pick_mmap_layout()
,它的定义在
here。请注意,我并不是针对 X86 等特定架构,因此我指的是通用函数定义。

以下是部分代码:

if (mmap_is_legacy(rlim_stack)) { mm->mmap_base = TASK_UNMAPPED_BASE + random_factor; mm->get_unmapped_area = arch_get_unmapped_area; } else { mm->mmap_base = mmap_base(random_factor, rlim_stack); mm->get_unmapped_area = arch_get_unmapped_area_topdown; }
根据代码,我知道有两种获取未映射区域的方法 - 自下而上(遗留)和自上而下。这两种方式也在这篇

LWN文章中进行了讨论。

为了区分,我们需要

mmap_is_legacy()

,即return sysctl_legacy_va_layout;
sysctl_legacy_va_layout
默认初始化为0。

这是否意味着默认情况下,进程的内存映射区域从上到下增长(从高地址到低地址;从堆栈到堆增长)?

linux memory-management linux-kernel
1个回答
2
投票
您的一般假设

“默认情况下,进程的内存映射区域从上到下增长”是正确的。

现在的默认布局和旧版布局应该如下所示:

DEFAULT LEGACY 0xffffffffffffffff 0xffffffffffffffff stack stack 🡓 🡓 mmap ... 🡓 🡑 ... heap ... ELF 🡑 ... heap 🡑 ELF mmap ... ... 0x0000000000000000 0x0000000000000000

[...] 如今,传统布局的 mmap 段是低地址。有代码证明这一点吗?另外,现在的遗留内存布局是从虚拟地址0开始的吗?

当然,您可以在通用

arch_pick_mmap_layout()

 实现中链接的代码中准确地看到这一点,该实现为旧版布局选择较低的 mmap_base
。计算结果为 
TASK_UNMAPPED_BASE + random_factor
random_factor
 来自 ASLR,请参阅 
/proc/sys/kernel/randomize_va_space
)。请注意,某些架构(即 x86、PA-RISC、PowerPC、S390、Sparc)会覆盖该函数并提供自己的函数,但完成的计算几乎相同(您可以检查源代码)。

TASK_UNMAPPED_BASE

 代表“mmap”虚拟内存区域的下边界,并且它因体系结构而异。但它不应该被定义为低至 
0
(零)。它通常用 
TASK_SIZE
 来定义。

一些例子:

  • TASK_SIZE / 3
     = 
    0x2aaaaaaab000
     x86-64
  • TASK_SIZE / 3
     = x86 32 位上的 
    0x40000000
    ,默认 
    VMSPLIT_3G
     配置
  • TASK_SIZE / 4
     = ARM64 上的
    0x400000000000
    
    
  • CONFIG_PAGE_OFFSET / 3
     = ARM 32 位上的 
    0x40000000
    ,默认 
    VMSPLIT_3G
     配置
  • TASK_SIZE / 3
     = 
    0x5555555000
     在具有 40 VA 位的 MIPS 64 位上
  • TASK_SIZE / 8 * 3
     = 
    0x30000000
     在 PowerPC 8xx(32 位)上
用户空间可映射的最低可能地址实际上是

/proc/sys/vm/mmap_min_addr

 和默认值 非零(例如 x86 上的 0x10000
)。此类低地址必须通过提示 
mmap
 显式请求,由于 
mmap(0, ...)
,它们不会由内核自愿映射。

因此,我们确定对于遗留布局,“mmap”区域从某个低地址开始,并且我们已经知道堆栈始终从最高地址开始。

根据 ELF 本身,文件仅由内核根据其类型和程序头映射到内存中,通常是连续的,没有漏洞,并且计算是相同的无论默认/旧版 mmap 布局如何。您将看到 ELF 程序头中指定的多个段映射了不同的权限(请参阅 readelf -l

 的输出),并且这些段将包含不同的部分,例如 
.text
.rodata
.bss
 等(参见 
readelf -S
 的输出)。

对于 ELF

Executables(即 e_type

 = 
ET_EXEC
,参见 
man 5 elf
),基虚拟地址由 ELF 本身选择:它在编译时固定并确定,这样的 ELF 无法加载位于不同的地址以使其正常工作。

对于 ELF

共享对象(即 e_type

 = 
ET_DYN
),这是当今的标准,基虚拟地址由内核本身选择,并由 
ELF_ET_DYN_BASE
 定义(如果启用 ASLR,则进行调整) 。 
我的另一个答案涵盖了 x86。该值是 above TASK_UNMAPPED_BASE
,因此您将在旧布局中看到 ELF 
above 的“mmap”区域(较高地址),以及在默认布局中的 below it(较低地址)。

无论如何,根据定义,“堆”区域(又名

程序中断)将在 ELF 向高地址增长之后立即开始。


这里有几个带注释的屏幕截图(单击可放大),以显示在我的 x86-64 机器上检查

/proc/[pid]/maps

 时默认布局与旧布局的样子。 
请注意,低地址位于顶部

默认

default

传统

legacy

© www.soinside.com 2019 - 2024. All rights reserved.