目标文件的格式
-
目标文件:已经编译后的可执行文件格式,还没有经过链接的过程,其中可能有一些符号和地址没有调整。它跟可执行文件的内容与结构很相似,所以一般和可执行文件以同一种格式存储。
-
现在PC平台流行的可执行文件格式主要是
PE(Windows平台)
和ELF(Linux平台)
。 -
动态链接库
(Window的.dll和Linux的.so)
和静态链接库(Windows的.lib和Linux的.a)
文件也按照可执行文件格式存储 -
静态链接库是把很多目标文件捆绑在一起形成一个文件
ELF格式的文件
-
可重定位文件:
Linux中的.o, Windows的.obj
,这类文件包含代码和数据,可被链接成可执行文件或共享目标文件,例如静态链接库。 -
可执行文件:可以直接执行的文件,如/bin/bash文件。
-
共享目标文件:Linux中的.so,包含代码和数据,一种是链接器可以使用这种文件和其它的可重定位文件和共享目标文件链接,另一种是动态链接器可以将几个这种共享目标文件和可执行文件结合,作为进程映像的一部分来执行。
-
core dump文件:进程意外终止时,系统可以将该进程的地址空间的内容和其它信息存到coredump文件用于调试,如gdb。
我们可以通过
file
命令查看相应的文件格式
file sum.o
sum.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
relocatable
:可重定位文件
file /bin/bash
/bin/bash: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=12f73d7a8e226c663034529c8dd20efec22dde54, stripped
shared object
:共享目标文件
目标文件是什么样的
-
目标文件内容有编译后的机器指令代码、数据。
-
还有链接时所需要的一些信息,比如符号表、调试信息、字符串等
目标文件将这些信息按不同的属性以段Segment
的形式存储
- 程序源代码编译后的机器指令放在代码段
.text
- 全局变量和局部静态变量放在数据段
.data
- 未初始化的或者初始化为
0
的全局变量和局部静态变量放在.bss
,不占文件的空间
int gdata1 = 10; //.data
int gdata2 = 0; //.bss
int gdata3; //.bss
static int gdata4 = 11;//.data
static int gdata5 = 0;//.bss
static int gdata6;//.bss
int main()
{
int a = 10;
int b = 0;
int c;
static int d = 12;//.data
static int e = 0;//.bss
static int f;//.bss
sum(a, b);
return 0;
}
程序员自我修养的例子
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DPBNJKzD-1650725716571)(https://syz-picture.oss-cn-shenzhen.aliyuncs.com/D:%5CPrograme%20Files(x86)]%5CPicGoimage-20220130194413822.png)
为什么未初始化的或者初始化为
0
的全局变量和局部静态变量要放在.bss
段
他们默认值都为0
,本来也可以放到.data
,但因为我们已经知晓他们的值都为0
,所以在.data
段分配空间并且存放数据是没必要的,这样也方便减小ELF
文件大小。既然这些数据值都相同,所以干脆把他们统一放到另一个段里,且不分配空间单独记录数据,只要在这个段里就知道他的数据为0。 但是程序运行期间这个是要占内存的,所以我们的.bss
段需要记录这些数据的大小,仅仅作为一个占用空间的标记,为未初始化的和初始化为0的全局变量和静态局部变量预留位置而已,并无内容。
为什么要把数据和指令分开放
- 程序被加载后,数据和指令分别被映射到两个虚存内存区域,数据区域对进程是可读写的,而指令区域是只读的,这样可以防止程序的指令被改写
- 另一方面对于现代CPU,它们有着强大的缓存体系。程序必须尽量提高缓存(Cache)的命中率,而指令区和数据区的分离有利于提高程序的局部性。
- 当系统运行多个程序的副本时,指令是一样的,所以只要保存一份指令部分即可。是属于共享的资源。但是数据区域是不一样的,是进程私有的。这个共享指令的概念,特别是在有动态链接的系统中可以节省大量的内存。(多进程状态下不用保存多份指令)
使用命令探索ELF文件格式
我们可以使用objdump
命令打印ELF
的信息
objdump
命令是Linux下的反汇编目标文件或者可执行文件的命令,它以一种可阅读的格式让你更多地了解二进制文件可能带有的附加信息。 不过它只会打印关键段的信息,像是符号表段之类的辅助段不会打印
参考视频例子
#include <stdio.h>
int global_1;
int global_2 = 0;
int global_3 = 10;
static int global_4;
static int global_5 = 0;
static int global_6 = 10;
char *str = "Hello World";
int main()
{
int global_7;
int global_8 = 0;
int global_9 = 10;
static int global_10;
static int global_11 = 0;
static int global_12 = 10;
return 0;
}
gcc test.c -c
-h
打印各个段的基本信息-x
打印各个段更多的信息
objdump -h test.o
test.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000019 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 0000000c 0000000000000000 0000000000000000 0000005c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000014 0000000000000000 0000000000000000 00000068 2**2
ALLOC
3 .rodata 0000000c 0000000000000000 0000000000000000 00000068 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .data.rel.local 00000008 0000000000000000 0000000000000000 00000078 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
5 .comment 0000002a 0000000000000000 0000000000000000 00000080 2**0
CONTENTS, READONLY
6 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000aa 2**0
CONTENTS, READONLY
7 .eh_frame 00000038 0000000000000000 0000000000000000 000000b0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
.text
:代码段.data
:数据段.bss
:….rodata
:只读数据段.comment
:注释信息段.note.GNU-stack
:堆栈提示段
对照一下.data
,其大小为Size = 0000000c
,将16进制转换得到12,正好是3个变量的大小,符合我们的判断。
File off
:段偏移量
CONTENTS
: 代表该段在文件中存在,.bss
段就没有CONTENTS
,说明.bss
段在文件中不存在;.note.GNU-stack
段有CONTENTS
,但是值为0
,我们暂时认为它不存在。
objdump -x test.o
test.o: file format elf64-x86-64
test.o
architecture: i386:x86-64, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x0000000000000000
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000019 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 0000000c 0000000000000000 0000000000000000 0000005c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000014 0000000000000000 0000000000000000 00000068 2**2
ALLOC
3 .rodata 0000000c 0000000000000000 0000000000000000 00000068 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .data.rel.local 00000008 0000000000000000 0000000000000000 00000078 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
5 .comment 0000002a 0000000000000000 0000000000000000 00000080 2**0
CONTENTS, READONLY
6 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000aa 2**0
CONTENTS, READONLY
7 .eh_frame 00000038 0000000000000000 0000000000000000 000000b0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
SYMBOL TABLE: //符号表
0000000000000000 l df *ABS* 0000000000000000 test.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000004 l O .bss 0000000000000004 global_4
0000000000000008 l O .bss 0000000000000004 global_5
0000000000000004 l O .data 0000000000000004 global_6
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000000 l d .data.rel.local 0000000000000000 .data.rel.local
0000000000000008 l O .data 0000000000000004 global_12.2261
000000000000000c l O .bss 0000000000000004 global_11.2260
0000000000000010 l O .bss 0000000000000004 global_10.2259
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000004 O *COM* 0000000000000004 global_1
0000000000000000 g O .bss 0000000000000004 global_2
0000000000000000 g O .data 0000000000000004 global_3
0000000000000000 g O .data.rel.local 0000000000000008 str
0000000000000000 g F .text 0000000000000019 main
RELOCATION RECORDS FOR [.data.rel.local]:
OFFSET TYPE VALUE
0000000000000000 R_X86_64_64 .rodata
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
继续探索——SimpleSection.o
代码示例
#include <stdio.h>
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
我们得到了SimpleSection.o
文件
使用
objdump
命令探索
objdump -h SimpleSection.o //打印目标文件的基本段信息
SimpleSection.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000057 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 00000098 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 000000a0 2**2
ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 000000a0 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002a 0000000000000000 0000000000000000 000000a4 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000ce 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000d0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
.text
.data
.bss
.rodata
.comment
.note.GNU-stack
代码段
objdump -s -d SimpleSection.o //-s可以将所有段内容以十六进制打印 -d可以将所有指令反汇编
SimpleSection.o: file format elf64-x86-64
Contents of section .text:
0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E...
0010 488d3d00 000000b8 00000000 e8000000 H.=.............
0020 0090c9c3 554889e5 4883ec10 c745f801 ....UH..H....E..
0030 0000008b 15000000 008b0500 00000001 ................
0040 c28b45f8 01c28b45 fc01d089 c7e80000 ..E....E........
0050 00008b45 f8c9c3 ...E...
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520372e .GCC: (Ubuntu 7.
0010 352e302d 33756275 6e747531 7e31382e 5.0-3ubuntu1~18.
0020 30342920 372e352e 3000 04) 7.5.0.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 24000000 00410e10 8602430d ....$....A....C.
0030 065f0c07 08000000 1c000000 3c000000 ._..........<...
0040 00000000 33000000 00410e10 8602430d ....3....A....C.
0050 066e0c07 08000000 .n......
Disassembly of section .text:
0000000000000000 <func1>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 8b 45 fc mov -0x4(%rbp),%eax
e: 89 c6 mov %eax,%esi
10: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 17 <func1+0x17>
17: b8 00 00 00 00 mov $0x0,%eax
1c: e8 00 00 00 00 callq 21 <func1+0x21>
21: 90 nop
22: c9 leaveq
23: c3 retq
0000000000000024 <main>:
24: 55 push %rbp
25: 48 89 e5 mov %rsp,%rbp
28: 48 83 ec 10 sub $0x10,%rsp
2c: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
33: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 39 <main+0x15>
39: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3f <main+0x1b>
3f: 01 c2 add %eax,%edx
41: 8b 45 f8 mov -0x8(%rbp),%eax
44: 01 c2 add %eax,%edx
46: 8b 45 fc mov -0x4(%rbp),%eax
49: 01 d0 add %edx,%eax
4b: 89 c7 mov %eax,%edi
4d: e8 00 00 00 00 callq 52 <main+0x2e>
52: 8b 45 f8 mov -0x8(%rbp),%eax
55: c9 leaveq
56: c3 retq
数据段和只读数据段
观察示例代码的函数部分
void func1(int i)
{
printf("%d\n", i);
}
SimepleSection.c
里面使用了printf
,里面用到了字符串常量%d\n
,它是一个只读数据,所以放到了.rodata
段。
使用
objdump -x SimpleSection.o
命令查看
objdump -x SimpleSection.o //-x打印更多段的信息,包括了辅助段符号表等
SimpleSection.o: file format elf64-x86-64
SimpleSection.o
architecture: i386:x86-64, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x0000000000000000
Sections:
Idx Name Size VMA LMA File off Algn
2 .bss 00000004 0000000000000000 0000000000000000 000000a0 2**2
ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 000000a0 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
//省略了许多
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
我们观察.rodata
,正好只有四个字节,符合那个int
变量。还有那个Contents of section .rodata:
的字节序也能对应的上。
rodata
段的意义
- 语义上支持了C++的
const
关键字 - 操作系统在加载时候可以将
.rodata
段的属性映射成只读,保证安全性
ELF文件结构描述
ELF Header |
---|
.text |
.data |
.bss |
…other sections |
Section header table |
String Tables |
Symbol Tables |
… |
ELF
目标文件格式的最前部是ELF文件头
,它包含了描述整个文件的基本属性。- 紧接着是各个段,其中
ELF
文件中与段有关的重要结构就是段表,该表描述了ELF
文件包含的所有段的信息。
使用
readelf
命令来详细查看ELF
文件
查看文件头
readelf -h test.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1024 (bytes into file)
Flags: 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
ELF
文件头定义了ELF
魔数、文件机器字节长度、数据存储方式、版本、运行平台…
段表
ELF
文件中有各种各样的段,段表就是保存这些段信息的基本结构。它描述了各个段的信息,比如段名、段长、在文件中的偏移、读写权限等
之前的objdump -h
只是打印了ELF
文件中关键的段,辅助段等被省略了。
使用
readelf
细致探索段表
readelf -S SimpleSection.o
There are 13 section headers, starting at offset 0x450:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000057 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000340
0000000000000078 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000098
0000000000000008 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000000a0
0000000000000004 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 000000a0
0000000000000004 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 000000a4
000000000000002a 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000ce
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000d0
0000000000000058 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 000003b8
0000000000000030 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 00000128
0000000000000198 0000000000000018 11 11 8
[11] .strtab STRTAB 0000000000000000 000002c0
000000000000007c 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 000003e8
0000000000000061 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)
段表结构简单,就是一个以ELf32_Shdr
结构体为元素的数据,每一个元素对应一个段
以下为
32位
系统下的ELF32_Shdr
结构体信息
typedef struct
{
ELF32_Word sh_name; //段名:是一个字符串,位于.shstrtab字符串表,sh_name是在字符串表中的偏移
ELF32_Word sh_type; //段类型
ELF32_Word sh_flags; //段标志位
ELF32_Addr sh_addr; //段虚拟地址:如果可加载,则此为该段被加载后在进程虚拟空间的地址,否则为0
ELF32_Off sh_offset; //段偏移:如果该段存于文件,则表示在文件中的偏移,否则无意义(比如.bss)
ELF32_Word sh_size; //段长度
ELF32_Word sh_link; //段链接信息
ELF32_Word sh_info;
ELF32_Word sh_addralign;//段地址对齐:表示是地址对齐数量中的指数
ELF32_Word sh_entsize; //项长度:有些段包含了固定大小的项,比如符号表,它包含每个符号所占大小一样
} ELF32_Shdr;
- 段名对于编译器,链接器有意义,对于
OS
无意义。OS
只在意段的属性和权限,即段的类型和标志位
段类型
sh_type
SHT_NULL 0 无效段;
SHT_PROGBITS 1 程序段、代码段、数据段都为这种类型;
SHT_SYMTAB 2 表示该段的内容为符号表;
…
链接的接口——符号
链接过程的本质是要把多个不同的目标文件之间相互粘在一起,目标文件之间相互拼合实际上是目标文件之间对地址的引用(对函数和变量的地址的引用)。
比如文件A
用到了文件B
的地址,它得知道这个地址在哪里吧,编译过程只是单独的对本文件编译,而不知道其他文件的情况。而函数和变量应该有不同的名字,否则链接过程会混淆报错。(函数内的不算,那不属于链接器管辖)
在链接中,我们将函数和变量统称为符号,函数名和变量名就是符号名
每一个目标文件都会有一个相应的符号表Symbol Table
,这个表里面记录了目标文件中所用到的符号,符号对应有符号值(变量和函数的地址)。对于变量和函数来说,符号值就是它们的地址
符号的分类
- 定义在本目标文件的全局符号,比如
func1
,main
,global_init_var
- 在本目标文件中引用的全局符号,却没有定义在本目标文件中,比如
printf
- 段名,这种由编译器产生,它的值就是该段的起始地址,比如
.text
- 局部符号,这类符号只在编译单元内可见。这些局部符号对链接过程无用,链接器不会注意,比如
static_var
,static_var2
- 行号信息,即目标文件指令与源代码中代码行的对应关系
nm SimpleSection.o //查看SimpleSection.o中的符号
0000000000000000 T func1
0000000000000000 D global_init_var
U _GLOBAL_OFFSET_TABLE_
0000000000000004 C global_uninit_var
0000000000000024 T main
U printf
0000000000000000 b static_var2.2258
0000000000000004 d static_var.2257
使用
readelf
命令查看符号属性
readelf -s SimpleSection.o
Symbol table '.symtab' contains 17 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.2257
7: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var2.2258
8: 0000000000000000 0 SECTION LOCAL DEFAULT 7
9: 0000000000000000 0 SECTION LOCAL DEFAULT 8
10: 0000000000000000 0 SECTION LOCAL DEFAULT 6
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var
12: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var
13: 0000000000000000 36 FUNC GLOBAL DEFAULT 1 func1
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
16: 0000000000000024 51 FUNC GLOBAL DEFAULT 1 main
- 第一列
Num
表示符号表数组的下标;第二列为符号值;第三列为符号大小;第四列和第五列分别为符号类型和绑定信息;第七列Ndx
表示该符号所属的段;最后一列为符号名称 func1
和main
函数都是在代码段,所以Ndx
都为1
,可以通过readelf -a 或者 objdump -x
来验证。他们是函数,所以类型为STT_FUNC
。他们是全局可见的,所以是STL_GLOBAL
,Size
表示函数指令所占的字节数,Value
表示函数相对于代码其实位置的偏移量printf
这个符号被引用,但是该目标文件没有定义,所以它的Ndx
为SHN_UNDEF
global_init_var
是已初始化的全局变量,位于
重定位表
[ 2] .rela.text RELA 0000000000000000 00000340
0000000000000078 0000000000000018 I 10 1 8
我们注意到一个.rel.text
段,它的类型为sh_type = SHT_REL
,说明这是一个重定位表(Relocation Table
)
链接器处理目标文件,需要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。这些重定位的信息都记录在ELF
文件的重定位表里面,对于每个须要重定位的代码段和数据段,都会有一个对应的重定位表。
SimpleSection.o
的.rel.text
就是针对.text
段的重定位表,因为.text
段中有一个绝对地址的引用,即对printf
函数的调用。
- 一个重定位表同时也是
ELF
的一个段,这个段的类型就是SHT_REL
,他的sh_link
表示符号表的下标,它的sh_info
表示它作用于哪个段
符号修饰与函数签名
- 早期编译器编译源代码产生目标文件时,符号名与相应的变量和函数名相同。如果程序员自己定义的变量函数和库中定义的重复,就会产生冲突
- 为了防止类似的符号名冲突,
UNIX
下的C语言规定,C语言源代码文件中所有全局的变量和函数经过编译之后,对应的符号名前加上下划线_
- 后来,不同人开发不同模块还是有可能产生冲突,在C++中引入了名称空间
namespace
的方法解决了多模块的符号冲突问题
C++符号修饰
C++拥有类,继承,虚机制,重载,名称空间这些特性,这会使得符号管理变得困难。比如函数重载,它们的名字就是相同的,如果没有特殊处理,符号名应该也一样,这会造成冲突。
为了支持C++这些复杂特性,人们发明了符号修饰的机制。
int func(int);
float func(float);
class C {
int func(int);
class C2 {
int func(int);
};
};
namespace N {
int func(int);
class C {
int func(int);
};
};
- 上述代码有六个同名函数叫做
func
,我们引入一个叫做函数签名的术语,函数签名包含了一个函数的信息(函数名,参数类型,所在的类和命名空间等) - 编译器及链接器处理符号时,它们使用名称修饰的方法,使得函数签名对应一个修饰后名称。
- GCC基本C++名称修饰方法
- 所有符号都以
_Z
开头 - 对于嵌套的名字,后面紧跟
N
,然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以E
结尾,后面再跟参数类型的首字母
- 所有符号都以
- 不同编译器采用不同的名字修饰方法
extern ”C“
C++为了与C兼容,在符号的管理上,C++有一个用来声明或定义一个C的符号的extern C
关键字用法
extern "C" {
int func(int);
int var;
}
C++编译器会将extern "C"
的大括号内部的代码当作C语言代码处理。那么C++的修饰机制将不会起作用
弱符号与强符号
我们经常遇到的一种情况叫做符号重复定义。多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误。
对于C++来说
- 编译器默认函数和初始化了的全局变量为强符号
- 未初始化的全局变量为弱符号
- 我们可以使用
_attribute_((weak))
来定义任何一个强符号为弱符号
强弱符号规则
- 不允许强符号被多次定义,否则链接器报重复定义错误
- 如果一个符号在某个目标文件是强符号,在其他文件都是弱符号,那么选择强符号
- 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个
强引用和弱引用
- 我们所看到的对外部目标文件中的符号引用在目标文件中被最终链接成可执行文件时,它们必须被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用。
- 在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议,如果该符号未被定义,则链接器对于该引用不报错,链接器默认其值为0。只不过其可被覆盖
强弱符号作用
- 比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使用自定义版本的库函数。