如何在Linux下运用GCC来生成共享连接库
本文摘自:
http://www.cprogramming.com/tutorial/shared-libraries-linux-gcc.html#fn:1
这几天,好久没有看看GCC了,偶尔在Cprogramming.com这个网站上看到了这篇英文的文章,觉得还是不错的,于是乎,决定把它翻译一下,以便自己更好的理解这篇文章的精神,同时也为GCC做一点贡献。(哈哈!大言不惭啊!)
为了和大家共同进步,我把英文的原文也摘下来,以便大家对照着原文阅读,(毕竟英文的才是原汁原味的,并且翻译的也不是很标准。)同时也感谢英文原著anduril462先生(假设是为先生,毕竟在计算机行业里,男的比例大一些~~O(∩_∩)O哈哈~)!由于水平有限,不足之处,恳请各位业界网友的读者提出批评和建议。
Libraries are an indispensable tool for any programmer. They are pre-existing code that is compiled and ready for you to use. They often provide generic functionality, likelinked lists orbinary trees that can hold any data, or specific functionality like an interface to adatabase server such asMySQL
在编程语言里,库(Libraries)是任何一个程序员不可或缺的工具。它们是预先存在一段代码(预编译代码),通过编译(预编译)之后,为程序员所用。共享链接库往往提供一些通用的函数和功能,像链表和二叉树一样,能够保存任何数据,或者是一种特定的功能:比如MySQL数据库服务器的接口等。
Most larger software projects will contain several components, some of which you may find use for later on in some other project, or that you just want to separate out for organizational purposes. When you have a reusable or logically distinct set of functions, it is helpful to build a library from it so that you don’t have to copy the source code into your current project and recompile it all the time–and so you can keep different modules of your program disjoint and change one without affecting others. Once it’s been written and tested, you can safely reuse it over and over again, saving the time and hassle of building it into your project every time.
大多数大型软件项目通常将包含几个组成部分,在你使用后发现,其中的某些部分可以运用于其他的大型项目当中,或者你想把它单独取出它,以便更好的组织。当你有一套可重复使用或者是逻辑上不同的一系列函数(功能)时,那么把它编译成一个库会带给我们很多便利,那样你就可以省去很多步骤:拷贝你的源代码到当前的项目中去,重新编译;并且你也可以保持你的程序当中的各个模块的相对独立性(降低耦合度),不至于改变一处而影响其他模块。一旦它被编写和测试,你就可以放心的重复使用这个库,这样你就可以节省很多时间,不至于每次都要进行构建。
Building static libraries is fairly simple, and since we rarely get questions on them, I won’t cover them. I’ll stick with shared libraries, which seem to be more confusing for most people
构建静态链接库(static libraries)相当简单,只要我们对此没有疑问,我不会重复提及,我会提倡使用共享链接库(shared libraries),这似乎这对大多数人来说带来了更多的迷惑。
Before we get started, it might help to get a quick rundown of everything that happens from source code to running program
在我们开始之前,在运行程序之前,我们先来了解一下C编译器是如何把C原文件编译成一个可执行文件的。
- C Preprocessor: This stage processes all the preprocessor directives. Basically, any line that starts with a #, such as #define and #include.
C预处理:此阶段处理所有的预处理指令。基本上,任何以“#”号开始的C语句,例如#define和#include。
- Compilation Proper: Once the source file has been preprocessed, the result is then compiled. Since many people refer to theentire build process as compilation, this stage is often referred to as “compilation proper.” This stage turns a .c file into an .o (object) file.
纯粹编译:源代码预处理一旦完成,接下来的工作就是编译,建造过程,很多程序员更倾向于使用“全局构建过程”作为编译。这个阶段通常只纯粹的编译,就是把C文件生成目标文件。
(通常所谓的编译是指编译工具链所做的从源代码映射到目标代码的一系列工作,包括预处理、编译、汇编、链接等等。compilation proper是指这些工作中“纯粹”的编译的过程,就是多数编译原理书中叙述的那些工作。)
- Linking: Here is where all of the object files and any libraries are linked together to make your final program. Note that for static libraries, the actual library is placed in your final program, while for shared libraries, only a reference to the library is placed inside. Now you have a complete program that is ready to run. You launch it from the shell, and the program is handed off to the loader.
链接:这个阶段是所有的目标文件和库共同链接来生成最终的可执行程序。提醒:对于静态库,链接器会将其库文件拷贝到应用程序当中,而对于共享库,在程序链接时候并不像静态库那样在拷贝使用函数的代码,而只是作些标记。现在你有一个完整的程序将要运行。启动shell,接下来就交给了加载器了。
- Loading: This stage happens when your program starts up. Your program is scanned for references to shared libraries. Any references found are resolved and the libraries are mapped into your program.
执行:程序会在这个阶段执行行。你编写的程序会进行扫描共享库的引用。解决所有引用并把库映射到你的程序当中。
Steps 3 and 4 are where the magic (and confusion) happens with shared libraries.
Now, on to our (very simple) example.
步骤3和4就是共享链接库(shared libraries)发生奇迹的时刻。
现在,再来看我们非常简单的例子:
foo.h: | foo.c: | main.c:
|
#ifndef foo_h__ #define foo_h__
extern void foo(void);
#endif // foo_h__ | #include <stdio.h>
void foo(void) { puts("Hello, I'm a shared library"); } | #include <stdio.h> #include "foo.h"
int main(void) { puts("This is a shared library test..."); foo();
return 0; } |
foo.h defines the interface to our library, a single function, foo(). foo.c contains the implementation of that function, and main.c is a driver program that uses our library.
For the purposes of this example, everything will happen in /home/username/foo
foo.h文件声明了我们所要用到库的接口,一个简单的函数——foo()。foo.c定义和描述了这个函数的实现,在主函数main.c里加载我们的foo.h库文件来执行C文件。
在这个例子当中,所有的过程均在/home/username/foo目录下完成。
Step 1: Compiling with Position Independent Code
We need to compile our library source code into position-independent code (PIC:What is position independent code? PIC is code that works no matter where in memory it is placed. Because several different programs can all use one instance of your shared library, the library cannot store things at fixed addresses, since the location of that library in memory will vary from program to program):
$ gcc -c -Wall -Werror -fpic foo.c
步骤1:编译与位置地址无关的代码
我们需要把我们的库文件编译到与位置地址无关的代码当中去。(何为与位置地址无关的代码?PIC是这个代码执行的平台,他的特征是代码执行时,与内存地址无关。因为几个不同的方案都可以使用你的共享库中的一个实例,库文件不能把代码存放在固定地址,因为该库在内存中的位置会随着程序的不同而有所不同)
gcc -c -Wall -Werror -fpic foo.c
Step 2: Creating a shared library from an object file
Now we need to actually turn this object file into a shared library. We’ll call it libfoo.so:
gcc -shared -o libfoo.so foo.o
步骤2:根据目标文件创建一个共享链接库
gcc -shared -o libfoo.so foo.o
这个命令创建了一个名为libfoo.so的共享链接库
Step 3: Linking with a shared library
As you can see, that was actually pretty easy. We have a shared library. Let’s compile our main.c and link it with libfoo. We’ll call our final program “test.” Note that the -lfoo option is not looking for foo.o, but libfoo.so. GCC assumes that all libraries start with ‘lib’ and end with .so or .a (.so is for shared object or shared libraries, and .a is for archive, or statically linked libraries).
$ gcc -Wall -o test main.c -lfoo
/usr/bin/ld: cannot find -lfoo
collect2: ld returned 1 exit status
步骤3:链接共享库
如你所见,这的确很容易。我们有了一个共享链接库。接着我们来编译main.c并用libfoo库来连接。我们把最后的可执行文件命名为test。提醒:-lfoo选项告诉编译器寻找步骤2生成的libfoo.so,而不是foo.o来链接。GCC在默认情况下在所有的库名字前加‘lib’,在其末尾加‘.so’或者‘.a’( .so表示共享目标或者共享库,.a 表示资料库或者静态库)。
Telling GCC where to find the shared library
Uh-oh! The linker doesn’t know where to find libfoo. GCC has a list of places it looks by default, but our directory is not in that list(GCC first searches for libraries in /usr/local/lib, then in /usr/lib. Following that, it searches for libraries in the directories specified by the -L parameter, in the order specified on the command line).2 We need to tell GCC where to find libfoo.so. We will do that with the -L option. In this example, we will use the current directory, /home/username/foo:
$ gcc -L/home/username/foo -Wall -o test main.c -lfoo
告诉GCC共享连接库的路径
看看~真是不幸!上边的运行结果显示:链接器不知道libfoo在哪里。(GCC会根据默认的库路径去搜索,首先会搜索/usr/local/lib,其次/usr/lib。在命令行当中,-L选项告诉GCC回去某个指定的路径去搜索库。)我们需要告诉GCC去哪个文件里搜索libfoo.so这个共享链接库。在这里例子中,libfoo.so在当前目录,/home/username/foo
gcc -L/home/username/foo -Wall -o test main.c -lfoo
Step 4: Making the library available at runtime
Good, no errors. Now let’s run our program:
$ ./test
./test: error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory
步骤4:步骤3运行的结果没有错,这当然是个好消息!那么现在我们来运行这个程序:
./test
很遗憾~有错误,在加载运行期间查找不到libfoo.so。
Oh no! The loader can’t find the shared library.3 We didn’t install it in a standard location, so we need to give the loader a little help. We have a couple of options: we can use the environment variable LD_LIBRARY_PATH for this, or rpath. Let’s take a look first at LD_LIBRARY_PATH。
很遗憾,加载器找不到共享链接库,我们把它安装在标准路径,所以我们需要给加载器一点提示,这里有一些选项可供使用:可以用环境变量LD_LIBRARY_PATH,或者rpath,首先我们来看看LD_LIBRARY_PATH的运用。
3.The default GNU loader, ld.so, looks for libraries in the following order:
1. It looks in the DT_RPATH section of the executable, unless there is a DT_RUNPATH section.
2. It looks in LD_LIBRARY_PATH. This is skipped if the executable is setuid/setgid for security reasons.
3. It looks in the DT_RUNPATH section of the executable unless the setuid/setgid bits are set (for security reasons).
4. It looks in the cache file /etc/ld/so/cache (disabled with the ‘-z nodeflib’ linker option).
5. It looks in the default directories /lib then /usr/lib (disabled with the ‘-z nodeflib’ linker option
(这段有点拗口,同时水平不够,我就不翻译了。)
Using LD_LIBRARY_PATH
$ echo $LD_LIBRARY_PATH
There’s nothing in there. Let’s fix that by prepending our working directory to the existing LD_LIBRARY_PATH:
$ LD_LIBRARY_PATH=/home/username/foo:$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
运行$ echo $LD_LIBRARY_PATH后,没有任何反应,我们需要修改LD_LIBRARY_PATH的值:
$ LD_LIBRARY_PATH=/home/username/foo:$LD_LIBRARY_PATH
并运行:
./test
./test: error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory
还是有错误。
What happened? Our directory is in LD_LIBRARY_PATH, but we didn’t export it. In Linux, if you don’t export the changes to an environment variable, they won’t be inherited by the child processes. The loader and our test program didn’t inherit the changes we made. Thankfully, the fix is easy:
$ export LD_LIBRARY_PATH=/home/username/foo:$LD_LIBRARY_PATH
$ ./test
This is a shared library test...
Hello, I'm a shared library
怎么回事呢?我们设置的路径的确是LD_LIBRARY_PATH,但是我们没有把它导出来。在Linux下,如果环境变量不知道我们所改变的信息,那么子进程则不会得知所改变的信息,我们的加载器和程序自然而然就不会随着我们的路劲改变而改变的,幸运的是,这个问题很容易解决。办法如下:
$ export LD_LIBRARY_PATH=/home/username/foo:$LD_LIBRARY_PATH
$ ./test
This is a shared library test...
Hello, I'm a shared library
Good, it worked! LD_LIBRARY_PATH is great for quick tests and for systems on which you don’t have admin privileges. As a downside, however, exporting the LD_LIBRARY_PATH variable means it may cause problems with other programs you run that also rely on LD_LIBRARY_PATH if you don’t reset it to its previous state when you’re done.
不错,一切正常!假如你没有系统管理员权限,但又希望快速测试程序,LD_LIBRARY_PATH是个好的方法。当然也有其缺点,导出LD_LIBRARY_PATH变量意味着如果你试图用同样的LD_LIBRARY_PATH变量来运行其他程序,你就必须重新设置你先前的状态。
Using rpath
Now let’s try rpath (first we’ll clear LD_LIBRARY_PATH to ensure it’s rpath that’s finding our library). Rpath, or the run path, is a way of embedding the location of shared libraries in the executable itself, instead of relying on default locations or environment variables. We do this during the linking stage. Notice the lengthy “-Wl,-rpath=/home/username/foo” option. The -Wl portion sends comma-separated options to the linker, so we tell it to send the -rpath option to the linker with our working directory.
运用rpath选项
现在我们来说说rpath(首先,我们得先清除LD_LIBRARY_PATH变量,以确保rpath能够找到我们所指定的库),Rpath就运行路径,它是把共享链接库地址嵌入到可指定文件本身去的,而不是依赖默认的路径或者环境变量的一种方法,这一步骤在链接的过程当中来完成,请注意这个冗长的选项:“-Wl,-rpath=/home/username/foo”,-Wl选项向链接器发送“,”逗号分隔符,所以我们告诉链接器让他发送-rpath选项去链接我们工作目录。
$ unset LD_LIBRARY_PATH
$ gcc -L/home/username/foo -Wl,-rpath=/home/username/foo -Wall -o test main.c -lfoo
$ ./test
This is a shared library test...
Hello, I'm a shared library
以上就是编译运行的结果。
Excellent, it worked. The rpath method is great because each program gets to list its shared library locations independently, so there are no issues with different programs looking in the wrong paths like there were for LD_LIBRARY_PATH.
漂亮!运行完全没有问题。这种添加rpath选项的方法很不错,正因为每一个程序都能够各自找到共享库的位置,这就没有像LD_LIBRARY_PATH这样,不同的程序运行时找不到正确的路径。
rpath vs. LD_LIBRARY_PATH
There are a few downsides to rpath, however. First, it requires that shared libraries be installed in a fixed location so that all users of your program will have access to those libraries in those locations. That means less flexibility in system configuration. Second, if that library refers to a NFS mount or other network drive, you may experience undesirable delays–or worse–on program startup.
rpath vs. LD_LIBRARY_PATH
然而,使用rpath选项也有其不足的地方,第一:他要求共享库必须安装在一个固定的位置以至于任何人使用你所设计的程序时都能访问这些共享库,这就会降低系统配置的灵活性。第二:如果该库指向NFS挂载或其他网络驱动器,那么在程序启动的时候,可能会遇到一些不必要的延时或者错误。
Using ldconfig to modify ld.so
What if we want to install our library so everybody on the system can use it? For that, you will need admin privileges. You will need this for two reasons: first, to put the library in a standard location, probably /usr/lib or /usr/local/lib, which normal users don’t have write access to. Second, you will need to modify the ld.so config file and cache. As root, do the following:
用ldconfig选项来修复ld.so文件
要是所有用户都能够使用我们的共享库,那又怎样呢?那么就需要管理员权限了。并且有以下两个原因,第一ldconfig用:把共享库保存在标准的文件夹位置,很可能是
/usr/lib 或者是 /usr/local/lib,然而这两个目录不是所有用户都能访问的。第二:
用户还需要以root用户来修改ls.so这个配置文件和缓存。方法如下:
$ cp /home/username/foo/libfoo.so /usr/lib
$ chmod 0755 /usr/lib/libfoo.so
Now the file is in a standard location, with correct permissions, readable by everybody. We need to tell the loader it’s available for use, so let’s update the cache:
现在,libfoo.so这个文件已在系统默认的文件地址,有正确的权限,并且每一个用户都可以访问。我们还需要告诉加载器这个文件的可用性,接下来我们就可以用如下的方法来更新我们的缓存:
$ ldconfig
That should create a link to our shared library and update the cache so it’s available for immediate use. Let’s double check:
这是就会创建一个链接文件,链接到我们的共享库和更新的缓存,并且可以立刻被使用。我们再次确认一下这个链接:
$ ldconfig -p | grep foo
libfoo.so (libc6) => /usr/lib/libfoo.so
果然有的!
Now our library is installed. Before we test it, we have to clean up a few things:
Clear our LD_LIBRARY_PATH once more, just in case:
此刻我们的共享库已经建立好了,在我们测试之前,我们的清理一些不必要的东西:再一次清除我们的LD_LIBRARY_PATH,就如下面所示:
$ unset LD_LIBRARY_PATH
Re-link our executable. Notice we don’t need the -L option since our library is stored in a default location and we aren’t using the rpath option:
再一次链接我们的可执行文件。提示:既然我们的共享库已存放在一个默认的路径,我们就不必使用-L选项,也不用rpath选项,编译方式如下:
$ gcc -Wall -o test main.c –lfoo
Let’s make sure we’re using the /usr/lib instance of our library using ldd:
通过ldd再来确认一下我们的实例当中是否用到了/usr/lib
$ ldd test | grep foo
libfoo.so => /usr/lib/libfoo.so (0x00a42000)
Good, now let’s run it:
好了,运行之:
$ ./test
This is a shared library test...
Hello, I'm a shared library
All is well!!O(∩_∩)O哈哈~
That about wraps it up. We’ve covered how to build a shared library, how to link with it, and how to resolve the most common loader issues with shared libraries–as well as the positives and negatives of different approaches.
现在我们来总结一下:在这篇文章当中,我们讲述了如何建立一个共享库,如何链接,并用不同的方法去解决常见加载问题。