c++|程序员自我修养阅读笔记——目标文件里有什么
测试环境:
?tmp uname --version
uname (GNU coreutils) 8.25
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later .
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.
1 目标文件格式 ??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 NameSizeVMALMAFile offAlgn
0 .text0000003e00000000000000000000000000000000000000402**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data0000000500000000000000000000000000000000000000802**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss0000000500000000000000000000000000000000000000882**2
ALLOC
3 .rodata0000000700000000000000000000000000000000000000882**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .data.rel.local 0000000800000000000000000000000000000000000000902**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
5 .comment0000002a00000000000000000000000000000000000000982**0
CONTENTS, READONLY
6 .note.GNU-stack 0000000000000000000000000000000000000000000000c22**0
CONTENTS, READONLY
7 .eh_frame0000005800000000000000000000000000000000000000c82**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
??
size main.o
能够查看数据段和代码段的大小:textdatabssdechex filename
152168176b0 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 2e6f00main.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 7e31382e5.0-3ubuntu1~18.
0020 30342920 372e352e 300004) 7.5.0.
??上面的内容中并未看到
bss
的内容,通过查看符号表objdump -t main.o
能够看到未经初始化的static_b
和glob_b
存储在bss中。但是这也不是很绝对,因为全局符号存在强符号和弱符号的区分,未经初始化的全局变量可能初始化为COMMON在链接时再分配内存。SYMBOL TABLE:
0000000000000000 ldf *ABS*0000000000000000 main.cpp
0000000000000000 ld.text0000000000000000 .text
0000000000000000 ld.data0000000000000000 .data
0000000000000000 ld.bss0000000000000000 .bss
0000000000000000 ld.rodata0000000000000000 .rodata
0000000000000000 ld.data.rel.local0000000000000000 .data.rel.local
0000000000000004 lO .data0000000000000001 _ZZ4mainE8static_a
0000000000000004 lO .bss0000000000000001 _ZZ4mainE8static_b
0000000000000000 ld.note.GNU-stack0000000000000000 .note.GNU-stack
0000000000000000 ld.eh_frame0000000000000000 .eh_frame
0000000000000000 ld.comment0000000000000000 .comment
0000000000000000 gF .text0000000000000014 _Z3addii
0000000000000000 gO .data.rel.local0000000000000008 file
0000000000000000 gO .data0000000000000004 glob_a
0000000000000000 gO .bss0000000000000004 glob_b
0000000000000014 gF .text000000000000002a main
??下面时将源文件使用c进行编译得到的未初始化的全局符号的存储方式,时典型的弱符号存储方式:
0000000000000004O *COM*0000000000000004 glob_b
??ELF文件还包含很多其他段,比如调试信息相关的段不再赘述。
3 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] NameTypeAddressOffset
SizeEntSizeFlagsLinkInfoAlign
[ 0]NULL000000000000000000000000
00000000000000000000000000000000000
[ 1] .textPROGBITS000000000000000000000040
000000000000003e0000000000000000AX001
[ 2] .rela.textRELA000000000000000000000310
00000000000000180000000000000018I1218
[ 3] .dataPROGBITS000000000000000000000080
00000000000000050000000000000000WA004
[ 4] .bssNOBITS000000000000000000000088
00000000000000050000000000000000WA004
[ 5] .rodataPROGBITS000000000000000000000088
00000000000000070000000000000000A001
[ 6] .data.rel.localPROGBITS000000000000000000000090
00000000000000080000000000000000WA008
[ 7] .rela.data.rel.lo RELA000000000000000000000328
00000000000000180000000000000018I1268
[ 8] .commentPROGBITS000000000000000000000098
000000000000002a0000000000000001MS001
[ 9] .note.GNU-stackPROGBITS0000000000000000000000c2
00000000000000000000000000000000001
[10] .eh_framePROGBITS0000000000000000000000c8
00000000000000580000000000000000A008
[11] .rela.eh_frameRELA000000000000000000000340
00000000000000300000000000000018I12108
[12] .symtabSYMTAB000000000000000000000120
0000000000000198000000000000001813128
[13] .strtabSTRTAB0000000000000000000002b8
00000000000000510000000000000000001
[14] .shstrtabSTRTAB000000000000000000000370
00000000000000760000000000000000001
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:
OffsetInfoTypeSym. ValueSym. Name + Addend
000000000033000c00000002 R_X86_64_PC320000000000000000 _Z3addii - 4Relocation section '.rela.data.rel.local' at offset 0x328 contains 1 entry:
OffsetInfoTypeSym. ValueSym. Name + Addend
000000000000000500000001 R_X86_64_640000000000000000 .rodata + 0Relocation section '.rela.eh_frame' at offset 0x340 contains 2 entries:
OffsetInfoTypeSym. ValueSym. Name + Addend
000000000020000200000002 R_X86_64_PC320000000000000000 .text + 0
000000000040000200000002 R_X86_64_PC320000000000000000 .text + 14
字符串表:
??字符串表中存储ELF文件中使用到的字符串,一般有三种字符串表分别为
shstrtab
保存section头中保存的字符串;strtab
保存elf中使用到的字符串;dynstr
保存了动态链接字符串表,表中存放了一系列字符串,这些字符串代表了符号名称,以空字符作为终止符。4 链接中的符号 4.1 符号 ??程序需要链接的原因时因为程序的每个文件特别是C类的语言时单独分模块编译的,每个编译单元仅仅知道当前编译单元中的信息,当引用到其他编译单元的函数或者变量时无法明确该变量或者函数的地址。因此需要在连接时将这些符号的地址明确,一般函数和变量统称为符号,函数名和变量名为符号名。
??编译时每个编译单元都会有一个符号表表明对应的符号在当前编译单元中的地址和值,因此在链接时需要将多个编译单元的符号表合并。
??使用
readelf -s main.o
查看符号表,能够看到符号表中包含符号的名称、索引、值、尺寸、作用域等信息。Symbol table '.symtab' contains 17 entries:
Num:ValueSize TypeBindVisNdx Name
0: 00000000000000000 NOTYPELOCALDEFAULTUND
1: 00000000000000000 FILELOCALDEFAULTABS main.cpp
2: 00000000000000000 SECTION LOCALDEFAULT1
3: 00000000000000000 SECTION LOCALDEFAULT3
4: 00000000000000000 SECTION LOCALDEFAULT4
5: 00000000000000000 SECTION LOCALDEFAULT5
6: 00000000000000000 SECTION LOCALDEFAULT6
7: 00000000000000041 OBJECTLOCALDEFAULT3 _ZZ4mainE8static_a
8: 00000000000000041 OBJECTLOCALDEFAULT4 _ZZ4mainE8static_b
9: 00000000000000000 SECTION LOCALDEFAULT9
10: 00000000000000000 SECTION LOCALDEFAULT10
11: 00000000000000000 SECTION LOCALDEFAULT8
12: 000000000000000020 FUNCGLOBAL DEFAULT1 _Z3addii
13: 00000000000000008 OBJECTGLOBAL DEFAULT6 file
14: 00000000000000004 OBJECTGLOBAL DEFAULT3 glob_a
15: 00000000000000004 OBJECTGLOBAL DEFAULT4 glob_b
16: 000000000000001442 FUNCGLOBAL DEFAULT1 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
4.1 函数签名 ??编译器为了更好的引用其他模块中的符号对模块中使用到的符号进行符号修饰,即符号签名。签名规则:
- 所有的符号都以"_Z"开头,对于嵌套的名字(在名称空间或在类里面的),后面紧跟"N";
-然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以"E"结尾。比如N::C::func经过名称修饰以后就是_ZN1N1C4funcE; - 对于一个函数来说,它的参数列表紧跟在"E"后面,对于int类型来说,就是字母"i"。所以整个N::C::func(int)函数签名经过修饰为_ZN1N1C4funcEi。
extern "C"
关键字保证对应的函数的符号签名使用C的规则。4.2 弱符号和强符号 ??C中存在强符号和弱符号,强符号不允许多重定义,弱符号允许多个定义但是实际运行时只有一个实体。对于C语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号(C++并没有将未初始化的全局符号视为弱符号)。
对于它们,下列三条规则使用:
- 同名的强符号只能有一个,否则编译器报"重复定义"错误;
- 允许一个强符号和多个弱符号,但定义会选择强符号的;
- 当多个弱符号时,选择占用空间最大的;
- 当有多个弱符号相同时,链接器选择最先出现那个,也就是与链接顺序有关。
5 reference
- introduce-elf
- elf文件结构
推荐阅读
- opencv|opencv C++模板匹配的简单实现
- 改变自己,先从自我反思开始
- 关于自我为中心的一点感想
- 《自我的追寻》读书笔记3
- C语言学习|第十一届蓝桥杯省赛 大学B组 C/C++ 第一场
- “大脑”能自我调节吗()
- 干货来袭(自我管理(来几款撩人的APP))
- c++基础概念笔记
- Java程序员阅读源码的小技巧,原来大牛都是这样读的,赶紧看看!
- 故事//出差前VS出差后分裂的自我