在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。然而,这个例子证明了这一原理:当存在未初始化的值时,编译器可以从根本上更改生成的代码。