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

Posted csstormq

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了计算机系统篇之链接:动态链接相关的知识,希望对你有一定的参考价值。

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

Author:stormQ

Wednesday, 15. April 2020 04:35PM


引入共享库的动机

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

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

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

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

    • Linux 中的静态链接器ld是可执行目标文件,Linux 中的动态链接器(如ld-linux-x86-64.so.2)是可共享目标文件。

    • Linux 中的静态链接器ld的输入文件可以是共享库,但只是拷贝了重定位信息和符号表并解析符号,但未重定位。也就是说,即便共享库可以作为Linux 中的静态链接器ld的输入文件,但实际上没有完全链接。

如何生成共享库

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

$ g++ -o libtest.so test.cpp -shared -fpic
  • 注:

    • -shared属于链接器选项,指示链接器生成共享库。

    • -fpic属于编译器器选项,指示编译器生成位置无关代码。

    • -fno-pic选项指示编译器不生成位置无关代码,但在 Linux x86-64 中使用该选项生成上述共享库时链接器会报错:/usr/bin/x86_64-linux-gnu-ld: /tmp/ccdwqRF4.o: relocation R_X86_64_PC32 against symbol g_val_1 can not be used when making a shared object; recompile with -fPIC /usr/bin/x86_64-linux-gnu-ld: final link failed: Bad value collect2: error: ld returned 1 exit status

查看 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;

使用共享库:

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

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

当加载器加载并运行可执行目标文件时,如果可执行目标文件中包含 .interp section(该 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 的 .interp section:

$ 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
  • 注:

    • dlopen()等函数包含在dlfcn.h头文件中。

    • RTLD_LAZY标志表示延迟绑定,即只有在执行符号引用的代码时才解析符号。延迟绑定的作用对象为对共享库中函数的引用。对共享库中全局变量的引用不涉及延迟绑定,即在 dlopen() 函数返回之前就完成了对共享库中全局变量的解析。

    • RTLD_NOW标志表示立即解析。也就是说,在 dlopen() 函数返回之前需要完成所有对共享库中函数引用和全局变量引用的解析。

    • RTLD_GLOBAL标志表示该共享库中的符号定义可以用于随后加载的共享库的符号解析。

    • RTLD_LOCAL标志表示的含义与RTLD_GLOBAL相反。

    • RTLD_LAZYRTLD_NOW只能二选其一。RTLD_GLOBALRTLD_LOCAL只能二选其一。这两组的标志可以取|,比如:RTLD_LAZY | RTLD_GLOBAL

进一步理解共享库:

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 0x400ddf: file main.cpp, line 12.
Starting program: /home/xuxiaoqiang/tx/dyn/t/main ./libtest.so func

Temporary breakpoint 1, main (argc=3, argv=0x7fffffffdb38) at 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
5: func = <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
5: func = <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函数加载同一个共享库,那么自从第一次将共享库中的代码和数据等映射到进程后,其余的加载操作只是将进程对该共享库的引用计数递增 1。

  • 如果同一个进程调用dlclose函数卸载同一个共享库,每成功调用dlclose函数一次,进程对该共享库的引用计数递减 1,当引用计数减为 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();
    void* getSymbol(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

如果你觉得本文对你有所帮助,欢迎关注公众号,支持一下!

以上是关于计算机系统篇之链接:动态链接的主要内容,如果未能解决你的问题,请参考以下文章

计算机系统篇之链接:静态链接(上)

计算机系统篇之链接:静态链接(中)——符号解析

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

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

计算机系统篇之链接:目标文件

计算机系统篇之链接:目标文件