如果没有初始化,为什么局部变量在C中有未确定的值



C-Linux OS中,当调用函数时,Assembly的尾声部分会创建一个堆栈框架,并且局部变量引用基指针。我的问题是,当我们在不初始化的情况下打印变量时,是什么使变量保持不确定的值。我的理论是,当我们使用变量时,OS带来了与局部变量的地址相对应的page,而page中的地址可能有一些值,使局部变量的值成为可能。这是正确的吗?

让我们来看看一个简单程序的反汇编:

#include <stdio.h>
int main() {
unsigned int i;
unsigned int j = 1;
printf("%un", j);
printf("%un", i);
}

在GCC-11.1默认优化的情况下,拆卸为:

.file   "char.c"
.text
.section    .rodata
.LC0:
.string "%un"
.text
.globl  main
.type   main, @function
/*So, till here is meta data and other stuff. We're interested in what's bottom*/
main:
.LFB0:
.cfi_startproc
endbr64
pushq   %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq    %rsp, %rbp
.cfi_def_cfa_register 6
subq    $16, %rsp
movl    $1, -8(%rbp)
movl    -8(%rbp), %eax /*See, it wrote 1 into -8(%rbp), which
represents the variable j, but didn't assign anything anything to
-4(%rbp), which represents the variable i*/
movl    %eax, %esi
leaq    .LC0(%rip), %rax
movq    %rax, %rdi
movl    $0, %eax
call    printf@PLT
movl    -4(%rbp), %eax /* Now we load -4(%rbp), which is i, into
%eax for printing. Whatever is at -4(%rbp) gets printed. So, it's
undetermined */
movl    %eax, %esi
leaq    .LC0(%rip), %rax
movq    %rax, %rdi
movl    $0, %eax
call    printf@PLT
movl    $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size   main, .-main
.ident  "GCC: (Ubuntu 11.1.0-3ubuntu1) 11.1.0"
.section    .note.GNU-stack,"",@progbits
.section    .note.gnu.property,"a"
.align 8
.long   1f - 0f
.long   4f - 1f
.long   5
0:
.string "GNU"
1:
.align 8
.long   0xc0000002
.long   3f - 2f
2:
.long   0x3
3:
.align 8
4:

阅读拆解中的注释以获得解释。

显然,在某些情况下,编译器甚至可能不会费力地将未插入的变量加载到寄存器中(而不是在本例中,可能取决于编译器、优化和情况(,而是只使用寄存器中的任何变量。我曾经看到有人说,我没有检查过ISO标准,也没有验证过。你是怎么开始在标准中发现这样的东西的?它是巨大的。

考虑编译器编译一个正确初始化对象的程序:

int x = 3;
printf("%dn", x);
int y = 4+x*7;
printf("%dn", y);

这可能导致汇编代码:

Store 3 in X.                   // "X" refers to the stack location assigned for x.
Load address of "%dn" into R0. // R0 is the register used for passing the first argument.
Load from X into R1.            // R1 is the register for the second argument.
Call printf.
Load 4 into R1.                 // Start the 4 of 4+x*7.
Load from X into R2             // Get x to calculate with it.
Multiply R2 by 7.               // Make x*7.
Add R2 to R1.                   // Finish 4+x*7.
Load address of "%dn" into R0.
Call printf.

这是一个工作程序。现在假设我们不初始化x,而是使用int x;。由于x没有初始化,规则说它没有确定的值。这意味着编译器可以省略所有获得x值的指令。因此,让我们取工作的汇编代码,并删除所有获得x:值的指令

Load address of "%dn" into R0. // R0 is the register used for passing the first argument.
Call printf.
Load 4 into R1.                 // Start the 4 of 4+x*7.
Multiply R2 by 7.               // Make x*7.
Add R2 to R1.                   // Finish 4+x*7.
Load address of "%dn" into R0.
Call printf.

在这个程序中,第一个printf打印R1中的任何内容,因为x的值从未加载到R1中。x*7的计算使用R2中的任何值,因为x的值从未加载到R2中。因此,这个程序可能会为第一个printf打印"37",因为R1中碰巧有一个37,但它可能会为第二个printf打印"4",因为在R2中恰好有一个0。因此,这个程序的输出"看起来像"x在某个时刻的值为37,在另一个时刻为0。程序的行为就好像x没有任何固定值一样。

这是一个非常简单的例子。实际上,当编译器在优化过程中删除代码时,它会删除更多的代码。例如,如果它知道x没有初始化,那么它可能不仅移除x的负载,还移除乘以7。然而,这个例子证明了这一原理:当存在未初始化的值时,编译器可以从根本上更改生成的代码。

最新更新