0x00 前言
前文讨论了进程,正在执行的程序,是可执行程序的动态实例,它是一个承担分配系统资源的实体,但操作系统创建进程时,会为进程创建相应的内存空间,这个内存空间称为进程的地址空间,每一个进程的地址空间(虚拟内存空间)都是独立的;当一个进程有了进程的地址空间,那么其管理结构被称为内存描述符mm_struct
虚拟内存地址
虚拟内存空间分布(32位)
32
位机器上进程的用户空间大小为 3GB
。内核将一个进程的用户空间划分为多个段:
- 代码段:用于存放程序中可执行代码的段
- 数据段:用于存放已经初始化的全局变量或静态变量的段(如
int global = 10;
定义的全局变量) - 未初始化数据段:用于存放未初始化的全局变量或静态变量的段(如
int global;
定义的全局变量) - 堆:用于存放使用
malloc
函数申请的内存 mmap
区:用于存放使用mmap
函数映射的内存区- 栈:用于存放函数局部变量和函数参数
虚拟内存空间分布(64位)
0x01 进程虚拟内存空间管理
无论是在 32
位机器上还是在 64
位机器上,进程虚拟内存空间的核心区域分布的相对位置是一样的,这个结构是怎么在内核反映的?
内存描述符:mm_struct
已知每个内核进程描述符结构体task_struct
中嵌套了一个mm_struct
结构体指针,该结构体中包含了的该进程虚拟内存空间的全部信息。同时每个进程都有唯一的 mm_struct
结构体,这说明每个进程的虚拟地址空间都是独立,互不干扰的。当调用 fork()
函数创建进程的时候,表示进程地址空间的 mm_struct
结构会随着进程描述符 task_struct
的创建而创建
创建mm_struct
的逻辑在内核函数copy_mm
中,注意这里区分用户态进程和用户态线程,前者会单独复制出一份mm_struct
,后者是共享父进程的mm_struct
。copy_mm
函数首先会将父进程的虚拟内存空间 current->mm
赋值给指针 oldmm
,然后通过 dup_mm
函数将父进程的虚拟内存空间以及相关页表拷贝到子进程的 mm_struct
结构中,最后将拷贝出来的 mm_struct
赋值给子进程的 task_struct
结构,最终布局参考
struct task_struct {
//.......
// 内存描述符表示进程虚拟地址空间
struct mm_struct *mm;
//.......
}
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */
struct rb_root mm_rb; /*红黑树的根节点*/
struct vm_area_struct * mmap_cache; /* last find_vma result */
//.......
}
mm_struct
结构体定义如下:
struct mm_struct {
//mmap指向虚拟区间链表
struct vm_area_struct * mmap; /* list of VMAs */
//指向红黑树的根节点
struct rb_root mm_rb;
//指向最近的虚拟空间
struct vm_area_struct * mmap_cache; /* last find_vma result */
//
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
unsigned long mmap_base; /* base of mmap area */
unsigned long task_size; /* size of task vm space */
unsigned long cached_hole_size; /* if non-zero, the largest hole below free_area_cache */
unsigned long free_area_cache; /* first hole of size cached_hole_size or larger */
//指向进程的页目录
pgd_t * pgd;
//空间中有多少用户
atomic_t mm_users; /* How many users with user space? */
//引用计数;描述有多少指针指向当前的mm_struct
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
//虚拟区间的个数
int map_count; /* number of VMAs */
struct rw_semaphore mmap_sem;
//保护任务页表
spinlock_t page_table_lock; /* Protects page tables and some counters */
//所有mm的链表
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
*/
/* Special counters, in some configurations protected by the
* page_table_lock, in other configurations by being atomic.
*/
mm_counter_t _file_rss;
mm_counter_t _anon_rss;
unsigned long hiwater_rss; /* High-watermark of RSS usage */
unsigned long hiwater_vm; /* High-water virtual memory usage */
unsigned long total_vm, locked_vm, shared_vm, exec_vm;
unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
//start_code:代码段的起始地址
//end_code:代码段的结束地址
//start_data:数据段起始地址
//end_data:数据段结束地址
unsigned long start_code, end_code, start_data, end_data;
//start_brk:堆的起始地址
//brk:堆的结束地址
//start_stack:栈的起始地址
unsigned long start_brk, brk, start_stack;
//arg_start,arg_end:参数段的起始和结束地址
//env_start,env_end:环境段的起始和结束地址
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
struct linux_binfmt *binfmt;
cpumask_t cpu_vm_mask;
/* Architecture-specific MM context */
mm_context_t context;
/* Swap token stuff */
/*
* Last value of global fault stamp as seen by this process.
* In other words, this value gives an indication of how long
* it has been since this task got the token.
* Look at mm/thrash.c
*/
unsigned int faultstamp;
unsigned int token_priority;
unsigned int last_interval;
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 hlist_head ioctx_list;
#endif
#ifdef CONFIG_MM_OWNER
/*
* "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 *owner;
#endif
#ifdef CONFIG_PROC_FS
/* store ref to file /proc/<pid>/exe symlink points to */
struct file *exe_file;
unsigned long num_exe_file_vmas;
#endif
#ifdef CONFIG_MMU_NOTIFIER
struct mmu_notifier_mm *mmu_notifier_mm;
#endif
};
task_size
task_size
:定义了用户态地址空间与内核态地址空间之间的分界线,如64
位系统中用户地址空间和内核地址空间的分界线在 0x0000 7FFF FFFF F000
地址处,那么task_struct.mm_struct
结构中的 task_size
为 0x0000 7FFF FFFF F000
进程虚拟内存空间布局(区间端点)
struct mm_struct {
unsigned long task_size; /* size of task vm space */
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 mmap_base; /* base of mmap area */
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 */
//......
}
vm_area_struct
Linux 内核按照功能上的差异,把虚拟内存空间划分为多个段。那么在内核中,是通过vm_area_struct
结构来管理这些段
内核使用结构体 vm_area_struct
来描述用户虚拟内存空间的各个逻辑区域:代码段,数据段,堆,内存映射区,栈等,也称为VMA(virtual memory area)
每个 vm_area_struct
结构对应于虚拟内存空间中的唯一虚拟内存区域 VMA,其中vm_start
指向了这块虚拟内存区域的起始地址(最低地址),vm_start
本身包含在这块虚拟内存区域内,vm_end
指向了这块虚拟内存区域的结束地址(最高地址),而 vm_end
本身包含在这块虚拟内存区域之外,所以 vm_area_struct
结构描述的是 [vm_start,vm_end)
这样一段左闭右开的虚拟内存区域
//内核通过 vm_area_struct 结构(虚拟内存区)来管理各个段
struct vm_area_struct {
struct mm_struct *vm_mm; /* The address space we belong to. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
/*
* Access permissions of this VMA.
*/
pgprot_t vm_page_prot;
unsigned long vm_flags;
//每个 VMA 区域都是红黑树中的一个节点,通过 struct vm_area_struct 结构中的 vm_rb 将自己连接到红黑树中
struct rb_node vm_rb;
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
struct file * vm_file; /* File we map to (can be NULL). */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units */
void * vm_private_data; /* was vm_pte (shared mem) */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
}
vm_mm
:指向进程的内存管理对象,每个进程都有一个类型为mm_struct
的内存管理对象,用于管理进程的虚拟内存空间和内存映射等vm_start
:虚拟内存区的起始虚拟内存地址vm_end
:虚拟内存区的结束虚拟内存地址vm_next
:Linux 会通过链表把进程的所有虚拟内存区连接起来,这个字段用于指向下一个虚拟内存区vm_page_prot
:主要用于保存当前虚拟内存区所映射的物理内存页的读写权限vm_flags
:标识当前虚拟内存区的功能特性vm_rb
:某些场景中需要通过虚拟内存地址查找对应的虚拟内存区,为了加速查找过程,内核以虚拟内存地址作为key,把进程所有的虚拟内存区保存到一棵红黑树中,而这个字段就是红黑树的节点结构vm_ops
:每个虚拟内存区都可以自定义一套操作接口,通过操作接口,能够让虚拟内存区实现一些特定的功能,比如:把虚拟内存区映射到文件。而vm_ops
字段就是虚拟内存区的操作接口集,一般在创建虚拟内存区时指定
内核通过一个链表和一棵红黑树来管理进程中所有的段。mm_struct
结构的 mmap
字段就是链表的头节点,而 mm_rb
字段就是红黑树的根节点,如下:
mm_rb && mmap
struct mm_struct {
struct rb_root mm_rb; //红黑树的root
//vm_area_struct 链表首节点
struct vm_area_struct *mmap; /* list of VMAs */
}
几个要点:
mmap
:串联起了整个虚拟内存空间中的虚拟内存区域,即一个双向链表将虚拟内存空间中的这些虚拟内存区域 VMA 串联起来。vm_area_struct
结构中的vm_next
/vm_prev
指针分别指向 VMA 节点所在双向链表中的后继和前驱节点,内核中的这个 VMA 双向链表是有顺序的,所有 VMA 节点按照低地址到高地址的增长方向排序。此外,双向链表中的最后一个 VMA 节点的vm_next
指向NULL
,双向链表的头指针存储在内存描述符struct mm_struct
结构中的mmap
成员- 在每个虚拟内存区域 VMA 中又通过
struct vm_area_struct
中的vm_mm
指针指向了所属的虚拟内存空间mm_struct
- 可通过
cat /proc/pid/maps
/pmap pid
查看进程的虚拟内存空间布局以及其中包含的所有内存区域(其实现原理就是通过遍历内核中的这个vm_area_struct
双向链表获取) mm_rb
:红黑树的root节点,每个vm_area_struct
都包含了struct rb_node vm_rb
成员即为红黑树的节点- 在内核中,同样的内存区域
vm_area_struct
会有两种组织形式,一种是双向链表用于高效的遍历,另一种就是红黑树用于高效的查找
使用rbtree组织VMA区域的场景如下:
- 需要根据特定虚拟内存地址在虚拟内存空间中查找特定的VMA虚拟内存区域,尤其在进程虚拟内存空间中包含的内存区域 VMA 比较多的情况下,使用rbtree查找更为高效
0x02 进程虚拟内存管理:ELF加载
本小节主要讨论下进程的虚拟内存区是如何建立起来的,主要涉及到:
- ELF文件
ELF:Executable and Linkable Format
在 Linux 系统中使用ELF格式来存储一个可执行的应用程序,一个 ELF 文件由以下三部分组成:
- ELF 头(ELF header):描述应用程序的类型、CPU架构、入口地址、程序头表偏移和节头表偏移等
- 程序头表(Program header table):列举了所有有效的段(segments)和其属性,程序头表需要加载器将文件中的段加载到虚拟内存段中
- 节头表(Section header table):包含对节(sections)的描述
当内核加载一个应用程序时,就是通过读取 ELF 文件的信息,然后把文件中所有的段加载到虚拟内存的段中。ELF 文件通过程序头表elf64_phdr
来描述应用程序中所有的段,表中的每一个项都描述一个段的信息,程序加载器可以通过 ELF 头中获取到程序头表的偏移量,然后通过程序头表的偏移量读取到程序头表的数据,再通过程序头表来获取到所有段的信息
typedef struct elf64_phdr {
Elf64_Word p_type; // 段的类型
Elf64_Word p_flags; // 可读写标志
Elf64_Off p_offset; // 段在ELF文件中的偏移量
Elf64_Addr p_vaddr; // 段的虚拟内存地址
Elf64_Addr p_paddr; // 段的物理内存地址
Elf64_Xword p_filesz; // 段占用文件的大小
Elf64_Xword p_memsz; // 段占用内存的大小
Elf64_Xword p_align; // 内存对齐
} Elf64_Phdr;
ELF的加载过程
要加载一个程序,需要调用 execve
系统调用来完成, execve
系统调用的调用栈如下,execve
系统调用最终会调用 load_elf_binary
函数来加载程序的 ELF 文件,接下来简单描述下load_elf_binary
的过程
sys_execve
->do_execve
->do_execveat_common
-> __do_execve_file
-> exec_binprm
-> search_binary_handler
-> load_elf_binary
load_elf_binary的实现
1、读取并检查ELF头
//这段代码的逻辑主要是读取应用程序的 ELF 头,然后检查 ELF 头信息是否合法
static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
{
//...
struct {
struct elfhdr elf_ex;
struct elfhdr interp_elf_ex;
} *loc;
loc = kmalloc(sizeof(*loc), GFP_KERNEL);
if (!loc) {
retval = -ENOMEM;
goto out_ret;
}
// 1. 获取ELF头
loc->elf_ex = *((struct elfhdr *)bprm->buf);
retval = -ENOEXEC;
// 2. 检查ELF签名是否正确
if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
goto out;
// 3. 是否是可执行文件或者动态库
if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
goto out;
// 4. 检查系统架构是否正确
if (!elf_check_arch(&loc->elf_ex))
goto out;
//...
}
2、读取程序头表,主要包含如下流程:
- 从 ELF 头的信息中获取到程序头表的大小
- 调用
kmalloc
函数申请一块内存来保存程序头表 - 调用
kernel_read
函数从 ELF 文件中读取程序头表的数据,保存到elf_phdata
变量中,程序头表的偏移量可以通过 ELF 头的e_phoff
字段获取
static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
{
// ......
// ......
// 接上面的代码
size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr); // 程序头表的大小
retval = -ENOMEM;
elf_phdata = kmalloc(size, GFP_KERNEL); // 申请一块内存来保存程序头表
if (!elf_phdata)
goto out;
// 从ELF文件中读取程序头表的数据, 并且保存到 elf_phdata 变量中
retval = kernel_read(bprm->file, loc->elf_ex.e_phoff, (char *)elf_phdata, size);
if (retval != size) {
if (retval >= 0)
retval = -EIO;
goto out_free_ph;
}
//...
}
3、加载段到虚拟内存,把段加载到虚拟内存主要通过 elf_map
函数完成
- 遍历程序头表所有的段
- 判断段是否需要加载
- 获取段的可读写权限和段的虚拟内存地址
- 调用
elf_map
函数把段加载到虚拟内存
// 遍历程序头表所有的段
for (i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
int elf_prot = 0, elf_flags;
unsigned long k, vaddr;
if (elf_ppnt->p_type != PT_LOAD) // 判断段是否需要加载
continue;
//...
// 段的可读写权限
if (elf_ppnt->p_flags & PF_R)
elf_prot |= PROT_READ;
if (elf_ppnt->p_flags & PF_W)
elf_prot |= PROT_WRITE;
if (elf_ppnt->p_flags & PF_X)
elf_prot |= PROT_EXEC;
elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE;
vaddr = elf_ppnt->p_vaddr; // 获取段的虚拟内存地址
//...
// 把段加载到虚拟内存
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, 0);
//...
}
4、 elf_map
函数的调用栈及核心流程,elf_map
函数最终会调用 mmap_region
来完成加载段到虚拟内存
elf_map
|--> do_mmap
|--> do_mmap_pgoff
|--> mmap_region
mmap_region
的主要流程如下:
- 调用
kmem_cache_zalloc
函数申请一个vm_area_struct
(虚拟内存区)结构 - 设置
vm_area_struct
结构各个字段的值 - 调用
vma_link
函数把vm_area_struct
结构连接到虚拟内存区链表和红黑树中,vma_link
实现 - 通过上面的过程,内核就把应用程序的所有段加载到虚拟内存中
unsigned long
mmap_region(struct file *file, unsigned long addr, unsigned long len,
unsigned long flags, unsigned int vm_flags, unsigned long pgoff)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
//...
// 申请一个 vm_area_struct 结构
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
// 设置 vm_area_struct 结构各个字段的值
vma->vm_mm = mm;
vma->vm_start = addr; // 段的开始虚拟内存地址
vma->vm_end = addr + len; // 段的结束虚拟内存地址
vma->vm_flags = vm_flags; // 段的功能特性
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
//...
// 把 vm_area_struct 结构链接到虚拟内存区链表和红黑树中
vma_link(mm, vma, prev, rb_link, rb_parent);
//...
return addr;
}