摘要:对于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> ) |VExecutable file(a.out)
预处理:C语言预处理器展开宏定义、#include、#deine生成纯C的代码编译词法分析语法分析语义分析源代码优化:循环优化、无用代码删除等代码生成
3. 链接:符号解析、重定位等。注意连接器和加载器的功能区分并不是那么清晰,对于loader而言,也会处理一些链接的工作。
后文用到的main.cpp
内容如下,其他代码都在这里 /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 %dn", 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下的两个命令工具readelf
和objdump
来详细分析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.o
和main
两种不同格式ELF文件的文件头,得到的结果如下
ykhuang@0062a6cb7e5e: readelf -h main.oELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00Class: ELF64Data:2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type:REL (Relocatable file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address:0x0Start of program headers:0 (bytes into file)Start of section headers:1112 (bytes into file)Flags: 0x0Size of this header:64 (bytes)Size of program headers: 0 (bytes)Number of program headers: 0Size of section headers: 64 (bytes)Number of section headers: 14Section header string table index: 1ykhuang@0062a6cb7e5e : readelf -h mainELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00Class: ELF64Data:2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type:EXEC (Executable file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address:0x400550Start of program headers:64 (bytes into file)Start of section headers:4536 (bytes into file)Flags: 0x0Size of this header:64 (bytes)Size of program headers: 56 (bytes)Number of program headers: 9Size of section headers: 64 (bytes)Number of section headers: 30Section 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.oThere are 14 section headers, starting at offset 0x1c0:Section Headers:[Nr] Name Type AddressOff Size ES Flg Lk Inf Al[ 0] NULL 0000000000000000 000000 000000 000 0 0[ 1] .text PROGBITS 0000000000000000 000040 000063 00 AX 0 0 4[ 2] .rela.text RELA 0000000000000000 000750 0000c0 1812 1 8[ 3] .data PROGBITS 0000000000000000 0000a8 000010 00 WA 0 0 8[ 4] .rela.data RELA 0000000000000000 000810 000018 1812 3 8[ 5] .bss NOBITS0000000000000000 0000c0 000068 00 WA 0 0 32[ 6] .rodata PROGBITS 0000000000000000 0000c0 00002e 00 A 0 0 4[ 7] .commentPROGBITS 0000000000000000 0000ee 00002d 01 MS 0 0 1[ 8] .note.GNU-stack PROGBITS 0000000000000000 00011b 000000 000 0 1[ 9] .eh_frame PROGBITS 0000000000000000 000120 000038 00 A 0 0 8[10] .rela.eh_frame RELA 0000000000000000 000828 000018 1812 9 8[11] .shstrtab STRTAB0000000000000000 000158 000066 000 0 1[12] .symtab SYMTAB0000000000000000 000540 0001b0 1813 11 8[13] .strtab STRTAB0000000000000000 0006f0 00005a 000 0 1Key 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 mainThere are 30 section headers, starting at offset 0x11b8:Section Headers:[Nr] Name Type AddressOff Size ES Flg Lk Inf Al[ 0] NULL 0000000000000000 000000 000000 000 0 0[ 1] .interp PROGBITS 0000000000400238 000238 00001c 00 A 0 0 1[ 2] .note.ABI-tagNOTE 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 DYNSYM00000000004002b8 0002b8 0000c0 18 A 6 1 8[ 6] .dynstr STRTAB0000000000400378 000378 0000b7 00 A 0 0 1[ 7] .gnu.versionVERSYM0000000000400430 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_hdrPROGBITS 00000000004007c0 0007c0 000044 00 A 0 0 4[17] .eh_frame PROGBITS 0000000000400808 000808 000134 00 A 0 0 8[18] .init_array INIT_ARRAY0000000000600de0 000de0 000008 00 WA 0 0 8[19] .fini_array FINI_ARRAY0000000000600de8 000de8 000008 00 WA 0 0 8[20] .jcr PROGBITS 0000000000600df0 000df0 000008 00 WA 0 0 8[21] .dynamicDYNAMIC 0000000000600df8 000df8 000200 10 WA 6 0 8[22] .got PROGBITS 0000000000600ff8 000ff8 000008 08 WA 0 0 8[23] .got.pltPROGBITS 0000000000601000 001000 000038 08 WA 0 0 8[24] .data PROGBITS 0000000000601038 001038 000018 00 WA 0 0 8[25] .bss NOBITS0000000000601050 001050 000038 00 WA 0 0 16[26] .commentPROGBITS 0000000000000000 001050 000060 01 MS 0 0 1[27] .shstrtab STRTAB0000000000000000 0010b0 000108 000 0 1[28] .symtab SYMTAB0000000000000000 001938 0006d8 1829 47 8[29] .strtab STRTAB0000000000000000 00 00028f 000 0 1Key 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.omain.o:file format elf64-x86-64Contents 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 ...............H0040 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 value0010 206f6620 66756e63 20697320 25640a00 of func is %d..0020 42656769 6e207072 696e7466 2000Begin printf .
查看符号表信息如下objdump -x main.o
SYMBOL TABLE:0000000000000000 gO .data 0000000000000004 global_b0000000000000000 gO .rodata 0000000000000004 global_c0000000000000000 gO .bss 0000000000000028 global_d0000000000000000 gF .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文件格式解析 | 优孚
参考:
linker && loader/questions/3996651/what-is-compiler-linker-loader如果觉得《cfile清空文件内容_编译-链接-加载 :ELF文件格式解析》对你有帮助,请点赞、收藏,并留下你的观点哦!