链接装载与库 第6章 可执行文件的装载与进程

可执行文件的装载与进程

在第一章中讲到,程序直接使用物理内存地址有以下缺点:

  1. 地址空间不隔离。恶意程序可以很容易的改写其他程序的数据。
  2. 内存使用效率低。一个程序需要执行时,需要将整个程序装入内存之中。
  3. 程序运行地址的不确定。因为无法保证每次都将程序加载到相同的地址,会涉及到程序重定位的问题。

解决方法是使用分页的方式:
操作系统将内存分成大小固定的页(最常用的页大小为4kb)。进程使用虚拟地址,虚拟地址也按页分割。由操作系统负责将虚拟地址的页映射到物理地址。
cpu-(virtual address)->MMU-(physical address)->Physical memory
疑问:
既然所有进程都使用虚拟地址,那么cpu建起一样的虚拟地址,是谁保存了额外的信息,将不同进程的虚拟地址映射到正确的物理地址?
大致理解:
内核内存管理模块保存了额外的信息。cpu在执行某个进程时,内核负责将MMU的对应的寄存器修改为正确的值,确保MMU将虚拟地址与寄存器的值结合起来得到正确的物理地址。

6.1 进程虚拟地址空间

每个进程拥有独立的虚拟地址空间。虚拟地址空间的大小由CPU的位数确定。32位cpu的最大虚拟地址空间为2^32字节。
并且这部分虚拟空间还有一部分是给操作系统预留的,应用程序访问此部分地址为非法操作。
疑问:
既然是虚拟地址,为什么还需要给操作系统预留呢?这不是相当于每个进程都给操作系统预留了部分空间???

PAE
在32位的CPU下,程序无法使用超过4GB的虚拟地址空间。但是却有办法使用超过4GB的物理空间。常用的方法叫窗口映射,在windows下叫做AWE(address windowing extensions),linux采用mmap()系统调用来实现。
以下理解参考:
物理地址扩展
物理地址扩展(PAE)分页机制

虚拟地址映射到物理地址,在传统的32位保护模式中,x86处理器使用一种两级的转换方案。
cr3寄存器指向一个4KB大的页目录。页目录包含1024个4KB大小页表,每个页表包含1024个页。所以共有2^20次方个页。
每个进程都有一个独立的页目录,因为拥有独立的页目录,从而也拥有了独立的虚拟地址空间。
在以上方案中,每个地址的大小都是4个字节,所以最大能使用的物理地址也只有4G。所以32位的cpu,要支持大于4G的物理内存,就必须使用PAE。
使用PAE扩展之后(设置CR4寄存器的第5位),地址变为8个字节。页目录和页表的大小没变,所以表示的项变少为一半。为了解决这个问题,增加了一级:cr3不再指向页目录表,而是指向一个大小为4的页目录指针表。(32字节对齐,所以只需要27位从便足够表示)
为了寻址超过4GB的空间,就需要对cr3设置不同的值。
通过设置cr3不同的值,就可以访问总共超过4GB大小的物理空间。
只有内核能够修改进程的页表,所以用户态下运行的进程不能使用大于4GB的物理空间。

6.2 装载的方式

6.2.1 覆盖装入

将内存管理的工作交给了程序员。

  1. 将模块按它们之间的调用依赖关系组织成树状结构。
  2. 从任何一个模块到树的根模块叫调用路径
  3. 禁止跨树间调用
    因为子树间有没有调用依赖关系,所以需要使用的最大内存比整个程序实际的内存要小。

6.2.2 页映射

将内存和磁盘上的数据都按页进行划分(最常见的页大小为4096)。操作系统不再需要将整个程序加载到内存中,而是缺少哪个页就加载哪个页。内存管理的事情完全由操作系统来完成,程序员不需要操心。

6.3 从操作系统看可执行文件的加载

在使用页映射的机制中,程序的某个页被加载到内存中的物理地址都是不确定的。如果程序中直接使用物理地址,那么每一个页加载之后,都需要对整个程序进行重定位。
现代操作系统使用虚拟地址进行操作,由MMU将虚拟地址映射为物理地址。

6.3.1 进程的建立

从操作系统的角度来看,一个进程最关键的特征是它拥有独立的虚拟地址空间。
在有虚拟存储的情况下,创建一个进程最开始只需要做3件事情:

  1. 创建虚拟地址空间。并不是创建空间,而是创建页映射函数所需要的数据结构,即页目录。
  2. 读取可执行文件文,并且建立虚拟地址空间与可执行文件的映射关系。第一步是虚拟空间到物理地址的映射,这一步所做的是虚拟空间与可执行文件的映射。当程序访问某个虚拟地址发生缺页错误时,操作系统分配物理内存页,并将物理内存页与虚拟地址映射起来。然后将可执行文件加载到对应的虚拟地址。显然,必须要保存虚拟地址与可执行文件的映射关系,才能由虚拟地址找到对应的可执行文件。操作系统具体用什么结构来保存这个映射关系呢?书中并没有提到。
  3. 将cpu指令寄存器设置成可执行文件入口,启动运行。

6.3.2 页错误

6.4 进程虚拟空间分布

6.4.1 ELF文件链接视图和执行视图

ELF文件被映射时,是以系统的页的长度作为单位。每个段在映射时的长度都应该是系统页的长度的整数倍。一般ELF可以执行文件都有十多个段,会造成相当的内存浪费。
解决方法就是对于相同权限的段,把它们合并到一起当作一个段进行映射。
所以ELF文件引入一个segment的概念,一个segment包含一个或多个属性类似的section。
链接器在把目标文件链接成可执行文件时,会尽量把相同权限属性的段分配在同一空间。

示例程序:

/*SectionMapping.c*/
#include <stdlib.h>

int main()
{
    while(1)
    {
        sleep(1000);
    }
    return 0;
}

段表结构:

root@debian:~# readelf -S SectionMapping.elf
There are 31 section headers, starting at offset 0xb16c4:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .note.ABI-tag     NOTE            080480f4 0000f4 000020 00   A  0   0  4
  [ 2] .note.gnu.build-i NOTE            08048114 000114 000024 00   A  0   0  4
readelf: Warning: [ 3]: Link field (0) should index a symtab section.
  [ 3] .rel.plt          REL             08048138 000138 000078 08  AI  0  23  4
  [ 4] .init             PROGBITS        080481b0 0001b0 000023 00  AX  0   0  4
  [ 5] .plt              PROGBITS        080481e0 0001e0 0000f0 00  AX  0   0 16
  [ 6] .text             PROGBITS        080482d0 0002d0 073644 00  AX  0   0 16
  [ 7] __libc_freeres_fn PROGBITS        080bb920 073920 000a6d 00  AX  0   0 16
  [ 8] __libc_thread_fre PROGBITS        080bc390 074390 00009e 00  AX  0   0 16
  [ 9] .fini             PROGBITS        080bc430 074430 000014 00  AX  0   0  4
  [10] .rodata           PROGBITS        080bc460 074460 01a46c 00   A  0   0 32
  [11] __libc_subfreeres PROGBITS        080d68cc 08e8cc 000028 00   A  0   0  4
  [12] __libc_IO_vtables PROGBITS        080d6900 08e900 000354 00   A  0   0 32
  [13] __libc_atexit     PROGBITS        080d6c54 08ec54 000004 00   A  0   0  4
  [14] __libc_thread_sub PROGBITS        080d6c58 08ec58 000004 00   A  0   0  4
  [15] .eh_frame         PROGBITS        080d6c5c 08ec5c 012c90 00   A  0   0  4
  [16] .gcc_except_table PROGBITS        080e98ec 0a18ec 0000af 00   A  0   0  1
  [17] .tdata            PROGBITS        080eaf5c 0a1f5c 000010 00 WAT  0   0  4
  [18] .tbss             NOBITS          080eaf6c 0a1f6c 000018 00 WAT  0   0  4
  [19] .init_array       INIT_ARRAY      080eaf6c 0a1f6c 000008 04  WA  0   0  4
  [20] .fini_array       FINI_ARRAY      080eaf74 0a1f74 000008 04  WA  0   0  4
  [21] .jcr              PROGBITS        080eaf7c 0a1f7c 000004 00  WA  0   0  4
  [22] .data.rel.ro      PROGBITS        080eaf80 0a1f80 000070 00  WA  0   0 32
  [23] .got.plt          PROGBITS        080eb000 0a2000 000048 04  WA  0   0  4
  [24] .data             PROGBITS        080eb060 0a2060 000f20 00  WA  0   0 32
  [25] .bss              NOBITS          080ebf80 0a2f80 000e0c 00  WA  0   0 32
  [26] __libc_freeres_pt NOBITS          080ecd8c 0a2f80 000018 00  WA  0   0  4
  [27] .comment          PROGBITS        00000000 0a2f80 00002d 01  MS  0   0  1
  [28] .symtab           SYMTAB          00000000 0a2fb0 007c20 10     29 846  4
  [29] .strtab           STRTAB          00000000 0aabd0 00699a 00      0   0  1
  [30] .shstrtab         STRTAB          00000000 0b156a 000159 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specif)

文件居然多达31个段。
描述section属性的结构叫做段表,描述segment的结构叫做程序头。描述ELF文件该如何被操作系统映射到内存空间。

root@debian:~# readelf -l SectionMapping.elf

Elf file type is EXEC (Executable file)
Entry point 0x804887f
There are 6 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x08048000 0x08048000 0xa199b 0xa199b R E 0x1000
  LOAD           0x0a1f5c 0x080eaf5c 0x080eaf5c 0x01024 0x01e48 RW  0x1000
  NOTE           0x0000f4 0x080480f4 0x080480f4 0x00044 0x00044 R   0x4
  TLS            0x0a1f5c 0x080eaf5c 0x080eaf5c 0x00010 0x00028 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO      0x0a1f5c 0x080eaf5c 0x080eaf5c 0x000a4 0x000a4 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     .note.ABI-tag .note.gnu.build-id .rel.plt .init .plt .text __libc_freeres_fn __libc_thread_freeres_fn .fini .rodata __libc_subfreeres __libc_IO_vtables __libc_atexit __libc_thread_subfreeres .eh_frame .gcc_except_table
   01     .tdata .init_array .fini_array .jcr .data.rel.ro .got.plt .data .bss __libc_freeres_ptrs
   02     .note.ABI-tag .note.gnu.build-id
   03     .tdata .tbss
   04
   05     .tdata .init_array .fini_array .jcr .data.rel.ro

只有6个segment,其中两个需要装载。section到segement的对应关系也补显示了出来。
从section的角度来看ELF文件就是链接视图。从segment的角度来看就是执行视图。

ELF可执行文件和共享库文件中有一个专门的数据结构叫做程序头表,用来保存segment的信息。因为ELF目标文件不需要被装载,所以它没有程序头表。
程序头表是一个结构体数组:

typedef struct
{
  Elf32_Word    p_type;                 /* Segment type */
  Elf32_Off     p_offset;               /* Segment file offset */
  Elf32_Addr    p_vaddr;                /* Segment virtual address */
  Elf32_Addr    p_paddr;                /* Segment physical address */
  Elf32_Word    p_filesz;               /* Segment size in file */
  Elf32_Word    p_memsz;                /* Segment size in memory */
  Elf32_Word    p_flags;                /* Segment flags */
  Elf32_Word    p_align;                /* Segment alignment */
} Elf32_Phdr;

结构体成员与readelf -l的输出一一对应。
但是文章没讲到,section与segment的对应关系是保存在什么数据结构里面的。

6.4.2 堆和栈

VMA除了被用来映射可执行文件的各个Segment外,还用来对进程和地址空间进行管理。
堆和栈都有对应的VMA。可以通过cat /proc/pid/maps查看。

一个进程基本上可以分为如下几种VMA区域:
代码VMA 权限只读,可执行。有映射文件
数据VMA 权限可读写,可执行。有映像文件
堆VMA 权限可读写,可执行。无映像文件,匿名,可向上扩展
栈VMA 权限可读写,不可执行 无映像文件 可向下扩展

6.4.3 堆的最大申请数量

用malloc测试,32位机器。作者最大能申请2.9GB左右。
每次运行结果可能不同,因为一些操作系统使用了ASLR技术,使得进程堆空间变小。
但是我在我的虚拟机上跑这个程序,只能申请到1.9G。

/*mallocTest1.c*/
#include <stdio.h>
#include <stdlib.h>

unsigned int maximum = 0;

int main(void)
{
    unsigned blocksize[] = {1024 * 1024, 1024, 1};
    void *block;
    int i, count;

    for(i = 0; i < 3; i++) {
        for(count = 1; ; count++) {
            block = malloc(maximum + blocksize[i] * count);
            if (block) {
                maximum = maximum + blocksize[i] * count;
                free(block);
            } else {
                break;
            }
        }
    }

    printf("maximum malloc size = %u bytes.\n", maximum);
    printf("maximum malloc size = %f MB\n",((float)maximum)/1024/1024);
    printf("maximum malloc size = %f GB\n",((float)maximum)/1024/1024/1024);
}

测试结果:

maximum malloc size = 2021424851 bytes.
maximum malloc size = 1927.781006 MB
maximum malloc size = 1.882599 GB
root@debian:~# ./mallocTest
maximum malloc size = 2021432980 bytes.
maximum malloc size = 1927.788696 MB
maximum malloc size = 1.882606 GB
root@debian:~# ./mallocTest
maximum malloc size = 2021453807 bytes.
maximum malloc size = 1927.808594 MB
maximum malloc size = 1.882626 GB
root@debian:~# ./mallocTest
maximum malloc size = 2021494727 bytes.
maximum malloc size = 1927.847656 MB
maximum malloc size = 1.882664 GB

6.4.4 段地址对齐

待补充…

猜你喜欢

转载自blog.csdn.net/qq_31567335/article/details/84420554