前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >linux进程虚拟空间布局

linux进程虚拟空间布局

作者头像
用户4415180
发布2022-06-23 14:27:34
2.4K0
发布2022-06-23 14:27:34
举报
文章被收录于专栏:高并发

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

                                 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下的经典布局如下图

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

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

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

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

代码语言:javascript
复制
/*
 * 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,
			&regs);
    .....
    .....
}

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

代码语言:javascript
复制
/*
 * 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函数处理,这个函数才调用到了真正装载二进制文件的函数,通过函数指针调用:

代码语言:javascript
复制
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文件的细节,先不看这些细节,主要看此函数调用的几个函数:

代码语言:javascript
复制
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,这是一个体系结构相关的函数

代码语言:javascript
复制
#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;
	}
}

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

代码语言:javascript
复制
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文件加载的详细过程。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-07-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
轻量应用服务器
轻量应用服务器(TencentCloud Lighthouse)是新一代开箱即用、面向轻量应用场景的云服务器产品,助力中小企业和开发者便捷高效的在云端构建网站、Web应用、小程序/小游戏、游戏服、电商应用、云盘/图床和开发测试环境,相比普通云服务器更加简单易用且更贴近应用,以套餐形式整体售卖云资源并提供高带宽流量包,将热门软件打包实现一键构建应用,提供极简上云体验。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档