初学ELF的一些见解

我自己学习一些知识的时候,首先就是学习一些框架性的东西,先知道这个东西是什么就够了,在用到的时候有个方向性的认知,配合现在的AI进行查询便能很快知道这个东西的使用方式。
这个文章简单写一下对于ELF的一些了解,是个方向性的概念,当然,最后也会附上一些相关资料供大家参考。

简单概述

ELF = Executable and Linkable Format,可执行连接格式,是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的。扩展名为elf。

其主要有三种主要类型:

  • 适于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。
  • 适于执行的可执行文件(executable file),用于提供程序的进程映像,加载的内存执行。
  • 共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。

为了方便和高效,ELF文件内容有两个平行的视角:
一个是程序连接角度,另一个是程序运行角度。
elf

ELF header在文件开始处描述了整个文件的组织
Section提供了目标文件的各项信息(如指令、数据、符号表、重定位信息等)
Program header table指出怎样创建进程映像,含有每个program header的入口
Section header table包含每一个section的入口,给出名字、大小等信息。

首先明确Segment和section间的区别,Program Head中的内容叫做Segment,它是由多个Section组成;Program Head中描述的是程序运行起来后的内存视图,而ELF Head和Section Head是文件视图,一个是动态的,一个是静态的。

ELF中的段(Segment)是按照内存视角对节(Section)进行重组的结果,用于运行时加载。

换句话说:

  • 节(Section) 是链接视角的划分,针对编译和链接过程;
  • 段(Segment) 是加载视角的划分,针对运行时内存布局;

操作系统加载器会按照段将相关节映射到内存中,比如把所有代码节(如 .text)放进一个可执行、可读的段,把所有数据节(如 .data.bss)放进一个可读写段。

总结一句话:段是运行时内存布局的单位,是把相关节“打包”后供操作系统加载用的。

接下来给大家看看ELF header、Program header table、Program header table和Section header table的细节样貌,有了上述的总体认知,接下来的知识更希望在大家用到的时候仔细查询,这里只是给大家一个目录。

ELF header

查看命令:readelf –h test

#define EI_NIDENT (16)
 
typedef struct
{
    unsigned char e_ident[EI_NIDENT];     /* Magic number and other info */
    Elf32_Half    e_type;                 /* Object file type */
    Elf32_Half    e_machine;              /* Architecture */
    Elf32_Word    e_version;              /* Object file version */
    Elf32_Addr    e_entry;                /* Entry point virtual address */
    Elf32_Off     e_phoff;                /* Program header table file offset */
    Elf32_Off     e_shoff;                /* Section header table file offset */
    Elf32_Word    e_flags;                /* Processor-specific flags */
    Elf32_Half    e_ehsize;               /* ELF header size in bytes */
    Elf32_Half    e_phentsize;            /* Program header table entry size */
    Elf32_Half    e_phnum;                /* Program header table entry count */
    Elf32_Half    e_shentsize;            /* Section header table entry size */
    Elf32_Half    e_shnum;                /* Section header table entry count */
    Elf32_Half    e_shstrndx;             /* Section header string table index */
} Elf32_Ehdr;

E_ident[EI_NIDENT]是一个数组,它有16个字节,这16个字节被分为好几个部分,分别代表不同的涵义:
(1) e_ident[0]e_ident[3]:这四个字节被称为“魔数“(Magic),它们分别是“0x7F“,”E“,”L“,”F“的ASCII码,对所有ELF格式的文件来说,这四个字节是固定的。
(2) e_ident[4]:这个字节给出了这个ELF文件是多少位的:
当取“0x1”时,代表这个ELF文件是32位的;
当取”0x2”时,代表这个ELF文件是64位的。
(3)e_ident[5]:这个字节给出了这个ELF文件使用的字节顺序:
当取”0x1”时,代表小端序;
当取”0x2”时,代表大端序。
(4)e_ident[6]:这个字节给出了这个ELF文件头的版本,当前只有一个版本,所以这个字节目前只能取”0x1”。
(5)e_ident[7]e_ident[15]:这九个字节目前还没有实际意义,一般为0,也有的文件会用它们做一些特殊的标记。
e_type表示文件类型。常见的:
1(可重定位文件:“.o 文件”);
2(可执行文件);
3(共享库文件:“.so 文件”)
e_machine指定该程序在什么平台上使用,比如 EM_386 表示在 Inter x86 机器上使用。
e_version表示ELF 文件版本号
e_entry表示程序的入口虚拟地址。对于可执行文件来说,当 ELF 文件加载完成后,将从这个地址开始执行。对于其它文件,该值为 0。
e_phoff指出 Program Header Table的文件偏移,没有则为 0。
e_phentsize表示 Program Header Table 中的每一项的字节大小(字节数表示)。
e_phnum给出program header表中共有多少项。
e_shoffe_shentsizee_shnum 分别表示section header表的文件偏移,表中每一项的字节大小和表中共有多少项。
e_flags给出与处理器相关的标志。
e_ehsize给出ELF文件头的长度(字节数表示)。
e_shstrndx表示section名表的位置,指出在section header表中的索引存储“节名字表”的 Section(就是 .shstrtab 节)所对应的索引。。

Program header table

查看命令:readelf –l test

 typedef struct
{
    Elf32_Word  p_type;          /* Segment type */
    Elf32_Off   p_offset;        /* Segment file offset */
    Elf32_Addr  p_vaddr;         /* Segment virtual address */
    Elf32_Addr  p_paddr;         /* Segment physical address */
    Elf32_Word  p_filesz;        /* Segment size in file */
    Elf32_Word  p_memsz;         /* Segment size in memory */
    Elf32_Word  p_flags;         /* Segment flags */
    Elf32_Word  p_align;         /* Segment alignment */
}Elf32_Phdr;

1)p_type:段类型,如:PT_LOAD (可加载段)、PT_DYNAMIC (动态段);只有类型为PT_LOAD的Segment才能够被加载到内存中。

{
    PT_NULL =0,
    PT_LOAD =1,
    PT_DYNAMIC =2,
    PT_INTERP =3,
    PT_NOTE =4,
    PT_SHLIB =5,
    PT_PHDR =6,
    PT_TLS =7,
    PT_LOOS =0x60000000,
    PT_HIOS =0x6fffffff,
    PT_LOPROC =0x70000000,
    PT_HIPROC =0x7fffffff,
}; 

2)p_offset:段数据的第一个字节相对于文件开头的偏移量。

3)p_vaddr:运行起来后,这个Segment在内存中的虚拟地址(只是一个偏移);

4)p_paddr:暂时不用关心,在没有MMU的系统中,这个表示Segment运行的物理地址;

5)p_filesz:段数据在文件中的的字节大小,可以是 0。

6)p_memsz:段数据在内存映像中的字节大小,可以是 0。

7)p_flags:段内存映像的访问权限:PF_X (可执行)、PF_W (可写)、PF_R (可读);

enum
{
    PF_X =1,
    PF_W =2,
    PF_R =4,
    PF_WX =3,
    FP_RX =5,
    PF_RW =6,
    PF_RWX =7,
}; 

8)p_align:如果为 0 或 1,表示不需要对齐。否则,p_align 应该是 2 的正整数幂,p_vaddr 和 p_offset 在对 p_align 取模后应相等。

Section header table

查看命令:readelf -S test

section header用来描述每个section的特性,如大小、类型、名称等等,它们都使用以下的数据结构来表示:

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;

1)sh_name:给出了节的名称,节名是一个字符串,保存在.shstrtab节中。sh_ name的值是这个字符串在字符串表.shstrtab中的偏移量

2)sh_type 用于指明 section 的类型。

3)sh_flags:给出了节在虚拟进程空间的属性,如是否可写,是否可执行等等:

4)sh_addr:这个成员给出了节的虚拟地址。如果该节可以被加载,则这个数值代表该节被加载后在进程地址空间中的虚拟地址;否则为0

5)sh_offset:这个成员给出了节在ELF文件中的偏移量。如果该节存在于ELF文件中,则这个数值代表节在ELF文件中的偏移量;否则无意义。比如.bss节的sh_offset成员就没有意义

6)sh_link 和sh_info:这两个成员是节的链接信息,如果节的类型是与链接相关的(不论是动态链接还是静态链接)

Section

.bss:保存未初始化的数据,比如那些未初始化的全局变量。因为是“未初始化”,所以也没必要在文件中占用任何空间去记录其初始值(所以类型为 SHT_NOBITS)。在程序开始运行时,系统会将 .bss 映射的内存区域清零。

.comment:保存版本控制信息。

.data / .data1:保存已初始化的数据。它们会在文件中占用存储空间,这与 .bss 不同。

.debug:保存调试相关的信息。

.dynamic:保存动态链接信息。

.dynstr:保存动态链接所需的字符串。比如符号表中的每个符号都有一个 st_name(符号名),他是指向字符串表的索引,这个字符串表可能就保存在 .dynstr。

.dynsym:保存需要动态连接的符号表。

.fini / .init:分别保存进程退出和初始化时要执行的指令。.init 指令会在程序入口点(main)之前被执行。

.got:保存全局偏移量表 (global offset table)。

.hash:保存符号哈希表,用于快速查找与其对应的符号表中的符号。

.interp:保存 ELF 程序解释器(比如 Android 下的动态链接器)的路径名。

.line:保存用于调试的行号信息。

.note:保存一些注释信息。

.plt:保存过程链接表 (procedure linkage table)。每个外部定义的函数都会在 PLT 中有对应的一项,用于定位外部函数的地址。

.relname / .relaname:保存重定位表。比如:.rel.dyn、.rel.plt。

.rodata / .rodata1:保存程序中的只读数据。

.shstrtab:保存一个字符串表,这些字符串都是 section 的名字。

.strtab:保存字符串表,类似于 .dynstr,但 .dynstr 中保存的都是那些需要动态链接的符号的名字。

.symtab:保存符号表(非动态链接)。

.text:保存可执行的指令代码。

这些以“.”为前缀的 section 名字为系统保留。应用程序可以构造自己的段,但最好不要与系统已定义 section 节重名,也不要以“.”开头,以避免潜在的冲突。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MBHB

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值