第3部分 装载与动态链接---(6)可执行文件的装载与进程

########################
# 6、可执行文件的装载与进程
########################

# 进程虚拟地址空间
程序(或者狭义上讲可执行文件)是一个静态的概念,它就是一些预先编译好的指令和数据集合的一个文件;
进程则是一个动态的概念,它是程序运行时的一个过程。
每个程序被运行起来以后,它将拥有自己独立的虚拟地址空间(Virtual Address Space),这个虚拟地址空间的大小由计算机的硬件平台决定,具体地说是由CPU的位数决定。
32的硬件平台决定了虚拟地址空间的地址为0到2^32 - 1,即0x00000000~0xFFFFFFF,也就是4G虚拟空间。
64位的硬件平台具有64位寻址能力,它的虚拟空间地址达到了2^64字节。
一般来说,C语言指针大小的位数与虚拟空间的位数相同,如32位平台下的指针为32位,即4字节;64位平台下的指针为64位,即8字节。
32位平台上的4GB虚拟空间被划分为两部分:从0xC0000000到0xFFFFFFFF共1GB留给操作系统本身用;从0x00000000到0xBFFFFFFF共3GB空间留给进程使用。


# 装载的方式
程序执行时所需要的指令和数据必须在内存中才能够正常运行,为了在不添加内存的情况下让更多的程序运行起来,我们将程序最常用的部分驻留在内存中,
将一些不常用的数据存放在磁盘里面,这就是动态装入的基本原理。
覆盖装入(Overlay)和 页映射(Paging)是两种典型的动态装载方法。
页映射是虚拟存储机制的一部分,将内存和所有磁盘中的数据和指令按照"页(Page)"为单位划分成若干个页,以后所有的装载和操作的单位是就是页。
假设32位机器有16KB的内存,每个页大小为4096字节则共有4个页,编号为F0~F3。假设程序所有的指令和数据总和为32KB,那么程序总共被分为8个页,编号为P0~P7。


# 从操作系统角度看可执行文件的装载
进程的建立:
    (1)创建一个独立的虚拟地址空间。 虚拟空间---物理内存的映射关系
创建虚拟地址空间实际上只是分配一个页目录,甚至不设置页映射关系,这些映射关系等到后面程序发生页错误的时候再进行设置。
    (2)读取可执行文件,并且建立虚拟空间与可执行文件的映射关系。 虚拟空间---可执行文件的映射关系
这种映射关系只是保存在操作系统内的 一个数据结构,Linux中将进程虚拟空间中的一个段叫做虚拟内存区域(VMA,Virtual Memory Area)。
由于可执行文件在装载时实际上是被映射的虚拟空间,所以可执行文件很多时候又被叫做映射文件(Image)。
假设ELF可执行文件只有一个代码段".text",它的虚拟地址为0x08048000,它在文件中的大小为0x000e1,对齐为0x1000。由于虚拟存储的页映射是以页为单位的,
32位CPU一般为4096字节,所以32位ELF的对齐粒度为0x1000。由于.text段大小不到一个页,考虑到对齐所以占用一个段。
    (3)将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。操作系统通过设置CPU的指令寄存器将将控制权转交给进程,由进程开始执行。


页错误
    假设在上面的例子中,程序的入口地址为0x08048000,即刚好是.text段的起始地址。当CPU开始打算执行这个地址的指令时,发现页面0x08048000~0x08049000是个空页面,于是它认为这是一个页错误(Page Fault)。CPU将控制权交给操作系统,操作系统有专门的页错误处理程序来处理这种情况。
操作系统将查询这个数据结构( 虚拟空间---可执行文件的映射关系所保存的),找到空页面所在的VMA,计算出相应的页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射关系,然后把控制权再还回给进程,进程从刚才页错误的位置重新开始执行。

# 进程虚存空间分布
ELF文件被映射时,是以系统的页长度作为单位的,那么每个段在映射时的长度应该都是系统页长度的整数倍。如果不是,那么多余部分也将占用一个页。
一个ELF文件中往往有十几个段,那么内存空间的浪费非常大。
操作系统在装载可执行文件时,只关心一些跟装载相关的问题,最主要的是段的权限(可读、可写、可执行)。
ELF文件中,段的权限主要有几种组合:
  (1)代码段:可读可执行的段
  (2)数据段和BSS段:可读可写的段
  (3)只读数据段:只读的段
对于相同权限的段,把它们合并到一起当作一个段进行映射。
从链接的角度看,ELF文件是按"Section"存储的;从装载的角度看,ELF文件可以按照"Segment"划分。
一个"Segment"包含一个或多个属性类似的"Section"。

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

int main()
{
    while(1)
    {
        sleep(1000);
    }
    return 0;
}
// 使用静态连接的方式将其编译连接成可执行文件
# gcc -static SectionMapping.c -o SectionMapping.elf
// 可以看到这个可执行文件中总共有33个段(Section),描述"Section"属性的结构叫做段表。
# readelf -S SectionMapping.elf      // 查看可执行文件的 Section
There are 33 section headers, starting at offset 0xde570:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .note.ABI-tag     NOTE             0000000000400190  00000190
       0000000000000020  0000000000000000   A       0     0     4
  [ 2] .note.gnu.build-i NOTE             00000000004001b0  000001b0
       0000000000000024  0000000000000000   A       0     0     4
  [ 3] .rela.plt         RELA             00000000004001d8  000001d8
       00000000000000f0  0000000000000018  AI       0    24     8
  [ 4] .init             PROGBITS         00000000004002c8  000002c8
       000000000000001a  0000000000000000  AX       0     0     4
  [ 5] .plt              PROGBITS         00000000004002f0  000002f0
       00000000000000a0  0000000000000000  AX       0     0     16
  [ 6] .text             PROGBITS         0000000000400390  00000390
       000000000009e4e4  0000000000000000  AX       0     0     16
  [ 7] __libc_freeres_fn PROGBITS         000000000049e880  0009e880
       0000000000002529  0000000000000000  AX       0     0     16
  [ 8] __libc_thread_fre PROGBITS         00000000004a0db0  000a0db0
       00000000000000de  0000000000000000  AX       0     0     16
  [ 9] .fini             PROGBITS         00000000004a0e90  000a0e90
       0000000000000009  0000000000000000  AX       0     0     4
  [10] .rodata           PROGBITS         00000000004a0ea0  000a0ea0
       000000000001d244  0000000000000000   A       0     0     32
  [11] __libc_subfreeres PROGBITS         00000000004be0e8  000be0e8
       0000000000000050  0000000000000000   A       0     0     8
  [12] __libc_atexit     PROGBITS         00000000004be138  000be138
       0000000000000008  0000000000000000   A       0     0     8
  [13] .stapsdt.base     PROGBITS         00000000004be140  000be140
       0000000000000001  0000000000000000   A       0     0     1
  [14] __libc_thread_sub PROGBITS         00000000004be148  000be148
       0000000000000008  0000000000000000   A       0     0     8
  [15] .eh_frame         PROGBITS         00000000004be150  000be150
       000000000000af8c  0000000000000000   A       0     0     8
  [16] .gcc_except_table PROGBITS         00000000004c90dc  000c90dc
       00000000000000a3  0000000000000000   A       0     0     1
  [17] .tdata            PROGBITS         00000000006c9eb8  000c9eb8
       0000000000000020  0000000000000000 WAT       0     0     8
  [18] .tbss             NOBITS           00000000006c9ed8  000c9ed8
       0000000000000030  0000000000000000 WAT       0     0     8
  [19] .init_array       INIT_ARRAY       00000000006c9ed8  000c9ed8
       0000000000000010  0000000000000000  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       00000000006c9ee8  000c9ee8
       0000000000000010  0000000000000000  WA       0     0     8
  [21] .jcr              PROGBITS         00000000006c9ef8  000c9ef8
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .data.rel.ro      PROGBITS         00000000006c9f00  000c9f00
       00000000000000e4  0000000000000000  WA       0     0     32
  [23] .got              PROGBITS         00000000006c9fe8  000c9fe8
       0000000000000010  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         00000000006ca000  000ca000
       0000000000000068  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         00000000006ca080  000ca080
       0000000000001ad0  0000000000000000  WA       0     0     32
  [26] .bss              NOBITS           00000000006cbb60  000cbb50
       0000000000001878  0000000000000000  WA       0     0     32
  [27] __libc_freeres_pt NOBITS           00000000006cd3d8  000cbb50
       0000000000000030  0000000000000000  WA       0     0     8
  [28] .comment          PROGBITS         0000000000000000  000cbb50
       0000000000000034  0000000000000001  MS       0     0     1
  [29] .note.stapsdt     NOTE             0000000000000000  000cbb84
       0000000000000f18  0000000000000000           0     0     4
  [30] .shstrtab         STRTAB           0000000000000000  000de401
       0000000000000169  0000000000000000           0     0     1
  [31] .symtab           SYMTAB           0000000000000000  000ccaa0
       000000000000b0e8  0000000000000018          32   711     8
  [32] .strtab           STRTAB           0000000000000000  000d7b88
       0000000000006879  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
// 查看ELF的"Segment",描述"Segment"的结构叫做程序头(Program Header),它描述了ELF文件该如何被操作系统映射到进程的虚拟空间。
# readelf -l SectionMapping.elf  
Elf file type is EXEC (Executable file)
Entry point 0x400890
There are 6 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000c917f 0x00000000000c917f  R E    200000
  LOAD           0x00000000000c9eb8 0x00000000006c9eb8 0x00000000006c9eb8
                 0x0000000000001c98 0x0000000000003550  RW     200000
  NOTE           0x0000000000000190 0x0000000000400190 0x0000000000400190
                 0x0000000000000044 0x0000000000000044  R      4
  TLS            0x00000000000c9eb8 0x00000000006c9eb8 0x00000000006c9eb8
                 0x0000000000000020 0x0000000000000050  R      8
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x00000000000c9eb8 0x00000000006c9eb8 0x00000000006c9eb8
                 0x0000000000000148 0x0000000000000148  R      1

 Section to Segment mapping:
  Segment Sections...
   00     .note.ABI-tag .note.gnu.build-id .rela.plt .init .plt .text __libc_freeres_fn __libc_thread_freeres_fn .fini .rodata __libc_subfreeres __libc_atexit .stapsdt.base __libc_thread_subfreeres .eh_frame .gcc_except_table
   01     .tdata .init_array .fini_array .jcr .data.rel.ro .got .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 .got
可以看到这个可执行文件中共有6个segment,从装载的角度看, 我们只关心两个"LOAD"类型的Segment,因为只有它是需要被映射的,其他都是在装载时起辅助作用的。
可读可执行的段,它们被统一映射到VMA0;可读可写的段,它们被映 射到了VMA1;还有一部分段在程序装载时没有被映射的,它们是一些包含调试信息和字符串表等段,
这些段在程序执行时没有用,所以不需要被映射。所有相同属性的"Section"被归类到一个"Segment",并且映射到同一个VMA。

堆和栈
    操作系统通过使用VMA来对进程的地址空间进行管理,进程在执行的时候还需要用到栈(Stack)、堆(Heap)等空间,
它们在进程的虚拟空间中的表现也是以VMA的形式存在的。很多情况下,一个进程中的栈和堆分别都有一个对应的VMA。
通过"/proc"来查看进程的虚拟空间分布:
第一列是VMA的地址范围;第二列是VMA的权限,"p"表示私有,"s"表示共享;
第三列是偏移,表示VMA对应的Segment在映像文件中的偏移;
第四列表示映像文件所在设备的主设备号和次设备号;
第五列表示映像文件的节点号;
第六列是映像文件的路径。
# ./SectionMapping.elf &
[1] 3519
# cat /proc/3519/maps
00400000-004ca000 r-xp 00000000 08:01 1835499                            /home/xl/xiaoloaw_study/compile_study/partThree/SectionMapping.elf
006c9000-006cc000 rw-p 000c9000 08:01 1835499                            /home/xl/xiaoloaw_study/compile_study/partThree/SectionMapping.elf
006cc000-006ce000 rw-p 00000000 00:00 0
01f95000-01fb8000 rw-p 00000000 00:00 0                                  [heap]
7fff4e1e1000-7fff4e202000 rw-p 00000000 00:00 0                          [stack]
7fff4e24a000-7fff4e24c000 r--p 00000000 00:00 0                          [vvar]
7fff4e24c000-7fff4e24e000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
只有前两个是映射到可执行文件中的两个Segment。
其他段的文件所在设备主设备号和次设备号以及文件节点号都是0,则表示它们没有映射到文件中,这种VMA叫做匿名虚拟内存区域。
堆(Heap)和栈(Stack) 它们的大小分别为140KB(01fb8000 - 01f95000=0x23000=140KB)和 132KB(7fff4e202000 - 7fff4e1e1000=0x21000=132KB)
每个线程都有属于自己的堆栈,对于单线程的程序来说,这个VMA堆栈就全部归它使用。
堆是在用户空间中(小于0xC0000000的地址),栈是在内核空间中(大于0xC0000000的地址)。// 需要再确认一下
"vdso"是一个特殊的VMA,它的地址是在内核空间中,它是一个内核的模块,进程可以通过访问这个VMA来跟内核进行通信。
一个进程基本上可以分为如下几种VMA区域:
(1)代码VMA:权限只读、可执行;有映像文件。
(2)数据VMA:权限可读写、可执行;有映像文件。
(3)堆VMA:权限可读写、可执行;无映像文件,匿名,可向上扩展。
(4)栈VMA:权限可读写、不可执行;无映像文件,匿名,可向下扩展。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值