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


82

根据这部电影(大约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

41
“他们很好地使用了相同的空间”-这是不正确的。他们可能。否则他们可能不会。而且您不能依靠这两种方式。

17
我想知道这种用法有什么用,如果有人在生产代码中使用它会被枪杀。
AndersK

12
@claptrap也许是为了学习调用堆栈的工作方式并了解计算机在做什么?人们对此太重视了。
Jonathon Reinhart

9
@claptrap再次,这是一个学习练习。如果您了解组装级别的情况,那么“必须跳过的陷阱”都是有意义的。我严重怀疑OP是否有意在“真实”程序中使用类似内容(如果他这样做,他应该被踢!)
Jonathon Reinhart

12
该示例误导了您的勿庸置疑,因为两个局部变量具有相同的名称。但这与正在发生的事情无关:仅变量的数量和类型很重要。不同的名称应完全相同。
Alexis

Answers:


130

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

但是,在x86体系结构2上该实验应该可以进行。该值不会从堆栈中“擦除”,并且由于未在中初始化B(),因此只要堆栈框架相同,该值仍应存在。

我斗胆猜测,由于int a使用内部的void B(),编译器优化的代码出来,和5永远不会写入到堆栈上的该位置。尝试添加printfB()还有-它可能只是工作。

同样,编译器标志(即优化级别)也可能会影响此实验。尝试通过传递-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)

我编译了$ 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变量初始化[ebp-0x4]为5。

A然而,编译器放置int a[ebp-0xc]。因此,在这种情况下,我们的局部变量不会在同一位置结束!通过添加printf()在呼叫A以及将导致堆栈帧AB是相同的,并打印55


7
好免责声明!
TobiasWärre13年

5
即使它可以工作一次,在某些架构上也不是可靠的-中断序言可以随时摧毁堆栈指针下方的所有内容。
马丁·詹姆斯

6
这么多的投票甚至没有提到“未定义的行为”。最重要的是,它也被接受。
2013年

25
而且,它被接受是因为它实际上回答了问题
slebetman

8
@BЈовић您看过任何视频吗?看,每个人和他们的兄弟都知道您不应该在真实的代码中执行此操作,它会调用未定义的行为。那不是重点。关键是计算机是定义明确,可预测的计算机。在x86盒(可能还有大多数其他体系结构)上,使用合理的编译器并可能进行某些代码/标志按摩,这将按预期工作。该代码以及视频仅是调用堆栈如何工作的演示。如果它使您感到困扰,我建议您去其他地方。我们中有些好奇的人喜欢理解事物。
Jonathon Reinhart

36

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


6
更确切地说,使用从未使用其地址的统一变量是未定义的行为。
詹斯·古斯特

@JensGustedt好评论。关于blog.frama-c.com/index.php?post/2013/03/13/…的“下一个示例”部分,您有什么要说的吗?
Pascal Cuoq

@PascalCuoq,这甚至似乎是标准委员会中正在进行的讨论。在某些情况下,检查通过指针获取的内存是有意义的,即使您不知道该内存是否已初始化也是如此。在所有情况下简单地使它变得不确定就太严格了。
詹斯·古斯特

@JensGustedt:接收地址如何导致使用它具有已定义的行为:{ int uninit; &uninit; printf("%d\n", uninit); }仍具有未定义的行为。另一方面,您可以将任何对象视为unsigned char;的数组。那是你的想法吗?
基思·汤普森

@KeithThompson,不,相反。拥有一个变量,使得它的地址从不被占用且未初始化这将导致UB。本身读取不确定的值不是不确定的行为,内容只是不可预测的。从6.3.2.1 p2开始:如果左值指定了可以使用寄存器存储类声明的自动存储持续时间的对象(从未使用其地址),并且该对象未初始化(未使用初始化器声明且未对其赋值)使用前已执行),则行为未定义。
詹斯·古斯特

12

要记住的一件事- 永远不要依赖那样的东西,永远不要在实际代码中使用它!这只是一件有趣的事情(甚至不总是如此),而不是功能或类似的东西。想象一下,您自己尝试查找由这种“功能”(噩梦)产生的错误。

顺便说一句。-C和C ++充满了这种“功能”,这是关于它的很棒的幻灯片:http : //www.slideshare.net/olvemaudal/deep-c因此,如果您想查看更多类似的“功能”,请理解在幕后及其运行方式中,只需观看此幻灯片演示-您就不会后悔,而且我敢肯定,即使是大多数有经验的c / c ++程序员,也可以从中学到很多。


7

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

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


1
该教程是100%正确的,但是是否可以对原始海报s machine will be the same depends on the assembly generated by the compiler. As @JonathonReinhart pointed out the call to B()上的结果进行优化。
劳埃德·克劳利

1
我对“那个教程是错误的”字眼有疑问。您实际上去看了教程吗?它并不是要教您如何做这样的疯狂事情,而是要演示调用堆栈的工作原理。在这种情况下,本教程是完全正确的。
Jonathon Reinhart 2013年

@JonathonReinhart我没有看过该教程,以为该示例来自该教程,因此我将删除此部分。
Yu Hao

@LloydCrawley我已经删除了有关教程的部分。我知道这与堆栈体系结构有关,这就是我所说的含义,即它们在打印时位于同一地址5,但显然Jonathon Reinhart的解释要好得多。
Yu Hao

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.

编辑1

作为@Jonathon Reinhart如果您通过使用-O标志禁用优化gcc-O0 则可能会得到输出5。

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

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


编辑2

Deep C幻灯片解释了为什么结果是5 /垃圾。添加这些幻灯片中的信息并进行一些细微的修改,使此答案的有效性降低。

情况1:未经优化

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

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

情况2:经过优化

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

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

垃圾!


1
您在Edit 2,Case 1中的逻辑是完全错误的。这是不是在所有它是如何工作的。局部变量的名称绝对没有任何意义。
Jonathon Reinhart 2013年

@JonathonReinhart正如在回答中提到的,我从deepc幻灯片中添加了此内容,请说明在哪个基础上是不正确的。
Gangadhar

3
堆栈空间和变量名称之间没有任何关联。该示例基于以下事实:从概念上讲,第二个函数调用中的堆栈框架将简单地覆盖第二个函数调用中的堆栈框架。名称无关紧要,只要两个方法签名相同,就可能发生相同的事情。正如其他人指出的那样,如果它在嵌入式系统中并且在对A()和B()的调用之间处理了硬件中断,则堆栈将包含随机值。诸如Code Guard for Borland的旧工具允许在每次调用之前将零写入堆栈。
Dan Haynes 2013年

@DanHaynes您的评论使我信服。就变量类型和函数原型而言,第二个函数调用中的堆栈框架可能会覆盖第一个函数调用中的堆栈框架。是的,我也同意,与变量名称无关。
Gangadhar
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.