用户虚拟地址空间布局
虚拟地址空间划分
因为目前应用程序没有那么大的内存需求,所以ARM64处理器不支持完全的64位虚拟地址。 虚拟地址的最大宽度一般是48位(ARMv8.2+支持52位)。
ARM64架构内核/用户虚拟地址空间划分如下图(假设虚拟地址的宽度是48位):
在编译ARM64架构的Linux内核时,可以选择虚拟地址宽度。
(1)如果选择页长度4KB,默认的虚拟地址宽度是39位。
(2)如果选择页长度16KB,默认的虚拟地址宽度是47位。
(3)如果选择页长度64KB,默认的虚拟地址宽度是42位。
(4)可以选择48位虚拟地址。
通过getconf PAGESIZE
或者grep "KernelPageSize" /proc/self/smaps
查询页大小。
通过zcat /proc/config.gz | grep CONFIG_ARM64_VA_BITS
查询虚拟地址宽度。
所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间,同一个线程组的用户线程共享用户虚拟地址空间,内核线程没有用户虚拟地址空间。
用户虚拟地址空间布局
进程的用户虚拟地址空间的起始地址是0,长度是`**TASK_SIZE**`,由每种处理器架构定义自己的宏`TASK_SIZE`。ARM64架构定义的宏`TASK_SIZE`如下所示。
/** PAGE_OFFSET - the virtual address of the start of the linear map (top* (VA_BITS - 1))* KIMAGE_VADDR - the virtual address of the start of the kernel image* VA_BITS - the maximum number of bits for virtual addresses.* VA_START - the first kernel virtual address.* TASK_SIZE - the maximum size of a user space task.* TASK_UNMAPPED_BASE - the lower boundary of the mmap VM area.*/
#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)
#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 */
编译内核的时候,指定虚拟地址位数CONFIG_ARM64_VA_BITS
。
如果CONFIG_ARM64_VA_BITS
为39,64位用户空间程序TASK_SIZE
为2<sup>39</sup>
。
用户空间详细布局
典型 ARM64 Linux 进程地址空间(以 48 位为例):
高地址
+--------------------------------+ ← 0x0000_FFFF_FFFF_FFFF (256TB)
| [vdso]/[vvar] | // 内核接口 (加速系统调用)
+--------------------------------+ ← 约 0x0000_FFFF_FFFE_FFFF
| 栈 (Stack) | // 主线程栈 (默认8MB)
| | // 向低地址增长
+--------------------------------+ ← 随机化起始
| 内存映射区 | // 文件/共享内存映射 (libc.so等)
| (Memory Mapping) | // 向低地址增长
+--------------------------------+ ← 随机化起始
| 堆 (Heap) | // 动态内存 (brk/sbrk管理)
| | // 向高地址增长
+--------------------------------+ ← 程序中断点 (brk)
| 未初始化数据 (.bss) | // 初始化为0
+--------------------------------+
| 已初始化数据 (.data) | // 全局/静态变量
+--------------------------------+
| 只读数据 (.rodata) | // 常量字符串等
+--------------------------------+
| 程序代码 (.text) | // 可执行指令
+--------------------------------+ ← 0x400000 (4MB)
| ELF 头部 / 程序头 |
+--------------------------------+ ← 0x400000 以下保留
| 保留区 | // 空指针访问保护
+--------------------------------+ ← 0x0
低地址
程序映像(ELF segments)
- 包含
.text
(代码段)、.rodata
(只读数据)、.data
(初始化数据)、.bss
(未初始化数据) - 非PIE程序加载地址通常固定为
0x400000
,PIE 程序(Position Independent Executable)起始地址随机
Heap(堆)
- 通过
brk()
和sbrk()
系统调用增长 malloc
默认使用堆- 对应于
mm_struct
中的start_brk
和brk
字段。
mmap 区域
- mmap、动态库加载都发生在这
- PIE 程序的代码段也会加载在这里
- 动态地址由内核随机决定(ASLR)
Stack(栈)
- 向下增长,每个线程有独立栈空间(默认 8MB,
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">ulimit -s</font>**
) - 通常位于靠近
TASK_SIZE
的高地址 - 命令行参数、环境变量等在栈顶位置,通过
/proc/<pid>/cmdline
、/proc/<pid>/environ
查询
vdso / vvar
vdso
:内核提供的用户态共享库,加速系统调用,如gettimeofday
vvar
:提供 vdso 所需的数据支持- 映射在栈附近的高地址区域
地址随机化(Address Space Layout Randomization)
为了使缓冲区溢出攻击更加困难,内核支持为内存映射区域、栈和堆选择随机的起始地址。进程是否使用虚拟地址空间随机化的功能,由以下两个因素共同决定。
(1)进程描述符的成员personality
(个性化)是否设置ADDR_NO_RANDOMIZE
。可以通过/proc/<pid>/personality
查看。
ADDR_NO_RANDOMIZE
值定义如下:
/** Flags for bug emulation.** These occupy the top three bytes.*/
enum {UNAME26 = 0x0020000,ADDR_NO_RANDOMIZE = 0x0040000, /* disable randomization of VA space */FDPIC_FUNCPTRS = 0x0080000, /* userspace function ptrs point to descriptors* (signal handling)*/MMAP_PAGE_ZERO = 0x0100000,ADDR_COMPAT_LAYOUT = 0x0200000,READ_IMPLIES_EXEC = 0x0400000,ADDR_LIMIT_32BIT = 0x0800000,SHORT_INODE = 0x1000000,WHOLE_SECONDS = 0x2000000,STICKY_TIMEOUTS = 0x4000000,ADDR_LIMIT_3GB = 0x8000000,
};
(2)全局变量randomize_va_space
:0 表示关闭虚拟地址空间随机化,1表示使内存映射区域和栈的起始地址随机化,2 表示使内存映射区域、栈和堆的起始地址随机化。 可以通过文件proc/sys/kernel/randomize_va_space
查看和修改。
/** Randomize the address space (stacks, mmaps, brk, etc.).** ( When CONFIG_COMPAT_BRK=y we exclude brk from randomization,* as ancient (libc5 based) binaries can segfault. )*/
int randomize_va_space __read_mostly =
#ifdef CONFIG_COMPAT_BRK1;
#else2;
#endif
PIE(Position Independent Executable,位置无关可执行文件)
PIE 是一种可执行文件格式,它支持在 任意虚拟内存地址 加载运行,而不依赖固定的起始地址。
这类程序在运行时可以被操作系统加载到 任意地址,而不会因为代码中的绝对地址而出错。
一句话解释:
PIE 是一种可以在任意内存地址运行的可执行程序,配合地址空间布局随机化(ASLR)增强系统安全性。
PIE 的关键特性:
特性 | 描述 |
---|---|
地址无关性 | 程序中使用相对地址,不依赖固定内存地址。 |
启用 ASLR | 支持 Address Space Layout Randomization(地址空间随机化),增加攻击者猜测地址的难度。 |
默认在现代系统开启 | 如 Ubuntu 18.04+ 默认使用 gcc 编译的程序是 PIE。 |
代码重定位 | 加载时动态链接器可以把程序加载到任意可用地址。 |
为什么需要 PIE?
安全性 是主要原因,特别是为了启用 ASLR:
- 在非-PIE 程序中,代码段是固定地址(如
0x400000
)。 - 攻击者可以根据这一点构造攻击(如 ROP)。
- PIE 程序的代码地址每次运行都变,攻击者无法预测地址。
测试验证
gcc编译默认是pie程序。如果编译非pie程序,需要加上-no-pie
参数。
PIE 与共享库的关系
- 所有 共享库(.so) 本质上都是 position-independent code(PIC)。
- PIE 程序类似共享库,编译器生成的机器码使用相对地址跳转。
struct mm_struct
内核使用内存描述符 mm_struct
描述进程的用户虚拟地址空间。
struct mm_struct {struct vm_area_struct *mmap; /* list of VMAs */struct rb_root mm_rb;u32 vmacache_seqnum; /* per-thread vmacache */
#ifdef CONFIG_MMUunsigned long (*get_unmapped_area) (struct file *filp,unsigned long addr, unsigned long len,unsigned long pgoff, unsigned long flags);
#endifunsigned long mmap_base; /* base of mmap area */unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
#ifdef CONFIG_HAVE_ARCH_COMPAT_MMAP_BASES/* Base adresses for compatible mmap() */unsigned long mmap_compat_base;unsigned long mmap_compat_legacy_base;
#endifunsigned long task_size; /* size of task vm space */unsigned long highest_vm_end; /* highest vma end address */pgd_t * pgd;/*** @mm_users: The number of users including userspace.** Use mmget()/mmget_not_zero()/mmput() to modify. When this drops* to 0 (i.e. when the task exits and there are no other temporary* reference holders), we also release a reference on @mm_count* (which may then free the &struct mm_struct if @mm_count also* drops to 0).*/atomic_t mm_users;/*** @mm_count: The number of references to &struct mm_struct* (@mm_users count as 1).** Use mmgrab()/mmdrop() to modify. When this drops to 0, the* &struct mm_struct is freed.*/atomic_t mm_count;atomic_long_t nr_ptes; /* PTE page table pages */
#if CONFIG_PGTABLE_LEVELS > 2atomic_long_t nr_pmds; /* PMD page table pages */
#endifint 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; /* List of maybe swapped mm's. These are globally strung* together off init_mm.mmlist, and are protected* by mmlist_lock*/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 data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */unsigned long stack_vm; /* VM_STACK */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 *//** Special counters, in some configurations protected by the* page_table_lock, in other configurations by being atomic.*/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_AIOspinlock_t ioctx_lock;struct kioctx_table __rcu *ioctx_table;
#endif
#ifdef CONFIG_MEMCG/** "owner" points to a task that is regarded as the canonical* user/owner of this mm. All of the following must be true in* order for it to be changed:** current == mm->owner* current->mm != mm* new_owner->mm == mm* new_owner->alloc_lock is held*/struct task_struct __rcu *owner;
#endifstruct user_namespace *user_ns;/* store ref to file /proc/<pid>/exe symlink points to */struct file __rcu *exe_file;
#ifdef CONFIG_MMU_NOTIFIERstruct mmu_notifier_mm *mmu_notifier_mm;
#endif
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKSpgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
#ifdef CONFIG_CPUMASK_OFFSTACKstruct cpumask cpumask_allocation;
#endif
#ifdef CONFIG_NUMA_BALANCING/** numa_next_scan is the next time that the PTEs will be marked* pte_numa. NUMA hinting faults will gather statistics and migrate* pages to new nodes if necessary.*/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)/** An operation with batched TLB flushing is going on. Anything that* can move process memory needs to flush the TLB when moving a* PROT_NONE or PROT_NUMA mapped page.*/bool tlb_flush_pending;
#endifstruct uprobes_state uprobes_state;
#ifdef CONFIG_HUGETLB_PAGEatomic_long_t hugetlb_usage;
#endifstruct work_struct async_put_work;
};
内存描述符的主要成员如下:
成员 | 说明 |
---|---|
atomic_t mm_users | 共享同一个用户虚拟地址空间的进程数量,即线程组包含的进程数量 |
atomic_t mm_count | 内存描述符的引用计数 |
struct vm_area_struct *mmap | 虚拟内存区域链表 |
struct rb_root mm_rb | 虚拟内存区域红黑树 |
unsigned long (*get_unmapped_area)(...) | 在内存映射区域中寻找未映射区域 |
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
,mm_struct
,vm_area_struct
关系
一个进程的虚拟地址空间主要由两个数据结构进行描述。一个是最高层次的mm_struct
,较高层次的vm_area_struct
。mm_struct
描述一个进程整个虚拟地址空间。vm_area_struct
描述虚拟地址空间的一个区间(称为虚拟区)。每个进程只有一个mm_struct
结构,在每个进程的task_struct
结构中,有一个专门用来指向该mm_struct
的成员。mm_struct
结构是对整个用户空间的描述。
task_struct
,mm_struct
,vm_area_struct
关系如下图:
查询实际布局
cat /proc/<pid>/cmdline # 命令行参数
cat /proc/<pid>/environ # 环境变量cat /proc/<pid>/maps #用户空间虚拟内存布局
cat /proc/<pid>/smaps #比maps更详细
pmap <pid>
cmdline
,environ
maps
每一行格式如下,每行对应一个vm_area_sturct
:
地址范围 权限 偏移量 设备号 inode 映射文件名或区域名
start-end perms offset dev inode pathname
pmap
每行格式如下:
<起始虚拟地址> <大小> <权限> <映射对象>
smaps
参考资料
- Professional Linux Kernel Architecture,Wolfgang Mauerer
- Linux内核深度解析,余华兵
- Linux设备驱动开发详解,宋宝华
- linux kernel 4.12