Linkers & Loaders

Someone2022年10月9日
大约 8 分钟

本文主要涉及的内容有:

  1. 目标文件的研究;
  2. Segment vs Section;
  3. objdump 细节;
  4. 从 readelf 走进 ELF 文件

Abstract

本文章主要是《程序员的自我修养》的读书笔记。

目标文件

目标文件的格式

  • .o 目标文件,就是编译后但是还未链接的那些中间文件。
  • .so Linux 下的动态链接库。
  • .elf Linux 下的可执行文件。
  • .a Linux 下的静态链接库。

这些都是按照可执行文件的格式存储的。

为了更加直观,我们把书中的表格也引用过来:

ELF 文件类型说明实例
可重定位文件
Relocatable
包含了代码和数据,可以被用来链接成可执行文件或者共享目标文件;静态链接库也可以属于这一类.o, .obj
可执行文件
Executable
包含了可以直接执行的文件,一般都没有扩展名elf, /bin/bash, exe
共享目标文件
Shared Object
包含了代码和数据,可以在以下两种情况中应用:
1. 链接器使用这种文件跟其他的可重定位文件和共享目录链接,产生新的目标文件;
2. 动态连接器将几个这种文件与可执行文件结合,作为进程映像的一部分来执行。
.so, DLL
核心转储文件core dump file, 当进程意外终止的时候,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件core dump

如果在遇到不确定的情况下,可以在命令行中使用 file 命令来查看相应的文件格式。

注意到以上表格中的文件格式都可统一称为目标文件

可重定位文件 .o

简单理解的话,编译后生成的文件就是可重定位文件。可以由 .s 文件得到,gcc -c xxx.s -o xxx.o;

Segement

那么目标文件中都有什么呢?除了必须有的编译后的机器指令代码和数据之外,还包括了链接时所需要的一些信息:符号表、调试信息、字符串等。这些链接所需要的信息都被存储在**段(Segment)**中,也可以称作节(Section).

程序代码编译后的机器指令经常被放在代码段中,代码段常见的名字有 .code 和 .text; 全局遍历和静态变量数据经常被放在数据段中,一般的名字都叫做 .data. 除此之外,还有一个 BSS 段,其中主要保存的就是未初始化的全局变量和局部静态变量。

含义
File Header描述了整个文件的属性。
除此之外,还会包括一个段表,用于描述文件中各个段的数组,其内容是各个段在该文件中的偏移位置以及段的属性。
.text section编译后的机器代码。
.data section已初始化的全局变量和局部静态变量。
.bss section未初始化的全局变量和局部静态变量。

分段的原因和优点如下列举:

  • 程序被装载后,数据段是可读写的,而代码段(指令区域)是只读的;

  • 将代码段和数据段分开,有助于利用到现在计算机的 icache 和 dcache.

  • 有利于代码段的共享;

需要注意,有时候会遇到 .rodata 段,这个段中存放的是只读数据,即对这个段的所有操作都当作非法处理;其次还在语义上支持了 C++ 的 const 关键字。

Section

笔者把 section 的研究[1]相关的内容放在一起,这样可以对比分析 section 和 segment 的区别,方便我们的理解。

objdump

objdump -h xxx.o

上述的 -h 选项是可以打印出 elf 文件每个段的基本信息。其中需要注意的是,CONTENTS 属性用来表示该段在文件中存在,如果没有这个属性的字段或者是 0, 我们就可以认为这个属性段在文件中是不存在的。

objdump -s -d xxx.o

-s 参数可以将所有段的内容以 16 进制的方式打印出来;

-d 参数可以将所有包含指令的段反汇编。

objdump -s -d -x xxx.o

-x 参数可以打印出详细信息,比如说这个文件里面的段,每个段具体的内容等。

example

我们给出来一个示例的 C 文件,方便我们理解:

/*
 * SimpleSection.c
 *
 * Linux:
 * gcc -c SimpleSection.c
 *
 * Windows:
 * cl SimpleSection.c /c /Za
 */
int printf(const char *format, ...);
int global_init_var = 84;
int global_uninit_var;
void func1(int i)
{
    printf("%d\n", i);
}
int main(void)
{
    static int static_var = 85;
    static int static_var2;
    int a = 1;
    int b;
    func1(static_var + static_var2 + a + b);
    return a;
}

在控制台执行:

gcc -c SimpleSection.c

然后使用 objdump 查看其信息,-h 选项打印出每一个段的基本信息:

objdump -h SimpleSection.o

出来的信息如下所示(看起来不整洁的话可以换为截图):

SimpleSection.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000057  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  00000098  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000004  0000000000000000  0000000000000000  000000a0  2**2
                  ALLOC
  3 .rodata       00000004  0000000000000000  0000000000000000  000000a0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      0000002a  0000000000000000  0000000000000000  000000a4  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000ce  2**0
                  CONTENTS, READONLY
  6 .eh_frame     00000058  0000000000000000  0000000000000000  000000d0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

我们使用 -s 参数将所有的内容以 16 进制的方式打印出来,-d 参数将所有包含指令的段反汇编,如下所示:

$ objdump -s -d SimpleSection.o

SimpleSection.o:     file format elf64-x86-64

Contents of section .text:
 0000 554889e5 4883ec10 897dfc8b 45fc89c6  UH..H....}..E...
 0010 488d3d00 000000b8 00000000 e8000000  H.=.............
 0020 0090c9c3 554889e5 4883ec10 c745f801  ....UH..H....E..
 0030 0000008b 15000000 008b0500 00000001  ................
 0040 c28b45f8 01c28b45 fc01d089 c7e80000  ..E....E........
 0050 00008b45 f8c9c3                      ...E...
Contents of section .data:
 0000 54000000 55000000                    T...U...
Contents of section .rodata:
 0000 25640a00                             %d..
Contents of section .comment:
 0000 00474343 3a202855 62756e74 7520372e  .GCC: (Ubuntu 7.
 0010 352e302d 33756275 6e747531 7e31382e  5.0-3ubuntu1~18.
 0020 30342920 372e352e 3000               04) 7.5.0.
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 24000000 00410e10 8602430d  ....$....A....C.
 0030 065f0c07 08000000 1c000000 3c000000  ._..........<...
 0040 00000000 33000000 00410e10 8602430d  ....3....A....C.
 0050 066e0c07 08000000                    .n......

Disassembly of section .text:

0000000000000000 <func1>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 83 ec 10             sub    $0x10,%rsp
   8:   89 7d fc                mov    %edi,-0x4(%rbp)
   b:   8b 45 fc                mov    -0x4(%rbp),%eax
   e:   89 c6                   mov    %eax,%esi
  10:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 17 <func1+0x17>
  17:   b8 00 00 00 00          mov    $0x0,%eax
  1c:   e8 00 00 00 00          callq  21 <func1+0x21>
  21:   90                      nop
  22:   c9                      leaveq
  23:   c3                      retq

0000000000000024 <main>:
  24:   55                      push   %rbp
  25:   48 89 e5                mov    %rsp,%rbp
  28:   48 83 ec 10             sub    $0x10,%rsp
  2c:   c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
  33:   8b 15 00 00 00 00       mov    0x0(%rip),%edx        # 39 <main+0x15>
  39:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 3f <main+0x1b>
  3f:   01 c2                   add    %eax,%edx
  41:   8b 45 f8                mov    -0x8(%rbp),%eax
  44:   01 c2                   add    %eax,%edx
  46:   8b 45 fc                mov    -0x4(%rbp),%eax
  49:   01 d0                   add    %edx,%eax
  4b:   89 c7                   mov    %eax,%edi
  4d:   e8 00 00 00 00          callq  52 <main+0x2e>
  52:   8b 45 f8                mov    -0x8(%rbp),%eax
  55:   c9                      leaveq
  56:   c3                      retq

这里面有一个细节需要注意,我们如何定位函数的地址,对于 main 函数,我们可以看到其地址是 0000000000000024, 而在第 8 行我们可以看到 0020 0090c9c3 554889e5 4883ec10 c745f801, 这行的意思就是起始地址是 0020, 所以我们 +4 就可以得到函数的起始汇编代码 55.

Contents of section .data:
 0000 54000000 55000000                    T...U...

上述 54000000 涉及到了字节序的问题,这里的实际上存储的是 0x54 即十进制的 84.

readelf

同时还有一个 readelf 工具可以作为 objdump 的对照:

$ readelf -h SimpleSection.o                                                     

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:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1104 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 12

readelf 可以来详细查看 elf 文件,使用 -h 选项可以查看 elf 的文件头。

上述的字段在 /usr/include/elf.h 都有定义,我们参考下表,对其做一个大概的认知。

Linux Elf32_Ehdr 的结构体如下(64 位对应的也可以找到,为 Elf64_Ehdr):

typedef struct {
    unsigned char e_ident[16];
    Elf32_Half e_type;
    Elf32_Half e_machine;
    Elf32_Word e_version;
    Elf32_Addr e_entry;
    Elf32_Off e_phoff;
    Elf32_Off e_shoff;
    Elf32_Word e_flags;
    Elf32_Half e_ehsize;
    Elf32_Half e_phentsize;
    Elf32_Half e_phnum;
    Elf32_Half e_shentsize;
    Elf32_Half e_shnum;
    Elf32_Half e_shstrndx;
} Elf32_Ehdr;

这些成员与 readelf 的打印的对应关系为:

成员readelf output
e_identMagic: 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
e_typeType: REL (Relocatable file)
elf 文件类型
e_machineMachine: Advanced Micro Devices X86-64
elf 文件的 CPU 平台属性;相关常量以 EM 开头
e_versionVersion: 0x1
elf 版本号,一般为常量 1
e_entryEntry point address: 0x0
入口地址,ELF 程序入口的虚拟地址,操作系统在加载完进程后从这个地址开始指向进程的指令;可重定位文件没有入口地址,该值为 0
e_phoffStart of program headers: 0 (bytes into file)
e_shoffStart of section headers: 1104 (bytes into file)
段表在文件中的偏移,1104 表示从段表的低 1104 个字节开始

对于 ELF 魔数,我们可以进行分析。

7f 45 4c 4602010100 00 00 00 00 00 00 00 00
4字节的,ELF 文件通用的,ELF 文件的魔数ELF 文件类
0 无效文件
1 32 位 ELF 文件
2 64 位 ELF 文件
字节序
0 无效格式
1 小端格式
2 大端格式
ELF 版本

Use readelf

🔴🔴🔴 Q:能否从 ELF 文件中得到符号表?


  1. ELF文件解析(二):ELF header详解open in new window]https://segmentfault.com/a/1190000016766079 ↩︎

Loading...