链接器如何处理具有不同链接的变量?

How does linker handle variables with different linkages?

在 C 和 C++ 中,我们可以操纵变量的链接。联动分为三种:不联动、内部联动、外部联动。我的问题可能与为什么将它们称为 "linkage" 有关(这与链接器有何关系)。

我知道链接器能够处理带有外部链接的变量,因为对该变量的引用不限于单个翻译单元,因此不限于单个目标文件。通常在操作系统课程中讨论其实际工作原理。

但是链接器如何处理变量 (1) 没有链接和 (2) 有内部链接?这两种情况有什么区别?

就 C++ 本身而言,这并不重要:唯一重要的是整个系统的行为。没有链接的变量不应链接;具有内部链接的变量不应跨翻译单元链接;具有外部链接的变量应该跨翻译单元链接。 (当然,作为编写 C++ 代码的人,您也必须遵守所有 您的 约束。)

然而,在程序的编译器和链接器套件中,我们当然确实必须关心这一点。达到预期结果的方法取决于我们。一种传统方法非常简单:

  • 没有链接的标识符永远不会传递给链接器。

  • 具有内部链接的标识符也不会传递给链接器,或者传递给链接器但标记为"for use within this one translation unit only"。也就是说,它们没有 .global 声明,或者它们有 .local 声明,或类似的。

  • 带有外部链接的标识符被传递给链接器,如果链接器看到内部链接标识符,这些外部链接符号被不同地标记,例如,有一个 .global 声明或者没有 .local 声明。

如果您有一个 Linux 或类似 Unix 的系统,运行 nm 编译器生成的对象 (.o) 文件。请注意,一些符号用大写字母注释,例如文本和数据的 TD:这些是全局的。其他符号用小写字母注释,如 td:这些是局部的。所以这些系统使用的是"pass internal linkage to the linker, but mark them differently from external linkage"方法。

链接器通常既不参与内部链接也不参与链接——它们完全由编译器解决,然后链接器才开始行动。

内部链接意味着同一翻译单元中不同范围的两个声明可以引用同一事物。

无链接

没有链接意味着同一翻译单元中不同作用域的两个声明不能引用同一事物。

所以,如果我有类似的东西:

int f() { 
    static int x; // no linkage
}

...任何其他范围内的 x 的其他声明都不能引用此 x。链接器仅在一定程度上涉及它通常必须在可执行文件中生成一个字段,告诉它可执行文件所需的静态 space 的大小,并且将包括此变量的 space。由于它永远不会被任何其他声明引用,因此链接器无需参与其中(特别是,链接器与解析名称无关)。

内部联动

内部链接意味着同一翻译单元中不同作用域的声明可以引用同一对象。例如:

static int x;  // a namespace scope, so `x` has internal linkage

int f() { 
    extern int x; // declaration in one scope
}

int g() { 
    extern int x; // declaration in another scope
}

假设我们将这些都放在一个文件中(即它们最终作为一个翻译单元),f()g() 中的声明指的是同一件事——x 在名称space 范围内被定义为 static

例如,考虑这样的代码:

#include <iostream>

static int x; // a namespace scope, so `x` has internal linkage

int f()
{
    extern int x;
    ++x;
}

int g()
{
    extern int x;
    std::cout << x << '\n';
}

int main() {
    g();
    f();
    g();
}

这将打印:

0
1

...因为在 f() 中递增的 x 与在 g() 中打印的 x 相同。

链接器在这里的参与可以(通常是)与无链接情况几乎相同——变量 x 需要一些 space,并且链接器指定 space 当它创建可执行文件时。它确实 而不是 ,但是,需要参与确定当 f()g() 都声明 x 时,它们指的是同一个x--编译器可以确定。

我们可以在生成的代码中看到这一点。例如,如果我们用 gcc 编译上面的代码,f()g() 的相关位就是这些。

f:

    movl    _ZL1x(%rip), %eax
    addl    , %eax
    movl    %eax, _ZL1x(%rip)

这是 x 的增量(它使用名称 _ZL1x)。

g:

    movl    _ZL1x(%rip), %eax
    [...]
    call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_c@PLT

所以这基本上是加载 x,然后将其发送到 std::cout(我省略了我们在这里不关心的其他参数的代码)。

重要的部分是代码引用了 _ZL1x——与使用的 f 相同的名称,因此它们都引用了同一个对象。

链接器并没有真正参与其中,因为它所看到的只是此文件已请求 space 一个静态分配的变量。它为此生成 space,但不必执行任何操作即可使 fg 引用同一事物——编译器已经处理了。

My question is probably related to why these are called "linkage" (How is that related to the linker).

根据C标准,

An identifier declared in different scopes or in the same scope more than once can be made to refer to the same object or function by a process called linkage.

术语 "linkage" 似乎非常合适 -- 同一标识符的不同声明 链接 在一起,因此它们指的是同一对象或函数。作为所选择的术语,一个真正实现链接的程序通常被称为 "linker".

是很自然的。

But how does the linker handle variables (1) with no linkage and (2) with internal linkage? What are the differences in these two cases?

链接器不必对没有链接的标识符做任何事情。每个这样的对象标识符声明都声明了一个不同的对象(并且函数声明始终具有内部或外部链接)。

链接器也不一定会对具有内部链接的标识符做任何事情,因为编译器通常可以做所有需要用这些做的事情。然而,具有内部链接的标识符可以在同一个翻译单元中多次声明,这些标识符都指向同一个对象或函数。最常见的情况是带有前向声明的 static 函数:

static void internal(void);

// ...

static void internal(void) {
    // do something
}

文件作用域变量也可以有内部链接和多重声明,它们都被链接以引用同一个对象,但多重声明部分对变量没有那么有用。