elf文件格式-- 2
elf文件格式-- 2 =================== String Table 字符串表========================= 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 n a m e . V a r 10 i a b l e a b l e 20 x x + Figure 1-15: String Table Indexes Index String ===== ====== 0 none 1 "name." 7 "Variable" 11 "able" 16 "able" 24 null string 如上所示,一个字符串表可能涉及该 section 中的任意字节。一个字符串可能 引用不止一次;引用子串的情况是可能存在的;一个字符串也可能被引用若干次;而 不被引用的字符串也是允许存在的。 ==================== Symbol Table 符号表========================= 一个object文件的符号表保存了一个程序在定位和重定位时需要的定义和引用的信息。 一个符号表索引是相应的下标。0表项特指了该表的第一个入口,就象未定义的符号 索引一样。初始入口的内容在该 section 的后续部分被指定。 Name Value ==== ===== STN_UNDEF 0 一个符号表入口有如下的格式: + Figure 1-16: Symbol Table Entry typedef struct { Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Half st_shndx; } Elf32_Sym; * st_name 该成员保存了进入该object文件的符号字符串表入口的索引(保留了符号名的表达字符)。 如果该值不为 0 ,则它代表了给出符号名的字符串表索引。否则,该符号无名。 注意:External C 符号和object文件的符号表有相同的名称。 * st_value 该成员给出了相应的符号值。它可能是绝对值或地址等等(依赖于上下文); 细节如下所述。 * st_size 许多符号和大小相关。比如,一个数据对象的大小是该对象所包含的字节数目。 如果该符号的大小未知或没有大小则这个成员为 0 。 * st_info 成员指出了符号的类型和相应的属性。相应的列表如下所示。下面的代码说明了 如何操作该值。 #define ELF32_ST_BIND(i) ((i)>>4) #define ELF32_ST_TYPE(i) ((i)&0xf) #define ELF32_ST_INFO(b, t) (((b)<<4)+((t)&0xf)) * st_other 该成员目前为 0 ,没有含义。 * st_shndx 每一个符号表的入口都定义为和某些 section 相关;该成员保存了相关的 section 头索引。就象 Figure 1-8 和相关的文字所描述的那样,某些 section 索引 指出了特殊的含义。 一个符号的属性决定了可链接性能和行为。 + Figure 1-17: Symbol Binding, ELF32_ST_BIND Name Value ==== ===== STB_LOCAL 0 STB_GLOBAL 1 STB_WEAK 2 STB_LOPROC 13 STB_HIPROC 15 * STB_LOCAL 在包含了其定义的object文件之外的局部符号是不可见的。不同文件中的具有相同 名称的局部符号并不相互妨碍。 * STB_GLOBAL 全局符号是对所有的object目标文件可见的。一个文件中的全局符号的定义可以 满足另一个文件中对(该文件中)未定义的全局符号的引用。 * STB_WEAK 弱符号相似于全局符号,但是他们定义的优先级比较低一些。 * STB_LOPROC through STB_HIPROC 其所包含范围中的值由相应的处理器语义所保留。 全局符号和弱符号的区别主要在两个方面。 * 当链接器链接几个可重定位的目标文件时,它不允许 STB_GLOBAL 符号的同名 多重定义。另一方面,如果一个全局符号的定义存在则具有相同名称的弱符号名不会 引起错误。链接器将认可全局符号的定义而忽略弱符号的定义。与此相似,如果有一个 普通符号(比如,一个符号的 st_shndx 域包含 SHN_COMMON),则一个同名的弱符号 不会引起错误。链接器同样认可普通符号的定义而忽略弱符号。 * 当链接器搜索档案库的时候,它选出包含了未定义的全局符号的存档成员。该成员 的定义或者是全局的或者是一个弱符号。链接器不会为了解决一个未定义的弱符号 选出存档成员。未定义的弱符号具有 0 值。 在每一个符号表中,所有具有 STB_LOCAL 约束的符号优先于弱符号和全局符号。 就象上面 "sections" 中描述的那样,一个符号表部分的 sh_info 头中的成员 保留了第一个非局部符号的符号表索引。 符号的类型提供了一个为相关入口的普遍分类。 + Figure 1-18: Symbol Types, ELF32_ST_TYPE Name Value ==== ===== STT_NOTYPE 0 STT_OBJECT 1 STT_FUNC 2 STT_SECTION 3 STT_FILE 4 STT_LOPROC 13 STT_HIPROC 15 * STT_NOTYPE 该符号的类型没有指定。 * STT_OBJECT 该符号和一个数据对象相关,比如一个变量、一个数组等。 * STT_FUNC 该符号和一个函数或其他可执行代码相关。 * STT_SECTION 该符号和一个 section 相关。这种类型的符号表入口主要是为了重定位,一般的 具有 STB_LOCAL 约束。 * STT_FILE 按惯例而言,该符号给出了和目标文件相关的源文件名称。一个具有 STB_LOCAL 约束的文件符号,其 section 索引为 SHN_ABS ,并且它优先于当前对应该文件的 其他 STB_LOCAL 符号。 * STT_LOPROC through STT_HIPROC 该范围中的值是为处理器语义保留的。 共享文件中的函数符号(具有 STT_FUNC 类型)有特殊的意义。当其他的目标文件 从一个共享文件中引用一个函数时,链接器自动的为引用符号创建一个链接表。除了 STT_FUNC 之外,共享的目标符号将不会自动的通过链接表引用。 如果一个符号涉及到一个 section 的特定定位,则其 section 索引成员 st_shndx 将保留一个到该 section 头的索引。当该 section 在重定位过程中不断 移动一样,符号的值也相应变化,而该符号的引用在程序中指向同样的定位。某些 特殊的 section 索引有其他的语义。 * SHN_ABS 该符号有一个不会随重定位变化的绝对值。 * SHN_COMMON 该符号标识了一个没有被分配的普通块。该符号的值给出了相应的系统参数,就象 一个 section 的 sh_addralign 成员。也就是说,链接器将分配一个地址给 该符号,地址的值是 st_value 的倍数。该符号的大小指出了需要的字节数。 * SHN_UNDEF 该 section 表索引表明该符号是未定义的。当链接器将该目标文件和另一个定义 该符号的文件相装配的时候,该文件内对该符号的引用将链接到当前实际的定义。 如上所述,符号表的 0 索引(STN_UNDEF)是保留的,它包含了如下内容: + Figure 1-19: Symbol Table Entry: Index 0 Name Value Note ==== ===== ==== st_name 0 No name st_value 0 Zero value st_size 0 No size st_info 0 No type, local binding st_other 0 st_shndx SHN_UNDEF No section Symbol Values(符号值) 符号表入口对于不同的目标文件而言对 st_value 成员有一些不同的解释。 * 在可重定位文件中, st_value 保存了 section 索引为 SHN_COMMON 符号 的强制对齐值。 * 在可重定位文件中, st_value 保存了一个符号的 section 偏移。也就是说, st_value 是从 st_shndx 定义的 section 开头的偏移量。 * 在可执行的和可共享的目标文件中, st_value 保存了一个虚拟地址。为了使 这些文件符号对于动态链接器更为有效,文件层面上的 section 偏移让位于内存 层面上的虚拟地址( section 编号无关的)。 尽管符号表值对于不同的目标文件有相似的含义,相应的程序还是可以有效地访问数据。 ====================== Relocation (重定位)========================== 重定位是连接符号引用和符号定义的过程。比如,当一个程序调用一个函数的时候, 相关的调用必须在执行时把控制传送到正确的目标地址。换句话说,重定位文件应当 包含有如何修改他们的 section 内容的信息,从而允许可执行文件或共享目标文件 为一个进程的程序映像保存正确的信息。重定位入口就是这样的数据。 + Figure 1-20: Relocation Entries typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel; typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; Elf32_Sword r_addend; } Elf32_Rela; * r_offset 该成员给出了应用重定位行为的地址。对于一个重定位文件而言,该值是从该 section 开始处到受到重定位影响的存储单位的字节偏移量。对一个可执行文件 或一个共享目标而言,该值是受到重定位影响的存储单位的虚拟地址。 * r_info 该成员给出了具有受重定位影响因素的符号表索引和重定位应用的类型。比如, 一个调用指令的重定位入口应当包含被调用函数的符号索引。如果该索引是 STN_UNDEF (未定义的符号索引),重定位将使用 0 作为该符号的值。重定位 类型是和处理器相关的。当正文(text)引用到一个重定位入口的重定位类型或符 号表索引,它表明相应的应用 ELF32_R_TYPE或 ELF32_R_SYM 于入口的 r_info 成员。 #define ELF32_R_SYM(i) ((i)>>8) #define ELF32_R_TYPE(i) ((unsigned char)(i)) #define ELF32_R_INFO(s, t) ((s)<<8+(unsigned char)(t)) * r_addend 该成员指定一个常量加数(用于计算将要存储于重定位域中的值)。 如上所述,只有 Elf32_Rela 入口包含一个明确的加数。Elf32_Rel 类型 的入口在可以修改的地址中存储一个隐含的加数。依赖于处理器结构,一种形式 或其他形式也许是必须的或更为方便的。因此,特定机器的应用应当使用一种排他 性的形式或依赖于上下文的另一种形式。 一个重定位 section 关联了两个其他的 section :一个符号表和一个可修改 的 section 。该 section 头的成员 sh_info 和 sh_link (在上文中的 “ section ”部分中有描述)指示了这种关系。重定位入口中的成员 r_offset 对于不同的目标文件有少许差异。 * 在可重定位文件中,r_offset 表示了一个 section 偏移。也就是说,重定位 section自己描述了如何修改其他在文件中的其他section; 重定位偏移量指 明了一个在第二个section中的存储器单元。 * 在可执行和共享的目标文件中,r_offset 表示一个虚拟地址。为了使得这些 文件的重定位入口更为有用(对于动态链接器而言),该 section 偏移(文件 中)应当让位于一个虚拟地址(内存中的)。 尽管为了允许相关的程序更为有效的访问而让 r_offset 的解释对于不同的目标 文件有所不同,重定位类型的含义是相同的。 Relocation Types(重定位类型) 重定位入口描述了怎样变更下面的指令和数据域(位数在表的两边角下)。 + Figure 1-21: Relocatable Fields +---------------------------+ | word32 | 31---------------------------0 * word32 指定一个以任意字节对齐方式占用 4 字节的 32 位域。这些值使用与 32 位 Intel 体系相同的字节顺序。 3------2------1------0------+ 0x01020304 | 01 | 02 | 03 | 04 | 31------+------+------+------0 下面的计算假设正在将一个可重定位文件转换为一个可执行或共享的目标文件。 从概念上来说,链接器合并一个或多个可重定位文件来组成输出。它首先决定 怎样合并、定位输入文件,然后更新符号值,最后进行重定位。对于可执行文件 和共享的目标文件而言,重定位过程是相似的并有相同的结果。下面的描述使用 如下的约定符号。 * A 表示用于计算可重定位的域值的加数。 * B 表示了在执行过程中一个共享目标被加载到内存时的基地址。一般情况下,一个 共享object文件使用的基虚地址为0,但是一个可执行地址就跟共享object文件 不同了。 * G 表示了在执行过程中重定位入口符号驻留在全局偏移表中的偏移。请参阅 第二部分中的“ Global Offset Table (全局偏移表)”获得更多 的信息。 * GOT 表示了全局偏移表的地址。请参阅第二部分中的“ Global Offset Table (全局偏移表)”获得更多的信息。 * L 表示一个符号的过程链接表入口的位置( section 偏移或地址)。一个过程 链接表入口重定位一个函数调用到正确的目的单元。链接器创建初始的链接表, 而动态链接器在执行中修改入口。 请参阅第二部分中的“ Procedure Linkage Table (过程链接表)”获得更多 的信息 * P 表示(section 偏移或地址)被重定位的存储单元位置(使用 r_offset 计算的)。 * S 表示索引驻留在重定位入口处的符号值。 一个重定位入口的 r_offset 值指定了受影响的存储单元的首字节的偏移 或虚拟地址。重定位类型指定了哪一位(bit)将要改变,以及怎样计算它们的值。 在 SYSTEM V 体系中仅仅使用 Elf32_Rel 重定位入口,将要被重定位的域中 保留了加数。在所有的情况下,加数和计算结果使用相同字节顺序。 + Figure 1-22(表 1-22): Relocation Types(重定位类型) Name Value Field Calculation ==== ===== ===== =========== R_386_NONE 0 none none R_386_32 1 word32 S + A R_386_PC32 2 word32 S + A - P R_386_GOT32 3 word32 G + A - P R_386_PLT32 4 word32 L + A - P R_386_COPY 5 none none R_386_GLOB_DAT 6 word32 S R_386_JMP_SLOT 7 word32 S R_386_RELATIVE 8 word32 B + A R_386_GOTOFF 9 word32 S + A - GOT R_386_GOTPC 10 word32 GOT + A - P 有的重定位类型有不同于简单计算的语义。 * R_386_GOT32 这种重定位类型计算全局偏移表基地址到符号的全局偏移表 入口之间的间隔。这样另外通知了 link editor 建立一个全局偏移表 。 * R_386_PLT32 这种重定位类型计算符号的过程链接表入口地址,并另外通知链接器建立一个 过程链接表。 * R_386_COPY 链接器创建该重定位类型用于动态链接。它的偏移成员涉及一个可写段中的一个 位置。符号表索引指定一个可能存在于当前 object file 或在一个shared object 中的符号。在执行过程中,动态链接器把和 shared object 符号相关的数据 拷贝到该偏移所指定的位置。 * R_386_GLOB_DAT 这种重定位类型用于设置一个全局偏移表入口为指定符号的地址。该特定的重定位 类型允许你决定符号和全局偏移表入口之间的一致性。 * R_386_JMP_SLOT 链接器创建该重定位类型用于动态链接。其偏移成员给出了一个过程链接表入口的 位置。动态链接器修改该过程链接表入口以便向特定的符号地址传递控制。 [参阅第二部分中的 "Procedure Linkage Table(过程链接表)"] * R_386_RELATIVE 链接器创建该重定位类型用于动态链接。其偏移成员给出了包含表达相关地址值 的一个 shared object 中的位置。动态链接器计算相应的虚拟地址(把该 shared object 装载地址和相对地址相加)。该类型的重定位入口必须为 符号表索引指定为 0 。 * R_386_GOTOFF 这种重定位类型计算符号值和全局偏移表地址之间的不同。另外还通知链接器 建立全局偏移表(GOT)。 * R_386_GOTPC 这种重定位类型类似于 R_386_PC32 ,不同的是它在计算中使用全局偏移表。 这种重定位中引用的符号通常是 _GLOBAL_OFFSET_TABLE_ ,该符号通知了 链接器建立全局偏移表(GOT)。 ________________________________________________________________ 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 pad namesz 7 descsz 8 type 3 name X Y Z spc C o pad desc word0 word1 注意:系统保留的注解信息没有名字 (namesz==0) ,有一个零长度的名字 (name[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 |
elf文件格式-- 3
elf文件格式-- 3 ==================== 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"获取更多的信息。 Dynamic Section(动态section) 假如一个object文件参与动态的连接,它的程序头表将有一个类型为PT_DYNAMIC 的元素。该“段”包含了.dynamic section。一个_DYNAMIC特别的符号,表明了 该section包含了以下结构的一个数组。 + Figure 2-9: Dynamic Structure typedef struct { Elf32_Sword d_tag; union { Elf32_Sword d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn; extern Elf32_Dyn _DYNAMIC[]; 对每一个有该类型的object,d_tag控制着d_un的解释。 * d_val 那些Elf32_Word object描绘了具有不同解释的整形变量。 * d_ptr 那些Elf32_Word object描绘了程序的虚拟地址。就象以前提到的,在执行时, 文件的虚拟地址可能和内存虚拟地址不匹配。当解释包含在动态结构中的地址 时是基于原始文件的值和内存的基地址。为了一致性,文件不包含在 重定位入口来纠正在动态结构中的地址。 以下的表格总结了对可执行和共享object文件需要的tag。假如tag被标为 mandatory,ABI-conforming文件的动态连接数组必须有一个那样的入口。 同样的,“optional”意味着一个可能出现tag的入口,但是不是必须的。 + Figure 2-10: Dynamic Array Tags, d_tag Name Value d_un Executable Shared Object ==== ===== ==== ========== ============= DT_NULL 0 ignored mandatory mandatory DT_NEEDED 1 d_val optional optional DT_PLTRELSZ 2 d_val optional optional DT_PLTGOT 3 d_ptr optional optional DT_HASH 4 d_ptr mandatory mandatory DT_STRTAB 5 d_ptr mandatory mandatory DT_SYMTAB 6 d_ptr mandatory mandatory DT_RELA 7 d_ptr mandatory optional DT_RELASZ 8 d_val mandatory optional DT_RELAENT 9 d_val mandatory optional DT_STRSZ 10 d_val mandatory mandatory DT_SYMENT 11 d_val mandatory mandatory DT_INIT 12 d_ptr optional optional DT_FINI 13 d_ptr optional optional DT_SONAME 14 d_val ignored optional DT_RPATH 15 d_val optional ignored DT_SYMBOLIC 16 ignored ignored optional DT_REL 17 d_ptr mandatory optional DT_RELSZ 18 d_val mandatory optional DT_RELENT 19 d_val mandatory optional DT_PLTREL 20 d_val optional optional DT_DEBUG 21 d_ptr optional ignored DT_TEXTREL 22 ignored optional optional DT_JMPREL 23 d_ptr optional optional DT_LOPROC 0x70000000 unspecified unspecified unspecified DT_HIPROC 0x7fffffff unspecified unspecified unspecified * DT_NULL 一个DT_NULL标记的入口表示了_DYNAMIC数组的结束。 * DT_NEEDED 这个元素保存着以NULL结尾的字符串表的偏移量,那些字符串是所需库的名字。 该偏移量是以DT_STRTAB 为入口的表的索引。看“Shared Object Dependencies” 关于那些名字的更多信息。动态数组可能包含了多个这个类型的入口。那些 入口的相关顺序是重要的,虽然它们跟其他入口的关系是不重要的。 * DT_PLTRELSZ 该元素保存着跟PLT关联的重定位入口的总共字节大小。假如一个入口类型 DT_JMPREL存在,那么DT_PLTRELSZ也必须存在。 * DT_PLTGOT 该元素保存着跟PLT关联的地址和(或者)是GOT。具体细节看处理器补充 (processor supplement)部分。 * DT_HASH 该元素保存着符号哈希表的地址,在“哈希表”有描述。该哈希表指向 被DT_SYMTAB元素引用的符号表。 * DT_STRTAB 该元素保存着字符串表地址,在第一部分有描述,包括了符号名,库名, 和一些其他的在该表中的字符串。 * DT_SYMTAB 该元素保存着符号表的地址,在第一部分有描述,对32-bit类型的文件来 说,关联着一个Elf32_Sym入口。 * DT_RELA 该元素保存着重定位表的地址,在第一部分有描述。在表中的入口有明确的 加数,就象32-bit类型文件的Elf32_Rela。一个object文件可能好多个重定位 section。当为一个可执行和共享文件建立重定位表的时候,连接编辑器连接 那些section到一个单一的表。尽管在object文件中那些section是保持独立的。 动态连接器只看成是一个简单的表。当动态连接器为一个可执行文件创建一个 进程映象或者是加一个共享object到进程映象中,它读重定位表和执行相关的 动作。假如该元素存在,动态结构必须也要有DT_RELASZ和DT_RELAENT元素。 当文件的重定位是mandatory,DT_RELA 或者 DT_REL可能出现(同时出现是 允许的,但是不必要的)。 * DT_RELASZ 该元素保存着DT_RELA重定位表总的字节大小。 * DT_RELAENT 该元素保存着DT_RELA重定位入口的字节大小。 * DT_STRSZ 该元素保存着字符串表的字节大小。 * DT_SYMENT 该元素保存着符号表入口的字节大小。 * DT_INIT 该元素保存着初始化函数的地址,在下面“初始化和终止函数”中讨论。 * DT_FINI 该元素保存着终止函数的地址,在下面“初始化和终止函数”中讨论。 * DT_SONAME 该元素保存着以NULL结尾的字符串的字符串表偏移量,那些名字是共享 object的名字。偏移量是在DT_STRTAB入口记录的表的索引。关于那些名字看 Shared Object Dependencies 部分获得更多的信息。 * DT_RPATH 该元素保存着以NULL结尾的搜索库的搜索目录字符串的字符串表偏移量。 在共享object依赖关系(Shared Object Dependencies)中有讨论 * DT_SYMBOLIC 在共享object库中出现的该元素为在库中的引用改变动态连接器符号解析的算法。 替代在可执行文件中的符号搜索,动态连接器从它自己的共享object开始。假如 一个共享的object提供引用参考失败,那么动态连接器再照常的搜索可执行文件 和其他的共享object。 * DT_REL 该元素相似于DT_RELA,除了它的表有潜在的加数,正如32-bit文件类型的 Elf32_Rel一样。假如这个元素存在,它的动态结构必须也同时要有DT_RELSZ 和DT_RELENT的元素。 * DT_RELSZ 该元素保存着DT_REL重定位表的总字节大小。 * DT_RELENT 该元素保存着DT_RELENT重定为入口的字节大小。 * DT_PLTREL 该成员指明了PLT指向的重定位入口的类型。适当地, d_val成员保存着 DT_REL或DT_RELA。在一个PLT中的所有重定位必须使用相同的转换。 * DT_DEBUG 该成员被调试使用。它的内容没有被ABI指定;访问该入口的程序不是 ABI-conforming的。 * DT_TEXTREL 如在程序头表中段许可所指出的那样,这个成员的缺乏代表没有重置入 口会引起非写段的修改。假如该成员存在,一个或多个重定位入口可能 请求修改一个非写段,并且动态连接器能因此有准备。 * DT_JMPREL 假如存在,它的入口d_ptr成员保存着重定位入口(该入口单独关联着 PLT)的地址。假如lazy方式打开,那么分离它们的重定位入口让动态连接 器在进程初始化时忽略它们。假如该入口存在,相关联的类型入口DT_PLTRELSZ 和DT_PLTREL一定要存在。 * DT_LOPROC through DT_HIPROC 在该范围内的变量为特殊的处理器语义保留。除了在数组末尾的DT_NULL元素, 和DT_NEEDED元素相关的次序,入口可能出现在任何次序中。在表中不出 现的Tag值是保留的。 Shared Object Dependencies(共享Object的依赖关系) 当连接器处理一个文档库时,它取出库中成员并且把它们拷贝到一个输出的 object文件中。当运行时没有包括一个动态连接器的时候,那些静态的连接服 务是可用的。共享object也提供服务,动态连接器必须把正确的共享object 文件连接到要实行的进程映象中。因此,可执行文件和共享的object文件之间 存在着明确的依赖性。 当动态连接器为一个object文件创建内存段时,依赖关系(在动态结构的 DT_NEEDED入口中记录)表明需要哪些object来为程序提供服务。通过 重复的连接参考的共享object和他们的依赖关系,动态连接器可以建造一个 完全的进程映象。当解决一个符号引用的时候,动态连接器以宽度优先搜索 (breadth-first)来检查符号表,换句话说,它先查看自己的可实行程序 中的符号表,然后是顶端DT_NEEDED入口(按顺序)的符号表,再接下来是 第二级的DT_NEEDED入口,依次类推。共享object文件必须对进程是可读的; 其他权限是不需要的。 注意:即使当一个共享object被引用多次(在依赖列关系表中),动态连接器 只把它连接到进程中一次。 在依赖关系列表中的名字既被DT_SONAME字符串拷贝,又被建立object文件 时的路径名拷贝。例如,动态连接器建立一个可执行文件(使用带DT_SONAME 入口的lib1共享文件)和一个路径名为/usr/lib/lib2的共享object库, 那么可执行文件将在它自己的依赖关系列表中包含lib1和/usr/bin/lib2。 假如一个共享object名字有一个或更多的反斜杠字符(/)在这名字的如何地方, 例如上面的/usr/lib/lib2文件或目录,动态连接器把那个字符串自己做为路径名。 假如名字没有反斜杠字符(/),例如上面的lib1,三种方法指定共享文件的 搜索路径,如下: * 第一,动态数组标记DT_RPATH保存着目录列表的字符串(用冒号(:)分隔)。 例如,字符串/home/dir/lib:/home/dir2/lib:告诉动态连接器先搜索 /home/dir/lib,再搜索/home/dir2/lib,再是当前目录。 * 第二,在进程环境中(see exec(BA_OS)),有一个变量称为LD_LIBRARY_PATH 可以保存象上面一样的目录列表(随意跟一个分号(;)和其他目录列表)。 以下变量等于前面的例子: LD_LIBRARY_PATH=/home/dir/lib:/home/dir2/lib: LD_LIBRARY_PATH=/home/dir/lib;/home/dir2/lib: LD_LIBRARY_PATH=/home/dir/lib:/home/dir2/lib:; 所以的LD_LIBRARY_PATH目录在DT_RPATH指向的目录之后被搜索。尽管一些 程序(例如连接编辑器)不同的处理分号前和分号后的目录,但是动态连接 不会。不过,动态连接器接受分号符号,具体语意在如上面描述。 * 最后,如果上面的两个目录查找想要得到的库失败,那么动态连接器搜索 /usr/lib. 注意:出于安全考虑,动态连接器忽略set-user和set-group的程序的 LD_LIBRARY_PATH所指定的搜索目录。但它会搜索DT_RPATH指明的目录和 /usr/lib。 Global Offset Table(GOT全局偏移量表) 一般情况下,位置无关的代码不包含绝对的虚拟地址。全局偏移量表在私有数据 中保存着绝对地址,所以应该使地址可用的,而不是和位置无关性和程序代码段 共享能力妥协。一个程序引用它的GOT(全局偏移量表)来使用位置无关的地址并且 提取绝对的变量,所以重定位位置无关的参考到绝对的位置。 初始时,GOT(全局偏移量表)保存着它重定位入口所需要的信息 [看第一部分的 “Relocation”]。在系统为一个可装载的object文件创建内存段以后,动态 连接器处理重定位入口,那些类型为R_386_GLOB_DAT的指明了GOT(全局偏移量表)。 动态连接器决定了相关的标号变量,计算他们的绝对地址,并且设置适当的内存 表入口到正确的变量。虽然当连接编辑器建造object文件的时候,绝对地址 是不知道,连接器知道所以内存段的地址并且能够因此计算出包含在那里的 标号地址。 假如程序需要直接访问符号的绝对地址,那么这个符号将有一个GOT(全局偏移量表) 入口。因为可执行文件和共享文件有独立的GOT(全局偏移量表),一个符号地址 可能出现在不同的几个表中。在交给进程映象的代码控制权以前,动态连接器处 理所有的重定位的GOT(全局偏移量表),所以在执行时,确认绝对地址是可用的。 该表的入口0是为保存动态结构地址保留的(参考_DYNAMIC标号)。这允许 象动态连接程序那样来找出他们自己的动态结构(还没有处理他们的重 定向入口)。这些对于动态连接器是重要的,因为它必要初始化自己而不 能依赖于其他程序来重定位他们的内存映象。在32位Interl系统结构中,在 GOT中的人口1和2也是保留的,具体看以下的过程连接表(Procedure Linkage Table)。 系统可以为在不同的程序中相同的共享object选择不同的内存段;它甚至可以 为相同的程序不同的进程选择不同的库地址。虽然如此,一旦进程映象被建立 以后,内存段不改变地址。只要一个进程存在,它的内存段驻留在固定的虚拟 地址。 GOT表的格式和解释是处理器相关的。在32位Intel体系结构下,标号 _GLOBAL_OFFSET_TABLE_可能被用来访问该表。 + Figure 2-11: Global Offset Table extern Elf32_Addr _GLOBAL_OFFSET_TABLE_[]; 标号_GLOBAL_OFFSET_TABLE_可能驻留在.got section的中间,允许负的和非负 的下标索引这个数组。 Procedure Linkage Table(PLT过程连接表) 就象GOT重定位把位置无关的地址计算成绝对地址一样,PLT过程连接表重定位 位置无关的函数调用到绝对的地址。从一个可执行或者共享的object文件到另外的, 连接编辑器不解析执行的传输(例如函数的调用)。因此,连接编辑器安排程序 的传递控制到PLT中的入口。在SYSTEM V体系下,PLT存在共享文本中,但是它们 使用的地址是在私有的GOT中。符号连接器决定了目标的绝对地址并且修改GOT的 内存映象。因此,在没有危及到位置无关、程序文本的共享能力的情况下。动态 连接器能重定位人口。 + Figure 2-12: Absolute Procedure Linkage Table 绝对的过程连接表 .PLT0:pushl got_plus_4 jmp *got_plus_8 nop; nop nop; nop .PLT1:jmp *name1_in_GOT pushl $offset jmp .PLT0@PC .PLT2:jmp *name2_in_GOT pushl $offset jmp .PLT0@PC ... + Figure 2-13: Position-Independent Procedure Linkage Table 位置无关(或者说位置独立)的过程连接表 .PLT0:pushl 4(%ebx) jmp *8(%ebx) nop; nop nop; nop .PLT1:jmp *name1@GOT(%ebx) pushl $offset jmp .PLT0@PC .PLT2:jmp *name2@GOT(%ebx) pushl $offset jmp .PLT0@PC ... 注意:如图所示,PLT的指令使用了不同的操作数地址方式,对绝对代码和 对位置无关的代码。但是,他们的界面对于动态连接器是相同的。 以下的步骤,动态连接器和程序协作(cooperate)通过PLT和GOT来解析符号 引用。 1. 当第一次创建程序的内存映象时,动态连接器为在GOT中特别的变量设置 第二次和第三次的入口。下面关于那些变量有更多的解释。 2. 假如PLT是位置无关的,那么GOT的地址一定是保留在%ebx中的。每个在进程 映象**享的object文件有它自己的PLT,并且仅仅在同一个object文件中, 控制传输到PLT入口。从而,要调用的函数有责任在调用PLT入口前,设置PLT 地址到寄存器中。 3. 举例说明,假如程序调用函数name1,它的传输控制到标号.PLT1. 4. 第一个指令跳到在GOT入口的name1地址。初始话时,GOT保存着紧跟着的push1 指令的地址,而不是真实的name1的地址。 5. 因此,程序在堆栈中压入(push)一个重定位的偏移量。重定位的偏移量是 一个32位,非负的字节偏移量(从定位表算起)。指派的重定位入口将是 一个R_386_JMP_SLOT类型,它的偏移量指明了GOT入口(在前面的jmp指令中 被使用)。该重定位入口也包含一个符号表的索引,因此告诉动态连接器 哪个符号要被引用,在这里是name1。 6. 在压入(push)一个重定位的偏移量后,程序跳到.PLT0,在PLT中的第一个入口。 push1指令在堆栈中放置第二个GOT入口(got_plus_4 or 4(%ebx))的值, 因此,给动态连接器一个word的鉴别信息。然后程序跳到第三个GOT入口 (got_plus_8 or 8(%ebx)),它传输控制到动态连接器。 7. 当动态连接器接到控制权,它展开堆栈,查看指派的重定位入口,寻找符号的 值,在GOT入口中存储真实的name1地址,然后传输控制想要目的地。 8. PLT入口的并发执行将直接传输控制到name1,而不用第二次调用动态连接器 了。所以,在.PLT1中的jmp指令将转到name1,代替“falling through” 转到pushl指令。 LD_BIND_NOW环境变量能改变动态连接器的行为。假如这个变量为非空,动态 连接器在传输控制到程序前计算PLT入口。换句话说,动态连接器处理重定位 类型为R_386_JMP_SLOT的入口在进程初始化时。否则,动态连接器计算PLT入口 懒惰的,推迟到符号解析和重定位直到一个表入口的第一次执行。 注意:一般来说,以懒惰(Lazy)方式绑定是对全应用程序执行的改进。 因为不使用的符号就不会招致动态连接器做无用功。然而,对一些应用程序, 两种情况使用懒惰(Lazy)方式是不受欢迎的。 第一 初始的引用一个共享object函数比后来的调用要花的时间长,因为动 态连接器截取调用来解析符号。一些应用程序是不能容忍这样的。 第二 假如这个错误发生并且动态连接器不能解析该符号,动态连接器将终止 程序。在懒惰(Lazy)方式下,这可能发生在任意的时候。一再的,一 些应用程序是不能容忍这样的。通过关掉懒惰(Lazy)方式,在应用程 序接到控制前,当在处理初始话时发生错误,动态连接器强迫程序,使 之失败。 Hash Table(哈希表) Elf32_Word object的哈希表支持符号表的访问。 标号出现在下面帮助解释哈希表的组织,但是它们不是规范的一部分。 + Figure 2-14: Symbol Hash Table nbucket nchain bucket[0] ... bucket[nbucket - 1] chain[0] ... chain[nchain - 1] bucket数组包含了nbucket入口,并且chain数组包含了nchain个入口;索引从0开始。 bucket和chain保存着符号表的索引。Chain表入口类似于符号表。符号表入口的 数目应该等于nchain;所以符号表的索引也选择chain表的入口。 一个哈希函数(如下的)接受一个符号名并且返回一个可以被计算机使用的bucket索引 的值。因此,假如一个哈希函数返回一些名字的值为X,那么bucket[x%nbucket] 将给出一个索引y(既是符号表和chain表的索引)。假如符号表入口不是期望的, chain[y]给出下一个符号表的入口(使用相同的哈希变量)。可以沿着chain 链直到选择到了期望名字的符号表入口或者是碰到了STN_UNDEF的入口。 + Figure 2-15: Hashing Function unsigned long elf_hash(const unsigned char *name) { unsigned long h = 0, g; while (*name) { h = (h << 4) + *name++; if (g = h & 0xf0000000) h ^= g >> 24; h &= ~g; } return h; } Initialization and Termination Functions 初始化和终止函数 在动态连接妻建立进程映象和执行重定位以后,每一个共享object得到适当 的机会来执行一些初始话代码。初始化函数不按特别的顺序被调用,但是 所有的共享object初始化发生在执行程序获得控制之前。 类似地,共享的object可能包含终止函数,它们在进程本身开始它的终止之后 被执行(以atexit(BA_OS)的机制)。 共享object通过设置在动态结构中的DT_INIT和DT_FINI入口来指派它们的初始化 和终止函数,如上动态section(Dynamic Section)部分描述。典型的,那些函数 代码存在.init和.fini section中,第一部分的“section”已经提到过。 注意:尽管atexit(BA_OS)的终止处理一般可可正常完成,但是不保证在死进程上 被执行。特别的,假如_exit被调用(看exit(BA_OS))或者假如进程死掉,那么 进程是不执行终止处理的。因为它收到一个信号,该信号可捕获或忽略。 ________________________________________________________________ 3. C LIBRARY ________________________________________________________________ ========================== C Library =========================== C库,libc,包含了所有的符号(包含在libsys),另外,包含在在下面两个 表中列出的运行函数。第一个表中的运行函数是ANSI C标准的。 + Figure 3-1: libc Contents, Names without Synonyms abort fputc isprint putc strncmp abs fputs ispunct putchar strncpy asctime fread isspace puts strpbrk atof freopen isupper qsort strrchr atoi frexp isxdigit raise strspn atol fscanf labs rand strstr bsearch fseek ldexp rewind strtod clearerr fsetpos ldiv scanf strtok clock ftell localtime setbuf strtol ctime fwrite longjmp setjmp strtoul difftime getc mblen setvbuf tmpfile div getchar mbstowcs sprintf tmpnam fclose getenv mbtowc srand tolower feof gets memchr sscanf toupper ferror gmtime memcmp strcat ungetc fflush isalnum memcpy strchr vfprintf fgetc isalpha memmove strcmp vprintf fgetpos iscntrl memset strcpy vsprintf fgets isdigit mktime strcspn wcstombs fopen isgraph perror strlen wctomb fprintf islower printf strncat 再加上, libc 保存着以下的服务。 + Figure 3-2: libc Contents, Names with Synonyms __assert getdate lockf ** sleep tell ** cfgetispeed getopt lsearch strdup tempnam cfgetospeed getpass memccpy swab tfind cfsetispeed getsubopt mkfifo tcdrain toascii cfsetospeed getw mktemp tcflow _tolower ctermid hcreate monitor tcflush tsearch cuserid hdestroy nftw tcgetattr _toupper dup2 hsearch nl_langinfo tcgetpgrp twalk fdopen isascii pclose tcgetsid tzset __filbuf isatty popen tcsendbreak _xftw fileno isnan putenv tcsetattr __flsbuf isnand ** putw tcsetpgrp fmtmsg ** lfind setlabel tdelete ** = Function is at Level 2 in the SVID Issue 3 and therefore at Level 2 in the ABI. 包括上面同义(Synonyms)表列出的标号,对于 入口已经存在的_ 形式(带一个下划线,上面没有列出来)优先权高于它们的名字。所以,例如, libc同时包含了getopt和_getopt。 在常规的上列中,其他地方以下没有被定义。 int __filbuf(FILE *f); This function returns the next input character for f, filling its buffer as appropriate. It returns EOF if an error occurs. int __flsbuf(int x, FILE *f); This function flushes the output characters for f as if putc(x, f) had been called and then appends the value of x to the resulting output stream. It returns EOF if an error occurs and x otherwise. int _xftw(int, char *, int (*)(char *, struct stat *, int), int); Calls to the ftw(BA_LIB) function are mapped to this function when applications are compiled. This function is identical to ftw(BA_LIB), except that _xftw() takes an interposed first argument, which must have the value 2. 要了解更多的关于SVID,ANSI C,POSIX的知识,可看该章节其他的库section部分。 该节“System Data Interfaces”后有更多的描述。 Global Data Symbols 全局数据符号 libc库需要一些外部的全局数据符号(为了它自己的常规工作而定义的)。 所有向libsys库请求的数据符号一定要让libc提供,就象下面表中的数据符号。 正式定义的数据object被他们的符号描述,看System V接口定义,第三版本或者第6章节的数据定义(Data Definitions)section(在适当的处理器补充到System V ABI)。 在下面表中的入口有-_的形式。一对符号都代表了一些数据。 下划线的synonyms假设满足ANSI C标准。 + Figure 3-3: libc Contents, Global External Data Symbols getdate_err optarg _getdate_err opterr __iob optind optopt |
Linux动态链接技术(转载)
Linux动态链接技术(转载) 在动态链接的应用程序或共享库中,ELF的程序头描述表具有一个PT_DYNAMIC类型的描述符,它指?br>隽?dynamic段的位 置,dynamic段用来描述动态链接过程。当应用程序调用的共享库函数时,要通过.plt段进行跳转。plt段又称为过程连接表,它是连接器ld所生成 的一组静态的trampline,是只读的可执行的段,包含在.text段一起映射到内存。plt每16个字节为一个槽位,plt的第1个槽位保留给动态 解析器使用,其余的槽位表示对不同共享库函数的调用。plt依赖于全局偏移量表(.got段),GOT表是一可写的数据段,包含在.data段中一起映射 到内存,用来存放共享符号的绝对地址。?br>τ贸绦虻饔霉蚕砜夂褪峭ü齪lt槽位上的一条jmp指令跳转到GOT表所指的一个共享函数指针。这 样,共享库的重定位就化为对GOT表项的重定位。GOT表的第1个指针指向.dynamic段,第2、3个?br>刚胗雙lt段的第1个槽位对应, 用来安装动态解析器。为了少做无用功,Linux采用了动态解析技术,就是说在加载共享库时,并不进行函数的解析,而是安装动态解析器,让共享库调用指向 解析器,只有当函数调用发生时才进行解析。为此,在ld在生成可执行程序时,让其GOT共享函数指针指?br>蚋髯詐lt槽位上的两条指令,一条是 pushl指令,将该函数所对应GOT重定位表的索引作为参数压入堆栈,然后通过另一条jmp指令跳转到plt槽位1,它再跳转到GOT表第3个指针所表 示的动态解析器?br>肟凇U庋狈⑸⒊晒馕瞿勘旰诠蚕砜庵械牡刂肥保煤诔绦騁OT表中的指针就被实际的地址刷新。 下面是基于动态解释器ld.so-1.9.9版本的简单分析 简单的测试文件testso.c: int x = 0; int test() { return x; } 用gcc -S -fPIC testso.c编绎成的汇编代码: .globl x .data .align 4 .type x,@object .size x,4 x: .long 0 .text .align 4 .globl test .type test,@function test: pushl %ebp movl %esp,%ebp pushl %ebx call .L2 .L2: popl %ebx # 取标号.L2所在的地址 addl $_GLOBAL_OFFSET_TABLE_+[.-.L2],%ebx #_GLOBAL_OFFSET_TABLE_为当前地址到GOT表的偏移 movl x@GOT(%ebx),%eax # ebx在-fPIC编绎的函数中用于指向本模块的GOT表 movl (%eax),%eax # x@GOT表示符号x在GOT表中的索引 movl -4(%ebp),%ebx leave ret .Lfe1: .size test,.Lfe1-test 用gcc -shared testso.s -o testso.so生成共享库的反汇编的有关输出: Disassembly of section .plt: 00000258 <.plt>: 258: ff b3 04 00 00 pushl 0x4(%ebx) #GOT表的第2个指针,对-fPIC编绎的函数,ebx总是指向GOT表 25d: 00 25e: ff a3 08 00 00 jmp *0x8(%ebx) #跳转到GOT表的第3个指针,调用__dl_linux_resolover 263: 00 264: 00 00 addb %al,(%eax) 266: 00 00 addb %al,(%eax) 268: ff a3 0c 00 00 jmp *0xc(%ebx) # 跳转到共享函数test()所在的GOT的指针 26d: 00 26e: 68 00 00 00 00 pushl x0 # test()所在GOT指针的初始入口 273: e9 e0 ff ff ff jmp 258 <_init+0x8> # 跳转到plt槽位1 Disassembly of section .text: 000002d8 : 2d8: 55 pushl %ebp 2d9: 89 e5 movl %esp,%ebp 2db: 53 pushl %ebx 2dc: e8 00 00 00 00 call 2e1 2e1: 5b popl %ebx 2e2: 81 c3 ab 10 00 addl x10ab,%ebx # 取GOT表指针 2e7: 00 2e8: 8b 83 10 00 00 movl 0x10(%ebx),%eax # 从GOT表中取变量x的地址 2ed: 00 2ee: 8b 00 movl (%eax),%eax 2f0: 8b 5d fc movl 0xfffffffc(%ebp),%ebx 2f3: c9 leave 2f4: c3 ret 引用testso的应用程序文件test.c: main() { printf("%d/n",test()); } 用gcc test.c testso.so -o test生成可执行文件的反汇编输出: Disassembly of section .plt: 08048398 <.plt>: 8048398: ff 35 54 95 04 pushl 0x8049554 # GOT表的第2个指针 804839d: 08 804839e: ff 25 58 95 04 jmp *0x8049558 #GOT表的第3个指针,运行_dl_linux_resolver 80483a3: 08 80483a4: 00 00 addb %al,(%eax) 80483a6: 00 00 addb %al,(%eax) 80483a8: ff 25 5c 95 04 jmp *0x804955c # printf()在plt的入口 80483ad: 08 80483ae: 68 00 00 00 00 pushl x0 80483b3: e9 e0 ff ff ff jmp 8048398 <_init+0x8> 80483b8: ff 25 60 95 04 jmp *0x8049560 80483bd: 08 80483be: 68 08 00 00 00 pushl x8 80483c3: e9 d0 ff ff ff jmp 8048398 <_init+0x8> 80483c8: ff 25 64 95 04 jmp *0x8049564 # test()在plt段的调用点 80483cd: 08 # [0x8048564]初始时指向0x80483ce 80483ce: 68 10 00 00 00 pushl x10 # test()在GOT表重定位表.rel.got中的索引 80483d3: e9 c0 ff ff ff jmp 8048398 <_init+0x8> # 跳转到plt的第1槽位 80483d8: ff 25 68 95 04 jmp *0x8049568 80483dd: 08 80483de: 68 18 00 00 00 pushl x18 80483e3: e9 b0 ff ff ff jmp 8048398 <_init+0x8> 80483e8: ff 25 6c 95 04 jmp *0x804956c 80483ed: 08 80483ee: 68 20 00 00 00 pushl x20 80483f3: e9 a0 ff ff ff jmp 8048398 <_init+0x8> 80483f8: ff 25 70 95 04 jmp *0x8049570 80483fd: 08 80483fe: 68 28 00 00 00 pushl x28 8048403: e9 90 ff ff ff jmp 8048398 <_init+0x8> Disassembly of section .text: 080484c8 : 80484c8: 55 pushl %ebp 80484c9: 89 e5 movl %esp,%ebp 80484cb: e8 f8 fe ff ff call 80483c8 <_init+0x38> # 80484d0: 89 c0 movl %eax,%eax 80484d2: 50 pushl %eax 80484d3: 68 38 85 04 08 pushl x8048538 80484d8: e8 cb fe ff ff call 80483a8 <_init+0x18> 80484dd: 83 c4 08 addl x8,%esp 80484e0: c9 leave 80484e1: c3 ret ld.so-1.9.9/d-link/i386/resolve.S #define ALIGN 4 #define RUN linux_run #define RESOLVE _dl_linux_resolve #define RESOLVER _dl_linux_resolver #define EXIT _interpreter_exit #define INIT __loader_bootstrap .text .align ALIGN .align 16 .globl RESOLVE .type RESOLVE,@function RESOLVE: pusha lea 0x20(%esp),%eax /* eax = tpnt and reloc_entry params */ pushl 4(%eax) /* push copy of reloc_entry param */ pushl (%eax) /* push copy of tpnt param */ pushl %eax /* _dl_linux_resolver expects a dummy * param - this could be removed */ #ifdef __PIC__ call .L24 .L24: popl %ebx addl $_GLOBAL_OFFSET_TABLE_+[.-.L24],%ebx movl RESOLVER@GOT(%ebx),%ebx /* eax = resolved func */ call *%ebx #else call RESOLVER #endif movl %eax,0x2C(%esp) /* store func addr over original * tpnt param */ addl xC,%esp /* remove copy parameters */ popa /* restore regs */ ret /* jump to func removing original * reloc_entry param from stack */ .LFE2: .size RESOLVE,.LFE2-RESOLVE d-link/i386/elfinterp.c: unsigned int _dl_linux_resolver(int dummy, int i) { unsigned int * sp; int reloc_entry; int reloc_type; struct elf32_rel * this_reloc; char * strtab; struct elf32_sym * symtab; struct elf32_rel * rel_addr; struct elf_resolve * tpnt; int symtab_index; char * new_addr; char ** got_addr; unsigned int instr_addr; sp = &i; reloc_entry = sp[1]; tpnt = (struct elf_resolve *) sp[0]; rel_addr = (struct elf32_rel *) (tpnt->dynamic_info[DT_JMPREL] + tpnt->loadaddr); 取可执行程序的GOT重定位表 this_reloc = rel_addr + (reloc_entry >> 3); reloc_type = ELF32_R_TYPE(this_reloc->r_info); symtab_index = ELF32_R_SYM(this_reloc->r_info); symtab = (struct elf32_sym *) (tpnt->dynamic_info[DT_SYMTAB] + tpnt->loadaddr); strtab = (char *) (tpnt->dynamic_info[DT_STRTAB] + tpnt->loadaddr); if (reloc_type != R_386_JMP_SLOT) { _dl_fdprintf(2, "%s: Incorrect relocation type in jump relocations/n", _dl_progname); _dl_exit(1); }; /* Address of jump instruction to fix up */ instr_addr = ((int)this_reloc->r_offset + (int)tpnt->loadaddr); got_addr = (char **) instr_addr; #ifdef DEBUG _dl_fdprintf(2, "Resolving symbol %s/n", strtab + symtab[symtab_index].st_name); #endif /* Get the address of the GOT entry */ new_addr = _dl_find_hash(strtab + symtab[symtab_index].st_name, tpnt->symbol_scope, (int) got_addr, tpnt, 0); if(!new_addr) { _dl_fdprintf(2, "%s: can't resolve symbol '%s'/n", _dl_progname, strtab + symtab[symtab_index].st_name); _dl_exit(1); }; /* #define DEBUG_LIBRARY */ #ifdef DEBUG_LIBRARY if((unsigned int) got_addr < 0x40000000) { _dl_fdprintf(2, "Calling library function: %s/n", strtab + symtab[symtab_index].st_name); } else { *got_addr = new_addr; } #else *got_addr = new_addr; 更新GOT函数指针 #endif return (unsigned int) new_addr; } |
Elf动态解析符号过程(转载)
本篇文章以linux为平台为例,演示ELF动态解析符号的过程 本篇文章以linux为平台为例,演示ELF动态解析符号的过程。 不正之处,还请斧正。 通常,ELF解析符号方式称为lazy MODE装载的。这种装载技术是ELF平台上 默认的方式。在不同的体系平台在实现这种机制也是不同的。但是i386和SPARC 在大部分上是相同的。 动态连接器(rtld)提供符号的动态连接,装载共享objects和解析标号的引用。 通常是ld.so,它可以是一个共享object也可以是个可执行的文件。 ★★ 符号表(symbol table) 每个object要想使它对其他的ELF文件可用,就要用到符号表(symbol table)中 symbol entry.事实上,一个symbol entry 是个symbol结构,它描述了这个 symbol的名字和该symbol的value.symbol name被编码作为dynamic string table的索引(index). The value of a symbol是在ELF OBJECT文件内该 symbol的地址。该地址通常需要被重新定位(加上该object装载到内存的基地址 (base load address)). 从而构成该symbol在内存中的绝对地址。 一个符号表入口有如下的格式: typedef struct { Elf32_Word st_name; /* Symbol name (string tbl index) */ Elf32_Addr st_value; /* Symbol value */ Elf32_Word st_size; /* Symbol size */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* No defined meaning, 0 */ Elf32_Section st_shndx; /* Section index */ } Elf32_Sym; 可执行文件他们知道运行时刻他们的地址,所以他们内部的引用符号在编译时候就已 经被重定位了。 ★★ GOT(global offset table) GOT是一个数组,存在ELF image的数据段中,他们是一些指向objects的指针(通常 是数据objects).动态连接器将重新修改那些编译时还没有确定下来地址的符号的 GOT入口。所以说GOT在i386动态连接中扮演着重要的角色。 ★★ PLT(procedure linkage table) PLT是一个这样的结构,它的entries包含了一些代码片段用来传输控制到外部的过程。 在i386体系下,PLT和他的代码片段entries有如下格式: PLT0: push GOT[1] ; word of identifying information jmp GOT[2] ; pointer to rtld function nop ... PLTn: jmp GOT[x + n] ; GOT offset of symbol address push n ; relocation offset of symbol jmp PLT0 ; call the rtld PLTn + 1 jmp GOT[x +n +1]; GOT offset of symbol address push n +1 ; relocation offset of symbol jmp PLT0 ; call the rtld 当传输控制到一个外部的函数时,它传输执行到PLT 中跟该symbol相关的那个entry (是在编译时候连接器安装的)。在PLT entry中第一条指令将jump到一个存储在GOT 中的一个指针地址;假如符号还没有被解析,该GOT中存放着的是该PLT entry中的 下一条指令地址。该指令push一个在重定位表中的偏移量到stack,然后下一条指令 传输控制到PLT[0]入口。该PLT[0]包含了调用RTLD解析符号的函数代码。该 解析符号函数地址由程序装载器已经插入到GOT[2]中了。 动态连接器将展开stack并且获取需要解析符号在重定位表地址信息。重定位入口、 符号表和字符串表共同决定着PLT entry引用的那个符号和在进程内存中符号应该 存放的地址。假如可能的话,该符号将被解析出来,它的地址将被存放在被该 PLT entry使用的GOT entry中。下一次该符号被请求时,与之对应的GOT已经包 含了该符号的地址了。所以,所有后来的调用将直接通过GOT传输控制。动态连接器 只解析第一次被二进制文件所引用的符号;这种引用方式就是我们上面所说的 lazy MODE。 ★★ 哈希表和链(hash table and chain) 除了符号表(symbol table),GOT(global offset table),PLT(procedure linkage table),字符串表(string table),ELF objects还可以包含一个 hash table和chain(用来使动态连接器解析符号更加容易)。hash table和chain 通常被用来迅速判定在符号表中哪个entry可能符合所请求的符号名。hash table(总 是伴随着chain的)被作为整型数组存放。在hash表中,一半位置是留给那些buckets的, 另一半是留给在chain中的元素(element)的. hash table直接反映了symbol table 的元素数目和他们的次序。 动态连接器结构提供了所有动态连接的执行是以透明方式访问动态连接器. 然而,明确访问也是可用的。动态连接(装载共享objects和解析符号), 可以通过直接访问RTLD的那些函数来完成:dlopen() , dlsym() and dlclose() .这些函数被包含在动态连接器本身中。为了访问那些函数, 连接时需要把动态连接函数库(libdl)连接进去。该库包含了一些stub函数 允许编译时候连接器解析那些函数的引用;然而那些stub函数只简单的返回0。 因为事实上函数驻留在动态连接器中,假如从静态连接的ELF文件中调用 那些函数,共享object的装载将会失败。 对于执行动态连接器所必须的是:hash table,hash table元素的数目, chain,dynamic string table和dynamic symbol talbe。满足了 这些条件,下面算法适用任何symbol的地址计算: 1. hn = elf_hash(sym_name) % nbuckets; 2. for (ndx = hash[ hn ]; ndx; ndx = chain[ ndx ]) { 3. symbol = sym_tab + ndx; 4. if (strcmp(sym_name, str_tab + symbol->st_name) == 0) 5. return (load_addr + symbol->st_value); } hash号是elf_hash()的返回值,在ELF规范的第4部分有定义,以hash table中元素 个数取模。该号被用来做hash table的下表索引,求得hash值,找出与之匹配的符号 名的chain的索引(line 3)。使用该索引,符号从符号表中获得(line 3).比较获得 的符号名和请求的符号名是否相同(line 5).使用这个算法,就可以简单解析任何符号了。 ★★ 演示 #include int main(int argc, char *argv[]) { printf("Hello, world/n"); return 0; } Relocation section '.rel.plt' at offset 0x278 contains 4 entries: Offset Info Type Symbol's Value Symbol's Name 0804947c 00107 R_386_JUMP_SLOT 080482d8 __register_frame_info 08049480 00207 R_386_JUMP_SLOT 080482e8 __deregister_frame_info 08049484 00307 R_386_JUMP_SLOT 080482f8 __libc_start_main 08049488 00407 R_386_JUMP_SLOT 08048308 printf 只有R_386_JUMP_SLOT的才会出现在GOT中 Symbol table '.dynsym' contains 7 entries: Num: Value Size Type Bind Ot Ndx Name 0: 0 0 NOTYPE LOCAL 0 UND 1: 80482d8 116 FUNC WEAK 0 UND __register_frame_info@GLIBC_2.0 (2) 2: 80482e8 162 FUNC WEAK 0 UND __deregister_frame_info@GLIBC_2.0 ( 2) 3: 80482f8 261 FUNC GLOBAL 0 UND __libc_start_main@GLIBC_2.0 (2) 4: 8048308 41 FUNC GLOBAL 0 UND printf@GLIBC_2.0 (2) 5: 804843c 4 OBJECT GLOBAL 0 14 _IO_stdin_used 6: 0 0 NOTYPE WEAK 0 UND __gmon_start__ [alert7@redhat]$ gcc -o test test.c [alert7@redhat]$ ./test Hello, world [alert7@redhat]$ gdb -q test (gdb) disass main Dump of assembler code for function main: 0x80483d0 : push %ebp 0x80483d1 : mov %esp,%ebp 0x80483d3 : push x8048440 0x80483d8 : call 0x8048308 0x80483dd : add x4,%esp 0x80483e0 : xor %eax,%eax 0x80483e2 : jmp 0x80483e4 0x80483e4 : leave 0x80483e5 : ret ... 0x80483ef : nop End of assembler dump. (gdb) b * 0x80483d8 Breakpoint 1 at 0x80483d8 (gdb) r Starting program: /home/alert7/test Breakpoint 1, 0x80483d8 in main () (gdb) disass 0x8048308 ① ⑴ Dump of assembler code for function printf: /****************************************/ //PLT4: 0x8048308 : jmp *0x8049488 //jmp GOT[6] //此时,GOT[6]中存在的是0x804830e 0x804830e : push x18 //x18为printf入口在GOT的偏移量 0x8048313 : jmp 0x80482c8 <_init+48> //jmp PLT0 //PLT0处存放着调用RTLD函数的指令 //当函数返回时候,把GOT[6]修改为真正的 //printf函数地址,然后直接跳到printf函数 //执行。 该部分为PLT的一部分 /****************************************/ End of assembler dump. (gdb) x 0x8049488 0x8049488 <_GLOBAL_OFFSET_TABLE_+24>: 0x0804830e 080482c8 <.plt>: ② //PLT0: 80482c8: ff 35 74 94 04 08 pushl 0x8049474 //pushl GOT[1]地址 //GOT[1]是一个鉴别信息,是link_map类型的一个指针 80482ce: ff 25 78 94 04 08 jmp *0x8049478 //JMP GOT[2] //跳到动态连接器解析函数执行 80482d4: 00 00 add %al,(%eax) 80482d6: 00 00 add %al,(%eax) 80482d8: ff 25 7c 94 04 08 jmp *0x804947c //PLT1: 80482de: 68 00 00 00 00 push x0 80482e3: e9 e0 ff ff ff jmp 80482c8 <_init+0x30> 80482e8: ff 25 80 94 04 08 jmp *0x8049480 //PLT2: 80482ee: 68 08 00 00 00 push x8 80482f3: e9 d0 ff ff ff jmp 80482c8 <_init+0x30> 80482f8: ff 25 84 94 04 08 jmp *0x8049484 //PLT3: 80482fe: 68 10 00 00 00 push x10 8048303: e9 c0 ff ff ff jmp 80482c8 <_init+0x30> 8048308: ff 25 88 94 04 08 jmp *0x8049488 //PLT4: 804830e: 68 18 00 00 00 push x18 8048313: e9 b0 ff ff ff jmp 80482c8 <_init+0x30> (gdb) b * 0x80482c8 Breakpoint 2 at 0x80482c8 (gdb) c Continuing. Breakpoint 2, 0x80482c8 in _init () (gdb) x/8x 0x8049470 0x8049470 <_GLOBAL_OFFSET_TABLE_>: 0x08049490 0x40013ed0 0x4000a960 0x400fa550 0x8049480 <_GLOBAL_OFFSET_TABLE_+16>: 0x080482ee 0x400328cc 0x0804830e 0x00000000 (gdb) x/50x 0x40013ed0 ( * link_map类型) 0x40013ed0: 0x00000000 0x40010c27 0x08049490 0x400143e0 0x40013ee0: 0x00000000 0x40014100 0x00000000 0x08049490 0x40013ef0: 0x080494e0 0x080494d8 0x080494a8 0x080494b0 0x40013f00: 0x080494b8 0x00000000 0x00000000 0x00000000 0x40013f10: 0x080494c0 0x080494c8 0x08049498 0x080494a0 0x40013f20: 0x00000000 0x00000000 0x00000000 0x080494f8 0x40013f30: 0x08049500 0x08049508 0x080494e8 0x080494d0 0x40013f40: 0x00000000 0x080494f0 0x00000000 0x00000000 0x40013f50: 0x00000000 0x00000000 0x00000000 0x00000000 0x40013f60: 0x00000000 0x00000000 0x00000000 0x00000000 (gdb) disass 0x4000a960 ③ Dump of assembler code for function _dl_runtime_resolve: 0x4000a960 <_dl_runtime_resolve>: push %eax 0x4000a961 <_dl_runtime_resolve+1>: push %ecx 0x4000a962 <_dl_runtime_resolve+2>: push %edx 0x4000a963 <_dl_runtime_resolve+3>: mov 0x10(%esp,1),%edx 0x4000a967 <_dl_runtime_resolve+7>: mov 0xc(%esp,1),%eax 0x4000a96b <_dl_runtime_resolve+11>: call 0x4000a740 //调用真正的解析函数fixup(),修正GOT[6],使它指向真正的printf函数地址 0x4000a970 <_dl_runtime_resolve+16>: pop %edx 0x4000a971 <_dl_runtime_resolve+17>: pop %ecx 0x4000a972 <_dl_runtime_resolve+18>: xchg %eax,(%esp,1) 0x4000a975 <_dl_runtime_resolve+21>: ret x8 //跳到printf函数地址执行 0x4000a978 <_dl_runtime_resolve+24>: nop 0x4000a979 <_dl_runtime_resolve+25>: lea 0x0(%esi,1),%esi End of assembler dump. (gdb) b * 0x4000a972 Breakpoint 4 at 0x4000a972: file dl-runtime.c, line 182. (gdb) c Continuing. Breakpoint 4, 0x4000a972 in _dl_runtime_resolve () at dl-runtime.c:182 182 in dl-runtime.c (gdb) i reg $eax $esp eax 0x4006804c 1074167884 esp 0xbffffb64 -1073743004 (gdb) b *0x4000a975 Breakpoint 5 at 0x4000a975: file dl-runtime.c, line 182. (gdb) c Continuing. Breakpoint 5, 0x4000a975 in _dl_runtime_resolve () at dl-runtime.c:182 182 in dl-runtime.c (gdb) si printf (format=0x1 ) at printf.c:26 26 printf.c: No such file or directory. (gdb) disass ④ ⑵ Dump of assembler code for function printf: 0x4006804c : push %ebp 0x4006804d : mov %esp,%ebp 0x4006804f : push %ebx 0x40068050 : call 0x40068055 0x40068055 : pop %ebx 0x40068056 : add xa2197,%ebx 0x4006805c : lea 0xc(%ebp),%eax 0x4006805f : push %eax 0x40068060 : pushl 0x8(%ebp) 0x40068063 : mov 0x81c(%ebx),%eax 0x40068069 : pushl (%eax) 0x4006806b : call 0x400325b4 0x40068070 : mov 0xfffffffc(%ebp),%ebx 0x40068073 : leave 0x40068074 : ret End of assembler dump. (gdb) x/8x 0x8049470 0x8049470 <_GLOBAL_OFFSET_TABLE_>: 0x08049490 0x40013ed0 0x4000a960 0x400fa550 0x8049480 <_GLOBAL_OFFSET_TABLE_+16>: 0x080482ee 0x400328cc 0x4006804c 0x00000000 GOT[6]已经被修正为0x4006804c了 第一次调用printf()的时候需要经过①->②->③->④ 以后调用printf()的时候就不需要这么复杂了,只要经过⑴->⑵就可以了 link_map结构说明如下: /* Structure describing a loaded shared object. The `l_next' and `l_prev' members form a chain of all the shared objects loaded at startup. These data structures exist in space used by the run-time dynamic linker; modifying them may have disastrous results. This data structure might change in future, if necessary. User-level programs must avoid defining objects of this type. */ ★★ glibc中动态解析符号的源代码(glibc 2.1.3的实现) .text .globl _dl_runtime_resolve .type _dl_runtime_resolve, @function .align 16 _dl_runtime_resolve: pushl %eax # Preserve registers otherwise clobbered. pushl %ecx pushl %edx movl 16(%esp), %edx # Copy args pushed by PLT in register. Note movl 12(%esp), %eax # that `fixup' takes its parameters in regs. call fixup # Call resolver. popl %edx # Get register content back. popl %ecx xchgl %eax, (%esp) # Get %eax contents end store function address. ret # Jump to function address. static ElfW(Addr) __attribute__ ((unused)) fixup ( # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS ELF_MACHINE_RUNTIME_FIXUP_ARGS, # endif struct link_map *l, ElfW(Word) reloc_offset) { const ElfW(Sym) *const symtab = (const void *) l->l_info[DT_SYMTAB]->d_un.d_ptr; const char *strtab = (const void *) l->l_info[DT_STRTAB]->d_un.d_ptr; const PLTREL *const reloc /*计算函数重定位人口*/ = (const void *) (l->l_info[DT_JMPREL]->d_un.d_ptr + reloc_offset); const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];/*计算函数的符号表入口*/ void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);/*GOT[n]的地址*/ ElfW(Addr) value; /* The use of `alloca' here looks ridiculous but it helps. The goal is to prevent the function from being inlined and thus optimized out. There is no official way to do this so we use this trick. gcc never inlines functions which use `alloca'. */ alloca (sizeof (int)); /* Sanity check that we're really looking at a PLT relocation. */ assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);/*健壮性检查*/ /* Look up the target symbol. */ switch (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) { default: { const ElfW(Half) *vernum = (const void *) l->l_info[VERSYMIDX (DT_VERSYM)]->d_un.d_ptr; ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)]; const struct r_found_version *version = &l->l_versions[ndx]; if (version->hash != 0) { value = _dl_lookup_versioned_symbol(strtab + sym->st_name, &sym, l->l_scope, l->l_name, version, ELF_MACHINE_JMP_SLOT); break; } } case 0: value = _dl_lookup_symbol (strtab + sym->st_name, &sym, l->l_scope, l->l_name, ELF_MACHINE_JMP_SLOT); } /*此时value为object装载的基地址*/ /* Currently value contains the base load address of the object that defines sym. Now add in the symbol offset. */ value = (sym ? value + sym->st_value : 0);/*在object中函数的绝对地址*/ /* And now perhaps the relocation addend. */ value = elf_machine_plt_value (l, reloc, value);/*可能还需要一下重定位*/ /* Finally, fix up the plt itself. */ elf_machine_fixup_plt (l, reloc, rel_addr, value);/*修正GOT[n]*/ return value; } static inline Elf32_Addr elf_machine_plt_value (struct link_map *map, const Elf32_Rela *reloc, Elf32_Addr value) { return value + reloc->r_addend; } /* Fixup a PLT entry to bounce directly to the function at VALUE. */ static inline void elf_machine_fixup_plt (struct link_map *map, const Elf32_Rel *reloc, Elf32_Addr *reloc_addr, Elf32_Addr value) { *reloc_addr = value; } |
Linux 动态函式库解析[转]
在本文的这个部分,针对 Linux 系统是如何来辨别这些不同的可执行档,以及整体的执行流程来作一个说明。 By Wing 程序启动的流程 在 linux 的环境中最常见的可执行档的种类包括了 Script 档、Aout 格式的执行档、ELF 格式的执行档。在本文的这个部分,我会针对 Linux 系统是如何来辨别这些不同的可执行档,以及整体的执行流程来作一个说明。 我在此大略说明一下程序启动的流程,当我们在 shell 中输入指令时,会先去系统的路径中来寻找是否有该可执行档存在,如果找不到的话,就会显示出找不到该可执行档的讯息。如果找到的话,就会去呼叫 execve()来执行该档案,接下来 execve() 会呼叫 System Call sys_execv(),这是在Linux 中 User Mode 透过 80 号中断(int 80 ah=11)进入 Kernel Mode 所执行的第一个指令,之後在 Kernel 中陆续执行 do_exec()、 prepare_binprm()、read_exec()、search_binary_handler(),而在 search_binary_handler() 函式中,会逐一的去检查目前所执行档案的型态(看看是否为Script File、aout 或 ELF 档),不过 Linux 所采用的方式是透过各个档案格式的处理程序来决定目前的执行档所属的处理程序。 如下图,会先去检验档案是否为 Script 档,若是直进入 Script 档的处理程序。若不是,则再进入 Aout 档案格式的处理程序,若该执行档为 Aout 的档案格式便交由 Aout档案格式的处理程序来执行。如果仍然不是的话,便再进入 ELF 档案格式的处理程序,如果都找不到的话,则传回错误讯息。 由这种执行的流程来看的话,如果 Linux Kernel 想要加入其他的执行档格式的话,就要在 search_binary_handler() 加入新的执行档的处理程序,这样一旦新的执行档格式产生後,在 Linux 下要执行时,因为在do_load_script、do_load_aout_binary、do_load_elf_binary都会传回错误,因此只 有我们自己的 do_load_xxxx_binary 函式可以正确的接手整个执行档的处理流程,因此便可以达成新的档案格式置入的动作哩。 在函式 do_load_elf_binary () 执行时,首先会去检视目前的档案是否为 ELF 格式,如下程序码 if (elf_ex.e_ident[0] != 0x7f' ' strncmp(&elf_ex.e_ident[1], "ELF", 3) != 0) goto out; 便是去检查该档的前四个 bytes 是否为 0x7f 加上 “ELF” (0x 45 0x4c 0x46),若非,则结束 do_load_elf_binary 的执行。之後,便是去检视我们之前提过的 e_type 属性,来得知是否为 ET_EXEC(Executable File) 或是ET_DYN(Shared Object File) 这两个值的其中之一 if (elf_ex.e_type != ET_EXEC && elf_ex.e_type != ET_DYN) goto out; 如果都不是这两个值之一,便结束 do_load_elf_binary 的执行之後便是一连串读取 ELF 档表格的动作,在此就不多说,有兴趣的读者可以自行参阅/usr/src/linux/fs/binfmt_elf.c 的内容即可。 在此我们检视一个执行档由启动到结束的完整流程,首先这个执行档具有如下的程序码 #include int main() { printf(" test "); } 然後,透过如下的编程过程 gcc test.c ˉo test 我们如果检视执行档的 ELF Header 可以得知它主要呼叫了 /lib/libc.so.6函式库中以下的函式 printf __deregister_frame_info __libc_start_main __register_frame_info 接下来,我们便把程序的执行流程大略整理如下,而 execve("./test", ["./test"], []) 执行的流程,就是刚刚我们所提到的内容,若不熟悉的读者,可以再回头看看刚刚的内容,即可对 execve("./test", ["./test"], []) 的执行流程有大略的了解。在这里,我们会把整个执行流程更完整的来检视一遍。 首先,我们所在的执行环境会透过 execve("./test", ["./test"], []) 的函式呼叫来启动 test 执行档。 呼叫 open("/etc/ld.so.cache", O_RDONLY),以唯读模式开启 ld.so.cache,这个档案的功能是作为动态函式库的快取,它会记录了目前系统中所存在的动态函式库的资讯以及这些函式库所存在的位置。所以说,如 果我们在系统中安装了新的函式库时,我们便需要去更新这个档案的内容,以使新的函式库可以在我们的 Linux 环境中发生作用,我们可以透过 ldconfig 这个指令来更新 ld.so.cache 的内容。 呼叫 mmap(0, 9937, PROT_READ, MAP_PRIVATE, 3, 0),把 ld.so.cache 档案映射到记忆体中,mmap 函式的宣告为 mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset),在笔者的电脑上 ld.so.cache 的档案大小为 9937 bytes,PROT_READ代表这块记忆体位置是可读取的,MAP_PRIVATE 则表示产生一个行程私有的 copy-on-write 映射,因此这个呼叫会把整个 ld.so.cache 档案映射到记忆体中,在笔者电脑上所传回的映射记忆体起始位置为 0x40013000。 注: mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)代表我们要求在档案 fd中,起始位置为offset去映射 length 长度的资料,到记忆体位置 start ,而 prot 是用来描述该记忆体位置的保护权限(例如:读、写、执行),flags用来定义所映射物件的型态,例如这块记忆体是否允许多个 Process 同时映射到,也就是说一旦有一个 Process 更改了这个记忆体空间,那所有映射到这块记忆体的Process 都会受到影响,或是 flag 设定为 Process 私有的记忆体映射,这样就会透过 copy-on-write 的机制,当这块记忆体被别的 Process 修改後,会自动配置实体的记忆体位置,让其他的 Process 所映射到的记忆体内容与原本的相同。(有关mmap的其它应用,可参考本文最後的注一) 呼叫 open("/lib/libc.so.6", O_RDONLY),开启 libc.so.6。 呼叫 read(3, "177ELF111331250202"..., 4096) 读取libc.so.6的档头。 呼叫 mmap(0, 993500, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0),把 libc.so.6 映射到记忆体中,由档头开始映射 993500 bytes,若是使用 RedHat 6.1(或其它版本的 RedHat)的读者或许会好奇 libc.so.6 所 link 到的档案 libc-2.1.2.so 大小不是 4118715 bytes 吗? 其实原本 RedHat 所附的 libc.so.6 动态函式库是没有经过 strip 过的,如果经过 strip 後,大小会变为 1052428 bytes,而 libc.so.6 由档头开始在 993500 bytes 之後都是一些版本的资讯,笔者猜想应该是这样的原因,所以在映射档时,并没有把整个 libc.so.6 档案映射到记忆体中,只映射前面有意义的部分。与映射 ld.so.cache 不同的是,除了 PROT_READ 属性之外,libc.so.6 的属性还多了PROT_EXEC,这代表了所映射的这块记忆体是可读可执行的。在笔者的电脑中,libc.so.6 所映射到的记忆体起始位置为 0x40016000。 呼叫 mprotect(0x40101000, 30940, PROT_NONE),用来设定记忆体的使用权限,而 PROT_NONE 属性是代表这块记忆体区间(0x40101000—0x401088DC)是不能读取、写入与执行的。 呼叫 mmap(0x40101000, 16384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0xea000),映射 libc.so.6 由起始位置 0xea000 映射 16384bytes 到记忆体位置 0x40101000。 呼叫 mmap(0x40105000, 14556, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0),MAP_ANONYMOUS 表示没有档案被映射,且产生一个初始值全为 0 的记忆体区块。 呼叫 munmap(0x40013000, 9937),把原本映射到 ld.so.cache 的记忆体解除映射(此时已把执行档所需的动态函式库都映射到记忆体中了)。 呼叫 personality(0),可以设定目前 Process 的执行区间(execution domain),换个说法就是 Linux 支援了多个执行区间,而我们所设定的执行区间会告诉 Linux 如何去映射我们的讯息号码(signal numbers)到各个不同的讯息动作(signal actions)中。这执行区间的功能,允许 Linux 对其它 Unix-Like 的操作系统,提供有限度的二进位档支援。如这个例子中,personality(0) 的参数为 0,就是指定为 PER_LINUX 的执行区间(execution domain)。 #define PER_MASK (0x00ff) #define PER_LINUX (0x0000) #define PER_LINUX_32BIT (0x0000 | ADDR_LIMIT_32BIT) #define PER_SVR4 (0x0001 | STICKY_TIMEOUTS) #define PER_SVR3 (0x0002 | STICKY_TIMEOUTS) #define PER_SCOSVR3 (0x0003 | STICKY_TIMEOUTS | WHOLE_SECONDS) #define PER_WYSEV386 (0x0004 | STICKY_TIMEOUTS) #define PER_ISCR4 (0x0005 | STICKY_TIMEOUTS) #define PER_BSD (0x0006) #define PER_XENIX (0x0007 | STICKY_TIMEOUTS) #define PER_LINUX32 (0x0008) #define PER_IRIX32 (0x0009 | STICKY_TIMEOUTS) /* IRIX5 32-bit */ #define PER_IRIXN32 (0x000a | STICKY_TIMEOUTS) /* IRIX6 new 32-bit */ #define PER_IRIX64 (0x000b | STICKY_TIMEOUTS) /* IRIX6 64-bit */ 呼叫 getpid(),取得目前 Process 的 Process ID。 呼叫 mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0),传回值为 0x400130,MAP_ANONYMOUS 表示没有档案被映射,且产生一个初始值全为 0 的记忆体区块。 呼叫 write(1, " test ", 6),显示字串在画面上。 呼叫 munmap(0x40013000, 4096),解除记忆体位置0x40013000的记忆体映射。 呼叫 _exit(6),结束程序执行。 在这段所举的例子,只用到了一个函式库 libc.so.6,我们可以举像是 RedHat 中 Telnet 指令为例,首先检视他的 ELF Header ==>libncurses.so.4 tgetent ==>libc.so.6 strcpy ioctl printf cfgetospeed recv connect ............┅ sigsetmask __register_frame_info close free 它主要呼叫了函式库 libncurses.so.4 的函式 tgetent,以及函式库 libc.so.6 中为数不少的函式,当然我们也可以去检视它执行的流程,与之前只呼叫了 libc.so.6 的printf 函式来比较,我们可以发现它主要的不同就是去载入了 libncurses.so.4 open("/usr/lib/libncurses.so.4", O_RDONLY) ; fstat(3, {st_mode=S_IFREG|0755, st_size=274985, ...}) ; read(3, "177ELF111331340335"..., 4096) ; mmap(0, 254540, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0); mprotect(0x40048000, 49740, PROT_NONE); mmap(0x40048000, 36864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x31000); mmap(0x40051000, 12876, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) ; close(3); |
Before main() 分析[转]
本文分析了在main()之前的ELF程序流程,试图让您更清楚的把握程序的流程的脉络走向。 从而更深入的了解ELF。不正确之处,还请斧正。 ★ 综述 ELF的可执行文件与共享库在结构上非常类似,它们具有一张程序段表,用来描述这些段如何映射到进程空间. 对于可执行文件来说,段的加载位置是固定的,程序段表中如实反映了段的加载地址.对于共享库来说,段的加 载位置是浮动的,位置无关的,程序段表反映的是以0作为基准地址的相对加载地址.尽管共享库的连接是不 充分的,为了便于测试动态链接器,Linux允许直接加载共享库运行.如果应用程序具有动态链接器的描述段, 内核在完成程序段加载后,紧接着加载动态链接器,并且启动动态链接器的入口.如果没有动态链接器的描述段, 就直接交给用户程序入口。 上述这部分请参考:linuxforum论坛上opera写的《分析ELF的加载过程》 在控制权交给动态链接器的入口后,首先调用_dl_start函数获得真实的程序入口(注:该入口地址 不是main的地址,也就是说一般程序的入口不是main),然后循环调用每个共享object的初始化函数, 接着跳转到真实的程序入口,一般为_start(程序中的_start)的一个例程,该例程压入一些参数到堆栈, 就直接调用__libc_start_main函数。在__libc_start_main函数中替动态连接器和自己程序安排 destructor,并运行程序的初始化函数。然后才把控制权交给main()函数。 ★ main()之前流程 下面就是动态链接器的入口。 /* Initial entry point code for the dynamic linker. The C function `_dl_start' is the real entry point; its return value is the user program's entry point. */ #define RTLD_START asm ("/ .text/n/ .globl _start/n/ .globl _dl_start_user/n/ _start:/n/ pushl %esp/n/ call _dl_start/n//*该函数返回时候,%eax中存放着user entry point address*/ popl %ebx/n//*%ebx放着是esp的内容*/ _dl_start_user:/n/ # Save the user entry point address in %edi./n/ movl %eax, %edi/n//*入口地址放在%edi*/ # Point %ebx at the GOT. call 0f/n/ 0: popl %ebx/n/ addl $_GLOBAL_OFFSET_TABLE_+[.-0b], %ebx/n/ # Store the highest stack address/n/ movl __libc_stack_end@GOT(%ebx), %eax/n/ movl %esp, (%eax)/n//*把栈顶%esp放到GOT的__libc_stack_end中*/ # See if we were run as a command with the executable file/n/ # name as an extra leading argument./n/ movl _dl_skip_args@GOT(%ebx), %eax/n/ movl (%eax), %eax/n/ # Pop the original argument count./n/ popl %ecx/n/ # Subtract _dl_skip_args from it./n/ subl %eax, %ecx/n/ # Adjust the stack pointer to skip _dl_skip_args words./n/ leal (%esp,%eax,4), %esp/n/ # Push back the modified argument count./n/ pushl %ecx/n/ # Push the searchlist of the main object as argument in/n/ # _dl_init_next call below./n/ movl _dl_main_searchlist@GOT(%ebx), %eax/n/ movl (%eax), %esi/n/ 0: movl %esi,%eax/n/ # Call _dl_init_next to return the address of an initializer/n/ # function to run./n/ call _dl_init_next@PLT/n//*该函数返回初始化函数的地址,返回地址放在%eax中*/ # Check for zero return, when out of initializers./n/ testl %eax, %eax/n/ jz 1f/n/ # Call the shared object initializer function./n/ # NOTE: We depend only on the registers (%ebx, %esi and %edi)/n/ # and the return address pushed by this call;/n/ # the initializer is called with the stack just/n/ # as it appears on entry, and it is free to move/n/ # the stack around, as long as it winds up jumping to/n/ # the return address on the top of the stack./n/ call *%eax/n//*调用共享object初始化函数*/ # Loop to call _dl_init_next for the next initializer./n/ jmp 0b/n/ 1: # Clear the startup flag./n/ movl _dl_starting_up@GOT(%ebx), %eax/n/ movl , (%eax)/n/ # Pass our finalizer function to the user in %edx, as per ELF ABI./n/ movl _dl_fini@GOT(%ebx), %edx/n/ # Jump to the user's entry point./n/ jmp *%edi/n/ .previous/n/ "); sysdeps/i386/start.s中 user's entry也就是下面的_start例程 /* This is the canonical entry point, usually the first thing in the text segment. The SVR4/i386 ABI (pages 3-31, 3-32) says that when the entry point runs, most registers' values are unspecified, except for: %edx Contains a function pointer to be registered with `atexit'. This is how the dynamic linker arranges to have DT_FINI functions called for shared libraries that have been loaded before this code runs. %esp The stack contains the arguments and environment: 0(%esp) argc 4(%esp) argv[0] ... (4*argc)(%esp) NULL (4*(argc+1))(%esp) envp[0] ... NULL */ .text .globl _start _start: /* Clear the frame pointer. The ABI suggests this be done, to mark the outermost frame obviously. */ xorl %ebp, %ebp /* Extract the arguments as encoded on the stack and set up the arguments for `main': argc, argv. envp will be determined later in __libc_start_main. */ popl %esi /* Pop the argument count. */ movl %esp, %ecx /* argv starts just at the current stack top.*/ /* Before pushing the arguments align the stack to a double word boundary to avoid penalties from misaligned accesses. Thanks to Edward Seidl andl xfffffff8, %esp pushl %eax /* Push garbage because we allocate 28 more bytes. */ /* Provide the highest stack address to the user code (for stacks which grow downwards). */ pushl %esp pushl %edx /* Push address of the shared library termination function. */ /* Push address of our own entry points to .fini and .init. */ pushl $_fini pushl $_init pushl %ecx /* Push second argument: argv. */ pushl %esi /* Push first argument: argc. */ pushl $main /* Call the user's main function, and exit with its value. But let the libc call main. */ call __libc_start_main hlt /* Crash if somehow `exit' does return. */ __libc_start_main在sysdeps/generic/libc_start.c中 假设定义的是PIC的代码。 struct startup_info { void *sda_base; int (*main) (int, char **, char **, void *); int (*init) (int, char **, char **, void *); void (*fini) (void); }; int __libc_start_main (int argc, char **argv, char **envp, void *auxvec, void (*rtld_fini) (void), struct startup_info *stinfo, char **stack_on_entry) { /* the PPC SVR4 ABI says that the top thing on the stack will be a NULL pointer, so if not we assume that we're being called as a statically-linked program by Linux... */ if (*stack_on_entry != NULL) { /* ...in which case, we have argc as the top thing on the stack, followed by argv (NULL-terminated), envp (likewise), and the auxilary vector. */ argc = *(int *) stack_on_entry; argv = stack_on_entry + 1; envp = argv + argc + 1; auxvec = envp; while (*(char **) auxvec != NULL) ++auxvec; ++auxvec; rtld_fini = NULL; } /* Store something that has some relationship to the end of the stack, for backtraces. This variable should be thread-specific. */ __libc_stack_end = stack_on_entry + 4; /* Set the global _environ variable correctly. */ __environ = envp; /* Register the destructor of the dynamic linker if there is any. */ if (rtld_fini != NULL) atexit (rtld_fini);/*替动态连接器安排destructor*/ /* Call the initializer of the libc. */ __libc_init_first (argc, argv, envp);/*一个空函数*/ /* Register the destructor of the program, if any. */ if (stinfo->fini) atexit (stinfo->fini);/*安排程序自己的destructor*/ /* Call the initializer of the program, if any. */ /*运行程序的初始化函数*/ if (stinfo->init) stinfo->init (argc, argv, __environ, auxvec); /*运行程序main函数,到此,控制权才交给我们一般所说的程序入口*/ exit (stinfo->main (argc, argv, __environ, auxvec)); } void __libc_init_first (int argc __attribute__ ((unused)), ...) { } int atexit (void (*func) (void)) { struct exit_function *new = __new_exitfn (); if (new == NULL) return -1; new->flavor = ef_at; new->func.at = func; return 0; } /* Run initializers for MAP and its dependencies, in inverse dependency order (that is, leaf nodes first). */ ElfW(Addr) internal_function _dl_init_next (struct r_scope_elem *searchlist) { unsigned int i; /* The search list for symbol lookup is a flat list in top-down dependency order, so processing that list from back to front gets us breadth-first leaf-to-root order. */ i = searchlist->r_nlist; while (i-- > 0) { struct link_map *l = searchlist->r_list[i]; if (l->l_init_called) /* This object is all done. */ continue; if (l->l_init_running) { /* This object's initializer was just running. Now mark it as having run, so this object will be skipped in the future. */ l->l_init_running = 0; l->l_init_called = 1; continue; } if (l->l_info[DT_INIT] && (l->l_name[0] != '' || l->l_type != lt_executable)) { /* Run this object's initializer. */ l->l_init_running = 1; /* Print a debug message if wanted. */ if (_dl_debug_impcalls) _dl_debug_message (1, "/ncalling init: ", l->l_name[0] ? l->l_name : _dl_argv[0], "/n/n", NULL); /*共享库的基地址+init在基地址中的偏移量*/ return l->l_addr + l->l_info[DT_INIT]->d_un.d_ptr; } /* No initializer for this object. Mark it so we will skip it in the future. */ l->l_init_called = 1; } /* Notify the debugger all new objects are now ready to go. */ _r_debug.r_state = RT_CONSISTENT; _dl_debug_state (); return 0; } 在main()之前的程序流程看试有点简单,但正在运行的时候还是比较复杂的 (自己用GBD跟踪下就知道了),因为一般的程序都需要涉及到PLT,GOT标号的 重定位。弄清楚这个对ELF由为重要,以后有机会再补上一篇吧。 ★ 手动确定程序和动态连接器的入口 [alert7@redhat62 alert7]$ cat helo.c #include int main(int argc,char **argv) { printf("hello/n"); return 0; } [alert7@redhat62 alert7]$ gcc -o helo helo.c [alert7@redhat62 alert7]$ readelf -h helo ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x8048320 Start of program headers: 52 (bytes into file) Start of section headers: 8848 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 6 Size of section headers: 40 (bytes) Number of section headers: 29 Section header string table index: 26 在这里我们看到程序的入口为0x8048320,可以看看是否为main函数。 [alert7@redhat62 alert7]$ gdb -q helo (gdb) disass 0x8048320 Dump of assembler code for function _start: 0x8048320 <_start>: xor %ebp,%ebp 0x8048322 <_start+2>: pop %esi 0x8048323 <_start+3>: mov %esp,%ecx 0x8048325 <_start+5>: and xfffffff8,%esp 0x8048328 <_start+8>: push %eax 0x8048329 <_start+9>: push %esp 0x804832a <_start+10>: push %edx 0x804832b <_start+11>: push x804841c 0x8048330 <_start+16>: push x8048298 0x8048335 <_start+21>: push %ecx 0x8048336 <_start+22>: push %esi 0x8048337 <_start+23>: push x80483d0 0x804833c <_start+28>: call 0x80482f8 <__libc_start_main> 0x8048341 <_start+33>: hlt 0x8048342 <_start+34>: nop End of assembler dump. 呵呵,不是main吧,程序的入口是个_start例程。 再来看动态连接器的入口是多少 [alert7@redhat62 alert7]$ ldd helo libc.so.6 => /lib/libc.so.6 (0x40018000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) 动态连接器ld-linux.so.2加载到进程地址空间0x40000000。 [alert7@redhat62 alert7]$ readelf -h /lib/ld-linux.so.2 ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Shared object file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x1990 Start of program headers: 52 (bytes into file) Start of section headers: 328916 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 3 Size of section headers: 40 (bytes) Number of section headers: 23 Section header string table index: 20 共享object入口地址为0x1990。加上整个ld-linux.so.2被加载到进程地址空间0x40000000。 那么动态连接器的入口地址为0x1990+0x40000000=0x40001990。 用户空间执行的第一条指令地址就是0x40001990,既上面#define RTLD_START的开始。 |
UNIX/LINUX 平台可执行文件格式分析[转】
本文讨论了 UNIX/LINUX 平台下三种主要的可执行文件格式:a.out(assembler and link editor output 汇编器和链接编辑器的输出)、COFF(Common Object File Format 通用对象文件格式)、ELF(Executable and Linking Format 可执行和链接格式)。首先是对可执行文件格式的一个综述,并通过描述 ELF 文件加载过程以揭示可执行文件内容与加载运行操作之间的关系。随后依此讨论了此三种文件格式,并着重讨论 ELF 文件的动态连接机制,其间也穿插了对各种文件格式优缺点的评价。最后对三种可执行文件格式有一个简单总结,并提出作者对可文件格式评价的一些感想。 UNIX/LINUX 平台可执行文件格式分析 stack 发表于 2005-4-17 22:03:00 转载自:IBM developerWorks 中国网站 本文讨论了 UNIX/LINUX 平台下三种主要的可执行文件格式:a.out(assembler and link editor output 汇编器和链接编辑器的输出)、COFF(Common Object File Format 通用对象文件格式)、ELF(Executable and Linking Format 可执行和链接格式)。首先是对可执行文件格式的一个综述,并通过描述 ELF 文件加载过程以揭示可执行文件内容与加载运行操作之间的关系。随后依此讨论了此三种文件格式,并着重讨论 ELF 文件的动态连接机制,其间也穿插了对各种文件格式优缺点的评价。最后对三种可执行文件格式有一个简单总结,并提出作者对可文件格式评价的一些感想。 可执行文件格式综述 相对于其它文件类型,可执行文件可能是一个操作系统中最重要的文件类型,因为它们是完成操作的真正执行者。可执行文件的大小、运行速度、资源占用情况以及 可扩展性、可移植性等与文件格式的定义和文件加载过程紧密相关。研究可执行文件的格式对编写高性能程序和一些黑客技术的运用都是非常有意义的。 不管何种可执行文件格式,一些基本的要素是必须的,显而易见的,文件中应包含代码和数据。因为文件可能引用外部文件定义的符号(变量和函数),因此重定位 信息和符号信息也是需要的。一些辅助信息是可选的,如调试信息、硬件信息等。基本上任意一种可执行文件格式都是按区间保存上述信息,称为段 (Segment)或节(Section)。不同的文件格式中段和节的含义可能有细微区别,但根据上下文关系可以很清楚的理解,这不是关键问题。最后,可 执行文件通常都有一个文件头部以描述本文件的总体结构。 相对可执行文件有三个重要的概念:编译(compile)、连接(link,也可称为链接、联接)、加载(load)。源程序文件被编译成目标文件,多个 目标文件被连接成一个最终的可执行文件,可执行文件被加载到内存中运行。因为本文重点是讨论可执行文件格式,因此加载过程也相对重点讨论。下面是 LINUX平台下ELF文件加载过程的一个简单描述。 1:内核首先读ELF文件的头部,然后根据头部的数据指示分别读入各种数据结构,找到标记为可加载(loadable)的段,并调用函数 mmap()把段内容加载到内存中。在加载之前,内核把段的标记直接传递给 mmap(),段的标记指示该段在内存中是否可读、可写,可执行。显然,文本段是只读可执行,而数据段是可读可写。这种方式是利用了现代操作系统和处理器 对内存的保护功能。著名的Shellcode(参考资料 17)的编写技巧则是突破此保护功能的一个实际例子。 2:内核分析出ELF文件标记为 PT_INTERP 的段中所对应的动态连接器名称,并加载动态连接器。现代 LINUX 系统的动态连接器通常是 /lib/ld-linux.so.2,相关细节在后面有详细描述。 3:内核在新进程的堆栈中设置一些标记-值对,以指示动态连接器的相关操作。 4:内核把控制传递给动态连接器。 5:动态连接器检查程序对外部文件(共享库)的依赖性,并在需要时对其进行加载。 6:动态连接器对程序的外部引用进行重定位,通俗的讲,就是告诉程序其引用的外部变量/函数的地址,此地址位于共享库被加载在内存的区间内。动态连接还有一个延迟(Lazy)定位的特性,即只在"真正"需要引用符号时才重定位,这对提高程序运行效率有极大帮助。 7:动态连接器执行在ELF文件中标记为 .init 的节的代码,进行程序运行的初始化。在早期系统中,初始化代码对应函数 _init(void)(函数名强制固定),在现代系统中,则对应形式为 void __attribute((constructor)) init_function(void) { …… } 其中函数名为任意。 8:动态连接器把控制传递给程序,从 ELF 文件头部中定义的程序进入点开始执行。在 a.out 格式和ELF格式中,程序进入点的值是显式存在的,在 COFF 格式中则是由规范隐含定义。 从上面的描述可以看出,加载文件最重要的是完成两件事情:加载程序段和数据段到内存;进行外部定义符号的重定位。重定位是程序连接中一个重要概念。我们知 道,一个可执行程序通常是由一个含有 main() 的主程序文件、若干目标文件、若干共享库(Shared Libraries)组成。(注:采用一些特别的技巧,也可编写没有 main 函数的程序,请参阅参考资料 2)一个 C 程序可能引用共享库定义的变量或函数,换句话说就是程序运行时必须知道这些变量/函数的地址。在静态连接中,程序所有需要使用的外部定义都完全包含在可执 行程序中,而动态连接则只在可执行文件中设置相关外部定义的一些引用信息,真正的重定位是在程序运行之时。静态连接方式有两个大问题:如果库中变量或函数 有任何变化都必须重新编译连接程序;如果多个程序引用同样的变量/函数,则此变量/函数会在文件/内存中出现多次,浪费硬盘/内存空间。比较两种连接方式 生成的可执行文件的大小,可以看出有明显的区别。 a.out 文件格式分析 a.out 格式在不同的机器平台和不同的 UNIX 操作系统上有轻微的不同,例如在 MC680x0 平台上有 6 个 section。下面我们讨论的是最"标准"的格式。 a.out 文件包含 7 个 section,格式如下: exec header(执行头部,也可理解为文件头部) text segment(文本段) data segment(数据段) text relocations(文本重定位段) data relocations(数据重定位段) symbol table(符号表) string table(字符串表) 执行头部的数据结构: struct exec { unsigned long a_midmag; /* 魔数和其它信息 */ unsigned long a_text; /* 文本段的长度 */ unsigned long a_data; /* 数据段的长度 */ unsigned long a_bss; /* BSS段的长度 */ unsigned long a_syms; /* 符号表的长度 */ unsigned long a_entry; /* 程序进入点 */ unsigned long a_trsize; /* 文本重定位表的长度 */ unsigned long a_drsize; /* 数据重定位表的长度 */ }; 文件头部主要描述了各个 section 的长度,比较重要的字段是 a_entry(程序进入点),代表了系统在加载程序并初试化各种环境后开始执行程序代码的入口。这个字段在后面讨论的 ELF 文件头部中也有出现。由 a.out 格式和头部数据结构我们可以看出,a.out 的格式非常紧凑,只包含了程序运行所必须的信息(文本、数据、BSS),而且每个 section 的顺序是固定的。这种结构缺乏扩展性,如不能包含"现代"可执行文件中常见的调试信息,最初的 UNIX 黑客对 a.out 文件调试使用的工具是 adb,而 adb 是一种机器语言调试器! a.out 文件中包含符号表和两个重定位表,这三个表的内容在连接目标文件以生成可执行文件时起作用。在最终可执行的 a.out 文件中,这三个表的长度都为 0。a.out 文件在连接时就把所有外部定义包含在可执行程序中,如果从程序设计的角度来看,这是一种硬编码方式,或者可称为模块之间是强藕和的。在后面的讨论中,我们 将会具体看到ELF格式和动态连接机制是如何对此进行改进的。 a.out 是早期UNIX系统使用的可执行文件格式,由 AT&T 设计,现在基本上已被 ELF 文件格式代替。a.out 的设计比较简单,但其设计思想明显的被后续的可执行文件格式所继承和发扬。可以参阅参考资料 16 和阅读参考资料 15 源代码加深对 a.out 格式的理解。参考资料 12 讨论了如何在"现代"的红帽LINUX运行 a.out 格式文件。 COFF 文件格式分析 COFF 格式比 a.out 格式要复杂一些,最重要的是包含一个节段表(section table),因此除了 .text,.data,和 .bss 区段以外,还可以包含其它的区段。另外也多了一个可选的头部,不同的操作系统可一对此头部做特定的定义。 COFF 文件格式如下: File Header(文件头部) Optional Header(可选文件头部) Section 1 Header(节头部) ……… Section n Header(节头部) Raw Data for Section 1(节数据) Raw Data for Section n(节数据) Relocation Info for Sect. 1(节重定位数据) Relocation Info for Sect. n(节重定位数据) Line Numbers for Sect. 1(节行号数据) Line Numbers for Sect. n(节行号数据) Symbol table(符号表) String table(字符串表) 文件头部的数据结构: struct filehdr { unsigned short f_magic; /* 魔数 */ unsigned short f_nscns; /* 节个数 */ long f_timdat; /* 文件建立时间 */ long f_symptr; /* 符号表相对文件的偏移量 */ long f_nsyms; /* 符号表条目个数 */ unsigned short f_opthdr; /* 可选头部长度 */ unsigned short f_flags; /* 标志 */ }; COFF 文件头部中魔数与其它两种格式的意义不太一样,它是表示针对的机器类型,例如 0x014c 相对于 I386 平台,而 0x268 相对于 Motorola 68000系列等。当 COFF 文件为可执行文件时,字段 f_flags 的值为 F_EXEC(0X00002),同时也表示此文件没有未解析的符号,换句话说,也就是重定位在连接时就已经完成。由此也可以看出,原始的 COFF 格式不支持动态连接。为了解决这个问题以及增加一些新的特性,一些操作系统对 COFF 格式进行了扩展。Microsoft 设计了名为 PE(Portable Executable)的文件格式,主要扩展是在 COFF 文件头部之上增加了一些专用头部,具体细节请参阅参考资料 18,某些 UNIX 系统也对 COFF 格式进行了扩展,如 XCOFF(extended common object file format)格式,支持动态连接,请参阅参考资料 5。 紧接文件头部的是可选头部,COFF 文件格式规范中规定可选头部的长度可以为 0,但在 LINUX 系统下可选头部是必须存在的。下面是 LINUX 下可选头部的数据结构: typedef struct { char magic[2]; /* 魔数 */ char vstamp[2]; /* 版本号 */ char tsize[4]; /* 文本段长度 */ char dsize[4]; /* 已初始化数据段长度 */ char bsize[4]; /* 未初始化数据段长度 */ char entry[4]; /* 程序进入点 */ char text_start[4]; /* 文本段基地址 */ char data_start[4]; /* 数据段基地址 */ } COFF_AOUTHDR; 字段 magic 为 0413 时表示 COFF 文件是可执行的,注意到可选头部中显式定义了程序进入点,标准的 COFF 文件没有明确的定义程序进入点的值,通常是从 .text 节开始执行,但这种设计并不好。 前面我们提到,COFF 格式比 a.out 格式多了一个节段表,一个节头条目描述一个节数据的细节,因此 COFF 格式能包含更多的节,或者说可以根据实际需要,增加特定的节,具体表现在 COFF 格式本身的定义以及稍早提及的 COFF 格式扩展。我个人认为,节段表的出现可能是 COFF 格式相对 a.out 格式最大的进步。下面我们将简单描述 COFF 文件中节的数据结构,因为节的意义更多体现在程序的编译和连接上,所以本文不对其做更多的描述。此外,ELF 格式和 COFF格式对节的定义非常相似,在随后的 ELF 格式分析中,我们将省略相关讨论。 struct COFF_scnhdr { char s_name[8]; /* 节名称 */ char s_paddr[4]; /* 物理地址 */ char s_vaddr[4]; /* 虚拟地址 */ char s_size[4]; /* 节长度 */ char s_scnptr[4]; /* 节数据相对文件的偏移量 */ char s_relptr[4]; /* 节重定位信息偏移量 */ char s_lnnoptr[4]; /* 节行信息偏移量 */ char s_nreloc[2]; /* 节重定位条目数 */ char s_nlnno[2]; /* 节行信息条目数 */ char s_flags[4]; /* 段标记 */ }; 有一点需要注意:LINUX系统中头文件coff.h中对字段s_paddr的注释是"physical address",但似乎应该理解为"节被加载到内存中所占用的空间长度"。字段s_flags标记该节的类型,如文本段、数据段、BSS段等。在 COFF的节中也出现了行信息,行信息描述了二进制代码与源代码的行号之间的对映关系,在调试时很有用。 参考资料 19是一份对 COFF格式详细描述的中文资料,更详细的内容请参阅参考资料 20。 ELF文件格式分析 ELF 文件有三种类型:可重定位文件:也就是通常称的目标文件,后缀为.o。共享文件:也就是通常称的库文件,后缀为.so。可执行文件:本文主要讨论的文件格 式,总的来说,可执行文件的格式与上述两种文件的格式之间的区别主要在于观察的角度不同:一种称为连接视图(Linking View),一种称为执行视图(Execution View)。 首先看看ELF文件的总体布局: ELF header(ELF头部) Program header table(程序头表) Segment1(段1) Segment2(段2) ……… Sengmentn(段n) Setion header table(节头表,可选) 段由若干个节(Section)构成,节头表对每一个节的信息有相关描述。对可执行程序而言,节头表是可选的。参考资料 1中作者谈到把节头表的所有数据全部设置为0,程序也能正确运行!ELF头部是一个关于本文件的路线图(road map),从总体上描述文件的结构。下面是ELF头部的数据结构: typedef struct { unsigned char e_ident[EI_NIDENT]; /* 魔数和相关信息 */ Elf32_Half e_type; /* 目标文件类型 */ Elf32_Half e_machine; /* 硬件体系 */ Elf32_Word e_version; /* 目标文件版本 */ Elf32_Addr e_entry; /* 程序进入点 */ Elf32_Off e_phoff; /* 程序头部偏移量 */ Elf32_Off e_shoff; /* 节头部偏移量 */ Elf32_Word e_flags; /* 处理器特定标志 */ Elf32_Half e_ehsize; /* ELF头部长度 */ Elf32_Half e_phentsize; /* 程序头部中一个条目的长度 */ Elf32_Half e_phnum; /* 程序头部条目个数 */ Elf32_Half e_shentsize; /* 节头部中一个条目的长度 */ Elf32_Half e_shnum; /* 节头部条目个数 */ Elf32_Half e_shstrndx; /* 节头部字符表索引 */ } Elf32_Ehdr; 下面我们对ELF头表中一些重要的字段作出相关说明,完整的ELF定义请参阅参考资料 6和参考资料7。 e_ident [0]-e_ident[3]包含了ELF文件的魔数,依次是0x7f、'E'、'L'、'F'。注意,任何一个ELF 文件必须包含此魔数。参考资料 3中讨论了利用程序、工具、/Proc文件系统等多种查看ELF魔数的方法。e_ident[4]表示硬件系统的位数,1代表32位,2代表64位。 e_ident[5] 表示数据编码方式,1代表小印第安排序(最大有意义的字节占有最低的地址),2代表大印第安排序(最大有意义的字节占有最高的地址)。e_ident [6]指定ELF头部的版本,当前必须为1。e_ident[7]到e_ident[14]是填充符,通常是0。ELF格式规范中定义这几个字节是被忽略 的,但实际上是这几个字节完全可以可被利用。如病毒Lin/Glaurung.676/666(参考资料 1)设置 e_ident[7]为0x21,表示本文件已被感染;或者存放可执行代码(参考资料 2)。ELF头部中大多数字段都是对子头部数据的描述,其意义相对比较简单。值得注意的是某些病毒可能修改字段e_entry(程序进入点)的值,以指向 病毒代码,例如上面提到的病毒Lin/Glaurung.676/666。 一个实际可执行文件的文件头部形式如下:(利用命令readelf) ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x80483cc Start of program headers: 52 (bytes into file) Start of section headers: 14936 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 6 Size of section headers: 40 (bytes) Number of section headers: 34 Section header string table index: 31 紧接ELF头部的是程序头表,它是一个结构数组,包含了ELF头表中字段e_phnum定义的条目,结构描述一个段或其他系统准备执行该程序所需要的信息。 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; 在详细讨论可执行文件程序头表之前,首先查看一个实际文件的输出: Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz *** Align PHDR 0x000034 0x08048034 0x08048034 0x000c0 0x000c0 R E 0x4 INTERP 0x0000f4 0x080480f4 0x080480f4 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x00684 0x00684 R E 0x1000 LOAD 0x000684 0x08049684 0x08049684 0x00118 0x00130 RW 0x1000 DYNAMIC 0x000690 0x08049690 0x08049690 0x000c8 0x000c8 RW 0x4 NOTE 0x000108 0x08048108 0x08048108 0x00020 0x00020 R 0x4 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame 03 .data .dynamic .ctors .dtors .jcr .got .bss 04 .dynamic 05 .note.ABI-tag Section Headers: [Nr] Name Type Addr Off Size ES *** Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 080480f4 0000f4 000013 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 08048108 000108 000020 00 A 0 0 4 [ 3] .hash HASH 08048128 000128 000040 04 A 4 0 4 [ 4] .dynsym DYNSYM 08048168 000168 0000b0 10 A 5 1 4 [ 5] .dynstr STRTAB 08048218 000218 00007b 00 A 0 0 1 [ 6] .gnu.version VERSYM 08048294 000294 000016 02 A 4 0 2 [ 7] .gnu.version_r VERNEED 080482ac 0002ac 000030 00 A 5 1 4 [ 8] .rel.dyn REL 080482dc 0002dc 000008 08 A 4 0 4 [ 9] .rel.plt REL 080482e4 0002e4 000040 08 A 4 b 4 [10] .init PROGBITS 08048324 000324 000017 00 AX 0 0 4 [11] .plt PROGBITS 0804833c 00033c 000090 04 AX 0 0 4 [12] .text PROGBITS 080483cc 0003cc 0001f8 00 AX 0 0 4 [13] .fini PROGBITS 080485c4 0005c4 00001b 00 AX 0 0 4 [14] .rodata PROGBITS 080485e0 0005e0 00009f 00 A 0 0 32 [15] .eh_frame PROGBITS 08048680 000680 000004 00 A 0 0 4 [16] .data PROGBITS 08049684 000684 00000c 00 WA 0 0 4 [17] .dynamic DYNAMIC 08049690 000690 0000c8 08 WA 5 0 4 [18] .ctors PROGBITS 08049758 000758 000008 00 WA 0 0 4 [19] .dtors PROGBITS 08049760 000760 000008 00 WA 0 0 4 [20] .jcr PROGBITS 08049768 000768 000004 00 WA 0 0 4 [21] .got PROGBITS 0804976c 00076c 000030 04 WA 0 0 4 [22] .bss NOBITS 0804979c 00079c 000018 00 WA 0 0 4 [23] .comment PROGBITS 00000000 00079c 000132 00 0 0 1 [24] .debug_aranges PROGBITS 00000000 0008d0 000098 00 0 0 8 [25] .debug_pubnames PROGBITS 00000000 000968 000040 00 0 0 1 [26] .debug_info PROGBITS 00000000 0009a8 001cc6 00 0 0 1 [27] .debug_abbrev PROGBITS 00000000 00266e 0002cc 00 0 0 1 [28] .debug_line PROGBITS 00000000 00293a 0003dc 00 0 0 1 [29] .debug_frame PROGBITS 00000000 002d18 000048 00 0 0 4 [30] .debug_str PROGBITS 00000000 002d60 000bcd 01 MS 0 0 1 [31] .shstrtab STRTAB 00000000 00392d 00012b 00 0 0 1 [32] .symtab SYMTAB 00000000 003fa8 000740 10 33 56 4 [33] .strtab STRTAB 00000000 0046e8 000467 00 0 0 1 对一个ELF可执行程序而言,一个基本的段是标记p_type为PT_INTERP的段,它表明了运行此程序所需要的程序解释器(/lib/ld- linux.so.2),实际上也就是动态连接器(dynamic linker)。最重要的段是标记p_type为PT_LOAD的段,它表明了为运行程序而需要加载到内存的数据。查看上面实际输入,可以看见有两个可 LOAD段,第一个为只读可执行(***为R E),第二个为可读可写(***为RW)。段1包含了文本节.text,注意到ELF文件头部中程序进入点的值为0x80483cc,正好是指向节. text在内存中的地址。段二包含了数据节.data,此数据节中数据是可读可写的,相对的只读数据节.rodata包含在段1中。ELF格式可以比 COFF格式包含更多的调试信息,如上面所列出的形式为.debug_xxx的节。在I386平台LINUX系统下,用命令file查看一个ELF可执行 程序的可能输出是:a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), not stripped。 ELF文件中包含了动态连接器的全路径,内核定位"正确"的动态连接器在内存中的地址是"正确"运行可执行文件的保证,参考资料 13讨论了如何通过查找动态连接器在内存中的地址以达到颠覆(Subversiver)动态连接机制的方法。 最后我们讨论ELF文件的动态连接机制。每一个外部定义的符号在全局偏移表(Global Offset Table GOT)中有相应的条目,如果符号是函数则在过程连接表(Procedure Linkage Table PLT)中也有相应的条目,且一个PLT条目对应一个GOT条目。对外部定义函数解析可能是整个ELF文件规范中最复杂的,下面是函数符号解析过程的一个 描述。 1:代码中调用外部函数func,语句形式为call 0xaabbccdd,地址0xaabbccdd实际上就是符号func在PLT表中对应的条目地址(假设地址为标号.PLT2)。 2:PLT表的形式如下 .PLT0: pushl 4(%ebx) /* GOT表的地址保存在寄存器ebx中 */ jmp *8(%ebx) nop; nop nop; nop .PLT1: jmp *name1@GOT(%ebx) pushl $offset jmp .PLT0@PC .PLT2: jmp *func@GOT(%ebx) pushl $offset jmp .PLT0@PC 3:查看标号.PLT2的语句,实际上是跳转到符号func在GOT表中对应的条目。 4:在符号没有重定位前,GOT表中此符号对应的地址为标号.PLT2的下一条语句,即是pushl $offset,其中$offset是符号func的重定位偏移量。注意到这是一个二次跳转。 5:在符号func的重定位偏移量压栈后,控制跳到PLT表的第一条目,把GOT[1]的内容压栈,并跳转到GOT[2]对应的地址。 6:GOT[2]对应的实际上是动态符号解析函数的代码,在对符号func的地址解析后,会把func在内存中的地址设置到GOT表中此符号对应的条目中。 7:当第二次调用此符号时,GOT表中对应的条目已经包含了此符号的地址,就可直接调用而不需要利用PLT表进行跳转。 动态连接是比较复杂的,但为了获得灵活性的代价通常就是复杂性。其最终目的是把GOT表中条目的值修改为符号的真实地址,这也可解释节.got包含在可读可写段中。 动态连接是一个非常重要的进步,这意味着库文件可以被升级、移动到其他目录等等而不需要重新编译程序(当然,这不意味库可以任意修改,如函数入参的个数、 数据类型应保持兼容性)。从很大程度上说,动态连接机制是ELF格式代替a.out格式的决定性原因。如果说面对对象的编程本质是面对接口 (interface)的编程,那么动态连接机制则是这种思想的地一个非常典型的应用,具体的讲,动态连接机制与设计模式中的桥接(BRIDGE)方法比 较类似,而它的LAZY特性则与**(PROXY)方法非常相似。动态连接操作的细节描述请参阅参考资料 8,9,10,11。通过阅读命令readelf、objdump 的源代码以及参考资料 14中所提及的相关软件源代码,可以对ELF文件的格式有更彻底的了解。 总结 不同时期的可执行文件格式深刻的反映了技术进步的过程,技术进步通常是针对解决存在的问题和适应新的环境。早期的UNIX系统使用a.out格式,随着操 作系统和硬件系统的进步,a.out格式的局限性越来越明显。新的可执行文件格式COFF在UNIX System VR3中出现,COFF格式相对a.out格式最大变化是多了一个节头表(section head table),能够在包含基础的文本段、数据段、BSS段之外包含更多的段,但是COFF对动态连接和C++程序的支持仍然比较困难。为了解决上述问题, UNIX系统实验室(UNIX SYSTEM Laboratories USL) 开发出ELF文件格式,它被作为应用程序二进制接口(Application binary Interface ABI)的一部分,其目的是替代传统的a.out格式。例如,ELF文件格式中引入初始化段.init和结束段.fini(分别对应构造函数和析构函数) 则主要是为了支持C++程序。1994年6月ELF格式出现在LINUX系统上,现在ELF格式作为UNIX/LINUX最主要的可执行文件格式。当然我 们完全有理由相信,在将来还会有新的可执行文件格式出现。 上述三种可执行文件格式都很好的体现了设计思想中分层的概念,由一个总的头部刻画了文件的基本要素,再由若干子头部/条目刻画了文件的若干细节。比较一下 可执行文件格式和以太数据包中以太头、IP头、TCP头的设计,我想我们能很好的感受分层这一重要的设计思想。参考资料 21从全局的角度讨论了各种文件的格式,并提出一个比较夸张的结论:Everything Is Byte! 最后的题外话:大多数资料中对a.out格式的评价较低,常见的词语有黑暗年代(dark ages)、丑陋(ugly)等等,当然,从现代的观点来看,的确是比较简单,但是如果没有曾经的简单何来今天的精巧?正如我们今天可以评价石器时代的技 术是ugly,那么将来的人们也可以嘲讽今天的技术是非常ugly。我想我们也许应该用更平和的心态来对曾经的技术有一个公正的评价。 参考资料 《LINUX VIRUSES - ELF FILE FORMAT》 Marius Van Oers 《A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux 》 breadbox 《The Linux Virus Writing And Detection HOWTO》Alexander Bartolich 《从程序员角度看ELF》Hongjiu Lu alert7(译) 《XCOFF Object File Format》 《Executable and Linkable Format(ELF)》 《elf文件格式 --另一文本方式的elf文档》alert7(译) 《如何修改动态库符号表》wangdb 《分析ELF的加载过程》opera 《Before main() 分析》 alert7 《Linkers & Loaders》John R. Levine 《Running a.out executables on modern Red Hat Linux》 《Cheating the ELF》 《ELF Binary Analysis Tools》 《dbxread.c》 《Manual Reference Pages - A.OUT (5)》 《Linux 下缓冲区溢出攻击的原理及对策》 《Microsoft Portable Executable and Common Object File Format Specification》 《COFF的文件结构》redleaves 《Common Object File Format (COFF)》 《Everything Is Byte》 mala |
分析ELF的加载过程
对于可执行文件来说,段的加载位置是固定的,程序段表中如实反映了段的加载地址.对于共享库来?段的加载位置是浮动的,位置无关的,程序段表反映的是以0 作为基准地址的相对加载地址.尽管共享库的连接是不充分的,为了便于测试动态链接器,Linux允许直接加载共享库运行.如果应用程序具有动态链接器的描 述段,内核在完成程序段加载后,紧接着加载动态链接器,并且启动动态链接器的 ELF的可执行文件与共享库在结构上非常类似,它们具有一张程序段表,用来描述这些段如何映射到?br>炭占? 对于可执行文件来说,段的加载位置是固定的,程序段表中如实反映了段的加载地址.对于共享库来?br>?段的加载位置是浮动的,位置无关的,程序段 表反映的是以0作为基准地址的相对加载地址.尽管共享库的连接是不充分的,为了便于测试动态链接器,Linux允许直接加载共享库运行.如果应用程序具有 动态链接器的描述段,内核在完成程序段加载后,紧接着加载动态链接器,并且启动动态链接器的?br>肟? typedef struct elf32_hdr{ unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; /* ET_EXEC ET_DYN 等 */ Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; /* Entry point */ Elf32_Off e_phoff; 程序段描述表的位置 Elf32_Off e_shoff; 一般段描述表的位置 Elf32_Word e_flags; Elf32_Half e_ehsize; ELF头的大小 Elf32_Half e_phentsize; 程序段描述表单元的大小 Elf32_Half e_phnum; 程序段描述表单元的个数 Elf32_Half e_shentsize; 一般段描述表的单元大小 Elf32_Half e_shnum; 一般段描述表单元的个数 Elf32_Half e_shstrndx; } Elf32_Ehdr; typedef struct elf32_phdr{ Elf32_Word p_type; PT_INTERP,PT_LOAD,PT_DYNAMIC等 Elf32_Off p_offset; 该程序段在ELF文件中的位置 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; struct linux_binprm{ char buf[BINPRM_BUF_SIZE]; 预先读入的ELF文件头 struct page *page[MAX_ARG_PAGES]; unsigned long p; /* current top of mem */ int sh_bang; struct file * file; int e_uid, e_gid; kernel_cap_t cap_inheritable, cap_permitted, cap_effective; int argc, envc; char * filename; /* Name of binary */ unsigned long loader, exec; }; static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs) { struct file *interpreter = NULL; /* to shut gcc up */ unsigned long load_addr = 0, load_bias; int load_addr_set = 0; char * elf_interpreter = NULL; unsigned int interpreter_type = INTERPRETER_NONE; unsigned char ibcs2_interpreter = 0; mm_segment_t old_fs; unsigned long error; struct elf_phdr * elf_ppnt, *elf_phdata; unsigned long elf_bss, k, elf_brk; int elf_exec_fileno; int retval, size, i; unsigned long elf_entry, interp_load_addr = 0; unsigned long start_code, end_code, start_data, end_data; struct elfhdr elf_ex; struct elfhdr interp_elf_ex; struct exec interp_ex; char passed_fileno[6]; /* Get the exec-header */ elf_ex = *((struct elfhdr *) bprm->buf); retval = -ENOEXEC; /* First of all, some simple consistency checks */ if (memcmp(elf_ex.e_ident, ELFMAG, SELFMAG) != 0) goto out; 文件头标记是否匹配 if (elf_ex.e_type != ET_EXEC && elf_ex.e_type != ET_DYN) goto out; 文件类型是否为可执行文件或共享库 if (!elf_check_arch(&elf_ex)) goto out; if (!bprm->file->f_op||!bprm->file->f_op->mmap) goto out; 所在的文件系统是否具有文件映射功能 /* Now read in all of the header information */ retval = -ENOMEM; size = elf_ex.e_phentsize * elf_ex.e_phnum; 求程序段表总长度 if (size > 65536) goto out; elf_phdata = (struct elf_phdr *) kmalloc(size, GFP_KERNEL); 分配程序段表空间 if (!elf_phdata) goto out; ; 读入程序段表 retval = kernel_read(bprm->file, elf_ex.e_phoff, (char *) elf_phdata, size); if (retval < 0) goto out_free_ph; retval = get_unused_fd(); 取可用进程文件表的自由槽位 if (retval < 0) goto out_free_ph; get_file(bprm->file); fd_install(elf_exec_fileno = retval, bprm->file); 将打开的文件安装到进程文件表 elf_ppnt = elf_phdata; 指向程序段表 elf_bss = 0; bss段的起始地址 elf_brk = 0; bss段的终止地址 start_code = ~0UL; 代码段的开始 end_code = 0; 代码段的终止 start_data = 0; 数据段的开始 end_data = 0; 数据段的终止 ; 扫描ELF程序段表,搜寻动态链接器定义 for (i = 0; i < elf_ex.e_phnum; i++) { if (elf_ppnt->p_type == PT_INTERP) { retval = -EINVAL; if (elf_interpreter) goto out_free_dentry; 如果包含多个动态链接器描述项 /* This is the program interpreter used for * shared libraries - for now assume that this * is an a.out format binary */ retval = -ENOMEM; 为动态链接器名称字符串分配空间 elf_interpreter = (char *) kmalloc(elf_ppnt->p_filesz, GFP_KERNEL); if (!elf_interpreter) goto out_free_file; ; 将动态链接器的文件名读入内存 retval = kernel_read(bprm->file, elf_ppnt->p_offset, elf_interpreter, elf_ppnt->p_filesz); if (retval < 0) goto out_free_interp; /* If the program interpreter is one of these two, * then assume an iBCS2 image. Otherwise assume * a native linux image. */ if (strcmp(elf_interpreter,"/usr/lib/libc.so.1") == 0 || strcmp(elf_interpreter,"/usr/lib/ld.so.1") == 0) ibcs2_interpreter = 1; 说明应用程序是IBCS2仿真代码 interpreter = open_exec(elf_interpreter); 打开动态链接器文件 retval = PTR_ERR(interpreter); if (IS_ERR(interpreter)) goto out_free_interp; retval = kernel_read(interpreter, 0, bprm->buf, BINPRM_BUF_SIZE); ; 读入动态链接器文件头 if (retval < 0) goto out_free_dentry; /* Get the exec headers */ interp_ex = *((struct exec *) bprm->buf); 假定为aout格式的文件头结构 interp_elf_ex = *((struct elfhdr *) bprm->buf); 假定为ELF文件头结构 } elf_ppnt++; 下一片段目录项 } /* Some simple consistency checks for the interpreter */ if (elf_interpreter) { ; 如果定义了动态链接器,分析其格式类型 interpreter_type = INTERPRETER_ELF | INTERPRETER_AOUT; /* Now figure out which format our binary is */ if ((N_MAGIC(interp_ex) != OMAGIC) && (N_MAGIC(interp_ex) != ZMAGIC) && (N_MAGIC(interp_ex) != QMAGIC)) interpreter_type = INTERPRETER_ELF; 如果不是AOUT标识 if (memcmp(interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0) interpreter_type &= ~INTERPRETER_ELF; 如果没有ELF标识 retval = -ELIBBAD; if (!interpreter_type) 不能识别动态链接器类型 goto out_free_dentry; /* Make sure only one type was selected */ if ((interpreter_type & INTERPRETER_ELF) && interpreter_type != INTERPRETER_ELF) { printk(KERN_WARNING "ELF: Ambiguous type, using ELF/n"); interpreter_type = INTERPRETER_ELF; } } /* OK, we are done with that, now set up the arg stuff, and then start this sucker up */ if (!bprm->sh_bang) { char * passed_p; if (interpreter_type == INTERPRETER_AOUT) { sprintf(passed_fileno, "%d", elf_exec_fileno); passed_p = passed_fileno; if (elf_interpreter) { retval = copy_strings_kernel(1,&passed_p,bprm); ; 将程序的文件描述符压入参数堆栈,准备传递给aout格式的动态链接器 if (retval) goto out_free_dentry; bprm->argc++; bprm->page[]中参数的数目 } } } /* Flush all traces of the currently running executable */ retval = flush_old_exec(bprm); if (retval) goto out_free_dentry; /* OK, This is the point of no return */ current->mm->start_data = 0; current->mm->end_data = 0; current->mm->end_code = 0; current->mm->mmap = NULL; current->flags &= ~PF_FORKNOEXEC; elf_entry = (unsigned long) elf_ex.e_entry; 应用程序的入口地址 /* Do this immediately, since STACK_TOP as used in setup_arg_pages may depend on the personality. */ SET_PERSONALITY(elf_ex, ibcs2_interpreter); ; 如果是ibcs2_interpreter非0,置PER_SVR4代码个性,否则置PER_LINUX代码个性 /* Do this so that we can load the interpreter, if need be. We will change some of these later */ current->mm->rss = 0; setup_arg_pages(bprm); /* XXX: check error */ ; 建立描述堆栈参数页的初始虚存范围结构 current->mm->start_stack = bprm->p; /* Try and get dynamic programs out of the way of the default mmap base, as well as whatever program they might try to exec. This is because the brk will follow the loader, and is not movable. */ load_bias = ELF_PAGESTART(elf_ex.e_type==ET_DYN ? ELF_ET_DYN_BASE : 0); ; 如果需要加载的是共享库,则设置共享库的加载偏置为ELF_ET_DYN_BASE /* Now we do a little grungy work by mmaping the ELF image into the correct location in memory. At this point, we assume that the image should be loaded at fixed address, not at a variable address. */ old_fs = get_fs(); set_fs(get_ds()); ; 再次扫描程序段描述表,映射其中的程序段到进程空间 for(i = 0, elf_ppnt = elf_phdata; i < elf_ex.e_phnum; i++, elf_ppnt++) { int elf_prot = 0, elf_flags; unsigned long vaddr; if (elf_ppnt->p_type != PT_LOAD) continue; 如果程序段不可加载 if (elf_ppnt->p_flags & PF_R) elf_prot |= PROT_READ; if (elf_ppnt->p_flags & PF_W) elf_prot |= PROT_WRITE; if (elf_ppnt->p_flags & PF_X) elf_prot |= PROT_EXEC; elf_flags = MAP_PRIVATE|MAP_DENYWRITE|MAP_EXECUTABLE; ; 根据程序段描述设置相应的mmap参数 vaddr = elf_ppnt->p_vaddr; 段起始地址 if (elf_ex.e_type == ET_EXEC || load_addr_set) { ; 对于可执行程序,使用固定映射 elf_flags |= MAP_FIXED; } error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags); ; 将该程序段[eppnt->p_offset,eppnt->p_filesz]映射到虚存(load_bias+vaddr)开始的区域 if (!load_addr_set) { load_addr_set = 1; load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset); ; 求出该ELF文件在用户虚存中的起始地址 if (elf_ex.e_type == ET_DYN) { load_bias += error - ELF_PAGESTART(load_bias + vaddr); load_addr += error; } } k = elf_ppnt->p_vaddr; if (k < start_code) start_code = k; 取最小的段地址作为代码段起始 if (start_data < k) start_data = k; 取最大的段地址作为数据段起始 k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz; 这时k指向文件段尾 if (k > elf_bss) elf_bss = k; 取最大文件段尾作为BSS段起始 if ((elf_ppnt->p_flags & PF_X) && end_code < k) end_code = k; 取最大可执行的文件段尾作为可执行段的终止 if (end_data < k) end_data = k; 取最大的文件段尾作为数据段的终止 k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz; 这时k指向内存段尾 if (k > elf_brk) elf_brk = k; 取最大的内存段尾作为BSS段的终止 } set_fs(old_fs); elf_entry += load_bias; elf_bss += load_bias; elf_brk += load_bias; start_code += load_bias; end_code += load_bias; start_data += load_bias; end_data += load_bias; if (elf_interpreter) { if (interpreter_type == INTERPRETER_AOUT) elf_entry = load_aout_interp(&interp_ex, interpreter); else elf_entry = load_elf_interp(&interp_elf_ex, 动态链接器的文件头 interpreter, 动态链接器打开的文件结构 &interp_load_addr); 输出链接器的加载地址 ; 可执行程序的入口变为动态链接器的入口 allow_write_access(interpreter); fput(interpreter); kfree(elf_interpreter); if (elf_entry == ~0UL) { printk(KERN_ERR "Unable to load interpreter/n"); kfree(elf_phdata); send_sig(SIGSEGV, current, 0); return 0; } } kfree(elf_phdata); 释放程序段表 if (interpreter_type != INTERPRETER_AOUT) sys_close(elf_exec_fileno); set_binfmt(&elf_format); 增加ELF内核模块的引用计数 compute_creds(bprm); current->flags &= ~PF_FORKNOEXEC; bprm->p = (unsigned long) 建立入口函数参数表 create_elf_tables((char *)bprm->p, bprm->argc, bprm->envc, (interpreter_type == INTERPRETER_ELF ? &elf_ex : NULL), load_addr, load_bias, interp_load_addr, (interpreter_type == INTERPRETER_AOUT ? 0 : 1)); /* N.B. passed_fileno might not be initialized? */ if (interpreter_type == INTERPRETER_AOUT) current->mm->arg_start += strlen(passed_fileno) + 1; current->mm->start_brk = current->mm->brk = elf_brk; current->mm->end_code = end_code; current->mm->start_code = start_code; current->mm->start_data = start_data; current->mm->end_data = end_data; current->mm->start_stack = bprm->p; /* Calling set_brk effectively mmaps the pages that we need * for the bss and break sections */ set_brk(elf_bss, elf_brk); 建立bss的虚存映射,elf_bss是bss的开始,elf_brk是bss的结束 padzero(elf_bss); 如果bss不起始于页连界上,说明与data段有重叠,则将该页bss区域清0 #if 0 printk("(start_brk) %lx/n" , (long) current->mm->start_brk); printk("(end_code) %lx/n" , (long) current->mm->end_code); printk("(start_code) %lx/n" , (long) current->mm->start_code); printk("(start_data) %lx/n" , (long) current->mm->start_data); printk("(end_data) %lx/n" , (long) current->mm->end_data); printk("(start_stack) %lx/n" , (long) current->mm->start_stack); printk("(brk) %lx/n" , (long) current->mm->brk); #endif if ( current->personality == PER_SVR4 ) { /* Why this, you ask??? Well SVr4 maps page 0 as read-only, and some applications "depend" upon this behavior. Since we do not have the power to recompile these, we emulate the SVr4 behavior. Sigh. */ /* N.B. Shouldn't the size here be PAGE_SIZE?? */ down(¤t->mm->mmap_sem); error = do_mmap(NULL, 0, 4096, PROT_READ | PROT_EXEC, MAP_FIXED | MAP_PRIVATE, 0); up(¤t->mm->mmap_sem); } #ifdef ELF_PLAT_INIT /* * The ABI may specify that certain registers be set up in special * ways (on i386 %edx is the address of a T_FINI function, for * example. This macro performs whatever initialization to * the regs structure is required. */ ELF_PLAT_INIT(regs); #endif start_thread(regs, elf_entry, bprm->p); 将返回的eip设为elf_entry,esp设为bprm->p if (current->ptrace & PT_PTRACED) send_sig(SIGTRAP, current, 0); 如果进程处于跟踪状态,则生成SIGTRAP信号 retval = 0; out: return retval; /* error cleanup */ out_free_dentry: allow_write_access(interpreter); fput(interpreter); out_free_interp: if (elf_interpreter) kfree(elf_interpreter); out_free_file: sys_close(elf_exec_fileno); out_free_ph: kfree(elf_phdata); goto out; } static unsigned long load_elf_interp(struct elfhdr * interp_elf_ex, struct file * interpreter, unsigned long *interp_load_addr) { struct elf_phdr *elf_phdata; struct elf_phdr *eppnt; unsigned long load_addr = 0; int load_addr_set = 0; unsigned long last_bss = 0, elf_bss = 0; unsigned long error = ~0UL; int retval, i, size; /* First of all, some simple consistency checks */ if (interp_elf_ex->e_type != ET_EXEC && interp_elf_ex->e_type != ET_DYN) goto out; if (!elf_check_arch(interp_elf_ex)) goto out; if (!interpreter->f_op || !interpreter->f_op->mmap) goto out; /* * If the size of this structure has changed, then punt, since * we will be doing the wrong thing. */ if (interp_elf_ex->e_phentsize != sizeof(struct elf_phdr)) goto out; /* Now read in all of the header information */ size = sizeof(struct elf_phdr) * interp_elf_ex->e_phnum; if (size > ELF_MIN_ALIGN) goto out; 如果动态链接器的程序段表的尺寸大于1个页面 elf_phdata = (struct elf_phdr *) kmalloc(size, GFP_KERNEL); if (!elf_phdata) goto out; retval = kernel_read(interpreter,interp_elf_ex->e_phoff,(char *)elf_phdata,size); error = retval; if (retval < 0) goto out_close; eppnt = elf_phdata; for (i=0; ie_phnum; i++, eppnt++) { if (eppnt->p_type == PT_LOAD) { int elf_type = MAP_PRIVATE | MAP_DENYWRITE; int elf_prot = 0; unsigned long vaddr = 0; unsigned long k, map_addr; if (eppnt->p_flags & PF_R) elf_prot = PROT_READ; if (eppnt->p_flags & PF_W) elf_prot |= PROT_WRITE; if (eppnt->p_flags & PF_X) elf_prot |= PROT_EXEC; vaddr = eppnt->p_vaddr; if (interp_elf_ex->e_type == ET_EXEC || load_addr_set) elf_type |= MAP_FIXED; map_addr = elf_map(interpreter, load_addr + vaddr, eppnt, elf_prot, elf_type); if (!load_addr_set && interp_elf_ex->e_type == ET_DYN) { load_addr = map_addr - ELF_PAGESTART(vaddr); load_addr_set = 1; } /* * Find the end of the file mapping for this phdr, and keep * track of the largest address we see for this. */ k = load_addr + eppnt->p_vaddr + eppnt->p_filesz; if (k > elf_bss) elf_bss = k; /* * Do the same thing for the memory mapping - between * elf_bss and last_bss is the bss section. */ k = load_addr + eppnt->p_memsz + eppnt->p_vaddr; if (k > last_bss) last_bss = k; } } /* Now use mmap to map the library into memory. */ /* * Now fill out the bss section. First pad the last page up * to the page boundary, and then perform a mmap to make sure * that there are zero-mapped pages up to and including the * last bss page. */ padzero(elf_bss); elf_bss = ELF_PAGESTART(elf_bss + ELF_MIN_ALIGN - 1); /* What we have mapped so far*/ /* Map the last of the bss segment */ if (last_bss > elf_bss) do_brk(elf_bss, last_bss - elf_bss); *interp_load_addr = load_addr; error = ((unsigned long) interp_elf_ex->e_entry) + load_addr; out_close: kfree(elf_phdata); out: return error; } static inline unsigned long elf_map (struct file *filep, unsigned long addr, struct elf_phdr *eppnt, int prot, inttype) { unsigned long map_addr; down(¤t->mm->mmap_sem); map_addr = do_mmap(filep, ELF_PAGESTART(addr), eppnt->p_filesz + ELF_PAGEOFFSET(eppnt->p_vaddr), prot, type, eppnt->p_offset - ELF_PAGEOFFSET(eppnt->p_vaddr)); up(¤t->mm->mmap_sem); return(map_addr); } void set_binfmt(struct linux_binfmt *new) { struct linux_binfmt *old = current->binfmt; if (new && new->module) __MOD_INC_USE_COUNT(new->module); current->binfmt = new; if (old && old->module) __MOD_DEC_USE_COUNT(old->module); } static void set_brk(unsigned long start, unsigned long end) { start = ELF_PAGEALIGN(start); end = ELF_PAGEALIGN(end); if (end <= start) return; do_brk(start, end - start); } static void padzero(unsigned long elf_bss) { unsigned long nbyte; nbyte = ELF_PAGEOFFSET(elf_bss); if (nbyte) { nbyte = ELF_MIN_ALIGN - nbyte; clear_user((void *) elf_bss, nbyte); } } |
xueyan | 05-06-24 03:12 |