内联静态数据会导致节类型冲突

ere*_*non 16 c++ linker gcc elf

我想将一些用户定义的数据放入自定义部分,以便同时由应用程序和离线分析器读取.假设以下示例:

const int* get_data()
{
  __attribute__((section(".custom")))
  static const int data = 123;

  return & data;
}

inline const int* inline_get_data()
{
  __attribute__((section(".custom")))
  static const int inline_data = 123;

  return & inline_data;
}

int main()
{
  (void) get_data();
  (void) inline_get_data();
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

的价值datainline_data将出现在部分.custom.Clang编译此示例并生成正确的结果,就像MSVC一样,当它__attributes__相应的编译指示替换时.

不幸的是,GCC 5.2给出了以下错误:

error: inline_data causes a section type conflict with data
Run Code Online (Sandbox Code Playgroud)

这个问题归结为一个事实,即这两个变量具有不同的链接(data在一节标记a,的部分inline_data将被标记aG).如果第二个函数没有标记为内联但是模板(GCC 5.2编译它),则GCC 4.9会以相同的方式失败.

如果临时更改了一个部分名称并在生成的程序集中手动修复,GCC 5.2也会编译正常.

这个问题有没有已知的解决方法?我无法控制函数签名,*data变量是由我提供的宏生成的,它们可以出现在任何地方.

Mik*_*han 12

为了一般的好处,我将重申你已经知道的以及@Rumbaruk已经引用的内容:gcc的文档明确地将section属性的应用限制为全局变量.因此,gcc行为的理想解决方法是让gcc不受barf限制,或者在不受支持的gcc特定语言扩展应用程序上发出损坏的代码.我们没有权利期望成功或期望成功始终如一.

这里有一个很长的解释,说明gcc如何以及为什么产生段类型冲突编译错误而clang没有.如果不耐烦,滚动到修复,但不要指望银弹.

出于演示目的,我将使用比您发布的更逼真的程序,即:

source.cpp

const int* get_data()
{
    __attribute__((section(".custom")))
    static const int data = 123;

    return & data;
}

inline const int* inline_get_data()
{
    __attribute__((section(".custom")))
    static const int inline_data = 123;

    return & inline_data;
}

const int* other_get_data()
{
    return inline_get_data();
}
Run Code Online (Sandbox Code Playgroud)

header.h

#ifndef HEADER_H
#define HEADER_H
extern const int* get_data();
extern const int* other_get_data();
#endif
Run Code Online (Sandbox Code Playgroud)

main.cpp中

#include "header.h"
#include <iostream>

int main()
{
    std::cout << (*get_data() + *other_get_data()) << std::endl;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

目前,该程序在使用gcc 5.2编译时重现了部分类型冲突错误:

$ g++-5 -Wall -pedantic -c source.cpp
source.cpp:12:22: error: inline_data causes a section type conflict with data
     static const int inline_data = 123;
                      ^
Run Code Online (Sandbox Code Playgroud)

Clang(3.6/3.7)没有投诉:

$ clang++ -Wall -pedantic -I. -o prog main.cpp source.cpp 
$ ./prog
246
Run Code Online (Sandbox Code Playgroud)

gcc的阻塞性的根源inline_get_data()是具有外部链接内联函数,该外部链接将链接部分归属于与非内联函数 相同的转换单元中的get_data()静态数据,其将相同的链接部分归因于其自己的静态数据.

编译器采用不同的规则分别为get_data() 和生成链接inline_get_data().get_data()是一个简单的案例,inline_get_data() 是棘手的案例.

要看到差距,让我们暂时替换缴械海合会部分冲突"custom""custom.a"get_data()和更换"custom""custom.b"inline_get_data().

现在我们可以source.cpp用gcc 编译并检查相关的符号表条目:

$ objdump -C -t source.o | grep get_data
0000000000000000 l     O .custom.a  0000000000000004 get_data()::data
0000000000000000 l    d  .text._Z15inline_get_datav 0000000000000000 .text._Z15inline_get_datav
0000000000000000 g     F .text  000000000000000b get_data()
0000000000000000 u     O .custom.b  0000000000000004 inline_get_data()::inline_data
0000000000000000  w    F .text._Z15inline_get_datav 000000000000000b inline_get_data()
000000000000000b g     F .text  000000000000000b other_get_data()
Run Code Online (Sandbox Code Playgroud)

get_data()当然,已经制作了一个全局符号(g)并get_data()::data制作了一个局部符号(l).但是inline_get_data()已经成为一个弱的,既不是全局的也不是局部的符号(w)inline_get_data()::inline_data,虽然它在语法上是块范围的静态,但它已经成为一个独特的全局符号(u).这是标准ELF符号绑定的GNU扩展,要求运行时链接程序确保该符号在整个运行时链接中是唯一的.

在这些不同的链接规定中inline_get_data(),gcc正在应对,因为它认为函数与外部链接内联的.函数是内联的这一事实意味着它必须在使用它的每个翻译单元中定义,并且它具有外部链接的事实意味着所有这些定义必须解决相同的问题inline_data()::get_data.因此,为了链接目的,块范围静态变量必须成为公共符号.

从相同的动机,GCC交易不同与归因节custom.a中的设置get_data()和归因节custom.binline_get_data().指定inline_get_data()::inline_data了唯一的全局符号后,它希望确保多个副本与custom.b不同翻译单元的部分的链接不会引入此符号的多个定义.为此,它将GROUPlinker属性应用于custom.b:this(跳过详细信息)使其能够生成.section指定custom.b给命名的section-group的指令,并指示链接器仅保留该节组的一个副本.注意:

$ readelf -t source.o
...
...
[ 7] .custom.a
   PROGBITS               PROGBITS         0000000000000000  0000000000000068  0
   0000000000000004 0000000000000000  0                 4
   [0000000000000002]: ALLOC
[ 8] .custom.b
   PROGBITS               PROGBITS         0000000000000000  000000000000006c  0
   0000000000000004 0000000000000000  0                 4
   [0000000000000202]: ALLOC, GROUP
                              ^^^^^
 ...
 ...
Run Code Online (Sandbox Code Playgroud)

这是款型冲突错误的触发器,当custom.acustom.b 是同一个.Gcc不能创建一个既有又没有GROUP 属性的部分.

现在如果get_data()并且inline_get_data在不同的翻译单元中定义,编译器就不会注意到冲突.那有什么关系呢?那种情况会出什么问题?

一切正常在这种情况下,因为在这种情况下,有无节型冲突.A节custom通过的gcc产生source.o是一个部分 source.o.它必须具有或不具有该GROUP属性,但无论哪种方式都与具有相反状态custom的同名部分没有冲突other_source.o.这些是链接器的不同输入节.它将对ed 的输入custom节进行重复数据删除GROUP,每个group-name只保留其中一个.对于不是的输入custom节,它不会这样做,GROUPed最后它会将custom剩下的所有输入节合并到custom二进制中的一个输出节中,而现在不适用的GROUP属性被抛弃.该输出custom部分将包含get_data()::data本地符号和inline_get_data()::inline_data唯一的全局符号.冲突只包括编译器遇到关于是否source.o(custom)GROUP编辑部分的矛盾规则.

那么为什么不谴责同样的矛盾呢?这是因为clang对包含静态数据的外部链接内联函数问题采用了更简单但不太稳健的方法.

与部分分化坚持custom.acustom.b,现在让我们编译source.cpp铿锵并检查相应的符号和部分特点:

$ objdump -C -t source.o | grep get_data
0000000000000000 l     O .custom.a  0000000000000004 get_data()::data
0000000000000000 l    d  .text._Z15inline_get_datav 0000000000000000 .text._Z15inline_get_datav
0000000000000010 g     F .text  000000000000000b other_get_data()
0000000000000000  w    F .text._Z15inline_get_datav 0000000000000010 inline_get_data()
0000000000000000 g     F .text  0000000000000010 get_data()
0000000000000000  w    O .custom.b  0000000000000004 inline_get_data()::inline_data
Run Code Online (Sandbox Code Playgroud)

与gcc的输出有一点不同.正如我们所预料的那样,clang没有利用GNU特定的符号绑定唯一的全局符号(u)inline_get_data()::inline_data.它使它成为一个弱的象征,就像inline_get_data()它本身一样.

对于我们的部分特征:

$ readelf -t source.o
...
...
[ 8] .custom.a
   PROGBITS               PROGBITS         0000000000000000  0000000000000080  0
   0000000000000004 0000000000000000  0                 4
   [0000000000000002]: ALLOC
[ 9] .custom.b
   PROGBITS               PROGBITS         0000000000000000  0000000000000084  0
   0000000000000004 0000000000000000  0                 4
   [0000000000000002]: ALLOC
...
...
Run Code Online (Sandbox Code Playgroud)

没有区别,所以没有冲突.这就是为什么我们可以更换的节名custom.a ,并custom.bcustom每原创,编译成功.

Clang依赖于它的弱绑定inline_get_data()::inline_data来回答这样的要求,即只有一个这样的符号inline_get_data()被进入链接的每个实现所解决.这使它免于部分类型冲突,但放弃了gcc更复杂方法的连接装甲.

你能告诉 gcc放弃这种强大的功能并采用类似铿锵的方式进行编译 inline_get_data()吗?你可以有点,但还不够.你可以给gcc一个选项 -fno-gnu-unique来指示编译器忘记GNU-specfic 唯一的全局 符号绑定.如果你这样做,那么它会形成inline_get_data()::inline_data 一个弱的象征,如铿锵; 但是这不会轻推它 - 也许它应该 - 删除符号的属性部分的部分分组链接,你仍然会得到部分类型冲突.我找不到任何选项来禁止这个非常严重的代码生成行为,因为你确实有臭味的问题代码.

修复

我们已经看到gcc部分类型冲突是如何以及为什么由两个函数的定义在同一个翻译单元中的存在而特别激发,一个内联外部链接,另一个不内联,每个都将相同的链接部分归属于它的静态数据.

我可以建议两种补救措施,其中一种简单而安全,但仅适用于问题的一种变体,另一种适用于总是,但是极端和绝望.

简单安全的一个

冲突函数定义可以通过两种方式进入同一个翻译单元: -

  1. 它们都在同一个source(.cpp)文件中定义.
  2. 非内联函数在源文件中定义,该源文件包括定义内联函数的头.

如果您有类型1的情况,那么对于使用外部链接编码源文件以编写内联函数的人来说,这只是一个蠢事.在这种情况下,内联函数是其翻译单元的本地函数,应该是static.如果它被创建,static那么gcc的外部链接努力消失,并且部分类型与它们冲突.你已经说过你无法控制你的属性部分内容被宏注入的代码,但它的作者应该接受这样的事实,即在源文件而不是标题中编写内联外部函数是一个错误,并愿意纠正它.

绝望的绝望之一

2型病例更有可能发生.对于这些,据我所知,你的唯一希望是在你的gcc构建中注入一个程序集hack,以便.section在目标外部函数定义中关于属性部分的gcc 指令以编程方式编辑为在目标代码之前是clang-like产生.

显然,这样的解决方案只适用于某些gcc版本,你知道这些版本可以生成"正确的错误.section指令模式",以便针对您的纠正黑客进行攻击,并且使用它的构建系统应该对操作gcc进行健全性检查版本提前.

一个必要的初步是修改您的宏产生的custom部分属性,以便代替均匀发热部分名称.custom它,而不是生成的序列.custom.1,custom.2,...,custom.N在连续调用在翻译单元.使用内置的预处理器__COUNTER__来执行此操作,例如

#define CAT2(x,y) x##y
#define CONCAT(x,y) CAT2(x,y)
#define QUOT(x) #x
#define QUOTE(x) QUOT(x) 
#define SET_SECT() __attribute__((section(QUOTE(CONCAT(.custom.,__COUNTER__)))))
Run Code Online (Sandbox Code Playgroud)

这一点只是为了让gcc预处理代码如下:

const int* get_data()
{
    SET_SECT()
    static const int data = 123;

    return & data;
}

inline const int* inline_get_data()
{
    SET_SECT()
    static const int inline_data = 123;

    return & inline_data;
}
Run Code Online (Sandbox Code Playgroud)

代码如下:

const int* get_data()
{

    __attribute__((section(".custom.0")))
    static const int data = 123;

    return & data;
}

inline const int* inline_get_data()
{

    __attribute__((section(".custom.1")))
    static const int inline_data = 123;

    return & inline_data;
}
Run Code Online (Sandbox Code Playgroud)

这不会引发部分类型的冲突.

有了这个并应用于source.cpp,您可以使用gcc组装文件:

g++ -S source.cpp
Run Code Online (Sandbox Code Playgroud)

并在输出source.s中观察到无问题部分custom.0 得到.section指令:

.section    .custom.0,"a",@progbits
Run Code Online (Sandbox Code Playgroud)

而有问题的部分custom.1得到:

.section    .custom.1,"aG",@progbits,_ZZ15inline_get_datavE11inline_data,comdat
Run Code Online (Sandbox Code Playgroud)

其中_ZZ15inline_get_datavE11inline_data是节组名称,并comdat 告诉链接器对此节组进行重复数据删除.

用clang重复此操作,并观察相应的指令是:

.section    .custom.0,"a",@progbits
.section    .custom.1,"a",@progbits
Run Code Online (Sandbox Code Playgroud)

除了部分名称之外没有其他区别.

因此,你需要的组装黑客将会转变为:

.section    .custom.0,"a",@progbits
.section    .custom.1,"aG",@progbits,_ZZ15inline_get_datavE11inline_data,comdat
Run Code Online (Sandbox Code Playgroud)

成:

.section    .custom,"a",@progbits
Run Code Online (Sandbox Code Playgroud)

这可以用sed替换来表示:

s|^\t\.section\t\.custom\.[0-9]\{1,\},"a\(G\)*",@progbits.*$|\t\.section\t\.custom,"a",@progbits|g
Run Code Online (Sandbox Code Playgroud)

对于演示程序,假设对宏设备进行了必要的更改,可以在makefile中制定Drastic解决方案,如下所示:

CXX ?= g++
SRCS = main.cpp source.cpp
ASMS = $(SRCS:.cpp=.s)
OBJS = $(SRCS:.cpp=.o)
CPPFLAGS = -I.
CXXFLAGS = -fno-gnu-unique

%.o: %.cpp

%.s: %.cpp
%.s: %.cpp
    $(CXX) $(CPPFLAGS) $(CXXFLAGS) -S -o $@ $<

%.o: %.s    
%.o: %.s
    sed -i 's|^\t\.section\t\.custom\.[0-9]\{1,\},"a\(G\)*",@progbits.*$$|\t\.section\t\.custom,"a",@progbits|g' $<
    $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $<

.PHONY: all clean
.INTERMEDIATE: $(ASMS)

all: prog

prog: $(OBJS)
    $(CXX) -o $@ $^

clean:
    rm -f prog $(OBJS) $(ASMS)
Run Code Online (Sandbox Code Playgroud)

从中./prog可以用gcc构建一个满足246 stdout 打印期望的.

请注意makefile的三个细节: -

  • 我们需要编写空模式规则,比如%.o: %.cpp删除make的内置配方.
  • sed命令中我们需要*$$作为eol标记来逃避扩展 $.
  • -fno-gnu-unique 在编译器标志中传递以填写clang模仿.

这不是我希望暴露给开放用户群的解决方案,除非是止损.如果从所有这些中获取的话,我不会反对:是不是有更好的方法来攻击潜在的问题?

  • @erenon我想在昨晚深夜举行大会,但我担心我不会用它来打败时钟.好你有一个不那么肮脏的修复.干杯. (2认同)