C中声明的未初始化variables会发生什么? 它有价值吗?
如果在CI中写:
int num;
在我给num
赋值之前, num
是不确定的值吗?
静态variables(文件范围和函数静态)被初始化为零:
int x; // zero int y = 0; // also zero void foo() { static int x; // also zero }
非静态variables(局部variables)是不确定的 。 在分配值之前读取它们会导致未定义的行为。
void foo() { int x; printf("%d", x); // the compiler is free to crash here }
在实践中,它们往往只是在最初有一些无意义的价值 – 一些编译器甚至可能会放入特定的固定值,以便在查看debugging器时显而易见 – 但严格来说,编译器可以自由地做任何事情,从崩溃到召唤通过你的鼻道恶魔 。
至于为什么它是未定义的行为,而不是简单的“未定义/任意值”,有许多CPU体系结构在各种types的表示中都有附加的标志位。 一个现代的例子就是Itanium,在它的寄存器中有一个“Not a Thing”位 。 当然,C标准的起草者正在考虑一些老的架构。
尝试使用设置了这些标志位的值进行操作可能会导致CPUexception,而这种exception操作确实不会失败(例如,整数加法或赋值给另一个variables)。 如果你离开一个未初始化的variables,编译器可能会select一些随机的垃圾,并设置这些标志位 – 这意味着接触未初始化的variables可能是致命的。
如果静态或全局,则为0,如果存储类为auto,则不确定
C一直非常关注对象的初始值。 如果全球或static
,他们将被归零。 如果是auto
,则值是不确定的 。
C89之前的编译器就是这种情况,K&R和DMR的原始C报告都是这样规定的。
C89的情况就是如此,参见6.5.7初始化 。
如果具有自动存储持续时间的对象未被明确初始化,则其值是不确定的。 如果具有静态存储持续时间的对象未被明确初始化,则隐式地初始化它,就好像每个具有算术types的成员都被赋值为0,并且每个具有指针types的成员都被赋予一个空指针常量。
C99中就是这种情况,请参见6.7.8初始化 。
如果具有自动存储持续时间的对象未被显式初始化,则其值是不确定的。 如果具有静态存储持续时间的对象未被明确初始化,则:
– 如果它有指针types,则它被初始化为空指针;
– 如果它有算术types,则它被初始化为(正或无符号)零;
– 如果它是一个聚合,每个成员根据这些规则初始化(recursion);
– 如果是联盟,则根据这些规则初始化(recursion)第一个命名成员。
至于什么不确定的意思,我不确定C89,C99说:
3.17.2
不确定的价值
要么是未指定的值,要么是陷阱表示
但不pipe什么标准说,在现实生活中,每个堆栈页实际上都是从零开始的,但是当程序查看任何auto
存储类的值时,它会看到上次使用这些堆栈地址时自己的程序留下了什么。 如果你分配了很多的auto
数组,你会看到它们最终以零开始。
你可能会想,为什么这样呢? 不同的SO回答处理这个问题,请参阅: https : //stackoverflow.com/a/2091505/140740
这取决于variables的存储时间。 具有静态存储持续时间的variables总是被隐式地初始化为零。
对于自动(本地)variables,未初始化的variables具有不确定的值 。 除此之外,不确定的价值意味着无论你在该variables中“看到”什么“价值”,不仅是不可预测的,甚至不能保证稳定 。 例如,在实践中(即忽略UB一秒钟)这个代码
int num; int a = num; int b = num;
并不能保证variablesa
和b
会得到相同的值。 有趣的是,这不是一些迂腐的理论概念,这在实践中容易发生优化的结果。
所以一般来说,“它是用内存中的任何垃圾进行初始化”的stream行答案甚至都不是很正确。 未初始化的variables的行为不同于用垃圾初始化的variables的行为。
Ubuntu 15.10,Kernel 4.2.0,x86-64,GCC 5.2.1的例子
足够的标准,让我们看看一个实现:-)
局部variables
标准:未定义的行为。
实现:程序分配堆栈空间,并且不会将任何东西移动到该地址,因此以前使用的都是这个地址。
#include <stdio.h> int main() { int i; printf("%d\n", i); }
编译:
gcc -O0 -std=c99 ac
输出:
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参数,因此地址为0x4005e4
的string"%d\n"
-
%rsi
是第二个printf参数,因此i
。它来自
-0x4(%rbp)
,这是第一个4字节的局部variables。此时,
rbp
已经在内核的第一页被分配了,所以为了理解这个值,我们将查看内核代码并找出它的设置。TODO是否在内核将某个内存设置为某个内容之前,在某个进程死亡之前将其重用于其他进程? 否则,新进程将能够读取其他已完成程序的内存,泄漏数据。 请参阅: 未初始化的值是否具有安全风险?
然后,我们也可以玩我们自己的堆栈修改,并写下如下有趣的东西:
#include <assert.h> int f() { int i = 13; return i; } int g() { int i; return i; } int main() { f(); assert(g() == 13); }
全局variables
标准:0
执行: .bss
部分。
#include <stdio.h> int i; int main() { printf("%d\n", i); } gcc -00 -std=c99 ac
编译为:
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
本节保存对程序内存映像有贡献的未初始化的数据。 根据定义,系统在程序开始运行时用零来初始化数据。 该部分不占用文件空间,如节typesSHT_NOBITS
。
此外,typesSHT_NOBITS
是有效的,并且在可执行文件上不占用空间:
sh_size
该成员以字节为单位给出该部分的大小。 除非段types是SHT_NOBITS
,否则该段占用文件中的sh_size
字节。SHT_NOBITS
types的SHT_NOBITS
可能有一个非零大小,但它不占用文件中的空间。
然后,当Linux内核启动时,将程序加载到内存中时,将内存区域清零。
那要看。 如果该定义是全局的(在任何函数之外),则num
将被初始化为零。 如果它是本地的(在一个函数内),那么它的值是不确定的。 从理论上讲,即使试图读取值也有未定义的行为 – C允许位不会对值产生影响,但是必须以特定方式设置,以便通过读取variables来获得定义的结果。
基本的答案是,是的,它是未定义的。
如果您因此而看到奇怪的行为,则可能取决于宣布的位置。 如果在栈中的一个函数内,那么每次函数被调用时,其内容将可能不同。 如果它是一个静态或模块范围,它是未定义的,但不会改变。
如果存储类是静态的或全局的,那么在加载期间, BSS将variables或存储单元(ML) 初始化为0,除非该variables初始分配了某个值。 在本地未初始化variables的情况下,陷阱表示被分配给内存位置。 所以如果你的任何包含重要信息的寄存器被编译器覆盖,程序可能会崩溃。
但是一些编译器可能有机制来避免这样的问题。
我正在与nec v850系列工作,当我意识到有陷阱表示有位模式代表未定义的数据types的值除字符。 当我采取了一个未初始化的字符由于陷阱表示得到了一个零默认值。 这可能对使用necv850es的任何1有用
由于计算机具有有限的存储容量,因此自动variables通常会保存在以前用于其他任意目的的存储元件(无论是寄存器还是RAM)中。 如果在赋值之前使用了这样一个variables,那么这个variables的内容可能是不可预知的。
作为一个额外的皱纹,许多编译器可能会保持大于相关types的寄存器中的variables。 虽然需要编译器来确保写入variables和回读的任何值将被截断和/或符号扩展到适当的大小,但是许多编译器会在写入variables时执行这种截断,并期望它将会在variables被读取之前被执行。 在这样的编译器上,像这样:
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()
。 由于在“foo”中不需要x
,并且由于函数应该把它们的返回值放到寄存器0中,所以编译器可以把寄存器0分配给q
。 如果mode
为1或3,寄存器0将分别装入2或4,但是如果它是某个其他值,该函数可能会返回寄存器0中的任何值(即值1234567),即使该值不在范围uint16_t。
为了避免需要编译器做额外的工作来确保未初始化的variables永远不会在其域外保存值,并且避免需要过多详细地指定不确定的行为,那么标准表示使用未初始化的自动variables是未定义行为。 在某些情况下,这样做的后果可能比超出其范围的价值更令人惊讶。 例如,给出:
void moo(int mode) { if (mode < 5) launch_nukes(); hey(0, mode); }
编译器可以推断出,因为调用moo()
的模式大于3将不可避免地导致程序调用未定义的行为,编译器可能会省略任何只在mode
为4或更大时才相关的代码,例如代码这通常会阻止在这种情况下发射核武器。 请注意,Standard和现代编译器哲学都不会在意来自“hey”的返回值被忽略的事实 – 尝试返回它的行为使得编译器无限制地生成任意代码的许可证。
num的值将是来自主存储器(RAM)的一些垃圾值。 它更好,如果你刚创build后初始化variables。
就我而言,这主要取决于编译器,但是一般情况下,编译器将其值假设为0。
在VC ++的情况下,我得到垃圾值,而TC的值为0. I打印如下
int i; printf('%d',i);