目标文件中到底有什么?
来源:互联网 发布:淘宝网个人资料 编辑:程序博客网 时间:2024/04/28 05:46
目标文件到底有什么?
本系列文章主要是讲述c++编译链接的那点事,这个对于刚入门的程序员来说是必须修炼的内功之一。第一篇文章将主要配合示例来说明c++编译后的.o文件究竟有些什么?
前言
在了解.o文件之前,有必要理清楚基本的编译流程, 如下图所示:
- 预处理阶段:预处理器主要是处理源代码中以”#”开始的命令,比如头文件等等,生成.i中间文件。预处理的命令:
$gcc -E hello.c -o hello.i
- 编译阶段: 编译器会将.i文件翻译成.s文件,具体过程是通过一系列的词法分析,语义分析,以及优化后生成相应的汇编文件。其处理命令为:
$gcc -S hello.i -o hello.s
- 汇编阶段: 汇编器是将汇编代码翻译成机器语言指令,并打包成一个可重定位目标程序,并将结果保留在目标文件中。它的字节编码是机器语言指令,而不是字符。
$gcc -c hello.s -o hello.o
- 链接阶段: 链接器会把所有的.o文件进行链接,并生成最终的可执行文件。那么为什么不直接在汇编过程中就完成链接过程呢?为什么需要链接?
这里先给出一个粗略的回答:首先,我们要明白链接器主要作用是符号解析和地址的重定位,这个具体是什么意思,后续系列会慢慢涉及到。其次,由于链接器处理了符号的解析和地址的重定位,那么使得我们在构造大型软件工程时,自然会思考我们可否借鉴建房子的过程,一块一块砖垒起来,水泥就是中间的粘合剂。于是,写代码就能够分模块完成,最后由链接器完成类似水泥的工作就行了。链接器使得每个模块的分离编译成为可能,当我们需要改变其中一个模块时,只需要重新编译和链接,而不必再编译其他模块文件了,大大节约了时间成本。
总之,编译的基本流程如上所述,在链接实现的过程中,时间点可以是在编译时链接,可以是加载时链接,甚至是运行时链接。这三种的不同会在下面以及后续文章慢慢涉及到。
目标文件的神秘面纱
从上述编译的流程我们可以大概猜到,目标文件至少是包括相关的机器指令代码和数据的,除此之外,也会包括链接时所需要的一些信息,比如符号表、调试信息等等。一般而言,目标文件是以节(section) 为单位进行组织的,一个典型的可重定位目标文件如下:
代码示例分析
源代码SimpleSection.c如下:
int printf(const char* format, ...);int g_init_var = 25; //初始化的全局变量int g_uninit_var; //未初始化的全局变量void callPrintf(int i) { printf("%d\n", i);}int main(void) { static int static_var = 70; //局部的初始化静态变量 static int static_var_uninit; //局部的未初始化的静态变量 int a = 1; //局部的初始化变量 int b; //局部的未初始化的变量 callPrintf(static_var + static_var_uninit + a + b); return a;}
我们先编译生成SimpleSection.o, 然后使用下面的命令查看
objdump -h SimpleSection.o
注意到.data起始位置是从0x00000094而不是0x00000091开始的,原因在于.text和.data的对齐方式是4字节,即必须要被4整除:91=9*16+1; 94=9*16+4,可以明白94能被4整除,所以.data是从0x00000094开始的。
根据偏移量可画出相应的图如下(注意地址是向上增长的):
为了看清楚每个section具体里面是什么,我们采用下面命令:
objdump -s -d SimpleSection.o
得到如下的内容:
SimpleSection.o: file format elf64-x86-64Contents of section .text: 0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E... 0010 bf000000 00b80000 0000e800 000000c9 ................ 0020 c3554889 e54883ec 10c745f8 01000000 .UH..H....E..... 0030 8b150000 00008b05 00000000 8d040203 ................ 0040 45f80345 fc89c7e8 00000000 8b45f8c9 E..E.........E.. 0050 c3 . Contents of section .data: 0000 19000000 46000000 ....F... Contents of section .rodata: 0000 25640a00 %d.. Contents of section .comment: 0000 00474343 3a202847 4e552920 342e342e .GCC: (GNU) 4.4. 0010 36203230 31313037 33312028 52656420 6 20110731 (Red 0020 48617420 342e342e 362d3429 00 Hat 4.4.6-4). Contents of section .eh_frame: 0000 14000000 00000000 017a5200 01781001 .........zR..x.. 0010 1b0c0708 90010000 1c000000 1c000000 ................ 0020 00000000 21000000 00410e10 8602430d ....!....A....C. 0030 065c0c07 08000000 1c000000 3c000000 .\..........<... 0040 00000000 30000000 00410e10 8602430d ....0....A....C. 0050 066b0c07 08000000 .k...... Disassembly of section .text:0000000000000000 <callPrintf>: 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: bf 00 00 00 00 mov $0x0,%edi 15: b8 00 00 00 00 mov $0x0,%eax 1a: e8 00 00 00 00 callq 1f <callPrintf+0x1f> 1f: c9 leaveq 20: c3 retq 0000000000000021 <main>: 21: 55 push %rbp 22: 48 89 e5 mov %rsp,%rbp 25: 48 83 ec 10 sub $0x10,%rsp 29: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp) 30: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 36 <main+0x15> 36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3c <main+0x1b> 3c: 8d 04 02 lea (%rdx,%rax,1),%eax 3f: 03 45 f8 add -0x8(%rbp),%eax 42: 03 45 fc add -0x4(%rbp),%eax 45: 89 c7 mov %eax,%edi 47: e8 00 00 00 00 callq 4c <main+0x2b> 4c: 8b 45 f8 mov -0x8(%rbp),%eax 4f: c9 leaveq 50: c3 retq
可以看到,“Contents of section .text”长度确实为51个字节,其反汇编形成的正好是函数callPrintf( )和main( )。在数据段.data中,内容为“19000000 46000000”,为8个字节。注意到数据段储存的是初始化的全局变量和局部静态变量。我们的代码中有g_init_var = 25, static_var = 70, 都属于int,而int所占字节为4,所以两个变量占据8个字节。由于机器是小端储存的形式,00000019对应25, 00000046对应70。
重头戏节头表和段表
在基本了解了ELF文件的轮廓后,我们需要更加细致地研究下其中某些重要的表,这些表中会记录更多信息,以方便链接器进行链接。
- 文件头(ELF Header)
文件头记录了描述整个文件的基本属性值,比如ELF文件版本、目标机器型号、程序入口地址等等。我们可以通过如下命令查看文件头的信息:
readelf -h SimpleSection.o
得到示例代码的如下信息:
我着重描述下几个重要的属性:程序入口地址,这个规定了ELF程序的入口虚拟地址,操作系统在加载完该程序后从这个地址开始执行进程的指令。Start of section headers, 段表在文件中的偏移值,该示例是400,也就是段表是从文件的第401个字节开始的。Size of section headers: 每个段表描述符的大小; Number of section headers: 段表的个数。
- 段表: 段表是除了文件头外最重要的结构,主要描述了ELF中的各个段的信息,比如每个段的段名,段的长度,段在文件中的偏移、读写权限等等。ELF文件的段结构就是由段表来决定的,编译器、链接器和装载器都是根据段表来定位和访问各个段的属性等等。并且通过文件头和段表信息,我们可以画出ELF更加细致的结构。
通过如下命令查看示例代码的段表结构:
readelf -S SimpleSection.o
结果如下所示:
There are 13 section headers, starting at offset 0x190: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 0000000000000051 0000000000000000 AX 0 0 4 [ 2] .rela.text RELA 0000000000000000 000006b8 0000000000000078 0000000000000018 11 1 8 [ 3] .data PROGBITS 0000000000000000 00000094 0000000000000008 0000000000000000 WA 0 0 4 [ 4] .bss NOBITS 0000000000000000 0000009c 0000000000000004 0000000000000000 WA 0 0 4 [ 5] .rodata PROGBITS 0000000000000000 0000009c 0000000000000004 0000000000000000 A 0 0 1 [ 6] .comment PROGBITS 0000000000000000 000000a0 000000000000002d 0000000000000001 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 0000000000000000 000000cd 0000000000000000 0000000000000000 0 0 1 [ 8] .eh_frame PROGBITS 0000000000000000 000000d0 0000000000000058 0000000000000000 A 0 0 8 [ 9] .rela.eh_frame RELA 0000000000000000 00000730 0000000000000030 0000000000000018 11 8 8 [10] .shstrtab STRTAB 0000000000000000 00000128 0000000000000061 0000000000000000 0 0 1 [11] .symtab SYMTAB 0000000000000000 000004d0 0000000000000180 0000000000000018 12 11 8 [12] .strtab STRTAB 0000000000000000 00000650 0000000000000067 0000000000000000 0 0 1Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
readelf 相比于objdump,可以查看到更加细致的结构。我们注意到第一个段类型为NULL,表示无效的,所以实际上SimpleSection.o只有12个有效的段。结合所有的节头表,得到完整的ELF结构如下:
- 重定位表:在上述分析中,我们看到有个段表叫做.rela.text,那么它的作用是什么呢?我们知道,链接器在处理目标文件时,需要对目标文件某些部分进行重定位,比如在我们的源码中,main函数中调用了callPrintf函数,这里表明.text中有一处对于该绝对地址的引用。
最后在详解静态链接时,我们再看看符号表里面的内容,为以后分析静态链接做准备。命令如下:
readelf -s SimpleSection.o
信息如下:
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 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.1601 7: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var_uninit.1602 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 g_init_var 12: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM g_uninit_var 13: 0000000000000000 33 FUNC GLOBAL DEFAULT 1 callPrintf 14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf 15: 0000000000000021 48 FUNC GLOBAL DEFAULT 1 main
- 目标文件中到底有什么?
- 目标文件里面到底有什么(1)?
- 目标文件里面到底有什么(2)?
- 目标文件里面到底有什么(1)?
- 目标文件里面到底有什么(2)?
- 聊聊目标文件到底长什么样子
- 目标文件里有什么
- 目标文件有什么读书笔记
- 目标文件里有什么
- 目标文件里有什么——揭秘目标文件
- 第三章 目标文件里有什么
- 目标文件有什么读书笔记2
- 目标文件有什么读书笔记3
- 3 目标文件里有什么
- 1.3.1 日志文件里到底有什么
- 程序员的自我修养<3.目标文件有什么>
- 第三章——目标文件里有什么
- 自我修养——目标文件.o有什么
- js中选中表格中的所有勾选框
- Linux C——TCP、UDP的区别和分别使用的场合
- 看懂执行计划
- 20161206CSS笔记
- Keil MDK从未有过的详细使用讲解
- 目标文件中到底有什么?
- 自己常用的实用vim快捷键
- Unity 3D——NGUI初级(3)
- 简单的轮播
- 在 caffe 的 data_later.cpp 中做数据增强
- 实习第二天
- 树的简单操作
- NullPointerException android.support.v4.app.FragmentManagerImpl.saveFragmentBasicState
- Python