bdo*_*lan 180

静态变量(文件范围和函数静态)初始化为零:

int x; // zero
int y = 0; // also zero

void foo() {
    static int x; // also zero
}
Run Code Online (Sandbox Code Playgroud)

非静态变量(局部变量)是不确定的.在分配值之前读取它们会导致未定义的行为.

void foo() {
    int x;
    printf("%d", x); // the compiler is free to crash here
}
Run Code Online (Sandbox Code Playgroud)

在实践中,它们最初只是有一些荒谬的价值 - 有些编译器甚至可能会在调试器中查看特定的固定值以使其显而易见 - 但严格来说,编译器可以自由地执行任何操作,从崩溃到召唤恶魔通过你的鼻腔通道.

至于为什么它是未定义的行为而不是简单的"未定义/任意值",有许多CPU架构在其各种类型的表示中具有额外的标志位.一个现代的例子是Itanium,它的寄存器中有一个"Not a Thing"位 ; 当然,C标准起草人正在考虑一些较旧的架构.

尝试使用设置了这些标志位的值可能会导致操作中的CPU异常确实不应该失败(例如,整数加法或分配给另一个变量).如果你继续保留未初始化的变量,编译器可能会设置这些标志位的随机垃圾 - 意味着触摸未初始化的变量可能是致命的.

  • 什么不是?标准要求静态初始化; 见ISO/IEC 9899:1999 6.7.8#10 (8认同)
  • @Stuart:有一种称为"陷阱表示"的东西,它基本上是一个不表示有效值的位模式,并且可能在运行时导致例如硬件异常.唯一能保证任何位模式为有效值的C类型是`char`; 所有其他人都可以有陷阱表示.或者 - 因为无论如何访问未初始化的变量都是UB - 符合标准的编译器可能只是做一些检查并决定发出问题的信号. (6认同)
  • bdonian是对的.C总是被精确地指定.在C89和C99之前,dmr的一篇论文在1970年代早期就指出了所有这些东西.即使在最粗糙的嵌入式系统中,只需要一个memset()来做正确的事情,因此没有理由不合格的环境.我在答案中引用了这个标准. (5认同)
  • 哦,不,他们不是.如果你是幸运的话,他们可能在调试模式下,当你不在客户面前时,在有R in的几个月里 (2认同)
  • 据我所知,第一个例子很好.我不太清楚为什么编译器可能会在第二个崩溃时崩溃:) (2认同)
  • @Pavel:令人着迷 - 我很想知道,例如,32 位处理器上的 32 位无符号整数的陷阱表示值是什么。我很难想象一个 int 有一些特殊的保留值,它不能被使用,而且没有人告诉过我们这些可怜的程序员。 (2认同)
  • @SoftwareMonkey 你读过链接吗?该示例被构建为没有(未指定的)值这样做(该示例小心地使用了 `unsigned int`)。这是链接博客文章声称的关键论点,即优化编译器将读取不确定的内存视为未定义的行为,**即使已知架构没有陷阱值**。我以为这就是我们在这里讨论的内容。 (2认同)
  • @haccks,由于该值是不确定的,因此该值可能是陷阱表示,并且访问陷阱表示是 UB,因此 UB 是访问未初始化变量的允许行为集合之一 - 因此,一般来说,是UB。如果您的编译器没有陷阱表示,您可能能够对程序的行为做出一些假设,而“真正的”UB 则无法做到这一点。 (2认同)
  • @immibis **一切**都是未定义行为的有效结果。该标准明确没有要求编译器在看到未定义的行为时“不”崩溃。 (2认同)

Dig*_*oss 57

0表示静态或全局,如果存储类是auto,则不确定

C一直非常具体地说明对象的初始值.如果是全球性的static,它们将被归零.如果auto,该值是不确定的.

在C89之前的编译器中就是这种情况,并且由K&R和DMR的原始C报告中指定.

这是C89中的情况,请参见6.5.7初始化.

如果具有自动存储持续时间的对象未明确初始化,则其值不确定.如果具有静态存储持续时间的对象未明确初始化,则会对其进行隐式初始化,就好像每个具有算术类型的成员都被赋值为0,并且每个具有指针类型的成员都被赋予空指针常量.

在C99中就是这种情况,参见6.7.8初始化部分.

如果未显式初始化具有自动存储持续时间的对象,则其值不确定.如果没有显式初始化具有静态存储持续时间的对象,则:
- 如果它具有指针类型,则将其初始化为空指针;
- 如果它有算术类型,则初始化为(正或无符号)零;
- 如果是聚合,则根据这些规则初始化(递归)每个成员;
- 如果它是一个联合,则根据这些规则初始化(递归)第一个命名成员.

至于究竟什么是不确定的,我不确定C89,C99说:

3.17.2
不确定值

或未指定值或陷阱表示

但无论标准说什么,在现实生活中,每个堆栈页面实际上都是从零开始,但是当你的程序查看任何auto存储类值时,它会看到你自己的程序在上次使用这些堆栈地址时留下的任何内容.如果你分配了很多auto数组,你会看到它们最终用零完全开始.

你可能想知道,为什么会这样?不同的SO答案处理该问题,请参阅:https://stackoverflow.com/a/2091505/140740

  • 通常不确定(用于?)意味着它可以做任何事情.它可以是零,它可以是那里的值,它可以使程序崩溃,它可以使计算机从CD插槽中产生蓝莓煎饼.你完全没有保证.它可能会导致地球的毁灭.至少就规范而言......任何制作编译器实际做过类似事情的人都会对B-非常不满意 (3认同)

AnT*_*AnT 11

它取决于变量的存储持续时间.具有静态存储持续时间的变量始终隐式初始化为零.

对于自动(本地)变量,未初始化的变量具有不确定的值.除其他外,不确定值意味着您可能"看到"该变量中的"值"不仅不可预测,甚至不能保证稳定.例如,在实践中(即忽略UB一秒钟)此代码

int num;
int a = num;
int b = num;
Run Code Online (Sandbox Code Playgroud)

不保证变量a并且b将接收相同的值.有趣的是,这不是一些迂腐的理论概念,这在实践中很容易作为优化的结果发生.

所以一般来说,流行的答案"它是用内存中的垃圾进行初始化"甚至都不是很正确的.未初始化的变量的行为是一个变量的不同初始化垃圾.


Cir*_*四事件 5

Ubuntu 15.10,内核4.2.0,x86-64,GCC 5.2.1示例

足够的标准,让我们来看一个实现:-)

局部变量

标准:未定义的行为.

实现:程序分配堆栈空间,并且永远不会将任何内容移动到该地址,因此以前使用的是什么.

#include <stdio.h>
int main() {
    int i;
    printf("%d\n", i);
}
Run Code Online (Sandbox Code Playgroud)

编译:

gcc -O0 -std=c99 a.c
Run Code Online (Sandbox Code Playgroud)

输出:

0
Run Code Online (Sandbox Code Playgroud)

和反编译:

objdump -dr a.out
Run Code Online (Sandbox Code Playgroud)

至:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       48 83 ec 10             sub    $0x10,%rsp
  40053e:       8b 45 fc                mov    -0x4(%rbp),%eax
  400541:       89 c6                   mov    %eax,%esi
  400543:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400548:       b8 00 00 00 00          mov    $0x0,%eax
  40054d:       e8 be fe ff ff          callq  400410 <printf@plt>
  400552:       b8 00 00 00 00          mov    $0x0,%eax
  400557:       c9                      leaveq
  400558:       c3                      retq
Run Code Online (Sandbox Code Playgroud)

根据我们对x86-64调用约定的了解:

  • %rdi是第一个printf参数,因此是"%d\n"地址处的字符串0x4005e4

  • %rsi因此是第二个printf参数i.

    它来自-0x4(%rbp),这是第一个4字节的局部变量.

    此时,rbp堆栈的第一页已经由内核分配,因此为了理解该值,我们将查看内核代码并找出它设置的内容.

    TODO是否会在进程终止时将内存设置为其他进程之前将内存设置为某种内容?如果没有,新进程将能够读取其他已完成程序的内存,泄漏数据.请参阅:未初始化的值是否存在安全风险?

然后我们也可以使用自己的堆栈修改并编写有趣的内容,例如:

#include <assert.h>

int f() {
    int i = 13;
    return i;
}

int g() {
    int i;
    return i;
}

int main() {
    f();
    assert(g() == 13);
}
Run Code Online (Sandbox Code Playgroud)

全局变量

标准:0

实施:.bss部分.

#include <stdio.h>
int i;
int main() {
    printf("%d\n", i);
}

gcc -00 -std=c99 a.c
Run Code Online (Sandbox Code Playgroud)

编译为:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       8b 05 04 0b 20 00       mov    0x200b04(%rip),%eax        # 601044 <i>
  400540:       89 c6                   mov    %eax,%esi
  400542:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400547:       b8 00 00 00 00          mov    $0x0,%eax
  40054c:       e8 bf fe ff ff          callq  400410 <printf@plt>
  400551:       b8 00 00 00 00          mov    $0x0,%eax
  400556:       5d                      pop    %rbp
  400557:       c3                      retq
  400558:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
  40055f:       00
Run Code Online (Sandbox Code Playgroud)

# 601044 <i>说这i是在地址0x601044和:

readelf -SW a.out
Run Code Online (Sandbox Code Playgroud)

包含:

[25] .bss              NOBITS          0000000000601040 001040 000008 00  WA  0   0  4
Run Code Online (Sandbox Code Playgroud)

它说0x601044是在该.bss部分的中间,从那里开始0x601040并且是8个字节长.

然后,ELF标准保证命名的部分.bss完全用零填充:

.bss此部分包含有助于程序内存映像的未初始化数据.根据定义,当程序开始运行时,系统用零初始化数据.该部分不占用文件空间,如部分类型所示SHT_NOBITS.

此外,类型SHT_NOBITS是有效的,并且在可执行文件上不占用空间:

sh_size该成员给出了部分的大小(以字节为单位).除非部分类型SHT_NOBITS,该部分占用sh_size 文件中的字节.类型的一部分SHT_NOBITS可能具有非零大小,但它在文件中不占用空间.

然后由Linux内核在启动时将程序加载到内存中时将该内存区域归零.