my_blog icon indicating copy to clipboard operation
my_blog copied to clipboard

Linux 下的共享库

Open JackieMium opened this issue 6 years ago • 0 comments

2018-02-28

linux 2014 dark

通过上次那个 Rtudio 输入法的事情 #12 ,我越来越觉得编译啊共享库啊什么的很有趣,然后我懂的太少。所以补课看了一些东西,这一篇我觉得很基础,也很有启发性。把这篇和之前的 #6 #7 #12 一起看理解下很重要。

下文原文来自博客园上的一篇博文 在 Linux 使用 GCC 编译C语言共享库,有删改。

这是一篇很基础的博文,通过一个小例子说明 Linux 下共享库的创建和使用。明白这些对于软件的编译会有很多帮助。

正式开始前,我们先看看源代码到运行程序之间发生了什么:

  1. 预处理:这个阶段处理所有预处理指令。基本上就是源代码中所有以 ‘#’ 开始的行,例如 #define#include
  2. 编译:一旦源文件预处理完毕,接下来就是编译。因为许多人提到编译时都是指整个程序构建过程,因此本步骤也称作“compilation proper”。本步骤将“.c”文件转换为“.o”文件。
  3. 连接:这一步将所有的对象文件和库文件串联起来使之成为最后的可运行程序。需要注意的是,静态库实际上已经植入到你的程序中,而共享库,只是在程序中包含了对它们的引用。现在你有了一个完整的程序,随时可以运行。当你从 shell 中启动它,它就被传递给了加载器。
  4. 加载:本步骤发生在程序启动时。首先程序需要被扫描以便引用共享库。程序中所有被发现的引用都立即生效,对应的库也被映射到程序。

第3步和第4步就是共享库的奥秘所在。

下面通过一个例子来说明这个过程。

首先我们在工作目录下有三个文件foo.hfoo.cmain.cfoo.h文件的内容为:

#ifndef foo_h__
#define foo_h__
 
extern void foo(void);
 
#endif  // foo_h__

foo.c文件的内容为:

#include <stdio.h>
 
void foo(void)
{
    puts("Hello, I'm a shared library");
}

main.c文件的内容为:

#include <stdio.h>
#include "foo.h"
 
int main(void)
{
    puts("This is a shared library test...");
    foo();
    return 0;
}

foo.h定义了一个接口连接我们的库,这个库里只有一个简单的函数,foo()foo.c包含了这个函数的实现,main.c是一个用到我们库的驱动程序。 接下来我们看看怎么在编译过程中使用共享库生成最终的可执行程序。

Step 1: 编译无约束位代码

我们需要把我们库的源文件编译成无约束位代码。无约束位代码是存储在主内存中的机器码,执行的时候与绝对地址无关。

$ gcc -c -Wall -Werror -fpic foo.c

这一步会得到对象文件foo.o

Step 2: 从一个对象文件创建共享库

现在让我们将对象文件变成共享库。我们将其命名为libfoo.so

$ gcc -shared -o libfoo.so foo.o

现在就得到了libfoo.so文件了。

Step 3: 链接共享库

现在我们得到共享库了,下一步就是编译main.c并让它链接到我们创建的这个共享库上。我们将最终的运行程序命名为test。 注意:-lfoo选项并不是搜寻foo.o,而是libfoo.so。GCC 编译器会假定所有的库都是以“lib”开头,以“.so”或“.a”结尾(“.so”是指 shared object 共享对象或者 shared libraries 共享库,“.a”是指 archive 档案,或者静态连接库)。

$ gcc -Wall -o test main.c -lfoo -lc

会出现报错:

/usr/bin/ld: cannot find -lfoo
collect2: ld returned 1 exit status

即编译器没有找到我们的共享库libfoo.so,链接器并不知道该去哪里找libfoo.so(事实上是不会去标准系统路径以外的地方去找共享库)。我们要指定 GCC 去哪找共享库。

GCC有一个默认的搜索列表,但我们的工作目录并不在那个列表中。我们需要告诉 GCC 去哪里找到libfoo.so。这就要用到-L选项。 在本例中,我们将使用当前目录.

$ gcc -Wall -o test main.c -L. -lfoo -lc

这样就能顺利编译出可执行文件test。我们执行看看:

$ ./test 
./test: error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory

报错了,出错原因还是找不到libfoo.so文件。虽然链接的时候我们通过指定路径链接成功了,但是运行时libfoo.so一样找不到。 那要怎么指定呢?两个办法:

  • 把需要的库文件(本例中的libfoo.so)移动到系统标准路径去;
  • 通过LD_LIBRARY_PATH环境变量或者rpath选项临时启用非标准路径中的库文件。

重点看看第二个方法是怎么做的。

使用 LD_LIBRARY_PATH 环境变量

先看看目前的LD_LIBRARY_PATH是什么:

$ echo $LD_LIBRARY_PATH

这个环境变量内容目前为空,即没有存储任何路径。 现在把当前工作目录添加到LD_LIBRARY_PATH中:

$ LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
$ ./test
./test: error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory

为什么还报错呢? 虽然我们的目录在LD_LIBRARY_PATH中,但是我们还没有导出它。在 Linux 中,如果你不将修改导出到一个环境变量,这些修改是不会被子进程继承的。加载器和我们的测试程序没有继承我们所做的修改。要修复这个问题很简单,export一下就行了:

$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
$ ./test
This is a shared library test...
Hello, I'm a shared library
$ unset LD_LIBRARY_PATH

这下终于可以了。

LD_LIBRARY_PATH很适合做快速测试,尤其在没有权限将需要的库放到系统标准路径或者只是想临时做测试的情况下。 另一方面,导出LD_LIBRARY_PATH变量意味着可能会造成其他依赖LD_LIBRARY_PATH的程序出现问题,因此在做完测试后最好将LD_LIBRARY_PATH恢复成之前的样子。

使用 rpath 选项

再来看看 rpath 选项的用法:

# make sure LD_LIBRARY_PATH is set to default
$ unset LD_LIBRARY_PATH
$ gcc -Wall -o test main.c -L. -Wl,-rpath=. -lfoo -lc
$ ./test
This is a shared library test...
Hello, I'm a shared library

也没问题。

rpath方法有一个优点,对于每个程序编译时我们都可以通过这个选项单独罗列它自己的共享库位置,因此不同的程序可以在指定的路径去加载需要的库文件,而不需要一次次的去指定LD_LIBRARY_PATH环境变量。

附:

  1. Shared Libraries(共享库) 和 Static Libraries(静态库)区别
  • 共享库是以“.so”(Windows 平台为“.dll”,Mac OS 平台为“.dylib”)作为后缀的文件。所有和库有关的代码都在这一个文件中,程序在运行时引用它。使用共享库的程序只会引用共享库中它要用到的那段代码。

    静态库是以“.a”(Windows平台为“.lib”)作为后缀的文件。所有和库有关的代码都在这一个文件中,静态库在编译时就被直接链接到了程序中。使用静态库的程序从静态库拷贝它要使用的代码到自身当中。

  • 两种库各有千秋。 使用共享库可以减少程序中重复代码的数量,让程序体积更小。而且让你可以用一个功能相同的对象来替换共享对象,这样可以在增加性能的同时不用重新编译那些使用到该库的程序。但是使用共享库会小额增加函数的执行的成本,同样还会增加运行时的加载成本,因为共享库中的符号需要关联到它们使用的东西上。共享库可以在运行时加载到程序中,这是二进制插件系统最通用的一种实现机制。 静态库总体上增加了程序体积,但它也意味着你无需随时随地都携带一份要用到的库的拷贝。因为代码在编译时就已经被关联在一起,因此在运行时没有额外的消耗。

  1. GCC 首先在/usr/local/lib搜索库文件,其次在/usr/lib,然后搜索-L参数指定路径,搜索顺序和-L参数给出路径的顺序一致。

  2. 默认的 GNU 加载器ld.so,按以下顺序搜索库文件:

  • 首先搜索程序中DT_RPATH区域,除非还有DT_RUNPATH区域。
  • 其次搜索LD_LIBRARY_PATH。如果程序是setuid/setgid,出于安全考虑会跳过这步。
  • 搜索DT_RUNPATH区域,除非程序是setuid/setgid
  • 搜索缓存文件/etc/ld/so/cache(停用该步可以使用-z nodeflib参数)
  • 搜索默认目录/lib,然后/usr/lib(停用该步请使用-z nodeflib参数)。

JackieMium avatar Mar 09 '18 02:03 JackieMium