剖析ELF文件格式的内容———文件头,段表,符号....(第三章)
本文介绍的是elf文件文件格式
凡是有格式就是按照一定的规则。
凡是有格式,都会有固定的软件解析他,以下是一些香瓜你的命令
比如用readelf是linux下查看elf文件格式的命令,直接输入readelf可以看帮助文档
比如用 file 命令来看具体属于什么格式的文件
比如用nm 查看 .a 中的目标文件有哪些,符号有有些
还有objdump
elf文件其实经常听到,跟编译链接息息相关,但是具体他的内容是什么呢?
首先最开始的文件头(文件头很好理解,就是一个小总结,没有文件头,你就不知道后面的是什么,小而重要。可以更快的找到需要的内容)
然后是段表的内容(段表啊,符号表啊这些也是跟编译链接中的术语)
然后是符号表段中的符号,
然后是编译器对符号的修饰,以及对符号的强弱的区分以及意义,最后介绍elf中的调试信息段。
概念比较多,看过一遍可能也会忘记,但是看了一遍以后,会对编译链接的过程更加清楚。
目录
文件头
1.readelf -h——查看文件头内容的命令
2.文件头结构体Elf32_Ehdr——定义在/usr/include/elf.h中
2.段表
1.使用readelf -S看elf文件格式的全部段表
2./usr/include/elf.h中定义的Elf32_Shdr
3.其他段(动态链接相关的段后面介绍)
3.符号表中的符号
1.使用nm 和 readelf -s 看符号表的符号
2./usr/include/elf.h中定义的Elf32_Sym
3.可执行文件中的特殊的符号
4.符号的修饰
C语言符号修饰
C++符号修饰
extern “C”
5.符号的强弱StrongSymbol WeakSymbol
强符号和弱符号
符号的强引用和弱引用
6.调试信息
7.elf中动态链接相关的段
.interp 段——指定动态链接器的位置
.dynamic段——动态链接下elf的“文件头”
符号表.dynsym段——动态链接下的“符号表”(vs:symtab是保存了所有符号)
.got段 和.got.plt段——PIC技术下的GOT
1)ELF将GOT分成了.got和.got.plt两个段
2)解析 .got.plt的内容
.plt段和.plt.got段——每个外部函数在plt中都有对应的一个项
重定位表.rel.dyn段 和 rel.plt段——分别相当于静态链接中的.rel.text和.rel.data
重定位入口类型——对比前文静态链接是的重定位:查看 编译和链接过程(第2章)
重定位过程
不用PIC模式编译的共享库
8.运行时库和启动文件相关的段
1.相关的目标文件 在/usr/lib64 或 /usr/lib下
crti.o和 crtn.o——有 .init 段和 finit段
crt1.o:
2.init 和 fini 段
3.分析最终可执行文件的init段
3.crt1.o的内容
4. __libc_csu_init函数
4.GCC平台相关目标文件
5.ctor段和dtor段
ELF的文件头
使用一个例子——elf目标文件SimpleSection.o——来说明elf的文件头内容。
.o文件是elf文件格式的目标文件,下面就以这个为例子,首先分析elf文件的文件头。
(elf文件格式的,除了目标文件,还有是可执行文件,动态链接库等。先以这个目标文件SimpleSection.o开始讲,中间会讲到其他类型。
就是elf文件格式还可以细分为 是目标文件 还是 可执行文件 还是动态链接库文件
)
首先,这个.o文件是由以下的源文件生成的:
使用如下命令生成SimpleSection.o文件:
gcc -c -m32(生成32位的)
1.readelf -h——使用这个命令来查看文件头内容
readelf是linux下查看elf文件格式的命令,直接输入readelf可以看帮助文档
readelf -h SimpleSection.o
显示出来的就是 这个目标文件的文件头的全部信息。
2.具体文件头的结构体类 Elf32_Ehdr——定义在/usr/include/elf.h中
下面这个类就是存文件头的类结构:
我们对照之前用readelf -h中显示的信息解读一下每个成员的含义:
- e_ident————就是魔数和其他信息。前4个字节是魔数,魔数之前也介绍过(https://blog.csdn.net/u012138730/article/details/90377865)————跟处理我这个文件的软件表明我是谁。
elf格式的魔数就是7f "e" "l" "f",对应 7f454c46。
这样系统就知道我是elf格式的文件了。
第5个字节是表示几位的 32位还是64位,01是32位
第6个字节是字节序,01是小端字节序
第7个字节是elf版本,一般都是01
后面9个是附加信息,一共16个,
- e_type————是类型。这里是可重定位类型,因为是目标文件,用来链接的。下面是其他类型的介绍:
可重定位文件:如上面的readelf -h中显示,显示为——REL
可执行文件类型:
共享库文件类型:
各种类型:
ps:可以是使用 file 命令来看具体属于什么格式的文件。(当然也可以用上面的 readelf -h )
ps:静态链接库,也算作是可重定文件,也就是目标文件,只不过静态链接库他是目标文件的一个打包的集合。
(怪不得之前用nm 在mac上查看 .a,是按不同的.o下进行显示的。
也怪不得还可以用其他命令剥离.a中的某个.o)
ps:共享目标文件的作用:
1)重新生成新的目标文件
2)运行时被动态链接器加载到内存运行。这里共享目标文件就是共享库。
ps:核心转储文件,崩溃的时候,系统进行现场保存,方便后续查看问题出在哪里。是不是跟windows下的 我们常说的dump文件,可以查看崩溃信息的很像,以后可以看看。
- e_machine ————elf的cpu平台属性。这里是 intel80386,只能在intel x86机器下运行。
- e_version ———— elf版本号。一般是1。
- e_entry ————入口地址。这个地址对应可重定位文件来说(就是这个目标文件来说)没啥意义,是0。但是对于elf的可执行文件来说,这个就是操作系装载的时候会把这个e_entry的值设置后续要执行进程的的地址(https://blog.csdn.net/u012138730/article/details/90377865中的讲elf可执行文件的装载 第3)中第5),比如之前的编译和链接文章中的helloworld的例子的可执行程序a.out,查看a.out的入口地址为400430
readelf -h a.out
使用objdump -d a.out正好可以看到 400430是代码段的_start函数 (静态可执行文件的入口地址就是_start函数)的起始地址 (a.out这个类型就是可执行的elf,所以e_entry 是很有用的)
ps:elf的目标文件和可执行文件在目录结构上并没有太大的区别,毕竟都是elf文件格式的。区别是内容上的,目标文件中没有包含了链接的内容,以及需要调整的符号和地址。
ps:查看a.out的格式,使用file命令:
- e_phoff :————程序头-program header的起始位置(执行视图中的segment),s_shoff:———— 段表头-section header的起始位置(链接视图中的section)。因为可重定位文件(即目标文件)没有程序头program header相关的,所以用上面的a.out(可执行文件)做举例。e_phnum和e_shnum 表示程序头和段表的个数。
我们对比readelf -h中打印出来的程序头相关 和 跟 readelf -l 打印出来的信息吻合(0x1960=6496)
- e_ehsize这个elf文件头的大小即sizeof(Elf32_Ehdr)=52,然后前提是32位,上面的a.out就是64位的,sizeof(Elf64_Ehdr)=64
- e_phentsize 程序头的大小即sizeof(Elf32_Phdr)或sizeof(Elf64_Phdr)或0,这里的.o可重定位文件都没有程序头就直接是0了。
- e_shentsize段表头的大小即sizeof(Elf32_Shdr)或sizeof(Elf64_Shdr)
- e_flags ELF标志位,标识平台相关属性
- e_shstrndx 表示段表字符串表所在的段在段表中的下标。(readelf -S 中显示的section 的第一列就是下标)
2.段表
有个疑问,回看到simplesection.o,文件头中有个s_shoff=0x330=816字节,是段表头的起始位置。而最开始的文件头大小是52个字节,也就是说文件头和段表头之间还有其他东西,其实存的正是段表头中汇总的各个段的内容。
先读elf的文件头(最开始的52个字节),从中知道段表头的位置(s_shoff=0x330=816字节),再读段表头内容,知道各个段的位置和属性
文件头只是elf的总结,段表头是elf文件的正文,段的汇总。
1.使用readelf -S 看elf文件格式的段表头
SimpleSection.c源代码编译生成的目标SimpleSection.o的段表头内容:
使用readelf -S (注意是大写的S)是全部段
我们也可以使用objdump -h 看elf文件中的关键段不是全部
ps:我们的源代码,对应存放的是在代码段(.text) 和 数据段(.data 等)。
其他的段如 :
.symtab是符号表(符号指的是函数和变量名),链接啊重定位的时候用的。
.strtab 常用的用来存符号名的字符串 .shstrtab常用的是段的名字的字符串。但是不是所有的字符串都存在字符串表里的,比如SimpleSection.c例子中的常量字符串%d\n就是存在了.rodata只读数据段中。
(注意 :下面图中是bss存放两个变量,其实看上面的readelf -S只有一个bss段的size只有4个字节,其实只存放着一个变量就是static_var2,全局未初始化的没存在bss段中)
.data段:保存的是初始化的【全局变量或(全局和局部的)静态变量】——如上图蓝色框框 以及下文使用objdump -s中的十六进制内容
.bss段: 存放的是未初始化的【全局变量或(全局和局部的)静态变量】——如上图橘黄色框框 以及下文使用objdump -s中是不存在bss段的,因为他们都是初始化为0,不需要保存,到进程的虚拟内存空间的时候就空间了,之前的目标文件和可执行文件都不占文件空间。(通过objdump -h看bss段大小的时候只有4个字节,而不是8个,因为有的编译器会把全局的未初始化的变量(global_uninit_var)不放到任何一个段中,只是留了一个符号,等到最终链接的时候再放到bss段,所以如果是可执行文件的话肯定是在bss段中有大小的。在链接之前不放是因为存在强符号弱符号的问题,后面会说道)。
使用objdump -s 将所有段的内容 以16进制方式打印出来。 可以直观的看书 .data段和 .rodata段 存的内容是什么。是变量static_var 和 global_init_var ,以及printf中的字符常量%d\n。使用objdump -d 将代码段 进行 反汇编
ps:为什么要分开存放代码段和数据段?
1)代码段一般是只读的,数据段一般是可读写的,这样权限不同的分成不同的区域,方便操作系统装载管理(可执行文件装载的时候讲过,虽然这个例子是目标文件,但是可执行文件也是这样的,也是elf格式的)。
2)可提高cpu的缓存命中率(不太了解这个)。
3)如果代码段是只读的,就可实现共享,内存中只需要存一份即可,如果多个程序需要用的话(后面dll会讲,windows下通过process explore 可以看到很多进程都是共享内存占很大的比例,提高内存利用率)
ps:源代码和汇编指令之间的对应(等以后看下汇编了以后再回来瞅瞅)
十六进制55,就是汇编指令push %ebp 的机器码,以及每条汇编指令的对应的机器码长度不一样。
2.Elf32_Shdr段表头结构体——/usr/include/elf.h中定义
插播一句这里的段表,英文是section header 不是program header
下面这类就是段表头的结构:
对应readelf -S中显示的信息解读一下每个成员的含义:
- 第一列是索引,不像头文件就一个,段表是有好多个的,这里有13个。可以看到索引0第一个行是个NULL的就是无效的段。
- 第二列是名字,对应sh_name。我可以看到sh_name的类型是Elf32_Word但是readelf -S显示出来是名字,是因为从表shstrtab中读出来了,sh_name是段名字符串在表shstrtab中的偏移。(shstrtab,也就是上文中e_shstrndx成员,现在知道为什么要存这个了吧,可以读出来是啥名)。
- 第三列是类型,对应sh_type。段的名字只是个可读的,段的类型和段的标志位才是段的本质属性(操作系统只看类型和标志位,不看名字没用),我可以挂羊头卖狗肉,叫个.text其实是数据段而不是代码段。就列举下上面有的类型如下:
SHT_NULL 无效段
SHT_PROGBITS 程序段。包括代码段和数据段。.text .data .rodata .comment .note.GNU-stack .eh_frame
SHT_SYMTAB 符号表。.symtab 。(符号表后面一节有介绍)
SHT_STRTAB 字符串表。.strtab .shstrtab
SHT_NOBITS 无内容。.bss
SHT_REL 重定位表。.rel.text .rel.eh_frame。顾名思义就是text段和eh_frame段的重定位信息。但编译器链接器判断该重定位表作用于哪个段会有sh_info和sh_link来标识的,后面会讲。
为什么会有.rel.text:
因为SimpleSection.o中有fun1函数有对printf的使用,所以在text段中有printf的绝对地址,printf不是别的目标文件中的需要链接,所以printf需要重定位,所以有.rel.text的重定位,即需要对这个符号进行重定位。而data段没有,如果有的话也会有对应的.rel.data的重定位表段的。(后面讲静态链接的时候会分析重定位表的内容)
为什么要有字符串表 ?
- 倒数第四列是标志位,对应sh_flags。表示该段在进程虚拟空间的属性。(映射到虚存空间的时候,是给操作系统看的,操作系统加载可执行文件的时候,不关心每个段的具体内容,只关心每个段的权限属性)
上面涉及标志位到的有:
A:SHF_ALLOC 要分配空间memory,在进程空间要分配空间 .text .data .bss .rodata .eh_frame
I:SHF_INFO_LINK sh_info相关的 .rel.text .rel.eh_frame 看样子跟重定位有关系
M:SHF_MERGE 可能合并 .common
S:SHF_STRINGS 包含string? .common
W:SHF_WRITE 可写 .data .bss
X:SHF_EXECINSTR 可以被执行 .text
对于一些其他段:(其中comment段不符合事实 我显示出来明明是MS,readelf -S中显示的)
- 倒数第二,三列 是与链接相关的 sh_link和sh_info,链接分成动态链接和静态链接,只要涉及到链接就涉及到符号的重定位。所以这两个对于符号表和重定位表有用,也就是 .rel.text .symtab .rel.eh_frame
看 .rel.text 的sh_link是10(符号表的下标就是10 .symtab ),sh_info是1 (作用于 .text 段,下标是1)
- 第四列和倒数第一列 跟进程的虚拟地址 sh_addr 和 对齐数sh_addralign 有关系。 sh_addr%(2^sh_addralign)= 0 。因为这个是可重定位文件所以sh_addr都是零,我们可以看下可执行文件a.out :
可以看到每个段的对齐数都是不一样的。正是因为有对齐的要求,所以段与段之间会有几个字节的空档,并不是紧凑的。
为什么要有对齐数,好像是cpu读取数据的时候有位数的要求吧,这样访问快。具体忘了。
- 第五列段偏移,对应sh_offset,如果该段存在为文件中,就表示在文件中的偏移。bss段不存在于文件中就是无意义的。
- 第六列大小,对应sh_size,段的长度
- 第七列对应sh_entsize Section Entry Size项的长度。有些段包含了固定大小的项,比如符号表,包含的每个符号所占的大小都是以一样的。如果该值为0,就是不包含这种项。下面这几个段有:
重定位表跟符号有关
符号表跟符号有关
3.其他段(动态链接相关的段后面介绍)
也可以自定义段名,让变量或者资源放入到自定义的段中。
3.符号表中的符号
上面说到段表中有个符号表 .symtab。
这个符号表什么作用呢?这个表和链接息息相关。动态链接,静态链接等等。
首先介绍一下符号的概念,符号也是我们在开发中经常接触到的名词,可以分类成:
(以目标文件位例子)
- 定义在本目标文件中的全局符号,可以被其他目标文件引用。他的值就是符号所在的地址了。
- 没有定义在本目标文件汇总,但是引用了别的里面的符号,一般叫外部符号。他的值一般都是0或者是其他。因为还没有解析到。
- 段名符号,这个符号是编译器产生的,比如.text段 data段。他的值就是该段的起始地址。
- 只在本编译单元内可见,对链接过程没有作用的,局部符号。
- 行号符号,目标文件指令与源代码中总行的对应关系。
这里我们先暂且只关心和链接相关的符号:
全局符号(可以导出给其他模块用的符号)和
外部符号(导入符号)。
1.使用nm 和 readelf -s 看符号表的符号
可以使用nm 查看符号简介
nm SimpleSection.o
第二列 符号类型 。其类型如果是小写的,则表明该符号是local的;大写则表明该符号是global(external)的。
T 该符号位于代码区text section【fun1 和 main】
U 该符号在当前文件中是未定义的,即该符号的定义在别的文件中。【printf 和 func1】 D 该符号位于初始数据段中。一般来说,分配到data section中【static_var是赋值了,local的,所以是d。 global_init_var 赋值了,全局的,所以是D】 C 该符号为common数据段。common symbol是未初始话数据段。【global_uninit_var 没有赋值,全局的,所以是C】【ps:https://blog.csdn.net/u012138730/article/details/90749833 这里搜common看解释。】 B 该符号的值出现在非初始化数据段(bss)中。【static_var2是未定义的。local的。所以是b】 额怎么有有个fun1还有个func1。func1还是U,未解析的。。
看了一下是我的源代码写错了:笔误。。
应该把main中的func1改成的fun1的
使用readelf -s看符号具体的符号信息:(objdump -t也可以)
符号表的第一个符号都是一个未定义的符号,后面的才是有意义的符号。具体含义下文分析。
其实符号就分成:函数、变量、常量(另外编译单元文件名的符号:类型是ABS)
2.符号的结构体Elf32_Sym——/usr/include/elf.h中定义的
符号表也是一个数组,元素类型就是Elf32_Sym,其成员有:
同样的,我们对应readelf -s中显示的信息解读一下每个成员的含义:
最后一列 Name——对应 st_name。st_name的值就是该符号名在字符串表(.strtab 普通字符串表)中的下标。我这里说的是st_name的值,不是Name的值。可以看到有的符号没有名字,总共有17个符号项,只有源代码中出现的8个,加一个源文件名字。其他的没有名字。
倒数第二列 Ndx—— 对应 st_shndx 表示符号所在的段(这里就要看段表索引了)。确切的说是该符号定义在本目标之内的是指示的是所在的段的下标;不在本目标文件之内,或者特殊的符号就用特殊的值
定义的源代码中的符号 下标有1 3 4, text data 和 bss 可以自己对应一下
其他的特殊的符号:UND ABS COM
倒数第三列Vis——应该对应sh_other 目前没用,在C/C++中没有用。倒数第三列好像都是DEFAULT,
倒数第四列和第五列——对应 sh_info,其中其低4位表示符号类型,高28位表示绑定信息。
绑定信息例子中只涉及到:LOCAL(局部,外部不可见)
和GLOBAL(全局,外部可见),
还有一种Week 弱符号不好理解。
(对于函数一般是外部可见的GLOBAL,但是如果是static函数就是LOCAL)
(也就是倒数第四列的值。)
符号类型(也就是倒数第五列)例子中涉及到:
NOTYPE,FILE,SECTION,OBJECT,FUNC
SECTION类型——表示该符号表示一个段,即段名。凡是SECTION类型的,最后一列就是没有值,即st_name就是空。倒数第二列,st_shndx 就是表示段的索引,例子中分别是1 3 4 5 6 7 8。【正好是除了重定位表和符号表和字符串表没有段名,估计这些符号用不到吧。】倒数第四列都是LOCAL。
NOTYPE类型——未知类型的符号,比如printf和笔误的func1,他们所在的段索引是U,因为并没有在本目标文件中定义。【也有从其他例子中看到,未知类型符号NOTYPE的,倒数第二列是具体的有具体的段索引号的,也能可能是ABS,不知道具体是什么场景下。】
第三列是符号大小,对应st_size,可以看到只有OBJECT 和 FUNC类型的有大小,即数据对象大小和函数指令所占的字节数。
第二列是符号值,对应st_value。符号值的含义是根据具体符号的类型和文件类型来区分的:
目标文件中,ndx段索引如果是FUNC类型的(fun1,main)和 OBJECT类型的,且不是COMMON的(两个static 和一个global_init),st_value就表示是在索引段的位置偏移:
目标文件中,如果是ndx段索引是COMMON值,那么st_value就是该符号的对齐属性。比如global_uninit
可执行文件中,st_value表示符号的虚拟地址。
3.可执行文件中的特殊的符号
上面分析的是目标文件的,下面看看可执行文件,a.out,
readelf -s a.out
红色框框里面 _edata表示数据段的结束地址,_start代码段的开始地址,_end整个程序的结束地址....这些符号都是ld链接器 链接的过程中生成的。
可以写个小程序试验一下,不定义这些符号,直接定义为extern的,然后打印出来
4.符号的修饰
上面说到符号,但是源代码中的符号和我们在目标文件中打印出来的符号不一定是一模一样的,比如static那两个变量。
不同的编译器 不同的平台 不同的语言 符号名字修饰的规则也都不一样,原因一是适应语言的特性,二是规则不一样就可以在源码中定义同名的符号了冲突概率小。
C语言符号修饰
现在的Linux下GCC编译器,C语言符号前已经不加下划线_,正如我们之前的看到的。
Windows平台下的编译器都加_,GCC在Windows下的版本(cygwin,mingw),会加_,Visual C++编译器也会加_
当然也有编译选项,可以控制加(-fleading-underscore)或者不加(-fno-leading-underscore)。
C++符号修饰
具体规则书中的第88页,也可以自己推理一下
1)Linux GCC 对C++的符号修饰举例
工具解析符号代表的函数签名:c++filt _ZN1NIC4funcEi
2)VisualC++对C++的符号修饰举例
Microsoft提供了一个UnDecorateSymbolName()可以将修饰后的名称转回数字签名。
extern “C”
C++为了兼容C,在符号管理上,会用extern “C”来定义这是一个C的符号。
一个有趣的小例子 MNMing.cpp:
编译生成,执行结果:
下面的代码片段就是经常用来兼容C和C++的:
#ifdef __cplusplus
extern "C" {
#endif// 写例如dll导出函数的定义
#ifdef __cplusplus}
#endif
这是一段条件编译预处理块,根据是否有定义对应的宏,来区别编译。
这段代码的具体含义就是:如果使用的C++编译器编译此函数的定义(__cplusplus是C++编译器内部定义的宏),dll函数就会包围上 extern "C" {},指明按C编译器方式进行编译。如果使用的C编译器编译这段代码,也就没有__cplusplus宏,就没有extern "C"。
所以C++调用一个C语言编写的.DLL时,在包含.DLL的头文件或声明接口函数时,应该要加上extern "C" { },指明其按C的方式进行编译命名,否则将无法找到对应的函数符号的实现,从而出现链接错误。
5.符号的强弱StrongSymbol WeakSymbol
强符号和弱符号
对于C/C++来说:
对于规则2的解读:如果是强符号和多个弱符号,那肯定选强符号,只有强符号有定义啊弱符号没有定义。其实不是,因为用过__attribute__((weak))可以把任何一个强符号定义成弱符号,所以弱符号也可能有定义的。
对于规则1,在xcode中有个duplicate symbols定义,符号重定义,就是多个目标文件中有符号定义,理论上这是两个强符号,为啥没有报错呢,难道是因为xcode中的编译器链接器不认为这是个错误?
符号的强引用和弱引用
这是针对引用类型来说的。
在C/C++中引用外部符号,对于数据,我们用extern,对于函数我们直接写一个函数的签名即可。
如果这个符号是强引用,那么找不到这个符号的定义,链接器就会报错。
如果这个符号是弱引用,那么找不到这个符号的定义,链接器不会报错,而是用一个特殊的值,或者是0。
在GCC中我们用 __attribute__((weakref)) 声明对这个符号的引用是弱引用。
6.调试信息
说回到在elf文件本身,我们可以通过-g参数,让编译的时候生成调试信息,看大小比原来的大,通过看段信息也能看到多了五六个.debug相关的段:
可以通过strip命令,去掉调试信息以及其他符号信息,可以看到比原来的还要小了
strip是binutils的一部分
strip可以清除共享库或可执行文件的所有符号和调试信息。
strip相当于 gcc -wl,-s/S或 ld -s/S ——-S清除调试符号信息,-s清除所有符号信息
ELF文件采用的是DWARF标准的调试信息格式。
Microsoft也有自己的调试信息格式标准,叫CodeView。
7.elf中动态链接相关的段
.interp 段——指定动态链接器的位置
地址是个软连接,指向位置:ld-2.17.so是Glibc库的一部分,当升级库的时候,软链接会自动修改。
.dynamic段——动态链接下elf的“文件头”
这个段指示了动态链接时用到的符号表和重定位表等。
1)/usr/include/elf.h中的elf32_dyn——类型,地址或者值
2)类型有哪些——动态链接的(符号表 ,字符串表,重定位表,哈希表)
DT_NEED类型的项如果是绝对路径,没什么好说的,直接查找
如果是相对路径,一般都是相对的,就一个文件名,那么其查找顺序是:
1)LD_LIBRARY_PATH指定的路径
2)路径缓存路径/etc/ld.so.cache指定的路径
3)在/lib,/usr/lib,/etc/ld.so.conf配置文件中指定的目录中去查找,这三个的顺序不确定谁前后,书中说的不一致
其中我的机器中/etc/ld.so.conf配置文件中指定的目录中是红色框框那两个:
实际例子中,查看dynamic段内容:(相当于一个汇总 都有什么)
readelf -d xxx.so
(可以验证下比如init段是不是地址4003c8
符号表.dynsym段——动态链接下的“符号表”(vs:symtab是保存了所有符号)
导入符号:即引用了其他模块中的符号
导出符号:本模块定义的符号,可以供其他模块调用
动态链接的模块含有两个dynsym和symtab,前者保存了所有符号包括dynsym中的。
动态链接符号表的结构和静态链接的符号表几乎一样。查看上文symtab的符号表结构内容
如果是动态链接的话-s会显示两个 dynsym以及symtab:
ps:哈希表就用
readelf -sD xxx
.got段 和.got.plt段——PIC技术下的GOT
1)ELF将GOT分成了.got和.got.plt两个段
.got段保存的是全部变量的引用的地址
.got.plt段保存的是外部函数引用的地址
2)解析 .got.plt的内容
其中前三项是有特殊含义的(下面的例子是adongtai.out是64位的,所以每一项都是8个字节):
第一项:dynamic段的地址
第二项:本共享模块的ID。现在是0。动态链接器在装载本共享模块的时候负责初始化。
第三项:动态链接器中地址解析函数的地址。 现在是0。动态链接器在装载本共享模块的时候负责初始化。
第四项:是printf@GOT项(地址为601018),初始化的值是一个plt段中的地址,00400406。装载以后第一次调用以后会解析成真正的的printf的地址。
第五项:是__libc_start_main@GOT项(地址为601020),初始化的是一个plt段中的地址,00400416。
.plt段和.plt.got段——每个外部函数在plt中都有对应的一个项
ELF使用PLT实现动态链接的延时绑定
调用函数不直接通过GOT进行跳转,而是通过一个PLT的结构进行跳转
PLT的每一项是16个字节。
其中第一项是解析函数抽象的公共的部分,三条指令内容:
1)第一条指令:压入模块ID。
2)第二条指令:跳转到动态链接器的某个函数进行地址绑定,输入参数分别是重定位表中的下标,以及模块ID——这个地址绑定函数完成符号解析以及重定位的工作,需要的两个条件是发生在哪个模块(模块ID)以及哪个函数需要进行地址绑定(重定位表中的下标)——最终把printf@GOT的真正地址给设置上,设置成真正的printf所在的地址。
后面每一项都是一个外部函数对应的plt项:比如printf@plt 和 __libc_start_main@plt,三条指令内容:
1)第一条指令:跳转到GOT对应的项目,比如printf@GOT(601018,即上面中GOT表中printf@GOT项的地址)。链接器在初始化阶段把后面一条指令的地址初始化给了printf@GOT。当装载以后在第一次调用的时候会把printf的地址填入到printf@GOT(第一项的2)第二条指令中做的)。
2)第二条指令:压入一个立即数,表示的是printf这个符号在重定位表(.rel.plt)中的下标。
3)第三条指令:跳转到第一项。
在代码段里调用的printf其实是调用到printf@plt,当第一次执行到printf@plt的时候,就会执行对象项中的第二条和第三条指令(因为第一条指令其实就是跳转到第二条指令,在链接器初始化阶段完成。),然后就会完成函数的解析,把printf@GOT的真正地址给设置上并执行内容。等到下次再调用能从第一条指令直接跳转到printf的地址了,因为printf@GOT地址已经设置成了真正的printf的地址了。
PLT的基本结构:其中GOT+4就是模块ID,GOT+8就是地址解析函数。之前说的GOT的前三项是系统占用的。
重定位表.rel.dyn段 和 rel.plt段——分别相当于静态链接中的.rel.text和.rel.data
重定位表就是指示需要对哪些地方进行地址的修正,其中:
.rel.dyn段指示的是修正.got和数据段的内容
.rel.plt段指示的是修正 got.plt段的内容
看两个重定位表的内容:readelf -r
重定位入口类型——对比前文静态链接是的重定位:查看 编译和链接过程(第2章)
动态链接相关的重定位类型
- R_386_RELATIVE:包含绝对地址的数据段中使用。基址重置(Rebasing),加上一个装载地址。
- R_386_GLOBAL_DAT(R_386_GLOB_DAT):在被修正的位置(在.got段中)中直接填入符号的地址。
- R_386_JUMP_SLOT:在被修正的位置(在.got.plt段)中直接填入符号的地址。
使用objdump -s adongtai.out查看内容进行验证。
注意:因为got.plt的前三项是系统占据(这里64位,每个项8个字节,所以601000+3*8=601018 就是printf)
重定位过程
拿printf来说。
1)查找符号,在全局符号表中找到printf的地址,printf位于GLIBC_2.2.5。
2)找到printf需要修正的位置:601018(在.got.plt段)。
3)把printf的地址填入601018(在.got.plt段)中。
不用PIC模式编译的共享库
导入函数会从.rel.plt段变到.rel.dyn段,类型也会从R_386_JUMP_SLOT变成R_386_PC32,因为不是PIC模式的话,就是不是地址无关代码了,不会有个在GOT中,不会在.got.plt中了,直接应该就是在.rel.text中进行修正位置了。
8.运行时库和启动文件相关的段
1.相关的目标文件 在/usr/lib64 或 /usr/lib下
crti.o和 crtn.o——有 .init 段和 finit段
crt1.o:
这些目标文件,最终会被链接到可执行文件中。
2.init 和 fini 段
有的目标文件有init段和finit段,那么这两个段的里面的内容是:
init段(内容是一个_init函数):在main函数执行之前需要做的事情,比如全局/静态对象构造,用户监控程序性能调试等工具的初始化。
finit段(内容是一个_finit函数):在main函数执行之后需要做的事情,比如全局/静态对象析构,用户监控程序性能调试等工具的反初始化。
在链接的时候,链接器会收集所有正常程序的目标文件的init段合并成输出文件的init段,同时需要额外的两个目标文件——crti.o中的init段作为最终输出文件的init段的开头,crtn.o中的init段作为最终输出文件的init段的结束,需要这两个特殊的目标文件来实现init段的启动(在链接时,必须保证crti.o在用户目标文件和系统库之前,crtn.o在其后,即:
ld crt1.o crti.o [usr_objects] [system_libraries] crtn.o
最终输出文件中的init段中的_init函数看上去应该是:
同理finit段。
可以看下反汇编的crti.o和crtn.o的init段(_init函数)和finit段(_finit函数):
PS:可以声明函数放在init段中:
最终输出文件中的init和dinit两个段实际上分别包含_init()和_finit()两个函数。而crti.o和crtn.o这两个目标文件中包含的代码实际上是_init()和_finit()的开头和结尾部分。当这两个目标文件和其他目标文件顺序链接起来以后,刚好行程完整的_init()和_finit()函数。
3.分析最终可执行文件的init段
实际的例子中 HelloWorld.cpp源代码是:
用gcc HelloWorld.cpp 动态链接成可执行文件a1.out,看汇编的init段:没有HelloWorld自己的东西,就是拼接的crti.o和crtn.o的init段
(第三句第四句:如果rax为0的话,zf标志位就是1,就会跳转到40041d执行,就是跳过callq 400470
Test的一个非常普遍的用法是用来测试一方寄存器是否为空:
test ecx, ecx
jz somewhere
如果ecx为零,设置ZF零标志为1,jz跳转。 (je)
)
400470处 plt.got的内容是:
(
jmpq 就是jmp 指令。q是gnu汇编的用法。q表示跳转到64位地址。
l表示32位地址。
)
其中600ff8是在 .got段中的地址,表示是引用的全局变量(现在是0,因为是动态链接,需要运行时重定位,到时候要填进去内容的):
可以查看可执行文件的动态链接的重定位入口中,有这个符号:
可以用另外一个命令:
参照上面的动态链接的内容。
所以上面那个__gmon_start__应该是跟全局构造有关的内容。不知道函数的实现是在哪个文件中
猜测__gmon_start__最终应该是去调用全局的构造函数。后面分析。
所以_init函数就是去执行了__gmon_start__。
3.crt1.o的内容
crt1.o中只有一个_start函数,里面调用了一个__libc_start_main函数(这个函数的实现应该是在类似于libc.so.6库中,或其他版本的库中,或静态库中),会把main函数也传进去。
crt1.o虽然本身不包含init段和finit段,但是支持init段和finit段的启动代码,可以看到他向libc的启动函数__libc_start_main除了传递main函数,还传递了__libc_csu_init和__libc_csu_fini 两个函数指针——用的是寄存器存参数的方式。_libc_csu_init和__libc_csu_fini 两个函数函数的实现里面会调用到 _init()和_finit()函数(一个在main之前的初始化工作,一个在main结束后的收尾工作,而_init()和_finit()函数的内容就是各个目标文件的.init段和.finit段)
- __libc_csu_init
不知道定义在哪个目标文件中,但在最后可执行文件中就有了)
- __libc_csu_fini
不知道定义在哪个目标文件中,但在最后可执行文件中就有了)
- main
即我们自己写的代码的入口函数了
- __libc_start_main(定义在libc.so.6中)
__libc_csu_init,__libc_csu_fini,main 都是被传入 __libc_start_main函数的参数,我们在入口函数的文章中介绍过__libc_start_main的部分实现内容,但是没有细说对于传入的__libc_csu_init __libc_csu_fini 的操作。
4. __libc_csu_init函数
在之前的HelloWorld的例子中可执行程序的输出中看到__libc_csu_init函数的实现(虽然不知道是哪个目标文件中合并过来的)
调用_init函数,会调用到__gmon_start__追踪不下去了。
而__frame_dummy_init_array_entry 和 __init_arry_endf分别又是什么?
地址为600e08和600e18,如下可以看到就是.init_arry段的内容
那么这两个地址 00400540 和 004005b5分别又是什么呢?
004005b5指向_GLOBAL__sub_I_Hw重要的应该是这个,这个函数会在main之前被调用,虽然不知道具体流程,但是肯定是在__libc_csu_init这个函数中被用到的。
每个全局对象应该都会生成类似的函数,然后再init_array段中占一个地址指向这个函数。
而这个函数做的就是调用构造函数和注册析构函数(4005e8正好是此析构函数的地址)(到时候main返回后,执行全局析构函数。先注册后调用,正好和构造函数相反的顺序)
4.GCC平台相关目标文件
C++这样的语言是和编译器密切相关的,GCC是C++的真正实现者,实现C++的全局构造和析构。
其中gcc下相关的文件如下:
ctrbeginT.o 和 crtend.o:全局的构造和析构的实现
libgcc.a:libgcc_s.so:帮助实现不同平台之间的计算,整数运算,浮点数运算(不同的cpu对浮点数的运算方法很不相同)等
libgcc_eh.a:支持C++的异常处理的平台相关的函数
5.ctor段和dtor段
Windows
1.obj文件查看
可以使用dumpbin.exe 查看obj文件
dumpbin是在Windows平台下用于显示COFF格式文件信息的一个命令行工具。你可以使用DUMPBIN去显示COFF格式的文件信息,比如像vc编译器生成的目标文件(obj),可执行文件(exe)和动态链接库(DLLs)等。
1.使用/summary选项,或者不输入任何选项
显示:每个段的基本信息(大小+段名)。
这里.obj 是 COFF OBJECT
另外还有:
.lib 是 LIBRARY
.dll 是 DLL
.exe 是 EXECUTABLE IMAGE
2.使用/headers 选项
所有节(SECTION)的描述结构,即节头
先显示一个汇总,
再分别显示各个section, SECTION HEADER #1 到 SECTION HEADER #B,11个
最后再显示上面1中显示的summary
3.使用/section:段名 查看具体的段的内容
/summary 中显示出来的段名
bss段存放的是未初始化的全局变量或(全局和局部的)静态变量。
data段存放的是初始化的全局变量或(全局和局部的)静态变量。
显示各个段的一些信息
:预留空间,cpp中分别定义了一个初始化的和未初始化的全局变量int,分别在data段和bss段,均占用4个字节。
:读写权限,bss和data段都是可读可写的段
:字节对齐align,影响预留空间 size of raw data,如果int int char 就是9,如果是int char int 就是12。
关于const变量属于只读。
const全局变量存储在只读数据段,编译期最初将其保存在符号表中。当运行时第一次使用时为其分配内存,在程序结束时释放。
const局部变量存储在栈中,栈区也是运行时才有的。当运行时第一次使用时为其分配内存,代码块结束时释放。
vs
只有存在于data段全局变量和静态变量,在编译期就分配空间。(bss段编译器不分配空间,的file pointer to raw data 是 空,bss段只是有个占位符,运行的时候系统自动都初始化为0)
4.使用/symbols 查看符号表
使用/symbols 查看符号表,符号就是通常指定义出的函数,全局变量。
源文件的全局符号 (global symbol) 分成强 (strong) 和弱 (weak) 两类传给汇编器
汇编器则将强弱信息编码并保存在目标文件的符号表中
编译器认为函数与初始化了的全局变量都是强符号,而未初始化的全局变量则成了弱符号
DIRECTIVES段
编译器传递给链接器的信息的段 obj文件中的段:DIRECTIVES段 中指示了需要链接什么库。
2.调试信息格式
- (没有)
- 不创建调试信息
- 编译时间更快
- / Z7
- 使用CodeView格式在.obj文件中生成完整的符号调试信息
- /Zi
- 使用程序数据库格式在目标的.pdb文件中生成完整的符号调试信息。
- 支持最小重建(/ Gm),这可以减少重新编译所需的时间。
- / ZI
- 除了支持Edit-and-Continue之外,生成像/ Zi这样的调试信息
剖析ELF文件格式的内容———文件头,段表,符号....(第三章)相关推荐
- SWF文件格式说明书--SWF文件头
SWF文件头 字段 类型 备注 标识 8位 标识字节: F代表未压缩 C代表已压缩(SWF6以后的版本特有) 标识 8位 代表W 标识 8位 代表S 版本号 8位 代表SWF文件的版本,比如0x06代 ...
- elf文件格式实例解析
试验环境:archlinux 速龙3000+(即x86兼容32位处理器) 必须软件:gcc binutils 参考资料: System V application binary interface E ...
- 手拆ELF32(一,文件头)
ELF详细指南 持续更新中- 拆解的文件放在百度云盘 百度云盘提取文件 elf.h中的宏定义 下列数据类型使用N比特架构 (N=32,64, ElfN 代表 Elf32 或 Elf64, uintN_ ...
- 一、各种WAV文件头格式
Wav文件也分好几个种类,相应的非数据信息存储在文件头部分,以下是各种WAV文件头格式. 表1 8KHz采样.16比特量化的线性PCM语音信号的WAVE文件头格式表(共44字节) 偏移地址 字节数 数 ...
- Linux里gcc编译过程分析和ELF文件格式学习
GCC编译器背后的故事及常用命令.了解ELF文件格式 前言 一.GCC简介 二.GCC背后的战友 1.Binutils 2.C运行库 三.GCC编译流程及对应命令 1.编译流程图及命令框图 2.实践操 ...
- Ubuntu18.04系统下,gcc编译过程分析、命令参数介绍及ELF文件格式学习
GCC编译器背后的故事及常用命令.了解ELF文件格式 文章目录 GCC编译器背后的故事及常用命令.了解ELF文件格式 前言 一.GCC简介 二.GCC背后的战友 1.Binutils 2.C运行库 三 ...
- 链接装载与库:第三章——目标文件里有什么(ELF文件结构)
文章目录 一.ELF文件的格式 二.ELF文件是什么样的 三.挖掘SimpleSection.o 3.1 代码段 3.2 数据段和只读数据段 3.3 BSS段 3.4 其他段 3.5 自定义段 四.E ...
- 【Android 逆向】ELF 文件格式 ( ELF 文件头 | ELF 文件头标志 | ELF 文件位数 | ELF 文件大小端格式 )
文章目录 一.ELF 文件简介 二.ELF 文件头 三.ELF 文件头标志 四.ELF 文件位数 五.ELF 文件大小端格式 一.ELF 文件简介 在上一篇博客 [Android 逆向]ELF 文件格 ...
- 【Android 逆向】ELF 文件格式 ( ELF 文件当前版本号 | 操作系统 ABI 信息 | ABI 版本 | 文件头校验 | 文件头长度信息 )
文章目录 一.ELF 文件当前版本号 二.操作系统 ABI 信息 三.ABI 版本 四.文件头校验 五.文件头长度信息 总结 一.ELF 文件当前版本号 ELF 文件头第 6 字节 : 版本信息 ; ...
最新文章
- 中国AI创业公司霸榜NeurIPS-AutoDL竞赛,代码已开源
- linux平台 一个简单的helloworld静态库的制作与使用
- Python基础教程:条件语句的七种写法
- 惠普大佬:未来30年四大趋势将推动科技产业发展
- 在WebPart中上传图片到SharePoint图片库,读取Exif信息到图片的自定义属性
- 征稿 | “健康知识图谱”投稿通道开启
- C++:构造函数重载类内定义函数(内联函数)
- [ActionScript 3.0] 获取TextFiled字符边框
- 雷军的手机屏保亮了,网友哭笑不得:我还以为是董明珠呢!
- 秋季海报设计元素|水彩手绘纹理植物素材,从人群中脱颖而出。
- FOUND MODULE 所在的表及刪除不啟作用的INCLUDE
- cast函数 oracle 日期_从 Oracle 到 PostgreSQL ,某保险公司迁移实践
- java工程师什么城市就业_热门城市的Java薪资情况
- WEB小项目-账务管理系统(2020年03月24日更新,附数据库和源码包)
- revit2019 导出obj_Revit导出OBJ格式
- 图形验证码文字识别——pytesseract
- java反应器构型_27种反应器的结构及原理,你想了解的都在这里
- 【HDU 5956】The Elder(树上斜率DP)
- 幽默的最高境界——这才叫幽默
- 押宝ACE平台 北电自救或转身服务型公司
热门文章
- 【Linux】嵌入式·NAND Flash
- 机器学习小组知识点7:伯努利分布(Bernouli Distribution)
- C#读取Excel数据的几种方式(包含大量数据读取)
- 什么是封装java_什么是封装java
- Ubuntu系统开机黑屏,且左上方存在不停闪烁的横杠
- java毕业设计在线家教预约系统Mybatis+系统+数据库+调试部署
- delphi 取屏幕分辨率_使用Delphi更改Windows屏幕分辨率的更新
- Android编译问题:java.util.zip.ZipException:duplicate entry...
- 太阳直射点纬度计算公式_高中地理——每日讲1题(太阳直射点、太阳高度角、极昼、极夜)...
- CH343PT库使用<二>USB转串口设备描述符配置