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

标签 c++ shared-libraries symbols

一个应用程序(app)依赖于两个共享库:liba.solibb.so
libalibb 具有相同的void 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;
}

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

最佳答案

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,它在不知不觉中私下实现了一个(注意指示内部链接的 static 关键字)。 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

关于c++ - 加载两个具有相同符号的共享库时是否存在符号冲突,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22004131/

相关文章:

PostgreSQL - 替换表列中特定字符的所有实例

C++ 程序在条件为 false 之前退出循环

c++ - 模板类库

c++ - qDebug 打印出错误的数字

c - libreadline.so.7 : undefined symbol: UP

linux - libpng12.so.0 : cannot open shared object file: wrong ELF class: ELFCLASS64

sympy - 如何在 SymPy 中定义大量符号

c++ - IntelliSense:无法打开包含路径中的源文件

unix - 库依赖工具

javascript - ECMAScript 6 中的 Symbol.for(string)