当前位置:   article > 正文

linux进程虚拟空间布局_linux进程 layout

linux进程 layout

首先看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函数族,具体代码如下:

  1. /*
  2. * sys_execve() executes a new program.
  3. */
  4. asmlinkage int sys_execve(struct pt_regs regs)
  5. {
  6. int error;
  7. char * filename;
  8. filename = getname((char __user *) regs.ebx); //获取文件名
  9. error = PTR_ERR(filename);
  10. if (IS_ERR(filename))
  11. goto out;
  12. error = do_execve(filename,
  13. (char __user * __user *) regs.ecx,
  14. (char __user * __user *) regs.edx,
  15. &regs);
  16. .....
  17. .....
  18. }

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

  1. /*
  2. * filename:可执行文件路径
  3. * argv:运行程序所需的参数
  4. * envp:运行程序所需的环境变量
  5. */
  6. int do_execve(char * filename,
  7. char __user *__user *argv,
  8. char __user *__user *envp,
  9. struct pt_regs * regs)
  10. {
  11. struct linux_binprm *bprm; //可执行文件相关参数结构体
  12. struct file *file;
  13. int retval;
  14. int i;
  15. retval = -ENOMEM;
  16. bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); //分配bprm结构
  17. if (!bprm)
  18. goto out_ret;
  19. file = open_exec(filename); //获取文件指针
  20. bprm->p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);//当前进程内存的最大地址
  21. bprm->mm = mm_alloc(); //分配内存描述符
  22. bprm->argc = count(argv, bprm->p / sizeof(void *)); //计算传入的参数个数
  23. if ((retval = bprm->argc) < 0)
  24. goto out_mm;
  25. bprm->envc = count(envp, bprm->p / sizeof(void *)); //计算环境变量个数
  26. if ((retval = bprm->envc) < 0)
  27. goto out_mm;
  28. retval = prepare_binprm(bprm); //读取elf文件的头,放入bprm->buf
  29. retval = search_binary_handler(bprm,regs);
  30. ......
  31. ......
  32. out_kfree:
  33. kfree(bprm);
  34. out_ret:
  35. return retval;
  36. }

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

  1. int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)
  2. {
  3. int try,retval;
  4. struct linux_binfmt *fmt;
  5. retval = security_bprm_check(bprm);
  6. if (retval)
  7. return retval;
  8. retval = -ENOENT;
  9. for (try=0; try<2; try++) {
  10. read_lock(&binfmt_lock); //读加锁
  11. //遍历linux_binfmt结构
  12. for (fmt = formats ; fmt ; fmt = fmt->next) {
  13. //获取加载二进制文件的函数指针,这个函数会对应到fs/binfmt_elf.c的 load_elf_binary函数
  14. int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
  15. if (!fn)
  16. continue;
  17. if (!try_module_get(fmt->module))
  18. continue;
  19. read_unlock(&binfmt_lock);
  20. retval = fn(bprm, regs); //调用加载二进制文件的函数
  21. .......
  22. }
  23. .........
  24. .........
  25. return retval;
  26. }

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

  1. static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
  2. {
  3. struct file *interpreter = NULL; /* to shut gcc up */
  4. unsigned long load_addr = 0, load_bias = 0;
  5. int load_addr_set = 0;
  6. char * elf_interpreter = NULL;
  7. unsigned int interpreter_type = INTERPRETER_NONE;
  8. unsigned char ibcs2_interpreter = 0;
  9. unsigned long error;
  10. struct elf_phdr *elf_ppnt, *elf_phdata;
  11. unsigned long elf_bss, elf_brk;
  12. int elf_exec_fileno;
  13. int retval, i;
  14. unsigned int size;
  15. unsigned long elf_entry, interp_load_addr = 0;
  16. unsigned long start_code, end_code, start_data, end_data;
  17. elf_ppnt = elf_phdata;
  18. elf_bss = 0; //bss段结束地址
  19. elf_brk = 0; //堆起始地址
  20. start_code = ~0UL; //代码段起始地址初始0xFFFFFFFF
  21. end_code = 0; //代码段结束地址
  22. start_data = 0; //数据段起始地址
  23. end_data = 0; //数据段结束地址
  24. /* 此处可以看出execv函数族是将当前进程内存布局替换为新进程的内存布局*/
  25. current->mm->start_data = 0; //重置当前进程数据段起始地址
  26. current->mm->end_data = 0; //重置当前进程数据段结束地址
  27. current->mm->end_code = 0; //重置当前进程代码段结束地址
  28. current->mm->mmap = NULL; //重置当前进程mmap区基地址
  29. //设置mmap的基地址
  30. arch_pick_mmap_layout(current->mm);
  31. //处理栈空间
  32. retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
  33. executable_stack);
  34. if (retval < 0) {
  35. send_sig(SIGKILL, current, 0);
  36. goto out_free_dentry;
  37. }
  38. //设置栈的起始地址
  39. current->mm->start_stack = bprm->p;
  40. /* 处理elf文件各个段,确定代码段,数据段,bss段的起始和结束地址*/
  41. for(i = 0, elf_ppnt = elf_phdata;
  42. i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
  43. int elf_prot = 0, elf_flags;
  44. unsigned long k, vaddr;
  45. if (elf_ppnt->p_type != PT_LOAD)
  46. continue;
  47. if (elf_ppnt->p_flags & PF_R)
  48. elf_prot |= PROT_READ;
  49. if (elf_ppnt->p_flags & PF_W)
  50. elf_prot |= PROT_WRITE;
  51. if (elf_ppnt->p_flags & PF_X)
  52. elf_prot |= PROT_EXEC;
  53. elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE;
  54. vaddr = elf_ppnt->p_vaddr; //当前段的起始地址
  55. //建立vma结构,也就是每个段一个vma
  56. error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
  57. elf_prot, elf_flags);
  58. if (BAD_ADDR(error)) {
  59. send_sig(SIGKILL, current, 0);
  60. goto out_free_dentry;
  61. }
  62. k = elf_ppnt->p_vaddr; //每个段的起始地址,如果是代码段,则表示程序入口地址
  63. if (k < start_code) //start_code初始是0Xffffffff,所以k一定小于
  64. start_code = k; //确定程序入口地址
  65. if (start_data < k) //确定数据段起始地址
  66. start_data = k;
  67. //p_filesz表示此段在文件中的大小 p_filesz <= p_memsz,因为BSS段是未初始化全局变量,在编译好的
  68. //目标文件中BSS段不占用文件的内存,只有加载到内存时,BSS段才会占用内存空间初始化为0
  69. k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;
  70. if (k > elf_bss)
  71. elf_bss = k; //BSS段的起始地址
  72. if ((elf_ppnt->p_flags & PF_X) && end_code < k)
  73. end_code = k; //确定代码段结束地址
  74. if (end_data < k)
  75. end_data = k; //数据段结束地址
  76. k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz; //p_memsz表示此段在内存中的大小,如果当前是data段则计算出加上BSS段的结束地址
  77. if (k > elf_brk)
  78. elf_brk = k; //堆起始地址
  79. }
  80. //确定最终的各段地址,load_bias在动态链接时有用到,暂不分析
  81. elf_bss += load_bias;
  82. elf_brk += load_bias;
  83. start_code += load_bias;
  84. end_code += load_bias;
  85. start_data += load_bias;
  86. end_data += load_bias;
  87. //设置堆空间
  88. retval = set_brk(elf_bss, elf_brk);
  89. //确定代码段数据段和栈
  90. current->mm->end_code = end_code;
  91. current->mm->start_code = start_code;
  92. current->mm->start_data = start_data;
  93. current->mm->end_data = end_data;
  94. current->mm->start_stack = bprm->p;
  95. out:
  96. kfree(loc);
  97. out_ret:
  98. return retval;
  99. /* error cleanup */
  100. out_free_dentry:
  101. allow_write_access(interpreter);
  102. if (interpreter)
  103. fput(interpreter);
  104. out_free_interp:
  105. kfree(elf_interpreter);
  106. out_free_file:
  107. sys_close(elf_exec_fileno);
  108. out_free_fh:
  109. if (files) {
  110. put_files_struct(current->files);
  111. current->files = files;
  112. }
  113. out_free_ph:
  114. kfree(elf_phdata);
  115. goto out;
  116. }

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

  1. #define MIN_GAP (128*1024*1024)
  2. #define MAX_GAP (TASK_SIZE/6*5)
  3. static inline unsigned long mmap_base(struct mm_struct *mm)
  4. {
  5. unsigned long gap = current->signal->rlim[RLIMIT_STACK].rlim_cur;
  6. unsigned long random_factor = 0;
  7. if (current->flags & PF_RANDOMIZE)
  8. random_factor = get_random_int() % (1024*1024); //随机数在1MB以内
  9. if (gap < MIN_GAP)
  10. gap = MIN_GAP; //最小128MB
  11. else if (gap > MAX_GAP)
  12. gap = MAX_GAP; //最大512MB
  13. return PAGE_ALIGN(TASK_SIZE - gap - random_factor);//0xc0000000 - 128MB - rand —— 0xc0000000 - 512MB - rand
  14. }
  15. void arch_pick_mmap_layout(struct mm_struct *mm)
  16. {
  17. //经典布局,堆空间只有不到1GB,mmap基地址0X40000000并且向高地址增长
  18. if (sysctl_legacy_va_layout ||
  19. (current->personality & ADDR_COMPAT_LAYOUT) ||
  20. current->signal->rlim[RLIMIT_STACK].rlim_cur == RLIM_INFINITY) {
  21. mm->mmap_base = TASK_UNMAPPED_BASE;
  22. mm->get_unmapped_area = arch_get_unmapped_area;
  23. mm->unmap_area = arch_unmap_area;
  24. } else { //新的布局mmap向低地址增长,堆向高地址增长
  25. mm->mmap_base = mmap_base(mm); //mmap基地址B8000000 ~ A0000000 - 随机数
  26. mm->get_unmapped_area = arch_get_unmapped_area_topdown;
  27. mm->unmap_area = arch_unmap_area_topdown;
  28. }
  29. }

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

  1. int setup_arg_pages(struct linux_binprm *bprm,
  2. unsigned long stack_top,
  3. int executable_stack)
  4. {
  5. unsigned long stack_base;
  6. struct vm_area_struct *mpnt;
  7. struct mm_struct *mm = current->mm;
  8. int i, ret;
  9. long arg_size;
  10. stack_base = arch_align_stack(stack_top - MAX_ARG_PAGES*PAGE_SIZE);//stack 基地址 0XC0000000 - rand - 128MB
  11. stack_base = PAGE_ALIGN(stack_base); //页面对齐
  12. bprm->p += stack_base; //128MB - 4B + 0XC0000000 - rand - 128MB,bprm->p也是栈的起始地址
  13. mm->arg_start = bprm->p; //运行参数起始地址
  14. arg_size = stack_top - (PAGE_MASK & (unsigned long) mm->arg_start);//运行参数大小
  15. return 0;
  16. }
至此内存布局就完成了,关于elf文件加载写的不详细,会单独写一篇elf文件加载的详细过程。


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/770001
推荐阅读
相关标签
  

闽ICP备14008679号