编译-链接-加载 :ELF文件格式解析

本文详述了C++程序从源代码到可执行文件的编译、链接及加载过程,重点介绍了ELF文件格式及其在Linux平台上的应用。通过实例程序,深入解析了ELF文件的结构,包括文件头、section头、代码段、数据段等关键部分。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

摘要:对于C++的初学者,经常在程序的编译或者加载过程中遇到很多错误,类似undefined reference to ...GLIBCXX_3.4.20 not found 等。这些错误都涉及到编译器、连接器、加载器的相关知识。本系列文章,将通过一个实例程序,详细介绍一个程序的编译、链接、加载的过程。为了弄清这个过程,本文会简要介绍文本代码到可执行二进制文件的大致过程,同时介绍x86平台上linux系统下ELF文件格式,方便后续详细探讨编译-链接-加载的详细过程。

1. 程序的编译与链接过程

对于编译型的程序,代码需要经过编译-链接的过程才会生成可执行程序,具体过程如下

=====> COMPILATION PROCESS <======

                     |
                     |---->  Input is Source file(.c)
                     |
                     V
            +=================+
            |                 |
            | C Preprocessor  |
            |                 |
            +=================+
                     |
                     | ---> Pure C file ( comd:cc -E <file.name> )
                     |
                     V
            +=================+
            |                 |
            | Lexical Analyzer|
            |                 |
            +-----------------+
            |                 |
            | Syntax Analyzer |
            |                 |
            +-----------------+
            |                 |
            | Semantic Analyze|
            |                 |
            +-----------------+
            |                 |
            | Pre Optimization|
            |                 |
            +-----------------+
            |                 |
            | Code generation |
            |                 |
            +-----------------+
            |                 |
            | Post Optimize   |
            |                 |
            +=================+
                     |
                     |--->  Assembly code (comd: cc -S <file.name> )
                     |
                     V
            +=================+
            |                 |
            |   Assembler     |
            |                 |
            +=================+
                     |
                     |--->  Object file (.obj) (comd: cc -c <file.name>)
                     |
                     V
            +=================+
            |     Linker      |
            |      and        |
            |     loader      |
            +=================+
                     |
                     |--->  Executable (.Exe/a.out) (com:cc <file.name> ) 
                     |
                     V
            Executable file(a.out)
  1. 预处理:C语言预处理器展开 宏定义、#include、#deine 生成纯C的代码
  2. 编译
  • 词法分析
  • 语法分析
  • 语义分析
  • 源代码优化:循环优化、无用代码删除等
  • 代码生成
    3. 链接:符号解析、重定位等。注意连接器和加载器的功能区分并不是那么清晰,对于loader而言,也会处理一些链接的工作。

后文用到的main.cpp内容如下,其他代码都在这里 https://github.com/yukun89/draft/tree/master/hello_world/chapter1

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include "func.h"
int global_b = 1;
const int global_c = 1;
int global_d[10];
static int global_e[10];

int main(){
    static char *p = "Begin printf ";
    int *ip = (int *)malloc(4);
    *ip = 1;
    global_b = func(*ip);
    printf("%s the value of func is %d\n", p, func(1));
    return global_b + global_c;
}

2.ELF文件格式

编译-链接-加载相关的ELF文件主要有两种格式:可重定位目标文件(后缀名为.o) 与 可执行目标文件。(另外还有两种是共享库文件 和 coredump文件。)

分析数据结构之前,我们秉承一个基本原则:结构决定功能;反过来说也成立,设计ELF文件结构,是为了满足特定的功能。这里我们先简要梳理一下,ELF文件应该提供哪些功能?简单来说,ELF文件需要满足可链接、可加载、可执行三大类基本功能,具体来说,包含以下详细功能。

  • 可执行的角度讲,程序需要包含指令与数据,也就是说ELF文件中需要存储程序对应的指令和数据
  • 可链接的角度讲,需要处理不同编译单元之间的引用问题,所以需要符号解析与重定位相关信息
  • 内容组织的角度讲,ELF文件中包含代码、数据、重定位信息等多个section,同时包含这些数据的元数据信息(每个section在文件的起始地址是什么,有多大)。另外,ELF文件格式和其他的任何二进制文件一样,还应该包含一个header,作为所有ELF文件中信息的元数据
  • 可加载的角度讲,ELF文件需要指定将那些代码、数据映射到虚拟内存的什么位置

综上,ELF的文件大致格式如图所示

 

 

注意:Section Headers并不在ELF文件的末尾;Program Header table并不存在于每一种ELF文件格式之中。下面我们用linux下的两个命令工具readelfobjdump来详细分析ELF文件中的各个部分。

2.1 ELF文件头

ELF文件的相关定义在/usr/include/elf.h文件之中,具体ELF文件头的信息如下

typedef struct
{
  unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
  Elf64_Half    e_type;         /* Object file type */
  Elf64_Half    e_machine;      /* Architecture */
  Elf64_Word    e_version;      /* Object file version */
  Elf64_Addr    e_entry;        /* Entry point virtual address */
  Elf64_Off e_phoff;        /* Program header table file offset */
  Elf64_Off e_shoff;        /* Section header table file offset */
  Elf64_Word    e_flags;        /* Processor-specific flags */
  Elf64_Half    e_ehsize;       /* ELF header size in bytes */
  Elf64_Half    e_phentsize;        /* Program header table entry size */
  Elf64_Half    e_phnum;        /* Program header table entry count */
  Elf64_Half    e_shentsize;        /* Section header table entry size */
  Elf64_Half    e_shnum;        /* Section header table entry count */
  Elf64_Half    e_shstrndx;     /* Section header string table index */
} Elf64_Ehdr;

这里,我们用readelf -h分别查看main.omain两种不同格式ELF文件的文件头,得到的结果如下

ykhuang@0062a6cb7e5e: readelf -h main.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:          1112 (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:         14
  Section header string table index: 1
 ykhuang@0062a6cb7e5e : readelf -h main
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:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400550
  Start of program headers:          64 (bytes into file)
  Start of section headers:          4536 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 27

通过以上的内容,我们不难分析,header的主要作用是标识ELF文件中section headers和program headers的位置与大小。header中各个其他字段的解释,我们主要关注以下几点

  • Type表示这个ELF文件属于上文说到的哪种(可重定位还是可执行)ELF文件
  • 程序入口地址Entry point address这一项对于可执行文件才有意义
  • 因为loader只会加载可执行文件,将文件中的代码和数据映射到虚拟MM,所以只有可执行文件的program headers相关信息才有意义。

2.2 ELF文件section

ELF文件中的section主要包括:代码段、数据段、重定位段等信息,section对应的数据结构如下

typedef struct
{
  Elf64_Word  sh_name;    /* Section name (string tbl index) */
  Elf64_Word  sh_type;    /* Section type */
  Elf64_Xword sh_flags;   /* Section flags */
  Elf64_Addr  sh_addr;    /* Section virtual addr at execution */
  Elf64_Off sh_offset;    /* Section file offset */
  Elf64_Xword sh_size;    /* Section size in bytes */
  Elf64_Word  sh_link;    /* Link to another section */
  Elf64_Word  sh_info;    /* Additional section information */
  Elf64_Xword sh_addralign;   /* Section alignment */
  Elf64_Xword sh_entsize;   /* Entry size if section holds table */
} Elf64_Shdr;

下面,让我们来分别查看可重定位目标文件与可执行目标文件的section信息

####可重定位目标文件的信息
 ykhuang@0062a6cb7e5e  ~/project/draft/hello_world/chapter1   master ●  readelf -S -W main.o
There are 14 section headers, starting at offset 0x1c0:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        0000000000000000 000040 000063 00  AX  0   0  4
  [ 2] .rela.text        RELA            0000000000000000 000750 0000c0 18     12   1  8
  [ 3] .data             PROGBITS        0000000000000000 0000a8 000010 00  WA  0   0  8
  [ 4] .rela.data        RELA            0000000000000000 000810 000018 18     12   3  8
  [ 5] .bss              NOBITS          0000000000000000 0000c0 000068 00  WA  0   0 32
  [ 6] .rodata           PROGBITS        0000000000000000 0000c0 00002e 00   A  0   0  4
  [ 7] .comment          PROGBITS        0000000000000000 0000ee 00002d 01  MS  0   0  1
  [ 8] .note.GNU-stack   PROGBITS        0000000000000000 00011b 000000 00      0   0  1
  [ 9] .eh_frame         PROGBITS        0000000000000000 000120 000038 00   A  0   0  8
  [10] .rela.eh_frame    RELA            0000000000000000 000828 000018 18     12   9  8
  [11] .shstrtab         STRTAB          0000000000000000 000158 000066 00      0   0  1
  [12] .symtab           SYMTAB          0000000000000000 000540 0001b0 18     13  11  8
  [13] .strtab           STRTAB          0000000000000000 0006f0 00005a 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

其中type字段的含义如下:

  • PROGBITS: 程序内容,包含代码、数据、调试相关信息
  • NOBITS:和PROGBITS类似,唯一不同的是在文件中不占空间,对应的进行内存空间是加载的时候申请的
  • SYSTAM/DYNSYM: SYSTAM 表用于普通链接;DYNSYM用于动态链接
  • STRTAB:string table,用于section名称、普通的符号名称、动态链接的符号名称。 据此,我们绘制出main.o文件的布局如下:

 

 

可执行文件的信息比较繁琐,我们大致给出,后续再分析具体每个section的含义与作用。

####可执行文件的section信息
 ykhuang@0062a6cb7e5e  ~/project/draft/hello_world/chapter1   master ●  readelf -S -W main
There are 30 section headers, starting at offset 0x11b8:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        0000000000400238 000238 00001c 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            0000000000400254 000254 000020 00   A  0   0  4
  [ 3] .note.gnu.build-id NOTE            0000000000400274 000274 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        0000000000400298 000298 00001c 00   A  5   0  8
  [ 5] .dynsym           DYNSYM          00000000004002b8 0002b8 0000c0 18   A  6   1  8
  [ 6] .dynstr           STRTAB          0000000000400378 000378 0000b7 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          0000000000400430 000430 000010 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         0000000000400440 000440 000020 00   A  6   1  8
  [ 9] .rela.dyn         RELA            0000000000400460 000460 000018 18   A  5   0  8
  [10] .rela.plt         RELA            0000000000400478 000478 000060 18   A  5  12  8
  [11] .init             PROGBITS        00000000004004d8 0004d8 00001a 00  AX  0   0  4
  [12] .plt              PROGBITS        0000000000400500 000500 000050 10  AX  0   0 16
  [13] .text             PROGBITS        0000000000400550 000550 000224 00  AX  0   0 16
  [14] .fini             PROGBITS        0000000000400774 000774 000009 00  AX  0   0  4
  [15] .rodata           PROGBITS        0000000000400780 000780 00003e 00   A  0   0  8
  [16] .eh_frame_hdr     PROGBITS        00000000004007c0 0007c0 000044 00   A  0   0  4
  [17] .eh_frame         PROGBITS        0000000000400808 000808 000134 00   A  0   0  8
  [18] .init_array       INIT_ARRAY      0000000000600de0 000de0 000008 00  WA  0   0  8
  [19] .fini_array       FINI_ARRAY      0000000000600de8 000de8 000008 00  WA  0   0  8
  [20] .jcr              PROGBITS        0000000000600df0 000df0 000008 00  WA  0   0  8
  [21] .dynamic          DYNAMIC         0000000000600df8 000df8 000200 10  WA  6   0  8
  [22] .got              PROGBITS        0000000000600ff8 000ff8 000008 08  WA  0   0  8
  [23] .got.plt          PROGBITS        0000000000601000 001000 000038 08  WA  0   0  8
  [24] .data             PROGBITS        0000000000601038 001038 000018 00  WA  0   0  8
  [25] .bss              NOBITS          0000000000601050 001050 000038 00  WA  0   0 16
  [26] .comment          PROGBITS        0000000000000000 001050 000060 01  MS  0   0  1
  [27] .shstrtab         STRTAB          0000000000000000 0010b0 000108 00      0   0  1
  [28] .symtab           SYMTAB          0000000000000000 001938 0006d8 18     29  47  8
  [29] .strtab           STRTAB          0000000000000000 002010 00028f 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

2.2.1 代码段(txt)-数据段-只读数据段

代码段的信息,我们可以用objdump -s -d main.o具体查看代码段的信息,此处不展开讨论。

数据段信息如下:

ykhuang@0062a6cb7e5e  ~/project/draft/hello_world/chapter1   master ●  objdump -s -d main.o

main.o:     file format elf64-x86-64

Contents of section .text:
 0000 554889e5 4883ec10 bf040000 00e80000  UH..H...........
 0010 00004889 45f8488b 45f8c700 01000000  ..H.E.H.E.......
 0020 488b45f8 8b0089c7 e8000000 00890500  H.E.............
 0030 000000bf 01000000 e8000000 0089c248  ...............H
 0040 8b050000 00004889 c6bf0000 0000b800  ......H.........
 0050 000000e8 00000000 8b050000 000083c0  ................
 0060 01c9c3                               ...
Contents of section .data:
 0000 01000000 00000000 00000000 00000000  ................
Contents of section .rodata:
 0000 01000000 25732074 68652076 616c7565  ....%s the value
 0010 206f6620 66756e63 20697320 25640a00   of func is %d..
 0020 42656769 6e207072 696e7466 2000      Begin printf .

查看符号表信息如下 objdump -x main.o

SYMBOL TABLE:
0000000000000000 g     O .data  0000000000000004 global_b
0000000000000000 g     O .rodata    0000000000000004 global_c
0000000000000000 g     O .bss   0000000000000028 global_d
0000000000000000 g     F .text  0000000000000063 main

从这里我们可以看出,我们只有依赖符号表,才能知道某个变量存放的具体数值信息。

3.其他

需要指出的是,ELF文件格式之所以是现在这种结构,是由体系结构和操作系统来决定的。在一些其他的系统上(例如MS-DOS或者IBM system V),编译-链接的中间文件具有完全不同的结构。总体来说,这些二进制文件主要需要满足可链接、可加载、可执行。这里,我们简要列出了另外两种编译-链接-加载相关的文件结构:

  • COM(component object model)文件:MS-DOS系统上的可执行文件。只有二进制代码,不包含其他任何信息,代码会自动load到0x100,只支持一个代码段。
  • a.out 文件:unix上可执行文件的一种,包含header、代码段、数据段、其他段。程序执行的过程主要是“读取文件头; map代码段;map私有数据段; 创建进行栈; 设置寄存器然后跳转到程序开头”

ELF文件是目前linux平台上最通用的一种可链接-加载-执行的文件结构,对于不同的语言,例如C/C++,他们对应的ELF文件格式略微有所不同:C++相对于C编译而成的ELF文件格式有自己独特的section。了解ELF文件格式有利于我们后续详细理解程序的链接-加载-执行过程

最后放一下blog地址,欢迎来玩

编译-链接-加载:ELF文件格式解析 | 优孚​www.uufool.com图标

参考:

  1. linker && loader
  2. https://stackoverflow.com/questions/3996651/what-is-compiler-linker-loader
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值