在C中声明的未初始化变量会发生什么?它有价值吗?

时间:2009-10-20 21:24:36

标签: c initialization declaration

如果在C中我写:

int num;

在我向num分配任何内容之前,num的值是否不确定?

10 个答案:

答案 0 :(得分:174)

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

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

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

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

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

在实践中,它们最初往往只有一些荒谬的价值 - 有些编译器甚至可能会设置特定的固定值,以便在查看调试器时显而易见 - 但严格来说,编译器可以自由地做任何事情。碰到召唤demons through your nasal passages

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

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

答案 1 :(得分:55)

如果是静态或全局,则为0;如果存储类为auto

,则为不确定

C对于对象的初始值一直非常具体。如果是全局或static,它们将被归零。如果auto,则值为 indeterminate

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

C89就是这种情况,请参阅 6.5.7初始化部分。

  

如果对象具有自动功能   存储持续时间未初始化   明确地说,它的价值是   不定。如果有一个对象   静态存储持续时间不是   明确地初始化,它是   隐含地初始化,好像每一个   具有算术类型的成员是   分配0和每个成员   指针类型被赋予null   指针常数。

C99就是这种情况,请参阅 6.7.8初始化部分。

  

如果对象具有自动功能   存储持续时间未初始化   显然,它的价值是   不定。如果有一个对象   静态存储持续时间不是   明确初始化,然后:
- 如果它   有指针类型,它被初始化为   空指针;
- 如果有算术   类型,它被初始化为(正面   零或无符号);
- 如果是的话   聚合,每个成员都被初始化   (递归地)根据这些   规则;
- 如果是工会,那么第一个   命名成员已初始化   (递归地)根据这些   规则。

至于不确定的含义是什么,我不确定C89,C99说:

  

3.17.2
不确定值

未指定的值或陷阱   表示

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

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

答案 2 :(得分:11)

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

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

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

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

所以一般来说,流行的答案“它是用内存中的垃圾进行初始化”甚至都不是很正确的。 未初始化的变量的行为与带有垃圾的变量 initialized 的行为不同。

答案 3 :(得分:5)

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,因此为了理解该值,我们将查看内核代码并找出它设置的内容。< / p>

    TODO内核是否将内存设置为某些内容,然后在进程终止时将其重新用于其他进程?如果没有,新进程将能够读取其他已完成程序的内存,泄漏数据。请参阅:Are uninitialized values ever a security risk?

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

#include <assert.h>

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

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

int main() {
    f();
    assert(g() == 13);
}

全局变量

标准: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 standard然后保证名为.bss的部分完全填充零:

  

.bss此部分包含有助于实现此目标的未初始化数据   程序的记忆图像。根据定义,系统初始化   程序开始运行时带零的数据。该部分 -   没有文件空间,如节类型SHT_NOBITS所示。

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

  

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

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

答案 4 :(得分:3)

这取决于。如果该定义是全局的(在任何函数之外),那么num将被初始化为零。如果它是本地的(在函数内)那么它的值是不确定的。从理论上讲,即使尝试读取值也有未定义的行为--C允许比特不会对值产生影响,但必须以特定的方式设置,以便您甚至通过读取变量获得定义的结果。

答案 5 :(得分:1)

基本答案是,是的,它是未定义的。

如果您因此而看到奇怪的行为,则可能取决于声明的位置。如果在堆栈中的函数内,则每次调用函数时内容都可能不同。如果它是静态或模块范围,则它是未定义的但不会更改。

答案 6 :(得分:1)

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

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

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

的any1有用

答案 7 :(得分:1)

由于计算机具有有限的存储容量,因此自动变量通常保存在先前用于某些其他任意目的的存储元件(无论是寄存器还是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 &#34; foo&#34;,因为函数应该将它们的返回值放入 寄存器0,编译器可以将寄存器0分配给q。如果mode为1或 3,寄存器0将分别加载2或4,但如果是一些 其他值,该函数可以返回寄存器0中的任何内容(即 值1234567)即使该值不在uint16_t范围内。

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

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

编译器可以推断出这一点,因为调用moo()的模式是 大于3将不可避免地导致程序调用Undefined 行为,编译器可能会省略任何只相关的代码 如果mode为4或更大,例如通常会阻止的代码 在这种情况下推出核武器。注意既不是标准,也不是 现代编译哲学,会关心返回值的事实 来自&#34;嘿&#34;被忽略 - 尝试返回它的行为给出了一个编译器 生成任意代码的无限许可。

答案 8 :(得分:-1)

num的值将是来自主存储器(RAM)的一些垃圾值。 如果在创建之后初始化变量,它会更好。

答案 9 :(得分:-3)

就我而言,它主要取决于编译器,但一般情况下,大多数情况下,编码器会假定该值为0。 在VC ++的情况下我获得了垃圾值,而TC的值为0。 我打印如下

int i;
printf('%d',i);