GCC: 为什么常量变量不放在.rodata中?

37

我一直认为GCC会将static const变量放置在ELF文件的.rodata段(或者出于优化考虑,放置在.text段)。但实际情况似乎并非如此。

我目前正在一台运行GNU/Linux的笔记本电脑上使用gcc (GCC) 4.7.0 20120505 (prerelease),它将静态常量变量放置在.bss段:

/*
 * this is a.c, and in its generated asm file a.s, the following line gives:
 *   .comm a,4,4 
 * which would place variable a in .bss but not .rodata(or .text)
 */
static const int a;

int main()
{
    int *p = (int*)&a;
    *p = 0;  /* since a is in .data, write access to that region */
             /* won't trigger an exception */
    return 0;
}

那么,这是一个bug还是一个特性?我决定将其归为bugzilla的bug,但最好先寻求帮助。

是否有任何原因导致GCC无法将const变量放置在.rodata中?

更新:

经过测试,具有显式初始化(如const int a = 0;)的常量变量将被GCC放置在.rodata中,而我未对变量进行初始化。因此,这个问题可能会被关闭 - 我没有提出正确的问题。

另外,在我之前的话中,我写道变量a被放置在'.data'部分,这是不正确的。实际上,由于未初始化,它实际上被放置在.bss部分。现在上面的文本已经更正。


在C++中,您可以从不是编译时常量的值初始化const变量。但我检查了一下,GCC在C模式下不允许这样的扩展。 - Potatoswatter
2
就此而言,如果您明确初始化它,则会进入只读部分。 - Mat
还是有点问题。据我所知,将变量初始化为0不会改变程序,如果您没有明确地这样做,那么静态变量应该被初始化为零。所以肯定是出了什么问题。(无论初始化与否,clang都将其放在一个只读(RO)部分。) - Mat
1
此外,在C++中编程错误不会“触发异常”。异常是正确的编程的一部分,而不是调试工具。 - Kerrek SB
1
@Potatoswatter:对于非静态对象(即在函数内部定义的没有 static 关键字的对象),在 C 中使用非常量初始化器是完全合法的,例如 const int r = rand();。但是对于静态对象,不允许使用非常量初始化器。const 的存在或不存在并不重要;在 C 中,const 意味着“只读”,而不是“常量”。 - Keith Thompson
显示剩余6条评论
4个回答

12

编译器将其变为普通变量,可以与其他兼容符号合并,并且如果没有显式初始化定义则可以放在bss段(磁盘上不占用空间)。将其放入rodata将是一个权衡之举;你可以在运行时节省内存(提交的内存),但可能会在磁盘上使用更多空间(对于巨大的数组可能会很多)。

如果您希望将其放入rodata,请使用-fno-common选项。


1
我认为,编译器没有传递“-fno-common”给汇编器,因为在使用“-fno-common”后,它仍然会在汇编文件中生成“.comm a,4,4”。此外,虽然这一次汇编器将“.bss”合并到“.data”中,但对a的写入访问仍然成功。这意味着a被放置在“.data”而不是“.rodata”中。我认为问题在于它没有被初始化。 - starrify

7

为什么GCC这样做呢?如果没有询问开发人员,就无法真正回答这个问题。如果我被允许猜测,我会打赌它与优化有关-编译器不一定要执行const。

话虽如此,我认为最好我们看看语言本身,特别是未定义行为。提到了一些未定义行为,但没有一个深入探讨。

修改常量是未定义行为。在C(和C ++)中,const是一项合同

"但是,如果我 const_cast 取消const并仍然修改y怎么办?"那么您就有未定义的行为。

未定义行为意味着编译器允许做任何它想做的事情,并且无论编译器决定做什么,都不会被视为违反ISO 9899标准。

3.4.3

1未定义行为

使用非便携或错误的程序构造或错误数据时的行为本国际标准不加强制要求的

2注释可能的未定义行为范围从完全忽略具有不可预测结果的情况到在环境中表现出文档化特征(带或不带诊断消息)的翻译或程序执行方式,以终止翻译或执行(伴随发出诊断消息)。

ISO / IEC 9899:1999,§3.4.3

这意味着,因为您已经调用了未定义的行为,编译器所做的任何事情在技术上都是正确的,因为它不是不正确。 因此,对于GCC来采取...

static const int a = 0;

将其转换为.rodata符号,同时进行...

(将代码中的文本转成只含中文的文本)
static const int a; // guaranteed to be zero

将其转换为.bss符号。
在前一种情况下,即使通过代理进行修改,任何尝试修改a的企图通常都会导致分段违规,从而导致内核强制终止运行程序。在后一种情况下,程序可能会在不崩溃的情况下运行。
话虽如此,猜测编译器将做哪一个是不合理的。 Const是一份合同,由您作为程序员来遵守该合同,不要修改应该是常量的数据。违反该合同意味着未定义的行为,以及随之而来的所有可移植性问题和程序错误。
因此,GCC可以做一些事情。 它可能会将符号写入.rodata中,在操作系统内核下保护它 它可能会将对象写入某个内存保护无法保证的位置,在这种情况下... 它可能会更改值 它可能会更改值并立即将其更改回来 它可能会完全删除有问题的代码,理由是该值没有更改(0 -> 0),从而进行优化...
int main(){
    int *p = &a;
    *p = 0;
    return 0;
}

...to...

int main(void){
    return 0;
}

它甚至可能会派遣一架T-800模型回到过去,在你出生之前终结你的父母。

所有这些行为都是合法的(嗯,合法在遵守标准的意义上),因此该Bug报告是没有必要的。


不确定这是否回答了问题,问题是为什么gcc会表现出这样的行为。当然,标准允许各种行为。 - Keith Thompson
没有办法在不询问GCC开发人员本身的情况下明确回答“为什么”的问题。然而,我可以从标准本身推导出理由,并且未定义行为肯定是一个重要的角度。在我看来,专注于这样的实现细节是有风险的,因为人们可能会基于这些知识做出不合理的假设,并依赖于特定实现的语义。 - Braden Best

3

将已声明为const限定符的对象写入是未定义的行为:可以发生任何事情,甚至可能会崩溃。

C语言中没有方法可以声明对象本身是不可变的,您只能通过特定访问禁止对其进行更改。在这里,您有一个int*,因此修改是“允许”的,也就是说编译器不强制发出诊断。在C语言中进行类型转换意味着您应该知道自己在做什么。


实际上,上面的代码并没有显式地写入 const 限定的变量 a。请参考 @Mat 上面的评论,我认为那应该是我需要的确切答案。 - starrify
@PengyuCHEN:Jens是对的,你所做的是未定义行为。为什么GCC在你不初始化变量时不将其放入RO节段,但在你初始化时却这样做,这对我来说是个谜。 - Mat
@Mat 是的,我相信Jens关于未定义行为的说法是正确的,我的意思是,该未定义行为的影响应该是在运行时而不是编译时。 - starrify
@PengyuCHEN,它明确地写入该对象,只是此程序中不会观察到该写操作的结果,因为对象在之后从未被评估。因此,如果变量没有被“const”限定,则编译器将允许对其进行优化。 - Jens Gustedt

1
GCC为什么不能将const变量放置在.rodata中?
您的程序由编译器进行优化(即使在-O0模式下也会进行一些优化)。常量传播是完成的:http://en.wikipedia.org/wiki/Constant_folding 尝试像这样欺骗编译器(请注意,此程序仍然是技术上的未定义行为):
#include <stdio.h>

static const int a;

int main(void)
{
    *(int *) &a = printf("");  // compiler cannot assume it is 0

    printf("%d\n", a);

    return 0;
}

1
删除自第4行以来的所有代码,gcc仍然在.data中给出变量a。我认为这不取决于我们访问它的方式,而是取决于我们声明它的方式。请参见@Mat上面的评论,我认为那应该是我需要的确切答案。 - starrify
我在汇编中看到了一个通过指针写入的操作,使用-O0编译选项时,它并没有被优化掉。 - Mat
@Mat,实现有权优化彭宇程序中的所有行(即等效于 int main(void) {} ),因为程序的可观察行为没有改变。实际上,在我对彭宇程序进行编译时,a 甚至被放置在 .bss 中,因为在最终的二进制文件中占用的空间比在 .rodata 中少。 - ouah
@ouah感谢您指出这一点。我之前说了.data,只是因为我看到了.comm指令,在这种情况下,由于我没有初始化a,它应该被放置在.bss中。我的话是错误的。 - starrify
我认为已知(且恒定)值并不会触发优化。实际上,编译器甚至可以推断出该值从未被读取,因此可以将赋值优化掉。这就是使用 printf 时的变化。 - Jens Gustedt
@JensGustedt 我知道通过阅读它生成的汇编代码不会减少访问权限。此外,我已经测试了你的代码,没有发生任何意外情况。 - starrify

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