Linux进程加载过程以及内存分析

进程加载过程以及Linux进程内存分析

1、进程加载分析

进程的加载过程从bash调用fork()系统调用创建一个新进程开始,新进程调用execve()系统调用执行指定的ELF文件,在执行execve()后,内核就开始了真正的装载过程,execvp()系统调用对应的入口是sys_execve(),在sys_execve()进行了一些参数的检查复制后,调用do_execve(),之后do_execve()会找到指定的ELF文件,读取前128个字节用于判断文件的格式和类型,之后调用search_binary_handle()去搜索匹配合适的可执行文件装载处理过程,search_binary_handle()通过ELF文件的魔数确定文件的格式,调用相应的装载处理方式,ELF的装载处理过程称为load_elf_binary(),a.out的叫load_aout_binary(),装载可执行脚本的叫做load_script()。这里介绍load_elf_binary()。

load_elf_binary()过程如下:

  1. 检查ELF文件格式的有效性,如魔数,段的数量
  2. 寻找动态链接的.interp段,设置动态链接路径。
  3. 根据ELF可执行文件的描述,对ELF文件进行映射,如代码,数据,只读数据。
  4. 初始化ELF进程环境,比如进程启动是的edx寄存器的地址应该是DT_FINI的地址
  5. 将系统调用的返回地址修改为ELF可执行文件的入口点,对于ELF可执行文件,入口点就是e_entry指向的地址,动态链接的ELF文件的入口点是动态链接器。

当load_elf_binary()执行完后,返回至do_execve(),再返回到sys_execve()时,第五步已经把系统调用的返回地址改成了被装在的ELF程序的入口地址了。所以当sys_execve()系统调用从内核态返回到用户态是,eip寄存器就直接跳转到ELF程序的入口地址了,新的程序开始执行,ELF文件装载完毕。

2、进程内存分析

对与ELF文件的映射,涉及到内存的问题,着重分析一下。

ELF文件是分段映射,根据不同段的属性,又将属性相同的段合成一个进行映射。

下面针对动态库的加载映射进行分析:

ELF文件的段:    

[root@FOSDEV vvv]# readelf -l /lib/libutil-2.12.so

Elf file type is DYN (Shared object file)

Entry point 0x72d2a20

There are 9 program headers, starting at offset 52

Program Headers:

  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align

  PHDR           0x000034 0x072d2034 0x072d2034 0x00120 0x00120 R E 0x4

  INTERP         0x001306 0x072d3306 0x072d3306 0x00013 0x00013 R   0x1

      [Requesting program interpreter: /lib/ld-linux.so.2]

  LOAD           0x000000 0x072d2000 0x072d2000 0x016cc 0x016cc R E 0x1000

  LOAD           0x001ef0 0x072d4ef0 0x072d4ef0 0x0018c 0x00194 RW  0x1000

  DYNAMIC        0x001f08 0x072d4f08 0x072d4f08 0x000e0 0x000e0 RW  0x4

  NOTE           0x000154 0x072d2154 0x072d2154 0x00044 0x00044 R   0x4

  GNU_EH_FRAME   0x00131c 0x072d331c 0x072d331c 0x00044 0x00044 R   0x4

  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

  GNU_RELRO      0x001ef0 0x072d4ef0 0x072d4ef0 0x00110 0x00110 R   0x1

 Section to Segment mapping:

  Segment Sections…

   00     

   01     .interp

   02     .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_d .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .interp .eh_frame_hdr .eh_frame .hash

   03     .ctors .dtors .jcr .data.rel.ro .dynamic .got .got.plt .bss

   04     .dynamic

   05     .note.gnu.build-id .note.ABI-tag

   06     .eh_frame_hdr

   07     

   08     .ctors .dtors .jcr .data.rel.ro .dynamic .got

其中00,01,02,03…..08;分别按顺序对应的是PHDR,INTERP,LOAD,LOAD….,GNU_RELOR类型的段。PHDR是描述程序头的,不含有段,因此00位空,INTERP类型包含有interp段,描述动态链接器。第一个LOAD段包含的段位02描述的段。各个段的用处请man elf.h查看 。将各个段组合在一起进行加载的原因是减少内存占用空间。

上述只用标红的两个LOAD段以及GNU_RELRO段会映射到内存中。其中FileSize必小于MemSize,Memsize便是描述在内存中需要占用的字节数。第一个LOAD段映射到内存大小为5836个字节,大小5kB多一点,类型是可读可执行,对齐方式是4kB对齐。第二个LOAD段大小404个字节,不足1kB,类型可读可写,对齐方式是4kB对齐。GNU_RELRO段大小272个字节,类型是可读,对齐方式是一字节对齐。

GNU_RELRO段是重定位后的只读数据段类型,不时所有的动态库都含有这种类型的段。

ELF动态链接文件映射到内存的分布

072d2000-072d4000 r-xp 00000000 08:02 3806857    /lib/libutil-2.12.so

Size:                  8 kB

KernelPageSize:        4 kB

MMUPageSize:           4 kB

072d4000-072d5000 r–p 00001000 08:02 3806857    /lib/libutil-2.12.so

Size:                  4 kB

KernelPageSize:        4 kB

MMUPageSize:           4 kB

072d5000-072d6000 rw-p 00002000 08:02 3806857    /lib/libutil-2.12.so

Size:                  4 kB

KernelPageSize:        4 kB

MMUPageSize:           4 kB

上面可以看到 /lib/libutil-2.12.so为成了三种类型进行映射,第一种:r-xp,表示可读,可执行,私有的,映射大小为8kB,可对可执行对应的是第一个LOAD段,但第一个LOAD段在内存的映射大小明明是5KB多一点啊,为什么变成了8kB呢?原因是第一个LOAD段的对齐方式是4kB对齐,同时MMUPageSize:是4kB,因此不满足4kB的一部分,由于对齐方式以及一个页的大小是4kB,因此不足4kB的部分也会占用一个页,所有占用的MMU内存为8kB。同理第二个LOAD段和GNU_RELRO段的大小都占用4kB内存的原因也是如此。

上述的映射均是ELF文件在运行时与MMU虚拟内存的映射大小关系。在于物理内存的映射关系跟此不一样,为了减少由于段地址对齐来的内存浪费,在映射到物理内存时,会将两块不满一页的段进行整合到一页物理页面上,一页物理内存可能含有两个不同的段。

虚拟内存的实质就是在一个巨大的文件,不是真正的大,这个文件形成了与磁盘之间的映射而已。

/proc/pid/maps文件中的有名映射就是上述ELF文件的映射,除了有名映射之外,还存在很多的匿名映射,这些匿名映射一般是堆栈的空间,还有一部分是线程溢出保护区,如pthread_create()函数中会开辟一段空间,还有部分是子线程的栈空间。

3、Linux内存分配策略

Linux系统的内存分配分为两部分,内核空间与用户空间,若物理内存共4个G,那么0~3G的内存为用户内存,3~4G的内存为内核使用的。3~4G的空间中,0~16位DMA内存,16~896M属于低端内存,剩余的128M属于高端内存。其中0~896M属于线性区,采用kmalloc()的方式进行申请,896M~1G空间属于非线性区,采用vmalloc()进行申请,现在的操作系统几乎美哦与高端内存这个说法,不过为了兼容之前的内核代码采用vmalloc()方式开辟空间,因此还是保留高端内存。其中高端内存若是不足,可以向低端内存临时占用。但是低端内存不能占用高端内存的空间。原因是线性和非线性的区别。

    原文作者:slowzhong
    原文地址: https://blog.csdn.net/zhongfujin/article/details/86720848
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞