如果我用 C 写:
int num;
在我给 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 异常,而 真的不应该失败(例如,整数加法或赋值给另一个变量)。如果你留下一个未初始化的变量,编译器可能会收到一些随机的垃圾,这些标志位设置-这意味着触摸未初始化的变量可能是致命的。
那要看情况了。如果该定义是全局的(在任何函数之外) ,那么 num将被初始化为零。如果它是局部的(在函数内部) ,那么它的值是不确定的。理论上,即使尝试读取这个值也是有未定义行为的—— C 允许比特不对值做出贡献的可能性,但是必须以特定的方式设置,这样你甚至可以通过读取变量得到定义好的结果。
基本的答案是,是的,它是未定义的。
如果您因此看到奇怪的行为,这可能取决于声明它的位置。如果在堆栈上的一个函数中,那么每次调用该函数时,内容很可能会有所不同。如果它是静态作用域或模块作用域,则它是未定义的,但不会更改。
它取决于变量的存储持续时间。具有静态存储持续时间的变量始终隐式初始化为零。
对于自动(局部)变量,未初始化的变量具有 不确定的价值。不确定值,在其他事情中,意味着无论你在该变量中“看到”什么样的“值”,它不仅是不可预测的,甚至不能保证是 稳定。例如,在实践中(即忽略 UB 一秒钟)这段代码
int num; int a = num; int b = num;
不能保证变量 a和 b得到相同的值。有趣的是,这并不是什么迂腐的理论概念,这很容易发生在实践中作为优化的结果。
a
b
因此,一般来说,流行的回答“它是用内存中的任何垃圾进行初始化的”根本就不正确。未初始化变量的行为与带有垃圾的变量 已初始化的行为不同。
C 对于对象的初始值总是非常具体的。如果是全局的或 static,它们将被归零。如果是 auto,则值为 不确定。
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
%rdi
"%d\n"
0x4005e4
%rsi是第二个 printf 参数,因此是 i。
%rsi
i
它来自 -0x4(%rbp),这是第一个4字节的本地变量。
-0x4(%rbp)
此时,rbp在堆栈的第一页已被内核分配,因此要理解这个值,我们需要查看内核代码并找出它将其设置为什么。
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中的局部变量
-O3
执行情况分析: 在广义开发银行中,< value Optimout > 意味着什么?
全局变量
标准: 0
执行情况: .bss节。
.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和:
# 601044 <i>
0x601044
readelf -SW a.out
包含:
[25] .bss NOBITS 0000000000601040 001040 000008 00 WA 0 0 4
表示 0x601044正好在 .bss部分的中间,它从 0x601040开始,长度为8字节。
0x601040
然后,ELF 标准保证名为 .bss的部分完全由零填充:
.bss此部分保存未初始化的数据,这些数据有助于 根据定义,系统初始化 当程序开始运行时,带有零的数据 不占用文件空间,如节类型 SHT_NOBITS所示。
SHT_NOBITS
此外,SHT_NOBITS类型是高效的,并且在可执行文件中不占用空间:
此成员以字节为单位给出节的大小。除非 sec- T 类型为 SHT_NOBITS,截面占用 sh_size 类型为 SHT_NOBITS的部分可能有一个非零 但是它在文件中不占用空间。
sh_size
然后由 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 的范围内。
wow()
foo()
x
q
避免要求编译器做额外的工作以确保未初始化 变量似乎从不保存它们域外的值,并且避免需要 标准规定了不确定行为的过多细节 使用未初始化的自动变量的未定义行为 在某些情况下,这种情况的后果可能比一个 值在其类型范围之外。例如,给定:
void moo(int mode) { if (mode < 5) launch_nukes(); hey(0, mode); }
编译器可以推断出这一点,因为调用 moo()的模式是 大于3将不可避免地导致程序调用未定义 行为时,编译器可能会省略任何仅相关的代码 如果 mode是4或更大,例如代码,通常会防止 在这种情况下发射核武器。请注意, 现代编译哲学,会关心返回值的事实 从“ hey”被忽略——尝试返回它的行为给出了一个编译器 生成任意代码的无限许可。
moo()
mode