如果在 CI 中写:
int num;
在我分配任何东西之前num
,是num
不确定的值吗?
静态变量(文件范围和函数静态)初始化为零:
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 异常发生在一个真正不应该失败的操作中(例如,整数加法,或分配给另一个变量)。而且,如果您离开未初始化的变量,编译器可能会在设置了这些标志位的情况下拾取一些随机垃圾——这意味着触摸未初始化的变量可能是致命的。
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
这取决于变量的存储时间。具有静态存储持续时间的变量始终隐式初始化为零。
至于自动(局部)变量,未初始化的变量具有不确定的值。除其他外,不确定值意味着您可能在该变量中“看到”的任何“值”不仅不可预测,甚至不能保证是稳定的。例如,在实践中(即忽略 UB 一秒钟)此代码
int num;
int a = num;
int b = num;
不保证变量a
会b
收到相同的值。有趣的是,这不是一些迂腐的理论概念,这在实践中很容易作为优化的结果发生。
因此,一般来说,“它是用内存中的任何垃圾初始化”的流行答案甚至都不是正确的。未初始化变量的行为与使用垃圾初始化的变量不同。
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 内核在程序启动时将程序加载到内存中时将该内存区域清零。
那要看。如果该定义是全局的(在任何函数之外),num
则将初始化为零。如果它是本地的(在函数内部),那么它的值是不确定的。从理论上讲,即使尝试读取值也有未定义的行为——C 允许位可能对值没有贡献,但必须以特定方式设置,以便您甚至可以通过读取变量获得定义的结果。
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.
由于计算机的存储容量有限,自动变量通常会保存在以前用于其他任意目的的存储元素(无论是寄存器还是 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 的模式调用将不可避免地导致程序调用未定义行为,编译器可能会省略任何仅在mode
4 或更大时才相关的代码,例如通常会阻止的代码在这种情况下发射核武器。请注意,无论是标准还是现代编译器哲学,都不会关心“嘿”的返回值被忽略的事实——试图返回它的行为给了编译器无限许可来生成任意代码。
如果存储类是静态的或全局的,那么在加载期间,BSS 会将变量或内存位置 (ML) 初始化为 0,除非该变量最初被分配了某个值。在局部未初始化变量的情况下,陷阱表示被分配给内存位置。因此,如果您的任何包含重要信息的寄存器被编译器覆盖,程序可能会崩溃。
但是一些编译器可能有机制来避免这样的问题。
当我意识到存在陷阱表示时,我正在使用 nec v850 系列,它的位模式表示除 char 之外的数据类型的未定义值。当我使用未初始化的字符时,由于陷阱表示,我得到了一个零默认值。这可能对使用 necv850es 的 any1 有用
就我而言,它主要取决于编译器,但在大多数情况下,编译器将值预先假定为 0。
在 VC++ 的情况下,我得到了垃圾值,而 TC 给出的值为 0。我打印如下
int i;
printf('%d',i);