深入理解C/C++编译与链接技术4:动态库A1:基本讨论之-fPIC

前言

这段时间比较累,经常忙着一大堆事情而且准备上班了,这里笔者就这几天终于可以小小的休息一下,继续更新这个系列的相关博客。

这一篇主要讨论的是动态库的基础内容,特别的,会讨论一下如何制作动态库(重点是Linux,Windows上基本因为MSVC工具链在命令行下使用有点折磨人,而且有大量成熟的构建系统已经覆盖了基本的细节,这里就不拿Windows如何构建动态库详细说明了),以及一些符号修饰签名下的问题。

如何在Linux下创建动态库

创建动态库并不麻烦,但是,基本上需要保证着一些步骤:

  • 所集成的二进制可重定位文件,要求采用位置无关标识进行编译(-fPIC,也就是flags Position Independent Code)
  • 集成这些PIC二进制可重定位文件,然后传递标识符-shared

关于-fPIC我们聊一聊

这个选项很有趣,当然,-shared选项是没啥可说的,只是纯粹的告诉我们的编译器链接动态库而已。但是,为什么这些可重定位文件要求使用位置无关代码进行编译呢?

在《高级C/C++编译技术》中,抛出了层层递进的三个问题:

  • 什么是 -fPIC
  • 创建动态库(.so)必须使用 -fPIC 吗?
  • 只有编译动态库时才使用 -fPIC 吗?

下面,笔者整理一下此书的说辞,结合一点笔者的看法,陈列在下面。

什么是-fPIC

-fPIC 的含义是**Position-Independent Code**(生成位置无关代码),换而言之, 编译出来的机器指令 不依赖固定的加载地址,在运行时可以被加载到任意内存位置而无需修改代码本身。这很符合我们对动态库功能的感知。我们最后,总是要将一个动态库的符号导出出来给其他第三方的应用程序或者是其他的库进行使用,因此,显然我们不能安排一个绝对的映射地址给这些动态库符号,而是在复用的时候,动态的给予一个偏移地址映射到使用者的进程地址上,这样我们才能实现符号的复用。按照步骤的说:

  • -fPIC将会对符号采用 相对地址 而不是绝对地址进行映射
  • 全局变量通过 GOT(Global Offset Table) 间接访问
  • 调用函数通过 PLT(Procedure Linkage Table) 跳转

创建动态库(.so)必须使用 -fPIC 吗?

这个很严肃的说,还真不一定。当然,如果我们说的是今天32位的PC机已经即将绝迹(原谅笔者孤陋寡闻,实在没见过物理的32位PC计算机,单片机倒是玩过一丁点)的话,那我们似乎可以对上述命题持以肯定态度。

我们思考一下,现代动态库跟共享库都是同义的,多个进程准备共享动态库的代码段。那对于不用的进程,代码被要求可以能放在任意虚拟地址是完全合情合理的。否则loader 必须在装载时对代码做 重定位修补,导致代码段无法被共享,加载速度变慢。

但是X86-64不是,还是可以不使用-fPIC编译出可用的动态库的,但是吧,共享的特性就丢掉了,而且加载速度变慢(加载的时候给所有的符号修正地址)。所以,如果我们严肃的想一想,笔者认为的结论是:

在今天,编译动态库是必须要带有-fPIC符号的,百利无一害(如果很担心轻微的性能损失当我没说,考虑的场景不一致)

-fPIC是动态库专属的嘛?能否在静态库下使用 -fPIC

显然不是,否则,没有必要让这个标志独立出来。实际上,我们完全可以对准备编译成静态库的可重定位文件一样上-fPIC,这个非常的常见。

举个例子,笔者手头有一个比较大的工程,他是对每一个子模块都生成一个静态库,然后对这个目录下所有生成的静态库打包生成一个动态库,我们在之前的文章就有讨论过,静态库就是一组可重定位文件的简单集合,所以,很自然的我们就意识到,上述情况我们必须对这个静态库所包含的可重定位文件采用-fPIC标志的编译这些源文件。