协慌网

登录 贡献 社区

可以在其范围之外访问局部变量的内存吗?

我有以下代码。

#include <iostream>

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

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

输出是58

怎么会这样?本地变量的内存不能在其功能之外无法访问吗?

答案

怎么会这样?本地变量的内存不能在其功能之外无法访问吗?

你租了一个酒店房间。你把一本书放在床头柜的顶部抽屉里去睡觉。你第二天早上退房,但 “忘了” 把你的钥匙还给我。你偷了钥匙!

一个星期后,你回到酒店,不要办理登机手续,用偷来的钥匙潜入你的旧房间,然后看看抽屉里。你的书还在那里。惊人!

怎么可能?如果您没有租用房间,是不是酒店房间抽屉的内容无法进入?

好吧,显然这种情况可能发生在现实世界中没问题。当您不再被授权进入房间时,没有神秘的力量会导致您的图书消失。也没有一种神秘的力量阻止你进入一个被盗钥匙的房间。

酒店管理层无需删除您的图书。你没有与他们签订合同,如果你留下东西,他们会为你粉碎它。如果您用偷来的钥匙非法重新进入您的房间以便将其取回,酒店保安人员无需让您偷偷溜进去。您没有与他们签订合同,说 “如果我试图潜入我的房间以后,你需要阻止我。“相反,你和他们签了一份合同,上面写着 “我保证不会再偷回我的房间”,这是你破坏的合同。

在这种情况下, 一切都会发生 。这本书可以在那里 - 你很幸运。别人的书可以在那里,你的可以在酒店的炉子里。当你进来时,有人可能会在你身边,将你的书撕成碎片。酒店可以完全拆除桌子和书本,并用衣柜取代。整个酒店可能即将被拆除,取而代之的是一个足球场,当你潜行时,你会在爆炸中死去。

你不知道会发生什么; 当你退房并偷了钥匙以后非法使用时,你放弃了生活在一个可预测,安全的世界的权利,因为选择违反了系统的规则。

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

UPDATE

圣洁的善良,这个答案得到了很多关注。 (我不确定为什么 - 我认为它只是一个 “有趣” 的小类比,但无论如何。)

我认为通过一些技术性的想法更新这一点可能是密切相关的。

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

第一种是拥有某种 “长寿命” 存储区域,其中存储中每个字节的 “生命周期” - 即与某个程序变量有效关联的时间段 - 无法在前面轻松预测时间编译器生成对 “堆管理器” 的调用,该管理器知道如何在需要时动态分配存储,并在不再需要时回收存储。

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

局部变量遵循后一种模式; 输入方法时,其局部变量变为活动状态。当该方法调用另一个方法时,新方法的局部变量就会生效。在第一个方法的局部变量死亡之前,它们将会死亡。可以提前计算与局部变量相关的存储寿命的开始和结束的相对顺序。

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

这就像酒店决定只按顺序出租房间,在房间号码高于您的所有人都检查出来之前,您不能退房。

所以让我们考虑一下堆栈。在许多操作系统中,每个线程获得一个堆栈,并且堆栈被分配为特定的固定大小。当你调用一个方法时,东西被推入堆栈。如果你然后从你的方法中传回一个指向堆栈的指针,就像原始海报在这里做的那样,那只是一个指向一些完全有效的百万字节内存块中间的指针。在我们的比喻中,您可以退房; 当你这样做时,你刚刚检查出编号最高的房间。如果没有其他人在您之后办理登机手续,并且您非法回到您的房间,那么您所有的东西都将保证在这个特定的酒店仍然存在。

我们将堆栈用于临时商店,因为它们非常便宜且容易。使用堆栈存储本地文件不需要 C ++ 的实现; 它可以使用堆。它没有,因为这会使程序变慢。

不需要实现 C ++ 就可以保持你在堆栈中留下的垃圾不受影响,这样你就可以非法地回来了。编译器生成的代码在您刚刚腾出的 “房间” 中变回零是完全合法的。它不是因为那将是昂贵的。

不需要 C ++ 的实现来确保当堆栈在逻辑上收缩时,过去有效的地址仍然映射到内存中。允许实现告诉操作系统 “我们现在已经完成了使用此页面的堆栈。除非我另有说明,否则发出一个异常,如果有人触及先前有效的堆栈页面则会破坏该进程”。同样,实现实际上并不这样做,因为它很慢且不必要。

相反,实现会让你犯错并逃脱它。大多数时候。直到有一天,真正可怕的事情出现了问题并且这个过程爆炸了。

这是有问题的。有很多规则,很容易意外地打破它们。我当然有很多次。更糟糕的是,这个问题通常只会在腐败发生后检测到内存损坏数十亿纳秒后才会出现,而很难弄清楚是谁弄乱了它。

更多内存安全语言通过限制您的电源来解决此问题。在 “普通”C#中,根本无法获取本地的地址并将其返回或存储以供日后使用。您可以获取本地的地址,但语言设计巧妙,因此在本地生命周期结束后无法使用它。为了获取本地的地址并将其传回,您必须将编译器置于特殊的 “不安全” 模式, 在程序中添加 “不安全” 一词,以引起注意您可能正在做的事实危险的东西,可能违反规则。

进一步阅读:

你在做什么在这里仅仅是读取和写入内存曾经是的地址a 。既然你不在foo之外,它只是指向一些随机存储区的指针。事实上,在您的示例中,该内存区域确实存在,此刻没有其他任何内容正在使用它。你不会因为继续使用它而破坏任何东西,而其他任何东西都没有覆盖它。因此, 5仍然存在。在一个真实的程序中,该内存几乎可以立即重用,你可以通过这样做来破坏某些东西(尽管这些症状可能要到很晚才出现!)

当您从foo返回时,您告诉操作系统您不再使用该内存,并且可以将其重新分配给其他内容。如果你很幸运,它永远不会被重新分配,并且操作系统不会让你再次使用它,那么你就可以逃脱谎言。尽管如此,你最终还是会写完最后的那个地址。

现在,如果你想知道编译器为什么不抱怨,可能是因为优化消除了foo 。它通常会警告你这类事情。 ç 假设你知道你在做什么,虽然,在技术上还没有在这里侵犯范围(有没有提到a本身之外的foo ),只读存储器访问规则,仅触发警告而不是错误。

简而言之:这通常不会起作用,但有时会偶然发生。

因为存储空间还没有被踩到。不要指望那种行为。

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

您租了旅馆房间。您将一本书放在床头柜的顶部抽屉中,然后入睡。您第二天早上退房,但是 “忘记了” 退还您的钥匙。您偷了钥匙!

一周后,您返回酒店,不办理入住手续,用偷来的钥匙偷偷进入旧房间,然后在抽屉里看。你的书还在那里。惊人!

这个怎么可能?如果您没有租房,不是不是无法进入酒店房间抽屉的内容吗?

好吧,很明显,这种情况可以在现实世界中发生,没有问题。当您不再被授权进入房间时,没有任何神秘的力量会使您的书消失。也没有一种神秘的力量阻止您使用失窃的钥匙进入房间。

不需要酒店管理人员删除您的书。您没有与他们签订合同,说如果您留下东西,他们会为您切碎。如果您用偷来的钥匙非法重新进入房间以将其取回,则无需酒店安全人员抓住您潜行。您没有与他们订立合同,说 “如果我尝试潜入我的房间,房间之后,您必须阻止我。” 相反,您与他们签订了一份合同,上面写着 “我保证以后不会再潜入我的房间”,这是您违反的合同。

在这种情况下, 任何事情都可能发生 。这本书可以在那里 - 您很幸运。可能有人的书在那里,而您的书可能在酒店的炉子里。当您进来时,有人可能会在那里,将您的书撕成碎片。该酒店本可以删除桌子并完全预订,然后用衣柜代替。整个酒店可能会被拆毁,取而代之的是一个足球场,当您潜行时,您将在爆炸中丧生。

您不知道会发生什么;当您退出酒店并偷走了以后非法使用的钥匙时,您放弃了生活在可预测的安全世界中的权利,因为选择了违反系统规则。

C ++ 不是安全的语言 。它会很乐意让您打破系统规则。如果您尝试做一些非法和愚蠢的事情,例如回到没有权限的房间,或者翻阅一张可能根本不在的桌子,那么 C ++ 不会阻止您。比 C ++ 更安全的语言通过限制您的力量来解决此问题 - 例如,通过对键进行更严格的控制。

更新

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

我认为用一些其他技术思想来对此进行更新可能是很紧要的。

编译器负责生成代码,该代码管理该程序处理的数据的存储。生成代码来管理内存的方法有很多,但是随着时间的流逝,两种基本技术已经根深蒂固。

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

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

局部变量遵循后一种模式;输入方法后,其局部变量将生效。当该方法调用另一个方法时,新方法的局部变量将生效。在第一个方法的局部变量失效之前,它们将失效。可以提前确定与局部变量关联的存储生命周期的开始和结束的相对顺序。

由于这个原因,局部变量通常作为 “堆栈” 数据结构上的存储生成,因为堆栈具有的属性是,首先要压入的堆栈将是最后弹出的堆栈。

就像酒店决定只按顺序出租房间一样,只有在房间号高于您所选择的每个人之前,您都无法退房。

因此,让我们考虑一下堆栈。在许多操作系统中,每个线程获得一个堆栈,并且该堆栈被分配为一定的固定大小。调用方法时,东西被压入堆栈。如果您然后将指针传递回方法之外的栈,就像原始海报在这里所做的那样,那仅仅是指向某个完全有效的百万字节内存块中间的指针。打个比方,您从酒店退房;当您这样做时,您只是从编号最高的占用房间中退出。如果没有其他人在您之后入住 ,并且您非法返回房间,则可以保证所有物品仍在该特定酒店中

我们将堆栈用于临时存储,因为它们确实便宜又容易。不需要使用 C ++ 实现就可以使用堆栈来存储本地对象;它可以使用堆。事实并非如此,因为那会使程序变慢。

不需要 C ++ 的实现就可以使您留在堆栈上的垃圾保持不变,以便以后可以非法返回它。编译器生成将刚腾出的 “房间” 中的所有内容都归零的代码是完全合法的。并不是因为这又会很昂贵。

不需要 C ++ 的实现来确保在逻辑上缩小堆栈时,曾经有效的地址仍会映射到内存中。该实现被允许告诉操作系统 “我们现在已经完成了该页面的使用。除非我另行声明,否则,如果有人触摸了先前有效的堆栈页面,则发出一个异常,该异常会破坏进程”。再次,实现实际上并没有这样做,因为它很慢且不必要。

取而代之的是,实现使您能够犯错误并摆脱错误。大多数时候。直到有一天,真正可怕的事情出了问题,整个过程爆炸了。

这是有问题的。有很多规则,很容易意外地打破它们。我当然有很多次。更糟糕的是,问题通常只会在发生损坏后数十亿纳秒内检测到内存损坏时才浮出水面,而很难弄清是谁弄乱了内存。

更多的内存安全语言通过限制您的能力来解决此问题。在 “普通” C#中,根本没有办法获取本地地址并将其返回或存储以供以后使用。您可以使用本地地址,但语言设计巧妙,因此在本地生命周期结束后无法使用它。为了获取本地地址并将其传递回去,您必须将编译器置于特殊的 “不安全” 模式, 并将 “不安全” 一词放入程序中,以引起注意以下事实:可能违反规则的危险。

进一步阅读:

您在这里所做的只是读取和写入曾经a地址的内存。现在您不在foo之外,它只是指向某个随机内存区域的指针。碰巧的是,在您的示例中,该内存区域确实存在,并且目前没有其他人在使用它。您不会通过继续使用而破坏任何东西,并且还没有其他东西覆盖它。因此, 5仍然存在。在实际的程序中,该内存将几乎立即被重新使用,并且这样做会破坏某些功能(尽管症状可能要等到很久以后才会出现!)

foo返回时,您告诉操作系统您不再使用该内存,可以将其重新分配给其他内存。如果您很幸运,但是它从未被重新分配,并且操作系统没有抓住您再次使用它的机会,那么您就可以摆脱谎言。尽管您最终可能会写满以该地址结尾的其他内容,但这种机会还是有可能的。

现在,如果您想知道为什么编译器不会抱怨,那可能是因为foo被优化消除了。通常会警告您这种事情。 ç 假设你知道你在做什么,虽然,在技术上还没有在这里侵犯范围(有没有引用a本身之外的foo ),只读存储器访问规则,仅触发警告而不是错误。

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

因为存储空间尚未增加。不要指望这种行为。