为什么链接器不能阻止C ++静态初始化命令惨败?

时间:2010-02-20 20:00:56

标签: c++ linker

编辑:将下面的示例更改为实际演示SIOF的示例。

我试图理解这个问题的所有细微之处,因为在我看来,这是语言的一个主要漏洞。我已经读过链接器无法阻止它,但为什么会这样呢?在这种简单的情况下防止这似乎是微不足道的:

// A.h
extern int x;

// A.cpp
#include <cstdlib>

int x = rand();

// B.cpp
#include "A.h"
#include <iostream>

int y = x;

int main()
{
    std::cout << y; // prints the random value (or garbage)?
}

这里,链接器应该能够轻松确定A.cpp的初始化代码应该在链接可执行文件中的B.cpp之前发生,因为B.cpp依赖于A.cpp中定义的符号(显然链接器)已经必须解决这个问题了。)

那么为什么不能将它推广到所有编译单元。如果链接器检测到循环依赖,那么它不能使链接失败(或者可能是警告,因为它可能是程序员的意图,我想在一个编译单元中定义一个全局符号,并在另一个编译单元中初始化它) ?

标准是否对实现有任何要求以确保在简单情况下正确的初始化顺序?什么是无法实现这种情况的例子?

据我所知,在全球销毁时可能发生类似的情况。如果程序员没有仔细确保破坏期间的依赖关系与构造对称,则会出现类似的问题。链接器是否也不会对这种情况发出警告?

7 个答案:

答案 0 :(得分:6)

Linkers传统上只是链接 - 即他们解析地址。您似乎希望他们对代码进行语义分析。但是他们无法访问语义信息 - 只有一堆目标代码。现代链接器至少可以处理大符号名称并丢弃重复符号以使模板更有用,但只要链接器和编译器是独立的,就是这样。当然,如果链接器和编译器都是由同一个团队开发的,并且如果该团队是一个大公司,那么可以在链接器中添加更多的智能,但是很难看出可移植语言的标准如何能够强制执行这样的事情。

如果你想了解更多关于连接子的信息,请看看http://www.iecc.com/linker/ - 关于经常被忽略的工具的唯一一本书。

答案 1 :(得分:3)

理论上,没有什么能阻止链接器处理这个 - 基本上在依赖关系中进行拓扑排序以得出初始化顺序。现有的链接器不会这样做,而C ++主要依赖于现有的链接器......

编辑:从标准的角度来看,这个问题的解决方案是完全无关紧要的:要求所有具有静态存储持续时间的对象在main()开始执行之前初始化。不幸的是,关于所有可能实现的目标是提出另一个几乎没有人符合标准的领域,或者(更糟)甚至有计划这样做。对于它意味着什么,委员会的实施者必须同意,他们将要实施它是非常重要的。

你是对的,很容易环顾四周,看到人们有这个问题。与此同时,我不知道单个供应商似乎认为这是一个真正的问题。他们似乎都没有参与其中。他们都没有安排在未来发布。据我所知,它甚至还没有发布到任何人的“如果有一天我们能够做到这一点会很好”。

这让我们回到了我最初所说的内容:即使它对我们来说可能看起来像一个严重的问题,但它显然 对大多数实施者来说都不是那样。我可以看到许多原因可能如此。首先,当然,C ++不是任何人的企业议程中的关键项目。微软推动.NET。 Sun / Oracle和IBM推动Java。其他人有自己的议程,但 none 他们试图让你使用C ++。在我看来,他们中的大多数人都认为这是一种必要的邪恶,而不是他们真正希望投入任何努力的东西。既然如此,那么完全重新设计其链接器的内容以处理这个特定问题的工作可能只有在他们得到关于它的很多的投诉时才会被考虑。那是两个问题。首先,C ++最初是一个相当小的社区,因此在实施者真正注意到他们所说的任何内容之前,它需要占很大比例。其次,只有相当小比例的C ++程序员无论如何都会遇到问题。关于他们打扰或关心的唯一原因是,如果它成为他们自己的内部发展的问题。不幸的是,大多数人没有理由关心可移植性。

答案 2 :(得分:2)

这是因为静态初始化与运行时初始化完全不同。 x的初始化在您的示例动态中就其性质而言。但它被写为静态初始化。这主要来自与数十年C练习的兼容性。

解析这样一个构造的一种方法是为main()之前运行的每个模块编译初始化代码,就像#pragma startup在某些实现中那样。

但实际上,声明模块多久不知道初始化值是什么?

答案 3 :(得分:1)

在您的简单示例中,一个足够智能的链接器确实可以确定A.o中的初始化需要在B.o中的初始化之前运行,因为B.o指的是在A.o中定义的符号。

但是像你这样简单的例子并没有真正表现出很大的问题,当然也不是“惨败”的问题。这是一个稍微复杂的例子。

// externs.h
extern int a;
extern int b;

// A.cpp
#include "externs.h"

int a = 5;
int aa = b;

// B.cpp
#include "externs.h"
int b = 10;
int bb = a;

标准要求单个编译单元中的变量按声明顺序初始化,因此必须在a之前初始化aa,并在b之前初始化bb,但没有任何进一步的订购要求。允许来自编译单元的初始化与来自其他编译单元的初始化交错。

至少有一个初始化顺序可以确保所有变量在用于初始化其他任何内容之前进行初始化,同时仍遵守标准:

  1. a
  2. b
  3. bb
  4. aa
  5. 链接器关于此程序的信息有限。它知道编译后的文件A.o定义了两个符号aaa,并且它引用了外部符号b。同样,它知道B.o定义bbb并引用外部符号a。这两个目标文件是相互依赖的,因此链接器不能使用它在您的示例中使用的相同技术。在此示例中,需要知道必须仅定义a才能初始化B.o.但是,目标文件中记录的信息并不具体。它不包含符号之间的依赖关系。

答案 4 :(得分:0)

传统链接器不查看源代码甚至AST,现有的目标文件格式提供有关导出和外部符号的相当少的信息。

答案 5 :(得分:0)

虽然链接器也许可以这样做,但是大多数你需要它的例子也是缺乏内聚和高耦合的坏代码的例子(通常是通过全局变量的恐怖)。你的榜样就是这样一个范例。

所以这不是“惨败”;这可能是一个太强烈的描述。它只是对您编码方式的一个小限制。

答案 6 :(得分:0)

任何语言标准都是许多事情之间的妥协。在这种情况下,我们谈论的是易于实现和易用性之间的折衷。如果一种语言难以实现,那么很少或没有符合要求的实现,并且该标准将是无用的。如果它太难使用,没有人会使用它,标准也将毫无用处。

因此,语言标准委员会将试图限制他们对实施的要求,特别是在更常见的系统上。在现代系统中,拥有各种不同的编译器但是共享链接器是很常见的,因此委员会对编译器编写者提出要求会更自由,但对链接器更容易。

C ++函数重载依赖于找到一个技巧,使其适用于链接器(“名称重整”)。 C90标准表示具有外部链接的变量名称必须在前六个字符中是唯一的,而不计算不同的情况。理由(对于1989年的ANSI版本,它是,IIRC,1990年ISO标准下降)说,委员会对保持这种限制非常不满意,但认为放弃它会使得太多地实施标准C太难了具有原始连接子的系统。

这里有一些鸡与蛋的情况,因为语言设计师不愿意对链接器提出要求,因此没有很大的推动连接器的发展,但这就是目前的工作方式。