动态加载库

核心dlopen API由以下函数(所有这些函数都在SUSv3进行了规定)构成。
dlopen()函数打开一个共享库,返回一个供后续调用使用的句柄。
dlsym()函数在库中搜索一个符号并返回其地址。
dlclose()函数关闭之前由dlopen()打开的库。
dlerror()函数返回一个错误消息字符串,在调用上述函数中的某个函数发生错误时可以使用这个函数来获取错误消息。

dlopen函数

dlopen函数将名libfilename的共享库加载进调用进程的虚拟地址空间并增加该库的打开引用计数。

#include <dlfcn.h>
void *dlopen(const char *libfilename, int flags);

如果filename包含了一个斜线(/),那么dlopen()会将其解释成一个绝对或相对路径名,否则动态链接器会按照相应的规则搜索共享库。
如果filename指定的共享库依赖于其它共享库,那么dlopen()会自动加载那些库。这种被加载进来的库被称为这个库的依赖树。
在同一个库文件中可以多次调用dlopen(),但dlopen API会为每个库句柄维护一个引用计数,每次调用dlopen()时都会增加引用计数,每次调用dlclose()都会减小引用计数,只有当计数为0时dlclose()才会从内存中删除这个库。

flags的取值可以为RTLD_LAZY和RTLD_NOW

RTLD_LAZY:
只有当代码被执行的时候才解析库中未定义的函数符号。延迟解析只适用于函数引用,对变量的引用会被立即解析。

RTLD_NOW:
在dlopen()结束之前立即加载库中所有的未定义符号,不管是否需要用到这些符号,这种做法的结果是打开库变得更慢了,但能够立即检测到任何潜在的未定义函数符号错误,而不是在后面某个时刻使用时检测到这种错误。在调试应用程序时这种做法是比较有用的,因为它能够确保应用程序在碰到未解析的符号时立即发生错误,而不是在执行了很长一段时间之后才发生错误。
我们可以结合如下例子理解RTLD_LAZY和RTLD_NOW之间的区别:

void bar();
void foo()
{
    bar();
}

使用命令生成libtest.so

[root@centos-7 shared]# gcc -g -c -fPIC -Wall test.c 
[root@centos-7 shared]# gcc -g -shared -o libtest.so test.o
$> nm libtest.so
000085d0 a _DYNAMIC
000086b0 a _GLOBAL_OFFSET_TABLE_
         w _ITM_deregisterTMCloneTable
         w _ITM_registerTMCloneTable
         w _Jv_RegisterClasses
000005c0 r __FRAME_END__
000085cc d __JCR_END__
000085cc d __JCR_LIST__
000086e0 d __TMC_END__
000086e4 B __bss_end__
000086e0 B __bss_start
000086e0 B __bss_start__
         w __cxa_finalize@@GLIBC_2.4
000004f8 t __do_global_dtors_aux
000085c8 t __do_global_dtors_aux_fini_array_entry
000086dc d __dso_handle
000086e4 B __end__
000085c4 t __frame_dummy_init_array_entry
         w __gmon_start__
000086e4 B _bss_end__
000086e0 D _edata
000086e4 B _end
000005b8 T _fini
000003e0 T _init
         U bar
00000424 t call_weak_fn
000086e0 b completed.8847
00000448 t deregister_tm_clones
000005a8 T foo
00000560 t frame_dummy
0000049c t register_tm_clones

T 该符号位于代码区text section。
U 该符号在当前文件中是未定义的,即该符号的定义在别的文件中。

main.c (flag == RTLD_LAZY):

#include <dlfcn.h>
#include <stdio.h>

int
main(int argc, char **argv)
{
    void *lib = dlopen("./libtest.so", RTLD_LAZY);
    if (!lib) {
        printf("error: %s\n", dlerror());
        return 0;
    }
    int (*a)() = dlsym(lib, "foo");
    printf("a: %p\n", a);
    (*a)();

    dlclose(lib);
    return 1;
}

使用命令gcc -g -o main main.c -ldl
运行结果如下:

[root@centos-7 shared]# ./main
a: 0x7f6df68ac665
./main: symbol lookup error: ./libtest.so: undefined symbol: bar

如果将main函数dlopen的flag修改为RTLD_NOW:
main.c

#include <dlfcn.h>
#include <stdio.h>

int
main(int argc, char **argv)
{
    void *lib = dlopen("./libtest.so", RTLD_NOW);
    if (!lib) {
        printf("error: %s\n", dlerror());
        return 0;
    }
    int (*a)() = dlsym(lib, "foo");
    printf("a: %p\n", a);
    (*a)();

    dlclose(lib);
    return 1;
}

执行结果:

[root@centos-7 shared]# vi main.c
[root@centos-7 shared]# gcc -g -o main main.c -ldl 
[root@centos-7 shared]# ./main
error: ./libtest.so: undefined symbol: bar

dlsym()

#include <dlfcn.h>
void *dlsym(void *handle, char *symbol);

常见用法:
如果symbol参数是一个变量的名称,那么可以使用dlsym返回的指针来调用该函数。可以将dlsym()返回的值存储到一个类型合适的指针中。

int *ip;
ip = (int *) dlsym(symbol, "myvar");
if (ip != NULL)
    printf("Value is %d\n", *ip);

如果symbol参数是一个函数的名称,那么可以使用dlsym返回的指针来调用该函数。可以将dlsym()返回的值存储到一个类型合适的指针中.

int (*funcp)(int);  /* Pointer to a function taking an integer argument and returning an integer */

由于C99标准进制函数指针和void *之间的赋值操作。可以使用如下方案解决:

*(void **) (&funcp) = dlsym(handle, symbol);

示例:

/*************************************************************************\
*                  Copyright (C) Michael Kerrisk, 2018.                   *
*                                                                         *
* This program is free software. You may use, modify, and redistribute it *
* under the terms of the GNU General Public License as published by the   *
* Free Software Foundation, either version 3 or (at your option) any      *
* later version. This program is distributed without any warranty.  See   *
* the file COPYING.gpl-v3 for details.                                    *
\*************************************************************************/

/* Listing 42-1 */

/* dynload.c

   Usage: dynload library-path function-name

   Demonstrate dynamic loading of libraries. The program loads the
   named library and then executes the named function in that library.
*/
#include <dlfcn.h>
#include "tlpi_hdr.h"

int
main(int argc, char *argv[])
{
    void *libHandle;            /* Handle for shared library */
    void (*funcp)(void);        /* Pointer to function with no arguments */
    const char *err;

    if (argc != 3 || strcmp(argv[1], "--help") == 0)
        usageErr("%s lib-path func-name\n", argv[0]);

    /* Load the shared library and get a handle for later use */

    libHandle = dlopen(argv[1], RTLD_LAZY);
    if (libHandle == NULL)
        fatal("dlopen: %s", dlerror());

    /* Search library for symbol named in argv[2] */

    (void) dlerror();                           /* Clear dlerror() */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
    funcp = (void (*)(void)) dlsym(libHandle, argv[2]);
#pragma GCC diagnostic pop

    /* In the book, instead of the preceding line, the code uses a
       rather clumsy looking cast of the form:

           *(void **) (&funcp) = dlsym(libHandle, argv[2]);

       This was done because the ISO C standard does not require compilers
       to allow casting of pointers to functions back and forth to 'void *'.
       (See TLPI pages 863-864.) SUSv3 TC1 and SUSv4 accepted the ISO C
       requirement and proposed the clumsy cast as the workaround. However,
       the 2013 Technical Corrigendum to SUSv4 requires implementations
       to support casts of the more natural form (now) used in the code
       above. However, various current compilers (e.g., gcc with the
       '-pedantic' flag) may still complain about such casts. Therefore,
       we use a gcc pragma to disable the warning.

       Note that this pragma is available only since gcc 4.6, released in
       2010. If you are using an older compiler, the pragma will generate
       an error. In that case, simply edit this program to remove the
       lines above that begin with '#pragma".

       See also the erratum note for page 864 at
       http://www.man7.org/tlpi/errata/. */

    err = dlerror();
    if (err != NULL)
        fatal("dlsym: %s", err);

    /* Try calling the address returned by dlsym() as a function
       that takes no arguments */

    (*funcp)();

    dlclose(libHandle);                         /* Close the library */

    exit(EXIT_SUCCESS);
}

dlclose()关闭共享库

dlclose()函数会减小handle所引用的库的打开引用的系统计数,如果这个引用计数变成了0并且其他库已经不需要用到该库中的符号了,那么就会卸载掉这个库。

获取与加载的符号相关的信息 dladdr()

#define _GNU_SOURCE
#include <dlfcn.h>

int dladdr(const void *addr, Dl_info *info);
typedef struct {
    const char *dli_fname;          /* Pathname of shared library
                                       containing 'addr' */
    void       *dli_fbase;          /* Base address at which shared
                                       library is loaded */
    const char *dli_sname;          /* Name of nearest run-time symbol
                                       with an address <= 'addr' */
    void       *dli_saddr;          /* Actual value of the symbol
                                       returned in 'dli_sname' */
} Dl_info;

Dl_info结构中的前两个字段指定了包含地址addr的共享库的路径名和运行时基地址。最后两个字段返回地址相关的信息。假设addr指向共享库中一个符号的确切地址,那么dli_saddr返回的值与传入的addr值一样。

在主程序中访问符号

假设使用dlopen()动态加载了一个共享库,然后使用dlsym()获取了共享库中x()函数的地址,接着调用x()。如果在x()中调用了函数y(),那么通常会在程序加载的其中一个共享库中搜索y().
有些时候需要让x()调用主程序中的y(),为了达到这一目的就必须要使主程序中的符号对动态链接器可用,即在链接的时候使用--export-dynamic连接器选项。

gcc  -Wl,--export-dynamic main.c
gcc -export-dynamic main.c

例子:

控制符号可见性

void
__attribute__ ((visibility("hidden")))
func(void) {
    /* Code */
}

static关键字使一个符号私有于一个源代码模块,从而使得它无法被其它目标文件绑定。
GNU C编译器gcc提供了一个特有的特性声明,它执行与static关键字类似的任务,static关键词将一个符号的可见性限制在单个源代码模文件中,而hidden特性使得一个符号对构成共享库的所有源代码文件都可见,但对库之外的文件不可见。

监控动态链接器:LD_DEBUG

查看LD_DEBUG的帮助信息:

$ LD_DEBUG=help date
Valid options for the LD_DEBUG environment variable are:

  libs       display library search paths
  reloc      display relocation processing
  files      display progress for input file
  symbols    display symbol table processing
  bindings   display information about symbol binding
  versions   display version dependencies
  all        all previous options combined
  statistics display relocation statistics
  unused     determine unused DSOs
  help       display this help message and exit

当请求与跟踪库相关的信息时会产生很多输出,下面的例子对输出进行了删减:

     $ LD_DEBUG=libs date
     10687:     find library=librt.so.1 [0]; searching
     10687:      search cache=/etc/ld.so.cache
     10687:       trying file=/lib/librt.so.1
     10687:     find library=libc.so.6 [0]; searching
     10687:      search cache=/etc/ld.so.cache
     10687:       trying file=/lib/libc.so.6
     10687:     find library=libpthread.so.0 [0]; searching
     10687:      search cache=/etc/ld.so.cache
     10687:       trying file=/lib/libpthread.so.0
     10687:     calling init: /lib/libpthread.so.0
     10687:     calling init: /lib/libc.so.6
     10687:     calling init: /lib/librt.so.1
     10687:     initialize program: date
     10687:     transferring control: date
Tue Dec 28 17:26:56 CEST 2010
     10687:     calling fini: date [0]
     10687:     calling fini: /lib/librt.so.1 [0]
     10687:     calling fini: /lib/libpthread.so.0 [0]
     10687:     calling fini: /lib/libc.so.6 [0] 

每一行开头处的10687是指所跟踪的进程的进程ID,当监控多个进程时会用到这个值。
默认情况下LD_DEBUG的输出会被写到标准错误上,但可以将一个路径名赋值给环境变量LD_DEBUG_OUTPUT来将输出重定向。