目标文件就是源代码编译后但未进行链接的那些中间文件(windows的.obj
和linux下的.o
),它跟可执行文件的内容与结构很相似,所以一般跟可执行文件格式一起采用一个格式存储。
平台 | 可执行文件格式 | 目标文件格式 | 动态链接库 | 静态链接库 |
---|---|---|---|---|
Linux | ELF | .o | .so | .a |
Windows | PE | .obj | .dll | .lib |
ELF文件类型 | 说明 |
---|---|
可重定位文件(Relocatable File) | 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类 |
可执行文件(executable file) | 这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,他们一般都没有扩展名 |
共享目标文件(Shared Object File) | 这种文件包含了代码和数据,可以在一下2种情况下使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行。 |
核心转储文件(Core Dump File) | 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件。 |
在Linux中,可以用file命令来查看相应的类型。
ELF文件的开头是一个文件头(File Header),它描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息,文件头还包括了一个段表(Section Table)。
段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等,从段表里面可以得到每个段的所有信息。
对照上图,一般C语言的编译后执行语句都编译成机器代码,保存在.text section
;已初始化的全局变量和局部静态变量一般放在.data section
;未初始化的全局变量和局部静态变量一般放在一个叫.bss section
。
我们知道未初始化的全局变量和局部变量默认值都是0,本来它们也可以被放在.data section
,但是因为它们都是0,所以为它们在.data section
分配空间并存放数据0是没有必要的。程序运行的时候它们的确是要占内存空间,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,记为.bss section
。所以.bss section
只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。
总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss
段属于程序数据。
为什么要将程序的指令和数据分开存放,原因有:
代码3-1
/*
* SimpleSection.c
*
* Linux:
* gcc -c -m32 SimpleSection.c
* -m32编译成32位
*
* Windows:
* c1 SimpleSection /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;
}
objdump -h SimpleSection.o
查看目标文件的结构和内容:
SimpleSection.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000062 00000000 00000000 00000034 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 00000000 00000000 00000098 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 00000000 00000000 000000a0 2**2
ALLOC
3 .rodata 00000004 00000000 00000000 000000a0 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 00000036 00000000 00000000 000000a4 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 00000000 00000000 000000da 2**0
CONTENTS, READONLY
6 .eh_frame 00000064 00000000 00000000 000000dc 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
SimpleSection.o
除了最基本的代码段,数据段和BSS段外,还有其他的只读数据段(.rodata
)、注释信息段(.comment
)和堆栈提示段(.note.GNU-stack
)。
objdump -s -d SImpleSection.o
查看代码段的内容:
SimpleSection.o: file format elf32-i386
Contents of section .text:
0000 5589e583 ec0883ec 08ff7508 68000000 U.........u.h...
0010 00e8fcff ffff83c4 1090c9c3 8d4c2404 .............L$.
0020 83e4f0ff 71fc5589 e55183ec 14c745f0 ....q.U..Q....E.
0030 01000000 8b150400 0000a100 00000001 ................
0040 c28b45f0 01c28b45 f401d083 ec0c50e8 ..E....E......P.
0050 fcffffff 83c4108b 45f08b4d fcc98d61 ........E..M...a
0060 fcc3 ..
......
Disassembly of section .text:
00000000 :
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 08 sub $0x8,%esp
6: 83 ec 08 sub $0x8,%esp
9: ff 75 08 pushl 0x8(%ebp)
c: 68 00 00 00 00 push $0x0
11: e8 fc ff ff ff call 12
16: 83 c4 10 add $0x10,%esp
19: 90 nop
1a: c9 leave
1b: c3 ret
0000001c :
1c: 8d 4c 24 04 lea 0x4(%esp),%ecx
20: 83 e4 f0 and $0xfffffff0,%esp
23: ff 71 fc pushl -0x4(%ecx)
26: 55 push %ebp
27: 89 e5 mov %esp,%ebp
29: 51 push %ecx
2a: 83 ec 14 sub $0x14,%esp
2d: c7 45 f0 01 00 00 00 movl $0x1,-0x10(%ebp)
34: 8b 15 04 00 00 00 mov 0x4,%edx
3a: a1 00 00 00 00 mov 0x0,%eax
3f: 01 c2 add %eax,%edx
41: 8b 45 f0 mov -0x10(%ebp),%eax
44: 01 c2 add %eax,%edx
46: 8b 45 f4 mov -0xc(%ebp),%eax
49: 01 d0 add %edx,%eax
4b: 83 ec 0c sub $0xc,%esp
4e: 50 push %eax
4f: e8 fc ff ff ff call 50
54: 83 c4 10 add $0x10,%esp
57: 8b 45 f0 mov -0x10(%ebp),%eax
5a: 8b 4d fc mov -0x4(%ebp),%ecx
5d: c9 leave
5e: 8d 61 fc lea -0x4(%ecx),%esp
61: c3 ret
对照下面的反汇编内容,可以很明显地看到,.text
段里面所包含的正是SimpleSection.c
里两个函数func1()
和main()
的指令。.text
段的第一个字节0x55
就是func1()
函数的第一条push %ebp
指令,而最后一个字节0xc3
正是main()
函数的最后一条指令ret
。
# objdump -x -s -d SimpleSection.o
SimpleSection.o: file format elf32-i386
SimpleSection.o
architecture: i386, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000
Sections:
Idx Name Size VMA LMA File off Algn
1 .data 00000008 00000000 00000000 00000098 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .rodata 00000004 00000000 00000000 000000a0 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
......
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
......
.data
段保存的是那些已经初始化了的全局变量和局部静态变量。SimpleSection.c
代码中一共有2个这样的变量,分别是global_init_varabal
与static_var
,一共8个字节,所以.data
段的大小为8个字节。
调用printf
的时候,用到了一个字符串常量%d\n
,它是一种只读数据,所以它被放到了.rodata
段。该段大小为4个字节,刚好是这个字符串常量的ASCII字节序,最后以\0
结尾。
我们注意到,.data
段里的前4个字节,从低到高分别为0x54 0x00 0x00 0x00
,该值刚好是global_init_varabal,即84。这里使用的是小端存储。
.bss
段存放的是没初始化的全局变量和局部静态变量。如上述代码中global_uninit_var
和static_var2
就是被存放在.bss
段,其实更准确的说法是为它们预留了空间。但是,该段大小只有4个字节,2个变量的大小为8个字节?
在随后的符号表(Symbol Table)中,只有static_var2
被存放在.bss
段,而global_uninit_var
却没有被存放在任何段中,只有一个未定义的COMMON
符号。
SYMBOL TABLE:
00000000 l df *ABS* 00000000 SimpleSection.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l d .rodata 00000000 .rodata
00000004 l O .data 00000004 static_var.1488
00000000 l O .bss 00000004 static_var2.1489
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000000 g O .data 00000004 global_init_var
00000004 O *COM* 00000004 global_uninit_var
00000000 g F .text 0000001c func1
00000000 *UND* 00000000 printf
0000001c g F .text 00000046 main
Quiz 变量存放位置
static int x1 = 0;
static int x2 = 1;
x1和x2会被放在什么段中呢?
x1会被放在.bss
中,x2会被放在.data
中。x1为0,可以认为是没初始化的,因为没初始化的都是0,所以被优化掉了可以放在.bss
,这样可以节省磁盘空间,因为.bss
不占磁盘空间。另一个变量x2初始值为1,是初始化的,所以放在.data
段中。
常用的段名 | 说明 |
---|---|
.rodata1 | Read only Data,这种段里存放的是只读数据,比如字符串常量、全局const变量。跟.rodata 一样 |
.comment | 存放的是编译器版本信息,比如字符串:GCC:(GNU) 4.2.0 |
.debug | 调试信息 |
.dynamic | 动态链接信息 |
.hash | 符号哈希表 |
.line | 调试时的行号表,即源代码行号与编译后指令的对应表 |
.note | 额外的编译器信息。比如程序的公司名,发布版本号 |
.strtab | String Table。字符串表,用于存储ELF文件中用到的各种字符串 |
.symtab | Symbol Table。符号表 |
.shstrtab | Section String Table。段名表 |
.plt .got | 动态链接的跳转表和全局入口表 |
.init .fini | 程序初始化与终结代码段 |
Q:如果我们要将一个二进制文件,比如图片,MAP3音乐一类的东西作为目标文件中的一个段,该怎么做?
A:可以使用objcopy
工具,比如我们有一个图片文件image.jpg
,大小为242,434 bytes
。
$ objcopy -I binary -O elf32-i386 -B i386 image.jpg image.o
$ objdump -ht image.o
image.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .data 0003b302 00000000 00000000 00000034 2**0
CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
00000000 l d .data 00000000 .data
00000000 g .data 00000000 _binary_image_jpg_start
0003b302 g .data 00000000 _binary_image_jpg_end
0003b302 g *ABS* 00000000 _binary_image_jpg_size
_binary_image_jpg_start
、_binary_image_jpg_end
、_binary_image_jpg_size
表示图片文件在内存中的起始地址、结束地址和大小,我们可以在程序里面直接声明并使用它们。
自定义段
有时候你希望变量或某些代码能够放到你指定的段中,以实现某些特定的功能。比如为了满足硬件的内存和I/O
的地址布局,或者是像Linux操作系统内核中用来完成一些初始化和用户空间复制时,出现页错误异常等。
__attribute__((section("FOO"))) int global = 42;
__attribute__((section("BAR"))) void foo()
{
}
ELF目标文件格式的最前部是ELF文件头(ELF Header),它包含了描述整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址等。紧接着是ELF文件各个段。其中ELF文件中与段有关的重要结构就是段表(Section Header Table),该表描述了ELF文件的所有段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。
用readelf -h SimpleSection.o
来查看ELF文件:
$ readelf -h SimpleSection.o
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 832 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 13
Section header string table index: 10
ELF文件头结构及相关参数被定义在/usr/include/elf.h
,有32版本和64为版本。
/* Type for a 16-bit quantity. */
typedef uint16_t Elf32_Half;
typedef uint16_t Elf64_Half;
/* Types for signed and unsigned 32-bit quantities. */
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;
typedef uint32_t Elf64_Word;
typedef int32_t Elf64_Sword;
/* Types for signed and unsigned 64-bit quantities. */
typedef uint64_t Elf32_Xword;
typedef int64_t Elf32_Sxword;
typedef uint64_t Elf64_Xword;
typedef int64_t Elf64_Sxword;
/* Type of addresses. */
typedef uint32_t Elf32_Addr;
typedef uint64_t Elf64_Addr;
/* Type of file offsets. */
typedef uint32_t Elf32_Off;
typedef uint64_t Elf64_Off;
/* Type for section indices, which are 16-bit quantities. */
typedef uint16_t Elf32_Section;
typedef uint16_t Elf64_Section;
/* Type for version symbol information. */
typedef Elf32_Half Elf32_Versym;
typedef Elf64_Half Elf64_Versym;
32位版本的文件头结构Elf32_Ehdr
作为例子来描述:
/* The ELF file header. This appears at the start of every ELF file. */
#define EI_NIDENT (16)
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
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输出结果与含义 |
---|---|
e_ident | 包括:Magic Class Data Version OS/ABI ABI Version |
e_type | Type ELF文件类型 |
e_machine | Machine ELF文件的CPU平台属性。相关常量以EM_ 开头 |
e_version | Version ELF版本号。一般为常数1 |
e_entry | Entry point address 入口地址,规定ELF程序的入口虚拟地址,操作系统在加载完该程序后从这个地址开始执行的指令。可重定位文件一般没有入口地址,则这个值为0 |
e_phoff | Start of program headers 略过 |
e_shoff | Start of section headers 段表在文件中的偏移,上面的例子里这个值是832,也就是段表从文件的第833个字节开始 |
e_flags | Flags ELF标志位,用来标识平台相关属性 |
e_ehsize | Size of this header ELF头文件本身的大小 |
e_phentsize | Size of program headers 略过 |
e_phnum | Number of program headers 略过 |
e_shentsize | Size of section headers 段表描述符的大小,一般为sizeof(Elf32_Shdr) |
e_shnum | Number of section headers 段表描述符数量。这个值等于ELF文件中拥有的段的数量 |
e_shstrndx | Section header string table index 段表字符串表所在的段在段表中的下标 |
ELF魔数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
------------------------------------------------
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
DEL E L F
。0x01
表示32位的,0x02
标识64位。0
无效格式,1
小端,2
大端文件类型
/* Legal values for e_type (object file type). */
#define ET_NONE 0 /* No file type */
#define ET_REL 1 /* Relocatable file */
#define ET_EXEC 2 /* Executable file */
#define ET_DYN 3 /* Shared object file */
#define ET_CORE 4 /* Core file */
#define ET_NUM 5 /* Number of defined types */
#define ET_LOOS 0xfe00 /* OS-specific range start */
#define ET_HIOS 0xfeff /* OS-specific range end */
#define ET_LOPROC 0xff00 /* Processor-specific range start */
#define ET_HIPROC 0xffff /* Processor-specific range end */
机器类型
ELF文件格式被设计成可以在多个平台下使用。这并不是表示同一个ELF文件可以在不同的平台下使用,而是表示不同平台下的ELF文件都遵循同一套ELF标准。
常量 | 值 | 含义 |
---|---|---|
EM_M32 | 1 | AT&T WE 32100 |
EM_SPARC | 2 | SPARC |
EM_386 | 3 | Intel x86 |
EM_68K | 4 | Motorola 68000 |
EM_88K | 5 | Motorola 88000 |
EM_860 | 6 | Intel 80860 |
段表描述了ELF的各个段的信息,比如每个段的段名、段的长度、在文件中的偏移量、读写权限及段的其他属性。段表在ELF、文件中的位置有ELF文件头的e_shoff
$ readelf -S SimpleSection.o
There are 13 section headers, starting at offset 0x340:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000062 00 AX 0 0 1
[ 2] .rel.text REL 00000000 0002a8 000028 08 I 11 1 4
[ 3] .data PROGBITS 00000000 000098 000008 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 0000a0 000004 00 WA 0 0 4
[ 5] .rodata PROGBITS 00000000 0000a0 000004 00 A 0 0 1
[ 6] .comment PROGBITS 00000000 0000a4 000036 01 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 00000000 0000da 000000 00 0 0 1
[ 8] .eh_frame PROGBITS 00000000 0000dc 000064 00 A 0 0 4
[ 9] .rel.eh_frame REL 00000000 0002d0 000010 08 I 11 8 4
[10] .shstrtab STRTAB 00000000 0002e0 00005f 00 0 0 1
[11] .symtab SYMTAB 00000000 000140 000100 10 12 11 4
[12] .strtab STRTAB 00000000 000240 000066 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
段表的结构是一个以Elf32_Shdr
结构体为元素的数组。数组元素的个数等于段的个数,每个Elf32_Shdr
结构体对应一个段。Elf32_Shdr
又被称为段描述符(Section Descriptor)。第一个类型为NULL
表示无效。
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
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;
段的类型(Sh_type)
段的名字只有在链接和编译过程中有意义,但是它不能真正的表示段的类型。我们也可以将一个数据段命名为.text
。对于编译器和链接器来说,主要决定段的属性的是段的类型sh_type
和段的标志位sh_flags
。段的类型相关常量以SHT_
开头。
/* Legal values for sh_type (section type). */
#define SHT_NULL 0 /* Section header table entry unused */
#define SHT_PROGBITS 1 /* Program data */
#define SHT_SYMTAB 2 /* Symbol table */
#define SHT_STRTAB 3 /* String table */
#define SHT_RELA 4 /* Relocation entries with addends */
#define SHT_HASH 5 /* Symbol hash table */
#define SHT_DYNAMIC 6 /* Dynamic linking information */
#define SHT_NOTE 7 /* Notes */
#define SHT_NOBITS 8 /* Program space with no data (bss) */
#define SHT_REL 9 /* Relocation entries, no addends */
#define SHT_SHLIB 10 /* Reserved */
#define SHT_DYNSYM 11 /* Dynamic linker symbol table */
#define SHT_INIT_ARRAY 14 /* Array of constructors */
#define SHT_FINI_ARRAY 15 /* Array of destructors */
#define SHT_PREINIT_ARRAY 16 /* Array of pre-constructors */
#define SHT_GROUP 17 /* Section group */
#define SHT_SYMTAB_SHNDX 18 /* Extended section indeces */
#define SHT_NUM 19 /* Number of defined types. */
#define SHT_LOOS 0x60000000 /* Start OS-specific. */
#define SHT_GNU_ATTRIBUTES 0x6ffffff5 /* Object attributes. */
#define SHT_GNU_HASH 0x6ffffff6 /* GNU-style hash table. */
#define SHT_GNU_LIBLIST 0x6ffffff7 /* Prelink library list */
#define SHT_CHECKSUM 0x6ffffff8 /* Checksum for DSO content. */
#define SHT_LOSUNW 0x6ffffffa /* Sun-specific low bound. */
#define SHT_SUNW_move 0x6ffffffa
#define SHT_SUNW_COMDAT 0x6ffffffb
#define SHT_SUNW_syminfo 0x6ffffffc
#define SHT_GNU_verdef 0x6ffffffd /* Version definition section. */
#define SHT_GNU_verneed 0x6ffffffe /* Version needs section. */
#define SHT_GNU_versym 0x6fffffff /* Version symbol table. */
#define SHT_HISUNW 0x6fffffff /* Sun-specific high bound. */
#define SHT_HIOS 0x6fffffff /* End OS-specific type */
#define SHT_LOPROC 0x70000000 /* Start of processor-specific */
#define SHT_HIPROC 0x7fffffff /* End of processor-specific */
#define SHT_LOUSER 0x80000000 /* Start of application-specific */
#define SHT_HIUSER 0x8fffffff /* End of application-specific */
段的标志位(sh_flag)
/* Legal values for sh_flags (section flags). */
#define SHF_WRITE (1 << 0) /* Writable */
#define SHF_ALLOC (1 << 1) /* Occupies memory during execution */
#define SHF_EXECINSTR (1 << 2) /* Executable */
#define SHF_MERGE (1 << 4) /* Might be merged */
#define SHF_STRINGS (1 << 5) /* Contains nul-terminated strings */
#define SHF_INFO_LINK (1 << 6) /* `sh_info' contains SHT index */
#define SHF_LINK_ORDER (1 << 7) /* Preserve order after combining */
#define SHF_OS_NONCONFORMING (1 << 8) /* Non-standard OS specific handlin required */
#define SHF_GROUP (1 << 9) /* Section is member of a group. */
#define SHF_TLS (1 << 10) /* Section hold thread-local data. */
#define SHF_COMPRESSED (1 << 11) /* Section with compressed data. */
#define SHF_MASKOS 0x0ff00000 /* OS-specific. */
#define SHF_MASKPROC 0xf0000000 /* Processor-specific */
#define SHF_ORDERED (1 << 30) /* Special ordering requirement (Solaris). */
#define SHF_EXCLUDE (1U << 31) /* Section is excluded unless referenced or allocated (Solaris).*/
对于系统保留段,下面列举了它们的属性:
Name | sh_type | sh_flag |
---|---|---|
.bss | SHT_NOBITS | SHF_ALLOC + SHF_WRITE |
.comment | SHT_PROGBITS | none |
.data | SHT_PROGBITS | SHF_ALLOC+SHF_WRITE |
.data1 | SHT_PROGBITS | SHF_ALLOC+SHF_WRITE |
.debug | SHT_PROGBITS | none |
.dynamic | SHT_DYNAMIC | SHF_ALLOC+SHF_WRITE。在有些系统下.dynamic 段可能是只读的,所以没有SHF_WRITE |
.hash | SHT_HASH | SHF_ALLOC |
.line | SHT_PROGBITS | none |
.note | SHT_NOTE | none |
.rodata | SHT_PROGBITS | SHF_ALLOC |
.rodata1 | SHT_PROGBITS | SHF_ALLOC |
.shstrtab | SHT_STRTAB | none |
.strtab | SHT_STRTAB | 如果该ELF文件中有可能装载的段须要用到该字符串表,那么该字符串表也将被装载到进程空间,那么有SHF_ALLOC标志位 |
.symtab | SHT_SYMTAB | 同字符串表 |
.text | SHT_PROGBITS | SHF_ALLOC+SHF_EXECINSTR |
段的链接信息
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 该段所使用的字符串表在段表中的下标 | 0 |
SHT_HASH | 该段所使用的符号表在段表中的下标 | 0 |
SHT_REL SHT_RELA |
该段所使用的相应符号表在段表中的下标 | 该重定位表所作用的段在段表中的下标 |
SHT_SYMTAB SHT_DYNSYM |
操作系统相关 | 操作系统相关 |
other | SHN_UNDEF | 0 |
(见静态链接过程)
常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。
偏移 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
+0 | \0 |
h | e | l | l | o | w | o | r | l |
+10 | d | \0 |
M | y | v | a | r | i | a | b |
+20 | l | e | \0 |
偏移 | 字符串 |
---|---|
0 | 空字符串 |
1 | helloworld |
6 | world |
12 | Myvariable |
一般字符串表在ELF文件中也以段的形式保存,常见的段名为.strtab
或.shstrtab
。这两个字符串表分别为字符串表(String Table)和段字符串表(Section Header String Table)。字符串表用来保存普通的字符串,比如符号的名字;段表字符串表用来保存段表中用到的字符串,最常见的就是段名。
我们在前面提到过,e_shstrndx
是Elf32_Ehdr
的最后一个成员,它是Section Header string Table index
的缩写。我们知道段表字符串本身也是ELF文件中的一个普通的段,知道它的名字往往叫做.shstrtab
。那么这个e_shstrndx
就表示.shstrtab
在段表中的下标,即段表字符串在段表中的下标。我们可以得出结论,只有分析ELF文件头,就可以得到段表和段表字符串表的位置,从而解析整个ELF文件。
在链接中,目标文件直接相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。比如目标文件B要用到目标文件A中的函数foo
,那么我们就称目标文件A定义了函数foo
,称目标文件B引用了目标文件A中的函数foo
。这两个概念同意适用于变量。每个函数或变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。在链接中,我们将函数和变量统称为符号(Symbol),函数名和变量名就是符号名(Symbol Name)。
链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,符号值就是它们的地址。
符号分类:
SimpleSection.o
里面的func1
main
global_init_var
。SimpleSection.o
里面的printf
。SimpleSection.o
里面的.text
.data
SimpleSection.o
里面的static_var
和static_var2
。查看符号:
$ nm SimpleSection.o
00000000 T func1
00000000 D global_init_var
00000004 C global_uninit_var
0000001c T main
U printf
00000004 d static_var.1488
00000000 b static_var2.1489
符号表的段名为.symbol
。它的结构就是一个Elf32_Sym
数组,每个元素对于一个符号。第一个元素为无效或者没定义的符号。
/* Symbol table entry. */
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;
符号类型和绑定信息(st_info)
该成员低4位表示符号的类型Symbol Type
,高28为表示符号绑定信息Symbol Binding
。
/* Legal values for ST_BIND subfield of st_info (symbol binding). */
#define STB_LOCAL 0 /* Local symbol */
#define STB_GLOBAL 1 /* Global symbol */
#define STB_WEAK 2 /* Weak symbol */
#define STB_NUM 3 /* Number of defined types. */
#define STB_LOOS 10 /* Start of OS-specific */
#define STB_GNU_UNIQUE 10 /* Unique symbol. */
#define STB_HIOS 12 /* End of OS-specific */
#define STB_LOPROC 13 /* Start of processor-specific */
#define STB_HIPROC 15 /* End of processor-specific */
/* Legal values for ST_TYPE subfield of st_info (symbol type). */
#define STT_NOTYPE 0 /* Symbol type is unspecified */
#define STT_OBJECT 1 /* Symbol is a data object */
#define STT_FUNC 2 /* Symbol is a code object */
#define STT_SECTION 3 /* Symbol associated with a section */
#define STT_FILE 4 /* Symbol's name is file name */
#define STT_COMMON 5 /* Symbol is a common data object */
#define STT_TLS 6 /* Symbol is thread-local data object*/
#define STT_NUM 7 /* Number of defined types. */
#define STT_LOOS 10 /* Start of OS-specific */
#define STT_GNU_IFUNC 10 /* Symbol is indirect code object */
#define STT_HIOS 12 /* End of OS-specific */
#define STT_LOPROC 13 /* Start of processor-specific */
#define STT_HIPROC 15 /* End of processor-specific */
符号所在段(st_shndx)
/* Special section indices. */
#define SHN_UNDEF 0 /* Undefined section */
#define SHN_LORESERVE 0xff00 /* Start of reserved indices */
#define SHN_LOPROC 0xff00 /* Start of processor-specific */
#define SHN_BEFORE 0xff00 /* Order section before all others (Solaris). */
#define SHN_AFTER 0xff01 /* Order section after all others (Solaris). */
#define SHN_HIPROC 0xff1f /* End of processor-specific */
#define SHN_LOOS 0xff20 /* Start of OS-specific */
#define SHN_HIOS 0xff3f /* End of OS-specific */
#define SHN_ABS 0xfff1 /* Associated symbol is absolute */
#define SHN_COMMON 0xfff2 /* Associated symbol is common */
#define SHN_XINDEX 0xffff /* Index is in extra table. */
#define SHN_HIRESERVE 0xffff /* End of reserved indices */
符号值(st_value)
COMMON块
类型的(即st_shndex
不为SHN_COMMON
),则st_value
表示该符号在段中的偏移。即符号所对应的函数或变量位于st_shndex
指定的段,偏移st_value
的位置。这也是目标文件中定义全局变量的符号的最常见情况。比如,SimpleSection.o
中的func1
main
global_init_var
。COMMON块
类型,则st_value
表示该符号的对其属性。比如SimpleSection.o
中的global_uninit_var
。st_value
表示符号的虚拟地址。这个虚拟地址对于动态链接器来说十分有用。分析各个符号在符号表中的状态:
$ readelf -s SimpleSection.o
Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS SimpleSection.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 5
6: 00000004 4 OBJECT LOCAL DEFAULT 3 static_var.1488
7: 00000000 4 OBJECT LOCAL DEFAULT 4 static_var2.1489
8: 00000000 0 SECTION LOCAL DEFAULT 7
9: 00000000 0 SECTION LOCAL DEFAULT 8
10: 00000000 0 SECTION LOCAL DEFAULT 6
11: 00000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var
12: 00000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var
13: 00000000 28 FUNC GLOBAL DEFAULT 1 func1
14: 00000000 0 NOTYPE GLOBAL DEFAULT UND printf
15: 0000001c 70 FUNC GLOBAL DEFAULT 1 main
$ readelf -a SimpleSection.o
.......
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000062 00 AX 0 0 1
[ 2] .rel.text REL 00000000 0002a8 000028 08 I 11 1 4
[ 3] .data PROGBITS 00000000 000098 000008 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 0000a0 000004 00 WA 0 0 4
[ 5] .rodata PROGBITS 00000000 0000a0 000004 00 A 0 0 1
[ 6] .comment PROGBITS 00000000 0000a4 000036 01 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 00000000 0000da 000000 00 0 0 1
[ 8] .eh_frame PROGBITS 00000000 0000dc 000064 00 A 0 0 4
[ 9] .rel.eh_frame REL 00000000 0002d0 000010 08 I 11 8 4
[10] .shstrtab STRTAB 00000000 0002e0 00005f 00 0 0 1
[11] .symtab SYMTAB 00000000 000140 000100 10 12 11 4
[12] .strtab STRTAB 00000000 000240 000066 00 0 0 1
.......
func1
和main
函数都是定义在SimpleSection.c
里面,它们所在的位置都为代码段,所以Ndx
为1,即SimpleSection.o
里面,.text
段的下标为1。它们是函数,所以类型是SHT_FUNC
;它们是全局可见的,所以是STB_GLOBAL
;Size
表示函数指令所占的字节数;Value
表示函数相对于代码段起始位置的偏移量。printf
这个符号,该符号在SimpleSection.c
里面被引用,但是没有被定义。所以它的Ndx
是SHN_UNDEF
。global_init_var
是已初始化的全局变量,它被定义在.data
段,即下标为3。global_uninit_var
是未初始化的全局变量,它是一个SHN_COMMON
类型的符号,它本身并没有存在于.bss
段。static_var.1488
static_var2.1489
是两个静态变量,它们的绑定属性是STB_LOCAL
,即只是在编译单元内部可见。这里它们被符号修饰了。STT_SECTION
类型的符号,它们表示下标为Ndx
的段的段名。它们的符号名没有显示,其实它们的符号名即它们的段名。比如2号的符号的Ndx
为1,那么它即表示.text
段的段名,该符号的符号名应该就是.text
。SimpleSection.c
这个符号表示编译单元的源文件名。当我们使用ld
作为链接器来链接生产可执行文件时,它会为我们定义很多特殊的符号,这些符号并没有在你的程序中定义,但是你可以直接声明并且引用它,称为特殊符号。链接器会在将程序最终链接成可执行文件的时候将其解析成正确的值,注意,只有使用ld
链接生成最终可执行文件的时候这些符号才会存在。代表性的有:
_executable_start
,该符号为程序起始地址,注意,不是入口地址,是程序的最开始的地址。__etext
或_etext
或etext
,该符号为代码段结束地址,即代码段最末尾的地址。_edata
或edata
,该符号为数据段结束地址,即数据段最末尾的地址。_end
或end
,该符号为程序结束地址。/*
* gcc -m32 SpecialSymbol.c -o SpecialSymbol 编译不成功,提示:
* fatal error: sys/cdefs.h: No such file or directory #include
*
* 解决(https://stackoverflow.com/questions/23498237/compile-program-for-32bit-on-64bit-linux-os-causes-fatal-error):
* sudo apt-get install libx32gcc-4.8-dev libc6-dev-i386
*/
#include
extern char __executable_start[];
extern char etext[], _etext[], __etext[];
extern char edata[], _edata[];
extern char end[], _end[];
int main()
{
printf("Executable Start %X\n", __executable_start);
printf("Text End %X %X %X\n", etext, _etext, __etext);
printf("Data End %X %X\n", edata, _edata);
printf("Executable End %X %X\n", end, _end);
return 0;
}
$ gcc -m32 SpecialSymbol.c -o SpecialSymbol
$ ./SpecialSymbol
Executable Start 8048000
Text End 8048508 8048508 8048508
Data End 804A01C 804A01C
Executable End 804A020 804A020
多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将或出现符号重复定义的错误。
对于C/C++语言来说,编译器默认函数和初始化的全局变量为强符号,为初始化的全局变量为弱符号。我们可以通过GCC的__attribute__((weak))
来定义任何一个强符号为弱符号。注意,强符号和弱符号都是针对定义来说,不是针对符号的引用。
extern int ext;
int week;
int strong = 1;
__attribute__((weak)) weak2 = 2;
int main()
{
return 0;
}
上面这段代码中,weak
和weak2
是弱符号,strong
和main
是强符号,而ext
即非强符号也弱符号,因为它是一个外部变量的引用。针对强弱符号的概念,链接器会根据如下规则处理与选择被多次定义的全局符号:
弱引用和强引用
目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们须要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错我,这种被称为强引用。与之相对应的有一种弱引用。在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未定义,则链接器对于该引用不报错。
一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊值,以便于程序代码能够识别。弱引用和弱符号主要用于库的链接过程。
/*
* Weakref1.c
*/
__attribute__((weak)) void foo();
int main()
{
foo();
}
$ gcc -m32 Weakref1.c -o Weakref1
$ ./Weakref1
Segmentation fault (core dumped)
可以看到,编译没报错,执行时报错。
/*
* Weakref2.c
*/
void foo() __attribute__((weak));
int main()
{
if (foo)
foo();
}
$ gcc -m32 Weakref2.c -o Weakref2
$ ./Weakref2
编译及运行都没有报错。
这种弱符号和弱引用对于库来说十分有用。比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义了弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用;
如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能。
我们在GCC编译时加上-g
参数,则会在产生的目标文件上加上调试信息。
在Linux下,我们可以使用strip
命令来去掉ELF文件中的调试信息:
$ strip foo