测试环境:
➜ tmp uname --version
uname (GNU coreutils) 8.25
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Written by David MacKenzie.
➜ tmp gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
PC平台的目标文件格式大都是COFF的变种,比如Windows的PE(Portable Executable)格式和Linux的ELF(Executable Linkable Format)格式。并且我们一般讲的目标文件格式多指可执行文件,但是实际上编译过程中的静态库文件、动态库文件和.o或者.obj文件都属于目标文件。常见的目标文件分类:
目标文件类型 | 说明 | 举例 |
---|---|---|
可执行文件 | 可以直接执行的程序 | windows的exe文件、linux的可执行文件、macOs的app文件 |
共享目标文件 | 包含了程序的代码和数据,可以在链接阶段和其他可重定位目标文件或者共享目标文件链接生成可执行文件;作为动态共享的链接库,在程序运行时进行装载 | Linux的so,windows的dll,macOs的dylib |
核心转储文件 | 当程序意外终止时,系统保存的进程的地址空间等信息的转储文件 | linux的core dump |
可重定位文件 | 包含程序的代码和数据,可被用来链接为目标文件 | 静态库,.o文件或者.obj文件 |
目标文件中无疑包含程序运行的代码和数据,只是如何对这些内容进行管理?目标文件管理这些内容通过段的方式进行,不同类型的数据等信息通过段进行区分。分段的好处:
尝试使用相关命令查看目标文件的内容:
使用的文件示例,文件中包含常量字符串、静态初始化变量、静态未初始化变量、局部初始化变量、局部未初始化变量、全局初始化变量和全局未初始化变量以及简单函数调用(文件中带a的都是初始化过的,带b的都是未经初始化的)。查看使用的命令的简单用法见Linux objdump使用。
int add(int a, int b){
return a + b;
}
const char *file = "main.o";
int glob_a = 15;
int glob_b;
//test
int main(){
static char static_a = 16;
static char static_b;
long long a = 3;
long long b;
add(a, b);
}
使用gcc -c main.cpp -o main.o
编译生成main.o
。使用objdmup -h main.o
查看各个段的大小:
Idx Name Size VMA LMA File off Algn
0 .text 0000003e 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000005 0000000000000000 0000000000000000 00000080 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000005 0000000000000000 0000000000000000 00000088 2**2
ALLOC
3 .rodata 00000007 0000000000000000 0000000000000000 00000088 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .data.rel.local 00000008 0000000000000000 0000000000000000 00000090 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
5 .comment 0000002a 0000000000000000 0000000000000000 00000098 2**0
CONTENTS, READONLY
6 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000c2 2**0
CONTENTS, READONLY
7 .eh_frame 00000058 0000000000000000 0000000000000000 000000c8 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
size main.o
能够查看数据段和代码段的大小:
text data bss dec hex filename
152 16 8 176 b0 main.o
从上面的结果中:第一列为段的索引;第二列为段的名称;第三列为段的尺寸;第三列为段的虚拟内存地址;第四段为局部内存地址;第五列为段在程序中的偏移;每个段再买呢的字段CONTENTS
表示该段在文件中存在,READONLY
表示只读,ALLOC
表示表示有该标记的节会在运行时分配并装载进入内存。根据文件中的偏移画出的文件结构图如下:
从输出的段结构图中能够看到bss
和rodata
的偏移一致,且二者都有各自的尺寸,并且虽然有.note.GUN-stack
但是该段没有尺寸:
.text
:代码段,存储程序的代码,可以通过objdump -s -d main.o
反汇编查看;.data
:数据段,存储已经初始化了的全局静态变量和局部静态变量,从图中查看刚好一个int和char的尺寸;.bss
:存储未经初始化的全局变量和局部静态变量,尺寸计算同.data
;.rodata
:存放只读数据,程序中main.o的字符串长度为6,而该段长度为7推断包含最后的\0
;.comment
:存放编译版本信息;.note.GNU-stack
:堆栈提示段;.eh_frame
:主要用于系统运行时调试使用的,便于栈展开调试。 使用objdump -s main.o
查看每个段的具体内容,能够看到data
段中0f
和10
刚好对应15和16:
Contents of section .data:
0000 0f000000 10 .....
Contents of section .rodata:
0000 6d61696e 2e6f00 main.o.
Contents of section .data.rel.local:
0000 00000000 00000000 ........
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.
上面的内容中并未看到bss
的内容,通过查看符号表objdump -t main.o
能够看到未经初始化的static_b
和glob_b
存储在bss中。但是这也不是很绝对,因为全局符号存在强符号和弱符号的区分,未经初始化的全局变量可能初始化为COMMON在链接时再分配内存。
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 main.cpp
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000000 l d .data.rel.local 0000000000000000 .data.rel.local
0000000000000004 l O .data 0000000000000001 _ZZ4mainE8static_a
0000000000000004 l O .bss 0000000000000001 _ZZ4mainE8static_b
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g F .text 0000000000000014 _Z3addii
0000000000000000 g O .data.rel.local 0000000000000008 file
0000000000000000 g O .data 0000000000000004 glob_a
0000000000000000 g O .bss 0000000000000004 glob_b
0000000000000014 g F .text 000000000000002a main
下面时将源文件使用c进行编译得到的未初始化的全局符号的存储方式,时典型的弱符号存储方式:
0000000000000004 O *COM* 0000000000000004 glob_b
ELF文件还包含很多其他段,比如调试信息相关的段不再赘述。
ELF文件的格式大致如下,其中比较重要的时文件头和段表:文件头描述文件的基本信息;段表类似所有段即section的指针表。
ELF Header:
可以使用readelf -h main.o
查看可执行文件中的header,ELF Header 中定义了 ELF Magic Code、文件机器字节长度、数据存储方式、版本、运行平台、ABI 版本、ELF 重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口与长度、Section Header 的偏移位置和长度以及 Section 数量等。
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: 1000 (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: 15
Section header string table index: 14
段表:
段表顾名思义,存储不同段的地方,实际存储的时段的描述符,该描述符会描述段的类型,大小等信息。可通过readelf -S main.o
查看,因为下面需要用到一些段因此贴到这里。
There are 15 section headers, starting at offset 0x3e8:
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
000000000000003e 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000310
0000000000000018 0000000000000018 I 12 1 8
[ 3] .data PROGBITS 0000000000000000 00000080
0000000000000005 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 00000088
0000000000000005 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 00000088
0000000000000007 0000000000000000 A 0 0 1
[ 6] .data.rel.local PROGBITS 0000000000000000 00000090
0000000000000008 0000000000000000 WA 0 0 8
[ 7] .rela.data.rel.lo RELA 0000000000000000 00000328
0000000000000018 0000000000000018 I 12 6 8
[ 8] .comment PROGBITS 0000000000000000 00000098
000000000000002a 0000000000000001 MS 0 0 1
[ 9] .note.GNU-stack PROGBITS 0000000000000000 000000c2
0000000000000000 0000000000000000 0 0 1
[10] .eh_frame PROGBITS 0000000000000000 000000c8
0000000000000058 0000000000000000 A 0 0 8
[11] .rela.eh_frame RELA 0000000000000000 00000340
0000000000000030 0000000000000018 I 12 10 8
[12] .symtab SYMTAB 0000000000000000 00000120
0000000000000198 0000000000000018 13 12 8
[13] .strtab STRTAB 0000000000000000 000002b8
0000000000000051 0000000000000000 0 0 1
[14] .shstrtab STRTAB 0000000000000000 00000370
0000000000000076 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)
重定位表:
重定位表主要记录了目标文件中所有需要重定位的符号所在的段以及相对(相对于该段开始)偏移位置。可以使用objdump -r main.o
查看该表的内容,从内容中能够看到存储的时相关函数和变量的在目标文件中的相对位置。
Relocation section '.rela.text' at offset 0x310 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000033 000c00000002 R_X86_64_PC32 0000000000000000 _Z3addii - 4
Relocation section '.rela.data.rel.local' at offset 0x328 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000000 000500000001 R_X86_64_64 0000000000000000 .rodata + 0
Relocation section '.rela.eh_frame' at offset 0x340 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
000000000040 000200000002 R_X86_64_PC32 0000000000000000 .text + 14
字符串表:
字符串表中存储ELF文件中使用到的字符串,一般有三种字符串表分别为shstrtab
保存section头中保存的字符串;strtab
保存elf中使用到的字符串;dynstr
保存了动态链接字符串表,表中存放了一系列字符串,这些字符串代表了符号名称,以空字符作为终止符。
程序需要链接的原因时因为程序的每个文件特别是C类的语言时单独分模块编译的,每个编译单元仅仅知道当前编译单元中的信息,当引用到其他编译单元的函数或者变量时无法明确该变量或者函数的地址。因此需要在连接时将这些符号的地址明确,一般函数和变量统称为符号,函数名和变量名为符号名。
编译时每个编译单元都会有一个符号表表明对应的符号在当前编译单元中的地址和值,因此在链接时需要将多个编译单元的符号表合并。
使用readelf -s main.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 main.cpp
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: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000004 1 OBJECT LOCAL DEFAULT 3 _ZZ4mainE8static_a
8: 0000000000000004 1 OBJECT LOCAL DEFAULT 4 _ZZ4mainE8static_b
9: 0000000000000000 0 SECTION LOCAL DEFAULT 9
10: 0000000000000000 0 SECTION LOCAL DEFAULT 10
11: 0000000000000000 0 SECTION LOCAL DEFAULT 8
12: 0000000000000000 20 FUNC GLOBAL DEFAULT 1 _Z3addii
13: 0000000000000000 8 OBJECT GLOBAL DEFAULT 6 file
14: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 glob_a
15: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 glob_b
16: 0000000000000014 42 FUNC GLOBAL DEFAULT 1 main
特殊符号:链接生成可执行文件时会连接器会定义很多特殊符号:
executable_start
:程序起始地址;etext,_etext,__etext
:代码段的结束地址;edata,_edata
:数据段的结束地址;end,_end
:程序的结束地址。#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;
}
运行结果:
executable start CB200000
text end CB20075D CB20075D CB20075D
data end CB401010 CB401010
executable end CB401018 CB401018
编译器为了更好的引用其他模块中的符号对模块中使用到的符号进行符号修饰,即符号签名。签名规则:
符号签名中包含参数类型也是C++实现函数重载的基础,但是C++也常常需要使用C的接口,如果使用C++的符号签名则无法找到对应的接口。可利用C++中的extern "C"
关键字保证对应的函数的符号签名使用C的规则。
C中存在强符号和弱符号,强符号不允许多重定义,弱符号允许多个定义但是实际运行时只有一个实体。对于C语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号(C++并没有将未初始化的全局符号视为弱符号)。
对于它们,下列三条规则使用:
强引用和弱引用主要针对函数,强引用如果未找到定义则报错,二弱引用未找到定义则不报错。如果未定义,连接器会将弱引用设定为0或者特殊值,弱引用可以用于接口设计。