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

ely*_*hiv 82 c gcc memory-management

根据这部电影(约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;
}
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

Jon*_*art 130

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

但是,在x86架构2上,此实验应该可行.该值不会从堆栈中"擦除",并且由于它未初始化B(),因此只要堆栈帧相同,该值仍应存在.

我冒昧地猜测,因为内部int a使用void B(),编译器优化了代码输出,并且从未将5写入堆栈中的该位置.尝试添加一个printfin B()- 它可能会工作.

此外,编译器标志 - 即优化级别 - 也可能影响该实验.尝试通过传递-O0给gcc来禁用优化.

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

免责声明:千万不要,永远使用这样的"真正的"代码!

1 - 关于这是否是正式的"UB",或者只是不可预测,下面正在进行辩论.

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


让我们来看看一个原因,它并没有正常工作.这在32位中最好看,所以我将编译-m32.

$ gcc --version
gcc (GCC) 4.7.2 20120921 (Red Hat 4.7.2-2)
Run Code Online (Sandbox Code Playgroud)

我编译$ 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    
Run Code Online (Sandbox Code Playgroud)

我们看到,B编译器保留了0x10字节的堆栈空间,并将int a变量初始化[ebp-0x4]为5.

A然而,编译器放置int a[ebp-0xc].所以在这种情况下,我们的局部变量并没有在同一个地方结束!通过添加printf()在呼叫A以及将导致堆栈帧为AB是相同的,并且打印55.

  • 此外,它被接受,因为它***实际上回答了问题***. (25认同)
  • @BЈовић你看过任何一个视频吗?看,每个人和他们的兄弟都知道你不应该在实际代码中执行此操作,并且它会调用*未定义的行为*.这不是重点.关键是计算机是一个定义明确,可预测的机器.在x86机器上(可能是大多数其他架构),使用理智的编译器和可能的一些代码/标志按摩,这*将按预期工作*.此代码以及视频仅仅是调用堆栈工作方式的*演示*.如果它让你感到困扰,我建议你去其他地方.我们中的一些好奇的人喜欢理解事物. (8认同)
  • 好免责声明! (7认同)
  • 如此多的人投票给答案甚至没有提到"未定义的行为".除此之外,它也被接受. (6认同)
  • 即使它工作一次,它在一些架构上也不可靠 - 一个中断前导码我随时都会在堆栈指针下面吹掉所有内容. (5认同)
  • @JonathonReinhart - 一个例子是没有内存管理单元,没有用户/ lernel模式distictions,因此在中断时没有硬件切换到不同的堆栈.在切换到内核中断堆栈之前,可能还有其他一些数据必须被推送到中断的任务堆栈. (4认同)
  • @MartinJames你能详细说明一个中断会修改用户模式堆栈的架构吗? (3认同)
  • @slebetman不,它没有.他在这个特定的未定义行为的情况下"分析"gcc正在做什么这一事实并不能证明这一点.由于标准没有规定什么是行为,如果它喜欢,二进制文件可以自由地[发布鼻子恶魔](http://www.catb.org/jargon/html/N/nasal-demons.html).甚至有人甚至引用了这个案例的标准,只获得了少数选票. (3认同)
  • @BЈовић:它不包含短语"未定义的行为",但它通过说"使用未初始化的变量"和"很可能仍然存在"来提及它.根据语言的说法,这种行为是巧合的,可能会也可能不会起作用,这取决于编译器的工作方式等等.在规范中,"可能或可能不巧合"的外行概念称为"未定义行为" (2认同)

Som*_*ude 36

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

  • 更确切地说,使用从未采用地址的单元化变量是未定义的行为. (6认同)

cyr*_*iel 12

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

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


Yu *_*Hao 7

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

在某些编译器中,变量ain Aain B在同一地址中,因此可以打印5,但同样,您不能依赖于未定义的行为.


Gan*_*har 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]  
Run Code Online (Sandbox Code Playgroud)

在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.
Run Code Online (Sandbox Code Playgroud)

EDIT1

如@Jonathon Reinhart如果您通过使用-O标志禁用优化,gcc-O0 那么您可能会得到输出5.

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

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


EDIT2

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

案例1:没有优化

$ gcc -O0 file.c && ./a.out  
5
Run Code Online (Sandbox Code Playgroud)

也许这个编译器有一个它重用的命名变量池.例如,变量a被使用并释放 B(),然后当A()需要整数名称时a,它将获得变量将获得相同的内存位置.如果您在重命名变量B(),比方说b,那么我不认为你会得到5.

案例2:优化

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

gcc -O file.c && ./a.out
1606415608  
Run Code Online (Sandbox Code Playgroud)

垃圾!

  • 堆栈空间和变量名之间没有任何关联.该示例依赖于以下事实:在第二个函数调用中,概念上的堆栈帧将简单地覆盖第二个函数调用的堆栈帧.名称是什么并不重要,只要两个方法签名相同,就可能发生同样的事情.正如其他人所指出的那样,如果它是在嵌入式系统中并且在对A()和B()的调用之间服务于硬件中断,则堆栈将包含随机值.像Borguard的Code Guard这样的旧工具允许在每次调用之前将零写入堆栈. (3认同)