在 C 语言中一个声明的,未初始化的变量会发生什么? 它有值吗?

如果我用 C 写:

int num;

在我给 num赋值之前,num的值是不确定的吗?

165828 次浏览

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

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 异常,而 真的不应该失败(例如,整数加法或赋值给另一个变量)。如果你留下一个未初始化的变量,编译器可能会收到一些随机的垃圾,这些标志位设置-这意味着触摸未初始化的变量可能是致命的。

那要看情况了。如果该定义是全局的(在任何函数之外) ,那么 num将被初始化为零。如果它是局部的(在函数内部) ,那么它的值是不确定的。理论上,即使尝试读取这个值也是有未定义行为的—— C 允许比特不对值做出贡献的可能性,但是必须以特定的方式设置,这样你甚至可以通过读取变量得到定义好的结果。

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

如果您因此看到奇怪的行为,这可能取决于声明它的位置。如果在堆栈上的一个函数中,那么每次调用该函数时,内容很可能会有所不同。如果它是静态作用域或模块作用域,则它是未定义的,但不会更改。

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

对于自动(局部)变量,未初始化的变量具有 不确定的价值。不确定值,在其他事情中,意味着无论你在该变量中“看到”什么样的“值”,它不仅是不可预测的,甚至不能保证是 稳定。例如,在实践中(即忽略 UB 一秒钟)这段代码

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

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

因此,一般来说,流行的回答“它是用内存中的任何垃圾进行初始化的”根本就不正确。未初始化变量的行为与带有垃圾的变量 已初始化的行为不同。

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

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

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

这是 C89中的情况,参见 6.5.7初始化部分。

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

这是 C99中的情况,参见 6.7.8初始化部分。

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

至于 不确定到底是什么意思,我不确定 C89,C99说:

3.17.2 < br > 不确定值 < br > 未指定值或陷阱 代表

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

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

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

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

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

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

当我使用 nec v850系列的时候,我意识到有一种陷阱表示法,它的位模式表示除了 char 以外的数据类型的未定义值。当我获取一个未初始化的 char 时,由于陷阱表示,我得到了一个零默认值。这可能对任何使用 necv850es 的人都有用

Ubuntu 15.10,Kernel 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在堆栈的第一页已被内核分配,因此要理解这个值,我们需要查看内核代码并找出它将其设置为什么。

    当一个进程死亡时,内核是否在为其他进程重用该内存之前将该内存设置为某个内存?如果没有,新的进程将能够读取其他已完成的程序的内存,泄漏数据。见: 未初始化的值是否存在安全风险?

然后,我们也可以玩我们自己的堆栈修改,并写一些有趣的东西,如:

#include <assert.h>


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


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


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

请注意,gcc11似乎产生了一个不同的汇编输出,上面的代码停止“工作”,毕竟是未定义行为:

-O3中的局部变量

执行情况分析: 在广义开发银行中,< value Optimout > 意味着什么?

全局变量

标准: 0

执行情况: .bss节。

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


gcc -O0 -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类型是高效的,并且在可执行文件中不占用空间:

此成员以字节为单位给出节的大小。除非 sec- T 类型为 SHT_NOBITS,截面占用 sh_size 类型为 SHT_NOBITS的部分可能有一个非零 但是它在文件中不占用空间。

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

由于计算机的存储容量有限,自动变量通常保存在以前用于其他任意目的的存储单元(无论是寄存器还是 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 寄存器0将分别加载2或4,但是如果它是 其他值,函数可以返回寄存器0中的任何值(即 值1234567) ,即使该值不在 uint16 _ t 的范围内。

避免要求编译器做额外的工作以确保未初始化 变量似乎从不保存它们域外的值,并且避免需要 标准规定了不确定行为的过多细节 使用未初始化的自动变量的未定义行为 在某些情况下,这种情况的后果可能比一个 值在其类型范围之外。例如,给定:

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

编译器可以推断出这一点,因为调用 moo()的模式是 大于3将不可避免地导致程序调用未定义 行为时,编译器可能会省略任何仅相关的代码 如果 mode是4或更大,例如代码,通常会防止 在这种情况下发射核武器。请注意, 现代编译哲学,会关心返回值的事实 从“ hey”被忽略——尝试返回它的行为给出了一个编译器 生成任意代码的无限许可。