计算机系统篇之链接(6):动态链接

Author: stormQ

Created: Wednesday, 15. April 2020 04:35PM

Last Modified: Sunday, 10. January 2021 03:43PM



摘要

本文描述了引入动态链接库的动机,并提供了 Linux 系统中生成和使用动态链接库的示例。

引入共享库的动机

共享库,也被称为可共享目标文件,可以被动态链接器在加载期或运行期加载到任意的内存地址,并和一个内存中的程序链接起来。

引入共享库的目的是为了解决静态库的缺点。相比于静态库的三个缺点,共享库对应的优点为:1)可以在加载期或运行期进行链接过程。因此,共享库更新后不用像静态库那样,可执行目标文件必须显式地重新链接。2)节省磁盘空间。共享库的代码和数据被所有引用该共享库的可执行目标文件共享。3)节省内存空间。共享库的代码在内存中的一份拷贝被所有引用该共享库的进程共享。

静态链接与动态链接的区别

区别 使用的链接器 作用对象 发生的时期
静态链接 静态链接器(如:ld on Linux) 静态库 编译期
动态链接 动态链接器(如:ld-linux-x86-64.so.2 on Linux) 共享库 加载期或运行期

如何生成共享库

1) 生成共享库(On x86-64 Linux)

$ g++ -o libtest.so test.cpp -shared -fPIC

查看 test.cpp 和 main.cpp 的源码:

$ cat test.cpp 
int g_val_1 = 1;
int g_val_2 = 2;

void func()
{
  g_val_1 *= 2;
  g_val_2 *= 2;
}
$ cat main.cpp
extern int g_val_1;
extern int g_val_2;

void func();

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

2) 使用共享库

$ g++ -o main main.cpp ./libtest.so

虽然生成的可执行目标文件引用了共享库中的函数或全局变量,但是共享库中的代码和数据不会拷贝到可执行目标文件中。实际上,只是将共享库中的重定位信息和符号表拷贝到了可执行目标文件中,以便于在加载期对共享库中的函数或全局变量的引用进行解析。

当加载器加载并运行可执行目标文件时,如果可执行目标文件中包含.interpsection(该 section 用于指定动态链接器的路径),那么加载器会加载并运行动态链接器,动态链接器完成链接过程后,动态链接器会将控制权交给应用程序,从而开始执行应用程序的入口点函数。

查看可执行目标文件 main 需要的共享库:

$ ldd main
    linux-vdso.so.1 =>  (0x00007ffc8e793000)
    ./libtest.so (0x00007f21de5ed000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f21de223000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f21de7ef000)

查看可执行目标文件 main 的.interpsection:

$ readelf -p .interp main

String dump of section '.interp':
  [     0]  /lib64/ld-linux-x86-64.so.2

运行期加载和链接共享库

共享库常用函数:

函数作用 函数原型 参数含义 返回值
打开共享库 void *dlopen(const char *filename, int flag);
  • 参数1:指定共享库的路径。
  • 参数2:指定打开共享库的标志。常用可选项:RTLD_LAZYRTLD_NOWRTLD_GLOBALRTLD_LOCAL
  • 执行成功,返回指向共享库的句柄指针
  • 执行失败,返回 NULL
  • 关闭共享库 int dlclose(void *handle);
  • 参数:指向共享库的句柄指针。
  • 执行成功,返回 0
  • 执行失败,返回 -1
  • 查找共享库中的符号定义 void *dlsym(void *handle, char *symbol);
  • 参数1:指向共享库的句柄指针。
  • 参数2:要查找的符号定义的名称。
  • 执行成功,返回指向符号定义的指针
  • 执行失败,返回 NULL
  • 进一步理解共享库:

    1) 问题 1:如果一个进程在关闭共享库后,正在执行或再次执行该共享库中的代码。那么,这两种情况分别会导致什么后果?

    验证过程:

    a)验证问题 1

    进程关闭共享库(即执行 dlclose 函数)后,如果该进程对该共享库的引用计数减为 0,那么无论是正在执行还是再次执行该共享库中的代码,都会发生违规访问,这是因为进程已撤销对共享库中代码和数据的映射,即进程看不到共享库中的任何代码和数据了。通常情况下,会导致进程崩溃退出。

    验证如下:

    首先,生成共享库 libtest.so 和可执行目标文件 main:

    $ g++ -o libtest.so test.cpp -shared -fpic -g
    $ g++ -o main main.cpp shared_library.cpp -std=c++11 -ldl -g

    接下来,使用 gdb 调试:

    $ gdb -q --args ./main "./libtest.so" "func"

    启动 main,并执行到第一次加载 libtest.so 完成的位置:

    (gdb) start
    Temporary breakpoint 1 at 0x400ddffile main.cpp, line 12.
    Starting program: /home/xuxiaoqiang/tx/dyn/t/main ./libtest.so func

    Temporary breakpoint 1main (argc=3, argv=0x7fffffffdb38at main.cpp:12
    12    {
    (gdb) n
    13        if (argc != 3)
    (gdb) 
    20            std::string lib_name(argv[1]);
    (gdb) 
    21            std::string symbol_name(argv[2]);
    (gdb) 
    23            SharedLibrary loader;
    (gdb) 
    25                SharedLibrary::LOAD_FLAG_LAZY);
    (gdb) 
    27            SharedLibrary loader_2;

    自动打印 libtest.so 中定义的全局变量 g_val_1g_val_2,及它们的内存地址、函数func的地址和函数指针lib_func的值,观察它们的值变化情况:

    (gdb) display/d g_val_1
    1: /d g_val_1 = 1
    (gdb) display/x &g_val_1
    2: /x &g_val_1 = 0x7ffff6f5b020
    (gdb) display/d g_val_2
    3: /d g_val_2 = 2
    (gdb) display/x &g_val_2
    4: /x &g_val_2 = 0x7ffff6f5b024
    (gdb) display func
    5: func =
     {<text variable, no debug info>} 0x7ffff6d5a680 <func>
    (gdb) display lib_func
    6: lib_func = (void (*)(void)) 0x7ffff729f299 <__GI___cxa_atexit+25>

    查看进程已加载 sections 的信息:

    (gdb) info files 
    // 省略 ...
        0x00007ffff6d5a1c8 - 0x00007ffff6d5a1ec is .note.gnu.build-id in ./libtest.so
        0x00007ffff6d5a1f0 - 0x00007ffff6d5a234 is .gnu.hash in ./libtest.so
        0x00007ffff6d5a238 - 0x00007ffff6d5a3a0 is .dynsym in ./libtest.so
        0x00007ffff6d5a3a0 - 0x00007ffff6d5a442 is .dynstr in ./libtest.so
        0x00007ffff6d5a448 - 0x00007ffff6d5a538 is .rela.dyn in ./libtest.so
        0x00007ffff6d5a538 - 0x00007ffff6d5a552 is .init in ./libtest.so
        0x00007ffff6d5a560 - 0x00007ffff6d5a570 is .plt in ./libtest.so
        0x00007ffff6d5a570 - 0x00007ffff6d5a580 is .plt.got in ./libtest.so
        0x00007ffff6d5a580 - 0x00007ffff6d5a6b1 is .text in ./libtest.so
        0x00007ffff6d5a6b4 - 0x00007ffff6d5a6bd is .fini in ./libtest.so
        0x00007ffff6d5a6c0 - 0x00007ffff6d5a6dc is .eh_frame_hdr in ./libtest.so
        0x00007ffff6d5a6e0 - 0x00007ffff6d5a744 is .eh_frame in ./libtest.so
        0x00007ffff6f5ae60 - 0x00007ffff6f5ae68 is .init_array in ./libtest.so
        0x00007ffff6f5ae68 - 0x00007ffff6f5ae70 is .fini_array in ./libtest.so
        0x00007ffff6f5ae70 - 0x00007ffff6f5ae78 is .jcr in ./libtest.so
        0x00007ffff6f5ae78 - 0x00007ffff6f5afc8 is .dynamic in ./libtest.so
        0x00007ffff6f5afc8 - 0x00007ffff6f5b000 is .got in ./libtest.so
        0x00007ffff6f5b000 - 0x00007ffff6f5b018 is .got.plt in ./libtest.so
        0x00007ffff6f5b018 - 0x00007ffff6f5b028 is .data in ./libtest.so
        0x00007ffff6f5b028 - 0x00007ffff6f5b030 is .bss in ./libtest.so

    可以看出,g_val_1g_val_2func的内存地址都在 ./libtest.so 中。

    继续执行至第一次执行lib_func完成的位置:

    (gdb) n
    29                SharedLibrary::LOAD_FLAG_LAZY);
    1: /d g_val_1 = 1
    2: /x &g_val_1 = 0x7ffff6f5b020
    3: /d g_val_2 = 2
    4: /x &g_val_2 = 0x7ffff6f5b024
    5func = {<text variable, no debug info>} 0x7ffff6d5a680 <func>
    6: lib_func = (void (*)(void)) 0x7ffff729f299 <__GI___cxa_atexit+25>
    (gdb) 
    32            *(void **)&lib_func = loader.getSymbol(symbol_name.c_str());
    1: /d g_val_1 = 1
    2: /x &g_val_1 = 0x7ffff6f5b020
    3: /d g_val_2 = 2
    4: /x &g_val_2 = 0x7ffff6f5b024
    5: func =
     {<text variable, no debug info>} 0x7ffff6d5a680 <func>
    6: lib_func = (void (*)(void)) 0x7ffff729f299 <__GI___cxa_atexit+25>
    (gdb) 
    34            lib_func();
    1: /d g_val_1 = 1
    2: /x &g_val_1 = 0x7ffff6f5b020
    3: /d g_val_2 = 2
    4: /x &g_val_2 = 0x7ffff6f5b024
    5: func =
     {<text variable, no debug info>} 0x7ffff6d5a680 <func>
    6: lib_func = (void (*)(void)) 0x7ffff6d5a680 <func>
    (gdb) 
    35            loader.unload();
    1: /d g_val_1 = 2
    2: /x &g_val_1 = 0x7ffff6f5b020
    3: /d g_val_2 = 4
    4: /x &g_val_2 = 0x7ffff6f5b024
    5: func =
     {<text variable, no debug info>} 0x7ffff6d5a680 <func>
    6: lib_func = (void (*)(void)) 0x7ffff6d5a680 <func>

    可以看出,第二次加载 libtest.so(dlopen 函数是执行成功了的),并未改变g_val_1g_val_2func的内存地址。

    继续执行至第一次关闭libtest.so完成的位置:

    (gdb) 
    36            lib_func();
    1: /d g_val_1 = 2
    2: /x &g_val_1 = 0x7ffff6f5b020
    3: /d g_val_2 = 4
    4: /x &g_val_2 = 0x7ffff6f5b024
    5func = {<text variable, no debug info>} 0x7ffff6d5a680 <func>
    6: lib_func = (void (*)(void)) 0x7ffff6d5a680 <func>
    (gdb) 
    37            loader_2.unload();
    1: /d g_val_1 = 4
    2: /x &g_val_1 = 0x7ffff6f5b020
    3: /d g_val_2 = 8
    4: /x &g_val_2 = 0x7ffff6f5b024
    5: func =
     {<text variable, no debug info>} 0x7ffff6d5a680 <func>
    6: lib_func = (void (*)(void)) 0x7ffff6d5a680 <func>

    可以看出,第一次关闭libtest.so(dlclose 函数是执行成功了的),并未改变g_val_1g_val_2func的内存地址。

    继续执行至第二次关闭libtest.so完成的位置:

    (gdb) 
    38            lib_func();
    warning: Unable to display "g_val_1": No symbol "g_val_1" in current context.
    warning: Unable to display "&g_val_1": No symbol "g_val_1" in current context.
    warning: Unable to display "g_val_2": No symbol "g_val_2" in current context.
    warning: Unable to display "&g_val_2": No symbol "g_val_2" in current context.
    warning: Unable to display "func": No symbol "func" in current context.
    6: lib_func = (void (*)(void)) 0x7ffff6d5a680

    可以看出,第二次关闭libtest.so(dlclose 函数也是执行成功了的),g_val_1g_val_2func的内存地址变为不可访问。而此时lib_func的值仍为func的内存地址。

    继续执行:

    (gdb) 

    Program received signal SIGSEGV, Segmentation fault.
    0x00007ffff6d5a680 in ?? ()
    (gdb) bt
    #0  0x00007ffff6d5a680 in ?? ()
    #1  0x0000000000400f85 in main (argc=3, argv=0x7fffffffdb38) at main.cpp:38

    可以看出,第二次关闭libtest.so后,进程对共享库的引用计数减为 0,进程会撤销对共享库中的代码和数据的映射。再次执行共享库中的代码,会发生越界访问,从而导致进程崩溃退出。

    从上述验证过程中,可以得出如下结论:

    注:dlopen、dlclose 的引用计数保存在link_map结构体中的l_direct_opencount字段。

    // in glibc-2.19/include/link.h
    struct link_map
    {

        // 省略...
        unsigned int l_direct_opencount; /* Reference count for dlopen/dlclose.  */
        // 省略...
    };

    完整程序:

    main.cpp:

    // how to compile: g++ -o main main.cpp shared_library.cpp -std=c++11 -ldl -g
    // how to debug: gdb -q --args ./main "./libtest.so" "func"

    #include "shared_library.h"
    #include <iostream>
    #include <cstdio>
    #include <string>

    using common::SharedLibrary;

    int main(int argc, char **argv)
    {
        if (argc != 3)
        {
            std::cerr << "Usage: <path of shared library> <symbol name>\n";
            return 1;
        }

        {
            std::string lib_name(argv[1]);
            std::string symbol_name(argv[2]);

            SharedLibrary loader;
            loader.load(lib_name, SharedLibrary::LOAD_FLAG_GLOBAL | 
                SharedLibrary::LOAD_FLAG_LAZY);

            SharedLibrary loader_2;
            loader_2.load(lib_name, SharedLibrary::LOAD_FLAG_GLOBAL | 
                SharedLibrary::LOAD_FLAG_LAZY);

            void (*lib_func)();
            *(void **)&lib_func = loader.getSymbol(symbol_name.c_str());

            lib_func();
            loader.unload();
            lib_func();
            loader_2.unload();
            lib_func();
        }
        return 0;
    }

    test.cpp:

    // how to compile: g++ -o libtest.so test.cpp -shared -fpic -g

    int g_val_1 = 1;
    int g_val_2 = 2;

    extern "C"
    {

    void func()
    {
        g_val_1 *= 2;
        g_val_2 *= 2;
    }

    }   // extern "C"

    shared_library.h:

    #pragma once

    #include <mutex>
    #include <string>

    namespace common
    {

    class SharedLibrary
    {

    public:
        enum LoadFlag
        {
            LOAD_FLAG_GLOBAL    = 1,
            LOAD_FLAG_LOCAL     = 2,
            LOAD_FLAG_NOW       = 4,
            LOAD_FLAG_LAZY      = 8,
        };

        SharedLibrary() = default;
        ~SharedLibrary() = default;
        SharedLibrary(const SharedLibrary &) = delete;
        SharedLibrary& operator = (const SharedLibrary &) = delete;

        void load(const std::string &file_name, int flag);
        void unload();
        voidgetSymbol(const std::string &symbol_name);

    private:
        int realLoadFlag(int flag) const;

    private:
        void *handle_ = nullptr;
        static std::mutex handle_mutex_;
    };

    }   // namespace common

    shared_library.cpp:

    #include "shared_library.h"
    #include <dlfcn.h>
    #include <iostream>

    #define CHECK_DLERROR(cond) if (cond) { std::cerr << dlerror(); }

    namespace common
    {

    std::mutex SharedLibrary::handle_mutex_;

    void SharedLibrary::load(const std::string &file_name, int flag)
    {
        const auto real_flag = realLoadFlag(flag);
        std::lock_guard<std::mutex> lock(handle_mutex_);
        if (nullptr == handle_)
        {
            handle_ = dlopen(file_name.c_str(), real_flag);
        }
        CHECK_DLERROR(!handle_);
    }

    int SharedLibrary::realLoadFlag(int flag) const
    {
        int real_flag = 0;
        real_flag |=  (flag & LOAD_FLAG_NOW ? RTLD_NOW : RTLD_LAZY);
        real_flag |=  (flag & LOAD_FLAG_GLOBAL ? RTLD_GLOBAL : RTLD_LOCAL);
        return real_flag;
    }

    void SharedLibrary::unload()
    {
        std::lock_guard<std::mutex> lock(handle_mutex_);
        if (handle_)
        {
            auto success = !dlclose(handle_);
            if (success)
            {
                handle_ = nullptr;
            }
            else
            {
                handle_mutex_.unlock();
                CHECK_DLERROR(!success);
            }
        }
    }

    void* SharedLibrary::getSymbol(const std::string &symbol_name)
    {
        std::lock_guard<std::mutex> lock(handle_mutex_);
        if (handle_)
        {
            auto result = dlsym(handle_, symbol_name.c_str());
            handle_mutex_.unlock();
            CHECK_DLERROR(!result);
            return result;
        }
        return nullptr;
    }

    }  // namespace common

    -fpic 和 -fPIC 的区别

    -fpic-fPIC都是用于指示编译器生成地址无关代码的编译器选项。

    《gcc_9.3_manual》中关于-fpic-fPIC的解释如下:

    -fpic

    Generate position-independent code (PIC) suitable for use in a shared library,
    if supported for the target machine. Such code accesses all constant addresses
    through a global offset table (GOT). The dynamic loader resolves the GOT
    entries when the program starts (the dynamic loader is not part of GCC; it
    is part of the operating system). If the GOT size for the linked executable
    exceeds a machine-specific maximum size, you get an error message from the
    linker indicating that ‘-fpic’ does not work; in that case, recompile with ‘-fPIC’
    instead. (These maximums are 8k on the SPARC, 28k on AArch64 and 32k on
    the m68k and RS/6000. The x86 has no such limit.)
    Position-independent code requires special support, and therefore works only on
    certain machines. For the x86, GCC supports PIC for System V but not for the
    Sun 386i. Code generated for the IBM RS/6000 is always position-independent.
    When this flag is set, the macros __pic__ and __PIC__ are defined to 1.

    -fPIC

    If supported for the target machine, emit position-independent code, suitable
    for dynamic linking and avoiding any limit on the size of the global offset table.
    This option makes a difference on AArch64, m68k, PowerPC and SPARC.
    Position-independent code requires special support, and therefore works only
    on certain machines.
    When this flag is set, the macros __pic__ and __PIC__ are defined to 2.

    从上面的解释中可以看出,-fpic-fPIC之间的区别为:前者生成的地址无关代码中,如果全局偏移表(GOT)的大小超过了机器所允许的最大值,那么链接器会报错;而后者不受限制。


    下一篇:计算机系统篇之链接(7):位置无关代码(上)

    上一篇:计算机系统篇之链接(5):静态链接(下)——重定位

    首页