1.5.3 Linux ELF

一个实例

1.5.1节 C语言基础 中我们看到了从源代码到可执行文件的全过程,现在我们来看一个更复杂的例子。

#include<stdio.h>

int global_init_var = 10;
int global_uninit_var;

void func(int sum) {
    printf("%d\n", sum);
}

void main(void) {
    static int local_static_init_var = 20;
    static int local_static_uninit_var;

    int local_init_val = 30;
    int local_uninit_var;

    func(global_init_var + local_init_val +
         local_static_init_var );
}

然后分别执行下列命令生成三个文件:

使用 ldd 命令打印所依赖的共享库:

elfDemo_static.out 采用了静态链接的方式。

使用 file 命令查看相应的文件格式:

于是我们得到了 Linux 可执行文件格式 ELF (Executable Linkable Format)文件的三种类型:

  • 可重定位文件(Relocatable file)

    • 包含了代码和数据,可以和其他目标文件链接生成一个可执行文件或共享目标文件。

    • elfDemo.o

  • 可执行文件(Executable File)

    • 包含了可以直接执行的文件。

    • elfDemo_static.out

  • 共享目标文件(Shared Object File)

    • 包含了用于链接的代码和数据,分两种情况。一种是链接器将其与其他的可重定位文件和共享目标文件链接起来,生产新的目标文件。另一种是动态链接器将多个共享目标文件与可执行文件结合,作为进程映像的一部分。

    • elfDemo.out

    • libc-2.25.so

此时他们的结构如图:

img

可以看到,在这个简化的 ELF 文件中,开头是一个“文件头”,之后分别是代码段、数据段和.bss段。程序源代码编译后,执行语句变成机器指令,保存在.text段;已初始化的全局变量和局部静态变量都保存在.data段;未初始化的全局变量和局部静态变量则放在.bss段。

把程序指令和程序数据分开存放有许多好处,从安全的角度讲,当程序被加载后,数据和指令分别被映射到两个虚拟区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读,可以防止程序的指令被改写和利用。

elfDemo.o

接下来,我们更深入地探索目标文件,使用 objdump 来查看目标文件的内部结构:

可以看到目标文件中除了最基本的代码段、数据段和 BSS 段以外,还有一些别的段。注意到 .bss 段没有 CONTENTS 属性,表示它实际上并不存在,.bss 段只是为为未初始化的全局变量和局部静态变量预留了位置而已。

代码段

Contents of section .text.text 的数据的十六进制形式,总共 0x78 个字节,最左边一列是偏移量,中间 4 列是内容,最右边一列是 ASCII 码形式。下面的 Disassembly of section .text 是反汇编结果。

数据段和只读数据段

.data 段保存已经初始化了的全局变量和局部静态变量。elfDemo.c 中共有两个这样的变量,global_init_varlocal_static_init_var,每个变量 4 个字节,一共 8 个字节。由于小端序的原因,0a000000 表示 global_init_var 值(10)的十六进制 0x0a14000000 表示 local_static_init_var 值(20)的十六进制 0x14

.rodata 段保存只读数据,包括只读变量和字符串常量。elfDemo.c 中调用 printf 的时候,用到了一个字符串变量 %d,它是一种只读数据,保存在 .rodata 段中,可以从输出结果看到字符串常量的 ASCII 形式,以 \0 结尾。

BSS段

.bss 段保存未初始化的全局变量和局部静态变量。

ELF 文件结构

对象文件参与程序链接(构建程序)和程序执行(运行程序)。ELF 结构几相关信息在 /usr/include/elf.h 文件中。

img
  • ELF 文件头(ELF Header) 在目标文件格式的最前面,包含了描述整个文件的基本属性。

  • 程序头表(Program Header Table) 是可选的,它告诉系统怎样创建一个进程映像。可执行文件必须有程序头表,而重定位文件不需要。

  • 段(Section) 包含了链接视图中大量的目标文件信息。

  • 段表(Section Header Table) 包含了描述文件中所有段的信息。

32位数据类型

名称
长度
对齐
描述
原始类型

Elf32_Addr

4

4

无符号程序地址

uint32_t

Elf32_Half

2

2

无符号短整型

uint16_t

Elf32_Off

4

4

无符号偏移地址

uint32_t

Elf32_Sword

4

4

有符号整型

int32_t

Elf32_Word

4

4

无符号整型

uint32_t

文件头

ELF 文件头必然存在于 ELF 文件的开头,表明这是一个 ELF 文件。定义如下:

e_ident 保存着 ELF 的幻数和其他信息,最前面四个字节是幻数,用字符串表示为 \177ELF,其后的字节如果是 32 位则是 ELFCLASS32 (1),如果是 64 位则是 ELFCLASS64 (2),再其后的字节表示端序,小端序为 ELFDATA2LSB (1),大端序为 ELFDATA2LSB (2)。最后一个字节则表示 ELF 的版本。

现在我们使用 readelf 命令来查看 elfDome.out 的文件头:

程序头

程序头表是由 ELF 头的 e_phoff 指定的偏移量和 e_phentsizee_phnum 共同确定大小的表格组成。e_phentsize 表示表格中程序头的大小,e_phnum 表示表格中程序头的数量。

程序头的定义如下:

使用 readelf 来查看程序头:

段表(Section Header Table)是一个以 Elf32_Shdr 结构体为元素的数组,每个结构体对应一个段,它描述了各个段的信息。ELF 文件头的 e_shoff 成员给出了段表在 ELF 中的偏移,e_shnum 成员给出了段描述符的数量,e_shentsize 给出了每个段描述符的大小。

使用 readelf 命令查看目标文件中完整的段:

注意,ELF 段表的第一个元素是被保留的,类型为 NULL。

字符串表

字符串表以段的形式存在,包含了以 null 结尾的字符序列。对象文件使用这些字符串来表示符号和段名称,引用字符串时只需给出在表中的偏移即可。字符串表的第一个字符和最后一个字符为空字符,以确保所有字符串的开始和终止。通常段名为 .strtab 的字符串表是 字符串表(Strings Table),段名为 .shstrtab 的是段表字符串表(Section Header String Table)。

偏移
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9

+0

\0

h

e

l

l

o

\0

w

o

r

+10

l

d

\0

h

e

l

l

o

w

o

+20

r

l

d

\0

偏移
字符串

0

空字符串

1

hello

7

world

13

helloworld

18

world

可以使用 readelf 读取这两个表:

符号表

目标文件的符号表保存了定位和重定位程序的符号定义和引用所需的信息。符号表索引是这个数组的下标。索引0指向表中的第一个条目,作为未定义的符号索引。

查看符号表:

重定位

重定位是连接符号定义与符号引用的过程。可重定位文件必须具有描述如何修改段内容的信息,从而运行可执行文件和共享对象文件保存进程程序映像的正确信息。

查看重定位表:

参考资料

Last updated