电脑疯子技术论坛|电脑极客社区

微信扫一扫 分享朋友圈

已有 764 人浏览分享

ELF文件基本结构格式

[复制链接]
764 0
本帖最后由 zhaorong 于 2022-12-8 15:57 编辑



可执行程序/共享库文件的产生过程

QQ截图20221208151952.png


现代操作系统如何装载可执行文件

给进程分配独立的虚拟地址空间
建立虚拟地址空间和可执行文件之间的映射关系
把CPU指令寄存器设成可执行文件的入口地址,启动执行

总体构成

QQ截图20221208152038.png

一共由 4部分组成,自上而下分别为
ELF 头部
程序头表
节(区段)
节头表(区段表)

ELF文件的种类

哪些文件的结构是 ELF 文件结构

ELF 的全名为Executable and Linkable Fromat可链接可执行文件格式,所以以下文件为 ELF 文件格式

可执行文件
.o--- 目标文件(可重定位文件)
.so--- 共享文件
他们之间的关系为:
目标文件 被 链接器链接为 可执行文件/共享文件,之后再由 加载器将其读进内存并执行

ELF的双重特性

ELF 格式具有不寻常的双重特性

编译器/汇编器/链接器

将文件看作ELF header、Sections以及Section header table的集合

加载器

系统加载器将文件看作ELF header、Program header table、Segment的集合
其中 一个Segment通常由多个Section(节)组成
比如:一个 "可加载只读"段可以由可执行代码(.text) Section、只读数据(.rodata) Section
和动态链接器需要的符号组成

QQ截图20221208152601.png

节(section)是从链接器的视角来看ELF文件,而 段(segment)是从执行的视角来看ELF文件也
就是它会被映射到内存中

ELF 四部分介绍

基础程序

共享库 libmymath.c -> libmymath.so

int fnAdd(int a,int b)
{
    return a + b;
}

目标文件 main.c -> main.o

#include <stdio.h>
extern int fnAdd(int a,int b)

int main()
{
    int i = 1;
    int j = 2;
    int k = fnAdd(i,j);

    fprintf(stdout,"k = %d \n",k);
}

8889.png

这里由于可能需要安装32位的依赖,所以直接以一个简单shell的命令集合给出

#!/bin/bash
sudo apt install gcc-multilib # 由于当前的gcc是64位,为了支持编译32位的文件需要安装依赖
gcc -m32 -no-pie --shared libmymath.c -o libmymath.so
gcc -m32 -no-pie main.c -o main ./libmymath.so

通过gcc -v可以在编译时会默认加上-enable-default-pie所以要关闭该选项
由于我们主要的目的是观察ELF文件的结构,所以 静态链接/动态链接都可以 无所谓 为了方
便观察地址,关闭位置无关PIE

QQ截图20221208152821.png

ELF header(ELF 头部)

首先先来看位于/usr/include/elf.h中的定义

此处主要关注 32位下 ELF header,64位下结构基本一致

8888.png

作用

ELF header的主要作用有如下几点:
表明文件格式(告诉读取该文件的程序这是一个 ELF 文件)
记录一些该文件与环境的基本信息
文件类型
机器架构
版本
程序的入口地址
Program header table的起始地址与其结构表项的基本信息
Section header table的起始地址与其结构表项的基本信息

观察 ELF header

readelf -h main

3516.png

如图所示便是ELF header的全部信息
先来看ELF header的大小,如图中第15行的信息,ELF header的总大小为52 bytes
也就是该文件的前52字节,16进制下显示为:

1616.png

od -Ax -t x1 -N 52 main

各字段含义

由于 freebuf 对 markdow n的支持有亿点点差,所以下面可能不美观,谅解谅解。。。

第一行 : 前16字节

7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
7f—文件标识
45 4c 46—ELF 三个字母的ASCII数值
01 File class byte index—文件类型位
00—非法
01—32位目标
02—64位目标
01 Data encoding byte index—数据编码位
00—非法
01—小端格式
02—大端格式
01 File version byte index—文件版本
00 OS ABI identification—ELF 文件操作系统的二进制接口的版本标识符
00 UNIX System V ABI
00 ABI version—ELF 文件的 ABI 版本。该位置一般值为零
00 Byte index of padding bytes—标识从该字节开始后面的内容都是用于填充的
为什么ELF header第一个字段占16字节,回看之前对于ELF header可以发现其起一个字段为一个
unsigned char e_ident[EI_NIDENT]数组,而其中的EI_NIDENT正是在该结构体上面定义的宏#d
efine EI_NIDENT (16),所以第一个字段就占16字节了

第二行: 第16 ~ 31字节

02 00 03 00 01 00 00 00 70 90 04 08 34 00 00 00
02 00 e_type—这是一个可执行文件
03 00 e_machine—机器架构位intel 80386
01 00 00 00—e_version—当前版本
01—非法 ELF 版本
02—当前版本
70 90 04 08—e_entry—程序入口地址0x08049070
34 00 00 00 e_phoff—Program header table的偏移地址(在该ELF文件中)
0x34 = 52 字节,由于 ELF header 的大小正好是52字节,所以说明紧接着 ELF header 的就是
Program header table

第三行:第32 ~ 49 字节(附带第四行的前两字节)

80 35 00 00 00 00 00 00 34 00 20 00 0b 00 28 00 1d 00
80 35 00 00—e_shoff—Section header table的偏移地址(在该ELF文件中)
0x3580 = 13696字节
00 00 00 00—e_flags—处理相关标志
34 00—e_ehsize—ELF header的长度 52字节
20 00—e_phentsize—Program header table中每个表项Entry的长度
0b 00—e_phnum—Program header table表中,一共有多少个Entry
一个Program header table表项长度为32字节(0x20)一共有11(0x0b)个表项 所以一共占32 * 11= 352字节
28 00—e_shentsize—Section header table中,每个表项Entry的长度
1d 00—e_shnum—Section header table表中,一共有多少个Entry
一个Section header table表项的长度为40字节(0x28)所以一共有29个表项,所以一共占 29 * 40 = 1160字节

第四行:ELF header 的最后两字节

1c 00—e_shstrndx字符传表表项在Section header table中的索引

此处为第28(0x1c)个表项,也就是Section中的倒数第二个表项

总结

经过上面每个字段的介绍,会觉得哇塞好复杂好麻烦,但其实只要理解每个字段的含义即可
在实际中需要我们一个字节一个字节看ELF header头的场景很少,通过readelf -h看即可其
中也会显示每个字段的含义,何乐而不为捏~~~

利用 Section header table 查看 Section 的内容

字符串表表项

就从字符串位于ELF文件末尾的字符串表项说起
为什么要单独建立该表项而且在ELF header中专门给该表象留一个位置出来?
这些字符串通常是符号的名字或者节的名字当ELF文件的其它部分需要引用某个字符串时
只需要提供该字符串在字符串表中的序号即可

下面我们来算出.shstrtab在文件中的位置,并通过od命令来查看他

首先查看ELF文件中所有Section的信息

1613.png

可以看出,.shstrtab的偏移地址为0x00347d(13437)表项的大小为0x000101(257)字节
那么该信息是如何计算出来的呢?
首先由刚刚 ELF header 的信息可以得知 Section header table 的起始偏移地址为13696字节
每个表项的大小为40字节,所以对于处于第28个表项的.shstrtab来说其偏移就是:
13696 + 28 * 40 = 14816
这个就是.shstrtab在Section header table的位置,现在来查看这个表项:
od -Ad -t x1 -j 14816 -N 40 main

1612.png

对照参考.shstrtab表项的结构体

typedef struct
{
  Elf32_Word    sh_name;                /* Section name (string tbl index) */
  Elf32_Word    sh_type;                /* Section type */
  Elf32_Word    sh_flags;               /* Section flags */
  Elf32_Addr    sh_addr;                /* Section virtual addr at execution */
  Elf32_Off     sh_offset;              /* Section file offset */
  Elf32_Word    sh_size;                /* Section size in bytes */
  Elf32_Word    sh_link;                /* Link to another section */
  Elf32_Word    sh_info;                /* Additional section information */
  Elf32_Word    sh_addralign;           /* Section alignment */
  Elf32_Word    sh_entsize;             /* Entry size if section holds table */
} Elf32_Shdr;

可以看出其偏移为:0x0000347d大小为0x000101正好对应刚刚readelf读出的内容
其中绿色方框圈出的 0x00000011 便是该字符串(.shstrtab这个字符串)在其所指
向的字符串表中的偏移地址
用这个偏移地址和大小来查看 字符串表

1611.png

放入文件中,利用 vim 将第一列删除,并去掉其空格后

%s/ //g

1610.png

可以看出第17(0x11)字节开始正是.shstrtab字符串

总结

利用相同的方法我们便可以算出任意Section的位置并查看其中的内容

利用 Program header table 查看 Segment 的内容

利用readelf -l查看Program header table

1609.png

上图中说的有点小问题(下面那段话是 R W 权限,少写了一个…..)

这表明Program header table将多个Sections分为了11个段 有些段不止有一个Section

其中LOAD表示可加载的段,程序执行时会由加载器将其加载进行内存

1608.png

为什么32位下程序的默认入口为0x0804800

可能是因为它从System V i386 ABI借用了该地址

为什么System V使用0x08048000?

因为,通过将text段放置在该地址和它正下方(但在0x08000000上方)的堆栈,一个进程可以只使
用一个二级页表。换句话说,您必须选择一个默认地址,并且该地址提供了潜在的性能优势 因为最
小化页表占用空间意味着优化TLB命中率。

0x08048000下面有什么吗?堆栈可能在它下面,尽管很可能什么都没有。那个地址只有128M。

总结

至此ELF文件基本结构就介绍完毕,至于其中各个段的作用,将在后面的静态/动态链接过程的
介绍中继续说明其用意。

您需要登录后才可以回帖 登录 | 注册

本版积分规则

1

关注

0

粉丝

9021

主题
精彩推荐
热门资讯
网友晒图
图文推荐

Powered by Pcgho! X3.4

© 2008-2022 Pcgho Inc.