GCC定位库

之前我们学习的是制作库文件,不管是静态库或者是共享库,根据具体的编译流程就可以制作,但是无论是什么库,使用的时候都需要链接。为了使库文件能够正确的链接,链接的时候需要能够定位库。对于静态库链接程序来说,所有的目标文件都集中在一起保存成一个独立的可执行文件,这个可执行文件完全可以移植到相互兼容的系统中并能正确运行,甚至目标文件都不存在也没有关系。但是共享库链接的时候必须存在,每次运行程序的时候需要使用。

链接时定位库

无论在什么时候,只要链接程序就需要查找库,编译器会查找指定的目录列表。这些目录是否被包含进查询路径,依赖于使用哪种竞争模式 ld(ld是链接器),编译的时候如何配置 ld,以及命令行指定的目录。大多数的系统库放置的位置是目录/lib/usr/lib中,这是系统配置好的,因此会自动查找这两个目录。通过使用一个或者多个-L选项,指定其他的查询目录。命令在执行的时候就会直接到我们指定的目录下寻找链接文件。

实例:/home/lib目录下存放着我们的库文件libtest.a(静态库),编译的时候命令如下:

 gcc -L/home/lib mian.o -o main -ltest

执行命令结束,我们就可以得到最终的目标文件。

运行时载入库

只要程序被链接想要使用共享库,就必须在运行的时候能够找到共享库的位置。因为是通过名字确定库,而不是通过目录定位库,因此可以在链接程序的时候使用一个库,而在运行的时候使用另一个库。如过我们改变库文件的版本号,而没有改变程序的版本,在运行的时候会出现问题,所以很多库会把版本号作为名字的一部分,例如:libm.so或libutil-2.2.4.so。

无论何时载入程序并打算运行的时候,共享库都应该位于以下的位置:
  1. 环境变量LD_LIBRARY_PATH列出的所有用分号分割的目录;
  2. 文件/etc/ld.so.cache中找到的库的列表,由工具ldconfig维护
  3. 目录/usr
  4. 目录/usr/lib

如果想要知道载入了那个库,以及确切的了解应用程序使用的那个库,可以使用ldd。命令格式如下:

ldd option filename

option可以表示的选项如下:
-d:进程数据重寻址
-r:进程数据和函数重寻址
-u:打印出未使用的直接依赖关系
-v:打印出所有的信息
实例:我们当前目录下存在一个可执行文件main。在命令行输入命令:

ldd -v main

显示的信息如下:

linux-vdso.so.1 (0x00007ffe6d730000)
       libadd.so => /usr/lib/libadd.so (0x00007fca6e1c5000)
       libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fca6ddd4000)
       /lib64/ld-linux-x86-64.so.2 (0x00007fca6e5c9000)
 
Version information:
./main:
       libc.so.6 (GLIBC_2.2.5) => /lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/libc.so.6:
       ld-linux-x86-64.so.2 (GLIBC_2.3) => /lib64/ld-linux-x86-64.so.2
       ld-linux-x86-64.so.2 (GLIBC_PRIVATE) => /lib64/ld-linux-x86-64.so.2

上面显示的信息,文件链接的库文件,以及库文件的版本号和库的位置。

动态加载动态库函数

共享库中的函数可被加载并执行而不需要链接到程序里。加载动态链接库,首先要为共享库分配物理内存,然后在进程对应的页表项中建立虚拟页和物理页面之间的映射。动态加载函数需要使用四个基本函数分别是:dlopen()、dlsym()、dlerror()、dlclose(),使用这些函数需要包含头文件dlfcn.h。

分别介绍一下各个函数的作用和使用方式(我们也可以通过man手册查看)。

1. 打开动态链接库文件函数为 dlopen,函数原型如下:

void *dlopen (const char *filename, int flag);

dlopen用于打开指定名字(filename)的动态链接库(最好文件绝对路径),并返回操作句柄。

flag的选项可以是以下两种:

RTLD_NOW:立即决定,返回前解除所有未决定的符号。
RTLD_LAZY:暂缓决定,等有需要时再解出符号

返回值:打开错误返回NULL,成功返回库引用。编译时要加入“-ldl”选项(指定dl库),实例:

gcc test.c -o test -ldl

2. 取函数执行地址的的函数为 dlsym,函数原型如下:

void *dlsym(void *handle, char *symbol);

dlsym 根据动态链接库操作句柄 (handle) 与符号 (symbol) ,返回符号对应的函数的执行代码地址。
 
3. 关闭动态链接库函数为dlclose,函数原型如下:

int dlclose (void *handle);

dlclose 用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为 0 时,才会真正被系统卸载。返回值为 0 表示成功,非零表示失败。
 
4. 动态库错误函数为 dlerror,函数原型如下:

const char *dlerror(void);

当动态链接库操作函数执行失败时,dlerror 可以返回出错信息,返回值为 NULL 时表示操作函数执行成功。
 
实例:只要两个函数向标准输出中显示字符,说明他们被调用了。
/*sayhello.c*/
#include <stdio.h>
 
void sayhello()
{
    printf("hello\n");
}
 
/*saysomething.c*/
#include <stdio.h>
 
void saysomething(char *str)
{
    printf("%s\n",str);
}
把上面这两个函数制作成静态库文件。使用命令:

gcc -shared -fpic sayhello.c saysomrthing.c -o libsayfn.so

 调用函数如下:
/*main.c*/
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
 
int main(int argc,char *argv[])
{
    void *handler;
    char *error;
    void (*sayhello)(void);
    void (*saysomething)(char *);
    handler = dlopen("libsayfn.so",RTLD_LAZY);
    if(error = dlerror())
    {
        printf("%s\n",error);
        exit(1);
    }
    sayhello = dlsym(handler,"sayhello");
    if(error = dlerror())
    {
        printf("%s\n",error);
        exit(1);
    }
    saysomething = dlsym(handler,"saysomething");
    if(error = dlerror())
    {
        printf("%s\n",error);
        exit(1);
    }
    sayhello();
    saysomething("this is somethng");
 
    dlclose(handler);
    return 0;
}
最后编译main.c文件,使用下面的命令:

gcc main.c -ldl -o main

编译的时候需要注意,需要添加链接选项-ldl,还有就是在执行程序的时候需要把动态库文件放到指定的位置,否则就会出现动态库找不到的错误信息。
运行程序就可以看到如下的信息:

hello
This is something

这就说明我们这里例子没有问题。