编译器编译源代码后生成的文件叫目标文件;
从结构上来说与可执行文件一致,只是还没有经过动态链接的过程,有符号还没有被调整。与真正可执行文件稍有区别。
可执行文件格式涵盖了程序的编译、链接、装载和执行的各个方面。
windows下的PE和Linux下的ELF,都是COFF格式的变种。
目标文件(Linux下的.o win下的.obj)与可执行文件就差了个链接过程,一般存储格式是一样的,在Linux下为ELF文件。
动态链接库(DLL .dll和.so )以及静态链接库(Static Linking Library .lib和.a)也都是按照可执行文件格式存储的。
ELF文件类型
可重定位文件(Relocatable File)包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态库也可以归为此类
可执行文件(Executable File)ELF可执行文件 已经重定位的文件
共享目标文件(Shared Object File)包含了代码和数据,两种情况:一种是静态链接器可以使用进行重定位产生新的目标文件,二是动态链接库,例如.so 和.lib。
核心转储文件(Core Dump File) 就是程序crash之后产生的dump文件,保存了程序crash时的各种现场,可以用于分析和调试程序crash的原因。
COFF的主要贡献是在目标文件中引入段的机制,不同的目标文件可以拥有不同数量以及不同类型的段,另外还定义了调试数据格式。
目标文件内容
内容包括编译后的机器指令代码、数据,以及一些链接时需要的信息:符号表、调试信息、字符串等。按照不同的属性,以“节”的形式进行存储,有时候也称“段”。
文件头:File Header (描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接以及入口地址(如果是可执行文件)、目标硬件、目标操作系统 )
(段表:属于文件头中一部分,段表是一个描述文件中各个段的数组,描述了文件中各段在文件的偏移位置以及段的属性)
代码段:.code .text 机器代码 汇编
数据段:.data 以及初始化的全局变量和局部静态变量
BSS(Block Started by Symbol)未初始化段: .bss 未初始化的全局变量和局部静态变量预留位置, 默认值为0, 在.data段中分配空间并存放数据0是没有必要的,elf文件中只是记录了需要分配内存的总和,该段不占用空间。
可执行文件必须记录所有全局变量和局部静态变量的大小总和。
此外还有.rodata .common等。
程序代码编译后主要分为两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss段属于程序数据。
数据和指令分开的好处:1:读写权限;2:CPU cache缓存问题,提升cache命中率;3:指令可以复用。
下面以64位Ubuntu系统为例进行演示
代码示例
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 SimpleSection.o
代码段、数据段、.bss段、只读数据段(.rodata)、注释信息段(.comment)和堆栈提示段(.note.GNU-stack)
size SimpleSection.o
objdump -s -d SimpleSection.o
一般而言bss是未初始化的全局变量和局部静态变量预留位置 ,这里有两个global_uninit_var和 static_var2,应该是8,实际才是4。实际这里只存了static_var2;
这跟强符号和弱符号有关系,global_uninit_var是等到最终链接成可执行文件时才在bss段分配标记。
自定义段:
有时候可能希望变量或者部分代码能够放到你所指定的段中去,以实现某些特定的功能。比如为了满足某些硬件的内存和I/O的地址布局,或者像Linux内核中用来完成一些初始化和用户空间复制时出现页错误异常等。
__attribute__((section("FOO"))) int global = 42;
在全局变量或函数之前加上“__attribute__((section("name")))”属性就可以把相应的变量或者函数放在以name作为段名的段中。
ELF文件的总体结构:
ELF Header |
.text |
.data |
.bss |
… Other sections |
Section header Table |
String Tables Symbol Tables |
readelf -h 查看命令的帮助信息,需要正确区分文件头、section头、程序头(可执行程序才有)。
程序头是可执行程序load和exec时使用的,在汇编和链接过程中用不到;
相应的Section头在load时也用不上。
ELF file Header : 描述了整个文件的基本属性,比如ELF文件版本,目标芯片类型,程序入口地址(Entry point address)
readelf -S 显示段表(Section Header Table)段表是elf文件除了文件头以外最重要的结构,描述了各个段的信息,比如各个段的段名,段的长度,在文件中的偏移、读写权限及段的其他属性。
编译器、链接器和装载器都是依靠段来定位和访问各个段的属性的,对比objdump -h也可以查看段表信息,但是不是完整的。
这里起始地址0x430等于上面的1072。
ELF相关的结构体定义在 “usr/include/elf.h”中,对应ndk的位置:./toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/linux/elf.h
关于段表的描述为一个结构体(Elf64_Shdr sizeof=64 (size of section header))数组,如上,对应长度为13的数组;
最终的文件结构顺序以及长度如下:
序号 | 段内容 | 长度 | 偏移 | 对齐 |
ELF file Header | 0x40 | 0 | ||
1 | .text | 0x55 | 0x40 | 1 |
2 | .data | 0x8 | 0x98 | 4 |
3 | .bss | 0 | 0xa0 | |
4 | .rodata | 0x4 | 0xa0 | 1 |
5 | .comment | 0x36 | 0xa4 | 1 |
6 | .note.GNU-stack | 0 | 0xda | |
7 | .eh_frame | 0x58 | 0xe0 | 8 |
8 | .symtab 符号表 | 0x180 | 0x138 | 8 |
9 | .strtab | 0x66 | 0x2b8 | 1 |
10 | .rela.text | 0x78 | 0x320 | 8 |
11 | .rela.eh_frame | 0x30 | 0x398 | 8 |
12 | .shstrtab | 0x61 | 0x3c8 | 1 |
段表 | Section header | 0x340 = 0x40*13 | 0x430 | 4? |
文件长度: | 0x770=1904 |
这与文件大小是可以对应上的:
readelf参考:
参考书籍:
《程序员的自我修养》