Linux内核机制总结内存管理之虚拟地址空间布局(九)

2022/2/21 7:30:10

本文主要是介绍Linux内核机制总结内存管理之虚拟地址空间布局(九),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

    • 1.1 虚拟地址空间划分
    • 1.2 用户虚拟地址空间的布局
    • 1.3 内核虚拟地址空间的布局

  • 重要:本系列文章内容摘自<Linux内核深度解析>基于ARM64架构的Linux4.x内核一书,作者余华兵。系列文章主要用于记录Linux内核的大部分机制及参数的总结说明

1.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];高16位是全1或全0的地址称为规范的地址,两者之间是不规范的地址,不允许使用。
在这里插入图片描述
(2)如果处理器实现了 ARMv8.2 标准的大虚拟地址(Large Virtual Address,LVA)支持,并且页长度是64KB,那么虚拟地址的最大宽度是52位。
(3)可以为虚拟地址配置比最大宽度小的宽度,并且可以为内核虚拟地址和用户虚拟地址配置不同的宽度。转换控制寄存器(Translation Control Register)TCR_EL1的字段T0SZ定义了必须是全0的最高位的数量,字段T1SZ定义了必须是全1的最高位的数量,用户虚拟地址的宽度是(64-TCR_EL1.T0SZ),内核虚拟地址的宽度是(64-TCR_EL1.T1SZ)。

在编译ARM64架构的Linux内核时,可以选择虚拟地址宽度。
(1)如果选择页长度4KB,默认的虚拟地址宽度是39位。
(2)如果选择页长度16KB,默认的虚拟地址宽度是47位。
(3)如果选择页长度64KB,默认的虚拟地址宽度是42位。
(4)可以选择48位虚拟地址。
在ARM64架构的Linux内核中,内核虚拟地址和用户虚拟地址的宽度相同。

所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间,同一个线程组的用户线程共享用户虚拟地址空间,内核线程没有用户虚拟地址空间。

1.2 用户虚拟地址空间的布局

进程的用户虚拟地址空间的起始地址是0,长度是TASK_SIZE,由每种处理器架构定义自己的宏TASK_SIZE。ARM64架构定义的宏TASK_SIZE如下所示。
(1)32位用户空间程序:TASK_SIZE的值是TASK_SIZE_32,即0x100000000,等于4GB。
(2)64位用户空间程序:TASK_SIZE的值是TASK_SIZE_64,即2VA_BITS字节,VA_BITS是编译内核时选择的虚拟地址位数。

arch/arm64/include/asm/memory.h
#define VA_BITS          (CONFIG_ARM64_VA_BITS)
#define TASK_SIZE_64     (UL(1) << VA_BITS)

#ifdef CONFIG_COMPAT    /* 支持执行32位用户空间程序 */
#define TASK_SIZE_32     UL(0x100000000)
/* test_thread_flag(TIF_32BIT)判断用户空间程序是不是32位 */
#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
#define TASK_SIZE    TASK_SIZE_64
#endif /* CONFIG_COMPAT */

进程的用户虚拟地址空间包含以下区域。
(1)代码段、数据段和未初始化数据段。
(2)动态库的代码段、数据段和未初始化数据段。
(3)存放动态生成的数据的堆。
(4)存放局部变量和实现函数调用的栈。
(5)存放在栈底部的环境变量和参数字符串。
(6)把文件区间映射到虚拟地址空间的内存映射区域。

内核使用内存描述符mm_struct描述进程的用户虚拟地址空间,内存描述符的主要成员如下所示。

成员说明
atomic_t mm_users;共享同一个用户虚拟地址空间的进程的数量,也就是线程组包含的进程的数量
atomic_t mm_count;内存描述符的引用计数
struct vm_area_struct *mmap;虚拟内存区域链表
struct rb_root mm_rb;虚拟内存区域红黑树
unsigned long (*get_unmapped_area) (struct file *filp, unsigned long addr, unsigned long len,unsigned long pgoff, unsigned long flags);在内存映射区域找到一个没有映射的区域
pgd_t * pgd;指向页全局目录,即第一级页表
unsigned long mmap_base;内存映射区域的起始地址
unsigned long task_size;用户虚拟地址空间的长度
unsigned long start_code, end_code;代码段的起始地址和结束地址
unsigned long start_data, end_data;数据段的起始地址和结束地址
unsigned long start_brk, brk;堆的起始地址和结束地址
unsigned long start_stack;栈的起始地址
unsigned long arg_start, arg_end;参数字符串的起始地址和结束地址
unsigned long env_start, env_end;环境变量的起始地址和结束地址
mm_context_t context;处理器架构特定的内存管理上下文

进程描述符(task_struct)中和内存描述符相关的成员如下所示。

进程描述符的成员说明
struct mm_struct *mm;进程的mm指向一个内存描述符内核线程没有用户虚拟地址空间,所以mm是空指针
struct mm_struct *active_mm;进程的active_mm和mm总是指向同一个内存描述符内核线程的active_mm在没有运行时是空指针,在运行时指向从上一个进程借用的内存描述符

如果进程不属于线程组,那么进程描述符和内存描述符的关系如下所示,进程描述符的成员mm和active_mm都指向同一个内存描述符,内存描述符的成员mm_users是1、成员mm_count是1。
在这里插入图片描述
如果两个进程属于同一个线程组,那么进程描述符和内存描述符的关系如下所示,每个进程的进程描述符的成员mm和active_mm都指向同一个内存描述符,内存描述符的成员mm_users是2、成员mm_count是1。
在这里插入图片描述
内核线程的进程描述符和内存描述符的关系如下所示,内核线程没有用户虚拟地址空间,当内核线程没有运行的时候,进程描述符的成员mm和active_mm都是空指针;当内核线程运行的时候,借用上一个进程的内存描述符,在被借用进程的用户虚拟地址空间的上方运行,进程描述符的成员active_mm指向借用的内存描述符,假设被借用的内存描述符所属的进程不属于线程组,那么内存描述符的成员mm_users不变,仍然是1,成员mm_count加1变成2。
在这里插入图片描述
为了使缓冲区溢出攻击更加困难,内核支持为内存映射区域、栈和堆选择随机的起始地址。进程是否使用虚拟地址空间随机化的功能,由以下两个因素共同决定。
(1)进程描述符的成员personality(个性化)是否设置ADDR_NO_RANDOMIZE。
(2)全局变量randomize_va_space:0表示关闭虚拟地址空间随机化,1表示使内存映射区域和栈的起始地址随机化,2表示使内存映射区域、栈和堆的起始地址随机化。可以通过文件“/proc/sys/kernel/randomize_va_space”修改。

mm/memory.c
int randomize_va_space __read_mostly =
#ifdef CONFIG_COMPAT_BRK
                    1;
#else
                    2;
#endif

为了使旧的应用程序(基于libc5)正常运行,默认打开配置宏CONFIG_COMPAT_BRK,禁止堆随机化。所以默认配置是使内存映射区域和栈的起始地址随机化。

栈通常自顶向下增长,当前只有惠普公司的PA-RISC处理器的栈是自底向上增长。栈的起始地址是STACK_TOP,默认启用栈随机化,需要把起始地址减去一个随机值。STACK_TOP是每种处理器架构自定义的宏,ARM64架构定义的STACK_TOP如下所示:如果是64位用户空间程序,STACK_TOP的值是TASK_SIZE_64;如果是32位用户空间程序,STACK_TOP的值是异常向量的基准地址0xFFFF0000。

arch/arm64/include/asm/processor.h
#define STACK_TOP_MAX         TASK_SIZE_64
#ifdef CONFIG_COMPAT  /* 支持执行32位用户空间程序 */
#define AARCH32_VECTORS_BASE  0xffff0000
#define STACK_TOP   (test_thread_flag(TIF_32BIT) ? \
                 AARCH32_VECTORS_BASE : STACK_TOP_MAX)
#else
#define STACK_TOP    STACK_TOP_MAX
#endif /* CONFIG_COMPAT */

内存映射区域的起始地址是内存描述符的成员 mmap_base。如下所示,用户虚拟地址空间有两种布局,区别是内存映射区域的起始位置和增长方向不同。
在这里插入图片描述
(1)传统布局:内存映射区域自底向上增长,起始地址是TASK_UNMAPPED_BASE,每种处理器架构都要定义这个宏,ARM64架构定义为 TASK_SIZE/4。默认启用内存映射区域随机化,需要把起始地址加上一个随机值。传统布局的缺点是堆的最大长度受到限制,在32位系统中影响比较大,但是在64位系统中这不是问题。
(2)新布局:内存映射区域自顶向下增长,起始地址是(STACK_TOP − 栈的最大长度 − 间隙)。默认启用内存映射区域随机化,需要把起始地址减去一个随机值。
当进程调用execve以装载ELF文件的时候,函数load_elf_binary将会创建进程的用户虚拟地址空间。函数load_elf_binary创建用户虚拟地址空间的过程如下所示。

如果没有给进程描述符的成员personality设置标志位ADDR_NO_RANDOMIZE(该标志位表示禁止虚拟地址空间随机化),并且全局变量randomize_va_space是非零值,那么给进程设置标志PF_RANDOMIZE,允许虚拟地址空间随机化。
在这里插入图片描述
各种处理器架构自定义的函数arch_pick_mmap_layout负责选择内存映射区域的布局。

1.3 内核虚拟地址空间的布局

ARM64处理器架构的内核地址空间布局如下所示。
在这里插入图片描述
(1)线性映射区域的范围是[PAGE_OFFSET, 264−1],起始位置是PAGE_OFFSET = (0xFFFF FFFF FFFF FFFF << (VA_BITS-1)),长度是内核虚拟地址空间的一半。称为线性映射区域的原因是虚拟地址和物理地址是线性关系:

虚拟地址 =((物理地址 − PHYS_OFFSET)+ PAGE_OFFSET),其中PHYS_OFFSET是内存的起始物理地址。

(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)。

外围组件互联(Peripheral Component Interconnect,PCI)是一种总线标准,PCI I/O区域是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_END = (PAGE_OFFSET − PUD_SIZE − VMEMMAP_SIZE − 64KB),其中PUD_SIZE是页上级目录表项映射的地址空间的长度。

vmalloc区域是函数vmalloc使用的虚拟地址空间,内核使用vmalloc分配虚拟地址连续但物理地址不连续的内存。

内核镜像在vmalloc区域,起始虚拟地址是(KIMAGE_VADDR + TEXT_OFFSET) ,其中KIMAGE_VADDR是内核镜像的虚拟地址的基准值,等于内核模块区域的结束地址MODULES_END;TEXT_OFFSET是内存中的内核镜像相对内存起始位置的偏移。

(6)内核模块区域的范围是[MODULES_VADDR, MODULES_END),长度是128MB,起始地址是MODULES_VADDR =(内核虚拟地址空间的起始地址 + KASAN影子区域的长度)。

内核模块区域是内核模块使用的虚拟地址空间。

(7)KASAN影子区域的起始地址是内核虚拟地址空间的起始地址,长度是内核虚拟地址空间长度的1/8。

内核地址消毒剂(Kernel Address SANitizer,KASAN)是一个动态的内存错误检查工具。它为发现释放后使用和越界访问这两类缺陷提供了快速和综合的解决方案。



这篇关于Linux内核机制总结内存管理之虚拟地址空间布局(九)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程