写着玩-硬怼目标文件
0x00 口胡
最近dota有点上头,就是怼,马上就能回家跟基友回家黑店了,我是屌屌的3号位,哈哈哈。。。
目标文件,怼起。。。
0x01 目标文件的格式
还记得我们前面所说的后缀是.o的文件,就是他了,他就是目标文件的一种,其实他已经很接近可执行文件了,不,它已经可以执行了,只要你给他创造相应的条件的话。所以,我们将目标文件与可执行文件看成是一种类型的文件也未尝不可。在windows下,我们统称为PE-COFF文件格式,在linux下,统称为ELF文件。
还有动态链接库-(DLL & .so),静态链接库-(windows下的.lib,linux的.a)也都是可执行文件的格式,但是静态链接库稍有不同,它是把很多目标文件捆绑在一起形成一个文件,再加上一些索引,可以简单的理解为一个包含有很多目标文件的文件包。
ELF文件类型:
0x02 ELF文件结构
ELF文件基本结构图:
ELF目标文件格式的最前部是ELF文件头(ELF Header),它包含了描述整个文件的基本属性。比如ELF文件版本、目标机器型号、程序入口地址等。紧接着是ELF文件各个段。其中ELF文件中与段有关的重要结构是段表(Section Header Table),该表描述了ELF文件包含的所有段的信息。比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。
1. 文件头
可以用readelf命令来详细查看ELF文件:
ELF文件头(ELF_Ehdr)结构的具体定义:
ELF文件头结构成员具体含义:
ELF文件头中的魔数(前16字节e_ident含义):
文件类型
机器类型
2. 段表
段表总体描述
所以,段表就是一个数组。
数组成员(ELF32_Shdr结构定义)
由段表视角出发得到的ELF文件布局
段描述符(ELF32-Shdr)各字段具体含义
段的类型(sh_types)
段的标志位(sh_flag)
对于系统保留段,下表列出了他们的属性
段的链接信息
3. 重定位表
在介绍链接的过程的时候,会详细的介绍重定位表的作用
4. 字符串表
这是个很重要的表,对于我们把整个ELF文件结构框架搭建起来必不可少
即:ELF文件头(e_shstrndx)->段表中的段表字符串表信息->段表字符串表->解析各个段。具体的索引想一下就知道了,这里就口胡下,首先拿到文件头中的e_shstrndx字段的值,得知下标和段表的起始位置,则由段表的起始位置+段表数组的成员大小(40 byte)*下标,可以得到段表字符串表的一些列信息,什么起始偏移,大小啊什么的,那么就可以找到段表字符串表的具体位置了,这时可以把段表字符串表拿到内存,供以后解析各个段时使用,然后再返回段表去解析各个段。
5. 链接的接口-符号
每个函数或变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。在链接中,我们将函数和变量统称为符号,函数名或变量名就是符号名。
符号概述
由于局部符号、段名、行号等对于其他目标文件来说是”不可见的”,在链接过程中无关紧要,我们只关心全局符号。我们可以使用很多命令来查看ELF文件的符号表,比如上面介绍的readelf、objdump,当然还有nm。
ELF文件中的符号表往往是文件中的一个段,段名一般叫”.symtab”.符号表的结构很简单,它是一个ELF32_sym结构的数组,每个ELF32_sym结构对应一个符号。这个数组的第一个元素,也就是下标为0的元素为无效的”未定义”符号
ELF32_sym结构定义
各字段含义
对一些字段的详解:
符号类型和绑定信息(st_info)
该成员低4位表示符号的类型,高28位表示符号绑定信息,见下表:
符号所在段(st_shndx)
如果符号定义在本目标文件中,那么这个成员表示符号所在的段在段表中的下标;但是如果符号不是定义在本目标文件中,或者对于有些特殊符号,sh_shndx的值有些特殊,见下表:
符号值(st_value)
通过上图中划红线的步骤可以找到该符号所对应的具体内容,比如,函数的话可以找到该函数所对应的具体代码处,而全局初始化变量的话,则可以找到全局初始化变量所对应的具体值。
还是以SimpleSection.o为例:
符号修饰和函数签名
对于符号修饰和函数签名问题,它是为了解决符号名冲突问题。很久以前,UNIX下的C语言就规定,C语言源代码文件中的所有全局的变量和函数经过编译以后,相对应的符号名前加上下划线”“。而Fortran语言的源代码经过编译后,所有的符号名前面加上”“,后面也加上”“。现在linux下的GCC编译器,默认去掉了在C语言符号前加”“的这种方式;但是windows平台下的编译器还保持着。VC++编译器,GCC在windows下的版本(cygwin,mingw)都会加”_”。GCC编译器可以通过参数选项”-fleading-underscore”或”-fno-leading-underscore”来打开和关闭是否在C语言符号前加上下划线。
C++符号修饰
C++都用过,强大而又复杂。他的很多机制都为符号的管理带来了很多麻烦。比如类、继承、虚机制、重载、名称空间等特性。为了支持这些特性,发明了符号修饰或符号改编的机制。
对于函数重载
给例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15int func(int);
float func(float);
class C {
int func(int);
class C2{
int func(int);
};
};
namespace N {
int func(int);
class C {
int func(int);
};
}
这段代码中有6个同名函数叫func,但他们的返回类型和参数及所在的名称空间不同。我们引入函数签名以及修饰后名称的概念。一个函数签名对应一个修饰后名称。C++的源代码编译后的目标文件中所使用的符号名是相应的函数和变量的修饰后名称。上面的6个函数的函数签名以及修饰后名称是:
下面解释下不同的函数签名是怎样修饰的。GCC的基本C++名称修饰方法如下:所有的符号都以”_Z”开头,对于嵌套的名字(在名称空间或在类里面的),后面紧跟”N”,然后是各个名称空间和类的名字,每个名字前是名字字符串长度,在以”E”结尾。比如N::C::func经过名称修饰以后就是_ZN1N1C4funcE。对于一个函数来说,他的参数列表紧跟在”E”后面。对于int类型来说,就是字母”i”。所以整个N::C::func(int)函数签名经过修饰为_ZN1N1C4funcEi。更为具体的修饰方法在这不介绍,可以去查GCC名称修饰标准。有工具可以帮我们解析被修饰过的名称,比如:1
2$ c++filt _ZN1N1C4funcEi
N::C::func(int)
这种修饰机制不光用到函数名上,他也被用到全局变量和静态变量上。但有一点需要注意,那就是变量的类型是没有被加入到修饰后名称中的,所以不论这个变量是整形还是浮点型甚至是一个全局对象,他们的名称都是一样的。
不同的编译器厂商的名称修饰方法可能不同,在Visual C++下:
以int N::C::func(int)这个函数签名来说明VC++修饰规则,其实我们都能猜出来→_→修饰名字由”?”开头,接着是函数名”@”符号结尾的函数名;后面跟着由”@”结尾的类名”C”和名称空间”N”,再一个”@”表示函数的名称空间结束,第一个”A”表示函数调用类型为”__cdecl”(函数调用类型会在以后说),接着是函数的参数类型及返回值,由”@”结束,最后由”Z”结尾。
其实,VC++的名称修饰规则并没有对外公开。MS提供了一个UNDecorateSymbolName()的API,可以将休市后名称转换成函数签名。
由于不同的编译器采用不同的名字修饰方法,必然会导致由不用编译器编译产生的目标文件无法正常相互链接,这是导致不同编译器之间不能相互操作的主要原因之一(被坑过T_T)。。。
extern “C”
弱符号与强符号
6. 调试信息
在GCC编译时加上”-g”参数编译器就会在产生的目标文件里面加上调试信息
0x03 小结
主要分析了各种目标文件的格式,对于ELF文件的代码段、数据段和BSS段没有过多阐述。详细介绍了ELF文件的文件头、段表、重定位表、字符串表、符号表、调试表等相关结构。
无路时可执行文件、目标文件还是库,他们实际上都是基于段的文件或是这种文件的集合,程序的源代码经过编译以后,按照代码和数据分别存放到相应的段中、编译器(汇编器)还会将一些辅助信息,比如符号、重定位信息等也按照表的方式存放到目标文件中,通常情况下,一个表就是一个段。
有了这些目标文件后,接下来就是如何将他们合起来,形成一个可以使用的程序或更得模块,这就是静态链接的问题,下面会在静态链接的续集介绍