C/C++ 多动态库标识符冲突

场景:
最近在写一个程序的时候,需要调A、B两个服务的客户端动态库,两个客户端动态库中函数有重名(更确切的是重定义)的情况。现象,单写A客户端demo程序运行正常,但集成到程序中死活连不上A服务。


原因:
A、B两个客户端动态库中函数有重定义现象。即都有funcX(int arg)(虽然funcX不是对外接口),但执行过程中A动态库调用了B的funcX,导致程序不正常。而单写A的客户端demo时只用A的动态库,A动态库还是用的A中的funcX,程序正常。

简单来说就是违反了单一定义原则One Definition Rule)。

简单示例说明

fun1.h

#include <stdio.h>
int print1();

fun1.cpp

#include "fun1.h"
int printfun()
{
    printf("1\n");
}
int print1()
{
    printfun();
    return 0;
}

fun2.h

#include <stdio.h>
int print2();

fun2.cpp

#include "fun2.h"
int printfun()
{
    printf("2\n");
}
int print2()
{
    printfun();
    return 0;
}

测试程序test.cpp

#include "fun1.h"
#include "fun2.h"

int main()
{
    print1();
    print2();
    return 0;
}

Makefile

test: libfun1.so libfun2.so
    g++ test.cpp -o test -L ./ -lfun1 -lfun2
libfun1.so:
    g++ fun1.cpp -shared -fpic -o libfun1.so
libfun2.so:
    g++ fun2.cpp -shared -fpic -o libfun2.so
clean:
    rm  -rf libfun1.so libfun2.so test

执行结果

可以看到我们本意是希望一个打印1一个打印2,结果全部打印的1

解决方案1(-Wl,-Bsymbolic )(不推荐)

创建动态链接库时,添加编译选项-Wl,-Bsymbolic,其中Wl表示将紧跟其后的参数,传递给连接器ld。首先看一下-Bsymbolic,    ld的man手册(意思就是优先使用链接库内的定义实现)

-Bsymbolic
When creating a shared library, bind references to global symbols to the definition within the shared library, if any.  Normally, it is possible for a program linked against a shared library to override the definition within the shared library.  This option can also be used with the –export-dynamic option, when creating a position independent executable, to bind references to global symbols to the definition within the executable.  This option is only meaningful on ELF platforms which support shared libraries and position independent executables.

修改Makefile

test: libfun1.so libfun2.so
    g++ test.cpp -o test -L ./ -lfun1 -lfun2
libfun1.so:
    g++ fun1.cpp -shared -fpic -o libfun1.so -Wl,-Bsymbolic
libfun2.so:
    g++ fun2.cpp -shared -fpic -o libfun2.so -Wl,-Bsymbolic
clean:
    rm  -rf libfun1.so libfun2.so test

方案一:执行结果


解决方案2( -fvisibility=hidden )

创建动态库的时,添加编译选项-fvisibility=hidden
首先看一下  -fvisibility=default|internal|hidden|protected,gcc的man手册.

-fvisibility=default|internal|hidden|protected

Set the default ELF image symbol visibility to the specified option—all symbols are marked with this unless overridden within the code. Using this feature can very substantially improve linking and load times of shared object libraries, produce more optimized code, provide near-perfect API export and prevent symbol clashes(防止标志符冲突).  It is strongly recommended that you use this in any shared objects you distribute(强烈建议使用在你写的共享库).

修改头文件
fun1.h

#include <stdio.h>
__attribute__ ((visibility("default"))) int print1();

fun2.h

#include <stdio.h>
__attribute__ ((visibility("default"))) int print2();

修改Makefile

test: libfun1.so libfun2.so
    g++ test.cpp -o test -L ./ -lfun1 -lfun2
libfun1.so:
    g++ fun1.cpp -shared -fpic -o libfun1.so -fvisibility=hidden
libfun2.so:
    g++ fun2.cpp -shared -fpic -o libfun2.so -fvisibility=hidden
clean:
    rm  -rf libfun1.so libfun2.so test

执行结果

解决方案3( 命名空间 )

严格使用命名空间。

fun1.h

#include <stdio.h>
namespace fun1 {
    
int print1();

}

fun1.cpp

#include "fun1.h"
namespace fun1 {
int printfun()
{
    printf("1\n");
}
int print1()
{
    printfun();
    return 0;
}
}

fun2.h

#include <stdio.h>
namespace fun2 {
    
int print2();

}

fun2.cpp

#include "fun2.h"
namespace fun2 {
int printfun()
{
    printf("2\n");
}
int print2()
{
    printfun();
    return 0;
}
}

test.cpp

#include "fun1.h"
#include "fun2.h"

int main()
{
    fun1::print1();
    fun2::print2();
    return 0;
}

Makefile

test: libfun1.so libfun2.so
    g++ test.cpp -o test -L ./ -lfun1 -lfun2
libfun1.so:
    g++ fun1.cpp -shared -fpic -o libfun1.so
libfun2.so:
    g++ fun2.cpp -shared -fpic -o libfun2.so
clean:
    rm  -rf libfun1.so libfun2.so test

执行结果

范例(leveldb)

项目中CMakeLists.txt片段,当编成动态库时添加编译选项-fvisibility=hidden

if(BUILD_SHARED_LIBS)
  # Only export LEVELDB_EXPORT symbols from the shared library.
  add_compile_options(-fvisibility=hidden)
endif(BUILD_SHARED_LIBS)

头文件export.h

// Copyright (c) 2017 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.

#ifndef STORAGE_LEVELDB_INCLUDE_EXPORT_H_
#define STORAGE_LEVELDB_INCLUDE_EXPORT_H_

#if !defined(LEVELDB_EXPORT)

#if defined(LEVELDB_SHARED_LIBRARY)
#if defined(_WIN32)
//Windows 系列
#if defined(LEVELDB_COMPILE_LIBRARY)
#define LEVELDB_EXPORT __declspec(dllexport)
#else
#define LEVELDB_EXPORT __declspec(dllimport)
#endif  // defined(LEVELDB_COMPILE_LIBRARY)

#else  // defined(_WIN32)
//Linux gcc系列
#if defined(LEVELDB_COMPILE_LIBRARY)
#define LEVELDB_EXPORT __attribute__((visibility("default")))
#else
#define LEVELDB_EXPORT
#endif
#endif  // defined(_WIN32)

#else  // defined(LEVELDB_SHARED_LIBRARY)
#define LEVELDB_EXPORT
#endif

#endif  // !defined(LEVELDB_EXPORT)

#endif  // STORAGE_LEVELDB_INCLUDE_EXPORT_H_

头文件db.h片段

...
namespace leveldb {
...
class LEVELDB_EXPORT DB {
...
};
...
}

总结

不推荐使用第一种做法的原因:你是首先用自己定义的标志符,但别人不一定这么做了,有可能其他的动态库没有添加优先使用本动态库标志符的选项。

自己提供动态库时,规范做法应该如leveldb项目,严格使用命名空间,并且使用-fvisibility=hidden,只导出要导出的定义,其他的隐藏。

此条目发表在C/C++, 编译分类目录,贴了, , , 标签。将固定链接加入收藏夹。

发表评论

您的电子邮箱地址不会被公开。