1 内存管理概述
内存管理子系统可分为用户空间,内核空间和硬件3个层面。
1.1 用户空间
应用程序使用malloc申请内存,free释放内存;malloc和free是glibc库的内存分配器ptmalloc提供的接口,ptmalloc使用系统调用brk或mmap向内核以页为单位申请内存,然后划分为小内存块分配给应用程序。
1.2 内核空间
1.2.1 内核空间的基本功能
虚拟内存管理负责从进程的虚拟地址空间分配虚拟页,sys_brk用来扩大或收缩堆,sys_mmap用来在内存映射区分配虚拟页,sys_munmap用来释放虚拟页。
内核使用延迟分配物理内存的策略,进程第一次访问虚拟页的时候,触发页错误异常,页错误异常处理程序从页分配器申请物理页,在进程的页表中把虚拟页映射到物理页。
页分配器负责分配物理页,当前使用的页分配器是伙伴分配器。
内核空间提供了把页划分成小内存块分配的块分配器,提供分配内存的接口kmalloc和kfree;支持三种块分配器:SLAB分配器,SLUB分配器和SLOB分配器。
1.2.2 内核空间的扩展功能
不连续页分配器提供了分配内存的接口vmalloc和vfree;在内存碎片化的时候,申请连续物理页的成功率很低,可以申请不连续的物理页,映射到连续的虚拟页,即虚拟地址连续而物理地址不连续。
每处理器内存分配器用来为每处理器变量分配内存。
连续内存分配(CMA)用来给驱动程序预留一段连续的内存,当驱动程序不用的时候,可以给进程使用;当驱动程序需要的时候,把进程占用的内存通过回收或迁移的方式让出来,给驱动程序使用。
内存控制组用来控制进程占用的内存资源。
在内存不足的时候,页回收负责回收物理页,对于没有后备存储设备支持的匿名页把数据换出到交换区,然后释放物理页;对于有后备存储设备支持的文件,把数据写回到存储设备,然后释放物理页。如果页回收失败,使用最后一招,out-of-memory killer,选择进程杀掉。
1.3 硬件层面
处理器包含一个称为内存管理单元MMU的部件,负责把虚拟地址转换成物理地址。
内存管理单元包含一个称为TLB的部件,保存最近使用过的页表映射,避免每次把虚拟地址转换成物理地址都需要查询内存中的页表。
为了解决处理器的执行速度和内存的访问速度不匹配的问题,在处理器和内存之间增加了缓存cache。缓存通常分为l1,l2,l3,为了支持并行地取指令和数据,l1 cache分为数据缓存和指令缓存。
2 虚拟地址空间布局
2.1 虚拟地址空间划分
实际ARM64不支持完全的64位虚拟地址:
(1)虚拟地址的最大宽度是48位,内核虚拟地址在64位地址空间的顶部,高16位全为1,范围是0xFFFF 0000 0000 0000到0xFFFF FFFF FFFF FFFF,用户态的虚拟地址在64位空间的底部,高16位全为0,范围是0x0000 0000 0000 0000到0x0000 FFFF FFFF FFFF。
(2)如果处理器实现了ARM8.2标准的大虚拟地址(LVA)支持,并且页长度是64KB,那么虚拟地址的最大宽度是52位。
(3) 可以为虚拟地址配置比最大宽度小的宽度,并且可以为内核虚拟地址和用户虚拟地址配置不同的宽度。转换控制器TCR_EL1的字段T0SZ定义了必须是全0的最高位的数量,字段T1SZ定义了必须是全1的最高位的数量,用户虚拟地址的宽度是(64 - TCR_EL1.T0SZ),内核虚拟地址的宽度是(64 - TCR_EL1.T1SZ)。
在ARM64架构的Linux内核中,内核虚拟地址和用户虚拟地址空间的宽度相同。
所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟空间,同一个线程组的用户线程共享用户虚拟空间,内核线程没有用户虚拟空间。
2.2 用户虚拟地址空间布局
2.2.1 TASK_SIZE
进程的用户虚拟地址空间的起始地址是0,长度是TASK_SIZE,由每种处理器架构定义自己的宏TASK_SIZE。ARM64架构定义的宏TASK_SIZE如下所示。
(1)32位用户空间程序:TASK_SIZE的值是TASK_SIZE_32,即0x100000000,等于4GB。
(2)64位用户空间程序:TASK_SIZE的值是TASK_SIZE_64,即2的VA_BITS次方字节,VA_BITS在内核编译时选择的虚拟地址位数。
#ifdef CONFIG_COMPAT
#define TASK_SIZE_32 UL(0x100000000)
#define TASK_SIZE (test_thread_flag(TIF_32BIT) ? \
TASK_SIZE_32 : TASK_SIZE_64)
#define TASK_SIZE_OF(tsk) (test_tsk_thread_flag(tsk, TIF_32BIT) ? \
TASK_SIZE_32 : TASK_SIZE_64)
#else
2.2.2 mm_struct
进程的用户虚拟地址空间包含以下区域:
(1)代码段,数据段和未初始化的数据段。
(2)动态库的代码段,数据段,和未初始化的数据段。
(3)存放动态生成的数据的堆。
(4)存放局部变量和实现函数调用的栈。
(5)存放在栈底部的环境变量和参数字符串。
(6)把文件区间映射到虚拟地址空间的内存映射区域。
内核使用内存描述符mm_struct描述进程的用户虚拟地址空间,成员如下:
struct mm_struct {
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;
u64 vmacache_seqnum; /* per-thread vmacache */
#ifdef CONFIG_MMU
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
#endif
unsigned long mmap_base; /* base of mmap area */
unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
unsigned long task_size; /* size of task vm space */
unsigned long highest_vm_end; /* highest vma end address */
pgd_t * pgd;
atomic_t mm_users; /* How many users with user space? */
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
atomic_long_t nr_ptes; /* PTE page table pages */
#if CONFIG_PGTABLE_LEVELS > 2
atomic_long_t nr_pmds; /* PMD page table pages */
#endif
int map_count; /* number of VMAs */
spinlock_t page_table_lock; /* Protects page tables and some counters */
struct rw_semaphore mmap_sem;
struct list_head mmlist;
unsigned long hiwater_rss; /* High-watermark of RSS usage */
unsigned long hiwater_vm; /* High-water virtual memory usage */
unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
unsigned long pinned_vm; /* Refcount permanently increased */
unsigned long shared_vm; /* Shared pages (files) */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE */
unsigned long stack_vm; /* VM_GROWSUP/DOWN */
unsigned long def_flags;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
struct mm_rss_stat rss_stat;
struct linux_binfmt *binfmt;
cpumask_var_t cpu_vm_mask_var;
/* Architecture-specific MM context */
mm_context_t context;
unsigned long flags; /* Must use atomic bitops to access the bits */
struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
spinlock_t ioctx_lock;
struct kioctx_table __rcu *ioctx_table;
#endif
#ifdef CONFIG_MEMCG
struct task_struct __rcu *owner;
#endif
struct user_namespace *user_ns;
/* store ref to file /proc/<pid>/exe symlink points to */
struct file __rcu *exe_file;
#ifdef CONFIG_MMU_NOTIFIER
struct mmu_notifier_mm *mmu_notifier_mm;
#endif
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
pgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
#ifdef CONFIG_CPUMASK_OFFSTACK
struct cpumask cpumask_allocation;
#endif
#ifdef CONFIG_NUMA_BALANCING
unsigned long numa_next_scan;
/* Restart point for scanning and setting pte_numa */
unsigned long numa_scan_offset;
/* numa_scan_seq prevents two threads setting pte_numa */
int numa_scan_seq;
#endif
#if defined(CONFIG_NUMA_BALANCING) || defined(CONFIG_COMPACTION)
bool tlb_flush_pending;
#endif
#ifdef CONFIG_ARCH_WANT_BATCHED_UNMAP_TLB_FLUSH
/* See flush_tlb_batched_pending() */
bool tlb_flush_batched;
#endif
struct uprobes_state uprobes_state;
#ifdef CONFIG_X86_INTEL_MPX
/* address of the bounds directory */
void __user *bd_addr;
#endif
#ifdef CONFIG_HUGETLB_PAGE
atomic_long_t hugetlb_usage;
#endif
};
进程描述符中和内存描述符相关的成员
进程描述符的成员 | 说明 |
---|---|
struct mm_struct *mm | 进程的mm指向一个内存描述符,内核线程没有用户虚拟地址空间,所以mm是空指针 |
struct mm_struct *active_mm | 进程中的active_mm和mm总是指向同一个内存描述符,内核线程的active_mm在没有运行时是空指针,在运行时指向从上一个进程借用的内存描述符 |
2.2.3 虚拟地址空间随机化
为了使缓冲区溢出攻击更加困难,内核支持为内存映射区域、栈和堆选择随机的起始地址。进程是否使用虚拟地址空间随机化功能,由以下两个因素共同决定:
(1)进程描述符的成员personality(个性化)是否设置ADDR_NO_RANDOMIZE。
(2)全局变量randomize_va_space:0表示关闭虚拟地址空间随机化,1表示使内存映射区域和栈的起始地址随机化,2表示使内存映射区域,栈和堆的起始地址随机化。
2.2.4 用户虚拟地址空间布局
用户虚拟地址空间布局有两种,区别是内存映射区域的起始位置和增长方向不同。
(1)传统布局:内存映射区域自底向上增长(地址增大的方向),mm->mmap_base = TASK_UNMAPPED_BASE + 随机值。
(2)新布局:内存映射区域自顶向下增长(地址减小的方向),mm->mmap_base = STACK_TOP - 栈的最大长度 - 间隙 - 随机值。
2.2.5 内核地址空间布局
(1)线性映射区:范围[PAGE_OFFSET, 264-1],大小是内核虚拟地址空间的一半。称为线性映射区域的原因是虚拟地址和物理地址是线性关系。
虚拟地址 = ((物理地址 - PHYS_OFFSET) + PAGE_OFFSET),其中PHYS_OFF是内存的起始物理地址。
(2)vmemmap区域的范围是[VMEMMAP_START,PAGE_OFFSET),大小是VMEMMAP_SIZE(线性映射区域的长度/页长度 * page结构体的长度上限)。内核使用page结构描述一个物理页,内存的所有物理页对应一个page结构体数组。如果内存的物理地址空间不连续,存在很多空洞,称为稀疏内存。vmemmap区域是稀疏内存的page结构体数组的虚拟地址空间。
(3)PCI I/O区域的范围是[PCI_IO_START, PCI_IO_END),长度是16MB,结束地址是PCI_IO_END = (VMEMMAP_START - 2MB);PCI设备的I/O地址空间。
(4)固定映射区域的范围是[FIXADDR_START,FIXADDR_TOP),长度是FIXADDR_SIZE,结束地址是FIXADDR_TOP=(PCI_IO_START - 2MB)。固定地址是编译时的特殊虚拟地址,编译的时候是一个常量,在内核初始化的时候映射到物理地址。
(5)vmalloc区域是[VMALLOC_START,VMALLOC_END),起始地址是VMALLOC_START,等于内核模块区域的结束地址。vmalloc区域是函数vmalloc使用的虚拟地址空间,内核使用vmalloc分配虚拟地址连续但物理地址不连续的内存。
内核镜像在vmalloc区域,起始地址是(KIMAGE_VADDR + TEXT_OFFSET),其中KIMAGE_VADDR 是内核镜像的虚拟地址的基准值,TEXT_OFFSET是内存中内核镜像相对内存起始位置的偏移。
(6)内核模块区域的范围是[MODULES_VADDR, MODULES_END),长度是128MB,内核模块区域是内核模块使用的虚拟地址空间。
(7)KASAN的起始地址是内核虚拟地址空间的起始地址,长度是内核虚拟空间的1/8。
KASAN是一个动态的内存错误检查工具。它为发现UAF和越界访问这两类缺陷提供了快速和综合的结局方案。
3 物理地址空间
3.1 物理地址
物理地址是处理器在系统总线上看到的地址。使用精简指令集RISC的处理器通常只实现一个物理地址空间,外围设备和物理内存使用统一的物理地址空间。 有些处理器把分配给外围设备的物理地址区域称为设备内存。
处理器通过外围设备控制器的寄存器访问外围设备,寄存器分为控制寄存器,状态寄存器和数据寄存器三大类,外围设备的寄存器通常被连续编址。处理器对外围设备寄存器的编址方式有两种:
(1) I/O映射方式:英特尔的x86处理器为外围设备专门实现了一个单独的地址空间,称为I/O地址空间或者I/O端口空间,处理器通过专门的I/O指令(如x86的in和out指令)来访问这一空间的地址单元。
(2)内存映射方式:使用精简指令集的处理器通常只实现一个物理地址空间,外围设备和物理内存使用统一的物理地址空间,处理器可以像访问一个内存单元那样访问外围设备,不需要提供专门的I/O指令。
3.2 外设寄存器映射
程序只能通过虚拟地址访问外设寄存器,内核提供了以下函数来把外设寄存器的物理地址映射到虚拟地址空间。
(1)函数ioremap把外设寄存器的物理地址映射到内核虚拟地址空间。
(2)函数io_remap_pfn_range把外设寄存器的物理地址映射到进程的用户虚拟地址空间。
3.3 ARM64架构的实现
3.3.1 ARM64定义了两种内存类型:
(1) 正常内存:包括物理内存和只读存储器。
(2) 设备内存:指分配给外围设备寄存器的物理地址区域。
对于正常内存,可以设置共享属性和缓存属性。共享属性用来定义一个位置是否可以被多个核共享,分为不可共享,内部共享和外部共享。不可共享是指只被处理器的一个核使用,内部共享是指一个处理器的所有核或多个处理器共享,外部共享是指处理器和其他观察者(GPU,DMA等)共享,缓存属性用来定义访问时是否通过处理器的缓存。
设备内存的共享属性总是外部共享,缓存属性总是不可缓存(即必须绕过处理器的缓存)。
ARM64架构根据3种属性把设备内存分为4种类型:
Device-nGnRnE、Device-nGnRE、Device-nGRE、Device-GRE。
G:聚集属性,决定了对内存区域的多个访问是否可以被合并成一个总线事务。
R:重排属性,决定了对相同的多个访问是否可以重新排序。
E:早期写确认属性,决定了是否允许处理器和从属设备之间的中间写缓冲区发送“写完成”确认。
3.3.2 物理地址宽度控制
可以使用寄存器TCR_EL1的地段IPS控制物理地址的宽度,IPS字段的长度是3位,IPS字段的值和物理地址宽度的对应关系:
IPS字段 | 物理地址宽度 |
---|---|
000 | 32位 |
001 | 36位 |
010 | 40位 |
011 | 42位 |
100 | 44位 |
101 | 48位 |
110 | 52位 |