【发布时间】:2021-06-29 08:12:43
【问题描述】:
概述
我有一个动态加载不同插件的core。我明确地实例化我导出的模板,然后在将插件链接到core 的某个版本时使用它们。我的期望是,当我添加新类型和模板实例时,它不会破坏 ABI,并且不必重新链接现有插件。
事实证明并非如此。当我添加新的实例化时,我会得到从不同(而不是在core)地址中执行的符号。
示例
项目仓库可以在here找到。
这是代码示例。
CMakeLists.txt
cmake_minimum_required(VERSION 3.7.2)
project(template_abi_test)
set(CMAKE_CXX_STANDARD 17)
# old core
add_executable(core core.cpp)
target_compile_definitions(core
PRIVATE BUILD_DLL
PUBLIC DYN_LINK
)
set_target_properties(core PROPERTIES ENABLE_EXPORTS 1)
# new core
add_executable(core_new core.cpp)
target_compile_definitions(core_new
PRIVATE BUILD_DLL NEW_VERSION
PUBLIC DYN_LINK
)
set_target_properties(core_new PROPERTIES ENABLE_EXPORTS 1)
# plugin
add_library(plugin SHARED plugin.cpp)
target_link_libraries(plugin PRIVATE core)
core.hpp
#pragma once
#if defined(_WIN32)
#if defined(DYN_LINK)
#if defined(CORE_SOURCE)
#define CORE_DECL __declspec(dllexport)
#else
#define CORE_DECL __declspec(dllimport)
#endif
#endif
#endif
#ifndef CORE_DECL
#define CORE_DECL
#endif
CORE_DECL int non_template();
template<typename>
struct foo_template {
CORE_DECL static int get();
};
core_impl.ipp // 这是 MinGW64 的解决方法。否则它不会为类内实现的函数导出符号。
#pragma once
#define CORE_SOURCE
#include "core.hpp"
template<typename T>
int foo_template<T>::get()
{
static int val = 0;
return ++val;
}
export_types.hpp - 此文件包含链接到核心“旧”版本的插件的导出类型
#pragma once
#include "core.hpp"
struct old{};
extern template struct foo_template<old>;
插件.cpp
#include "core.hpp"
#include "export_types.hpp"
#ifdef _WIN32
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT
#endif
extern "C"
{
DLL_EXPORT int foo_class_template();
}
int foo_class_template()
{
return foo_template<old>::get();
}
core.cpp
#include "core_impl.ipp"
#include "core.hpp"
#include "export_types.hpp"
template struct foo_template<old>;
#ifdef NEXT_VERSION
struct new_0 {
};
template struct foo_template<new_0>;
#endif
#include <filesystem>
#include <iostream>
#include <string_view>
#include <vector>
#include <Windows.h>
int main(int argc, char **argv)
{
// preincrement all the values
int fooClassTemplate = foo_template<old>::get();
auto plugin = LoadLibraryA("plugin.dll");
auto pFooClassTemplate = reinterpret_cast<int(*)()>(GetProcAddress(plugin, "foo_class_template"));
int pluginFooClassTemplate = pFooClassTemplate();
std::cout << fooClassTemplate + 1 << " : " << pluginFooClassTemplate << "\n";
return 0;
}
因此,当执行使用 MSVC、MinGW64 或 Clang 编译的旧版本(在 Windows 上,尚未在 Ubuntu 上检查)时,输出如预期:
2 : 2
但是当我运行 core_new 加载相同的插件时,该插件链接到旧版本的 core,结果不同:
2 : 1
确实,如果我们检查从core 和plugin 中调用的函数的地址,我们会发现它们是不同的,尽管它们似乎位于core.exe 中
// code executed within main
(gdb) info sym 0x00007ff67da46be0
foo_template<old>::get() in section .text of C:\dev\builds\template_abi_test\Debug-MinGW-w64\core_new.exe
(gdb) info sym 0x00007ff67da4a0e0
foo_template<old>::get()::val in section .data of C:\dev\builds\template_abi_test\Debug-MinGW-w64\core_new.exe
// code executed within plugin's functions
(gdb) info sym 0x00007ff6c5cc6be0
foo_template<old>::get() in section .text of C:\dev\builds\template_abi_test\Debug-MinGW-w64\core.exe
(gdb) info sym 0x00007ff6c5cca0e0
foo_template<old>::get()::val in section .data of C:\dev\builds\template_abi_test\Debug-MinGW-w64\core.exe
总结起来就是一张表:
| core address | plugin address | |
|---|---|---|
| foo_template::get | 0x00007ff67da46be0 | 0x00007ff6c5cc6be0 |
| foo_template::get::val | 0x00007ff67da4a0e0 | 0x00007ff6c5cca0e0 |
由于地址位于可执行文件中,我认为原因是符号地址,因此一开始我是在比较符号表。
为什么会这样?可以避免吗?
这是原文。我认为符号地址的差异是原因,但正如 cmets 中的人所指出的,符号地址并不重要。所以这是一个错误的方向。
考虑以下动态库的示例代码:
template <typename>
struct foo_template
{
static inline int get()
{
static int val = 0;
return ++val;
}
};
struct foo
{
template <typename>
static inline int get()
{
static int val = 0;
return ++val;
}
};
template <typename>
inline int get()
{
static int val = 0;
return ++val;
}
struct old{};
template struct foo_template<old>;
template int foo::get<old>();
template int get<old>();
#ifdef NEXT_VERSION
struct new_0{};
template struct foo_template<new_0>;
template int foo::get<new_0>();
template int get<new_0>();
struct new_1{};
template struct foo_template<new_1>;
template int foo::get<new_1>();
template int get<new_1>();
struct new_2{};
template struct foo_template<new_2>;
template int foo::get<new_2>();
template int get<new_2>();
struct new_3{};
template struct foo_template<new_3>;
template int foo::get<new_3>();
template int get<new_3>();
#endif
在新版本中,当添加新模板实例时,ABI 会因类模板和模板成员函数而损坏,而现有代码则完好无损。
新的实例化以严格的顺序添加 - 仅在现有实例化之后。
This 是通过objdump -t 在“旧”和“下”版本之间的输出比较。
可以看出,只有函数模板符号int get没有改变:
// Old
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 90](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x00000000000012d0 _Z3getI3oldEiv
[ 91](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000070 .data$_ZZ3getI3oldEivE3val
// New
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 90](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x00000000000012d0 _Z3getI3oldEiv
[ 91](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000070 .data$_ZZ3getI3oldEivE3val
但是类模板和模板静态成员函数改变了地址:
// Old
File
[ 77](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000000012f0 .text$_ZN12foo_templateI3oldE3getEv
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 79](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 1) 0x00000000000012f0 _ZN12foo_templateI3oldE3getEv
AUX tagndx 0 ttlsiz 0x0 lnnos 0 next 0
[ 81](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000080 .data$_ZZN12foo_templateI3oldE3getEvE3val
AUX scnlen 0x4 nreloc 0 nlnno 0 checksum 0x0 assoc 0 comdat 3
[ 83](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000001310 .text$_ZN3foo3getI3oldEEiv
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 85](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x0000000000001310 _ZN3foo3getI3oldEEiv
[ 86](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000090 .data$_ZZN3foo3getI3oldEEivE3val
// New
File
[ 77](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000001370 .text$_ZN12foo_templateI3oldE3getEv
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 79](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 1) 0x0000000000001370 _ZN12foo_templateI3oldE3getEv
AUX tagndx 0 ttlsiz 0x0 lnnos 0 next 0
[ 81](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000000000c0 .data$_ZZN12foo_templateI3oldE3getEvE3val
AUX scnlen 0x4 nreloc 0 nlnno 0 checksum 0x0 assoc 0 comdat 3
[ 83](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000001410 .text$_ZN3foo3getI3oldEEiv
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 85](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x0000000000001410 _ZN3foo3getI3oldEEiv
[ 86](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000110 .data$_ZZN3foo3getI3oldEEivE3val
【问题讨论】:
-
更改符号的位置不会破坏 ABI,如果这样做就不可能保持 ABI 兼容性
-
为什么要依赖共享库中的符号地址??
-
@rustyx 试图找出原因。我要修改问题。
-
请提供minimal reproducible example(关注最小化)
-
@AlanBirtles 不幸的是,这是我可以做到的最小限度,以便 能够重现错误行为。我只能删除旧文本,但这会使您的答案脱离上下文
标签: c++ templates dll plugins abi