链接(一)
参考文献:
[1]Randal E.Bryant、David R. O’Hallaron.Computer Systems A Programmer’s Perspective Third Edition 北京:机械工业出版社,2017.4;
[2]蒋本珊.计算机组成原理(第3版)[M].北京:清华大学出版社,2013;
目录:
1、目标文件:
2、可重定位目标文件格式:
3、C语言的static:
4、求符号表条目、符号类型、模块及节:
5、符号解析:
6、链接器解析多重定义的全局符号:
1、目标文件:
(1)格式:
目标文件是按照特定的目标文件格式来组织的,不同系统的目标文件格式不同。
①Linux和Unix系统:ELF(Executable and Linkable Format,可执行可链接)格式;
②Windows系统:PE(Portable Executable,可移植可执行)格式;
③MacOS-X系统:Mach-O格式。
(2)形式:
①可重定位目标文件:包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
eg:Linux:.o Windows:.obj
②可执行目标文件:包含二进制代码和数据,其形式可以被直接复制到内存并执行。
eg:Linux: /bin/bash Windows:.exe
③共享目标文件:一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。
eg:Linux:.so Windows:DLL
2、可重定位目标文件格式:
图2.1 测试代码
(1)命令:gcc -c -g 1.cpp:编译源文件
-g:带有调试信息的目标代码。
(2)命令:objdump -h 1.o:查看object内容的结构
-h:把目标文件的各个段的基本信息打印出来。
图2.2 结果截图1
部分参数说明:
①.text:已编译程序的机器代码。
②.data:已初始化的全局变量和静态变量。
③.bss:未初始化的全局变量和静态变量。(Block Storage Start,块存储开始的首字母缩写),在目标文件中这个字节不占据实际的空间,仅仅是一个占位符。
目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。等到最终链接成可执行文件的时候再在.bss段分配空间。
④.rdata:只读数据,比如printf语句中的格式串和开关语句的跳转表。
⑤.debug:一个调试符号表,存放调试信息,其条目是程序中定义的局部变量和类型定义,程序中定义和应用的全局变量,以及原始的C源文件。
⑥Size:内存中的段大小
⑦File off:File offset,目标文件中的偏移,即段所在的位置
⑧CONTENTS:该段在文件中存在
⑨ALLOC:实际上文件中不存在的内容
⑩Algn:Align,是程序头部指定的对齐,对齐要求是一种优化,使得当程序执行时,目标文件中的段能够有效率地传送到内存中。
如图所示,各段的位置和长度为:
头部信息:从0x00000000到0x000001cc,长度为0x1cc;
代码段.text:从0x000001cd到0x00000208,长度为0x3c;
数据段data:从0x00000208到0x0000020c,长度为4,即存放int b = 2;
只读数据段rdate:从0x0000020c到0x00000210,长度为4,即存放printf中的%d\n内容。
(3)命令:size 1.o:用于查看目标文件的代码段、数据段、bss段的长度。
图2.3 结果截图2
部分参数说明:
dec:三个段的长度之和的十进制
hex:三个段的长度之和的十六进制
因为size默认是运行在“Berkeley compatibility mode”模式下,会将不可执行的拥有ALLOC属性的只读段归到.text下,导致.text段的长度与上面计算的长度0x3c(60)有所差异,因此使用如下命令,使得size运行在“System V compatibility mode”模式下:
(4)命令:size -A 1.o:
图2.4 结果截图3
(5)命令:objdump -s -d 1.o
-s参数将所有段的内容以十六进制的方式打印出来,-d参数将所有包含指令的段反汇编。
反汇编定义:将目标代码转换为汇编代码的过程,也可以说是把机器语言转换为汇编语言代码。
图2.5 结果截图4
图2.6 结果截图5
最左边一列是偏移量,中间四列是十六进制内容,最右边一列是.text段的ASCII码。
(6)命令:objdump -x -s -d 1.o
可以查看到代码段、数据段、只读数据段的内容
图2.7 结果截图6
观察可以发现.data段的四个字节为0x02,0x00,0x00,0x00,转换为十进制,为2,0,0,0,即int b = 2的值。
.rdate段的四个字节为0x25,0x64,0x0a,0x00,转换为十进制,为37,100,10,0,查看ascii码表,对应的字符为%,d,\n,即printf中存放的内容。
3、C语言的static:
C程序员使用static属性隐藏模块内部的变量和函数声明,如同Java中使用public和private声明一样。
在C中,源文件扮演模块的角色,任何带有static属性声明的全局变量或者函数都是模块私有的。任何不带static属性声明的全局变量和函数都是公共的,可以被其他模块访问。因此尽可能用static属性来保护变量和函数时很好的编程习惯。
4、求符号表条目、符号类型、模块及节:
(1)符号表:每个可重定位目标模块m都有一个符号表,它包含m定义和引用的符号的信息。
.symtab:存放在程序中定义和引用的函数和全局变量的信息,不包含对应于本地非静态程序变量的任何符号。
(2)符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。.symtab节中包含ELF符号表。
(3)COMMON和.bss的区别:
①COMMON:未初始化的全局变量;
②.bss:未初始化的静态变量,以及初始化为0的全局或静态变量。
(4)示例:
对于下图所示的m.o和swap.o模块。对于每个在swap.o中定义或引用的符号,请指出它是否在模块swap.o中的.symtab节中有一个符号表条目。如果是,请指出定义该符号的模块(swap.o或者m.o)、符号类型(局部、全局或者外部)以及它在模块中被分配到的节(.text、.data、.bss或者COMMON)。(m.c和swap.c在同一目录下)
图4.1 题目m.c源码
图4.2 题目swap.c源码
图4.3 题目
图4.4 参考答案
5、符号解析:
(1)链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。
(2)对C++和Java中链接器符号的重整:
C++和Java中能够使用重载函数,是因为编译器将每个唯一的方法和参数列表组合编码成一个对链接器来说唯一的名字。这种编码过程叫做重整(mangling),而相反的过程叫做恢复(demangling)。
(3)C++和Java使用的重整策略:
①一个被重整的类名字是由名字中字符的整数数量,后面跟原始名字组成的。
比如:类People编码为6People。
②一个被重整的方法被编码为原始方法,后面加上双下划线,加上被重整的类名,再加上每个参数的单字母编码。
比如:People::setInfo(int, String)编码为setInfo__6PeopleiS。
6、链接器解析多重定义的全局符号:
(1)强符号:函数和已初始化的全局变量
弱符号:未初始化的全局变量
(2)Linux链接器使用下面三条原则来处理多重定义的符号名:
①不允许有多个同名的强符号。
②如果有一个强符号和多个弱符号同名,那么选择强符号。
③如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
(3)示例:
REF(x.i)->DEF(x.k)表示链接器将把模块i中对符号x的任意引用与模块k中x的定义关联起来。对于下面的示例,用这种表示法来说明链接器将如何解析每个模块中对多重定义符号的引用。如果有一个链接时错误,写“错误”,如果链接器从定义中任意选择一个,则写“未知”。
A)①int main(){}
②int main;
int p2(){}
REF(main.1)->DEF(main.1)
REF(main.2)->DEF(main.1)
B)①void main(){}
②int main = 1;
int p2(){}
REF(main.1)->DEF(错误)
REF(main.2)->DEF(错误)
C)①int x;
void main(){}
②double x = 1.0;
int p2(){}
REF(x.1)->DEF(x.2)
REF(x.2)->DEF(x.2)