ELF符号链接

何为ELF?

ELF的全称是executable linkable format,中文直译是可执行可链接格式。使用gcc -c file.c -o file.o,也就是.o格式的文件。这里举一个简单的例子。

1
int a = 10;

我们使用如下命令对其进行编译可得到test.o

1
gcc -c test.c -o test.o

使用readelf -a test.o即可查看其全部信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
$ readelf -a test.o   
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 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: 384 (bytes into file)
标志: 0x0
本头的大小: 64 (字节)
程序头大小: 0 (字节)
Number of program headers: 0
节头大小: 64 (字节)
节头数量: 9
字符串表索引节头: 8

节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000000 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000040
0000000000000004 0000000000000000 WA 0 0 4
[ 3] .bss NOBITS 0000000000000000 00000044
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .comment PROGBITS 0000000000000000 00000044
000000000000002a 0000000000000001 MS 0 0 1
[ 5] .note.GNU-stack PROGBITS 0000000000000000 0000006e
0000000000000000 0000000000000000 0 0 1
[ 6] .symtab SYMTAB 0000000000000000 00000070
00000000000000c0 0000000000000018 7 7 8
[ 7] .strtab STRTAB 0000000000000000 00000130
000000000000000a 0000000000000000 0 0 1
[ 8] .shstrtab STRTAB 0000000000000000 0000013a
0000000000000045 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)

There are no section groups in this file.

本文件中没有程序头。

There is no dynamic section in this file.

该文件中没有重定位信息。

The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.

Symbol table '.symtab' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 4
7: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 a

No version information found in this file.

由于全部讲解篇幅会比较长,在这里就挑几个重点的和接下来会用到部分进行讲解。

首先我们找到其节头对应的主要数据结构(/usr/inlcude/elf.h)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 85 typedef struct
86 {
87 unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
88 Elf64_Half e_type; /* Object file type */
89 Elf64_Half e_machine; /* Architecture */
90 Elf64_Word e_version; /* Object file version */
91 Elf64_Addr e_entry; /* Entry point virtual address */
92 Elf64_Off e_phoff; /* Program header table file offset */
93 Elf64_Off e_shoff; /* Section header table file offset */
94 Elf64_Word e_flags; /* Processor-specific flags */
95 Elf64_Half e_ehsize; /* ELF header size in bytes */
96 Elf64_Half e_phentsize; /* Program header table entry size */
97 Elf64_Half e_phnum; /* Program header table entry count */
98 Elf64_Half e_shentsize; /* Section header table entry size */
99 Elf64_Half e_shnum; /* Section header table entry count */
100 Elf64_Half e_shstrndx; /* Section header string table index */
101 } Elf64_Ehdr;

其中内容对应表如下

20170615093506128

可执行文件或者共享目标文件的程序头部是一个结构数组,每个结构描述了一个段 或者系统准备程序执行所必需的其它信息。目标文件的“段”包含一个或者多个“节区”, 也就是“段内容(Segment Contents)”。程序头部仅对于可执行文件和共享目标文件 有意义。 可执行目标文件在 ELF 头部的 e_phentsize和e_phnum 成员中给出其自身程序头部 的大小。程序头部的数据结构:

之后是节头(section)

1
2
3
4
5
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0

由于早期的计算机屏幕比较小,因此采用了现在看起来十分怪的排练,其展开是如下状态

1
2
[号] 名称              类型             地址              偏移量 	大小              全体大小          旗标   链接   信息   对齐
[ 0] (holder place) NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0

再然后是符号表(symbol table)

1
2
3
4
5
6
7
8
9
10
Symbol table '.symtab' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 4
7: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 a

可以看到符号表分为很多个Num,每一个number有对应的name,在动态加载到内存中成为程序运行是便成了segment,其中比较出名比较重要的段有.bss(未初始化),.text(代码段),.data(数据段)。

之后是size,这个size是指当前段所占的bytes(8bit)。用于标识段范围。

Type用于标识符号的类型,通常来说有section,file,func,object,notype等类型,其中重点是func用于标识已经定义过的function,object用于标识已经定义过的变量,notype用于标识只声明未定义的函数和变量。

bind用于标识范围,对于一个文件中的变量来说可以分为函数内(internal)和函数外(external)两种情况,分别对应local和global两种标识,还用一种标识是weak,用于对应不确定在程序运行时是否会加载这个对应函数的库的函数或者变量,写法为

1
2
3
__attribute__((weak)) void func(){
return value;
}

其中当函数为被加载时会使用此文件中的函数定义。对应的bind符号为weak,也就是通常所说的弱符号,global对应强符号。

vis对应的是变量或者函数是否初始化,通常有default和undefined两种定义。

Name为section Name,其中函数和变量所对应的位置比较复杂,下文重点讨论。

symbol table解析

首先我们来看一个比较简单的test.c文件

Test.c

1
2
3
4
int a = 10;
int b;
void func1();
void func2(){};

我们使用编译此文件

1
gcc -c test.c -o test.o

我们首先用

1
readelf -S test.o

查看此文件的section headers会得到如下表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
There are 11 section headers, starting at offset 0x240:

节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000007 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000048
0000000000000004 0000000000000000 WA 0 0 4
[ 3] .bss NOBITS 0000000000000000 0000004c
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .comment PROGBITS 0000000000000000 0000004c
000000000000002a 0000000000000001 MS 0 0 1
[ 5] .note.GNU-stack PROGBITS 0000000000000000 00000076
0000000000000000 0000000000000000 0 0 1
[ 6] .eh_frame PROGBITS 0000000000000000 00000078
0000000000000038 0000000000000000 A 0 0 8
[ 7] .rela.eh_frame RELA 0000000000000000 000001d0
0000000000000018 0000000000000018 I 8 6 8
[ 8] .symtab SYMTAB 0000000000000000 000000b0
0000000000000108 0000000000000018 9 8 8
[ 9] .strtab STRTAB 0000000000000000 000001b8
0000000000000012 0000000000000000 0 0 1
[10] .shstrtab STRTAB 0000000000000000 000001e8
0000000000000054 0000000000000000 0 0 1
Key to Flags:

其中的各个section大小对应的是此section的结束地址,例如.text的大小就为[1] - [0] = [0x0,0x07)

然后我们在看到其类型有NULL, PROGBITS, NOBITS, RELA, STRTAB其实际上在elf.h中定义的类型有466 - 431种类型,下面主要讲解几种常见的类型

NOBITS:是指存放在.bss段中的数据。

RELA:是指会发生重定向的数据(程序链接后)。

STRTAB:字符串表,可以看作是一长串字符串(中间会有很多/0),用于存放字符串数据。

之后我们在用

1
readelf -s test.o

查看符号表可得如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 a
9: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM b
10: 0000000000000000 7 FUNC GLOBAL DEFAULT 1 func2

这里可以发现

1
2
3
4
5
									//  Type    Bind   Vis  
int a = 10; // OBJECT GLOBAL DEFAULT
int b; // OBJECT GLOBAL DEFAULT
void func1(); // 被优化掉了,因为没有使用
void func2(){}; // FUNC GLOBAL DEFAULT

好得相信你已经掌握了基本的类型定义对应的符号了,我们再来看看俩个文件链接到一起所对应的符号表

1.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 1 #include "stdio.h"
2 #include "stdlib.h"
3 static int a = 10;
4 __attribute__((weak)) int add(int a,int b)
5 {
6 return a+b;
7 }
8
9 __attribute__((weak)) int f()
10 {
11
12 return -1;
13 }
14
15 int main()
16 {
17 if(f() == -1) {
18
19 printf("function f undefine !\n");
20 exit(0);
21 }
22 else {
23 printf("function f define !\n");
24 exit(0);
25 }
26 return 0;
27 }

2.c

1
2
3
4
1 int a = 20;
2 int f() {
3 return 2;
4 }

相信2.o这种情况已经不困难了索性跳过我们先来看看1.o所对应的符号表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS 1.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 4 OBJECT LOCAL DEFAULT 3 a
6: 0000000000000000 0 SECTION LOCAL DEFAULT 5
7: 0000000000000000 0 SECTION LOCAL DEFAULT 7
8: 0000000000000000 0 SECTION LOCAL DEFAULT 8
9: 0000000000000000 0 SECTION LOCAL DEFAULT 6
10: 0000000000000000 20 FUNC WEAK DEFAULT 1 add
11: 0000000000000014 11 FUNC WEAK DEFAULT 1 f
12: 000000000000001f 63 FUNC GLOBAL DEFAULT 1 main
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
																																// Size   Type    Bind 
3 static int a = 10; // OBJECT LOCAL DEFAULT
4 __attribute__((weak)) int add(int a,int b) // FUNC WEAK DEFAULT
5 {
6 return a+b;
7 }
8
9 __attribute__((weak)) int f() // FUNC WEAK DEFAULT
10 {
11
12 return -1;
13 }
14
15 int main() // FUNC GLOBAL DEFAULT
16 {
17 if(f() == -1) {
18
19 printf("function f undefine !\n");
20 exit(0);
21 }
22 else {
23 printf("function f define !\n");
24 exit(0);
25 }
26 return 0;
27 }

这里我们可以看到static int a = 10对应的type是local,也就是局部类型,使用static会将其global类型改为local类型,但是其与function internal还是有区别的,区别在于在此文件中它依旧是global类型。然后是使用 attribute((weak)) 所修饰过的int f() 和int add(int a,int b)两个函数他们的type都是weak,也就是说这里直接编译执行此文件会得到如下输出

1
function f undefine !

因为此时的程序加载到内存中之后找不到对应的int f()所对应的库文件。

之后我们再把1.o和2.o俩个文件链接到一起

1
ld -r 1.o 2.o -o 12.o

得到如下符号表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Symbol table '.symtab' contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 SECTION LOCAL DEFAULT 1
2: 0000000000000000 0 SECTION LOCAL DEFAULT 3
3: 0000000000000000 0 SECTION LOCAL DEFAULT 4
4: 0000000000000000 0 SECTION LOCAL DEFAULT 6
5: 0000000000000000 0 SECTION LOCAL DEFAULT 7
6: 0000000000000000 0 SECTION LOCAL DEFAULT 8
7: 0000000000000000 0 SECTION LOCAL DEFAULT 9
8: 0000000000000000 0 FILE LOCAL DEFAULT ABS 1.c
9: 0000000000000000 4 OBJECT LOCAL DEFAULT 6 a
10: 0000000000000000 0 FILE LOCAL DEFAULT ABS 2.c
11: 000000000000005e 11 FUNC GLOBAL DEFAULT 1 f #global
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
13: 0000000000000000 20 FUNC WEAK DEFAULT 1 add #weak
14: 000000000000001f 63 FUNC GLOBAL DEFAULT 1 main
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit
17: 0000000000000004 4 OBJECT GLOBAL DEFAULT 6 a

我们可以看到由于2.c中有定义int f()所有链接之后的f的type变成了global类型,此时生成的程序会输出

1
function f define !

因为程序在运行时找到了外部定义(重定向后的f())因此不会执行原有的function(链接重定向部分内容较长由于篇幅原因暂时不扩展讲)。

好的看到这里想必你已经对程序Type Bind Vis已经了然于胸,但是我已经建议你看完并了解如下结果所形成的运行(由于不同编译器和不同系统所产生的符号表大不相同,因此请以具体机器的符号表为准,此处结果仅供参考)

我们枚举出所有可能,得到如下程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
  1 int a1;
2 int a2 = 0;
3 int a3 = 1;
4 __attribute__((weak)) int b1;
5 __attribute__((weak)) int b2 = 0;
6 __attribute__((weak)) int b3 = 1;
7 static int c1;
8 static int c2 = 0;
9 static int c3 = 1;
//###error: weak declaration of ‘d1’ must be public __attribute__((weak)) static int d1;
10 //__attribute__((weak)) static int d1;
11 //__attribute__((weak)) static int d2 = 0;
12 //__attribute__((weak)) static int d3 = 1;

13 extern int e1;
// warning: ‘e2’ initialized and declared ‘extern’
14 extern int e2 = 0;
// warning: ‘e3’ initialized and declared ‘extern’
15 extern int e3 = 1;
16
//error: multiple storage classes in declaration specifiers
17 //extern static int g1;
18 //extern static int g2 = 0;
19 //extern static int g3 = 1;

//error: multiple storage classes in declaration specifiers __attribute__((weak)) extern static int h1;
// warning: ‘h2’ initialized and declared ‘extern’ __attribute__((weak)) extern static int h2 = 0;
20 //__attribute__((weak)) extern static int h1;
21 //__attribute__((weak)) extern static int h2 = 0;
22 //__attribute__((weak)) extern static int h3 = 1;
23
24
25 static void fa();
26 static void fb(){}
27 extern void f1();
28 extern void f2(){}
29 void f3();
30 void f4(){}
31 __attribute__((weak)) void f5();
32 __attribute__((weak)) void f6(){}
//error: multiple storage classes in declaration specifiers extern static void f7();
33 //extern static void f7();
34 //extern static void f8(){}
35 //__attribute__((weak)) static void f9();

其对应的符号表如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Symbol table '.symtab' contains 23 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000010 4 OBJECT LOCAL DEFAULT 3 c1 # static int c1;
6: 0000000000000014 4 OBJECT LOCAL DEFAULT 3 c2 # static int c2 = 0;
7: 0000000000000008 4 OBJECT LOCAL DEFAULT 2 c3 # static int c3 = 1;
8: 0000000000000000 7 FUNC LOCAL DEFAULT 1 fb # static void fb(){}
9: 0000000000000000 0 SECTION LOCAL DEFAULT 5
10: 0000000000000000 0 SECTION LOCAL DEFAULT 6
11: 0000000000000000 0 SECTION LOCAL DEFAULT 4
12: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM a1 # int a1;
13: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 a2 # int a2 = 0;
14: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 a3 # int a3 = 1;
15: 0000000000000004 4 OBJECT WEAK DEFAULT 3 b1 # __attribute__((weak)) int b1;
16: 0000000000000008 4 OBJECT WEAK DEFAULT 3 b2 # __attribute__((weak)) int b2 = 0;
17: 0000000000000004 4 OBJECT WEAK DEFAULT 2 b3 # __attribute__((weak)) int b3 = 1;
18: 000000000000000c 4 OBJECT GLOBAL DEFAULT 3 e2 # extern int e2 = 0;
19: 000000000000000c 4 OBJECT GLOBAL DEFAULT 2 e3 # extern int e3 = 1;
20: 0000000000000007 7 FUNC GLOBAL DEFAULT 1 f2 # extern void f2(){}
21: 000000000000000e 7 FUNC GLOBAL DEFAULT 1 f4 # void f4(){}
22: 0000000000000015 7 FUNC WEAK DEFAULT 1 f6 # __attribute__((weak)) void f6(){}

其中此表的Ndx对应的symbol table name为如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[号] 名称              类型             地址              偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000001c 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 0000005c
0000000000000010 0000000000000000 WA 0 0 4
[ 3] .bss NOBITS 0000000000000000 0000006c
0000000000000018 0000000000000000 WA 0 0 4
[ 4] .comment PROGBITS 0000000000000000 0000006c
000000000000002a 0000000000000001 MS 0 0 1
[ 5] .note.GNU-stack PROGBITS 0000000000000000 00000096
0000000000000000 0000000000000000 0 0 1
[ 6] .eh_frame PROGBITS 0000000000000000 00000098
0000000000000098 0000000000000000 A 0 0 8
[ 7] .rela.eh_frame RELA 0000000000000000 00000390
0000000000000060 0000000000000018 I 8 6 8
[ 8] .symtab SYMTAB 0000000000000000 00000130
0000000000000228 0000000000000018 9 12 8
[ 9] .strtab STRTAB 0000000000000000 00000358
0000000000000035 0000000000000000 0 0 1
[10] .shstrtab STRTAB 0000000000000000 000003f0
0000000000000054 0000000000000000 0 0 1