linux进程虚拟空间布局

首先看linux进程在32位处理器下的虚拟空间内存布局,以i386 32位机器为例

linux进程虚拟空间布局_第1张图片

                                 x86_32 32位处理器进程虚拟地址空间布局

每个用户进程的虚拟地址空间为0x0—0xC0000000也就是3GB,其中0x0—0x08000000 128MB地址空间用于捕获空指针,用户空间分为代码段,堆,mmap区,栈。

堆的起始地址start_brk依据代码段和数据段的大小确定,堆从低地址往高地址增长,mmap区从高地址往低地址增长当两个区域相撞时则区域耗完,mmap区基地址最大为A0000000,mmap_base — 0XC0000000为栈的空间,其中两个区间之间都有一个缝隙。

内核空间为0XC0000000—0xFFFFFFFF 1GB, 如果物理内存大于896MB,则内核的虚拟地址0xC0000000—0xF8000000 和 物理内存0—896MB对等映射。所以内核 为了访问大于896MB的物理内存需要设置一段虚拟区域映射其他的物理内存,这段虚拟地址叫做高端内存,VMALLOC区用函数vmalloc分配内存页面不保证连续,持久映射用函数kmap建立映射,这段映射是长期映射,固定映射是虚拟地址和物理内存固定的地址进行映射。

x86_64的进程地址空间布局就不一样了,intel的64位处理器地址线最多52根,也就是支持2^52的地址排布,理论上最大支持4096TB的内存,但是不同的处理器地址线个数不一样,有36,40,46 ,52等。根据英特尔手册查看,实际支持的物理内存大多是64GB,最多的是至强处理器支持16TB的物理内存。所以以现在的物理内存大小对于虚拟地址空间完全够用。英特尔64位处理器支持最大的线性地址是48位,也就是mmu从虚拟地址映射到物理地址只使用了48位,所以[48:63]位是扩展位,必须和第47位值一样,否则会#GP(General Protection)异常。所以x86_64的线性地址空间是0x0—0x00007FFFFFFFFFFF, 0xFFFF800000000000—0xFFFFFFFFFFFFFFFFFFFF。linux在x86_64下的经典布局如下图

linux进程虚拟空间布局_第2张图片

                    x86_64 64位处理器进程地址空间布局

用户空间分区一致,区别就是地址空间变大了,内核空间取消了高端内存,因为内核空间的地址空间完全可以访问全部物理内存。

下面以32位处理器为例看linux内核如何建立用户进程空间的内存布局的,fork调用是复制父进程的struct mm_struct的内存描述符不需要重新建立布局,而建立新的内存布局是通过加载二进制可执行文件。execve函数族加载可执行文件是将当前进程镜像替换为新的进程映像,我们看一下linux加载二进制文件建立布局的流程,只分析内存布局代码,其它的会专门写一篇二进制文件加载的分析。

    linux内核提供了sys_execve函数,对应用户态的execv函数族,具体代码如下:

/*
 * sys_execve() executes a new program.
 */
asmlinkage int sys_execve(struct pt_regs regs)
{
	int error;
	char * filename;

	filename = getname((char __user *) regs.ebx);   //获取文件名
	error = PTR_ERR(filename);
	if (IS_ERR(filename))
		goto out;
	error = do_execve(filename,
			(char __user * __user *) regs.ecx,
			(char __user * __user *) regs.edx,
			®s);
    .....
    .....
}

此函数获取可执行文件路径然后调用了do_execve

/*
 * filename:可执行文件路径
 * argv:运行程序所需的参数
 * envp:运行程序所需的环境变量
 */
int do_execve(char * filename,
	char __user *__user *argv,
	char __user *__user *envp,
	struct pt_regs * regs)
{
	struct linux_binprm *bprm;  //可执行文件相关参数结构体
	struct file *file;
	int retval;
	int i;

	retval = -ENOMEM;
	bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); //分配bprm结构
	if (!bprm)
		goto out_ret;

	file = open_exec(filename); //获取文件指针

	bprm->p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);//当前进程内存的最大地址


	bprm->mm = mm_alloc(); //分配内存描述符

	bprm->argc = count(argv, bprm->p / sizeof(void *));  //计算传入的参数个数
	if ((retval = bprm->argc) < 0)
		goto out_mm;

	bprm->envc = count(envp, bprm->p / sizeof(void *));  //计算环境变量个数
	if ((retval = bprm->envc) < 0)
		goto out_mm;


	retval = prepare_binprm(bprm);  //读取elf文件的头,放入bprm->buf


	retval = search_binary_handler(bprm,regs); 
	......
	......
out_kfree:
	kfree(bprm);

out_ret:
	return retval;
}

然后进入search_binary_handler函数处理,这个函数才调用到了真正装载二进制文件的函数,通过函数指针调用:

int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)
{
	int try,retval;
	struct linux_binfmt *fmt;

	retval = security_bprm_check(bprm);
	if (retval)
		return retval;

	retval = -ENOENT;
	for (try=0; try<2; try++) {
		read_lock(&binfmt_lock); //读加锁
		//遍历linux_binfmt结构
		for (fmt = formats ; fmt ; fmt = fmt->next) {
             //获取加载二进制文件的函数指针,这个函数会对应到fs/binfmt_elf.c的 load_elf_binary函数
			int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
			if (!fn)
				continue;
			if (!try_module_get(fmt->module))
				continue;
			read_unlock(&binfmt_lock);
			retval = fn(bprm, regs); //调用加载二进制文件的函数
			.......
		}
		.........
		.........
	return retval;
}

下面看真正的加载elf文件函数load_elf_binary,这个函数比较长,很多涉及到处理elf文件的细节,先不看这些细节,主要看此函数调用的几个函数:

static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
{
	struct file *interpreter = NULL; /* to shut gcc up */
 	unsigned long load_addr = 0, load_bias = 0;
	int load_addr_set = 0;
	char * elf_interpreter = NULL;
	unsigned int interpreter_type = INTERPRETER_NONE;
	unsigned char ibcs2_interpreter = 0;
	unsigned long error;
	struct elf_phdr *elf_ppnt, *elf_phdata;
	unsigned long elf_bss, elf_brk;
	int elf_exec_fileno;
	int retval, i;
	unsigned int size;
	unsigned long elf_entry, interp_load_addr = 0;
	unsigned long start_code, end_code, start_data, end_data;


	elf_ppnt = elf_phdata;
	elf_bss = 0;    //bss段结束地址
	elf_brk = 0;   //堆起始地址

	start_code = ~0UL;   //代码段起始地址初始0xFFFFFFFF
	end_code = 0;       //代码段结束地址
	start_data = 0;     //数据段起始地址
	end_data = 0;       //数据段结束地址


	/* 此处可以看出execv函数族是将当前进程内存布局替换为新进程的内存布局*/
	current->mm->start_data = 0; //重置当前进程数据段起始地址
	current->mm->end_data = 0;   //重置当前进程数据段结束地址
	current->mm->end_code = 0;   //重置当前进程代码段结束地址
	current->mm->mmap = NULL;    //重置当前进程mmap区基地址

    //设置mmap的基地址
	arch_pick_mmap_layout(current->mm);

	//处理栈空间
	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;

	/* 处理elf文件各个段,确定代码段,数据段,bss段的起始和结束地址*/
	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; //当前段的起始地址
        
        //建立vma结构,也就是每个段一个vma
		error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
				elf_prot, elf_flags);
		if (BAD_ADDR(error)) {
			send_sig(SIGKILL, current, 0);
			goto out_free_dentry;
		}

		k = elf_ppnt->p_vaddr; //每个段的起始地址,如果是代码段,则表示程序入口地址
		if (k < start_code)   //start_code初始是0Xffffffff,所以k一定小于
			start_code = k;   //确定程序入口地址
		if (start_data < k)   //确定数据段起始地址
			start_data = k;

        //p_filesz表示此段在文件中的大小 p_filesz <= p_memsz,因为BSS段是未初始化全局变量,在编译好的
        //目标文件中BSS段不占用文件的内存,只有加载到内存时,BSS段才会占用内存空间初始化为0
		k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;

		if (k > elf_bss)
			elf_bss = k;    //BSS段的起始地址
		if ((elf_ppnt->p_flags & PF_X) && end_code < k)
			end_code = k;    //确定代码段结束地址
		if (end_data < k)
			end_data = k;  //数据段结束地址
		k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz; //p_memsz表示此段在内存中的大小,如果当前是data段则计算出加上BSS段的结束地址
		if (k > elf_brk)
			elf_brk = k;   //堆起始地址
	}
    
    //确定最终的各段地址,load_bias在动态链接时有用到,暂不分析
	elf_bss += load_bias; 
	elf_brk += load_bias;
	start_code += load_bias;
	end_code += load_bias;
	start_data += load_bias;
	end_data += load_bias;

	//设置堆空间
	retval = set_brk(elf_bss, elf_brk);
    
    //确定代码段数据段和栈
	current->mm->end_code = end_code;
	current->mm->start_code = start_code;
	current->mm->start_data = start_data;
	current->mm->end_data = end_data;
	current->mm->start_stack = bprm->p;

out:
	kfree(loc);
out_ret:
	return retval;

	/* error cleanup */
out_free_dentry:
	allow_write_access(interpreter);
	if (interpreter)
		fput(interpreter);
out_free_interp:
	kfree(elf_interpreter);
out_free_file:
	sys_close(elf_exec_fileno);
out_free_fh:
	if (files) {
		put_files_struct(current->files);
		current->files = files;
	}
out_free_ph:
	kfree(elf_phdata);
	goto out;
}

再看如何确定mmap的基地址arch_pick_mmap_layout,这是一个体系结构相关的函数

#define MIN_GAP (128*1024*1024)
#define MAX_GAP (TASK_SIZE/6*5)

static inline unsigned long mmap_base(struct mm_struct *mm)
{
	unsigned long gap = current->signal->rlim[RLIMIT_STACK].rlim_cur;
	unsigned long random_factor = 0;

	if (current->flags & PF_RANDOMIZE)
		random_factor = get_random_int() % (1024*1024); //随机数在1MB以内

	if (gap < MIN_GAP)
		gap = MIN_GAP;  //最小128MB
	else if (gap > MAX_GAP)
		gap = MAX_GAP; //最大512MB

	return PAGE_ALIGN(TASK_SIZE - gap - random_factor);//0xc0000000 - 128MB - rand —— 0xc0000000 - 512MB - rand
}

void arch_pick_mmap_layout(struct mm_struct *mm)
{
	//经典布局,堆空间只有不到1GB,mmap基地址0X40000000并且向高地址增长
	if (sysctl_legacy_va_layout ||
			(current->personality & ADDR_COMPAT_LAYOUT) ||
			current->signal->rlim[RLIMIT_STACK].rlim_cur == RLIM_INFINITY) {
		mm->mmap_base = TASK_UNMAPPED_BASE;
		mm->get_unmapped_area = arch_get_unmapped_area;
		mm->unmap_area = arch_unmap_area;
	} else {   //新的布局mmap向低地址增长,堆向高地址增长
		mm->mmap_base = mmap_base(mm);  //mmap基地址B8000000 ~ A0000000 - 随机数
		mm->get_unmapped_area = arch_get_unmapped_area_topdown;
		mm->unmap_area = arch_unmap_area_topdown;
	}
}

再看确定栈指针的函数,栈分为向上增长和向下增长,默认向下增长:

int setup_arg_pages(struct linux_binprm *bprm,
		    unsigned long stack_top,
		    int executable_stack)
{
	unsigned long stack_base;
	struct vm_area_struct *mpnt;
	struct mm_struct *mm = current->mm;
	int i, ret;
	long arg_size;


	stack_base = arch_align_stack(stack_top - MAX_ARG_PAGES*PAGE_SIZE);//stack 基地址 0XC0000000 - rand - 128MB
	stack_base = PAGE_ALIGN(stack_base);   //页面对齐
	bprm->p += stack_base;  //128MB - 4B + 0XC0000000 - rand - 128MB,bprm->p也是栈的起始地址
	mm->arg_start = bprm->p; //运行参数起始地址
	arg_size = stack_top - (PAGE_MASK & (unsigned long) mm->arg_start);//运行参数大小
	
	return 0;
}
至此内存布局就完成了,关于elf文件加载写的不详细,会单独写一篇elf文件加载的详细过程。


你可能感兴趣的:(操作系统)