前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >探索ELF可执行文件的“干货”:段头表和段的基本介绍

探索ELF可执行文件的“干货”:段头表和段的基本介绍

作者头像
望月从良
发布2020-11-13 12:06:51
1.4K0
发布2020-11-13 12:06:51
举报
文章被收录于专栏:Coding迪斯尼Coding迪斯尼

可执行文件的数据结构通常都很复杂和繁琐。原因在于程序在加载到内存中执行时需要经过一系列非常复杂的步骤。例如要计算数据或代码被加载到虚拟内存时的位置,计算重定向数值,实现不同代码的链接等。

本节我们一点一滴的了解段的数据格式和作用,这对我们后面了解Linux系统如何加载运行程序,并掌握相关的高级hack技术有非常重要的作用,首先我们看段头对应的数据结构,它用于描述ELF文件中某个段的基本特征:

代码语言:javascript
复制
typedef  struct {
uint32_t sh_name; #段名
uint32_t sh_type; #段的类型
uint64_t sh_flags; #段标志位
uint64_t sh_addr; #段被加载到内存中的位置
uint64_t sh_offset; #段对应数据在ELF文件中的偏移
uint64_t sh_size; #段的大小
uint64_t sh_link; #与该段有关系的其他段对应的段头在段头表中的下标
uint32_t sh_info;  #与该段有关的信息
uint64_t sh_addralign;  #段是否需要字节对齐、
uint64_t sh_entsize; #如果段含有表结构,该字段对应表中每一项的大小
}Elf64_Shdr;

用于描述段性质的段头数据结构以表格的方式存储在ELF文件中。表中每一项就对应上面描述的数据结构。该结构中的第一个字段用于指向段的名称。名称当然得对应字符串,但sh_name却是一个四字节整形,那是因为所有字符串都被存储在字符串段中,这个段里面的数据全是以0结尾的字符串组成,sh_name表示在字符串段里,第几个字符串对应当前段的名称。

字段sh_type用于描述段的类型,某些特定类型的段对应特定的数据结构,操作系统的加载器通过解读该字段,了解段的类型后就可以知道如何读取段的内容。常见的段类型有SHT_PROGBITS,这种段包含机器指令或者常量数据,它就是一堆数据的集合,因此没有特定结构。

SHT_SYMTAB表示静态符号表,SHT_DYNSYM表示动态符号表,这些段有特定的数据结构,他们会被调试器或连接器读取。SHT_REL或SHT_RELA表示该段用于重定向,链接器需要深入读取这些段的信息。

sh_flag字段用于描述段的属性。如果他的值位SHF_WRITE,表示该段的内容在程序运行时可以被修改,SHF_ALLOC表示该段的数据在程序运行时动态加载到内存中,SHF_EXEINSTR表示该段包含了可以被执行的机器指令。其他字段在后面需要时再了解,下面我们看段的内容和作用。

使用命令readelf —section —wide a.out我们可以得到如下内容:

上图展现可执行文件各个段的信息,其中若干段需要我们多了解。我们看.init和.fini这两段,其类型为PROGBITS,这表明这两个段的内容为可执行指令。.init段包含了程序在执行前所需要的初始化操作,使用C语言编程时入口是main,这部分代码就是main在执行前所需要运行的指令。当程序运行结束后,.fini中对应的代码会被执行已完成资源回收等操作。

很重要的一个段就是.text,它包含了我们编写的代码编译成的二进制指令都在该段中。如果代码使用gcc编译,那么编译器在编译代码时会自动插入一些通用函数,例如_start, reigser_tm_clones等,可以使用命令将这些指令反编译出来:objdump -M intel -d a.out:

上图就是_start函数对应的代码指令。顺序看下来有一条语句mov rdi, 400526,后面数值就是main函数的入口地址,把该地址放入rdi寄存器目的是为了调用main函数时传递调用参数,接下来语句call libc_start_main@plt,该函数属于plt段,是一个链接库里面的函数,在这个函数里面会使用call指令调用我们所写的main函数,后面我们会详细研究这些函数。

任何存储代码指令的段都不可写,类似的段还有.rodata,它用来存储只读数据。.data段用于存储程序默认初始化数据,因此它可写,也就是里面的数据可以修改。.bss段用于存储那些没有在代码中初始化的变量,在程序加载后,系统会为该段内的数据分配内存。

系统在加载ELF文件执行时,代码中有不少函数对应的调用地址还没有确定。例如常用的C函数像memset等,这些函数由于位于共享链接库中,因此他们对应的虚拟地址编译器不知道,这就需要系统在代码调用这些函数时才去确定他们的具体地址,这种技术也叫有延迟绑定。

ELF文件中帮助系统进行延迟绑定的有两个段分别为.plt和.got。我们使用如下命令来反编译.glt以便看看它的结构和内容: objdump -M intel —section .plt -d a.out

上图中我们可以看到puts,它是linux系统中常用的将信息输出到控制台的函数。如果我们在代码中调用puts函数时,实际上编译器会先调用上图里面的puts@plt这部分的指令。我们看上图中第一段里面的指令jumpq *0x200c12(%rip),它的意思是将数值0x200c12加上rip寄存器中的值,rip存储当前指令下一条指令的地址,也就是400406,相加后结果为0x601018,然后读取所在地址的数值,我们通过指令objdump -M intel -section .got.plt -d a.out来看对应地址的数值:

可以看到对应地址的数值是06,于是将当前地址加上06得到下一条指令的地址,也就是400406,这其实就是jumpq语句下一条指令的地址,也就是代码绕了一个大弯后回到了下面指令,然后执行指令push 0,将数值0压到堆栈上,接着jumppq跳转到地址0x4003f0也就是最上面代码的入口。然后又执行指令pushq 0x200c12(%rip),这个地址实际落在段.got里面,然后又执行语句jump *0x200c14(%rip),后面对应的地址其实也在.got段里。

当这些代码执行时,动态链接库就会修改0x200c12(%rip)地址存储的数值,使得该数值加上jumpq指令所在地址后指向puts函数的虚拟地址,于是在真正执行指令jumpq *0x200c12(%rip)时就不会绕个大弯转回下面的push指令,而是直接跳转到puts函数,这个过程很繁琐,下一节我们再看为何要使用.got段来实现动态链接。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Coding迪斯尼 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档