解决动态库链接过程中的符号冲突问题

3,552 阅读2分钟

动态库中函数调用

环境:

  • clang version 12.0.1
  • cmake version 3.21.2
  • g++ (GCC) 11.1.0

包含两个相同函数的库链接冲突

试想一下,在你的主函数中调用了一个函数,但是这个函数可以在不同的库中都有实现,那主函数中调用的应该是哪一个呢?

link.drawio.png 我们来试验一下,构建测试代码:

.
├── CMakeLists.txt
├── include
│   ├── second.h
│   └── work.h
├── main.cc
└── src
   ├── CMakeLists.txt
   ├── second.cc
   └── work.cc

./CMakeLists.txt文件内容:

cmake_minimum_required(VERSION 3.14)
project(symbol)
include_directories(include)
add_subdirectory(src)
add_executable(${PROJECT_NAME} main.cc)
target_link_libraries(${PROJECT_NAME} second work)

./include/second.h

#pragma once
#include <iostream>
void SoFunction();
void DoFunction();

./include/work.h

#pragma once
#include <iostream>
void SoFunction();

src/CMakeLists.txt内容:

include_directories(${CMAKE_PROJECT_PATH}/include)
add_library(second SHARED second.cc)
add_library(work SHARED work.cc)

src/second.cc

#include "second.h"
void SoFunction()
{
 std::cout << "conflict function call\n";
}
void DoFunction()
{
 std::cout<<"DoFunction\n";
 SoFunction();
}

src/work.cc

#include "work.h"
void SoFunction() { std::cout << "Call Sofunctiuon\n"; }

./main.cc

#include <iostream>
void SoFunction();
int main() {
 std::cout<<"1. Main start... \n";
 SoFunction();
 std::cout<<"SoFunction call finished\n";
 return 0;
}

构建运行:mkdir build && pushd build && cmake .. && make && ./symbol && popd && rm -rf build 执行结果如下:

conflict function call
SoFunction call finished

可以看到主函数调用了second.cc里面的SoFunction,因为我们先link的时libsecond.so的动态库,自然后link的libwork.so会被覆盖,如果我们在./CMakeLists.txt中调整link顺序,你将能得到不同的结果.

包含两个相同函数的动态库调用覆盖问题

如果我们在主函数中调用second/DoFunction其会调用SoFunction,那么这时会调用second.ccSoFunction还是调用work.ccSoFunction? 修改main函数进行测试:

#include <iostream>
#include "second.h"
#include "work.h"
int main() {
  std::cout<<"1. Main start... \n";
  DoFunction();
  std::cout<<"DoFunction call finished\n";
  SoFunction();
  return 0;
}


这时候我们不修改link顺序,保持second先link,我们得到下面的结果:

1. Main start... 
Second DoFunction
conflict function call
DoFunction call finished
conflict function call

link.drawio (1).png 不出意外,函数调用顺序为:main.cc/DoFunction->second.cc/DoFunction->second.cc/SoFunction->work.cc/SoFunction 现在我们修改./CMakeLists.txt以调整link顺序target_link_libraries(${PROJECT_NAME} work second),重新运行代码,我们将得到下面的结果:

1. Main start... 
Second DoFunction
Call Sofunctiuon
DoFunction call finished
Call Sofunctiuon

link.drawio (2).png 函数的调用顺序为:main.cc/DoFunction->second.cc/DoFunction->work.cc/SoFunction->work.cc/SoFunction,这时因为函数SoFunction因为libwork.so先link,函数使用其的.那如何link先后顺序发生变化的情况下仍然实现同一个库的函数相互调用?

我们先看看两个库中的符号信息:nm -CD src/libwork.so,内容如下:

U __cxa_atexit@GLIBC_2.2.5
                w __cxa_finalize@GLIBC_2.2.5
                w __gmon_start__
                w _ITM_deregisterTMCloneTable
                w _ITM_registerTMCloneTable
0000000000001129 T SoFunction()
                U std::ios_base::Init::Init()@GLIBCXX_3.4
                U std::ios_base::Init::~Init()@GLIBCXX_3.4
                U std::cout@GLIBCXX_3.4
                U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<c
har> >&, char const*)@GLIBCXX_3.4

libsecond.so里面的内容:

U __cxa_atexit@GLIBC_2.2.5
                w __cxa_finalize@GLIBC_2.2.5
                w __gmon_start__
                w _ITM_deregisterTMCloneTable
                w _ITM_registerTMCloneTable
0000000000001159 T DoFunction()
0000000000001139 T SoFunction()
                U std::ios_base::Init::Init()@GLIBCXX_3.4
                U std::ios_base::Init::~Init()@GLIBCXX_3.4
                U std::cout@GLIBCXX_3.4
                U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<c
har> >&, char const*)@GLIBCXX_3.4

解决符号冲突导致的错误调用

隐藏符号

我们可以通过设置invisiable让动态库的符号是否对外可见,例如我们可以让libwork.so的符号对外不可见,那么我们就可以无论怎样都能调用到second的函数.你可以设置src/CMakeLists.txt的内容如下:

include_directories(${CMAKE_PROJECT_PATH}/include)

if(UNIX AND CMAKE_COMPILER_IS_GNUCC)
    set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
    add_library(work SHARED work.cc)
endif()
add_library(second SHARED second.cc)


上面的编译过程会自动将两个库的符号隐藏(CXX_FLAG在当前CMakeLists.txt一直生效,不能直接设置一个加参数,一个不加),这时候我们看到libwork.so的符号信息,执行上面的编译运行命令的时候会出错,因为找不到符号表DoFunctionSoFunction,从而出现未定义的引用.

                 U __cxa_atexit@GLIBC_2.2.5
                 w __cxa_finalize@GLIBC_2.2.5
                 w __gmon_start__
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 U std::ios_base::Init::Init()@GLIBCXX_3.4
                 U std::ios_base::Init::~Init()@GLIBCXX_3.4
                 U std::cout@GLIBCXX_3.4
                 U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)@GLIBCXX_3.4

这里我们使用手动link生成最后的二进制文件:clang++ main.cc -Iinclude -L. -lsecond -lwork -o symbol 这时候我们执行的时候将屏蔽libwork.so里面的符号,从而实现只调用second.cc里面的SoFunction.结果如下:

1. Main start...  
Second DoFunction
conflict function call
SoFunction call finished
conflict function call

函数代码中禁用

libsecond.so中人为开启SoFunctionDoFunction两个函数对外可见,修改second.cc(单独编译second.cc为libsecond.soclang++ -fvisibility=hidden -Iinclude src/second.cc -fPIC -shared -o libsecond.so)

#include "second.h"
__attribute__ ((visibility ("default")))  void SoFunction()
{
  std::cout << "conflict function call\n";
}
__attribute__ ((visibility ("default"))) void DoFunction()
{
  std::cout<<"Second DoFunction\n";
  SoFunction();
}

然后在src/CMakeLists.txt开启visibility,编译即可正常运行,因为work.cc里面的符号没有对外暴露,所以我们不会调到work.cc里面的SoFunction.

通过文件设置导出函数表

在include/export.symb文件中添加下面信息:

{
global: *SoFunction*;
local: *;
};
  • global:对应的是你想导出的函数
  • local区域代表不想导出的符号,*号表示除了global中的符号全部不导出 手动编译动态库实现只保留SoFunctionclang++ -Wl,--version-script=include/export.symb -s -Iinclude src/second.cc -fPIC -shared -o libsecond.so,此时libsecond.so的内容如下:
U __cxa_atexit@GLIBC_2.2.5
                w __cxa_finalize@GLIBC_2.2.5
                w __gmon_start__
                w _ITM_deregisterTMCloneTable
                w _ITM_registerTMCloneTable
0000000000001190 T DoFunction()
                U std::ios_base::Init::Init()@GLIBCXX_3.4
                U std::ios_base::Init::~Init()@GLIBCXX_3.4
                U std::cout@GLIBCXX_3.4
                U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<c
har> >&, char const*)@GLIBCXX_3.4

输出如下:

1. Main start...  
Second DoFunction
conflict function call
DoFunction call finished
Call Sofunctiuon

这里的输出有点不一样(我们屏蔽了second.cc里面的SoFunction)

参考

部分内容参考了:编译链接时如何解决符号冲突问题