【问题标题】:Do new template instantiations break ABI?新模板实例化会破坏 ABI 吗?
【发布时间】: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

确实,如果我们检查从coreplugin 中调用的函数的地址,我们会发现它们是不同的,尽管它们似乎位于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


【解决方案1】:

符号按名称链接,因此更改共享库中符号的位置不会更改 ABI。

您的插件链接到core,因此它使用来自core 的符号,core_new 中定义的符号是分开的,因此您违反了 ODR。编译您的仓库,删除core.exe,将core_new.exe 重命名为core.exe,以便只有一份副本,然后运行core.exe 现在输出您的预期值。这些都与 ABI 无关

【讨论】:

  • 嗯,这是有道理的。这意味着这不是我的原因。我会用更合适的例子修改我的问题。
  • @SergeyKolesnik 如果您要彻底改变问题,那么最好提出一个新问题
  • 我不会改变这个问题,因为它描述了我遇到的确切问题。当我添加新的模板实例时,我的代码会中断。我认为这是由于符号地址,但显然,这不是原因。所以我将添加一个重现我的问题的最小示例
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-02-02
  • 2010-11-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-10
相关资源
最近更新 更多