32位进程经典内存布局

Linux内核 2.6.7 以前的默认进程内存布局形式,mmap区域与栈区域相对增长, 堆只有 1GB 的虚拟地址空间可以使用,继续增长就会进入 mmap 映射区域. 对于 64 位系统,提供了巨大的虚拟地址空间,这种布局就非常合适.

linux java 进程 gc后物理内存不变 linux进程内存布局_#define

32位进程默认内存布局

栈至顶向下扩展,并且栈是有界的。堆至底向上扩展,mmap 映射区域至顶向下扩展,mmap 映射区域和堆相对扩展,直至耗尽虚拟地址空间中的剩余区域,这种结构便于 C 运行时库使用 mmap 映射区域和堆进行内存分配。这种布局形式是在内核 2.6.7 以后才引入的,属于是 32 位模式下进程的默认内存布局形式

linux java 进程 gc后物理内存不变 linux进程内存布局_内存布局_02

64位进程内存布局

在 64 位模式下, 对于 AMD64 系统,内存布局采用经典内存布局,text 的起始地址为0x0000000000400000,堆紧接着 BSS 段向上增长,mmap 映射区域开始位置一般设为 TASK_SIZE/3 == 0x00002AAAAAAAA000, 栈顶地址 0x00007FFFFFFFF000

#define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE)
#define TASK_SIZE (test_thread_flag(TIF_IA32) ? IA32_PAGE_OFFSET : TASK_SIZE_MAX)
#define STACK_TOP TASK_SIZE
#define TASK_UNMAPPED_BASE (PAGE_ALIGN(TASK_SIZE / 3))

linux java 进程 gc后物理内存不变 linux进程内存布局_内存布局_03

当前内核默认配置下,进程的栈和 mmap 映射区域并不是从一个固定地址开始,并且每次启动时的值都不一样,程序在启动时随机改变这些值的设置,使得使用缓冲区溢出进行攻击更加困难
当然也可以让进程的栈和 mmap 映射区域从一个固定位置开始,只需要设置全局变量randomize_va_space值为 0 通过设置/proc/sys/kernel/randomize_va_space来停用该特性, 也可以使用命令

sudo sysctl -w kernel.randomize_va_space = 0

内存管理函数

对heap 的操作, 操作系统提供了 brk()函数, C 运行时库提供了sbrk()函数; 对 mmap 映射区域的操作,操作系统提供了mmap()和munmap()函数。sbrk(),brk() 或者 mmap() 都可以用来向进程添加额外的虚拟内存。Glibc 即使用这些函数向操作系统申请虚拟内存。

这里同时涉及到一个很重要的概念,内存的延迟分配,只有在真正访问一个地址的时候才建立这个地址的物理映射,这是 Linux 内存管理的基本思想之一。Linux 内核在用户申请内存的时候,只是给它分配了一个线性区(虚拟内存), 并没有分配实际物理内存; 只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户. 释放时即释放与虚拟内存对应的物理内存页面.

heap区域操作相关函数

Glibc 的 malloc 函数族(realloc,calloc 等)调用 sbrk()函数将数据段的下界移动,sbrk()函数在内核的管理下将虚拟地址空间映射到内存,供 malloc()函数使用。

内核数据结构 mm_struct 中的成员变量 start_code 和 end_code 是进程代码段的起始和
终止地址,start_data 和 end_data 是进程数据段的起始和终止地址,start_stack 是进程堆栈段起始地址,start_brk 是进程动态内存分配起始地址(堆的起始地址),还有一个 brk(堆的最后地址),就是动态内存分配当前的终止地址。C 语言的动态内存分配基本函数是malloc(),在 Linux 上的实现是通过内核的 brk 系统调用。brk()是一个非常简单的系统调用,只是简单地改变 mm_struct 结构的成员变量 brk 的值。函数定义如下

#include <unistd.h>
int brk(void *addr);
void *sbrk(intptr_t increment);

sbrk()的参数 increment 为 0 时, sbrk()返回的是进程的当前 brk 值, increment为正数时扩展 brk 值, 当 increment 为负值时收缩 brk 值.

mmap映射区域操作相关函数

mmap()函数将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的
大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。munmap 执行相反的操作,删除特定地址区域的对象映射, 函数定义为

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
参数:

(1) addr:映射区的开始地址。
(2) length:映射区的长度。
(3) prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过
or 运算合理地组合在一起。Ptmalloc 中主要使用了如下的几个标志:

  • PROT_EXEC //页内容可以被执行,ptmalloc 中没有使用
  • PROT_READ //页内容可以被读取,ptmalloc 直接用 mmap 分配内存并立即返回给用户时
    设置该标志
  • PROT_WRITE //页可以被写入,ptmalloc 直接用 mmap 分配内存并立即返回给用户时设
    置该标志
  • PROT_NONE //页不可访问,ptmalloc 用 mmap 向系统“批发”一块内存进行管理时设置
    该标志

(4) flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下标志的组合体

  • MAP_FIXED //使用指定的映射起始地址,如果由 start 和 len 参数指定的内存区重叠于现
    存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。Ptmalloc 在回收从系统中“批发”的内存时设置该标志。
  • MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。
    这个标志和以上标志是互斥的,只能使用其中一个。Ptmalloc每次调用mmap都设置该标志。
  • MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改
    的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信
    号。Ptmalloc 向系统“批发”内存块时设置该标志。
  • MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。Ptmalloc 每次调用 mmap
    都设置该标志。

(5) fd:有效的文件描述词。如果 MAP_ANONYMOUS 被设定,为了兼容问题,其值应为-1。
(6) offset:被映射对象内容的起点。