加载具有相同符号的两个共享库时是不是存在符号冲突

Posted

技术标签:

【中文标题】加载具有相同符号的两个共享库时是不是存在符号冲突【英文标题】:Is there symbol conflict when loading two shared libraries with a same symbol加载具有相同符号的两个共享库时是否存在符号冲突 【发布时间】:2014-03-27 01:57:56 【问题描述】:

一个应用程序(app)依赖于两个共享库:liba.solibb.soliba libbvoid Hello() 的功能相同,但实现方式不同。 在运行时加载两个共享库,并尝试访问 Hello() 的两个版本。 我通过 poco C++ 共享库加载 liba.so 和 libb.so,但最终它调用 dlopen() 来加载共享库。代码如下:

#include "Poco/SharedLibrary.h"
using Poco::SharedLibrary;
typedef void (*HelloFunc)(); // function pointer type


int main(int argc, char** argv)

    std::string path("liba");
    path.append(SharedLibrary::suffix()); // adds ".so"
    SharedLibrary library(path);
    HelloFunc func = (HelloFunc) library.getSymbol("hello");
    func();

    std::string path2("libb");
    path2.append(SharedLibrary::suffix()); // adds ".so"
    SharedLibrary library2(path2);
    HelloFunc func2 = (HelloFunc) library2.getSymbol("hello");
    func2();

    library.unload();
    library2.unload();

    return 0;

我的问题是,当 app 通过 dlopen() 加载 liba.so 和 libb.so 时,这两个 Hello() 实现会不会有符号冲突? 事实上,代码运行良好,但我想知道加载这样的库是否存在任何潜在风险

【问题讨论】:

【参考方案1】:

我遇到了这样的问题,我做了一个例子:

// lib.h
#pragma once

int libfunc();

实现使用一个名为“myfunc”的函数

// lib.c
#include "lib.h"

#include <stdio.h>

int myfunc()

        return printf("lib myfunc()\n");


int libfunc()

        myfunc();
        return printf("libfunc()\n");

这是从定义了另一个“myfunc”函数的代码的主要部分调用的

// main.c
#include <stdio.h>
#include "lib.h"

int myfunc()

        return printf("main myfunc()\n");


int main(void)

        libfunc();
        return 0;

编译和执行:

> gcc -shared -fPIC -o liblib.so lib.c
> file liblib.so
liblib.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=a66c7c56b191995df01dbb0a6a94e2358716b369, with debug_info, not stripped
> gcc main.c -o main -L. -llib
> LD_LIBRARY_PATH=. ./main
main myfunc()
libfunc()
>

可以看出,调用的不是库中的函数,而是 main.c 文件中已经解析的函数。

【讨论】:

【参考方案2】:

TL;DR:如果您想防止已经加载的全局符号在您 dlopen() 时劫持您的库,请始终使用 RTLD_DEEPBIND

当您使用 dlopen 加载库时,您可以使用 dlsym 访问其中的所有符号,这些符号将是该库中的正确符号,并且不会污染全局符号空间(除非您使用了 RTLD_GLOBAL)。 但即使库本身定义了符号,它的依赖关系仍会使用已加载的全局符号(如果可用)来解决。

考虑将第三方库称为 libexternal.so、external.c:

#include <stdio.h>

void externalFn()

    printf("External function from the EXTERNAL library.\n");

然后考虑liba.so,它在不知不觉中私下实现了一个(注意指示内部链接的静态关键字)。 liba.c:

#include <stdio.h>

static void externalFn()

    printf("Private implementation of external function from A.\n");


void hello()

    printf("Hello from A!\n");
    printf("Calling external from A...\n");
    externalFn();

然后考虑libb.so,它不知不觉地实现了一个并将其导出,libb.c:

#include <stdio.h>

void externalFn()

    printf("External implementation from B\n");


void hello()

    printf("Hello from B!\n");
    printf("Calling external from B...\n");
    externalFn();

然后链接到 libexternal.so 的主应用程序会动态加载上述两个库并调用其中的东西,main.c:

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

void externalFn();

int main()

    printf("Calling external function from main app.\n");
    externalFn();

    printf("Calling libA stuff...\n");
    void *lib = dlopen("liba.so", RTLD_NOW);
    void (*hello)();
    hello = dlsym(lib, "hello");
    hello();

    printf("Calling libB stuff...\n");
    void *libB = dlopen("libb.so", RTLD_NOW);
    void (*helloB)();
    helloB = dlsym(libB, "hello");
    helloB();

    printf("Calling externalFn via libB...\n");
    void (*externalB)() = dlsym(libB, "externalFn");
    externalB();

    return 0;

构建命令是:

#!/bin/bash

echo "Building External..."
gcc external.c -shared -fPIC -o libexternal.so

echo "Building LibA..."
gcc liba.c -shared -fPIC -o liba.so

echo "Building LibB..."
gcc libb.c -shared -fPIC -o libb.so

echo "Building App..."
gcc main.c libexternal.so -ldl -Wl,-rpath,\$ORIGIN -o app

当您运行 app 时,它会打印:

Calling external function from main app.
External function from the EXTERNAL library.
Calling libA stuff...
Hello from A!
Calling external from A...
Private implementation of external function from A.
Calling libB stuff...
Hello from B!
Calling external from B...
External function from the EXTERNAL library.
Calling externalFn via libB...
External implementation from B

您可以看到,当 libb.so 调用 externalFn 时,将调用来自 libexternal.so 的那个!但是您仍然可以通过 dlsym 访问 libb.so 的 externalFn() 实现。

你什么时候会遇到这个问题?在我们的案例中,当我们为 Linux 发布库时,我们会尽量使其自包含,因此如果可以的话,我们会静态链接每个第三方库依赖项。但是仅仅添加 libwhatever.a 会导致你的库导出 libwhatever.a 中的所有符号 因此,如果消费者应用程序还使用系统预装的 libwhatever.so,那么您的库对 libwhatever 符号的符号引用将链接到已加载的库,而不是您静态链接的库。如果两者不同,结果是崩溃或内存损坏。

解决方法是使用链接器脚本来防止导出不需要的符号以避免混淆动态链接器。

但不幸的是,问题并不止于此。

LibA 的供应商决定在一个插件目录中提供多个库。所以他们将 externalFn() 的实现移到他们自己的库 external2.c 中:

#include <stdio.h>

void externalFn()

    printf("External function from the EXTERNAL2 library.\n");

然后构建脚本更改为构建新的外部库并将所有东西移动到插件目录中:

#!/bin/bash

echo "Building External..."
gcc external.c -shared -fPIC -o libexternal.so

echo "Building External2..."
gcc external2.c -shared -fPIC -o libexternal2.so

echo "Building LibA..."
gcc liba.c libexternal2.so -shared -fPIC -Wl,-rpath,\$ORIGIN,--disable-new-dtags -o liba.so

echo "Building LibB..."
gcc libb.c -shared -fPIC -o libb.so

echo "Installing plugin"
mkdir -p plugins
mv liba.so plugins/
mv libexternal2.so plugins/

echo "Building App..."
gcc main.c libexternal.so -ldl -Wl,-rpath,\$ORIGIN,--disable-new-dtags -o app

很明显,liba.c 依赖于 libexternal2.so,因为我们链接它,我们甚至设置 RPATH 以使链接器在它所在的文件夹中查找它,因此即使 ldd 也显示它没有引用 libexternal.so全部,只有 libexternal2.so:

$ ldd liba.so
    linux-vdso.so.1 (0x00007fff75870000)
    libexternal2.so => /home/calmarius/stuff/source/linking/plugins/./libexternal2.so (0x00007fd9b9bcd000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd9b97d5000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fd9b9fdd000)

所以更改应用程序以从插件目录加载 liba.so。 所以它应该可以正常工作,对吧?错误的!运行应用程序,你会得到:

Calling external function from main app.
External function from the EXTERNAL library.
Calling libA stuff...
Hello from A!
Calling external from A...
External function from the EXTERNAL library.
Calling libB stuff...
Hello from B!
Calling external from B...
External function from the EXTERNAL library.
Calling externalFn via libB...
External implementation from B

您可以看到,现在甚至 libA 都调用了应用程序链接的库,而不是 lib 链接的库!

解决办法是什么?自 glibc 2.3.4(自 2004 年以来存在)有一个选项 RTLD_DEEPBIND 如果你想避免与已经全局符号冲突,你必须始终需要在 dlopen-ing 库时指定此标志。因此,如果我们将标志更改为 RTLD_NOW | RTLD_DEEPBIND,我们会在运行应用程序时得到预期结果:

Calling external function from main app.
External function from the EXTERNAL library.
Calling libA stuff...
Hello from A!
Calling external from A...
External function from the EXTERNAL2 library.
Calling libB stuff...
Hello from B!
Calling external from B...
External implementation from B
Calling externalFn via libB...
External implementation from B

【讨论】:

这是一个更好的解释!这对我调试遇到的情况有很大帮助! 这应该是公认的答案。在不知道注意事项的情况下,很容易创建需要很长时间才能调试的动态链接相关错误。【参考方案3】:

我的问题是,当app通过dlopen()加载liba.so和libb.so时,这两个Hello()实现会不会有符号冲突?

没有。这些是返回的地址,两个动态加载的库都将存在于单独的地址空间中。

即使是 dlsym 函数也不会混淆,因为您传递了 dlopen 函数返回的句柄,所以它不会变得模棱两可。

(这甚至不会是在同一个库中重载的问题,分别)

【讨论】:

那么 dlopen 的行为是否不同于加载时动态链接,如果它找到一个符号,它将链接到应用程序的符号而不是库的符号?

以上是关于加载具有相同符号的两个共享库时是不是存在符号冲突的主要内容,如果未能解决你的问题,请参考以下文章

尝试使用 dlopen 加载库时未定义的符号

如何防止静态库中的所有符号加载以及为什么在链接静态库时导出相同.o文件中的其他符号进行测试

当另一个库具有相同的符号时,gdb 不显示来自共享库的符号

链接共享库时限制符号的可见性

定义相同函数名的两个 linux 共享对象之间的冲突

typedef 结构在链接静态库时会导致名称冲突吗?