【链接与加载学习】第三章 目标文件

觉得这一章写的很乱,我懒得看windows的知识,也懒得看ibm的知识,所以直接上unix吧。

一、目标文件中都有什么?

一个目标文件包含五类信息。

  • 头信息:关于文件的整体信息,诸如代码大小,翻译成该目标文件的源文件名称和创建日期等。
  • 目标代码:程序和数据
  • 重定位信息:一个位置列表,链接器在修改目标代码的地址时会对他进行调整。
  • 符号:该模块定义的全局符号(导出),以及从其它模块导入的符号,或者由链接器定义的符号。
  • 调试信息:目标代码中与链接无关但会被调试器使用到的其他信息。

二、设计一个目标文件格式。

一个目标文件可能是可链接的(可重定位的),可执行的,可载入的(共享库)。

一个可链接的目标文件包含目标代码所需的扩展符号和重定位信息。目标代码经常会被划分成多个小逻辑段,而链接器会区别对待它们。

三、可重定为的a.out格式。

MS-DOS的.COM文件中出了二进制代码没有别的,所以它很弱。

而a.out就很强大了,可以重定位,假如能给一个进程创建一个地址空间,不需要加载时重定位。(多好啊,为什么不采用。)

3.1 初始a.out

初始的a.out包含一个小头文件,后面是可执行代码(文本段),然后是静态的初始值(数据段)。

把代码段和数据段分开是有优势的:

  • 每个区段寻址空间增加了。
  • 一个程序的多个实体可以共享一份程序代码的副本。

实际中一个目标文件可以有多个段,比如可以把数据段分为只读段和写时复制段。

a.out的头部

 1 struct exec

 2 {

 3     unsigned int a_info;    /* Use macros N_MAGIC, etc for access */

 4     unsigned a_text;    /* length of text, in bytes */

 5     unsigned a_data;    /* length of data, in bytes */

 6     unsigned a_bss;     /* length of uninitialized data area for file, in bytes */

 7     unsigned a_syms;    /* length of symbol table data in file, in bytes */

 8     unsigned a_entry;   /* start address */

 9     unsigned a_trsize;  /* length of relocation info for text, in bytes */

10     unsigned a_drsize;  /* length of relocation info for data, in bytes */

11 };

3.2 NMAGIC到ZMAGIC:与虚拟内存交互

操作系统加载和启动一个简单的双段文件的过程非常简单

  •  读取 a.out 的头部获取段的大小。
  •  检查是否已存在该文件的可共享代码段。如果是的话,将那个段映射到该进程的地址空间。如果不是,创建一个并将它映射到地址空间中,然后从文件中读取文本段放入这个新的内存区域。
  •  创建一个足够容纳数据段和 BSS 的私有数据段,将它映射到进程的地址空间中,然后从文件中读取数据段放入内存中的数据段并将 BSS 段对应的内存空间清零。
  •  创建一个栈的段并将其映射到进程的地址空间(由于数据堆和栈的增长方向不同,因此栈段通常是独立于数据段的)。将命令行或者调用程序传递的参数放入栈中。
  •  适当的设置各种寄存器并跳转到起始地址。

在一个分页系统中,上述的简单机制会为每一个文本段和数据段分配新的虚拟内存。由于 a.out 文件已经存储在磁盘中了,所以目标文件本身可以被映射到进程的地址空间中。虚拟内存只需要为程序写入的那些页分配新的磁盘空间,这样可以节省磁盘空间。并且由于虚拟内存系统只需要将程序确实需要的那些页从磁盘加载到内存中(而不是整个文件),这样也加快了程序启动的速度。
在加载的时候,各个段对齐到页边界,这样就是ZMAGIC了。

3.3 QMAGIC 格式的可执行文件

QMAGIC 格式的可执行文件中文本和数据段都各自扩充到一个整页,这样系统就可以很容易的将文件中的页映射到地址空间中的页。数据段的最后一页由值为零的 BSS 数据填充补齐;如果 BSS 数据大于可以填充补齐的空间,那么 a.out 的头部中会保存剩余需要分配的 BSS 空间大小。
只要地址是页对齐的,并且能够与链接器和加载器达成一致,加载到哪里都没有关系。

3.4 重定位项

a.out 头部
文本段
数据段
文本重定位表
数据重定位表
符号表
字符串

 

 

 

 

 

a.out头部已经讲过了,现在来看重定位表。

 1 typedef struct __index

 2 {

 3     unsigned long index         : 24; 

 4     unsigned long pc_rel        : 1;//是否是相对PC偏移。  5     unsigned long length        : 2;

 6     unsigned long extern_flag   : 1;//假如是off说明是内部,那么只是简单的重定位,比如说这是文本段,那么这个偏移就是相对文本段的。假如是on,那么是对外部符号的引用,用index是该文件符号表中符号序号。(这是文本重定位表)

 7     unsigned long spare         : 4;//备用

 8 }INDEX;

 9 typedef struct __text_reloc

10 {

11     unsigned long address;

12     INDEX index_i;

13 

14 }TEXT_RELOC;

15 TEXT_RELOC text_reloc[100];

如代码清单所示,假设第15行是一个表长度为100的表,每个表项的类型是TEXT_RELOC,其实对.data也适用。

我们要重定位表来干什么呢?重定位项有两个功能。当一个代码段被重定位到另一个不同的段基址时,重定位项标注出代码中需要被修改的地方。(不是说了,用虚拟空间解决这个问题吗?)

在一个可链接文件中,同样也有用来标注对未定义符号引用的重定位项,这样链接器就知道在最终解析符号时应当向何处补写符号的值。(带着疑问:符号表自己不能解决这个问题吗?)

3.5 符号和字串

a.out 文件的最后一个段是符号表。每个表项长度为 12 字节,描述一个符号。

typedef struct _symbol

{

    unsigned int name_offset;  //这是符号表,所有“名字字串”都在字符串表,所以在这里name_offset是这个名字字串在字符串表中的偏移量。

    unsigned char type;     //若低位被置位,则表明是全局的,若没置位,对链接器没有意义,但是可用于调试。

                     //重要的类型有文本、数据、bss,值为该模块的某一个地址,明显这个地址是向外部提供的。(导出符号表)

                               //abs absolute non-relocatable symbol,很少在调试信息以外的地方使用,值为(value)该符号的绝对地址。

                               //undefined:在该模块为定义的符号。通常value为0.对c++来说,不足够。

    unsigned char spare;

    unsigned short debug_info;

    unsigned int value;

}SYMBOL;

SYMBOL symbol[100];

通过3.4节和3.5节的对比,我们可以知道符号表立只给出了这个符号的值,但是这个符号在哪里,还是要靠重定位表来搞定的。(解释了3.4节的疑问。)

作为一种特例,编译器可以使用一个未定义的符号来要求链接器为该符号的名字预留
一块存储空间。如果一个外部符号的值不为零,则该值是提示链接器程序希望该符号寻址存
储空间的大小。在链接时,若该符号的定义不存在,则链接器根据其名字在 BSS 中创建一块
存储空间,大小为所有被链接模块中该符号提示尺寸中的最大值。如果该符号在某个模块中
被定义了,则链接器使用该定义而忽略提示的空间大小。这种“公共块技巧(common block
hack)”支持 Fortran 公共块和未初始化的 C 外部数据的典型用法(尽管是非标准的)。

四、UNIX ELF格式。

ELF 格式有三个略有不同的类型:可重定位的,可执行的,和共享目标(shared objects)。

可重定位文件由编译器和汇编器创建,但在运行前需要被链接器处理。

可执行文件完成了所有的重定位工作和符号解析(除了那些可能需要在运行时被解析的共享库符号)。

共享目标就是共享库,即包括链接器所需的符号信息,也包括运行时可以直接执行的代码。


 

4.1 ELF 的头部。

#define EI_NIDENT   16



typedef struct elf32_hdr{

  unsigned char e_ident[EI_NIDENT];

  Elf32_Half    e_type;

  Elf32_Half    e_machine;

  Elf32_Word    e_version;

  Elf32_Addr    e_entry;         /* Entry point 入口地址(若为可执行文件)*/
 Elf32_Off e_phoff;             //program header table 程序头部在文件中的位置(不存在则为 0) Elf32_Off e_shoff;             //section header table 区段头部在文件中的位置(不存在则为 0) Elf32_Word e_flags;         //体系结构相关的标志,总是 0 Elf32_Half e_ehsize;        //该 ELF 头部的大小 Elf32_Half e_phentsize;     //程序头部表项的大小 Elf32_Half e_phnum;         //程序头部表项个数(不存在则为 0) Elf32_Half e_shentsize;     //区段头部表项的大小 Elf32_Half e_shnum;         //区段头部表项的个数(不存在则为 0) Elf32_Half e_shstrndx;      //保存有区段名称字串的区段的序号 } Elf32_Ehdr;
  • 其中前16个字节是下面这个样子的。
char magic[4] = "\177ELF";//幻数

char class;               //地址宽度, 1 = 32 位, 2 = 64 位

char byteorder;           //字节序, 1 = little-endian,2 = big-endian

char hversion;            //头部版本,总是 1

char pad[9];              //填充字节
  • 然后e_type域。
/* These constants define the different elf file types */

#define ET_NONE   0      //No file type

#define ET_REL    1      //Relocatable file

#define ET_EXEC   2      //Executable file

#define ET_DYN    3      //Shared object file

#define ET_CORE   4      //Core file

#define ET_LOPROC 0xff00   //Processor-specific

#define ET_HIPROC 0xffff   //Processor-specific
  • e_machine
Name Value Meaning
EM_NONE 0 No machine
EM_M32 1 AT&T WE 32100
EM_SPARC 2 SPARC
EM_386 3 Intel 80386
EM_68K 4 Motorola 68000
EM_88K 5 Motorola 88000
EM_860 7 Intel 80860
EM_MIPS 8 MIPS RS3000

 

  •  e_version 
#define EV_NONE     0       /* e_version, EI_VERSION */

#define EV_CURRENT  1

#define EV_NUM      2

4.1.1 区段头部(Section)


http://blog.csdn.net/tenfyguo/article/details/5631561

sections包含了在一个object文件中的所有信息,除了ELF报头,程序报头表(program header table),和section报头表(section header table)。
此外,object文件的sections满足几个条件:

  • 每个在object文件中的section都有自己的一个section的报头来描述它。section头可能存在但section可以不存在。
  • 每个section在文件中都占有一个连续顺序的空间(但可能是空的)。
  • 文件中的Sections不可能重复。文件中没有一个字节既在这个section中又在另外的一个section中。
  • object文件可以有"非活动的"空间。不同的报头和sections可以不覆盖到object文件中的每个字节。"非活动"数据内容是未指定的。(空洞)
#define SHN_UNDEF       0        //该值表明没有定义,缺少,不相关的或者其他涉及到的无意义的section。

#define SHN_LORESERVE   0xff00   //该值指定保留的索引范围的最小值。

#define SHN_LOPROC      0xff00   //

#define SHN_HIPROC      0xff1f   //该值包含了特定处理器语意的保留范围。

#define SHN_ABS         0xfff1   //该变量是相对于相应参考的绝对地址。

#define SHN_COMMON      0xfff2   //该section的标号是一个公共(common)的标号,就象FORTRAN COMMON或者不允许的C扩展变量。

#define SHN_HIRESERVE   0xffff   //该值指定保留的索引范围的上限。系统保留的索引值是从SHN_LORESERVE到SHN_HIRESERVE;
                                 //该变量不涉及到section报头表(section header table)。因此,section报头表不为保留的索引值包含入口。

一个section头有如下的结构。

typedef struct elf32_shdr {

  Elf32_Word    sh_name;        //名称,可在字串表中索引到

  Elf32_Word    sh_type;        //区段类型

  Elf32_Word    sh_flags;       //标志位,见下

  Elf32_Addr    sh_addr;        //若可加载则为内存基址,否则为 0

  Elf32_Off     sh_offset;      //区段起始点在文件中的位置

  Elf32_Word    sh_size;        //区段大小(字节为单位)

  Elf32_Word    sh_link;        //相关信息对应的区段号,若没有则为 0,该成员保存了一个section报头表的索引连接,它的解释依靠该section的类型。下一个表描述了这些值。

  Elf32_Word    sh_info;        //区段相关的更多信息,该成员保存着额外的信息,它的解释依靠该section的类型。

  Elf32_Word    sh_addralign;   //移动区段时的对齐粒度

  Elf32_Word    sh_entsize;     //若该区段为一个表时其中表项的大小

} Elf32_Shdr;
  • sh_type解释
/* sh_type */

#define SHT_NULL        0    //  该值表明该section头是无效的;它没有相关的section。该section的其他成员的值都是未定义的。

#define SHT_PROGBITS    1    //  该section保存被程序定义了的一些信息,它的格式和意义取决于程序本身。程序或数据

#define SHT_SYMTAB  2        //  字符表:普通的链接器
#define SHT_STRTAB 3       //  该section保存着一个字符串表。一个object文件可以包含多个字符串表的 #define SHT_RELA 4     //  该section保存着具有明确加数的重定位入口。就象object文件32位的Elf32_Rela类型。一个object文件可能有多个重定位的sections。重定位需要的基地址也保存在重定位项自身中
#define SHT_HASH 5       // 该标号保存着一个标号的哈希(hash)表。所有的参与动态连接的object一定包含了一个标号哈希表(hash table)。当前的,一个object文件可能只有一个哈希表。详细细节看第二部分的哈希表"Hash Table"。 #define SHT_DYNAMIC 6       //  该section保存着动态连接的信息。当前的,一个object可能只有一个动态的section,但是,将来这个限制可能被取消。详细细节看第二部分的动态section(“Dynamic Section”)。 #define SHT_NOTE 7       //  该section保存着其他的一些标志文件的信息。详细细节看第二部分的“NoteSection” 。 #define SHT_NOBITS 8       //  该类型的section在文件中不占空间,但是类SHT_PROGBITS。尽管该section不包含字节,sh_offset成员包含了概念上的文件偏移量。bss段 #define SHT_REL 9       // 该section保存着具有明确加数的重定位的入口。一个object文件可能有多个重定位的sections。重定位值加到存储在代码和数据中的基地址值 #define SHT_SHLIB 10      //  该section类型保留但语意没有指明。包含这个类型的section的程序是不符合ABI的。 #define SHT_DYNSYM 11      //  一个object文件可能也包含了一个SHT_DYNSYM的section,它保存着一个动态连接时所需最小的标号集合来节省空间。动态链接器 #define SHT_NUM 12      // #define SHT_LOPROC 0x70000000  // #define SHT_HIPROC 0x7fffffff // 在这范围之间的值为特定处理器语意保留的。 #define SHT_LOUSER 0x80000000 // #define SHT_HIUSER 0xffffffff // 该变量为应用程序保留的索引范围的最大边界。在SHT_LOUSER和HIUSER的section类型可能被应用程序使用,这和当前或者将来系统定义的section类型是不矛盾的。
  • sh_flags

一个section报头(section header table)的sh_flags成员保存着1位标记,用来描述section的属性。以下是定义的值;其他的值保留。

/* sh_flags */

#define SHF_WRITE   0x1  //该section包含了在进程执行过程中可被写的数据。

#define SHF_ALLOC   0x2  // 该section在进程执行过程中占据着内存。

#define SHF_EXECINSTR   0x4  //该section包含了可执行的机器指令。

#define SHF_MASKPROC    0xf0000000  //所有的包括在这掩码中的位为特定处理语意保留的。

在section报头中,两个成员sh_link和sh_info的解释依靠该section的类型。

不同的sections保存着程序和控制信息。下面列表中的section被系统使用,指示了类型和属性。

  Name         Type           Attributes

  ====         ====           ==========

  .bss         SHT_NOBITS     SHF_ALLOC+SHF_WRITE           

  .comment     SHT_PROGBITS   none                       //该section保存着版本控制信息。

  .data        SHT_PROGBITS   SHF_ALLOC+SHF_WRITE

  .data1       SHT_PROGBITS   SHF_ALLOC+SHF_WRITE        //.data .data1这些sections保存着初始化了的数据,那些数据存在于程序内存映象中。

  .debug       SHT_PROGBITS   none                       //该section保存着为标号调试的信息。该内容是未指明的。

  .dynamic     SHT_DYNAMIC    see below                  //该section保存着动态连接的信息。该section的属性将包括SHF_ALLOC位。是否需要SHF_WRITE是跟处理器有关。第二部分有更详细的信息。

  .dynstr      SHT_STRTAB     SHF_ALLOC                  //该section保存着动态连接时需要的字符串,一般情况下,名字字符串关联着符号表的入口。第二部分有更详细的信息。

  .dynsym      SHT_DYNSYM     SHF_ALLOC                  //该section保存着动态符号表,如“Symbol Table”的描述。第二部分有更详细的信息。

  .fini        SHT_PROGBITS   SHF_ALLOC+SHF_EXECINSTR    //该section保存着可执行指令,它构成了进程的终止代码。因此,当一个程序正常退出时,系统安排执行这个section的中的代码。

  .got         SHT_PROGBITS   see below                  //该section保存着全局的偏移量表。看第一部分的“Special Sections”和第二部分的“Global Offset Table”获得更多的信息。

  .hash        SHT_HASH       SHF_ALLOC                  //该section保存着一个标号的哈希表。看第二部分的“Hash Table”获得更多的信息。

  .init        SHT_PROGBITS   SHF_ALLOC+SHF_EXECINSTR    //该section保存着可执行指令,它构成了进程的初始化代码。因此,当一个程序开始运行时,在main函数被调用之前(c语言称为main),系统安排执行这个section的中的代码。

  .interp      SHT_PROGBITS   see below                  //该section保存了程序的解释程序(interpreter)的路径。假如在这个section中有一个可装载的段,那么该section的属性的SHF_ALLOC位将被设置;否则,该位不会被设置。看第二部分获得更多的信息。

  .line        SHT_PROGBITS   none                       //该section包含编辑字符的行数信息,它描述源程序与机器代码之间的对于关系。该section内容不明确的。

  .note        SHT_NOTE       none                       //该section保存一些信息,使用“Note Section”(在第二部分)中提到的格式。

  .plt         SHT_PROGBITS   see below                  //该section保存着过程连接表(Procedure Linkage Table)。看第一部分的"Special Sections''和第二部分的“Procedure Linkage Table”。

  .rel<name>   SHT_REL        see below                  //这些section保存着重定位的信息

  .rela<name>  SHT_RELA       see below                  //

  .rodata      SHT_PROGBITS   SHF_ALLOC                  //

  .rodata1     SHT_PROGBITS   SHF_ALLOC                  //这些section保存着只读数据,在进程映象中构造不可写的段。

  .shstrtab    SHT_STRTAB     none                       //该section保存着section名称。

  .strtab      SHT_STRTAB     see below                  //该section保存着字符串,一般地,描述名字的字符串和一个标号的入口相关联。

  .symtab      SHT_SYMTAB     see below                  //该section保存着一个符号表

  .text        SHT_PROGBITS   SHF_ALLOC+SHF_EXECINSTR    //该section保存着程序的"text"或者说是可执行指令。

4.1.2 字符串表

String table sections 保存着以NULL终止的一系列字符,一般我们称为字符串。object文件使用这些字符串来描绘符号和section名。一个字符串的参考是一个string table section的索引。第一个字节,即索引0,被定义保存着一个NULL字符。同样的,一个string table的最后一个字节保存着一个NULL字符,所有的字符串都是以NULL终止。索引0的字符串是没有名字或者说是NULL,它的解释依靠上下文。一个空的string table section是允许的;它的section header的成员sh_size将为0。对空的string table来说,非0的索引是没有用的。一个 settion 头的 sh_name 成员保存了一个对应于该 setion 头字符表部分的索引(就象ELF头的 e_shstrndx 成员所特指的那样。下表列出了一个有 25 字节的字符串表(这些字符串和不同的索引相关联):

   Index   +0   +1   +2   +3   +4   +5   +6   +7   +8   +9

   =====   ==   ==   ==   ==   ==   ==   ==   ==   ==   ==

      0    /0   n    a    m    e    .    /0   V    a    r    

     10    i    a    b    l    e    /0   a    b    l    e

     20    /0   /0   x    x    /0

可以得到字符串如下。

  Index   String

  =====   ======

      0   none

      1   "name."

      7   "Variable"

     11   "able"

     16   "able"

     24   null string

如上所示,一个字符串表可能涉及该 section 中的任意字节。一个字符串可能 引用不止一次;引用子串的情况是可能存在的;一个字符串也可能被引用若干次;而不被引用的字符串也是允许存在的。

4.1.3 Symbol Table 符号表

 typedef struct {

      Elf32_Word st_name;     //该成员保存了进入该object文件的符号字符串表入口的索引。

      Elf32_Addr st_value;    //符号值,在可重定位文件中是段相对地址,在可执行文件中是绝对地址

      Elf32_Word st_size;     //许多符号和大小相关。比如,一个数据对象的大小是该对象包含的字节数目。如果该符号的大小未知或没有大小则这个成员为 0 。

      unsigned char st_info;  //成员指出了符号的类型和相应的属性。

      unsigned char st_other; //该成员目前为 0 ,没有含义。

      Elf32_Half st_shndx;    // 每一个符号表的入口都定义为和某些 section 相关;该成员保存了相关的 section头索引

  } Elf32_Sym;

 4.2 ELF可执行文件

一个ELF 可执行文件具有与可重定位ELF 文件相同的通用格式,但对数据部分进行了调整以使得文件可以被映射到内存中并运行。文件中会在 ELF 头部后面存在程序头部。程序头部定义了要被映射的段。

 程序头表:program header table

  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;  //内存中的段大小(如果包含 BSS 的话会更大些)

      Elf32_Word p_flags;  //读,写,执行标志位

      Elf32_Word p_align;  //对齐要求,根据硬件页尺大小不同有变动

  } Elf32_Phdr;

一个可执行程序通常只有少数几种段,如代码和数据的只读段,可读写数据的可读写
段。所有的可加载区段都归并到适当类型的段中以便系统可以通过少数的一两个操作就可以
完成文件映射。

ELF 共享目标包含了可重定位和可执行文件的所有东西。它在文件的开头具有程序头部
表,随后是可加载段的各区段,包括动态链接信息。在构成可加载段的各区段之后的,是重
定位符号表和链接器在根据共享目标创建可执行程序时需要的其它信息,最后是区段表。

至此,两种视角都讲完了,假如有时间的话,回头学习一下吧。

 

【链接与加载学习】第三章 目标文件

大部分参考http://blog.csdn.net/tenfyguo/article/details/5631561一文,但是没看完。

你可能感兴趣的:(文件)