JackieMium / my_blog

在 Issues 中建立的个人博客
GNU General Public License v3.0
31 stars 12 forks source link

Linux 下的共享库 #14

Open JackieMium opened 6 years ago

JackieMium commented 6 years ago
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一样找不到。 那要怎么指定呢?两个办法:

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

使用 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(静态库)区别
  1. GCC 首先在/usr/local/lib搜索库文件,其次在/usr/lib,然后搜索-L参数指定路径,搜索顺序和-L参数给出路径的顺序一致。

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