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文件
2 目标文件内容 ??目标文件中无疑包含程序运行的代码和数据,只是如何对这些内容进行管理?目标文件管理这些内容通过段的方式进行,不同类型的数据等信息通过段进行区分。分段的好处:
  • 程序中代码是只读,部分数据可读可写,通过分段能够方便进行权限管理;
  • 计算机的八二原则,不同数据和代码分开存储能够有效利用计算机的缓存功能;
  • 有利于资源共享,通常计算机中代码是只读的,因此当又多个程序需要使用同一份代码时,可以将共享的内容区分开方便共享,节省资源。
尝试使用相关命令查看目标文件的内容:
??使用的文件示例,文件中包含常量字符串、静态初始化变量、静态未初始化变量、局部初始化变量、局部未初始化变量、全局初始化变量和全局未初始化变量以及简单函数调用(文件中带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表示表示有该标记的节会在运行时分配并装载进入内存。根据文件中的偏移画出的文件结构图如下:
c++|程序员自我修养阅读笔记——目标文件里有什么
文章图片

??从输出的段结构图中能够看到bssrodata的偏移一致,且二者都有各自的尺寸,并且虽然有.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段中0f10刚好对应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_bglob_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的指针表。
c++|程序员自我修养阅读笔记——目标文件里有什么
文章图片


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。
??符号签名中包含参数类型也是C++实现函数重载的基础,但是C++也常常需要使用C的接口,如果使用C++的符号签名则无法找到对应的接口。可利用C++中的extern "C"关键字保证对应的函数的符号签名使用C的规则。
4.2 弱符号和强符号 ??C中存在强符号和弱符号,强符号不允许多重定义,弱符号允许多个定义但是实际运行时只有一个实体。对于C语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号(C++并没有将未初始化的全局符号视为弱符号)。
对于它们,下列三条规则使用:
  • 同名的强符号只能有一个,否则编译器报"重复定义"错误;
  • 允许一个强符号和多个弱符号,但定义会选择强符号的;
  • 当多个弱符号时,选择占用空间最大的;
  • 当有多个弱符号相同时,链接器选择最先出现那个,也就是与链接顺序有关。
【c++|程序员自我修养阅读笔记——目标文件里有什么】??强引用和弱引用主要针对函数,强引用如果未找到定义则报错,二弱引用未找到定义则不报错。如果未定义,连接器会将弱引用设定为0或者特殊值,弱引用可以用于接口设计。
5 reference
  • introduce-elf
  • elf文件结构

    推荐阅读