C中声明的、未初始化的变量会发生什么?它有价值吗?

p4rjhz4m  于 2023-05-16  发布在  其他
关注(0)|答案(9)|浏览(103)

如果在C中我写:

int num;

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

2q5ifsrm

2q5ifsrm1#

静态变量(文件作用域和函数静态)初始化为零:

int x; // zero
int y = 0; // also zero

void foo() {
    static int x; // also zero
}

非静态变量(局部变量)是 * 不确定的 *。在赋值之前阅读它们会导致 undefined behavior

void foo() {
    int x;
    printf("%d", x); // the compiler is free to crash here
}

在实践中,它们往往只是在最初有一些无意义的值-一些编译器甚至可能会放入特定的固定值,以便在调试器中查看时显而易见-但严格地说,编译器可以自由地做任何事情,从崩溃到调用demons through your nasal passages
至于为什么它是未定义的行为而不是简单的“未定义/任意值”,有许多CPU架构在其表示中具有用于各种类型的附加标志位。一个现代的例子是the Itanium, which has a "Not a Thing" bit in its registersthe Itanium, which has a "Not a Thing" bit in its registers;当然,C标准的起草者考虑的是一些较旧的体系结构。
尝试使用设置了这些标志位的值可能会导致CPU异常,而这些异常是 * 实际上 * 不应该失败的操作(例如,整数加法或分配给另一个变量)。如果你让一个变量保持未初始化,编译器可能会拾取一些设置了这些标志位的随机垃圾--这意味着触及未初始化的变量可能是致命的。

mwkjh3gx

mwkjh3gx2#

0(静态或全局),不确定(存储类为auto)

C语言对对象的初始值总是非常具体。如果是global或static,则它们将被归零。如果auto,则值为 indeterminate
这是C89之前的编译器的情况,K&R和DMR的原始C报告也是如此规定的。
在C89中就是这种情况,参见章节6.5.7初始化
如果一个具有自动存储持续时间的对象没有被显式初始化,那么它的值是不确定的。如果具有静态存储持续时间的对象未显式初始化,则隐式初始化该对象,就好像具有算术类型的每个成员被分配0并且具有指针类型的每个成员被分配空指针常量。
在C99中就是这种情况,参见章节6.7.8初始化
如果没有显式初始化具有自动存储持续时间的对象,则其值不确定。如果具有静态存储持续时间的对象未显式初始化,则:

  • 如果是指针类型,则初始化为空指针;
  • 如果是算术类型,则初始化为(正的或无符号的)零;
  • 如果它是一个聚合,每个成员都根据这些规则进行初始化(递归);
  • 如果它是一个联合体,第一个命名的成员根据这些规则被初始化(递归地)。
    至于 indeterminate 到底是什么意思,我不确定C89,C99说:

**3.17.2

不确定值**
未指定的值或陷阱表示
但是不管标准怎么说,在真实的生活中,每个堆栈页实际上都是从零开始的,但是当你的程序查看任何auto存储类值时,它会看到你自己的程序上次使用这些堆栈地址时留下的任何东西。如果你分配了很多auto数组,你会看到它们最终都是从零开始的。
你可能会想,为什么会这样?一个不同的SO答案处理这个问题,见:https://stackoverflow.com/a/2091505/140740

hujrc8aj

hujrc8aj3#

这取决于变量的存储时间。具有静态存储持续时间的变量总是隐式初始化为零。
对于自动(局部)变量,未初始化的变量具有 * 不确定的值 *。不确定值意味着你可能在该变量中“看到”的任何“值”不仅是不可预测的,甚至不能保证它是稳定的。例如,在实践中(即忽略UB一秒钟)这个代码

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

不保证变量ab将接收相同的值。有趣的是,这不是一些迂腐的理论概念,这在实践中很容易发生,作为优化的结果。
因此,一般来说,流行的答案“它是用内存中的任何垃圾初始化的”甚至是不正确的。Uninitialized 变量的行为与带有垃圾的 initialized 变量的行为不同。

a0x5cqrl

a0x5cqrl4#

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参数,因此地址为0x4005e4的字符串"%d\n"
  • %rsi是第二个printf参数,因此是i

它来自-0x4(%rbp),这是第一个4字节局部变量。
此时,rbp位于堆栈的第一页,已由内核分配,因此要理解该值,我们需要查看内核代码并找出它设置的值。
TODO当一个进程死亡时,内核是否在为其他进程重用该内存之前将该内存设置为某个对象?否则,新进程将能够读取其他已完成程序的内存,从而泄漏数据。参见:Are uninitialized values ever a security risk?
然后,我们还可以玩我们自己的堆栈修改,并编写有趣的东西,如:

#include <assert.h>

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

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

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

请注意,GCC 11似乎产生了不同的汇编输出,上面的代码停止“工作”,它毕竟是未定义的行为:Why does -O3 in gcc seem to initialize my local variable to 0, while -O0 does not?

-O3中的本地变量

执行情况分析:gdb是什么意思?

全局变量

标准品: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 standard保证名为.bss的部分完全由零填充:
.bss此部分保存未初始化的数据,这些数据构成程序的内存映像。根据定义,当程序开始运行时,系统用零初始化数据。该节occu- pies没有文件空间,由节类型,SHT_NOBITS指示。
此外,类型SHT_NOBITS是高效的,并且在可执行文件上不占用空间:
sh_size此成员以字节为单位给出节的大小。除非节类型为SHT_NOBITS,否则节在文件中占用sh_size字节。类型为SHT_NOBITS的节可能具有非零大小,但它不占用文件中的空间。
然后,当Linux内核启动程序并将其加载到内存中时,它将负责将该内存区域清零。

w8rqjzmb

w8rqjzmb5#

看情况如果该定义是全局的(在任何函数之外),那么num将被初始化为零。如果它是局部的(在函数内部),那么它的值是不确定的。从理论上讲,即使尝试读取值也会有未定义的行为-- C允许可能存在对值没有贡献的位,但必须以特定的方式进行设置,以便您甚至可以通过阅读变量获得定义的结果。

w51jfk4q

w51jfk4q6#

基本的答案是,是的,它没有定义。
如果你因此看到奇怪的行为,这可能取决于它是在哪里声明的。如果在堆栈上的一个函数中,那么每次调用该函数时,内容都很可能不同。如果它是一个静态或模块作用域,它是未定义的,但不会改变。

zsbz8rwp

zsbz8rwp7#

因为计算机具有有限的存储容量,所以自动变量通常将被保存在先前用于一些其他任意目的的存储元件(无论是寄存器还是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);      
}

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

ie3xauqp

ie3xauqp8#

如果存储类是静态或全局的,则在加载期间,BSS将变量或内存位置(ML)初始化为0,除非变量最初被分配了某个值。在局部未初始化变量的情况下,陷阱表示被分配给内存位置。因此,如果任何包含重要信息的寄存器被编译器覆盖,程序可能会崩溃。
但是一些编译器可以具有避免这种问题的机制。
我正在使用nec v850系列时,我意识到有一种陷阱表示法,它具有表示除char之外的数据类型的未定义值的位模式。当我接受一个未初始化的字符时,由于陷阱表示,我得到了一个零默认值。这可能对任何使用necv850es的人有用

eufgjt7s

eufgjt7s9#

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

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

相关问题