使用shared library动态链接库

以前在Windows里面写C程序,总是用各种IDE,编译、链接之类的东西全不用操心,写好代码就好了。换到linux环境有几年了,写C程序不再用IDE,而是自己用gcc/g++编译,不过之前的程序都不大,也没真正用到gcc/g++的各个选项。现在在学习cuda c,有了一些可以公用的工具代码,想要封装成库文件以共享,这才学会了使用shared library动态链接库。在这里总结一下,其实非常容易。

首先要对C的编译过程有个基本的了解。编译的第一步是预编译,处理各种预编译指令,各种带#号的指令如#include, #define都是预编译命令。#define是简单的字符串替换,而#include则是对文件的复制粘贴。在C中,将类型和函数定义与实现分开放在.c/.cpp和.h文件中,实现了一层封装,这样就可以在不改变定义的情况下改变函数或类型的实现。另一方面,将实现与定义拆分开来使得其他文件调用它们变得更容易,只需要导入函数和类型的声明,而不需要加载整个实现的文件。

注意编译时只编译.c/.cpp文件,.h文件不用编译,这些文件仅仅是用来做文本上的复制粘贴的。每一个c/cpp文件的编译过程是将c代码转变成汇编代码或者机器码的过程,编译的结果是生成object文件,在linux中后缀一般为.o。编译的过程中可能用到其他没有在这个文件中定义的函数或者类型,只要文件中包含了函数或者类型的声明,编译还是能够正常进行。.h文件的意义这时候就体现出来了。

接下来的链接这个步骤就是来解决文件间的互相依赖问题的。这个过程大概就是把一部分object文件中缺失的符号定义在另一部分object文件中找到,再通过某种方式把不同的部分拼接起来。链接的结果是生成可执行文件,或者是库文件。这些文件可以直接运行或者被调用,因为所有的相互依赖问题都已经得到了解决。可执行文件都有一个main函数作为执行入口,库文件不需要main函数,只被别的代码调用。链接有两种类型,一种是静态链接,直接将所用到的各个部分拼接起来作为所得到的最终文件。另一种是动态链接,不是直接的进行拼接,最终得到的文件仍然不是完整的程序,而是保留了接口。动态链接所得到的可执行文件在执行的时候按照需要动态的加载用到的部分,与此不同的静态链接没有这个动态加载的过程,因为所有需要的部分已经包含在可执行文件里面了。这里可以看到动态链接生成的文件往往会小一些,因为可重用的部分已经独立出来作为库文件了,这也是构建大型软件所必须的。Windows中的.dll文件,linux中的.so文件都是动态链接库文件,可以被其他程序在运行时动态的加载进来。

现在介绍一些gcc/g++的基本使用和一些选项。对于简单的小程序,直接gcc/g++加上所有的c/cpp文件不需要任何选项就可以得到可执行文件了,默认生成的文件叫做a.out。选项-o可以用来指定生成的文件名。对于复杂一些的程序,一般要向上面介绍的那样分步进行编译和链接。编译的选项是-c,有了这个选项之后就只进行编译,并生成object文件,如前所述,这些文件中可以含有没有定义的部分。不加-c选项,gcc/g++就会自动进行这个步骤。注意当编译含有没有定义的函数或者类型的文件时-c这个选项是必须的。

这里给一个简单的例子,有三个文件,foo.h和foo.c定义实现了一个简单的函数,然后test.c调用这个函数。

// foo.h
#ifndef __FOO_H__
#define __FOO_H__

void print_msg();

#endif
// foo.c
#include <stdio.h>
#include "foo.h"

void print_msg()
{
    printf("Hello
");   
}
// test.c
#include "foo.h"

int main()
{
    print_msg();
    return 0;
}

 现在假定这三个文件都在同一个文件夹下。首先,编译foo.c,

gcc -c foo.c -o foo.o

注意编译的命令中不需要.h文件,这些.h文件会在预编译时自动粘贴进来。这一步完成之后生成object文件foo.o。其内容是一个print_msg()函数。

然后编译链接test,

gcc test.c foo.o -o test

这一步会编译test.c文件,然后将foo.o链接进来,生成最终的可执行文件test。这一步也可以拆成两步,先用-c选项编译test.c,然后再链接生成test。这一步种的链接过程属于静态链接,因为foo.o的内容会被放到最终生成的test可执行文件中。

转换成使用动态链接,编译foo.c的时候需要用到-shared和-fPIC两个额外的选项。-shared选项指定编译器生成shared library,-fPIC选项中PIC是Position-Independent Code的缩写,意思是生成的机器代码不依赖于某个绝对内存地址。因为shared library可能被多个程序使用,若是用绝对地址的话不同的程序使用过程中很可能会产生冲突,所以使用-fPIC选项,使得生成的代码适合作为shared library使用。gcc/g++有不少选项是-f开头的,这些选项常常是一些小功能的开关。编译foo.c的命令如下:

gcc -shared -fPIC foo.c -o libfoo.so

注意生成的文件后缀一般为so,而且shared library一般都以lib开头。

编译链接test的时候,需要用到libfoo.so,只需要把foo.o换成libfoo.so,

gcc test.c libfoo.so -o test

这时生成的test就是依赖于shared library libfoo.so的了。运行test,可以正常执行。但若把libfoo.so移到另一个文件夹,test就无法正常执行了,因为找不到libfoo这个库。与此不同的是,对于静态链接的程序,无论把foo.o移到哪里,编译好的test文件都可以正常执行。

现在考虑更通用的情况,test.c与foo.c和foo.h分别放在两个不同的路径中,设test.c的路径为/u/dev/test/test.c,foo.c(h)的路径为/u/dev/foo/foo.c(h)。通常库和调用库的程序是不同时写的,放在不同的地方也是最通常的情况。首先到/u/dev/foo/里面去编译libfoo.so,所用的命令同上面的一样。然后回到/u/dev/test/,来编译test。首先第一个问题就是到哪里去找foo.h?这需要用到-I选项,它指定了include文件的路径。于是我们可以用这个命令

gcc -I/u/dev/foo/ test.c /u/dev/foo/libfoo.so -o test

往往库文件会集中放在一个路径中,要指定所有库文件的完整路径非常费事,gcc提供了一个-L选项,用来指定库文件的路径,结合-l(小写L)来使用,指定用哪一些库文件。用在这个例子上,完整的命令就变为

gcc -I/u/dev/foo -L/u/dev/foo -lfoo main.c -o test

-l选项后跟的是库文件的名字,注意gcc自动把文件名的lib前缀去掉了,所以-lfoo实际上用的是libfoo.so,如果文件名是foo.so则反而找不到。若/u/dev/foo中有多个库文件需要用,那么使用-lfoo1 -lfoo2就比用完整路径简单多了。据说在某些新版本的gcc中,-l选项要放到-o选项的后面,按照依赖关系来排,不过我还暂时没有遇到这个问题。

接下来运行test文件时仍然不能正常运行,这是因为找不到foo库文件的缘故。一个(可能不太好但可以用的)解决的办法是使用LD_LIBRARY_PATH环境变量,这个环境变量指定了shared library的路径。在运行test之前,更新这个环境变量

export LD_LIBRARY_PATH=/u/dev/foo:$LD_LIBRARY_PATH

然后再执行test,就没有问题了。

在通常的开发过程中,编译好的.so文件一般放在名为lib的文件夹中,.h文件一般放在include文件夹中,linux系统本身也如此。在使用这些库的时候,编译时用-I, -L和-l选项即可。将上述环境变量的更新放在.bashrc中,便可以省去每次运行前都指定库文件目录的麻烦了。

原文地址:https://www.cnblogs.com/alexdeblog/p/3240343.html