157

如果在 CI 中写:

int num;

在我分配任何东西之前num,是num不确定的值吗?

4

9 回答 9

211

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

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

void foo() {
    static int x; // also zero
}

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

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

在实践中,它们最初往往只是有一些无意义的值——一些编译器甚至可能会放入特定的固定值,以便在调试器中查看时使其显而易见——但严格来说,编译器可以自由地做任何事情,从崩溃到召唤恶魔通过你的鼻道

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

尝试使用设置了这些标志位的值可能会导致 CPU 异常发生在一个真正不应该失败的操作中(例如,整数加法,或分配给另一个变量)。而且,如果您离开未初始化的变量,编译器可能会在设置了这些标志位的情况下拾取一些随机垃圾——这意味着触摸未初始化的变量可能是致命的。

于 2009-10-20T21:27:45.653 回答
61

0 如果是静态或全局,则不确定存储类是否为自动

C 语言一直对对象的初始值非常具体。如果是全局或static,它们将被归零。如果auto,则该值是不确定的。

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

在 C89 中就是这种情况,请参阅第6.5.7 节初始化

如果具有自动存储持续时间的对象未显式初始化,则其值是不确定的。如果一个具有静态存储持续时间的对象没有被显式初始化,它被隐式初始化,就好像每个具有算术类型的成员都被分配了 0 并且每个具有指针类型的成员都被分配了一个空指针常量。

在 C99 中就是这种情况,请参阅第6.7.8 节初始化

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

至于不确定的确切含义,我不确定 C89,C99 说:

3.17.2
indeterminate value

未指定的值或陷阱表示

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

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

于 2009-10-20T21:38:39.753 回答
14

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

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

int num;
int a = num;
int b = num;

不保证变量ab收到相同的值。有趣的是,这不是一些迂腐的理论概念,这在实践中很容易作为优化的结果发生。

因此,一般来说,“它是用内存中的任何垃圾初始化”的流行答案甚至都不是正确的。未初始化变量的行为与使用垃圾初始化的变量不同。

于 2009-10-20T21:37:30.750 回答
7

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

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

局部变量

标准:未定义的行为。

实现:程序分配堆栈空间,并且从不将任何内容移动到该地址,因此使用之前存在的任何内容。

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

编译:

gcc -O0 -std=c99 a.c

输出:

0

并反编译:

objdump -dr a.out

至:

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

根据我们对 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);
}

请注意,GCC 11 似乎产生了不同的汇编输出,并且上面的代码停止“工作”,毕竟这是未定义的行为:为什么 gcc 中的 -O3 似乎将我的局部变量初始化为 0,而 -O0 没有?

局部变量-O3

实现分析在:gdb中<value optimization out>是什么意思?

全局变量

标准:0

实施:.bss部分。

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

gcc -00 -std=c99 a.c

编译为:

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

# 601044 <i>i在地址0x601044并且:

readelf -SW a.out

包含:

[25] .bss              NOBITS          0000000000601040 001040 000008 00  WA  0   0  4

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

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

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

此外,该类型SHT_NOBITS高效且不占用可执行文件空间:

sh_size该成员以字节为单位给出节的大小。除非节类型是SHT_NOBITS,否则节会占用sh_size 文件中的字节。类型的部分SHT_NOBITS可能具有非零大小,但它不占用文件中的空间。

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

于 2016-04-19T17:17:34.550 回答
3

那要看。如果该定义是全局的(在任何函数之外),num则将初始化为零。如果它是本地的(在函数内部),那么它的值是不确定的。从理论上讲,即使尝试读取值也有未定义的行为——C 允许位可能对值没有贡献,但必须以特定方式设置,以便您甚至可以通过读取变量获得定义的结果。

于 2009-10-20T21:28:39.827 回答
0

The basic answer is, yes it is undefined.

If you are seeing odd behavior because of this, it may depended on where it is declared. If within a function on the stack then the contents will more than likely be different every time the function gets called. If it is a static or module scope it is undefined but will not change.

于 2009-10-20T21:30:14.497 回答
0

由于计算机的存储容量有限,自动变量通常会保存在以前用于其他任意目的的存储元素(无论是寄存器还是 RAM)中。如果在为其分配值之前使用了这样的变量,则该存储可能会保存它之前保存的任何内容,因此该变量的内容将是不可预测的。

另外一个问题是,许多编译器可能会将变量保存在大于相关类型的寄存器中。尽管编译器需要确保写入变量并读回的任何值都将被截断和/或符号扩展为适当的大小,但许多编译器会在写入变量时执行此类截断,并期望它具有在读取变量之前执行。在这样的编译器上,类似:

uint16_t hey(uint32_t x, uint32_t mode)
{ uint16_t q; 
  if (mode==1) q=2; 
  if (mode==3) q=4; 
  return q; }

 uint32_t wow(uint32_t mode) {
   return hey(1234567, mode);
 }

很可能会导致wow()将值 1234567 分别存储到寄存器 0 和 1 中,然后调用foo(). 由于x在“foo”中不需要,并且由于函数应该将它们的返回值放入寄存器 0,编译器可以将寄存器 0 分配给q. 如果mode是 1 或 3,则寄存器 0 将分别加载 2 或 4,但如果是其他值,则该函数可能会返回寄存器 0 中的任何值(即值 1234567),即使该值不在范围内uint16_t。

为了避免要求编译器做额外的工作以确保未初始化的变量似乎永远不会在其域之外保存值,并避免需要过多详细地指定不确定的行为,标准规定使用未初始化的自动变量是未定义的行为。在某些情况下,这样做的后果可能比超出其类型范围的值更令人惊讶。例如,给定:

void moo(int mode)
{
  if (mode < 5)
    launch_nukes();
  hey(0, mode);      
}

编译器可以推断,因为moo()使用大于 3 的模式调用将不可避免地导致程序调用未定义行为,编译器可能会省略任何仅在mode4 或更大时才相关的代码,例如通常会阻止的代码在这种情况下发射核武器。请注意,无论是标准还是现代编译器哲学,都不会关心“嘿”的返回值被忽略的事实——试图返回它的行为给了编译器无限许可来生成任意代码。

于 2016-05-06T15:28:36.143 回答
-1

如果存储类是静态的或全局的,那么在加载期间,BSS 会将变量或内存位置 (ML) 初始化为 0,除非该变量最初被分配了某个值。在局部未初始化变量的情况下,陷阱表示被分配给内存位置。因此,如果您的任何包含重要信息的寄存器被编译器覆盖,程序可能会崩溃。

但是一些编译器可能有机制来避免这样的问题。

当我意识到存在陷阱表示时,我正在使用 nec v850 系列,它的位模式表示除 char 之外的数据类型的未定义值。当我使用未初始化的字符时,由于陷阱表示,我得到了一个零默认值。这可能对使用 necv850es 的 any1 有用

于 2013-05-30T10:29:53.553 回答
-4

就我而言,它主要取决于编译器,但在大多数情况下,编译器将值预先假定为 0。
在 VC++ 的情况下,我得到了垃圾值,而 TC 给出的值为 0。我打印如下

int i;
printf('%d',i);
于 2012-06-27T19:34:39.350 回答