ELF

来自osdev
跳到导航 跳到搜索
可执行文件格式
Microsoft

16 bit:
COM
MZ
NE
32/64 bit:
PE
Mixed (16/32 bit):
LE

*nix

ELF (可执行和可链接格式-Executable and Linkable Format) 是由Unix系统实验室在SVR4上与Sun Microsystems一起工作时设计的 (UNIX System V Release 4.0)。 因此,ELF最早出现在基于SVR4的Solaris 2.0(又名SunOS 5.0)中。 格式在System V ABI中规范化。

它是一种非常通用的文件格式,后来被许多其他操作系统用作可执行文件和共享库文件。 它确实区分了TEXT、DATA和BSS。(译者注:在可执行文件中Text和Section等有特殊的对应含义,例如Text指代码内容,所以不翻译;Section指文件结构,统一翻译为节)

今天,ELF被认为是类Unix系统上的标准格式。 虽然它有一些缺点 (例如,当使用位置无关代码时,会使用IA-32上的一些稀缺通用寄存器),但它得到了很好的支持和文档化。

文件结构

ELF是一种在磁盘上存储程序或程序片段的格式,作为编译和链接的结果而创建。 ELF文件分为许多节(section)。 对于一个可执行程序,它们是代码的text节、全局变量的data节和通常包含常量字符串的rodata节。 ELF文件包含描述这些部分应如何存储在内存中的标头。

请注意,根据你的文件是可链接文件还是可执行文件,ELF文件中的标头将不同: process.o,是运行gcc -c process.c $SOME_FLAGS的结果

C32/kernel/bin/.process.o
architecture: i386, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000333  00000000  00000000  00000040  2**4
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000050  00000000  00000000  00000380  2**5
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  00000000  00000000  000003d0  2**2
                  ALLOC
  3 .note         00000014  00000000  00000000  000003d0  2**0
                  CONTENTS, READONLY
  4 .stab         000020e8  00000000  00000000  000003e4  2**2
                  CONTENTS, RELOC, READONLY, DEBUGGING
  5 .stabstr      00008f17  00000000  00000000  000024cc  2**0
                  CONTENTS, READONLY, DEBUGGING
  6 .rodata       000001e4  00000000  00000000  0000b400  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .comment      00000023  00000000  00000000  0000b5e4  2**0
                  CONTENTS, READONLY

“flags” 告诉你ELF文件中实际可用的内容。 在这里,我们有符号表-symbol tables和重定位: 我们需要将这个文件链接到另一个文件,但实际上没有关于如何将文件加载到内存中的信息(即使可以猜到)。 作为例子,我们没有程序入口点,并且我们有一个节表(sections table)而不是程序头(program header)。

.text 如上所述是代码所在的地方。 可以通过命令行objdump -drS .process.o 展示
.data 全局表、变量等所在位置。 objdump -s -j .data .process.o 将对其进行十六进制转储。
.bss 不要寻找在文件中的bss: 这里没有。 这就是你未初始化的数组和变量所在的位置,加载器“知道”它们应该用零填充…… 没必要在磁盘上存储那么多零,不是吗?
.rodata 这就是字符串所在的位置,通常是链接时忘记的东西,这些东西会导致内核无法工作。 objdump -s -j .rodata .process.o 将对其进行十六进制转储。 请注意,根据编译器的不同,你可能会有更多这样的节。
.comment & .note 只是编译器/链接器工具链放在那里的注释
.stab & .stabstr 调试符号和类似信息。

/bin/bash,一个真正的可执行文件(译者注:和上面哪个不可执行没有program header的库文件不同,最好用objdump工具亲自试一下。)

/bin/bash:     file format elf32-i386
/bin/bash
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x08056c40

Program Header:
    PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
         filesz 0x000000e0 memsz 0x000000e0 flags r-x

程序头(program header)本身...取224个字节,从文件中的偏移量0x34开始

  INTERP off    0x00000114 vaddr 0x08048114 paddr 0x08048114 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--

用于“执行”二进制文件的程序。 在这里,它显示为‘/lib/ld-linux.so.2’,这意味着在我们运行程序之前需要一些动态库链接。

    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x0007411c memsz 0x0007411c flags r-x

现在我们被要求读取7411c字节,从文件的开始 (?) 到7411c字节 (这实际上是整个文件!),这将是只读的,但可执行。 它们将出现在虚拟地址0x08048000处,以便程序正常工作。

    LOAD off    0x00074120 vaddr 0x080bd120 paddr 0x080bd120 align 2**12
         filesz 0x000022ac memsz 0x000082d0 flags rw-

要加载的位更多(可能是.data节)。 请注意,'filesize' 和 'memsize' 不同,这意味着.bss部分实际上将通过此语句分配,但初始为零,而 “真实” data仅占用从虚拟地址0x80bd120开始的0x22ac字节。

 DYNAMIC off    0x00075f4c vaddr 0x080bef4c paddr 0x080bef4c align 2**2
         filesz 0x000000e8 memsz 0x000000e8 flags rw-

动态(dynamic)节用于存储动态链接过程中使用的信息,例如所需的库和重新定位条目。

    NOTE off    0x00000128 vaddr 0x08048128 paddr 0x08048128 align 2**2
         filesz 0x00000020 memsz 0x00000020 flags r--

NOTE节包含程序员或链接器留下的信息,对于大多数使用GNU ‘ld’链接器链接的程序,它只显示为‘GNU’

EH_FRAME off    0x000740f0 vaddr 0x080bc0f0 paddr 0x080bc0f0 align 2**2
         filesz 0x0000002c memsz 0x0000002c flags r--

这是针对异常处理程序信息,用于我们在执行时链接到一些C++二进制文件 (异常需要引证)。

/bin/bash, loaded (as in /proc/xxxx/maps)

08048000-080bd000 r-xp 00000000 03:06 30574      /bin/bash
080bd000-080c0000 rw-p 00074000 03:06 30574      /bin/bash
080c0000-08103000 rwxp 00000000 00:00 0
40000000-40014000 r-xp 00000000 03:06 27304      /lib/ld-2.3.2.so
40014000-40015000 rw-p 00013000 03:06 27304      /lib/ld-2.3.2.so

我们可以通过声明第二个应在0x080bd*120*处加载,并从0x00074*120*处的文件开始,来识别“代码位”和“数据位”, 我们实际上要保留页面到磁盘块的映射(例如,如果缺少页面0x80bc000,只要从0x75000获取文件块)。 但是,这意味着代码的一部分被映射两次,但具有不同的权限。 我建议你也给他们不同的物理页面,如果你不想最终代码变成可改。

加载ELF二进制文件

可执行映像和elf二进制文件可以相互映射

ELF头包含加载ELF可执行文件所需的所有相关信息。 此文件头的格式在ELF规范中进行了描述。 此内容最相关的章节在1.1到1.4,以及2.1到2.7。 关于加载可执行文件的说明包含在第2.7节中。

以下是ELF可执行加载程序必须执行的步骤的粗略概述:

  • 验证文件是否以ELF规范中第11页的图1-4 (和后续表) 所述的ELF固定数字 (4字节) 开头。
  • 读ELF文件头。 ELF头始终位于ELF文件的最开始处。 ELF头包含有关文件其余部分的布局方式的信息。 可执行加载程序只与程序头有关。
  • 读取ELF可执行文件的程序头(program headers)。 这些指定程序段(program segments)在文件中的位置,以及它们需要加载到内存中的位置。
  • 解析程序头以确定必须加载的程序段数。 每个程序头都有一个相关的类型,如ELF规范的图2-2所示。 只有类型为 PT_LOAD 的标头才描述可加载的段。
  • 加载每个可加载段。 这是按如下方式执行的:
    • 在程序头中 p_vaddr成员指定的地址为每个段分配虚拟内存。 内存中段的大小由p_memsz成员指定。
    • 将段数据从p_offset成员指定的文件偏移量复制到p_vaddr成员指定的虚拟内存地址。 文件中段的大小包含在 p_filesz 成员中。 可以是零。
    • p_memsz成员指定段在内存中所占的大小。 可以是零。 如果p_fileszp_memsz成员不同,这表明该段用零填充。 内存中文件大小的结束偏移量和段的虚拟内存大小之间的所有字节都将用零清除。
  • 从ELF文件头中读取可执行文件的入口点。
  • 跳转到新加载内存中可执行文件的入口点。

重定位

当你需要加载 (例如,模块或驱动程序) 时,重新定位变得很方便。 可以使用ld的“-r”选项,允许将多个目标文件链接到一个大文件中,这意味着更容易编码和更快的测试。

重定位需要做的事情的基本概述如下:

  1. 检查目标文件头 (例如,它必须是ELF,而不是PE)
  2. 获取加载地址(例如,所有驱动程序都从0xA0000000开始,需要一些跟踪驱动程序位置的方法)
  3. 为所有程序节(ST_PROGBITS)分配足够的空间
  4. 从RAM中的映像复制到分配的空间
  5. 浏览所有部分,根据内核符号表解析外部引用
  6. 如果全部成功,你可以使用头部的“e_entry”字段作为距加载地址的偏移量来调用入口点(如果指定了入口点),或者执行符号查找,或者直接返回成功(错误)码。

一旦你可以重新定位ELF对象,就可以在需要时加载驱动程序,而不是在启动时加载驱动程序-这始终是一件好事 (tm)。

文件头

文件头于ELF文件的开头。

Position (32 bit) Position (64 bit) Value
0-3 0-3 固定数字 - 0x7F,然后是ASCII中的“ELF”
4 4 1 = 32 bit, 2 = 64 bit
5 5 1 = little endian, 2 = big endian
6 6 ELF头版本
7 7 OS ABI-System V通常为0
8-15 8-15 未使用/填充
16-17 16-17 1 = relocatable, 2 = executable, 3 = shared, 4 = core
18-19 18-19 指令集-见下表
20-23 20-23 ELF版本
24-27 24-31 程序入口位置
28-31 32-39 程序头表(Program header table)位置
32-35 40-47 节头表(Section header table)位置
36-39 48-51 标志 —— 依赖于架构;见下面的注释
40-41 52-53 文件头大小
42-43 54-55 程序头表(program header table)中条目的大小
44-45 56-57 程序头表中的条目数
46-47 58-59 节头表(section header table)单条目大小
48-49 60-61 节头表中的条目数
50-51 62-63 在节头表中使用节名称进行索引

对于x86 ELF,可能可以忽略flags条目,因为实际上没有定义任何标志。

指令集架构:

架构
未规范 0
Sparc 2
x86 3
MIPS 8
PowerPC 0x14
ARM 0x28
SuperH 0x2A
IA-64 0x32
x86-64 0x3E
AArch64 0xB7
RISC-V 0xF3

粗体是指最常见的架构。

程序头

这是N个 (在文件头中给出) 条目的数组,格式如下: 确保使用正确的版本,这取决于文件是32位还是64位,表是完全不同的。

32位版本:

位置
0-3 段类型-Type of segment(详见下文)
4-7 可以找到该段数据的文件中的偏移量 (p_offset)
8-11 你应该从哪里开始将此段放入虚拟内存(p_vaddr)
12-15 System V ABI未定义
16-19 文件中段的大小 (p_filesz)
20-23 内存中段的大小(p_memsz)
24-27 标志(见下文)
28-31 此部分所需的对齐方式 (必须为2的幂)

64位版本:

位置
0-3 段类型(见下文)
4-7 标志(见下文)
8-15 可以找到该段数据的文件中的偏移量 (p_offset)
16-23 你应该从哪里开始将此段放入虚拟内存(p_vaddr)
24-31 System V ABI未定义
32-39 文件中段的大小 (p_filesz)
40-47 内存中段的大小(p_memsz)
48-55 此段所需的对齐方式(必须为2的幂)

段类型: 0 = null - 忽略该条目; 1 = 加载(load) - 将p_vaddr处的p_memsz字节清除为0,然后将p_filesz字节从p_offset复制到p_vaddr; 2 = 动态(dynamic)- 需要动态链接; 3 = 解释(interp) - 包含一个可执行文件的文件路径,用作以下段的解释器; 4 = 注释部分。 有更多的值,但主要包含特定于体系结构/环境的信息,这可能不是大多数ELF文件所必需的。

标志:1 = 可执行,2 = 可写,4 = 可读。

动态链接

正文: Dynamic Linker

动态链接用在操作系统提供程序共享库 (如果需要的话) 的时候。 也就是说,这些库在系统中找到,然后在程序运行时“绑定(bind)”到需要它们的程序,而静态链接则在程序运行之前已链接这些库。 其主要优点是程序占用较少的内存,并且文件大小较小。 然而,主要的缺点是程序变得不那么可移植性,因为程序依赖于许多不同的共享库。

为了实现这一点,你需要有适当的调度、一个库和一个使用该库的程序。 你可以使用GCC创建库:

myos-gcc -c -fPIC -o oneobject.o oneobject.c
myos-gcc -c -fPIC -o anotherobject.o anotherobject.c
myos-gcc -shared -fPIC -Wl,-soname,nameofmylib oneobject.o anotherobject.o -o mylib.so

该库应被视为一个文件,当操作系统检测到它的尝试使用时,该文件将被加载。 你需要将这个“动态链接”技术实现到特定的一类代码中去,比如内存管理或任务管理部分。 当ELF程序运行时,系统应该将共享目标数据附加到malloc()的内存区域,其中对库的函数调用重定向到malloc()的内存区域。 程序完成后,可以在调用free()时将区域放弃交还给OS。

这才是一个编写良好的动态链接器。

另见

文章

外部链接

de:Executable and Linking Format