Chinaunix首页 | 论坛 | 博客
  • 博客访问: 297773
  • 博文数量: 71
  • 博客积分: 30
  • 博客等级: 民兵
  • 技术积分: 217
  • 用 户 组: 普通用户
  • 注册时间: 2012-07-31 15:43
文章分类

全部博文(71)

文章存档

2016年(4)

2015年(2)

2014年(2)

2013年(63)

分类: LINUX

2013-05-10 13:15:30

今天进入了第三章目标文件的学习和分析,所接触的东西对于我来说基本上都是扫盲了,呵呵,收获非浅~~~~

  •     经过预编译、编译和汇编得到的目标文件,从文件结构上来说,与经过链接后得到的可执行文件已经没有多少差别了,故而可以将目标文件与可执行文件看作一个类型的文件。当前PC环境下的可执行文件主要是Windows平台下的PE格式和Linux下的ELF格式,二者其实都源于COFF格式,所以PE-COFF和ELF其实很接近。
        COFF格式文件最大的特点是在文件构成中引入了“Section(段)”机制,不同的目标文件可以拥有不同数量不同类型的段。
    /* COFF格式源于UNIX System V Release 3,Microsoft在此基础上制定了Windows平台下的可执行文件格式PE-COFF; UNIX System V Release 4则在COFF的基础上引入了ELF格式,当前流行的Linux均以ELF作为基本的可执行文件格式 */
  •     其实不光是可执行文件(Windows下的*.exe和Linux下的ELF可执行文件),动态链接库(Windows下的*.dll和Linux下 的*.so)和静态链接库(Windows下的*.lib和Linux下的*.a)文件也都是按照可执行文件格式存储的
  • GNU的 binutils 工具是针对二进制文件的有效工具包,其中包括 objdump、readelf、ld、as 等诸多常用工具
  • ELF文件标准中定义了四类ELF文件类型,Linux下可通过“file 文件名”指令查看ELF类型。
    1、可重定位文件——包含代码和数据,可以被链接得到可执行文件或者可共享目标文件;
    2、可执行文件
    3、共享目标文件——用法暂不清楚,以后来更新
    4、核心转储文件——用法暂不清楚,以后来更新
  • 一个ELF文件的构成部件主要包括:ELF文件头,.text段、.data段、.rodata段、.bss段等section,段表(Section Header Table)等
  • ELF文件头的定义对应于/usr/include/Elf.h中的Elf32_Ehdr结构体,可以通过 readelf -h 文件名 来查看
    e_ident[16]; ——对于ELF文件,前四个字节恒为0x7f  0x45  0x4C  0x46,这就是每种目标文件格式所特有的Magic Number,第五个字节为0x01表示32位elf文件,第六个字节表示大小端排序,1是小端,2是大端
    e_type;      —— 在Elf.h中定义的宏 ET_REL(可重定位文件),ET_EXEC(可执行文件),ET_DYN(共享目标文件)
    e_machine;   —— 在Elf.h中定义的宏 EM_386, EM_ARM, EM_SPARC, EM_MIPS等
    e_version;   —— 恒为1(ELF标准委员会退出ELF 1.2标准后,委员会宣告解散,ELF版本从此未变)
    e_entry;     —— ELF程序的入口虚拟地址
    e_phoff;     —— 用法暂不清楚,以后来更新
    e_shoff;   —— 非常重要!表示ELF文件的段表(Section Header Table)在ELF文件这哦
    e_ehsize;    —— ELF文件头自身的大小;
    e_phentsize; —— 用法暂不清楚,以后来更新
    e_phnum;     —— 用法暂不清楚,以后来更新
    e_shentsize; —— 表示ELF文件的段表中的每个段描述符结构的大小(Linux中为40 Bytes,对应于Elf.h中的 Elf32_Shdr 结构体)
    e_shnum;     —— 表示ELF文件的段表中共有多少个段描述符,即ELF文件中共有多少个section
    e_shstrndx;  —— 表示段表字符串表所在的段(即.shstrtab段)在段表中的索引号
  •     ELF文件头之后就是各个section依次排列,可以通过 objdump -s 文件名 命令查看非空的主要的代码段、数据段、.bss段、.comment段的内容,其中.bss段中其实并没有内容,是为未初始化的全局变量和未初始化的局部静态变量在运行时预留的section,也不在ELF目标文件中占据空间。
        在使用 objdump -s 时添加-d参数会得到代码的反汇编形式,可以看到直接使用 objdump -s 文件名 命令查看的代码段内容与反汇编内容是一致的
  •     段表(Section Header Table)是非常重要的内容,它完全地描述了一个ELF文件中各个段的详细信息。从结构上讲,段表是一个 Elf32_Shdr 结构体的数组,该数组中成员的个数即为ELF文件中真正的段的数量,而每个 Elf32_Shdr 结构体就完整地描述了每一个section,该结构体如下:
    sh_name;     —— 作为段名的字符串常量在段表字符串表(.shstrtab)中的偏移量,诸如.text, .data, .bss等,用户也可以自定义新的段名,但是自定义段名不能再以.开头。须注意:段名仅仅在编译和链接的时候有用,而对于操作系统来说,段名是无用的,OS只是根据sh_type和sh_flags来决定如何处理
    sh_type;     —— 在Elf.h中定以为宏 SHT_NULL, SHT_PROGBITS(程序段,代码段和数据段都是这个类型),SHT_SYMTAB(段内容为符号表), SHT_STRTAB(段内容为字符串表,如.strtab和.shstrtab), SHT_REL(段内容为重定位表,如.rel.text), SHT_NOBITS(段中无内容,如.bss) 等
    sh_flags;    —— 表示段在进程虚拟地址空间中的属性,包括可写SHF_WRITE,需要分配虚拟地址空间SHF_ALLOC,在进程空间中可执行SHF_EXECINSTR
    sh_addr;    —— 概念还不是很清楚,以后来更新
    sh_offset;   —— 如果该段确实物理上存在于ELF文件中,则给出该段在ELF文件中的偏移量,这个量对于诸如.bss段来说就没有意义
    sh_size;      —— 该段在ELF文件中的大小
    sh_link;     —— 仅仅对于链接相关的段有意义,对于SHT_REL段而言,表示该段所使用的符号表在Section Header Table中的索引
    sh_info;     —— 仅仅对于链接相关的段有意义,对于SHT_REL段而言,表示该重定位段所作用的段在Section Header Table中的索引,例如.rel.text就是作用于.text的,sh_info的值即为.text在段表中的索引
    sh_addralign; —— 该段在ELF文件中的起始地址sh_addr必须满足的对齐条件,即sh_addr必须是2的sh_addralign次幂的整数倍
    sh_entsize;   —— 如果该段包含table(例如符号表),则表示table中每个entry的固定大小
  •     重定位表也是ELF中的一个段,其段类型为SHT_REL或SHT_RELA,对于每个需要进行重定位的代码段或数据段,都对应着一个重定位表,例如.text需要重定位则有一个.rel.text,如果.data需要重定位则对应着.rel.data
  •     ELF文件中会用到很多的字符串常量,这些字符串长度不一定,故而不宜使用固定的结构来存放和表示,因此在ELF文件中将所有的字符串常量都同意保存到字 符串表中,字符串表作为类型为SHT_STRTAB的段。然后使用各个字符串在字符串表中的索引来引用这些不同的字符串,从而就不用再考虑字符串的长度 了。其中.strtab存放的是程序中使用到的字符串,而.shstrtab存放的是段表中用到的字符串(即各个段的段名)
  •     链接的过程其实就是对不同目标模块中定义的函数和全局变量相互引用的解析,函数和全局变量都称为“符号(Symbol)”,因此链接的过程必须要对符号进行完善的管理,因此每个ELF目标文件都有一个自己的符号表,即 .symtab 段,语法上符号表即是一个Elf32_Sym结构体数组——注意,提到符号,头脑中形成的概念觉不应该只是如汉字表面意思那样理解为一个字符串名字,符号除了符号名之外,还有很多其他的属性,一个符号对应于Elf.h中的一个 Elf32_Sym 结构体,其中的st前缀表示Symbol Table,每个Elf32_Sym结构体为16 Bytes.
    st_name;    —— 符号名字符串在.strtab或.shstrtab中的索引
    st_value;   —— 符号值,对于在本目标文件中定义的非COMMON类型符号,表示该符号在所在段中的偏移量;对于COMMON类型符号,表示其对齐属性;
                 在可执行文件中,则表示符号的虚拟地址(概念还不是很清楚,以后来更新
    st_size;    —— 符号在ELF文件中所占的大小
    st_info;    —— 低4位表示符号类型:STT_NOTYPE、STT_OBJECT(变量、数组等)、STT_FILE(目标文件名)、STT_FUNC(函数)、STT_SECTION(一个段)
                     高4为表示符号绑定信息:STB_LOCAL(局部符号)、STB_GLOBAL(全局符号)、STB_WEAK(弱符号)
    st_other;   —— 恒为0,留着扩展用
    st_shndx;   —— 对于在本目标文件中定义的局部变量、已初始化全局变量、函数,则表示符号所在段,对于在本目标文件中定义的未初始化全局变量为
                     SHN_COMMON ,对于未在本目标文件中定义的符号为 SHN_UNDEF
    使用 readelf -s *.o 命令可以详细查看目标文件的符号表情况。

  •     当一个使用C语言编写的模块要于其他语言编写的库文件进行链接的时候,存在着符号冲突的现象——即C语言模块中与库文件中存在着符号名相同的符号;随着软 件规模的不断扩大,即使采用相同的编程语言,但是由于不同模块开发人员不同,也可能在不同模块中使用相同的符号名,产生符号名冲突,为此编译器乃至编程语 言本身都需要对此作出修改
        在C++中为此引入了“函数签名”的概念,每个函数签名包含这一个函数的返回类型、参数类型、函数名、所在名字空间、所在类等所有的信息,然后编译器和链接器在处理一个符号的时候,按照某种“符号名称修饰”法则使得每一个函数签名对应着一个修饰后的符号名。对于全局变量和静态变量同样采用类似的签名机制。——对于具体的符号修饰方法不用深究,binutils中提供了一个 c++filt 工具可以分析一个修饰后的符号名称
        符号名修饰机制使得编译器和链接器就可以分清楚各个模块中定义的每一个符号而不至产生符号名冲突了,但是由此另外一个问题则是,不同编译器采用不同的符号修饰法则,故而不同编译器编译出来的目标文件往往无法正常链接到一起。
  •     为了解决C和C++编译器因为符号修饰方法不同导致的使用C和C++编写的模块之间不能正确链接的问题,C++中通过 extern "C"{ } 机制来实现兼容。即包括在大括号内部的代码将被C++编译器当作纯粹的C语言代码来编译,这样C++的符号修饰方法就不会用在这段代码上。
        C++编译器在编译C++文件时,会自动定义 __cplusplus 宏,而在C标准库头文件中定义C语言用的库函数原型时,可以通过添加
    #ifdef __cplusplus
    extern "C" {
    #endif
       C语言库函数原型
    #ifdef __cplusplus

    #endif
        这样的处理方式来实现C和C++对于C标准库的兼容使用,否则C++编译器将会把这些C语言库函数当作是一般的C++函数,而在编译后对其符号作修正,使得链接的时候无法正确链接到C语言标准库。
  •     很多时候,用户可能并不希望使用现有库中的函数而是使用自己定义的同名库函数,这就要求允许不同程序模块中存在相同名字的函数定义——这就是“强符号”和“弱符号”的概念;
        对于C/C++编译器,在本目标文件中定义的函数和初始化了的全局变量为强符号,本目标文件中定义的未初始化全局变量为弱符号,而在本目标文件中使用 extern引用的符号不属于强弱符号定义的范畴。链接器工作时,不允许各个模块中出现同名的强符号,而当仅有一个模块中为强符号其他模块中为弱符号时, 链接器选择强符号;如果只存在多个弱符号链接器选择占用空间最大的那个——这种情况必须避免,否则错误难以发现。弱符号往往使用在库中,GCC下可以通过 __attribute__((weak)) 关键字显式地定义一个弱符号。

  •     系统中某些扩展功能模块可能时有时无,如果要求系统在两种情况下都能正常工作,这就需要在功能模块不存在时主程序中对这些扩展功能模块的引用不能报错——这就是“强引用”和“弱引用”的概念。
        默认地,本模块中所有对外部符号的引用在链接时都是强引用,该符号必须能够被正确决议(理解为“绑定”的同义词),否则链接器就会报错;GCC中可以通过 __attribute__ ((weakref)) 关键字显式地将对一个外部符号的应用定义为弱引用,这样即使在链接的时候该符号并没有正确地找到定义,链接器也不会报错,而只是将该符号的符号值(st_value)置为0,但是这样得到的可执行程序在执行时会出错,因为当调用弱符号时,弱符号地址为0,属于非法访问。因此在程序中调用一个外部符号时,应该先判断其值是否为0,若不为0再进行调用。
    在GCC下使用 gcc -g 参数会在目标文件中加入大量的debug相关的段,调试信息在目标文件/可执行文件中占据了不少的空间,当程序最终发布的时候应该将调试信息从中去除以节省大量空间。Linux下可以使用 strip 文件名 命令来去除ELF文件中的调试信息。
阅读(1799) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~