为什么“-fvisibility-inlines-hidden”不是默认选项?

5
我想确认一下我的理解是否正确。 inline是对C++编译器的建议,当它看到更好的替代函数时,就会将其替换,因此从库外部调用标记为内联的过程可能不可靠,它们应该默认隐藏,以防止其他人调用它们,因为编译器或代码库的更新可能会改变决策(因此移除inline函数和ABI破坏?)。
然而,似乎这不是默认设置,需要设置-fvisibility-inlines-hidden才能实现。我在这里问问为什么会这样?不设置有任何真正的用例吗?只是因为遗留原因而存在吗?

4
“inline”在C++中表示建议编译器在看到更好的函数时进行替换...但这已经不再是真实情况了:现在,“inline”只是意味着允许有多个定义。如果你想抑制“inline”定义的外部可见性(链接),你需要声明它们为静态的。请参阅:http://en.cppreference.com/w/cpp/language/inline - Richard Critten
谢谢大家的回答,我已经点赞了所有的回答,并会尝试稍后将得票最高的标记为被接受的答案 :) - Ebrahim Byagowi
1
@EbrahimByagowi 请查看这个问题。它展示了在构建PIC时ELF符号插入的影响。inline的一个关键特性是它是C++代码中避免符号插入开销的标准方式之一。 - user1143634
3个回答

11

它们在逻辑上应该默认为隐藏状态。

C++ 要求所有函数(包括 inline 函数)在所有翻译单元中具有相同的地址。局部静态应该在所有翻译单元中共享。如果程序作为多个共享对象(.so 文件)构建,则使内联函数隐藏会违反这些要求。

内联函数应该具有公共可见性,以便动态链接器可以在运行时从所有现有定义中选择一个定义。

GCC wiki 提到:

-fvisibility-inlines-hidden 可以在没有源更改的情况下使用,除非您需要覆盖对于函数本身或任何函数局部静态数据的地址标识至关重要的内联内容


考虑以下示例。可执行源代码:
// main.cpp
#include <cstdio>

struct A { };

inline A &foo()
{
    static A a;
    return a;
}

int main()
{
    void *p = &foo();
    std::printf("main() - %p\n", p);
}

共享对象源代码:

#include <cstdio>

struct A { };

inline A &foo()
{
    static A a;
    return a;
}

static __attribute__((constructor)) void init()
{
    void *p = &foo();
    std::printf("main() - %p\n", p);
}

如果您同时构建并链接可执行文件到此共享对象,则可以看到foo在两个翻译单元中始终返回相同的地址。

现在,如果您将__attribute__((visibility("hidden")))添加到这些内联函数中,则会发现不同翻译单元中的地址是不同的。

这不是某些C++程序可能期望的结果。


现在大多数人认为inline与实际的函数内联无关。这并不完全正确。ELF目标试图使动态链接透明化,例如,如果将程序构建为单个可执行文件或多个共享对象,则程序应该表现出相同的行为。

为了实现这一点,ELF要求所有具有公共可见性的函数都必须通过GOT或PLT调用,就像是“导入”的函数一样。这是必需的,以便每个函数都可以被另一个库(或可执行文件本身)覆盖。这也禁止了所有公共非内联函数的内联(请参见第3.5.5节here,其中显示PIC中的公共函数调用应通过PLT进行)。

对于公共内联函数,代码内联是可能的,因为inline允许在多个内联函数定义不等效时程序行为未定义。

有趣的是:Clang违反了这个ELF要求,在ELF目标上仍然能够内联公共函数。GCC可以使用-fno-semantic-interposition标志做到同样的事情。


C++要求所有函数(包括内联函数)在所有翻译单元中具有相同的地址,这正是内联的含义,不必发生这种情况。 - UKMonkey
4
不是这样的。内联函数可以被内联(此时该实例没有地址),但获取内联函数的地址应该始终给您相同的结果。 - user1143634
1
根据其他答案,inline并不是关于内联代码的提示;它字面意思是在多个翻译单元中定义。链接器将决定删除重复项,但链接之前的翻译单元中的地址将有所不同。 - UKMonkey
@UKMonkey 当我们在Linux上使用ELF链接多个SO时,与内联相关的问题非常重要。公共的非内联函数根本无法进行内联,因为这违反了整个程序中的ODR。内联允许存在多个等效的定义。如果它们不等效-行为是未定义的。这种未定义的行为使得编译器能够在ELF上内联函数调用。请参阅GCC中的-fno-semantic-interposition标志。 - user1143634
公共的非内联函数根本不能被内联;正如你所说,它们变成了不是函数,保持ODR。你谈论的是编译器的一个特性,他们选择遵守提示而不是执行自己的优化;既然标准说优化是可选的,那就没有问题。同样地,即使标记为内联,编译器也没有义务内联任何函数。正是标准已经将所有内容都变成了可选项,这才改变了内联的含义。 - UKMonkey
3
@UKMonkey,你从来没有在构建“纯C++程序”,而是在为具有ELF可执行文件格式的POSIX系统构建C++程序。 ELF要求通过PLT调用公共函数。一些编译器(如Clang)选择忽略此要求,而另一些编译器(如GCC)选择遵循它。如果您想编写C++程序,您必须了解这些内容。 C++标准并不是万能的,在确定如何构建程序方面,其权力最小。像POSIX这样的标准可以在必要时扩展和覆盖C++标准。 - user1143634

2

inline是建议C++编译器在看到更好的函数时替换函数的内容。

不,这最初可能是这样,大约在90年代末,但已经很久没有这种情况了。

请参见此答案以获得良好的解释。

因此,从库外部调用被标记为内联的过程可能不可靠。

  1. 你最初的假设已经是错误的,因此“因此”是基于错误前提进行的。
  2. 即使编译器在有或没有inline关键字的情况下都可以内联调用(call),这是在特定的调用点完成的。内联不是发生在一个函数上,而是发生在函数调用上。

    编译器完全可以内联一些对函数的调用,而忽略其他调用,这取决于编译器对在调用点产生最佳代码的看法。

现在,不清楚你认为inline在库中引起了什么问题,因此很难直接解决。


“inline” 仍然与 ELF 目标上的内联相关,在没有 “inline” 关键字的情况下,内联是 禁止 的。 - user1143634
现代编译器实际上会将 inline 视为内联提示,并考虑其主要含义。 - M.M
@Ivan - 你在其他评论中提供的问题链接展示了clang内联一个未使用inline关键字声明的函数,是吗? - Useless
@Useless 是的,正如我在答案中提到的那样,Clang选择忽略这里的ABI。GCC仍然遵循它。毕竟,标准只是纸张而已。每个人都可以自由地忽略它们。在我看来,clang做得很对。ELF在这里就是有问题。 - user1143634

2
内联是给C++编译器的建议,当它看到更好的时候会替换函数,因此从库外部调用标记为内联的过程不可靠,它们逻辑上应该默认隐藏。
编译器仍然可以决定内联一些调用并将其中一些调用保持不内联。在这种情况下,在您链接在一起的所有库中都会得到多个内联函数的副本。
此外,标准在某种程度上要求&foo在程序中的任何位置都是相同的地址,尽管标准没有关于DSO/DLLs的说明,所以编译器在这方面有一定的自由(实际上,MSVC遵循相反的方法,即默认隐藏所有内容)。
然而,似乎这不是默认设置,应该设置-fvisibility-inlines-hidden来使其发生作用。
尽管名字如此,-fvisibility-inlines-hidden仅影响内联类成员函数。对于其他所有内容,-fvisibility=hidden应该足够了。
没有设置这个标志有任何真正的用例吗?它只是因为遗留原因存在吗?
是的。如果我没记错,该标志是在GCC 3.2中实现的(或者非常接近),将其设为默认值将破坏大量遗留代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接