目录
1. 简介 2. 进程虚拟地址空间 3. 内存映射的原理 4. 数据结构 5. 对区域的操作 6. 地址空间 7. 内存映射 8. 反向映射 9.堆的管理 10. 缺页异常的处理 11. 用户空间缺页异常的校正 12. 内核缺页异常 13. 在内核和用户空间之间复制数据
1. 简介
用户层进程的虚拟地址空间是Linux的一个重要抽象,它向每个运行进程提供了同样的系统视图,这使得多个进程可以同时运行,而不会干扰到其他进程内存中的内容,此外,它容许使用各种高级的程序设计技术,如内存映射,学习虚拟内存,同样需要考察可用物理内存中的页帧与所有的进程虚拟地址空间中的页之间的关联: 逆向映射(reverse mapping)技术有助于从虚拟内存页中跟踪到对应的物理内存页,而缺页处理(page fault handling)则允许从块设备按需读取数据填充虚拟地址空间(本质上是从块设备中读取数据物理内存中,并重新建立物理内存到虚拟内存的映射关系)
相对于物理内存的组织,或者内核的虚拟地址空间的管理(内核的虚拟内存是直接线性物理内存映射的),虚拟内存的管理更加复杂
1. 每个应用程序都有自身的地址空间,与所有其他应用程序分隔开 2. 通常在巨大的线性地址空间中,只有很少的段可用于各个用户空间进程,这些段彼此之间有一定的距离,内核需要一些数据结构,来有效地管理这些"随机"分布的段 3. 地址空间只有极小一部分与物理内存页直接关联(建立页表映射),不经常使用的部分,则仅当必要时与页帧关联(基于缺页中断机制的内存换入换出机制) 4. 内核信任自身,但无法信任用户进程,因此,各个操作用户地址空间的操作都伴随有各种检查,以确保程序的权限不会超出应用的限制,进而危及系统的稳定性和安全性 5. fork-exec模型在UNIX操作系统下用于产生新进程,内核需要借助一些技巧,来尽可能高效地管理用户地址空间
0x1: MMU(memory management unit)
需要明白的是,我们接下来讨论的内容都似乎基于系统有一个内存管理单元MMU,该单元支持使用虚拟内存,大多数"正常"的处理器都包含这个组件
内存管理单元(memory management unit MMU),有时称作分页内存管理单元(paged memory management unit PMMU)。它是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件。它的功能包括
1. 虚拟地址到物理地址的转换(即虚拟内存管理) 2. 内存保护 3. 中央处理器高速缓存的控制 4. 在较为简单的计算机体系结构中,负责总线的仲裁以及存储体切换(bank switching,尤其是在8位的系统上)
现代的内存管理单元是以页的方式,分区虚拟地址空间(处理器使用的地址范围)的;页的大小是2的n次方,通常为几KB。地址尾部的n位(页大小的2的次方数)作为页内的偏移量保持不变。其余的地址位(address)为(虚拟)页号。内存管理单元通常借助一种叫做转译旁观缓冲器(Translation Lookaside Buffer TLB)的相联高速缓存(associative cache)来将虚拟页号转换为物理页号。当后备缓冲器中没有转换记录时,则使用一种较慢的机制,其中包括专用硬件(hardware-specific)的数据结构(Data structure)或软件辅助手段
Each process a pointer (mm_struct→pgd) to its own Page Global Directory (PGD) which is a physical page frame. This frame contains an array of type pgd_t which is an architecture specific type defined in <asm/page.h>. The page tables are loaded differently depending on the architecture. On the x86, the process page table is loaded by copying mm_struct→pgd into the cr3 register which has the side effect of flushing the TLB. In fact this is how the function __flush_tlb() is implemented in the architecture dependent code.
Each active entry in the PGD table points to a page frame containing an array of Page Middle Directory (PMD) entries of type pmd_t which in turn points to page frames containing Page Table Entries (PTE) of type pte_t, which finally points to page frames containing the actual user data. In the event the page has been swapped out to backing storage, the swap entry is stored in the PTE and used by do_swap_page() during page fault to find the swap entry containing the page data. The page table layout is illustrated in Figure below
现代操作系统普遍采用多级(四级)页表的方式来组织虚拟内存和物理内存的映射关系,从虚拟地址内存到物理地址内存的翻译就是在进行多级页表的寻址过程
0x2: Describing a Page Table Entry
1. _PAGE_PRESENT: Page is resident in memory and not swapped out,该页是否应被高速缓冲的信息 2. _PAGE_PROTNONE: Page is resident but not accessable 3. _PAGE_RW: Set if the page may be written to 4. _PAGE_USER: Set if the page is accessible from user space,"特权位": 哪种进程可以读写该页的信息,例如用户模式(user mode)进程、还是特权模式(supervisor mode)进程 5. _PAGE_DIRTY: Set if the page is written to,"脏位"(页面重写标志位)(dirty bit): 表示该页是否被写过 6. _PAGE_ACCESSED: Set if the page is accessed,"访问位"(accessed bit): 表示该页最后使用于何时,以便于最近最少使用页面置换算法(least recently used page replacement algorithm)的实现
有时,TLB或PTE会禁止对虚拟页的访问,这可能是因为没有物理随机存取存储器(random access memory)与虚拟页相关联。如果是这种情况,MMU将向CPU发出页错误(page fault)的信号。操作系统将进行处理,也许会尝试寻找RAM的空白帧,同时创建一个新的PTE将之映射到所请求的虚拟地址。如果没有空闲的RAM,可能必须关闭一个已经存在的页面,使用一些替换算法,将之保存到磁盘中(这被称之为页面调度(paging))
Relevant Link:
http://zh.wikipedia.org/wiki/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E5%8D%95%E5%85%83 https://www.kernel.org/doc/gorman/html/understand/understand006.html
2. 进程虚拟地址空间
各个进程的虚拟地址空间范围为: [0 ~ TASK_SIZE - 1],再往"上"(栈高地址空间)是内核地址空间,在IA-32系统上地址空间的范围为: [0 ~ 2^32] = 4GB,总的地址空间通常按3:1划分
与系统完整性相关的非常重要的一个方面是,用户程序只能访问整个地址空间的下半部分,不能访问内核部分,如果没有预先达成"协议(即IPC机制)",用户进程也不能操作另一个进程的地址空间,因为不同进程的地址空间互相是不可见的
值得注意的是,无论当前哪个用户进程处于活动状态,虚拟地址空间内核部分的内容总是同样的,实现这个功能的技术取决于具体的硬件
1. 通过操作各个用户进程的页表,使得虚拟地址空间的上半部分(内核高地址段)看上去总是相同的 2. 指示处理器为内核提供一个独立的地址空间,映射在各个用户地址空间之上
虚拟地址空间由许多不同长度的段组成,用于不同的目的,必须分别处理,例如在大多数情况下,不允许修改.text段,但必须可以执行其中内容。另一方面,必须可以修改映射到地址空间中的文本文件内容,而不能允许执行其内容,文件内容只是数据,而并非机器代码
从这里我们也可以看到操作提供的安全机制本质上是一个自上而下的过程,每一级都对上游负责,并对下游提出要求
1. 进程ELF的节区中的属性标志指明了当前节区(段)具有哪些属性(R/W/RW/E),这需要操作系统在内存的PTE页表项中提供支持 2. PTE中的_PAGE_RW位表示该页具有哪些读写属性,而需要禁止某些内存页的二进制字节被CPU执行,需要底层CPU提供硬件支持 3. CPU的NX bit位提供了硬件级的禁止内存页数据执行支持,这也是DEP技术的基础
0x1: 进程地址空间的布局
虚拟地址空间中包含了若干区域,其分布方式是特定于体系结构的,但所有方法都有下列共同成分
1. 当前运行代码的二进制代码,该代码通常称之为.text,所处的虚拟内存区域称之为.text段 2. 程序使用的动态库的代码 3. 存储全局变量和动态产生的数据的堆 4. 用于保存局部变量和实现函数/过程调用的栈 5. 环境变量和命令行参数的段 6. 将文件内容映射到虚拟地址空间中的内存映射
我们知道,系统中的各个进程都具有一个struct mm_struct的实例,关于数据结构的相关知识,参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/3865490.html //0x1: struct mm_struct
各个体系结构可以通过几个配置选项影响虚拟地址空间的布局
1. 如果体系结构想要在不同mmap区域布局之间作出选择,则需要设置HAVE_ARCH_PICK_MMAP_LAYOUT,并提供arch_pick_mmap_layout函数 2. 在创建新的内存映射时,除非用户指定了具体的地址,否则内核需要找到一个适当的位置,如果体系结构自身想要选择合适的位置,则必须设置预处理器符号HAVE_ARCH_UNMAPPED_AREA,并相应地定义arch_get_unmapped_area函数 3. 在寻找新的内存映射低端内存位置时,通常从较低的内存位置开始,逐渐向较高的内存地址搜索,内核提供了默认的函数arch_get_unmapped_area_topdown用于搜索,但如果某个体系结构想要提供专门的实现,则需要设置预处理符号HAVE_ARCH_UNMAPPED_AREA 4. 通常,栈自顶向下增长,具有不同处理方式的体系结构需要设置配置选项CONFIG_STACK_GROWSUP
最后,我们需要考虑进程标志PF_RANDOMIZE,如果设置了该标志,则内核不会为栈和内存映射的起点选择固定位置,而是在每次新进程启动时随机改变这些值的设置,这引入了一些复杂性,例如使得缓冲区溢出攻击更加困难,如果攻击者无法依靠固定地址找到栈,那么想要构建恶意代码,通过缓冲区溢出获得栈内存区域的访问权从而恶意操纵栈的内容,将会困难得多
1. .text段
.text段如何映射到虚拟地址空间中由ELF标准确定,每个体系结构都指定了一个特定的起始地址
1. IA-32: 起始于0x08048000 //在text段的起始地址与最低的可用地址之间大约有128MB的间距,用于捕获NULL指针,其他体系结构也有类似的缺口 2. UltraSparc: 起始于0x100000000 3. AMD64: 0x0000000000400000
2. 堆
堆紧接着text段开始,向上增长
3. MMAP
用于内存映射的区域起始于mm_struct->mmap_base,通常设置为TASK_UNMAPPED_BASE,每个体系结构都需要定义,在几乎所有的情况下,其值都是TASK_SIZE / 3,值得注意的是,内核的默认配置,mmap区域的起始点不是随机的,即ASLR不是默认开启的,需要特定的内核配置
4. 栈
栈起始于STACK_TOP(栈是从高地址向低地址生长的,这里的起始指的最高的地址位置),如果设置了PF_RANDOMIZE,则起始点会减少一个小的随机量,每个体系结构都必须定义STACK_TOP,大多数都设置为TASK_SIZE,即用户地址空间中最高可用地址。进程的参数列表和环境变量都是栈的初始数据(即位于栈的最高地址区域)
如果计算机提供了巨大的虚拟地址空间,那么使用上述的地址空间布局会工作地非常好,但在32位计算机上可能会出现问题。考虑IA-32的情况,虚拟地址空间从0~0xC0000000,每个用户进程有3GB可用,TASK_UMMAPPED_BASE起始于0x4000000,即1GB处( TASK_UMMAPPED_BASE = TASK_SIZE / 3),这意味着堆只有1GB空间可供使用,继续增长会进入到mmap区域,这显然不可接受
这里的关键问题在于: 内存映射区域位于虚拟地址空间的中间
因此,在内核版本2.6.7开发期间为IA-32计算机引入了一个新的虚拟地址空间布局
新的布局的思想在于使用固定值限制栈的最大长度(简单来说是将原本栈和mmap共享增长的空间,转移到了mmap和堆共享增长空间)。由于栈是有界的,因此安置内存映射的区域可以在栈末端的下方立即开始,与经典方法相反,mmap区域是自顶向下扩展,由于堆仍然位于虚拟地址空间中较低的区域并向上增长,因此mmap区域和堆可以相对扩展,直至耗尽虚拟地址空间中剩余的区域。
为了确保栈与mmap区域不发生冲突,两者之间设置了一个安全隙
0x2: 建立布局
在do_execve()函数的准备阶段,已经从可执行文件头部读入128字节存放在bprm的缓冲区中,而且运行所需的参数和环境变量也已收集在bprm中
search_binary_handler()函数就是逐个扫描formats队列,直到找到一个匹配的可执行文件格式,运行的事就交给它
1. 如果在这个队列中没有找到相应的可执行文件格式,就要根据文件头部的信息来查找是否有为此种格式设计的可动态安装的模块 2. 如果找到对应的可执行文件格式,就把这个模块安装进内核,并挂入formats队列,然后再重新扫描
在linux_binfmt数据结构中,有三个函数指针
1. load_binary load_binary就是具体的ELF程序装载程序,不同的可执行文件其装载函数也不同 1) a.out格式的装载函数为: load_aout_binary() 2) elf的装载函数为: load_elf_binary() 3) .. 2. load_shlib 3. core_dump
对于ELF文件对应的linux_binfmt结构体来说,结构体如下
static struct linux_binfmt elf_format = { .module = THIS_MODULE, .load_binary = load_elf_binary, .load_shlib = load_elf_library, .core_dump = elf_core_dump, .min_coredump = ELF_EXEC_PAGESIZE, .hasvdso = 1 };
在使用load_elf_binary装载一个ELF二进制文件时,将创建进程的地址空间
如果进程在ELF文件中明确指出需要ASLR机制(即PF_RANDOMIZE被置位)、且全局变量randomize_va_space设置为1,则启动地址空间随机化机制。此外,用户可以通过/proc/sys/kernel/randomize_va_space停用内核对该特性的支持
static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs) { .. if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space) current->flags |= PF_RANDOMIZE; setup_new_exec(bprm); /* Do this so that we can load the interpreter, if need be. We will change some of these later */ current->mm->free_area_cache = current->mm->mmap_base; current->mm->cached_hole_size = 0; retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack); if (retval < 0) { send_sig(SIGKILL, current, 0); goto out_free_dentry; } current->mm->start_stack = bprm->p; .. }
这再次说明了ASLR这种安全机制是需要操作系统内核支持,并且编译器需要显示指出需要开启指定功能的互相配合的这种模式
static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs) { .. /* Flush all traces of the currently running executable */ retval = flush_old_exec(bprm); if (retval) goto out_free_dentry; } int flush_old_exec(struct linux_binprm * bprm) { .. retval = exec_mmap(bprm->mm); if (retval) goto out; .. } EXPORT_SYMBOL(flush_old_exec); static int exec_mmap(struct mm_struct *mm) { .. arch_pick_mmap_layout(mm); .. }
选择布局的工作由arch_pick_mmap_layout完成,如果对应的体系结构没有提供一个具体的函数,则使用内核的默认例程,我们额外关注一下IA-32如何在经典布局和新的布局之间选择
\linux-2.6.32.63\arch\x86\mm\mmap.c
/* 如果设置了personality比特位,或栈的增长不受限制,则回退到标准布局 */ static int mmap_is_legacy(void) { if (current->personality & ADDR_COMPAT_LAYOUT) return 1; if (current->signal->rlim[RLIMIT_STACK].rlim_cur == RLIM_INFINITY) return 1; //否则,使用新的布局方式 return sysctl_legacy_va_layout; } /* * This function, called very early during the creation of a new * process VM image, sets up which VM layout function to use: */ void arch_pick_mmap_layout(struct mm_struct *mm) { if (mmap_is_legacy()) { mm->mmap_base = mmap_legacy_base(); mm->get_unmapped_area = arch_get_unmapped_area; mm->unmap_area = arch_unmap_area; } else { mm->mmap_base = mmap_base(); mm->get_unmapped_area = arch_get_unmapped_area_topdown; mm->unmap_area = arch_unmap_area_topdown; } }
在经典的配置下,mmap区域的起始点是TASK_UNMAPPED_BASE(0x4000000),而标准函数arch_get_unmapped_area用于自下而上地创建新的映射
在使用新布局时,内存映射自顶向下下增长,标准函数arch_get_unmapped_area_topdown负责该工作,我们着重关注一下如何选择内存映射的基地址
\linux-2.6.32.63\arch\x86\mm\mmap.c
static unsigned long mmap_base(void) { unsigned long gap = current->signal->rlim[RLIMIT_STACK].rlim_cur; /* 在新的进程布局中,可以根据栈的最大长度,来计算栈最低的可能位置,用作mmap区域的起始点,但内核会确保栈至少可以跨越128MB的空间(即栈最小要有128MB的空间) 另外,如果指定的栈界限非常巨大,那么内核会保证至少有一小部分地址空间不被栈占据 */ if (gap < MIN_GAP) gap = MIN_GAP; else if (gap > MAX_GAP) gap = MAX_GAP; //如果要求使用地址空间随机化机制,则栈的 起始位置会减去一个随机的偏移量,最大为1MB,另外,内核会确保该区域对齐到页帧,这是体系结构的要求 return PAGE_ALIGN(TASK_SIZE - gap - mmap_rnd()); }
从某种程度上来说,64位体系结构的情况会好一点,因为不需要在不同的地址空间布局中进行选择,在64位体系结构下,虚拟地址空间如此巨大,以至于堆和mmap区域的碰撞几乎不可能,所以依然可以使用老的进程布局空间,AMD64系统上对虚拟地址空间总是使用经典布局,因此无需区分各种选项,如果设置了PF_RANDOMIZE标志,则进行地址空间随机化,变动原本固定的mmap_base
我们回到最初对load_elf_binary函数的讨论上来,该函数最后需要在适当的位置创建栈
static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs) { .. retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack); if (retval < 0) { send_sig(SIGKILL, current, 0); goto out_free_dentry; } .. }
标准函数setup_arg_pages即用于建立在适当的位置创建栈,该函数需要栈顶的位置作为参数,栈顶由特定于体系结构的常数STACK_TOP给出,而后调用randomize_stack_top,确保在启用地址空间随机化的情况下,对该地址进行随机偏移
3. 内存映射的原理
由于所有用户进程总的虚拟地址空间比可用的物理内存大得多,因此只有最常用的部分才会与物理页帧关联,大部多程序只占用实际内存的一小部分。内核必须提供数据结构,以建立虚拟地址空间的区域和相关数据所在位置之间的关联,例如在映射文本文件时,映射的虚拟内存区必须关联到文件系统在硬盘上存储文件内容的区域
需要明白的是,文件数据在硬盘上的存储通常并不是连续的,而是分布到若干小的区域,内核利用address_space数据结构,提供一组方法从"后备存储器"读取数据,例如从文件系统读取,因此address_space形成了一个辅助层,将映射的数据表示为连续的线性区域,提供给内存管理子系统
按需分配和填充页称之为"按需调页法(demand paging)",它基于处理器和内核之间的交互