Lab_4实验报告
屏幕截图
考察内容
本次lab的考察内容为链接的有关知识(其实感觉更像是在传授技巧?),包括通过Makefile,利用g++或ld命令实现静态/动态链接。
解题思路
Task0
目标任务
编写Makefile文件的同时修改main.cpp,将main.cpp编译为可执行文件main。
思路
首先观察各份代码,理清程序思路。main函数的思路是调用三个函数:testPrint(),testPrint(num),notATest()。其中前两个函数在some.h中声明,在some.cpp中定义;最后一个函数在some.cpp中定义。因为main函数只能使用some.h中声明的函数,因此notATest不能被调用。应注释掉该行代码。随后在Makefile中键入以下代码:
main:main.cpp g++ -o main main.cpp some.cpp
运行make main,成功。
思考题
Q1:代码中的错误是什么?由此看来.h与.cpp的关系是怎样的?
A1:错误是main函数调用了.cpp文件中定义的函数。.h和.cpp的关系类似包含关系,链接器在遇到未声明函数时总是先在.h里找声明,随后在.cpp里找对应函数的实现。
Task1
目标任务
编写Makefile文件,将main0.cpp编译为main0,将main1.cpp编译为main1,当编译目标为main1时,提供以下形式的“调试开关”选项:
make main1 debug=True
思路
不考虑“调试开关”选项,这个Makefile文件和Task0没有太大区别。只是编译目标从一个变成了两个。
程序思路是:main0/1.cpp都调用了function0/1.h中声明的函数,这些函数在实现过程中又都调用了shared.h中的函数。因此makefile语句应为:
main0:main0/1.cpp function0.cpp function1.cpp shared.cpp g++ main0/1.cpp function0.cpp function1.cpp shared.cpp -o main0/1
现在考虑加入“调试开关”。在Makefile文件开头定义变量debug=False,随后在main1的入口处键入ifeq语句:
ifeq ($(debug),True) g++ main1.cpp function0.cpp -DDEBUG function1.cpp shared.cpp -o main1 else g++ main1.cpp function0.cpp function1.cpp shared.cpp -o main1 endif
结果运行后总是出错,故障为:
/bin/sh: 1: Syntax error: word unexpected (expecting ")")
随后百度查询到类似情况,原因是ifeq似乎不支持缩进(……),于是去除缩进后再次运行,成功。
思考题
Q1:为什么两个function.h都引用了shared.h而没有出问题?
A1:因为链接器在链接之前,编译器先将function0/1.cpp编译为独立的.o文件,其中各自包含了shared.h中需要调用的函数,因此在链接时没有出问题。
Q2:如果把shared.h中注释掉的变量定义取消注释会出什么问题?为什么?
A2:会报错:"'FOO[abi:cxx11]'被多次定义"。原因在于function0/1.h和shared.cpp都包含了shared.h,即shared.h在没有添加#ifnedf的情形下被重复包含了,因此变量被重复定义,导致链接器报错。
Q3:通常使用shared.h中另外被注释掉的宏命令来规避重复引用的风险,原理是什么?取消这些注释之后上一题的问题解除了吗?不管解没解除背后的原因是什么?
A3:原理是通过宏命令定义判断一个.h文件是否已经被包含过,避免重复包含。取消注释之后问题未能解除。查询资料\(^{[1]}\)得知原因为function0/1.o中均包含了shared.h中变量定义的部分,因此虽然shared.h没有被重复包含,变量依然被重复定义了。
Task2
目标任务
将A文件夹和C文件夹中的代码编译为静态链接库,与B文件夹中已经生成的静态链接库一起和main.cpp编译链接为可执行文件。
思路
在Github\(^{[2]}\)上复习了一下gcc编译为.o文件的指令,将其分别键入A文件夹和C文件夹的Makefile文件中。
在Task2文件夹中编写Makefile文件,利用提示中的cd
随后调用g++指令将三个静态链接库和main.cpp编译链接为可执行文件。
思考题
Q1:若有多个静态链接库需要链接,写命令时需要考虑静态链接库和源文件在命令中的顺序吗?是否需要考虑是由什么决定的?
A1:多次尝试后发现需要考虑源文件和静态链接库之间的顺序,源文件应放在其依赖的链接库的前面。根据CSAPP,这是由于链接器本身的工作原理决定的。此外,不需要考虑静态链接库内部的顺序,无论静态链接库之间是否有依赖关系。这个现象的原因我不太清楚。
Q2:可以使用size main命令来查看可执行文件A所占的空间,输出结果的每一项是什么意思?
A2:
text | data | bss | dec | hex | filename |
---|---|---|---|---|---|
23000 | 744 | 288 | 24032 | 5de0 | main |
text指储存机器代码的部分占内存的大小;data指初始化的全局和静态变量占内存的大小;bss指未初始化的全局和静态变量占内存的大小;dec和hex为前三者之和的十进制和十六进制形式;filename为可执行文件名。
Task3
目标任务
将A/C文件夹中的代码编译为动态链接库放在/Task3中,与既有的libB.so一起和main.cpp动态链接编译为可执行文件。
思路
按照提示中的步骤,对Task2中操作进行简单替换和重复即可。
思考题
Q1:动态链接库在运行时也需要查找库的位置,在Linux中,运行时动态链接库的查找顺序是怎样的?
A1:根据查找到的资料\(^{[3]}\),动态链接库的查找顺序为:
- 编译目标代码时指定的动态库搜索路径
- 环境变量LD_LIBRARY_PATH指定的动态库搜索路径
- 配置文件/etc/ld.so.conf中指定的动态库搜索路径
- 默认的动态库搜索路径/lib
- 默认的动态库搜索路径/usr/lib
Q2:使用size main查看编译出的可执行文件占据的空间,与使用静态链接库相比占用空间有何变化?哪些部分的哪些代码会导致编译出文件的占用空间发生这种变化?
text | data | bss | dec | hex | filename |
---|---|---|---|---|---|
3264 | 768 | 96 | 4128 | 1020 | main |
text部分占用空间大幅减少,data和bss部分占用空间变化不大。推测是由于A.cpp中定义的MassSTR在动态链接时未被包含在text中。
Q3:编译动态链接库时-fPIC的作用是什么,不加会有什么后果?
A3:-fPIC的作用是生成与位置无关的代码。即在Makefile中生成的.so文件中,相关数据对象不使用绝对地址,而是相对地址。这样在动态链接时.so文件可以方便地被插入到内存的任意位置。如果不加-fPIC的话,.so文件使用绝对地址,则在每次链接时会对其中绝对地址进行重定位,生成一份副本,失去了动态链接的意义。
Q4:现在被广泛使用的公开的动态链接库如何进行版本替换或共存?(选做)
Task4
目标任务
在Makefile文件中通过ld命令对Task0的代码进行手动链接。
思路
看了看CSAPP,发现上面关于具体操作的内容似乎只有一句话:
ld -o prog [system object files and args] /tmp/main.o /tmp/sum.o
所以这个[system object files and args]是什么呢……我就直接求助百度了。
百度上关于ld手动链接的内容不是很多,其中看起来有用的大多数也是建议使用g++ -v然后将输出参数拷贝到终端/Makefile文件中。最后在stackoverflow\(^{[4]}\)上找到了比较详细的解答。
这个解答的大意就是在寻常g++指令的末尾添加-v参数,然后在输出信息中找到包含collect2(因为collect2就是ld命令的别名)的一行,将collect2之后的内容拷贝到编辑器中,用"\"分割修饰一下之后运行。这个作者还尝试着删去了一些他觉得无关紧要的部分,然而我并不清楚这个技巧,于是就机械复读了。
随后发现运行时出现了遗漏分隔符的现象,百度之后发现是因为Makefile的缩进格式要求非常严格,我之前用编辑器编辑时为了美观,一次缩进用了两个Tab,纠正之后运行成功。
思考题
Q1:C runtime是什么,主要负责什么工作?(选做)
Q2:动态链接器一个操作系统只需要一个吗?为什么?(选做)