斯坦福大学教程和GCC之间的冲突

时间:2013-10-24 07:03:53

标签: c gcc memory-management

根据this电影(大约38分钟),如果我有两个具有相同本地变量的函数,它们将使用相同的空间。所以下面的程序应该打印5。使用gcc结果-1218960859进行编译。为什么呢?

该计划:

#include <stdio.h>

void A()
{
    int a;
    printf("%i",a);
}

void B()
{
    int a;
    a = 5;
}

int main()
{
    B();
    A();
    return 0;
}

根据要求,这是反汇编程序的输出:

0804840c <A>:
 804840c:   55                      push   ebp
 804840d:   89 e5                   mov    ebp,esp
 804840f:   83 ec 28                sub    esp,0x28
 8048412:   8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 8048415:   89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 8048419:   c7 04 24 e8 84 04 08    mov    DWORD PTR [esp],0x80484e8
 8048420:   e8 cb fe ff ff          call   80482f0 <printf@plt>
 8048425:   c9                      leave  
 8048426:   c3                      ret    

08048427 <B>:
 8048427:   55                      push   ebp
 8048428:   89 e5                   mov    ebp,esp
 804842a:   83 ec 10                sub    esp,0x10
 804842d:   c7 45 fc 05 00 00 00    mov    DWORD PTR [ebp-0x4],0x5
 8048434:   c9                      leave  
 8048435:   c3                      ret    

08048436 <main>:
 8048436:   55                      push   ebp
 8048437:   89 e5                   mov    ebp,esp
 8048439:   83 e4 f0                and    esp,0xfffffff0
 804843c:   e8 e6 ff ff ff          call   8048427 <B>
 8048441:   e8 c6 ff ff ff          call   804840c <A>
 8048446:   b8 00 00 00 00          mov    eax,0x0
 804844b:   c9                      leave  
 804844c:   c3                      ret    
 804844d:   66 90                   xchg   ax,ax
 804844f:   90                      nop

5 个答案:

答案 0 :(得分:130)

是的,是的,这是未定义的行为,因为您使用的是未初始化的 1 变量。

但是,在x86架构 2 上, 此实验应该可以正常工作 。该值不会从堆栈中“擦除”,并且因为它未在B()中初始化,所以如果堆栈帧相同,则仍应存在相同的值。

我冒昧地猜测,因为int avoid B()内没有使用,编译器优化了该代码,并且从未写入5堆栈上的位置。尝试在printf中添加B() - 它可能会起作用。

此外,编译器标志 - 即优化级别 - 也可能会影响此实验。尝试通过将-O0传递给gcc。

来禁用优化

编辑:我刚刚使用gcc -O0(64位)编译了代码,实际上,程序打印5,就像熟悉调用堆栈所期望的那样。实际上,即使没有-O0,它也能正常工作。 32位版本可能表现不同。

免责声明:不要,永远在“真实”代码中使用这样的内容!

1 - 关于这是否正式为“UB”,或者只是不可预测,below正在进行辩论。

2 - 也是x64,可能是使用调用堆栈的所有其他架构(至少是带有MMU的架构)


让我们来看看它没有工作的原因。最好在32位中看到,因此我将使用-m32进行编译。

$ gcc --version
gcc (GCC) 4.7.2 20120921 (Red Hat 4.7.2-2)

我使用$ gcc -m32 -O0 test.c进行了编译(禁用了优化)。当我运行它时,它会打印垃圾。

查看$ objdump -Mintel -d ./a.out

080483ec <A>:
 80483ec:   55                      push   ebp
 80483ed:   89 e5                   mov    ebp,esp
 80483ef:   83 ec 28                sub    esp,0x28
 80483f2:   8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 80483f5:   89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 80483f9:   c7 04 24 c4 84 04 08    mov    DWORD PTR [esp],0x80484c4
 8048400:   e8 cb fe ff ff          call   80482d0 <printf@plt>
 8048405:   c9                      leave  
 8048406:   c3                      ret    

08048407 <B>:
 8048407:   55                      push   ebp
 8048408:   89 e5                   mov    ebp,esp
 804840a:   83 ec 10                sub    esp,0x10
 804840d:   c7 45 fc 05 00 00 00    mov    DWORD PTR [ebp-0x4],0x5
 8048414:   c9                      leave  
 8048415:   c3                      ret    

我们在B中看到,编译器保留了0x10字节的堆栈空间,并将int a的{​​{1}}变量初始化为5。

但是在[ebp-0x4]中,编译器将A放在int a。所以在这种情况下,我们的局部变量没有最终在同一个地方!在[ebp-0xc]中添加printf()调用也会导致AA的堆栈帧相同,并打印B

答案 1 :(得分:36)

未定义的行为。未初始化的局部变量具有不确定的值,使用它将导致未定义的行为。

答案 2 :(得分:12)

要记住一件重要的事情 - 不要依赖于类似的东西,而从不在实际代码中使用它!这只是一个有趣的事情(甚至并非总是如此),而不是一个特征或类似的东西。想象一下,你自己试图找到那种“特征”所产生的错误 - 噩梦。

顺便说一下。 - C和C ++充满了这种“功能”,这里有 GREAT 幻灯片: http://www.slideshare.net/olvemaudal/deep-c因此,如果你想看到更多类似的“功能”,了解它的内幕以及它是如何工作只是观看这个幻灯片 - 你不会后悔,我相信大多数经验丰富的c / c ++程序员可以从中学到很多东西。

答案 3 :(得分:7)

在函数A中,变量a未初始化,打印其值会导致未定义的行为。

在某些编译器中,a中的变量Aa中的B位于同一地址,因此可能会打印5,但是,你不能依赖未定义的行为。

答案 4 :(得分:7)

使用gcc -Wall filename.c编译代码您将看到这些警告。

In function 'B':
11:9: warning: variable 'a' set but not used [-Wunused-but-set-variable]

In function 'A':
6:11: warning: 'a' is used uninitialized in this function [-Wuninitialized]  

在c中打印未初始化的变量导致未定义的行为。

第6.7.8节C99标准的初始化说明

  

如果没有显式初始化具有自动存储持续时间的对象,则其值是不确定的。如果没有显式初始化具有静态存储持续时间的对象,则:

— if it has pointer type, it is initialized to a null pointer;
— if it has arithmetic type, it is initialized to (positive or unsigned) zero;
— if it is an aggregate, every member is initialized (recursively) according to these rules;
— if it is a union, the first named member is initialized (recursively) according to these rules.

Edit1

As @Jonathon Reinhart如果您使用-O标记gcc-O0禁用优化,那么您可能会获得输出5.

但这并不是一个好主意,永远不要在生产代码中使用它。

-Wuninitialized这是一个有价值的警告您应该考虑这个警告您不应该禁用或跳过此警告,这会导致生产中的巨大损失,例如在运行守护程序时导致崩溃。


<强> EDIT2

Deep C幻灯片解释了为什么结果是5 /垃圾。从这些幻灯片中添加这些信息并进行微小修改,以使这个答案更有效。

  

案例1:没有优化

$ gcc -O0 file.c && ./a.out  
5

也许这个编译器有一个池 它重用的命名变量。例如 变量a在 B()中使用和发布,然后A()需要时 它将获得整数名a 变量将获得相同的内存 地点。如果重命名变量 在B()中,比如b,我不认为 你会得到5

  

案例2:优化

当优化器启动时,可能会发生很多事情 这种情况我猜想可以跳过对B()的调用 它没有任何副作用。另外,我不会感到惊讶 如果在A()中内联main(),即没有函数调用。 (但由于A ()具有链接器可见性,因此该函数的目标代码仍然必须 创建以防其他目标文件想要链接 功能)。无论如何,我怀疑打印的价值会是 如果你优化代码,别的东西。

gcc -O file.c && ./a.out
1606415608  

垃圾!