【问题标题】:Why would dlopen reuse the address of a previously loaded symbol?为什么 dlopen 会重用先前加载的符号的地址?
【发布时间】:2020-01-30 00:19:06
【问题描述】:

我刚刚调试了一个奇怪的问题,我有两个库,我们称之为 libA.so 和 libB.so

应用程序 dlopens libA.so(编辑:它不是:它由 -l 选项链接)这是一个瘦库,然后加载 libB.so 这是实际的实现。

使用 RTLD_NOW 选项调用 dlopen,不传递其他选项。

并且两个库都使用相同的记录器模块,其中记录器的状态存储在全局变量中,因为它们都使用相同的记录器并静态链接到它们,它们中的全局变量具有相同的名称。

加载 libB 时,两个全局变量位于同一地址并发生冲突。所以动态链接器重用了变量的地址来使用libB中的同一个变量。

如果这个变量是在 .cpp 文件的深处定义的,我不确定 C 和 C++ 之间的链接是否不同。

阅读dlopen's documentation 上面写着:

RTLD_GLOBAL

此库定义的符号将可用于后续加载的库的符号解析。

RTLD_LOCAL

这是 RTLD_GLOBAL 的反面,如果两个标志都没有指定,则为默认值。此库中定义的符号不可用于解析后续加载的库中的引用。

所以 RTLD_LOCAL 应该是默认值,即在解析 libB 的符号时不应使用 libA 的符号。但它仍在发生。为什么?

作为一种解决方法,我为此全局添加了可见性(“隐藏”)选项以避免导出。并提出了一张票,让所有符号默认隐藏,所以以后不应该发生这样的碰撞,但我仍然想知道为什么会发生这种情况而不应该发生。

编辑2:

来源示例:

commonvar.h:

#pragma once

#include <iostream>

struct A
{
    A()
    {
        std::cout << "A inited. Address: " << this << "\n";
    }
    virtual ~A() {}
};

extern A object;

struct POD
{
    int x, y, z;
};

extern POD pod;

commonvar.cpp:

#include <string>
#include "commonvar.h"

A object;

POD pod = {1, 2, 3};

啊哈:

#pragma once

extern "C" void foo();

a.cpp:

#include <iostream>
#include "commonvar.h"

using FnFoo = void (*)();

extern "C" void foo()
{
    std::cout << "A called.\n";
    std::cout << "A: Address of foo  is: " << &object << "\n";
    std::cout << "A: Address of pod  is: " << &pod << "\n";
    std::cout << "A: {" << pod.x << ", " << pod.y << ", " << pod.z << "}\n";

    pod.x = 42;
}

b.cpp:

#include <iostream>
#include <string>
#include "commonvar.h"

extern "C" void foo()
{
    std::cout << "B called.\n";
    std::cout << "B: Address of foo  is: " << &object << "\n";
    std::cout << "B: Address of pod  is: " << &pod << "\n";
    std::cout << "B: {" << pod.x << ", " << pod.y << ", " << pod.z << "}\n";
}

main.cpp:

#include <dlfcn.h>
#include <iostream>
#include <cassert>

#include "a.h"

using FnFoo = void (*)();

int main()
{
    std::cout << "Start of program.\n";
    foo();

    std::cout << "Loading B\n";
    void *b = dlopen("libb.so", RTLD_NOW);
    assert(b);
    FnFoo fnB;
    fnB = FnFoo(dlsym(b, "foo"));
    assert(fnB);

    fnB();
}

构建脚本:

#!/bin/bash

g++ -fPIC -c commonvar.cpp
ar rcs common.a commonvar.o
g++ -fPIC -shared a.cpp common.a -o liba.so
g++ -fPIC -shared b.cpp common.a -o libb.so
g++ main.cpp liba.so -ldl -o main

main的动态符号:

                U __assert_fail
0000000000202010 B __bss_start
                 U __cxa_atexit
                 w __cxa_finalize
                 U dlopen
                 U dlsym
0000000000202010 D _edata
0000000000202138 B _end
0000000000000bc4 T _fini
                 U foo
                 w __gmon_start__
0000000000000860 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 U __libc_start_main
                 U _ZNSt8ios_base4InitC1Ev
                 U _ZNSt8ios_base4InitD1Ev
0000000000202020 B _ZSt4cout
                 U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc

liba.so 的动态符号:

0000000000202064 B __bss_start
                 U __cxa_atexit
                 w __cxa_finalize
0000000000202064 D _edata
0000000000202080 B _end
0000000000000e6c T _fini
0000000000000bba T foo
                 w __gmon_start__
0000000000000a30 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000202070 B object
0000000000202058 D pod
                 U _ZdlPvm
0000000000000dca W _ZN1AC1Ev
0000000000000dca W _ZN1AC2Ev
0000000000000e40 W _ZN1AD0Ev
0000000000000e22 W _ZN1AD1Ev
0000000000000e22 W _ZN1AD2Ev
                 U _ZNSolsEi
                 U _ZNSolsEPKv
                 U _ZNSt8ios_base4InitC1Ev
                 U _ZNSt8ios_base4InitD1Ev
                 U _ZSt4cout
                 U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
0000000000201dd0 V _ZTI1A
0000000000000ed5 V _ZTS1A
0000000000201db0 V _ZTV1A
                 U _ZTVN10__cxxabiv117__class_type_infoE

libb.so 的动态符号:

$ nm -D libb.so
0000000000202064 B __bss_start
                 U __cxa_atexit
                 w __cxa_finalize
0000000000202064 D _edata
0000000000202080 B _end
0000000000000e60 T _fini
0000000000000bba T foo
                 w __gmon_start__
0000000000000a30 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000202070 B object
0000000000202058 D pod
                 U _ZdlPvm
0000000000000dbe W _ZN1AC1Ev
0000000000000dbe W _ZN1AC2Ev
0000000000000e34 W _ZN1AD0Ev
0000000000000e16 W _ZN1AD1Ev
0000000000000e16 W _ZN1AD2Ev
                 U _ZNSolsEi
                 U _ZNSolsEPKv
                 U _ZNSt8ios_base4InitC1Ev
                 U _ZNSt8ios_base4InitD1Ev
                 U _ZSt4cout
                 U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
0000000000201dd0 V _ZTI1A
0000000000000ec9 V _ZTS1A
0000000000201db0 V _ZTV1A
                 U _ZTVN10__cxxabiv117__class_type_infoE

输出:

A inited. Address: 0x7efd6cf97070
Start of program.
A called.
A: Address of foo  is: 0x7efd6cf97070
A: Address of pod  is: 0x7efd6cf97058
A: {1, 2, 3}
Loading B
A inited. Address: 0x7efd6cf97070
B called.
B: Address of foo  is: 0x7efd6cf97070
B: Address of pod  is: 0x7efd6cf97058
B: {42, 2, 3}

可以看出变量的地址会发生冲突,但函数的地址不会。

此外,C++ 初始化是特殊的:聚合 pod 变量仅在您看到对 foo() 的调用修改它时才被初始化,但是当 B 被加载时,它不会重新初始化它,而是调用构造函数加载 libb.so 时的完整对象。

【问题讨论】:

  • 您确定主应用程序没有使用相同的符号吗?
  • @n.m.是的……但有趣的是,当我将变量包装在boost::synchronized_value 中以确保线程安全时,问题就开始出现了。在此之前,它曾经是一个普通的结构。 C++ 的初始化东西在这里也能起作用吗?
  • 好吧,看起来你的两个库都在使用来自可执行位的僵硬,我不知道它什么时候发生,什么时候没有。我现在有一个悬而未决的问题stackoverflow.com/questions/58174403/…
  • 用源示例编辑了问题。

标签: linux gcc shared-libraries dynamic-linking


【解决方案1】:

回答这个问题的关键是主可执行文件是否在其动态符号表中导出相同的符号。也就是说,输出是什么:

nm -D a.out | grep ' mangled_name_of_the_symbol'

如果输出为空,则两个库确实应该使用单独的(它们自己的)符号副本。但是如果输出 not 为空,那么两个库都应该重用主二进制文件中定义的符号(这是因为 UNIX 动态链接试图模拟如果一切都发生的话会发生什么被静态链接到主二进制文件中——UNIX 对共享库的支持发生在 UNIX 本身流行很久之后,在这种情况下,这个设计决定是有意义的)。

演示:

// main.c
#include <assert.h>
#include <dlfcn.h>
#include <stdio.h>

int foo = 12;

int main()
{
  printf("main: &foo = %p, foo = %d\n", &foo, foo);
  void *h = dlopen("./foo.so", RTLD_NOW);
  assert (h != NULL);
  void (*fn)(void) = (void (*)()) dlsym(h, "fn");
  fn();

  return 0;
}
// foo.c
#include <assert.h>
#include <dlfcn.h>
#include <stdio.h>

int foo = 42;

void fn()
{
  printf("foo:  &foo = %p, foo = %d\n", &foo, foo);
  void *h = dlopen("./bar.so", RTLD_NOW);
  assert (h != NULL);

  void (*fn)(void) = (void (*)()) dlsym(h, "fn");
  fn();
}
// bar.c
#include <stdio.h>

int foo = 24;

void fn()
{
  printf("bar:  &foo = %p, foo = %d\n", &foo, foo);
}

构建这个:

gcc -fPIC -shared -o foo.so foo.c && gcc -fPIC -shared -o bar.so bar.c &&
gcc main.c -ldl && ./a.out

输出:

main: &foo = 0x5618f1d61048, foo = 12
foo:  &foo = 0x7faad6955040, foo = 42
bar:  &foo = 0x7faad6950028, foo = 24

现在只使用-rdynamic 重建主二进制文件(这会导致从其中导出foo):gcc main.c -ldl -rdynamic。输出变为:

main: &foo = 0x55ced88f1048, foo = 12
foo:  &foo = 0x55ced88f1048, foo = 12
bar:  &foo = 0x55ced88f1048, foo = 12

附: 通过运行以下命令,您可以深入了解动态链接器的行为:

LD_DEBUG=symbols,bindings ./a.out

更新:

原来我问错了问题...添加了源示例。

如果您查看LD_DEBUG 输出,您会看到:

    165089: symbol=object;  lookup in file=./main [0]
    165089: symbol=object;  lookup in file=./liba.so [0]
    165089: binding file ./liba.so [0] to ./liba.so [0]: normal symbol `object'
    165089: symbol=object;  lookup in file=./main [0]
    165089: symbol=object;  lookup in file=./liba.so [0]
    165089: binding file ./libb.so [0] to ./liba.so [0]: normal symbol `object'

这意味着:liba.so 在全局搜索列表中(由于直接被main 链接到)。这大约相当于完成了dlopen("./liba.so", RTLD_GLOBAL)

因此,其中的符号可供随后加载的共享库绑定到,这正是动态加载器所做的。

【讨论】:

  • 原来我问错了问题。 libA.so 没有被释放(它由 -l 链接),只有 libB.so 被 libA 释放。这将如何影响链接?
  • 此外,可执行文件没有通过 -rdynamic 链接,但符号仍然由 nm -D 列出。
  • 如果我没有在应用程序中定义符号,而是用-l链接第一个库,那么符号冲突仍然会发生。
  • 添加源示例。
  • 同时我也通过仔细阅读 dlopen 的文档发现了这一点。
【解决方案2】:

此问题的一个可能解决方案是使用 dlopen 的 RTLD_DEEPBIND 标志(但是,它是特定于 Linux 的,不是 POSIX 标准),这将使加载的库在运行之前尝试针对自身(及其自身的依赖项)解析符号通过全局范围内的那些。

要使其正常工作,必须使用-fPIE 构建可执行文件,否则libstdc++ 所做的一些违反ODR 假设可能会导致段错误(或者,如果iostream 被替换为cstdio,它可以在没有-fPIE)。

【讨论】:

  • 还有dlmopen
猜你喜欢
  • 1970-01-01
  • 2012-02-02
  • 2019-12-29
  • 1970-01-01
  • 2014-06-27
  • 2013-05-16
  • 2022-10-12
  • 2012-01-11
  • 2015-02-05
相关资源
最近更新 更多