赞
踩
首先看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函数族,具体代码如下:
- /*
- * 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文件加载的详细过程。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。