可执行文件格式主要是:Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format)他们都是COFF(Common file format)格式的变种。
目标文件就是源代码编译后但未进行链接的那些中间文件。
动态链接库(windows的.dll和Linux的.so)及静态链接库(Windows的.lib和Linux的.a)文件都按照可执行文件格式存储。
在Linux下可以使用file命令来查看相应的文件格式:
$ file first.sh
first.sh: POSIX shell script, ASCII text executable, with CRLF line terminators
目标文件是什么样的:
目标文件的内容包含了及其指令代码、数据,还有链接的时候需要的如符号表、调试信息、字符串等。
目标文件格式将这些信息按不同的属性,以“节(Section)”的形式存储,有时候也叫“段(Segment)”,他们都表示一个一定长度的区域。
程序源代码编译后的及其指令经常被放在代码段(Code Section)里,代码段常见的名字有“.code”或“.text”;全局变量和局部静态变量数据经常放在数据段
(Data Section),数据段的名字一般叫“.data”。未初始化的全局变量和局部变量一般放在一个叫“.bss”的段里,该段只是为未初始化的全局变量和局部静态
变量预留位置,并没有内容,所以也不占据空间。
程序源代码被编译以后主要分成两种段:程序指令和程序数据段。代码段属于程序指令,而数据段和.bss段属于程序数据。
为什么要分段:
1.程序被装载以后数据和指令分别被放在两个虚存区域
2.高速缓存的应用,指令和数据区的分离有利于提高程序
的局部性
3.防止多个副本同时存在于内存中
/*
* SimpleSection.c
* Linux:
* gcc -c SimpleSection.c
*
* Windows:
* c1 SimpleSection.c /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;
}
挖掘 SimpleSection.o:
将SimpleSection.c 只编译不链接:
$gcc -c SimpleSection.c
得到一个目标文件SimpleSection.o
查看其内部结构:
$objdump -h SimpleSection.o
参数“-h”就是把ELF文件的各个段的基本信息打印出来。
$ objdump -h SimpleSection.o
SimpleSection.o: file format pe-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000006c 00000000 00000000 000000b4 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 00000000 00000000 00000120 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000010 00000000 00000000 00000000 2**3
ALLOC
3 .rdata 00000004 00000000 00000000 00000128 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
通过size命令查看各段的长度
$ size SimpleSection.o
text data bss dec hex filename
112 8 16 136 88 SimpleSection.o
Objdump “-s”参数可以将所有段的内容以十六进制的方式打印出来,“-d”参数可以将所有的包含指令的段反汇编。
$ objdump -s -d SimpleSection.o
SimpleSection.o: file format pe-i386
Contents of section .text:
0000 5589e583 ec088b45 08894424 04c70424 U......E..D$...$
0010 00000000 e8000000 00c9c355 89e583ec ...........U....
0020 1883e4f0 b8000000 0083c00f 83c00fc1 ................
0030 e804c1e0 048945f4 8b45f4e8 00000000 ......E..E......
0040 e8000000 00c745fc 01000000 a1000000 ......E.........
0050 00030504 00000003 45fc0345 f8890424 ........E..E...$
0060 e89bffff ff8b45fc c9c39090 ......E.....
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rdata:
0000 25640a00 %d..
Disassembly of section .text:
00000000 <_func1>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 08 sub $0x8,%esp
6: 8b 45 08 mov 0x8(%ebp),%eax
9: 89 44 24 04 mov %eax,0x4(%esp)
d: c7 04 24 00 00 00 00 movl $0x0,(%esp)
14: e8 00 00 00 00 call 19 <_func1+0x19>
19: c9 leave
1a: c3 ret
0000001b <_main>:
1b: 55 push %ebp
1c: 89 e5 mov %esp,%ebp
1e: 83 ec 18 sub $0x18,%esp
21: 83 e4 f0 and $0xfffffff0,%esp
24: b8 00 00 00 00 mov $0x0,%eax
29: 83 c0 0f add $0xf,%eax
2c: 83 c0 0f add $0xf,%eax
2f: c1 e8 04 shr $0x4,%eax
32: c1 e0 04 shl $0x4,%eax
35: 89 45 f4 mov %eax,-0xc(%ebp)
38: 8b 45 f4 mov -0xc(%ebp),%eax
3b: e8 00 00 00 00 call 40 <_main+0x25>
40: e8 00 00 00 00 call 45 <_main+0x2a>
45: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%ebp)
4c: a1 00 00 00 00 mov 0x0,%eax
51: 03 05 04 00 00 00 add 0x4,%eax
57: 03 45 fc add -0x4(%ebp),%eax
5a: 03 45 f8 add -0x8(%ebp),%eax
5d: 89 04 24 mov %eax,(%esp)
60: e8 9b ff ff ff call 0 <_func1>
65: 8b 45 fc mov -0x4(%ebp),%eax
68: c9 leave
69: c3 ret
6a: 90 nop
6b: 90 nop
各个主要段存放的内容前面已经讲过。
看一下其他段:
自定义段:
自定义段:__attribute__((section("name"))) 把相应的变量和函数放到"name"作为段名的段中
例如:
__attribute__((section(".text"))) int global = 42;
ELF文件结构描述:
ELF目标文件的格式最前部是ELF文件头,它包含了描述整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址等信息。紧接着是ELF文件的各个段。其中ELF文件中与段有关的重要结构就是段表。该表描述了ELF文件包含的所有段的信息。
ELF文件头中定义了:ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度以及段的数量等。
可以由一个数据结构来表示:
typedef struct {
...
}struct Elf32_ehdr
root@ubuntu:LinuxPractice# 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: 272 (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: 11
Section header string table index: 8
魔数:16个字节被ELF标准规定为用来标识ELF文件平台属性
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
段表:
段表是ELF文件中除了头文件以外最重要的结构,它描述了ELF的各段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。
编译器、连接器和装载器都是依靠段表来定位和访问各个段的属性的。
root@ubuntu:...LinuxPractice# readelf -S SimpleSection.o
There are 11 section headers, starting at offset 0x110:
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 000050 00 AX 0 0 4
[ 2] .rel.text REL 00000000 000420 000028 08 9 1 4
[ 3] .data PROGBITS 00000000 000084 000008 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 00008c 000004 00 WA 0 0 4
[ 5] .rodata PROGBITS 00000000 00008c 000004 00 A 0 0 1
[ 6] .comment PROGBITS 00000000 000090 00002c 01 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 00000000 0000bc 000000 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 0000bc 000051 00 0 0 1
[ 9] .symtab SYMTAB 00000000 0002c8 0000f0 10 10 10 4
[10] .strtab STRTAB 00000000 0003b8 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”结构体为元素的数组。数组元素的个数等于段的个数,每个“Elf_Shdr”结构体对应一个段。“Elf32_Shdr”又被称为段描述符。
typedef struct
{
Elf64_Word sh_name ;
Elf64_Word sh_type ;
Elf64_Xword sh_flags ;
Elf64_Addr sh_addr ;
Elf64_Off sh_offset ;
Elf64_Xword sh_size ;
Elf64_Word sh_link ;
Elf64_Word sh_info ;
Elf64_Xword sh_addralign ;
Elf64_Xword sh_entsize ;
} Elf32_Shdr
每个段描述符为40个字节。
对于编译器和连接器来说,主要决定段的属性的是段的类型(sh_type)和段的标志位(sh_flags)。
段的标志位(sh_flag)表示该段在进程虚拟地址空间中的属性,比如是否可写,是否可执行等。
重定位表:
“.rel.text”段,它的类型(sh_type)为“SHT_REL”,也就是说它是一个重定位表(RelocationTable)。
“.rel.text”段表示针对“.text”段的重定位表。
字符串表:
字符串表把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。
一般字符串表在ELF文件中也以段的形式保存,常见的名字为“.strtab”或“.shstrtab”。分别表示字符串表和段字符串表
链接的接口——符号:
目标文件B用到目标文件A中的函数“foo”,那么我们称目标文件A定义了函数“foo”,称目标文件B引用了目标文件A中的函数“foo”。
在链接中,我们讲函数和变量统称为符号(Symbol),函数名和变量名就是符号名(Symbol Name).
每个目标文件都会有一个相应的符号表(Symbol Table),这个表里面记录了目标文件所用到的所有符号。每个定义的符号一个一个对应的值,叫做符号值,对于变量和函数来说,符号值就是他们的地址。符号进行分类:
全局符号、外部符号、段名、局部符号、行号信息
使用nm查看符号信息:
$ nm SimpleSection.o
00000000 b .bss
00000000 d .data
00000000 r .rdata
00000000 t .text
U ___main
U __alloca
00000000 T _func1
00000000 D _global_init_var
00000010 C _global_uninit_var
0000001b T _main
U _printf
00000004 d static_var.0
00000000 b static_var2.1
ELF文件中的符号表往往是文件中的段,段名一般叫“.symtab”。符号表的结构很简单,它是一个Elf32_sym结构的数组。每个Elf32_Sym结构对应一个符号。
/usr/src/linux/include/linux/elf.h
typedef struct elf32_sym{
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;
Elf32_Sym->st_name
又是一个名字索引。凡是字符串索引必然和一个字符串表相关联,那么与这个字段相关的字符串表在哪里呢? 往下看。
Elf32_Sym->st_value
该字段给出了相应的符号值。根据符号类型和用途的不同它可能是一个内存地址也可能是一个绝对值。
Elf32_Sym->st_size
符号的字节大小。如果是数据对象可能是该对象占用的字节大小,如果是函数符号则可能是函数体的指令占用字节数。如果符号没有大小或大小不可知此字段为0。
Elf32_Sym->st_info
这个字段包含两部分。低4位用来表示符号的类型,对于函数这字段的低4位应该等于STT_FUCN;高4位是这个符号的绑定类型,对于从动态库中引入的函数这个字段的高4位应为STB_GLOBAL,表示这个符号是全局符号。在elf.h中,给出了ELF32_ST_TYPE和ELF32_ST_BIND 两个宏分别用于获取这个字段的低4位与高4位。相应64位的宏不过是它们的别名。
Elf32_Sym->st_other
此字段无用,恒为0。
Elf32_Sym->st_shndx
每个符号都和某些节相关,这个字段给出了一个节头索引。如果函数体所在的节不存于当前文件中,此字段为0。Elf64_Section和Elf32_Section我们头一次遇到,它们都是占两字的整数。
源文档 <http://hi.baidu.com/zengzhaonong/blog/item/6ff726128fb64252f919b856.html>
特殊符号:
ld作为可执行文件来链接产生可执行文件时,它会为我们定义很多特殊符号,这些符号并没有在你的程序中定义,但是你可以直接什么并且引用它,我们称之为特殊符号。
__executable_start : 程序需起始地址(不是入口地址)
__etext或_etext或etext : 该符号位代码段结束地址
_edata 或edata : 该符号为数据段结束地址
_end 或 end : 该符号为程序的结束地址
/*
SpecialSymbol.c
*/
#include<stdio.h>
extern char __executable_start[];
extern char etext[], _etext[], __etext[];
extern char edata[], _edata[];
extern char end[], _end[];
int main()
{
printf("Excutable 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;
}
注:这个程序在我的机器上run不起来,报错!
符号修饰与函数签名:
防止符号名称冲突,C语言源代码文件中的所有全部变量和函数编译后在相应的符号前加下划线"_"
GCC通过参数"-fleading-underscore"和"-fno-leading=underscore"来打开和关闭是否在C语言符号前加下划线
C++符号修饰:参见GCC的名称修饰标准
GCC的基本C++名称修饰方法:所有的符号都以"_Z"开头,对于嵌套的名字(在名称空间或者在类里面的),后面紧跟着一个"N",然后是各个名称空间和类的名字,每个名字前是名字字符串的长度,再以"E"结尾。
使用extern "C"{
}
的使用会使 C++的名称修饰机制不会起作用。
判断当前编译的是不是C++代码
#ifdef __cplusplus
extern "C" {
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif
对于 C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。GCC中__attribute__((weak))来定义任何一个强符号为弱符号。(针对符号定义而非引用)
针对强符号的概念,链接器就按如下规则处理与选择多次定义的全局符号:
不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);如果有多个强符号定义,则链接器报重复定义错误
在强符号和弱符号同时存在的一个符号选择强符号
某个在所有目标中都是弱符号,则选择占用空间最大的
弱引用指定__attribute__((weakref)),对未定义的弱引用,链接器默认为0,不会报错。