编译器编译源代码后生成的文件叫做 “目标文件”。
目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。
现在PC平台流行的 “可执行文件格式”(Executable)主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format),它们都是 COFF(Common File Format)格式的变种。
目标文件就是源代码编译后但未进行链接的那些中间文件(Windows的 .obj
和 Linux下的 .o
),它跟可执行文件的内容与结构很相似,所以一般跟可执行文件一起采用一种格式存储。
从广义上可以将目标文件与可执行文件看成是一种类型的文件,在Windows下统称它们为 PE-COFF 文件格式,在Linux下它们统称为 ELF文件。
不只是“可执行文件”按照ELF文件格式存储,“动态链接库”以及“静态链接库”文件也都按照ELF格式存储。
动态链接库:DLL,Dynamic Linking Library。Windows下的 .dll
和 Linux下的 .so
文件;
静态链接库:Static Linking Library。Windows下的 .lib
和 Linux下的 .a
文件;
静态链接库稍有不同,它是把很多文件捆绑在一起形成一个文件,再加上一些索引,你可以简单的把它理解为一个包含很多目标文件的文件包。
ELF文件标准里面把系统中采用ELF格式的文件归为以下4类:
.o
,Windows下的.obj
;.so
,Windows下的.dll
;我们可以在Linux下使用 file
命令查看响应的文件格式:
# gcc -E demo.c -o demo.i //
# gcc -S demo.i -o demo.s //生成汇编文件
# gcc -c demo.s -o demo.o //生成目标文件
# gcc demo.c -o demo //生成可执行文件
# file demo.c
demo.c: C source, ASCII text
# file demo.i
demo.i: C source, ASCII text
# file demo.s
demo.s: assembler source, ASCII text
# file demo.o
demo.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
# file demo
demo: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=9bee5fbd55540c9ab45e68ac1af1569a9abde37c, for GNU/Linux 3.2.0, not stripped
目标文件与可执行文件格式的小历史:
COFF 是由 Unix System V Release 3 首先提出并且使用的格式规范,后来微软公司基于COFF格式指定了PE格式标准,并将其用于Windows NT系统。
System V Release 4 在 COFF 的基础上引入了 ELF格式,目前流行的Linux系统也以ELF作为基本可执行文件格式。
这也是为什么目前PE和ELF如此相似的主要原因,因为它们都是源于同一种可执行文件格式COFF。
目标文件中的内容包括:编译后的机器指令代码、数据,以及用于链接时所需要的一些信息,例如符号表、调试信息、字符串等。
目标文件将这些信息按不同的属性,以“段”(Segment)的方式存储,例如代码段、数据段。
例如上图中的ELF文件:
ELF文件的开头是一个 “文件头”,它描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息;
文件头还包括一个**“段表”**(Section Table),段表其实是一个描述文件中各个段的数组,段表描述了文件中各个段在文件中的偏移位置及段的属性等,从段表里面可以得到每个段的所有信息。
.bss
段只是为未初始化的全局变量和静态局部变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。
总体来说,程序源代码被编译以后主要分成两种段: 程序指令 和 程序数据。
代码段(.text)属于程序指令,而数据段(.data) 和 .bss端属于程序数据。
为什么要将代码段和数据段分开存放,而不是简单的混杂放在一个段里面?因为将它们分开存放有以下几点好处:
示例程序 SimipleSection.c
:
int printf(const char* format, ...);
int global_init_var = 64;
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生成目标文件:
[linux] gcc -c SimpleSection.c
//生成目标文件 SimpleSection.o
使用 objdump -h
命令可查看.o目标文件的ELF头中内容:
(ELF文件头中的内容描述了整个文件的基本属性,例如:ELF文件版本、体系结构、起始地址等)
[linux] objdump -h SimpleSection.o
//objdump:
//-h, --[section-]headers Display the contents of the section headers
//objdump的参数“-h”就是把ELF文件的各个段的基本信息打印出来。
//也可以使用“-x”打印更多信息
SimpleSection.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 0000005f 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 000000a0 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 000000a8 2**2
ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 000000a8 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002b 0000000000000000 0000000000000000 000000ac 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000d7 2**0
CONTENTS, READONLY
6 .note.gnu.property 00000020 0000000000000000 0000000000000000 000000d8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .eh_frame 00000058 0000000000000000 0000000000000000 000000f8 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
.o目标文件中的各个段在ELF文件中的结构如下图所示:
<br.>
.text
代码段通过 objdump -s -d
命令进行反汇编后,可以看出与C源程序的代码内容完全一致。
.data
段 保存的是 已初始化的全局静态变量和已初始化的局部静态变量。
使用 objdump -s
可以看到目标文件中的 .data段的内容:
[linux] objdump -s SimpleSection.o
SimpleSection.o: 文件格式 elf64-x86-64
Contents of section .data:
0000 40000000 55000000 @...U...
x86 CPU的字节序是小端序,高地址存放低字节,所以 0x40 是第一个字节,十进制为64,即对应的是源代码中的 global_init_var
变量,40000000
长度为4个字节,32比特,刚好对应一个int型;55000000
第一个字节 0x50 = 85,对应源代码中的 static_var
。
使用 size
命令可以看到 SimpleSection.o 目标文件中 .data
段的大小为 8个字节:
[linux] size SimpleSection.o
text data bss dec hex filename
219 8 4 231 e7 SimpleSection.o
.rodata
段存放的是只读数据,一般是程序里const关键字修饰的只读变量和 字符串常量。
有时候编译器会把 字符串常量 放在 .data段,而不会放在 .rodata段。这种行为因编译器而异。
使用 size
命令可以看到 .bss 段的大小为4,但是使用 objdump -s
命令却发现目标文件中没有 .bss 段的内容。
因为 .bss段中存放的是未初始化的全局变量和未初始化的局部静态变量,它们当前是没有初始化值的,所以没有必要在ELF文件中开辟空间为其存放值,更确切的说法是 “.bss段为它们预留了空间”。
另外,SimpleSection.c中有两个变量 global_uninit_var
和 static_var2
都属于未初始化的静态变量,为什么目标文件 SimpleSection.o 中的 .bss段的大小是 4字节而不是 8字节?
这与编译器有关,有些编译器会将 “全局未初始化变量”存放在 .bss段,有些则不放。
!!! 重要:ELF文件结构:
文件头:(ELF Header)
用于描述整个ELF文件的基本属性,例如:ELF文件版本、目标机器型号、程序入口地址等;
段表:(Section Header Table)
段表是一个结构体数组,用于描述ELF文件中的所有段的信息,包括每个段的段名、段长度、偏移量、读写权限等属性。
ELF Header :
描述整个文件的基本属性,例如:ELF文件版本、目标机器型号、程序入口地址 等。
使用 objdump -x
就能看到 ELF“文件头”中的所有信息:
[linux] objdump -x SimpleSection.o
SimpleSection.o: 文件格式 elf64-x86-64
SimpleSection.o
体系结构:i386:x86-64, 标志 0x00000011:
HAS_RELOC, HAS_SYMS
起始地址 0x0000000000000000
节:
Idx Name Size VMA LMA File off Algn
0 .text 0000005f 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 000000a0 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 000000a8 2**2
ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 000000a8 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002b 0000000000000000 0000000000000000 000000ac 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000d7 2**0
CONTENTS, READONLY
6 .note.gnu.property 00000020 0000000000000000 0000000000000000 000000d8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .eh_frame 00000058 0000000000000000 0000000000000000 000000f8 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 SimpleSection.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000004 l O .data 0000000000000004 static_var.1920
0000000000000000 l O .bss 0000000000000004 static_var2.1921
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .note.gnu.property 0000000000000000 .note.gnu.property
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g O .data 0000000000000004 global_init_var
0000000000000004 O *COM* 0000000000000004 global_uninit_var
0000000000000000 g F .text 0000000000000028 func1
0000000000000000 *UND* 0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000 *UND* 0000000000000000 printf
0000000000000028 g F .text 0000000000000037 main
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000017 R_X86_64_PC32 .rodata-0x0000000000000004
0000000000000021 R_X86_64_PLT32 printf-0x0000000000000004
000000000000003d R_X86_64_PC32 .data
0000000000000043 R_X86_64_PC32 .bss-0x0000000000000004
0000000000000056 R_X86_64_PLT32 func1-0x0000000000000004
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
0000000000000040 R_X86_64_PC32 .text+0x0000000000000028
文件头的内容存放在 /usr/include/elf.h
头文件中的 Elf32_Ehdr
结构体中:
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ //Magic, Class, Data, Version, OS/ABI, AB Version
Elf32_Half e_type; /* Object file type */ //ELF文件类型: ET_REL=1, 可重定位文件(.o); ET_EXEC=2, 可执行文件; ET_DYN=3, 共享目标文件(.so);
Elf32_Half e_machine; /* Architecture */ //该ELF文件支持的运行平台,ELF文件格式被设计成可以在多个平台下使用,但不意味着同一个ELF文件可以在多个平台下运行,而是不同平台下的ELF文件都遵循同一套ELF标准。以“EM_”开头: EM_M32: AT&T; EMP_SPARC: SPARC; EM_386: Intel x86; EM_860: Intel 80860; EM_68K: Motorola 68000; EM_88K: Motorola 88000.
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
使用 readelf -h
命令查看 SimpleSection.o目标文件中的内容:
readelf -h SimpleSection.o
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 1184 (bytes into file)
标志: 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: 13
其中,Elf32_Ehdr
结构体中的e_ident
成员对应了 readelf输出结果中的 Class
、Data
、Version
、OS/ABI
和 ABI Version
这5个参数,其中还包括 Magic魔数,其余的 Elf32_Ehdr
结构体中的成员与 readelf输出结果的参数一一对应。
段表用于描述ELF文件中的各个段(.text, .data, .bss 等)的信息,例如:每个段的 段名、段的长度、在文件中的偏移量、读写权限、其他属性。
段表的结构:
它是一个以 Elf32_Shdr
结构体为元素的数组,数组元素个数等于段的个数,每个 Elf32_Shdr
结构体对应一个段,又称为 “段描述符”(Section Descriptor)。
Elf32_Shdr
结构体的结构:(用于描述一个段的属性)
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */ //段的名字,如: .text, .data, .rodata, .bss 等
Elf32_Word sh_type; /* Section type */ //段的类型,以“SHT_”开头,如: SHT_NULL:无效段; SHT_PROBITS:程序段、代码段、数据段都是这种类型; SHT_SYMTAB:符号表; SHT_RELA:字符串表; SHT_DNYSYM:动态链接的符号表;
Elf32_Word sh_flags; /* Section flags */ //段的标志位:SHF_WRITE:表示该段在进程空间中可写; SHF_ALLOC:表示该段需要在进程虚拟空间中分配空间,代码段、数据段都需要这个标志位; SHF_EXCINSTR:表示该段可执行,一般指代码段;
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;
链接过程的本质就是要把多个不同的目标文件相互“粘”在一起,组成一个整体。
为了使不同的目标文件之间能够相互粘合,这些目标文件之间需要有固定的规则才行。
在链接中,目标文件之间相互拼合实际上是目标文件回见对 地址 的引用,即对函数和变量地址的引用。
例如目标文件B引用了目标文件A中的函数foo(),那么就称目标文件A中 “定义”(Define) 了函数foo(),称目标文件B中 “引用”(Reference) 了目标文件A中的函数foo()。
“定义”和“引用”这两个概念也同样适用于变量。
每个函数和变量都要有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。
在链接中,将函数和变量统称为 “符号”(Symbol),函数名和变量名就是 “符号表”(Symbol Name)。
我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。
链接过程中很关键的一部分就是符号的管理,每一个目标文件都有一个相应的 “符号表”(Symbol Table),表中记录目标文件所用到的所有符号。
每个定义的符号有一个对应的值,称为 “符号值”(Symbol Value),对于变量和函数来说,符号值就是它们的地址。
符号表中的符号可以分为以下几类:
对于链接过程来说,最值得关注的就是 “全局符号”,因为链接过程只关心全局符号的相互粘合,局部符号、段名、行号等都是次要的,它们对于目标文件来说是不可见的,在链接过程中也是无关紧要的。
可用于查看ELF文件符号的工具:readelf
、objdump
、nm
等。
使用 nm
命令列出 SimpleSection.o目标文件中的所有符号:
(大写表示全局,如T;小写表示局部,如d)
nm SimpleSection.o
0000000000000000 T func1 //T: 表示该符号位于 代码段(.text)
0000000000000000 D global_init_var //D: 表示该符号位于 数据段(.data)
0000000000000004 C global_uninit_var //C: 表示该符号为 COMMON
0000000000000028 T main
U printf //U: 表示该符号在当前文件中是“未定义的”,即该符号定义在其他的文件中
0000000000000004 d static_var.1920
0000000000000000 b static_var2.1921
使用 objdump -x
命令查看SimpleSection.o目标文件中的符号:
[linux] objdump -x SimpleSection.o
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 SimpleSection.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000004 l O .data 0000000000000004 static_var.1920
0000000000000000 l O .bss 0000000000000004 static_var2.1921
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .note.gnu.property 0000000000000000 .note.gnu.property
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g O .data 0000000000000004 global_init_var
0000000000000004 O *COM* 0000000000000004 global_uninit_var
0000000000000000 g F .text 0000000000000028 func1
0000000000000000 *UND* 0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000 *UND* 0000000000000000 printf
0000000000000028 g F .text 0000000000000037 main
使用 readelf -s
查看ELF文件中的符号:
[linux] readelf -s SimpleSection.o
Symbol table '.symtab' contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS SimpleSection.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_var.1920
7: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var2.1921
8: 0000000000000000 0 SECTION LOCAL DEFAULT 7
9: 0000000000000000 0 SECTION LOCAL DEFAULT 8
10: 0000000000000000 0 SECTION LOCAL DEFAULT 9
11: 0000000000000000 0 SECTION LOCAL DEFAULT 6
12: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var
13: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var
14: 0000000000000000 40 FUNC GLOBAL DEFAULT 1 func1
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
17: 0000000000000028 55 FUNC GLOBAL DEFAULT 1 main
Num : 表示符号表数组的下标
Value : 符号值
Size : 符号大小
Type : 符号类型
Bind : 绑定信息
Vis : 目前在C/C++中未使用,暂时忽略
Ndx : 表示该符号所属的段
Name : 符号名称
ELF符号表示一个结构体数组,数组中的每个元素对应一个 Elf32_Sym
结构体,一个结构体对应一个符号:
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */ //符号名
Elf32_Addr st_value; /* Symbol value */ //符号对应的值,可能是一个地址,可能是一个绝对值,这取决于不同的符号
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
C为了与C兼容,在符号管理上,C有一个用来声明或定义一个C的符号的 extern "C"
关键字:
extern "C" {
int func(int);
int var;
}
用 extern "C" { }
大括号括起来的部分的代码,C编译器会将其当作C语言代码来处理,此时C的名称修饰机制(name-manling)将不会起作用。
例如:
[linux] file SimpleSection.o
SimpleSection.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
objdump命令有点像那种快速查看之类的工具,就是以一种可阅读的格式让你更多的了解 二进制文件 可能带有的附加信息。
对于一个只想让自己的程序跑起来的程序员,这个命令没有更多意义;对于想进一步了解系统的程序员,应该掌握这个工具。
objdump -h, --[section-]headers Display the contents of the section headers
//查看目标文件的“文件头”,可以看到目标文件的代码段、数据段的大小、属性等信息
objdump -x, --all-headers Display the contents of all headers
//“-h”只是查看目标文件的“文件头”信息,内容精简;“-x”用于查看目标文件的所有信息,内容丰富复杂
objdump -s, --full-contents Display the full contents of all sections requested
//将所有的段内容以十六进制的方式打印出来
objdump -d, --disassemble Display assembler contents of executable sections
//将所有包含指令的段进行反汇编(将目标文件由机器语言格式 反汇编 成为汇编语言格式,在这里是将ELF格式文件反汇编成为汇编语言文件)
objdump -r, --reloc Display the relocation entries in the file
//查看ELF文件的重定位表,即查看每个重定位入口
例如:
[linux] size SimpleSection.o
text data bss dec hex filename
219 8 4 231 e7 SimpleSection.o
理解:
objdump -h SimpleSection.o
命令看到的东西更倾向于文件的 “内容”、“结构”;
readelf -h SimpleSection.o
命令看到的东西更倾向于文件的 “属性”;
readelf -h --file-header Display the ELF file header
//查看ELF文件的“文件头”信息
readelf -s --syms Display the symbol table
--symbols An alias for --syms
//查看ELF文件的“符号表”信息
readelf -S --section-headers Display the sections' header
--sections An alias for --section-headers
//查看ELF文件的“段表头”信息
“nm”
是 names的缩写,nm命令用于列出某些文件中的符号(函数、全局变量等)。
例如:
nm SimpleSection.o
0000000000000000 T func1
0000000000000000 D global_init_var
U _GLOBAL_OFFSET_TABLE_
0000000000000004 C global_uninit_var
0000000000000028 T main
U printf
0000000000000004 d static_var.1920
0000000000000000 b static_var2.1921
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/ArtAndLife/article/details/121200109
内容来源于网络,如有侵权,请联系作者删除!