本节书摘来自异步社区《C程序设计新思维》一书中的第1章,第1.3节,作者 【美】Ben Klemens,更多章节内容可以访问云栖社区“异步社区”公众号查看
1.3 库的路径
现在我们有了编译器,有POSIX的工具包,还有一个可以用来方便地安装几百个库的包管理器。现在我们着手解决用这些工具来编译我们程序的问题。
我们必须从编译器命令行开始,并很快会陷入一团混乱,但是我们还是可以用三个(有时候是三个半)相对简单的步骤结束。
1. 设置一个编译器配置变量表。
2. 设置一个要连接的库的变量表。所谓的半个步骤是指,有时你不得不设定一个唯一的变量,用来一边编译一边连接;或者有时不得不设定两个变量,分别用在编译时和运行时的连接。
3. 设置一个根据这些变量来协调编译的系统。
为了用一个库,你必须告诉编译器你将从库中两次导入函数:一次是为了编译,一次是为了连接。对于一个在标准位置的库,这两次声明都通过程序中的#include和-l编译选项发生。
例1-1展示了一个小例子,可以用来做一些神奇的(至少对于我来说是这样的;如果统计学术语对你来说就像希腊文一样,其实也没关系)算数。erf(x)是C99标准的错误处理函数,是与平均数为0、均方差为sqrt{2}的从0到x的正则分布的积分紧密相关。这个例子中,我们用erf来验证一个在统计学家中流行的领域(一个标准大样本假设的95%置信区间)。我们把这个文件命名为erf.c。
例1-1 一个对标准库的一行调用(erf.c)
对#include行你应该已经很熟悉了。编译器将把math.h和stdio.h文件添加在源文件的这个地方,并因此导入了printf、erf和sqrt的声明。在math.h中的声明并没有erf函数做什么,只是说这个函数接收一个double型参数也返回一个double型值。这些已经足够编译器去检查我们使用的合法性并产生一个源文件,带着一个给计算机的标记:一旦你看到这个标记,你可以找到erf函数,并将这个标记用erf的返回值替代。
而连接器的任务是通过确实找到erf来解析那个标记,也就是在你硬盘的某个库里。
在math.h中的数学函数分散在他们自己的库中,你需要通过一个-lm编译选项告诉连接器。这里,-l是一个用来指示库需要连接的选项,而本例中的库有一个用单个字母表示的名字:m。你不用设定任何选项就可以使用printf,因为在连接命令行的末尾,有一个隐含的-lc选项来要求连接器连接标准libc库。随后,我们将看到GLib 2.0通过-lglib-2.0被连接进来,GNU科学计算库也通过-lgsl连接进来,依此类推。
所以,如果文件名为erf.c,那么完整的gcc编译器命令行,应该如下所示(这里包括几个选项,在后面将会详细介绍):
这样就能告诉编译器通过程序中的#include包含数学函数,并告诉连接器通过命令行中的-lm连接数学库。
-o选项用来给出输出文件的名字;否则我们将得到一个默认的可执行文件名a.out。
1.3.1 一些我喜欢的选项
在后面你将看到我几乎每次都用到一些编译器选项,并且我也建议你使用它们。
-g,表示加入调试符号。没有这个选项,调试器不会给你变量或者函数的名字。这些调试信息并不会把程序拖慢,而且我们也不在乎程序增加1K字节,所以看起来没啥理由不去用它。该选项对gcc、Clang和icc(Intel的编译器)都有效。
-std=gnu11,这是gcc特有的选项,用来提示gcc应该允许符合C11和POSIX标准的代码使用。否则,gcc可能将一些目前有效的语法判为非法。与之类似的,一些系统还是在C11之前发布的,用-std=gnu99。这是gcc独有的;而其他的编译器都从很久以前就将C99当作默认配置。POSIX标准规定你的系统中必须有C99,因此以上这一命令行的与编译器无关的版本应该是:
在后文将介绍的makefile中,我通过设定一个变量CC=c99实现了这个效果。
在Mac系统中,c99是一个特别修改的gcc版本,可能并不是你想要的。如果你有一个不是很理想的c99版本,或者它整个就被忽略了,那就自己建立一个。把一个叫做c99的文件放在你的搜索路径中的目录里:
或者如果你愿意,就是
并通过chmod +x c99让它成为可执行的文件。
-O3显示出这里的优化等级是三级,也就是尝试已知的所有方式去建立更快的代码。如果你运行调试器,你会发现太多的变量被优化掉了以至于你都没办法跟踪执行情况,那么换成-O0。这样就只提供随后的CFLAGS变量里包含的常用方法。该选项对gcc、Clang和icc都有效。
-Wall添加编译器警告。该选项对gcc、Clang和icc都有效。对于icc用户,你可能更喜欢用-w1,即在显示编译器警告的同时并不显示备注。
要坚持使用编译器警告。你可能已经熟知C语言标准,但是你不可能比你的编译器更挑剔和更了解C。旧的C教材连篇累牍地警告你注意=和==的差别,或者检查是否所有的变量在使用前都被初始化了。作为一本更加现代的书的作者,我却将以平常心来对待,因为我可以把所有的警告总结为一点:永远都要用你的编译器警告。
如果编译器建议你做一个改变,不要怀疑或试图碰运气而放弃修改。尽可能去:(1)理解为什么你得到警告,(2)修改代码直到不产生任何警告和错误。编译器信息是出了名的难懂的,所以如果你在第(1)步有困难,把警告信息贴在搜索引擎上,就能看到有多少人在你之前也面对了类似的问题。你可能想加上-Werror编译选项,这样你的编译器将把警告当作错误来处理。
1.3.2 路径
在笔者的硬盘中有超过700 000个文件,声明sqrt和erf函数的头文件只是其中之一,而还有一个是包含了这些函数对应的被编译后的目标文件(你可以在任何POSIX标准系统中试一下find /-type f | wc –l来得到一个粗略的文件数)。编译器需要知道在哪个目录中去查找并找到正确的头文件和目标文件,当我们开始使用非标准C库的时候,这个问题就会变得更加严重。
在一个典型的安装中,库可能存放的地方至少有三个。
操作系统的厂家可能预定义了一两个厂家自己用来安装库文件的目录。
可能存在为本地系统管理员准备的用于安装包的目录,并且不能被来自厂家的下一次操作系统更新覆盖。系统管理员可以有一个特殊的破解版本的库可以用来覆盖系统缺省的版本。
用户一般来说没有向这些路径写操作的权限,但是有从他们的主目录利用那些库的权限。
操作系统标准的路径一般不会引发什么问题,编译器也应该知道如何查找那些路径,并找到标准C库以及伴随其安装的任何文件。POSIX标准用“通常位置”来指代上面所说的目录。
但是对于其他的东西,你必须告诉编译器如何查找。这真是拜占庭风格:没有一个标准的方法去找到不按标准位置安装的库,这一点是人们对C比较恼火的地方。另一方面,编译器知道如何在通常的位置查找,而库的提供者倾向于将库安放在通常位置,所以你可能从来没有真正手工去配置地址。再者,也有几种方法使你可以指定路径。最后,一旦你把非标准库安装在系统上,你可以在一个shell脚本或makefiles中配置好然后再也不用去想这个。
假设你在计算机上安装了一个叫做Libuseful的库,并且你知道与之相关的文件放在/usr/local/目录,也就是你的系统管理员的官方主目录。你已经把#include 放在了你的代码里;现在你必须把下一行放在你的命令行中:
-I添加指定的路径到搜索范围内,也就是编译器用来搜索你放在代码里面的#included的头文件的。
-L添加指定的路径到库的搜索范围内。
注意顺序问题。如果你有一个叫做specific.o的文件依赖于Libbroad库,而Libbroad库依赖于Libgeneral,那么你应该输入:
任何其他顺序,比如gcc –lbroad –lgeneral specific.o,都可能失败。你可能认为连接器首先查看第一个目标——specific.o,将无法解析的函数、结构和变量名记入一个列表。然后连接器查看下一个目标——lbroad,并在这个目标内搜索列表中仍然缺失的项目,同时有可能在列表中添加新的项目;所有再在-lgeneral查找仍然缺失的项目。如果直到搜索完最后的目标仍然存在未解析的符号(包括在最后的隐含的-lc),连接器将终止运行并向用户给出最后剩下的未解析项目。
现在回到位置路径问题:要连接的库到底在哪里呢?如果它是用你安装操作系统其他部分同样的包管理器安装的,那么最可能在通常路径,你也不用去担心这个。
你可能想不清你自己的本地库应该放在何处,比如/usr/local还是/sw或者/opt。你无疑可以用硬盘搜索的方式来查找,比如在你的机器或POSIX环境中使用
来搜索/usr中以libuseful开头的文件。当你发现Libuseful库的共享目标文件在/some/path/lib,那么几乎可以肯定对应的头文件一定在/some/path/include。
在硬盘里到处找库文件是一件很恼人的事情,于是pkg-config通过将每个包自己报告的用于编译的配置和位置信息存在一个知识库中,解决了这个问题。在命令行中输入pkg-config;如果你得到一个错误提示说“没有指定包名字”,那么很好,说明你有pkg-config并且可以用它来做研究。例如,在我个人计算机的命令行上输入这两行命令:
得到下面两行输出:
这些就是我用来编译GSL和LibXML2所需要的所有选项。-l选项揭示出GNU科学计算库依赖于基本线性数学子程序库(BLAS),而GSL的BLAS库依赖于标准数学库。看起来所有这些库都在通常路径,因为这里没有-L选项,但是-I选项表明LibXML2的头文件的特殊位置。
回到命令行,shell提供了一个方法,就是当你把一个命令行用反引号包围时,这个命令行会被其自身的输出替代。就是说,输入:
编译器看到的是:
所以pkg-config会为我们做很多工作,但是这并不是标准配置的:我们期望所有人都拥有它,或者每个库都用它注册了。如果你没有pkg-config,你就必须自己研究,比如读这个库的手册,或者像前面那样搜索。
有很多与路径有关的环境变量,比如CPATH、LIBRARY_PATH或者C_INCLUDE_PATH。你可以在.bashrc或别的用户定义的环境变量列表中设定它们。它们都不是标准的——连Linux和Mac中的gcc都分别使用不同的变量,别的编译器自然也使用自己的变量。我发现在每个项目的makefile或类似机制的基础上,用-I和-L来设定这些路径相对更容易。如果你喜欢这些路径变量,可以在你的编译器的帮助文件的末尾查找符合你的情况的相关变量。
即便用pkg-config,我们也显然需要某种工具帮我们把所有这些自动执行。每个元素都很容易理解,但是这可是一个冗长机械的琐碎工作。
1.3.3 运行时连接
编译器连接静态库的时候,是将库里的相关内容直接复制到最终的可执行文件中的。所以程序本身或多或少是一个独立的系统。而共享库与你的程序是在运行时连接的,就是说我们在运行时会遇到像编译器在编译时寻找库的路径那样的问题。甚至更糟的是,你的程序的用户也存在同样的问题。如果你的库在一个非标准的路径,那你需要找到一个修改运行时搜索库的路径的方法。有以下选择。
如果你用Autotools打包你的程序,Libtools知道如何添加合适的配置,也就是说你不用再操心什么。
需要更改搜索路径的最可能的原因,是由于你没有Root权限,所以你把库文件都放在你的个人目录下。如果你把所有的库都安装在libpath中,那么需要设定环境变量LD_LIBRARY_PATH。一般是在shell(.bashrc、.zshrc,或者别的什么对应物等)启动脚本中来做这个工作,可使用命令:
有些人反对过度使用LD_LIBRARY_PATH(万一有人把恶意伪装的库放到那个路径,在你没有察觉的情况下代替了真正的库怎么办?),但是如果你所有的库都在一个地方,在你虚拟的控制下添加一个目录到路径中就不是不合理的了。
当用gcc、Clang或者icc来基于一个在libpath中的库编译程序的时候,加入
到相应的makefile中。-L选项告诉编译器到哪里去找到库以解析符号;-Wl选项从gcc/Clang/icc传递选项到连接器,而连接器将给定的-R嵌入所连接的库的运行时搜索路径。不幸的是,pkg-config经常不知道运行时路径,所以你可能必须有手工输入这些信息。