Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1305027
  • 博文数量: 92
  • 博客积分: 10389
  • 博客等级: 上将
  • 技术积分: 1918
  • 用 户 组: 普通用户
  • 注册时间: 2006-08-10 16:13
文章存档

2014年(1)

2012年(15)

2009年(6)

2008年(37)

2007年(72)

2006年(54)

我的朋友

分类: LINUX

2008-01-28 15:15:56

  2. PROGRAM LOADING AND DYNAMIC LINKING
                   程序装入和动态链接
   ________________________________________________________________


   ======================== Introduction(介绍) =========================


第二部分描述了 object file 信息和创建运行程序的系统行为。其中部分信息
适合所有的系统,其他信息是和特定处理器相关的。


可执行和共享的 object file 静态的描绘了程序。为了执行这样的程序,系统
用这些文件创建动态的程序表现,或进程映像。一个进程映像有用于保存其代码、
数据、堆栈等等的段。这个部分的主要章节讨论如下的内容。


* 程序头(Program header)。该章节补充第一部分,描述和程序运行相关的
  object file 结构。即文件中主要的数据结构、程序头表、定位段映像,也
  包含了为该程序创建内存映像所需要的信息。

* 载入程序(Program loading)。在给定一个 object file 时,系统为了
  让它运行必须将它载入内存。

* 动态链接(Dynamic linking)。在载入了程序之后,系统必须通过解决组
  成该进程的 object file之间的符号引用问题来完成进程映像的过程。

注意:指定了处理器范围的 ELF 常量是有命名约定的。比如,DT_ , PT_ ,
用于特定处理器扩展名,组合了处理器的名称(如 DT_M32_SPECIAL )。
没有使用这种约定但是预先存在的处理器扩展名是允许的。


       Pre-existing Extensions
                       (预先存在的扩展名)
       =======================
      DT_JMP_REL


   ====================== Program Header(程序头)  ======================


一个可执行的或共享的 object file 的程序头表是一个结构数组,每一个
结构描述一个段或其他系统准备执行该程序所需要的信息。一个 object file
段包含一个或多个部分(就象下面的“段目录”所描述的那样)。程序头仅仅对于
可执行或共享的 object file 有意义。一个文件使用 ELF 头的 e_phentsize
和 e_phnum 成员来指定其拥有的程序头大小。[参阅 第一部分中的 "ELF 头"]


+ Figure 2-1: Program Header

  typedef struct {
      Elf32_Word p_type;
      Elf32_Off p_offset;
      Elf32_Addr p_vaddr;
      Elf32_Addr p_paddr;
      Elf32_Word p_filesz;
      Elf32_Word p_memsz;
      Elf32_Word p_flags;
      Elf32_Word p_align;
  } Elf32_Phdr;

* p_type

  该成员指出了这个数组的元素描述了什么类型的段,或怎样解释该数组元素的信息。
  类型值和含义如下所述。

* p_offset

  该成员给出了该段的驻留位置相对于文件开始处的偏移。

* p_vaddr

  该成员给出了该段在内存中的首字节地址。

* p_paddr

  在物理地址定位有关联的系统中,该成员是为该段的物理地址而保留的。由于
  System V 忽略了应用程序的物理地址定位,该成员对于可执行文件和共享的
  object 而言是未指定内容的。


* p_filesz

  该成员给出了文件映像中该段的字节数;它可能是 0 。
 
* p_memsz

  该成员给出了内存映像中该段的字节数;它可能是 0 。

* p_flags

  该成员给出了和该段相关的标志。定义的标志值如下所述。

* p_align

  就象在后面“载入程序”部分中所说的那样,可载入的进程段必须有合适的
  p_vaddr 、 p_offset 值,取页面大小的模。该成员给出了该段在内存和
  文件中排列值。 0 和 1 表示不需要排列。否则, p_align 必须为正的 2 的幂,
  并且 p_vaddr 应当等于 p_offset 模 p_align 。


某些入口描述了进程段;其他的则提供补充信息并且无益于进程映像。已经
定义的入口可以以任何顺序出现,除非是下面明确声明的。后面是段类型值;
其他的值保留以便将来用于其他用途。


+ Figure 2-2: Segment Types, p_type

  Name             Value
  ====             =====
  PT_NULL              0
  PT_LOAD              1
  PT_DYNAMIC           2
  PT_INTERP            3
  PT_NOTE              4
  PT_SHLIB             5
  PT_PHDR              6
  PT_LOPROC   0x70000000
  PT_HIPROC   0x7fffffff

* PT_NULL

  该数组元素未使用;其他的成员值是未定义的。这种类型让程序头表忽略入口。

* PT_LOAD

  该数组元素指定一个可载入的段,由 p_filesz 和 p_memsz 描述。文件中
  字节被映射到内存段中。如果该段的内存大小( p_memsz )比文件大小( p_filesz )
  要大,则多出的字节将象段初始化区域那样保持为 0 。文件的大小不会比内存大小值大。
  在程序头表中,可载入段入口是以 p_vaddr 的升序排列的。

* PT_DYNAMIC

  该数组元素指定动态链接信息。参阅 后面的“动态部分”以获得更多信息。

* PT_INTERP

  该数组元素指定一个 null-terminated 路径名的位置和大小(作为解释程序)。
  这种段类型仅仅对可执行文件有意义(尽管它可能发生在一个共享 object 上);
  它在一个文件中只能出现一次。如果它出现,它必须先于任何一个可载入段入口。
  参阅 后面的“程序解释器”(Program Interpreter)以获得更多的信息。


* PT_NOTE

  该数组元素指定辅助信息的位置和大小。参阅 后面的“注意部分”以获得细节。

* PT_SHLIB

  该段类型保留且具有未指定的语义。具有一个这种类型数组元素的程序并不
  遵守 ABI 。

* PT_PHDR

  该数组元素(如果出现),指定了程序头表本身的位置和大小(包括在文件中
  和在该程序的内存映像中)。更进一步来说,它仅仅在该程序头表是程序内存映像
  的一部分时才有效。如果它出现,它必须先于任何可载入段入口。参阅 后面的
  “程序解释器”(Program Interpreter)以获得更多的信息。


* PT_LOPROC through PT_HIPROC

  该范围中的值保留用于特定处理器的语义。

注意:除非在别处的特殊要求,所有的程序头的段类型是可选的。也就是说,
一个文件的程序头表也许仅仅包含和其内容相关的元素。



  Base Address(基地址)

可执行和共享的 object file 有一个基地址,该基地址是与程序的 object file
在内存中映像相关的最低虚拟地址。基地址的用途之一是在动态链接过程中重定位
该程序的内存映像。


一个可执行的 object file 或 一个共享的 object file 的基地址是在
执行的时候从三个值计算而来的:内存载入地址、页面大小的最大值 和 程序可
载入段的最低虚拟地址。就象在“程序载入”中所描述的那样,程序头中的虚拟地址
也许和程序的内存映像中实际的虚拟地址并不相同。为了计算基地址,必须确定与
PT_LOAD 段 p_vaddr 的最小值相关的内存地址。获得基地址的方法是将内存
地址截去最大页面大小的最接近的整数倍。由于依赖载入内存中的文件类型,
该内存地址和 p_vaddr 值可能匹配也可能不匹配。


就象在第一部分中 "Section" 中描述的那样, .bss section 具有 SHT_NOBITS
的类型。尽管在文件中不占用空间,它在段的内存映像中起作用。通常,没有初始化
的数据驻留在段尾,因此使得在相关的程序头元素中的 p_memsz 比 p_filesz 大。


  Note Section(注解部分)

有的时候供应商或系统设计者需要用特定的信息标记一个
object file 以便其他程序检查其兼容的一致性,等等此类。 SHT_NOTE
类型的 section 和 PT_NOTE 类型的程序头元素能够被用于此目的。 section
和程序头中的注解信息包含了任意数目的入口,每一个入口的格式都是对应于特定
处理器格式的 4-字节数组。下面的标签有助于解释注释信息的组织形式,但是这些
标签不是规格说明的一部分。


+ Figure 2-3: Note Information

  namesz
  descsz
  type
  name ...
  desc ...

* namesz and name

  名字中 namesz 的第一个字节包含了一个 null-terminated 字符
  表达了该入口的拥有者或始发者。没有正式的机制来避免名字冲突。从
  惯例来说,供应商使用他们自己的名称,比如 "XYZ Computer Company" ,
  作为标志。如果没有提供名字, namesz 值为 0 。 如果有必要,确定
  描述信息4-字节对齐。 这样的填充信息并不包含在namesz 中。


* descsz and desc

  desc 中 descsz 的首字节包含了注解描述符。ABI 不会在一个描述符内容中
  放入任何系统参数。如果没有描述符, descsz 将为 0 。  如果有必要,确定
  描述信息4-字节对齐。 这样的填充信息并不包含在descsz中。


* type

  该 word 给出了描述符的解释。每一个创造着(originator) 控制着自己的类型;
  对于单单一个类型值的多种解释是可能存在的。因此,一个程序必须辨认出该名字
  和其类型以便理解一个描述符。这个时候的类型必须是非负的。ABI 没有定义
  描述符的含义。

为了举例说明,下面的解释段包含两个入口。

+ Figure 2-4: Example Note Segment

           +0   +1   +2   +3
          -------------------
  namesz           7
  descsz           0           No descriptor
    type           1
    name   X    Y    Z    spc
           C    o    \0   pad
  namesz           7
  descsz           8
    type           3
    name   X    Y    Z    spc
           C    o    \0   pad
    desc         word0
                 word1

注意:系统保留的注解信息没有名字 (namesz==0) ,有一个零长度的名字
(name[0]=='\0') 现在还没有类型为其定义。所有其他的名字必须至少有
一个非空的字符。


注意:注解信息是可选的。注解信息的出现并不影响一个程序的 ABI 一致性,
前提是该信息不影响程序的执行行为。否则,该程序将不遵循 ABI 并将出现
未定义的行为。


   ===================== Program Loading(程序载入) =====================

当创建或增加一个进程映像的时候,系统在理论上将拷贝一个文件的段到一个虚拟
的内存段。系统什么时候实际地读文件依赖于程序的执行行为,系统载入等等。一个
进程仅仅在执行时需要引用逻辑页面的时候才需要一个物理页面,实际上进程通常会
留下许多未引用的页面。因此推迟物理上的读取常常可以避免这些情况,改良系统的
特性。为了在实践中达到这种效果,可执行的和共享的 object file 必须具有
合适于页面大小取模值的文件偏移和虚拟地址这样条件的段映像。

虚拟地址和文件偏移在 SYSTEM V 结构的段中是模 4KB(0x1000) 或大的 2 的幂。
由于 4KB 是最大的页面大小,因此无论物理页面大小是多少,文件必须去适合页面。


+ Figure 2-5: Executable File

           File Offset   File                  Virtual Address
           ===========   ====                  ===============
                     0   ELF header
  Program header table
                         Other information
                 0x100   Text segment          0x8048100
                         ...
                         0x2be00 bytes         0x8073eff
               0x2bf00   Data segment          0x8074f00
                         ...
                         0x4e00 bytes          0x8079cff
               0x30d00   Other information
                         ...

+ Figure 2-6: Program Header Segments(程序头段)

  Member    Text Data
  ======    ====         ====
  p_type    PT_LOAD      PT_LOAD
  p_offset  0x100 0x2bf00
  p_vaddr   0x8048100 0x8074f00
  p_paddr   unspecified unspecified
  p_filesz  0x2be00 0x4e00
  p_memsz   0x2be00 0x5e24
  p_flags   PF_R+PF_X    PF_R+PF_W+PF_X
  p_align   0x1000 0x1000

尽管示例中的文件偏移和虚拟地址在文本和数据两方面都适合模 4KB ,但是还有
4 个文件页面混合了代码和数据(依赖于页面大小和文件系统块的大小)。


* 第一个文本页面包含了 ELF 头、程序头以及其他信息。
* 最后的文本页包含了一个数据开始的拷贝。
* 第一个数据页面有一个文本结束的拷贝。
* 最后的数据页面也许会包含与正在运行的进程无关的文件信息。

理论上,系统强制内存中段的区别;段地址被调整为适应每一个逻辑页面在地址空间
中有一个简单的准许集合。在上面的示例中,包含文本结束和数据开始的文件区域将
被映射两次:在一个虚拟地址上为文本而另一个虚拟地址上为数据。


数据段的结束处需要对未初始化的数据进行特殊处理(系统定义的以 0 值开始)。
因此如果一个文件包含信息的最后一个数据页面不在逻辑内存页面中,则无关的
数据应当被置为 0 (这里不是指未知的可执行文件的内容)。在其他三个页面中
"Impurities" 理论上并不是进程映像的一部分;系统是否擦掉它们是未指定的。
下面程序的内存映像假设了 4KB 的页面。


+ Figure 2-7: Process Image Segments(进程映像段)

  Virtual Address  Contents            Segment
  ===============  ========            =======
        0x8048000  Header padding      Text
                   0x100 bytes
        0x8048100  Text segment
                   ...
                   0x2be00 bytes
        0x8073f00  Data padding
                   0x100 bytes
        0x8074000  Text padding        Data
                   0xf00 bytes
        0x8074f00  Data segment
                   ...
                   0x4e00 bytes
        0x8079d00  Uninitialized data
                   0x1024 zero bytes
        0x807ad24  Page padding
                   0x2dc zero bytes

可执行文件和共享文件在段载入方面有所不同。典型地,可执行文件段包含了
绝对代码。为了让进程正确执行,这些段必须驻留在建立可执行文件的虚拟地址
处。因此系统使用不变的 p_vaddr 作为虚拟地址。


另一方面,共享文件段包含与位置无关的代码。这让不同进程的相应段虚拟地址
各不相同,且不影响执行。虽然系统为各个进程选择虚拟地址,它还要维护各个
段的相对位置。因为位置无关的代码在段间使用相对定址,故而内存中的虚拟地址
的不同必须符合文件中虚拟地址的不同。下表给出了几个进程可能的共享对象虚拟
地址的分配,演示了不变的相对定位。该表同时演示了基地址的计算。


+ Figure 2-8: Example Shared Object Segment Addresses

  Sourc             Text        Data  Base Address
  =====             ====        ====  ============
  File             0x200     0x2a400           0x0
  Process 1   0x80000200  0x8002a400    0x80000000
  Process 2   0x80081200  0x800ab400    0x80081000
  Process 3   0x900c0200  0x900ea400    0x900c0000
  Process 4   0x900c6200  0x900f0400    0x900c6000


   ==================== Dynamic Linking (动态链接) =====================


一个可执行文件可能有一个 PT_INTERP 程序头元素。在 exec(BA_OS) 的
过程中,系统从 PT_INTERP 段中取回一个路径名并由解释器文件的段创建初始的
进程映像。也就是说,系统为解释器“编写”了一个内存映像,而不是使用原始
的可执行文件的段映像。此时该解释器就负责接收系统来的控制并且为应用程序
提供一个环境变量。


解释器使用两种方法中的一种来接收系统来的控制。首先,它会接收一个文件描述符
来读取该可执行文件,定位于开头。它可以使用这个文件描述符来读取 并且(或者)
映射该可执行文件的段到内存中。其次,依赖于该可执行文件的格式,系统会载入
这个可执行文件到内存中而不是给该解释器一个文件描述符。伴随着可能的文件描述符
异常的情况,解释器的初始进程声明应匹配该可执行文件应当收到的内容。解释器本身
并不需要第二个解释器。一个解释器可能是一个共享对象也可能是一个可执行文件。


* 一个共享对象(通常的情况)在被载入的时候是位置无关的,各个进程可能不同;
  系统在 mmap(KE_OS) 使用的动态段域为它创建段和相关的服务。因而,一个
  共享对象的解释器将不会和原始的可执行文件的原始段地址相冲突。


* 一个可执行文件被载入到固定地址;系统使用程序头表中的虚拟地址为其创建段。
  因而,一个可执行文件解释器的虚拟地址可能和第一个可执行文件相冲突;这种
  冲突由解释器来解决。



Dynamic Linker(动态链接器)

当使用动态链接方式建立一个可执行文件时,链接器把一个 PT_INTERP 类型
的元素加到可执行文件中,告诉系统把动态链接器做为该程序的解释器。

注意:由系统提供的动态链接器是和特定处理器相关的。

Exec(BA_OS) 和动态链接器合作为程序创建进程,必须有如下的动作:

* 将可执行文件的内存段加入进程映像中;
* 将共享对象的内存段加入进程映像中;
* 为可执行文件和它的共享对象进行重定位;
* 如果有一个用于读取可执行文件的文件描述符传递给了动态链接器,那么关闭它。
* 向程序传递控制,就象该程序已经直接从 exec(BA_OS) 接收控制一样。

链接器同时也为动态链接器构建各种可执行文件和共享对象文件的相关数据。就象
在上面“程序头”中说的那样,这些数据驻留在可载入段中,使得它们在执行过程
中有效。(再一次的,要记住精确的段内容是处理器相关的。可以参阅相应处理器
的补充说明来获得详尽的信息。)


* 一个具有 SHT_DYNAMIC 类型的 .dynamic section 包含各种数据。驻留在
  section 开头的结构包含了其他动态链接信息的地址。

* SHT_HASH 类型的 .hash section 包含了一个 symbol hash table.

* SHT_PROGBITS 类型的 .got 和 .plt section 包含了两个分离的 table:
  全局偏移表和过程链接表。 下面的 section 演示了动态链接器使用和改变
  这些表来为 object file 创建内存映像。


由于每一个遵循 ABI 的程序从一个共享对象库中输入基本的系统服务,因此动态
链接器分享于每一个遵循 ABI 的程序的执行过程中。


就象在处理器补充说明的“程序载入”所解释的那样,共享对象也许会占用与记录在
文件的程序头表中的地址不同的虚拟内存地址。动态链接器重定位内存映像,在应用程序
获得控制之前更新绝对地址。尽管在库被载入到由程序头表指定的地址的情况下绝对地址
应当是正确的,通常的情况却不是这样。


如果进程环境 [see exec(BA_OS)] 包含了一个非零的 LD_BIND_NOW 变量,
动态链接器将在控制传递到程序之前进行所有的重定位。举例而言,所有下面的
环境入口将指定这种行为。


* LD_BIND_NOW=1
* LD_BIND_NOW=on
* LD_BIND_NOW=off

其他情况下, LD_BIND_NOW 或者不在环境中或者为空值。动态链接器可以不急于
处理过程链接表入口,因而避免了对没有调用的函数的符号解析和重定位。参阅
"Procedure Linkage Table"获取更多的信息。
阅读(1504) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~