代码之家  ›  专栏  ›  技术社区  ›  Stelios Papamichail

奇怪的施法行为[复制]

  •  0
  • Stelios Papamichail  · 技术社区  · 7 年前

    我有以下代码。

    #include <iostream>
    
    int * foo()
    {
        int a = 5;
        return &a;
    }
    
    int main()
    {
        int* p = foo();
        std::cout << *p;
        *p = 8;
        std::cout << *p;
    }
    

    代码只是在运行,没有运行时异常!

    结果是 58

    怎么可能?局部变量的内存不在其函数之外是不可访问的吗?

    0 回复  |  直到 7 年前
        1
  •  4682
  •   TechnicallyTrue Rohit Jain    7 年前

    怎么可能?局部变量的内存不在其函数之外是不可访问的吗?

    你租了个旅馆房间。你把一本书放在床头柜最上面的抽屉里睡觉。你第二天早上退房,但忘了把钥匙还给我。你偷了钥匙!

    一周后,你回到酒店,不办理入住手续,拿着偷来的钥匙偷偷溜进你的旧房间,在抽屉里找。你的书还在那儿。令人吃惊的!

    怎么会这样?如果你还没有租过房间,难道酒店房间抽屉里的东西就不可接近吗?

    很明显,这种情况在现实世界中是可以发生的,没问题。当你不再被授权进入房间时,没有神秘的力量使你的书消失。也没有神秘的力量阻止你带着偷来的钥匙进入房间。

    酒店管理层 必修的 把你的书拿走。你没有和他们签合同说如果你把东西留下,他们会给你撕碎的。如果你非法用偷来的钥匙重新进入房间取回,酒店保安人员不会 必修的 抓住你偷偷溜进来。你没有和他们签订合同,上面写着“如果我以后想溜回我的房间,你必须阻止我。”相反,你和他们签订了一份合同,上面写着“我保证以后不溜回我的房间”,一份合同 你破产了 .

    在这种情况下 任何事情都有可能发生 . 书可以在那儿——你很幸运。别人的书可以在那里,你的书可以在酒店的火炉里。你进来的时候可能有人在那里,把你的书撕成碎片。旅馆本可以把桌子和书全部搬走,换成一个衣柜。整个酒店可能就要被拆掉,换成一个足球场,你会在偷偷摸摸的时候死于爆炸。

    你不知道会发生什么;当你离开酒店,偷了一把钥匙以后非法使用时,你放弃了生活在一个可预测的、安全的世界的权利,因为 选择打破系统规则。

    C++不是一种安全的语言 . 它会让你愉快地打破制度的规则。如果你试图做一些非法的、愚蠢的事情,比如回到房间里,你没有被授权进入并翻找一张甚至不在那里的桌子,C++就不会阻止你。比C++更安全的语言通过限制你的力量来解决这个问题,例如,对键有更严格的控制。

    更新

    天哪,这个答案引起了很多关注。(我不知道为什么——我认为这只是一个“有趣”的小比喻,但无论如何。)

    我想用一些技术性的想法来更新这一点可能是有关系的。

    编译器的任务是生成代码来管理由该程序操作的数据的存储。有很多不同的方法生成代码来管理内存,但随着时间的推移,两种基本技术已经根深蒂固。

    首先是要有某种“长寿命”的存储区,其中存储区中每个字节的“寿命”(即与某个程序变量有效关联的时间段)不容易提前预测。编译器生成对“堆管理器”的调用,该管理器知道如何在需要时动态分配存储,并在不再需要时回收存储。

    第二种方法是有一个“短命”存储区域,其中每个字节的生存期是众所周知的。在这里,生命周期遵循一种“嵌套”模式。这些短期变量中寿命最长的将在任何其他短期变量之前分配,并在最后释放。寿命较短的变量将在寿命最长的变量之后分配,并在它们之前释放。这些寿命较短的变量的生存期是在寿命较长的变量的生存期内嵌套的。

    局部变量遵循后一种模式;当一个方法被输入时,它的局部变量会被激活。当该方法调用另一个方法时,新方法的局部变量将激活。在第一个方法的局部变量死之前,它们就死了。与局部变量相关的存储的生命周期的开始和结束的相对顺序可以提前计算出来。

    由于这个原因,局部变量通常作为“堆栈”数据结构上的存储生成,因为堆栈具有这样的属性:第一个被推到它上面的东西将是最后一个被弹出的东西。

    就像酒店决定只按顺序出租房间一样,除非每个房间号都比您的退房号高,否则您无法退房。

    所以让我们考虑一下堆栈。在许多操作系统中,每个线程都有一个堆栈,并且堆栈被分配为一定的固定大小。当你调用一个方法时,东西被推到堆栈上。如果你然后把一个指向堆栈的指针从你的方法中传出来,就像最初的海报所做的那样,那只是一个指向一些完全有效的百万字节内存块中间的指针。在我们的类比中,你是从酒店退房的;当你退房的时候,你只是从人数最多的房间退房。如果没有其他人在你之后登记入住,而你又非法回到房间,那么你所有的东西肯定还在那里 在这个特别的酒店

    我们使用堆栈作为临时存储,因为它们非常便宜和容易。不需要使用C++来实现本地存储的堆栈;它可以使用堆。不会,因为那样会使程序变慢。

    C++的实现不需要将未被保留的堆栈中的垃圾留下,以便以后可以非法返回;对于编译器来说,生成代码是完全合法的,在您刚刚腾出的“房间”中,所有的代码都会变回零。又不是因为那会很贵。

    不需要实现C++,以确保栈在逻辑上收缩时,仍然有效的地址仍然映射到内存中。实现可以告诉操作系统“我们已经使用完这个堆栈页了。除非我另有说明,否则如果有人触碰先前有效的堆栈页,将发出一个异常来破坏进程”。同样,实现并没有真正做到这一点,因为它是缓慢和不必要的。

    相反,实现会让您犯错误并逃脱惩罚。大多数时候。直到有一天,真正可怕的事情发生了,整个过程爆发了。

    这是有问题的。有很多规则,很容易不小心打破它们。我当然有很多次了。更糟糕的是,当内存在崩溃发生后数十亿纳秒内被检测到被破坏时,这个问题往往才浮出水面,而此时很难找出是谁把它弄糟了。

    更多的内存安全语言通过限制你的能力来解决这个问题。在“普通的”C中,根本无法获取本地地址并将其返回或存储以备以后使用。你可以取一个本地人的地址,但是语言设计得很巧妙,所以在本地人的生命周期结束后不可能使用它。为了获取本地地址并将其传回,必须将编译器置于特殊的“不安全”模式中, 在你的程序中加上“不安全”这个词,提醒大家注意你可能在做一些可能违反规则的危险的事情。

    进一步阅读:

        2
  •  269
  •   chue x    12 年前

    你在这里所做的只是读写记忆 用于 地址是 a . 既然你不在 foo ,它只是指向某个随机内存区域的指针。在您的示例中,内存区域确实存在,而此时没有其他东西在使用它。你不会因为继续使用而破坏任何东西,也没有其他东西覆盖它。因此, 5 还在那里。在一个真正的程序中,这种记忆几乎会立即被重新使用,这样做会破坏某些东西(尽管症状可能要等很久才会出现!)

    当你回来的时候 ,告诉操作系统不再使用该内存,并且可以将其重新分配给其他对象。如果你很幸运,而且它永远不会被重新分配,而且操作系统没有发现你再次使用它,那么你就可以逃脱这个谎言。不过,你很有可能会把其他地址写下来。

    如果你想知道为什么编译器不抱怨,可能是因为 通过优化消除。它通常会警告你这类事情。C假设你知道你在做什么,而且技术上你没有违反这里的范围(没有提到 自身不在 ),只有内存访问规则,它只触发警告而不是错误。

    简而言之:这通常不起作用,但有时是偶然的。

        3
  •  146
  •   msw    16 年前

    因为存储空间还没有被踩踏。别指望那种行为。

        4
  •  78
  •   Michael    15 年前

    在所有答案的基础上增加一点:

    如果你这样做:

    #include<stdio.h>
    #include <stdlib.h>
    int * foo(){
        int a = 5;
        return &a;
    }
    void boo(){
        int a = 7;
    
    }
    int main(){
        int * p = foo();
        boo();
        printf("%d\n",*p);
    }
    

    输出可能是:7

    这是因为在从foo()返回之后,堆栈被释放,然后被boo()重用。 如果对可执行文件进行去汇编,您将清楚地看到它。

        5
  •  67
  •   Peter Mortensen Pieter Jan Bonestroo    15 年前

    在C++中,你 可以 访问任何地址,但这并不意味着你 应该 . 您正在访问的地址不再有效。它 作品 因为foo返回后没有其他东西扰乱内存,但在许多情况下可能会崩溃。尝试使用分析程序 Valgrind ,甚至只是优化编译,然后看…

        6
  •  64
  •   martijnn2008    10 年前

    您从不通过访问无效内存抛出C++异常。您只是给出了引用任意内存位置的一般概念的一个示例。我也可以这样做:

    unsigned int q = 123456;
    
    *(double*)(q) = 1.2;
    

    在这里,我只是把123456当作一个double的地址并给它写信。任何事情都有可能发生:

    1. q 实际上可能是一个有效的double地址,例如。 double p; q = &p; .
    2. Q 可能指向分配内存中的某个位置,而我只是覆盖其中的8个字节。
    3. Q 点在分配的内存之外,操作系统的内存管理器向我的程序发送一个分段错误信号,导致运行时终止它。
    4. 你中了彩票。

    您设置它的方式更合理一点,返回的地址指向内存的有效区域,因为它可能就在堆栈的下面一点,但它仍然是一个无效的位置,您无法以确定的方式访问它。

    在正常程序执行期间,没有人会自动为您检查这样的内存地址的语义有效性。但是,内存调试器如 valgrind 会很高兴这样做,所以你应该运行你的程序通过它,并见证错误。

        7
  •  28
  •   Alec    7 年前

    你在启用乐观主义者的情况下编译程序了吗?这个 foo() 函数非常简单,可能已在结果代码中内联或替换。

    但我同意马克B的观点,即最终的行为是不确定的。

        8
  •  22
  •   Chang Peng    15 年前

    你的问题与此无关 范围 . 在您显示的代码中,函数 main 在函数中看不到名称 foo ,所以您无法访问 a 直接与 外部名称 .

    你遇到的问题是为什么程序在引用非法内存时没有发出错误信号。这是因为C++标准没有规定非法内存和合法内存之间非常明确的界限。引用弹出堆栈中的内容有时会导致错误,有时不会。这要看情况。别指望这种行为。假设在编程时它总是会导致错误,但假设在调试时它永远不会发出错误信号。

        9
  •  17
  •   Brian R. Bondy    16 年前

    您只是返回了一个内存地址,这是允许的,但可能是一个错误。

    是的,如果您试图取消引用该内存地址,您将有未定义的行为。

    int * ref () {
    
     int tmp = 100;
     return &tmp;
    }
    
    int main () {
    
     int * a = ref();
     //Up until this point there is defined results
     //You can even print the address returned
     // but yes probably a bug
    
     cout << *a << endl;//Undefined results
    }
    
        10
  •  16
  •   Adrian Grigore    15 年前

    它之所以起作用,是因为自a被放在那里以来,堆栈还没有被改变。 在访问之前调用一些其他函数(它们也在调用其他函数) a 你可能再也不会这么幸运了…;-)

        11
  •  16
  •   Kerrek SB    15 年前

    那很经典 未定义行为 这是两天前讨论过的——在网站上搜索一下。简而言之,你很幸运,但是任何事情都有可能发生,你的代码对内存的访问无效。

        12
  •  16
  •   AHelps    15 年前

    正如alex指出的,这种行为是未定义的——事实上,大多数编译器都会警告不要这样做,因为这是一种容易导致崩溃的方法。

    比如你的怪异行为 可能的 要获取,请尝试以下示例:

    int *a()
    {
       int x = 5;
       return &x;
    }
    
    void b( int *c )
    {
       int y = 29;
       *c = 123;
       cout << "y=" << y << endl;
    }
    
    int main()
    {
       b( a() );
       return 0;
    }
    

    这会打印出“y=123”,但结果可能会有所不同(真的!)。指针正在删除其他不相关的局部变量。

        13
  •  16
  •   sam    10 年前

    注意所有警告。不要只解决错误。
    GCC显示此警告

    警告:返回局部变量“a”的地址

    这是C++的力量。你应该关心记忆力。与 -Werror 标志,此警告将变为错误,现在必须对其进行调试。

        14
  •  15
  •   Alexander Gessler    15 年前

    您实际上调用了未定义的行为。

    返回临时作品的地址,但由于临时作品在函数结束时被销毁,因此访问它们的结果将是未定义的。

    所以你没有修改 a 而是记忆的位置 曾经是。这种区别与撞车和不撞车的区别非常相似。

        15
  •  13
  •   larsmoa    15 年前

    在典型的编译器实现中,可以将代码看作是“用 以前是 被一个“.另外,如果您将一个新的函数调用添加到一个约束本地 int 很有可能 a (或存储地址 用于指向)更改。这是因为堆栈将被包含不同数据的新帧覆盖。

    然而,这是 未定义 行为和你不应该依赖它来工作!

        16
  •  13
  •   littleadv    15 年前

    可以,因为 a 是在其作用域的生存期内临时分配的变量( foo 功能)。你回来后 内存是空闲的,可以被覆盖。

    你所做的被描述为 未定义的行为 . 结果无法预测。

        17
  •  11
  •   Mykola    15 年前

    正确的东西(?)如果使用::printf而不是cout,控制台输出可能会发生显著变化。 您可以在下面的代码中使用调试器(在x86、32位、msvisual studio上测试):

    char* foo() 
    {
      char buf[10];
      ::strcpy(buf, "TEST”);
      return buf;
    }
    
    int main() 
    {
      char* s = foo();    //place breakpoint & check 's' varialbe here
      ::printf("%s\n", s); 
    }
    
        18
  •  4
  •   Ghulam Moinul Quadir    9 年前

    从函数返回后,所有标识符都会被销毁,而不是保存在内存位置中的值,如果没有标识符,则无法定位这些值,但该位置仍然包含前一个函数存储的值。

    所以,这里是函数 foo() 正在返回的地址 a 在返回地址后被销毁。您可以通过返回的地址访问修改后的值。

    让我举一个现实世界的例子:

    假设一个人把钱藏在一个地方,告诉你地点。过了一段时间,那个告诉你钱的地点的人死了。但你还是可以拿到那笔隐藏的钱。

        19
  •  2
  •   Ayub    9 年前

    这是“肮脏”的使用内存地址的方式。当您返回一个地址(指针)时,您不知道它是否属于函数的本地作用域。只是一个地址。既然您调用了“foo”函数,那么“a”的地址(内存位置)已经在应用程序(进程)的(至少目前是安全的)可寻址内存中分配到了那里。在“foo”函数返回后,“a”的地址可以被认为是“脏的”,但它在那里,没有被清除,也没有被程序其他部分的表达式干扰/修改(至少在这种特定情况下)。一个C/C++编译器不会阻止你这样的“脏”访问(如果你在意的话,也许会警告你)。您可以安全地使用(更新)程序实例(进程)的数据段中的任何内存位置,除非您通过某种方式保护该地址。