Linux 不能在没有 GCC 优化的情况下编译;影响?

Dan*_*096 10 linux compiling c gcc

人们可以在互联网上找到几个线程,例如:

http://www.gossamer-threads.com/lists/linux/kernel/972619

人们抱怨他们无法使用 -O0 构建 Linux,并被告知这不受支持;Linux 依靠 GCC 优化来自动内联函数、删除死代码,以及执行构建成功所需的其他操作。

我已经为至少一些 3.x 内核亲自验证了这一点。如果使用 -O0 编译,我尝试过的那些会在几秒钟的构建时间后退出。

这通常被认为是可接受的编码实践吗?编译器优化(例如自动内联)是否具有足够的可预测性以供依赖;至少在只处理一个编译器时?未来版本的 GCC 有多大可能破坏具有默认优化(即 -O2 或 -Os)的当前 Linux 内核的构建?

还有一个更迂腐的注释:由于 3.x 内核不能在没有优化的情况下编译,它们是否应该被视为技术上不正确的 C 代码?

der*_*ert 14

您已将几个不同(但相关)的问题组合在一起。其中一些并不是真正的主题(例如,编码标准),所以我将忽略它们。

如果内核是“技术上不正确的 C 代码”,我将开始。我从这里开始是因为答案解释了内核占据的特殊位置,这对于理解其余部分至关重要。

内核技术上是否有错误的 C 代码?

答案肯定是它的“不正确”。

有几种方法可以说 C 程序是不正确的。让我们先来一些简单的:

  • 不遵循 C 语法(即有语法错误)的程序是不正确的。内核对 C 语法使用了各种 GNU 扩展。就 C 标准而言,这些是语法错误。(当然,对于 GCC,它们不是。尝试编译-std=c99 -pedantic或类似...)
  • 一个不按照其设计做的程序是不正确的。内核是一个巨大的程序,即使快速检查其更改日志也会证明,肯定不会。或者,正如我们通常所说的,它有错误。

优化在 C 中的含义

[注意:本节包含对实际规则的重述;有关详细信息,请参阅标准并搜索 Stack Overflow。]

现在是需要更多解释的那个。C 标准说某些代码必须产生某些行为。它还说某些在语法上有效的 C 语言具有“未定义的行为”;一个(不幸的是常见的!)示例是访问超出数组末尾(例如,缓冲区溢出)。

未定义的行为非常有效。如果程序包含它,即使是一点点,C 标准也不再关心程序表现出的行为或编译器在面对它时产生的输出。

但是即使程序只包含定义的行为,C 仍然允许编译器有很大的回旋余地。作为一个简单的例子(注意:对于我的例子#include,为了简洁,我省略了线条等):

void f() {
    int *i = malloc(sizeof(int));
    *i = 3;
    *i += 2;
    printf("%i\n", *i);
    free(i);
}
Run Code Online (Sandbox Code Playgroud)

当然,这应该打印 5 后跟一个换行符。这就是 C 标准所要求的。

如果您编译该程序并反汇编输出,您会期望调用 malloc 以获取一些内存,返回的指针存储在某处(可能是寄存器),值 3 存储到该内存中,然后将 2 添加到该内存中(也许甚至需要加载,添加和存储),然后将内存复制到堆栈中,并将一个点字符串"%i\n"放在堆栈上,然后printf调用该函数。相当多的工作。但相反,您可能会看到好像您已经写过:

/* Note that isn't hypothetical; gcc 4.9 at -O1 or higher does this. */
void f() { printf("%i\n", 5) }
Run Code Online (Sandbox Code Playgroud)

事情就是这样:C 标准允许这样做。C 标准只关心结果,而不关心实现的方式。

这就是 C 中优化的意义所在。编译器提出了一种更智能(通常更小或更快,取决于标志)的方法来实现 C 标准所需的结果。有一些例外,例如 GCC 的-ffast-math选项,但除此之外,优化级别不会改变技术上正确的程序的行为(即,仅包含定义行为的程序)。

您可以仅使用定义的行为来编写内核吗?

让我们继续检查我们的示例程序。我们编写的版本,而不是编译器将其转换成的版本。我们做的第一件事是调用malloc以获取一些内存。C 标准告诉我们做什么malloc,但没有告诉我们它是如何做的。

如果我们查看以malloc清晰(而不是速度)为目标的实现,我们会看到它进行了一些系统调用(例如mmapwith MAP_ANONYMOUS)来获取大量内存。它内部保留了一些数据结构,告诉它该块的哪些部分是使用的还是空闲的。它找到一个至少与您要求的一样大的空闲块,计算出您要求的数量,并返回一个指向它的指针。它也完全用 C 编写,并且只包含定义的行为。如果它是线程安全的,它可能包含一些 pthread 调用。

现在,最后,如果我们看看什么 mmap确实,我们看到了各种有趣的东西。首先,它会检查系统是否有足够的空闲 RAM 和/或交换空间用于映射。接下来,它会找到一些空闲地址空间来放置块。然后它会编辑一个称为页表的数据结构,并可能在此过程中进行一堆内联汇编调用。它实际上可能会找到一些物理内存的空闲页面(即,实际 DRAM 模块中的实际位)——一个可能需要强制其他内存交换的过程——以及。如果它没有对整个请求的块执行此操作,则会进行设置,以便在第一次访问所述内存时发生这种情况。其中大部分是通过内联汇编、写入各种魔术地址等来完成的。还要注意,它还使用了内核的大部分内容,尤其是在需要交换时。

内联汇编、写入魔术地址等都在 C 规范之外。这并不奇怪;C 运行在许多不同的机器架构上——包括在 1970 年代初 C 被发明时几乎无法想象的一堆。隐藏特定于机器的代码是内核(以及在某种程度上 C 库)的核心部分。

当然,如果你回到示例程序,就会明白printf一定是相似的。很清楚如何在标准 C 中进行所有格式化等;但实际上是在监视器上吗?还是通过管道传输到另一个程序?内核(可能还有 X11 或 Wayland)再一次施展了魔法。

如果你想想内核做的其他事情,很多都在 C 之外。 例如,内核从磁盘读取数据(C 对磁盘、PCIe 总线或 SATA 一无所知)到物理内存中(C 只知道 malloc,不是 DIMM、MMU 等),使其可执行(C 对处理器执行位一无所知),然后将其作为函数调用(不仅在 C 之外,非常不允许)。

内核与其编译器之间的关系

如果您还记得之前的情况,如果程序包含未定义的行为,就 C 标准而言,所有赌注都没有了。但是内核确实必须包含未定义的行为。因此,内核与其编译器之间必须存在某种关系,至少足以让内核开发人员确信内核在违反 C 标准的情况下仍能正常工作。至少在 Linux 的情况下,这包括内核对 GCC 内部如何工作有一些了解。

破裂的可能性有多大?

未来的 GCC 版本可能会破坏内核。我可以非常自信地说出这句话,因为它之前已经发生过好几次了。当然,像 GCC 中严格的别名优化之类的东西也破坏了内核之外的很多东西。

另请注意,Linux 内核所依赖的内联不是自动内联,而是内核开发人员手动指定的内联。有很多人用 -O0 编译内核并报告它在修复一些小问题后基本有效。(一个甚至在您链接到的线程中)。大多数情况下,内核开发人员认为没有理由使用 进行编译-O0,并且需要优化作为副作用会使一些技巧起作用,并且没有人使用 进行测试-O0,因此不支持。

作为一个例子,这编译和链接-O1或更高,但不与-O0

void f();

int main() {
    int x = 0, *y;
    y = &x;

    if (*y)
        f();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

通过优化,gcc 可以确定f()永远不会被调用,并忽略它。如果没有优化,gcc 将保留调用,并且链接器失败,因为没有f(). 内核开发人员依靠类似的行为使内核代码更易于读/写。