为什么 dlopen 会重用先前加载的符号的地址?

Why would dlopen reuse the address of a previously loaded symbol?

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

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

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

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

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

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

阅读 dlopen's documentation 它说:

RTLD_GLOBAL

The symbols defined by this library will be made available for symbol resolution of subsequently loaded libraries.

RTLD_LOCAL

This is the converse of RTLD_GLOBAL, and the default if neither flag is specified. Symbols defined in this library are not made available to resolve references in subsequently loaded libraries.

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

作为解决方法,我向这个全局添加了 visibility("hidden") 选项以避免导出。并提出了一个让所有符号默认隐藏的票,所以以后不应该发生这样的碰撞,但我仍然想知道为什么在不应该发生的时候发生这种情况。

编辑 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};

a.h:

#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

主要的动态符号:

                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 时调用完整对象的构造函数。

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

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

P.S。 您可以通过 运行:

深入了解动态链接器的行为
LD_DEBUG=symbols,bindings ./a.out

更新:

It turns out I asked a wrong question ... Added source example.

如果您查看 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).

随后加载的共享库可以绑定其中的符号,这应该不足为奇,这正是动态加载程序所做的

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

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