第三章 目标文件里面有什么
1. 可执行文件格式
-
目标文件:源代码编译后但未进行链接的中间文件。例如Windows的.obj和Linux的.o。目标文件的内容和结构和可执行文件几乎是一样的。
-
静态链接库(Static Linking Lirary):把多个目标文件捆绑在一起形成一个文件。可以简单地理解为一个包含很多目标文件的文件包。例如Windows的.lib和Linux的.a。
-
可执行文件格式(Executable Format):Window下的PE(Portable Executable)和Linux下的ELF(Executable Linkable Format)。
-
可执行文件(Executable File)(Windows的.exe和Linux的ELF可执行文件),动态链接库(Dynamic Linking Library)(Windows的.dll和Linux的.so),静态链接库(Static Linking Lirary)(Windows的.lib和Linux的.a)都按照可执行文件的格式进行存储。
-
Linux下可使用file命令查看文件的格式。例如:
$ file /bin/bash
2. 段
-
可执行文件格式按照信息的不同属性,以段(Section)的形式进行存储。基本上分为两个段:程序指令段和程序数据段。
-
GCC提供工具objdump可以查看目标文件的结构和内容。例如:
$ objdump -h SimpleSection.o
-
使用readelf命令可以产看ELF文件。例如查看ELF文件头:
$ readelf -h SimpleSection.o
-
ELF文件格式示例
- Header文件头段:描述文件的属性,包含是否可执行,是静态链接还是动态链接,入口地址,目标操作系统等。
- .text代码段:保存源代码编译后的机器指令。
- .data数据段:保存初始化了的全局静态变量和局部静态变量。
- .rodata只读数据段:保存程序里的只读变量(如const修饰的变量)和字符串常量。
- .bss段:保存未初始化的全局变量和局部静态变量。
- 其他段:
3. 链接的接口—符号
-
链接过程的本质:把多个不同的目标文件相互粘在一起。目标文件之间的相互拼合实际上就是目标文件之间对地址的引用,即对函数和变量的地址的引用。
-
在链接中每个函数和变量都必须有自己独特的名字,才能避免链接过程中不同的变量和函数之间的混淆。所以使用符号来当作链接中的粘合剂,整个链接的过程基于符号来找到引用的函数和变量的地址。
-
在链接中,将函数和变量称为 符号(Symbol),函数名和变量名就是符号名(Symbol Name)。每个定义的符号都有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,符号值就是它们的地址。
-
符号表(Symbol Table):每一个目标文件里面都会有一个相应的符号表。这个表里面记录了目标文件所用到的所有符号。
-
使用nm命令可以查看符号:
$ nm SimpleSection.o
-
目标文件里面可以保存调试信息,例如在使用GCC编译时加上"-g"参数。Linux下使用strip命令可以去掉ELF文件中的调试信息。例如:
$ strip foo
4. 符号修饰与函数签名
-
编译器最初编译源代码产生目标文件的时候,符号名与变量和函数的名字是一样的。如果用户自定义的符号名和标准函数库中的一样,就会导致目标文件冲突,所以在定义函数名和变量名时一定要避免与其他文件中的定义发生冲突。
-
为了防止符号名冲突,UNIX下C语言规定,所有的全局变量和函数经过编译后,都在符号名前加上下划线“”。例如函数“foo”编译后的符号名为“foo”。Linux下GCC编译器默认去掉了“”。而Windows下的Visual C++还保留着“”。
-
C++增加了命名空间(Namespace)来解决多模块的符号冲突问题。
-
C++有函数重载特性,为了使链接器区分这两些重载函数,引入了符号修饰(Name Decoration)机制。C++的源代码编译后的目标文件中所使用的符号名是相对应的函数和变量的经过修饰后的名称,即使函数名相同,它们的符号名也不相同。
-
函数签名(Function Signature):函数签名包含了一个函数的信息,包括函数名,参数类型,参数个数,所在的类和命名空间及其它信息。函数签名用于识别不同的函数。例如函数重载时,两个函数名相同,但是函数签名不一样,那么编译器就认为是两个函数。每个函数签名对应一个修饰后的名称。例如GCC的修饰方法:
5. extern “C”
-
C++为了与C兼容,在符号的管理上,C++用关键字extern “C” 来声明或定义C的符号。C++编译器会将在其修饰的内部的代码当作C语言来处理,也就是C++的名称修饰机制不会应用到该函数上面。这个函数将按照C语言的方式来进行符号修饰,这样C语言的链接器就可以链接到这个目标文件,而C++链接器也会通过C语言的修饰方式来链接这个函数。
extern "C" { int func(int); int var; }
-
有些头文件可能被C语言代码或C++代码包含。例如C语言函数库中的string.h中声明了
void *memset(void *, size_t);
。- 如果不加任何处理,当C语言包含这个头文件并用到这个函数时,编译器会将memset符号引用正确处理,链接到C函数库的相应符号memset。
- 但是如果在C++中,编译器会将引入的string.h头文件中的memset符号修饰为_Z6memsetPvii,链接器就无法从C函数库中找到memset符号进行链接。
所以对于C++来说,需要用extern “C” 来修饰memset这个函数。但是C语言没有这个关键字,所以不能直接在string.h中用这个关键字。所以用宏“__cplusplus”来隔开这个关键字,如果是C++编译器就默认定义这个宏。
#ifdef __cplusplus extern "C" { #endif void *memset(void *, size_t); #ifdef __cplusplus } #endif