首页 > 解决方案 > 为什么 dlopen 会重用先前加载的符号的地址?

问题描述

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

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

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

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

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

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

阅读dlopen 的文档,它说:

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();

一个.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";
}

主.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.xml 调用完整对象的构造函数。如此加载。

标签: linuxgccshared-librariesdynamic-linking

解决方案


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

nm -D a.out | grep ' mangled_name_of_the_symbol'

如果输出为空,则这两个库确实应该使用单独的(它们自己的)符号副本。但是如果输出为空,那么两个库都应该重用在主二进制文件中定义的符号(这是因为 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

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

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在全局搜索列表中(由于直接链接到 by main)。这大约相当于已经完成dlopen("./liba.so", RTLD_GLOBAL)

毫不奇怪,其中的符号可用于随后加载的共享库绑定,这正是动态加载器所做的。


推荐阅读