【译】《可执行文件背后的原理》—— 第2章 可执行和可链接格式(ELF)

ELF: 揭秘可执行文件格式

在第1章中,我们发现简单的 “Hello, World!”程序生成了一个出乎意料大的可执行文件。现在,让我们深入探讨使这一切成为可能的格式:可执行与可链接格式(ELF)。本章将探讨ELF文件(Linux 系统中可执行文件的标准二进制格式)的结构、用途及其复杂性。

剖析ELF文件

ELF 文件就像一个精心组织的容器,其中保存着我们程序的各种组件。让我们首先创建一个简单的程序,以便在本章中对其进行研究:

// example.c
#include <stdio.h>

const char message[] = "Hello from ELF!";
int initialized_var = 42;
int uninitialized_var;

void print_message() {
    printf("%s\n", message);
}

int main() {
    uninitialized_var = initialized_var;
    print_message();
    return 0;
}

让我们编译此程序,开始我们的探索:

$ gcc -g -o example example.c

ELF File Structure Overview

ELF文件由四个主要部分构成:
1、ELF文件头(ELF Header)
2、 程序头表(译者注:也称段表)(Program Header Table)
3、各种节(Sections)
4、节头表(Section Header Table)

下面是一个直观的示意图:

+-------------------+
|    ELF文件头       |
+-------------------+
|    程序头表        |
+-------------------+
|                   |
|      节           |
|  (.text, etc)     |
|                   |
+-------------------+
|     节头表         |
+-------------------+

ELF 文件头: 文件的身份证

让我们详细研究一下 ELF 文件头:

$ readelf -h example
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                           UNIX - System V
  ABI Version:                       0
  Type:                             DYN (Shared object file)
  Machine:                          Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1060
  Start of program headers:          64 (bytes into file)
  Start of section headers:          14816 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30

让我们来分析一下这些字段:

1、魔数(译者注:计16字节)

Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  • 前四个字节总是:0x7F(译者注:ASCII中的DEL控制字符,对应EI_MAG0), ‘E’(译者注:对应EI_MAG1), ‘L’(译者注:对应EI_MAG2), ‘F’(译者注:对应EI_MAG3)
  • 以下字节指定了:
    • 文件类别 (32/64位)(译者注:1为ELF32、2为ELF64,0为非法目标文件,对应第5个字节(EI_CLASS))
    • 数据编码格式(译者注:大小端字节序,1为LSB,2为MSB,,对应第6个字节(EI_DATA))
    • ELF版本号(译者注:对应第7个字节(EI_VERSION))
    • 操作系统ABI(译者注:指的是目标操作系统ABI编号,对应第8个字节(EI_OSABI))
    • ABI版本(译者注:进一步指定ABI的版本,用于区分ABI的部兼容版本,对应第9个字节(EI_ABIVERSION))
    • 填充(译者注:保留并置为0,对应剩余7个字节(EI_PAD))

2、文件类型

Type: DYN (Shared object file)

常见类型包括:

  • EXEC: 可执行文件
  • DYN: 共享目标文件(包括位置无关的可执行文件)
  • REL: 可重定位文件
  • CORE: 核心转储文件

3、处理器架构

Machine: Advanced Micro Devices X86-64

指定目标架构。其他常用值如下:

  • EM_386: x86
  • EM_ARM: ARM
  • EM_RISCV: RISC-V

4、入口地址

Entry point address: 0x1060

程序执行的起始内存地址。它通常指向 _start函数,而非 main函数(我们将在第 4 章中探讨原因)。

Program Headers: Loading Instructions

程序头告诉系统如何创建进程映像。让我们来检查它们:

$ readelf -l example

Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000898 0x0000000000000898  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x0000000000000235 0x0000000000000235  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x00000000000001a4 0x00000000000001a4  R      0x1000
  LOAD           0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
                 0x0000000000000258 0x0000000000000260  RW     0x1000

关键程序头类型如下:

1、 PHDR

指向程序头表本身。

2、INTERP

指定动态链接器/解释器:

$ readelf -p .interp example
String dump of section '.interp':
  [     0]  /lib64/ld-linux-x86-64.so.2

3、LOAD

描述即将加载到内存中的段:

  • 只读段 (代码和常量)
  • 可读可执行段 (代码)
  • 可读可写段 (数据)

4、DYNAMIC

包含动态链接信息:

$ readelf -d example
Dynamic section at offset 0x2dc8 contains 27 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x1000
 0x000000000000000d (FINI)               0x11f8
 [...]

节: 程序组件的位置

让我们全面了解一下各个节:

$ readelf -S example
There are 31 section headers, starting at offset 0x39f0:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000000318  00000318
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.gnu.property NOTE             0000000000000338  00000338
       0000000000000030  0000000000000000   A       0     0     8
  [...]

重要的节包括:

1、代码节

  • .text: 可执行代码
  • .init: 初始化代码
  • .fini: 清理代码
  • .plt: 程序链接表(用于动态链接)(译者注:PLT)

2、数据节

  • .data: 初始化数据
  • .rodata: 只读数据
  • .bss: 未初始化数据
  • .got: 全局偏移表(译者注:GOT)

3、符号和重定位部分

  • .symtab: 符号表
  • .strtab: 字符串表
  • .rela.text: .text节的重定位
  • .rela.data: .data节的重定位

检查节的内容

让我们来看一些具体的节:

1、.text节中的代码

$ objdump -d -j .text example
...
0000000000001169 <print_message>:
    1169:       55                      push   %rbp
    116a:       48 89 e5                mov    %rsp,%rbp
    116d:       48 8d 05 94 0e 00 00    lea    0xe94(%rip),%rax
    1174:       48 89 c7                mov    %rax,%rdi
    1177:       e8 e4 fe ff ff          call   1060 <puts@plt>
    117c:       90                      nop
    117d:       5d                      pop    %rbp
    117e:       c3                      ret
...

2、.rodata节的只读数据

$ objdump -s -j .rodata example
Contents of section .rodata:
 2000 01000200 48656c6c 6f206672 6f6d2045  ....Hello from E
 2010 4c462100                             LF!.

3、.data节的初始化数据

$ objdump -s -j .data example
Contents of section .data:
 4000 2a000000                             *...

符号表: 程序的字典

符号表将名称映射为地址:

$ readelf -s example
Symbol table '.dynsym' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterT[...]
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMC[...]
     5: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND [...]@GLIBC_2.2.5 (2)
     6: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __cxa_finalize@[...]

处理ELF文件

1、创建不同类型的ELF文件

可重定位的目标文件
$ gcc -c -o example.o example.c
$ file example.o
example.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
共享库
$ gcc -shared -fPIC -o libexample.so example.c
$ file libexample.so
libexample.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV)
静态可执行文件
$ gcc -static -o example_static example.c
$ file example_static
example_static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux)

2、操作ELF文件

添加节
$ objcopy --add-section .mydata=myfile.txt example example_modified
剥离调试信息
$ strip --strip-debug example
提取节
$ objcopy -O binary --only-section=.text example text.bin

不同上下文中的ELF

1、可重定位文件 (.o)

  • 包含适合链接的代码和数据
  • 具有重定位信息,用于指明如何修改节内容
  • 包括用于外部引用的符号表

2、可执行文件

  • 包含可直接执行的代码和数据
  • 包含描述如何创建进程映像的程序头
  • 指定动态链接的解释器

3、共享对象 (.so)

  • 可在运行时链接
  • 位置无关代码(译者注:PIC)
  • 可被多个进程共享

实践中的应用

1、调试

$ gdb example
(gdb) info files
Symbols from "/path/to/example".
Local exec file:
        `/path/to/example', file type elf64-x86-64.
        Entry point: 0x1060
        0x0000000000000318 - 0x0000000000000334 is .interp
        [...]

2、二进制分析

$ nm example
0000000000004010 B __bss_start
0000000000004010 b completed.8061
                 w __cxa_finalize@@GLIBC_2.2.5
0000000000004000 D __data_start
[...]

3、运行时分析

$ strace ./example
execve("./example", ["./example"], 0x7ffc9487d7e0 /* 66 vars */) = 0
brk(NULL)                               = 0x557a5a43c000
arch_prctl(ARCH_SET_FS, 0x557a5a43b540) = 0
[...]

安全方面的影响

1、ASLR (地址空间随机化)

$ cat /proc/sys/kernel/randomize_va_space
2
$ ./example
$ cat /proc/$(pgrep example)/maps
00400000-00401000 r-xp 00000000 08:01 1048578    /path/to/example
[...]

2、栈保护

$ readelf -s example | grep -i stack
     8: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4

3、只读重定位( 译者注:RELRO)

$ readelf -d example | grep RELRO
 0x000000000000001e (FLAGS)              BIND_NOW RTLD_GLOBAL

小结

理解ELF格式对以下工作至关重要:

  • 调试复杂问题
  • 理解程序加载和执行的过程
  • 实施安全措施
  • 优化程序大小和性能

在下一章中,我们将深入研究ELF节,并探讨不同类型的代码和数据是如何在其中组织起来的。

延伸阅读

1、官方参考文献:

  • System V ABI规范
  • 工具接口标准 (TIS) ELF 规范

2、命令文档:

  • man elf
  • man objdump
  • man readelf

3、在线资源:

  • Linux基金会文档
  • ELF 工具链项目

如本文对你有些许帮助,欢迎大佬支持我一下(点赞+收藏+关注、关注公众号等),您的支持是我持续创作的竭动力
支持我的方式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

花神庙码农

你的鼓励是我码字的最大动力!

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

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

打赏作者

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

抵扣说明:

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

余额充值