如果在CI中写:
int num;
在将任何东西分配给之前num
,值是num
不确定的吗?
extern int x;
但是,定义总是意味着要声明。在C ++中不是这样,因为静态类成员变量可以在不声明的情况下进行定义,因为声明必须在类定义中(而不是声明!),并且该定义必须在类定义之外。
如果在CI中写:
int num;
在将任何东西分配给之前num
,值是num
不确定的吗?
extern int x;
但是,定义总是意味着要声明。在C ++中不是这样,因为静态类成员变量可以在不声明的情况下进行定义,因为声明必须在类定义中(而不是声明!),并且该定义必须在类定义之外。
Answers:
静态变量(文件作用域和函数静态)初始化为零:
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 Thing”位。当然,C标准起草者正在考虑一些较旧的体系结构。
尝试使用设置了这些标志位的值可能会导致CPU异常,而该异常实际上不会失败(例如,整数加法或分配给另一个变量)。而且,如果您保留未初始化的变量,则编译器可能会在设置了这些标志位的情况下拾取一些随机垃圾,这意味着触摸未初始化的变量可能是致命的。
char
;。其他所有对象都有陷阱表示。或者-由于访问未初始化的变量无论如何都是UB-合格的编译器可能只是进行一些检查并决定发出问题。
C对于对象的初始值一直很具体。如果是global或static
,它们将被清零。如果为auto
,则该值不确定。
在C89之前的编译器中就是这种情况,这由K&R和DMR的原始C报告指定。
在C89中就是这种情况,请参见第6.5.7节“ 初始化”。
如果没有明确初始化具有自动存储期限的对象,则其值不确定。如果未明确初始化具有静态存储持续时间的对象,则将隐式初始化该对象,就好像每个具有算术类型的成员都被分配了0,而每个具有指针类型的成员都被分配了一个空指针常量。
在C99中就是这种情况,请参见第6.7.8节“ 初始化”。
如果未自动初始化具有自动存储期限的对象,则其值不确定。如果未明确初始化具有静态存储持续时间的对象,则:
— —如果具有指针类型,则将其初始化为空指针;
—如果具有算术类型,则将其初始化为(正数或无符号)零;
—如果是聚合,则根据这些规则(递归)初始化每个成员;
—如果它是一个联合,则将根据这些规则(递归)初始化第一个命名成员。
至于究竟是什么不确定的手段,我不知道的C89,C99说:
3.17.2
不确定值
(未指定值)或陷阱表示
但是,不管标准说什么,在现实生活中,每个堆栈页面实际上都是从零开始的,但是当您的程序查看任何auto
存储类值时,它将看到您自己的程序最后一次使用这些堆栈地址时所留下的内容。如果您分配了很多auto
数组,您将看到它们最终以零开始整洁。
您可能想知道,为什么会这样呢?一个不同的SO答案可解决该问题,请参阅:https : //stackoverflow.com/a/2091505/140740
indeterminate value
可以在3.19.2中找到。
它取决于变量的存储持续时间。具有静态存储持续时间的变量始终隐式初始化为零。
对于自动(局部)变量,未初始化的变量的值不确定。不确定的价值,除其他外,意味着您可能在该变量中“看到”的任何“价值”不仅是不可预测的,甚至不能保证稳定。例如,在实践中(即一秒钟忽略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);
}
局部变量 -O3
在以下位置进行实施分析:<值优化输出>在gdb中是什么意思?
全局变量
标准: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内核在启动程序时将程序加载到内存中时将该内存区域清零。
由于计算机的存储容量有限,因此自动变量通常将保存在先前用于其他任意目的的存储元素(无论是寄存器还是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有用
num的值将是来自主内存(RAM)的一些垃圾值。如果在创建后立即初始化变量,则更好。
就我所知,它主要取决于编译器,但是在大多数情况下,编译器通常将值假定为0。
在VC ++的情况下,我得到了垃圾值,而TC将值设为0。我将其打印如下
int i;
printf('%d',i);
0
编译器很可能会采取额外的步骤来确保获得该值(通过添加代码来初始化变量)。一些编译器在进行“调试”编译时会这样做,但是0
为它们选择值不是一个好主意,因为它会隐藏代码中的错误(更正确的做法是保证不会出现0xBAADF00D
类似的数字)。我认为大多数编译器只会将碰巧占用内存的所有垃圾作为变量的值保留(即,通常不将其赋值为0
)。