Linux动态链接器

参考:http://nicephil.blinkenshell.org/my_book/ch07s07.html

显式运行时链接:

运行时加载,让程序自己在运行时控制加载制定的模块,并可以在不需要该模块时将其卸载。动态装载库。

在Linux中,动态库实际上跟一般的共享对象没区别。主要区别是共享对象由动态链接器在程序启动之前负责装载和链接的。而动态库是通过一系列API程序自己控制的,打开动态库dlopen(),查找符号dlsym(),错误处理dlerror(),以及关闭动态库dlclose()。

头文件 <dlfcn.h>

dlopen()

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

第一个参数是被加载动态库的路径,如果是绝对路径直接尝试打开,如果是相对路径,查找如下:
a. 环境变量LD_LIBRARY_PATH指定的一系列目录
b. 由/etc/ld.so.cache里面指定的共享库路径
c. /lib,/usr/lib

如果库在多个目录下有不同副本会导致系统极为不可靠。

如果filename是0,那么dlopen会返回全局符号表的句柄,我们可以在运行时找到全局符号表里的任何符号,并可执行他们。全局符号表包含了程序的可执行文件本身,被动态链接器加载到进程中的所有共享模块已经在运行时通过dlopen打开并使用RTLD_GLOBAL方式的模块中的符号。

第二个参数表示函数符号的解析方式,常量RTLD_LAZY表示使用延迟绑定,即PLT机制。而RTLD_NOW表示当模块被加载时即完成所有函数的绑定工作,如果有任何未定义的符号引用的绑定工作无法完成。另外还有RTLD_GLOBAL跟上面两种的一个配合使用,表示被加载的模块的全局符号合并到进程全局符号表中,使得以后加载的模块可以使用这些符号。

dlopen返回被加载的模块的句柄,如果失败返回NULL,如果已经被加载过了返回同一个句柄。如果模块间由依赖关系,需要手动加载被依赖模块。

实际上dlopen还在加载模块时执行模块中的初始化部分代码.init段的代码。

dlsym()

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

第一个参数是dlopen()返回的动态库句柄

第二个参数是所要查找的符号的名字。

如果dlsym()找到相应的符号,返回符号的值,如果没有找到返回NULL。

dlsym()返回值对于不同类型的符号,意义不同。如果查找函数符号,返回函数地址。如果查找变量符号,返回变量地址。如果查找常量符号,返回该常量的值(存疑?)。但如果该常量刚好是NULL或0,那么需要使用dlerror()函数。

如果符号找到了,dlerror()返回NULL,如果没有,dlerror返回相应的错误信息。

dlerror()

判断上次调用是否成功,上次失败返回char *字符串,否则NULL。

dlclose()

将一个已经加载的模块卸载,系统维持一个加载引用计数器,每次使用dlopen()加载某模块,相应计数器加1;每次使用dlclose()卸载某模块,相应计数器减1.当计数器减到0,才真正卸载模块,先执行.fini段的代码,然后将相应符号从符号表中去除,取消进程空间的映射关系,然后关闭模块文件。

动态库句柄在没有真正卸载(计数器减为0)之前,不管so文件内容怎么变化,内存中的句柄都不会更新。

这个变化,仅限于mv+cp或者rm+cp更新改so;如果直接cp覆盖旧版本,程序只要用到动态库相关的函数或符号,程序都会core dump,包括dlopen操作。

当然如果so的任何更新操作前(cp或mv+cp等),句柄都真正卸载了,这时,重新dlopen没有任何问题,并且会拿到更新后的动态库内容。

详解同一进程中多线程“同时”加载同一动态库

因为:

第一,同一进程的不同线程共享进程的共享进程的堆区、静态存储区、和代码区,同一进程的不同线程有自己独立的栈区和寄存器

第二,dlopen加载动态库时,如果已经被加载(未执行dlclose),则返回同一句柄。此时,dlopen打开的符号的地址值都是相同的。此为本文中“同时”含义。

导致:

在同一进程中的不同线程中,使用同一句柄查找符号时:

  1. 函数符号:返回相同的函数地址值,属于代码区,共享,但是代码是只读的,等于无影响;
  2. 全局变量符号:返回相同的变量地址值,属于静态存储区,共享,不同线程之间该值的改变会互相影响;
  3. 静态全局变量符号:无法查找到此类符号,但是实际上在每个线程中,该静态全局变量的地址也相同(它存储在静态存储区域,该区域在线程之间是共享的),所以动态库函数中若使用了静态全局变量,不同线程之间该值的改变会互相影响。
    (与全局变量相同,唯一区别是静态全局变量有文件作用域,所以无法dlsym)
  4. 局部变量:自然是无法查找的,属于栈区,互不影响。

所以:
对于一个可能被同一进程中多线程“同时”打开的动态库,在动态库代码中使用全部变量或静态全局变量都是不科学的。

测试:

动态库代码

#include <string.h>
#include "func.h"
#include <stdlib.h>
static int g_age = 17;
int g_group = 147;
#define MIN 2
Student GetStuInfo() {
    Student stu;
    strcpy(stu.name, "lexi");
    char des[] = "i am lexi!";
    stu.des = des;
    stu.age = g_age;
    stu.group = g_group;
    stu.age = g_age;
    printf("age = %d\tgroup = %d\n", g_age, g_group);
    stu.group = g_group;
    g_age++;
    g_group += 2;
    return stu;
}

运行结果
每个线程等其余全部dlclose后再加载动态库,则无影响。

进程与线程对比结论:

在不同进程中,虽然同时加载时,返回的地址值可能相同,但这可以理解成虚拟地址相同,由于本质上各进程占用的堆区、栈区、静态存储区、和代码区都是独立的,所以不会互相影响。

更准确的说,使用fork()创建的进程,由于linux中引入了“写时复制“技术,当没有更改相应段的行为出现时,不同进程将共享父进程的物理空间。但是只要有更改行为出现,就会分配各自的物理空间,所以不同进程中,同时打开同一个动态库,没有什么特殊问题。

生成动态库

foo.c

#include <stdio.h>
#include "foo.h"
void hi(void) {
    printf("Hello Cgo!\n");
}

foo.h

void hi(void);

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

(等同于:
gcc -c -fPIC -o foo.o foo.c
gcc -shared -o libfoo.so foo.o
两条命令)

-fPIC 作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的

链接动态库

#include <dlfcn.h>
#include <stdio.h>
int main() {
    void *handle = dlopen("./libfoo.so", RTLD_LAZY);
    if (!handle) {
        printf("dlopen error:%s\n", dlerror());
        return -1;
    }
    typedef void (*HI)(void);
    HI hi = dlsym(handle, "hi");
    
    if (dlerror() != NULL) {
        printf("dlsym m_pr error:%s\n", dlerror());
        return -1;
    }
    hi();
    dlclose(handle);
    dlerror();
    printf("------------------------\n");
    return 0;
}

编译命令:gcc test.c –ldl
编译时不依赖libfoo.so动态库。

猜你喜欢

转载自blog.csdn.net/chenmeiling_5/article/details/107774012